diff --git a/cli/package.json b/cli/package.json index 2934375a..1b03e4c3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -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", diff --git a/cli/src/adapters/claude-local/index.ts b/cli/src/adapters/claude-local/index.ts deleted file mode 100644 index 9ea34c83..00000000 --- a/cli/src/adapters/claude-local/index.ts +++ /dev/null @@ -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, -}; diff --git a/cli/src/adapters/codex-local/index.ts b/cli/src/adapters/codex-local/index.ts deleted file mode 100644 index 68dc19b2..00000000 --- a/cli/src/adapters/codex-local/index.ts +++ /dev/null @@ -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, -}; diff --git a/cli/src/adapters/http/index.ts b/cli/src/adapters/http/index.ts index 0e684dc6..f4b48cdc 100644 --- a/cli/src/adapters/http/index.ts +++ b/cli/src/adapters/http/index.ts @@ -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 = { diff --git a/cli/src/adapters/index.ts b/cli/src/adapters/index.ts index d8d30aba..20fd16db 100644 --- a/cli/src/adapters/index.ts +++ b/cli/src/adapters/index.ts @@ -1,2 +1,2 @@ export { getCLIAdapter } from "./registry.js"; -export type { CLIAdapterModule } from "./types.js"; +export type { CLIAdapterModule } from "@paperclip/adapter-utils"; diff --git a/cli/src/adapters/process/index.ts b/cli/src/adapters/process/index.ts index ff76ef47..be521b12 100644 --- a/cli/src/adapters/process/index.ts +++ b/cli/src/adapters/process/index.ts @@ -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 = { diff --git a/cli/src/adapters/registry.ts b/cli/src/adapters/registry.ts index 0c9ae367..461a6e2d 100644 --- a/cli/src/adapters/registry.ts +++ b/cli/src/adapters/registry.ts @@ -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( [claudeLocalCLIAdapter, codexLocalCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]), ); diff --git a/cli/src/adapters/types.ts b/cli/src/adapters/types.ts deleted file mode 100644 index 00a69cb7..00000000 --- a/cli/src/adapters/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CLIAdapterModule { - type: string; - formatStdoutEvent: (line: string, debug: boolean) => void; -} diff --git a/packages/adapter-utils/package.json b/packages/adapter-utils/package.json new file mode 100644 index 00000000..47ed6525 --- /dev/null +++ b/packages/adapter-utils/package.json @@ -0,0 +1,16 @@ +{ + "name": "@paperclip/adapter-utils", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/packages/adapter-utils/src/index.ts b/packages/adapter-utils/src/index.ts new file mode 100644 index 00000000..6869c1ea --- /dev/null +++ b/packages/adapter-utils/src/index.ts @@ -0,0 +1,13 @@ +export type { + AdapterAgent, + AdapterRuntime, + UsageSummary, + AdapterExecutionResult, + AdapterInvocationMeta, + AdapterExecutionContext, + ServerAdapterModule, + TranscriptEntry, + StdoutLineParser, + CLIAdapterModule, + CreateConfigValues, +} from "./types.js"; diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts new file mode 100644 index 00000000..fe47b99a --- /dev/null +++ b/packages/adapter-utils/src/server-utils.ts @@ -0,0 +1,250 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { constants as fsConstants, promises as fs } from "node:fs"; +import path from "node:path"; + +export interface RunProcessResult { + exitCode: number | null; + signal: string | null; + timedOut: boolean; + stdout: string; + stderr: string; +} + +interface RunningProcess { + child: ChildProcess; + graceSec: number; +} + +export const runningProcesses = new Map(); +export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024; +export const MAX_EXCERPT_BYTES = 32 * 1024; +const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i; + +export function parseObject(value: unknown): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + return {}; + } + return value as Record; +} + +export function asString(value: unknown, fallback: string): string { + return typeof value === "string" && value.length > 0 ? value : fallback; +} + +export function asNumber(value: unknown, fallback: number): number { + return typeof value === "number" && Number.isFinite(value) ? value : fallback; +} + +export function asBoolean(value: unknown, fallback: boolean): boolean { + return typeof value === "boolean" ? value : fallback; +} + +export function asStringArray(value: unknown): string[] { + return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; +} + +export function parseJson(value: string): Record | null { + try { + return JSON.parse(value) as Record; + } catch { + return null; + } +} + +export function appendWithCap(prev: string, chunk: string, cap = MAX_CAPTURE_BYTES) { + const combined = prev + chunk; + return combined.length > cap ? combined.slice(combined.length - cap) : combined; +} + +export function resolvePathValue(obj: Record, dottedPath: string) { + const parts = dottedPath.split("."); + let cursor: unknown = obj; + + for (const part of parts) { + if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) { + return ""; + } + cursor = (cursor as Record)[part]; + } + + if (cursor === null || cursor === undefined) return ""; + if (typeof cursor === "string") return cursor; + if (typeof cursor === "number" || typeof cursor === "boolean") return String(cursor); + + try { + return JSON.stringify(cursor); + } catch { + return ""; + } +} + +export function renderTemplate(template: string, data: Record) { + return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path)); +} + +export function redactEnvForLogs(env: Record): Record { + const redacted: Record = {}; + for (const [key, value] of Object.entries(env)) { + redacted[key] = SENSITIVE_ENV_KEY.test(key) ? "***REDACTED***" : value; + } + return redacted; +} + +export function buildPaperclipEnv(agent: { id: string; companyId: string }): Record { + const vars: Record = { + PAPERCLIP_AGENT_ID: agent.id, + PAPERCLIP_COMPANY_ID: agent.companyId, + }; + const apiUrl = process.env.PAPERCLIP_API_URL ?? `http://localhost:${process.env.PORT ?? 3100}`; + vars.PAPERCLIP_API_URL = apiUrl; + return vars; +} + +export function defaultPathForPlatform() { + if (process.platform === "win32") { + return "C:\\Windows\\System32;C:\\Windows;C:\\Windows\\System32\\Wbem"; + } + return "/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"; +} + +export function ensurePathInEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + if (typeof env.PATH === "string" && env.PATH.length > 0) return env; + if (typeof env.Path === "string" && env.Path.length > 0) return env; + return { ...env, PATH: defaultPathForPlatform() }; +} + +export async function ensureAbsoluteDirectory(cwd: string) { + if (!path.isAbsolute(cwd)) { + throw new Error(`Working directory must be an absolute path: "${cwd}"`); + } + + let stats; + try { + stats = await fs.stat(cwd); + } catch { + throw new Error(`Working directory does not exist: "${cwd}"`); + } + + if (!stats.isDirectory()) { + throw new Error(`Working directory is not a directory: "${cwd}"`); + } +} + +export async function ensureCommandResolvable(command: string, cwd: string, env: NodeJS.ProcessEnv) { + const hasPathSeparator = command.includes("/") || command.includes("\\"); + if (hasPathSeparator) { + const absolute = path.isAbsolute(command) ? command : path.resolve(cwd, command); + try { + await fs.access(absolute, fsConstants.X_OK); + } catch { + throw new Error(`Command is not executable: "${command}" (resolved: "${absolute}")`); + } + return; + } + + const pathValue = env.PATH ?? env.Path ?? ""; + const delimiter = process.platform === "win32" ? ";" : ":"; + const dirs = pathValue.split(delimiter).filter(Boolean); + const windowsExt = process.platform === "win32" + ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") + : [""]; + + for (const dir of dirs) { + for (const ext of windowsExt) { + const candidate = path.join(dir, process.platform === "win32" ? `${command}${ext}` : command); + try { + await fs.access(candidate, fsConstants.X_OK); + return; + } catch { + // continue scanning PATH + } + } + } + + throw new Error(`Command not found in PATH: "${command}"`); +} + +export async function runChildProcess( + runId: string, + command: string, + args: string[], + opts: { + cwd: string; + env: Record; + timeoutSec: number; + graceSec: number; + onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; + onLogError?: (err: unknown, runId: string, message: string) => void; + }, +): Promise { + const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg)); + + return new Promise((resolve, reject) => { + const mergedEnv = ensurePathInEnv({ ...process.env, ...opts.env }); + const child = spawn(command, args, { + cwd: opts.cwd, + env: mergedEnv, + shell: false, + stdio: ["ignore", "pipe", "pipe"], + }); + + runningProcesses.set(runId, { child, graceSec: opts.graceSec }); + + let timedOut = false; + let stdout = ""; + let stderr = ""; + let logChain: Promise = Promise.resolve(); + + const timeout = setTimeout(() => { + timedOut = true; + child.kill("SIGTERM"); + setTimeout(() => { + if (!child.killed) { + child.kill("SIGKILL"); + } + }, Math.max(1, opts.graceSec) * 1000); + }, Math.max(1, opts.timeoutSec) * 1000); + + child.stdout?.on("data", (chunk) => { + const text = String(chunk); + stdout = appendWithCap(stdout, text); + logChain = logChain + .then(() => opts.onLog("stdout", text)) + .catch((err) => onLogError(err, runId, "failed to append stdout log chunk")); + }); + + child.stderr?.on("data", (chunk) => { + const text = String(chunk); + stderr = appendWithCap(stderr, text); + logChain = logChain + .then(() => opts.onLog("stderr", text)) + .catch((err) => onLogError(err, runId, "failed to append stderr log chunk")); + }); + + child.on("error", (err) => { + clearTimeout(timeout); + runningProcesses.delete(runId); + const errno = (err as NodeJS.ErrnoException).code; + const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? ""; + const msg = + errno === "ENOENT" + ? `Failed to start command "${command}" in "${opts.cwd}". Verify adapter command, working directory, and PATH (${pathValue}).` + : `Failed to start command "${command}" in "${opts.cwd}": ${err.message}`; + reject(new Error(msg)); + }); + + child.on("close", (code, signal) => { + clearTimeout(timeout); + runningProcesses.delete(runId); + void logChain.finally(() => { + resolve({ + exitCode: code, + signal, + timedOut, + stdout, + stderr, + }); + }); + }); + }); +} diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts new file mode 100644 index 00000000..c5bf7895 --- /dev/null +++ b/packages/adapter-utils/src/types.ts @@ -0,0 +1,113 @@ +// --------------------------------------------------------------------------- +// Minimal adapter-facing interfaces (no drizzle dependency) +// --------------------------------------------------------------------------- + +export interface AdapterAgent { + id: string; + companyId: string; + name: string; + adapterType: string | null; + adapterConfig: unknown; +} + +export interface AdapterRuntime { + sessionId: string | null; +} + +// --------------------------------------------------------------------------- +// Execution types (moved from server/src/adapters/types.ts) +// --------------------------------------------------------------------------- + +export interface UsageSummary { + inputTokens: number; + outputTokens: number; + cachedInputTokens?: number; +} + +export interface AdapterExecutionResult { + exitCode: number | null; + signal: string | null; + timedOut: boolean; + errorMessage?: string | null; + usage?: UsageSummary; + sessionId?: string | null; + provider?: string | null; + model?: string | null; + costUsd?: number | null; + resultJson?: Record | null; + summary?: string | null; + clearSession?: boolean; +} + +export interface AdapterInvocationMeta { + adapterType: string; + command: string; + cwd?: string; + commandArgs?: string[]; + env?: Record; + prompt?: string; + context?: Record; +} + +export interface AdapterExecutionContext { + runId: string; + agent: AdapterAgent; + runtime: AdapterRuntime; + config: Record; + context: Record; + onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; + onMeta?: (meta: AdapterInvocationMeta) => Promise; +} + +export interface ServerAdapterModule { + type: string; + execute(ctx: AdapterExecutionContext): Promise; + models?: { id: string; label: string }[]; +} + +// --------------------------------------------------------------------------- +// UI types (moved from ui/src/adapters/types.ts) +// --------------------------------------------------------------------------- + +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[]; + +// --------------------------------------------------------------------------- +// CLI types (moved from cli/src/adapters/types.ts) +// --------------------------------------------------------------------------- + +export interface CLIAdapterModule { + type: string; + formatStdoutEvent: (line: string, debug: boolean) => void; +} + +// --------------------------------------------------------------------------- +// UI config form values (moved from ui/src/components/AgentConfigForm.tsx) +// --------------------------------------------------------------------------- + +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; +} diff --git a/packages/adapter-utils/tsconfig.json b/packages/adapter-utils/tsconfig.json new file mode 100644 index 00000000..a086b149 --- /dev/null +++ b/packages/adapter-utils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/adapters/claude-local/package.json b/packages/adapters/claude-local/package.json new file mode 100644 index 00000000..badddd2b --- /dev/null +++ b/packages/adapters/claude-local/package.json @@ -0,0 +1,22 @@ +{ + "name": "@paperclip/adapter-claude-local", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./server": "./src/server/index.ts", + "./ui": "./src/ui/index.ts", + "./cli": "./src/cli/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@paperclip/adapter-utils": "workspace:*", + "picocolors": "^1.1.1" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/cli/src/adapters/claude-local/format-event.ts b/packages/adapters/claude-local/src/cli/format-event.ts similarity index 100% rename from cli/src/adapters/claude-local/format-event.ts rename to packages/adapters/claude-local/src/cli/format-event.ts diff --git a/packages/adapters/claude-local/src/cli/index.ts b/packages/adapters/claude-local/src/cli/index.ts new file mode 100644 index 00000000..7c81ab6f --- /dev/null +++ b/packages/adapters/claude-local/src/cli/index.ts @@ -0,0 +1 @@ +export { printClaudeStreamEvent } from "./format-event.js"; diff --git a/packages/adapters/claude-local/src/index.ts b/packages/adapters/claude-local/src/index.ts new file mode 100644 index 00000000..802e4f54 --- /dev/null +++ b/packages/adapters/claude-local/src/index.ts @@ -0,0 +1,8 @@ +export const type = "claude_local"; +export const label = "Claude Code (local)"; + +export const models = [ + { id: "claude-opus-4-6", label: "Claude Opus 4.6" }, + { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" }, + { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }, +]; diff --git a/server/src/adapters/claude-local/execute.ts b/packages/adapters/claude-local/src/server/execute.ts similarity index 97% rename from server/src/adapters/claude-local/execute.ts rename to packages/adapters/claude-local/src/server/execute.ts index 7afee0d5..592052b2 100644 --- a/server/src/adapters/claude-local/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -1,5 +1,5 @@ -import type { AdapterExecutionContext, AdapterExecutionResult } from "../types.js"; -import type { RunProcessResult } from "../utils.js"; +import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils"; +import type { RunProcessResult } from "@paperclip/adapter-utils/server-utils"; import { asString, asNumber, @@ -14,7 +14,7 @@ import { ensurePathInEnv, renderTemplate, runChildProcess, -} from "../utils.js"; +} from "@paperclip/adapter-utils/server-utils"; import { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js"; export async function execute(ctx: AdapterExecutionContext): Promise { diff --git a/packages/adapters/claude-local/src/server/index.ts b/packages/adapters/claude-local/src/server/index.ts new file mode 100644 index 00000000..bf3566b5 --- /dev/null +++ b/packages/adapters/claude-local/src/server/index.ts @@ -0,0 +1,2 @@ +export { execute } from "./execute.js"; +export { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js"; diff --git a/server/src/adapters/claude-local/parse.ts b/packages/adapters/claude-local/src/server/parse.ts similarity index 96% rename from server/src/adapters/claude-local/parse.ts rename to packages/adapters/claude-local/src/server/parse.ts index ab498837..f9eed7ff 100644 --- a/server/src/adapters/claude-local/parse.ts +++ b/packages/adapters/claude-local/src/server/parse.ts @@ -1,5 +1,5 @@ -import type { UsageSummary } from "../types.js"; -import { asString, asNumber, parseObject, parseJson } from "../utils.js"; +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; diff --git a/ui/src/adapters/claude-local/build-config.ts b/packages/adapters/claude-local/src/ui/build-config.ts similarity index 94% rename from ui/src/adapters/claude-local/build-config.ts rename to packages/adapters/claude-local/src/ui/build-config.ts index 999b114b..ca5bf199 100644 --- a/ui/src/adapters/claude-local/build-config.ts +++ b/packages/adapters/claude-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "../../components/AgentConfigForm"; +import type { CreateConfigValues } from "@paperclip/adapter-utils"; function parseCommaArgs(value: string): string[] { return value diff --git a/packages/adapters/claude-local/src/ui/index.ts b/packages/adapters/claude-local/src/ui/index.ts new file mode 100644 index 00000000..a7939af3 --- /dev/null +++ b/packages/adapters/claude-local/src/ui/index.ts @@ -0,0 +1,2 @@ +export { parseClaudeStdoutLine } from "./parse-stdout.js"; +export { buildClaudeLocalConfig } from "./build-config.js"; diff --git a/ui/src/adapters/claude-local/parse-stdout.ts b/packages/adapters/claude-local/src/ui/parse-stdout.ts similarity index 97% rename from ui/src/adapters/claude-local/parse-stdout.ts rename to packages/adapters/claude-local/src/ui/parse-stdout.ts index 5d6d4ad3..15ee761f 100644 --- a/ui/src/adapters/claude-local/parse-stdout.ts +++ b/packages/adapters/claude-local/src/ui/parse-stdout.ts @@ -1,4 +1,4 @@ -import type { TranscriptEntry } from "../types"; +import type { TranscriptEntry } from "@paperclip/adapter-utils"; function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; diff --git a/packages/adapters/claude-local/tsconfig.json b/packages/adapters/claude-local/tsconfig.json new file mode 100644 index 00000000..2f355cfe --- /dev/null +++ b/packages/adapters/claude-local/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/adapters/codex-local/package.json b/packages/adapters/codex-local/package.json new file mode 100644 index 00000000..0dacb1ad --- /dev/null +++ b/packages/adapters/codex-local/package.json @@ -0,0 +1,22 @@ +{ + "name": "@paperclip/adapter-codex-local", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./server": "./src/server/index.ts", + "./ui": "./src/ui/index.ts", + "./cli": "./src/cli/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@paperclip/adapter-utils": "workspace:*", + "picocolors": "^1.1.1" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/cli/src/adapters/codex-local/format-event.ts b/packages/adapters/codex-local/src/cli/format-event.ts similarity index 100% rename from cli/src/adapters/codex-local/format-event.ts rename to packages/adapters/codex-local/src/cli/format-event.ts diff --git a/packages/adapters/codex-local/src/cli/index.ts b/packages/adapters/codex-local/src/cli/index.ts new file mode 100644 index 00000000..22fec464 --- /dev/null +++ b/packages/adapters/codex-local/src/cli/index.ts @@ -0,0 +1 @@ +export { printCodexStreamEvent } from "./format-event.js"; diff --git a/packages/adapters/codex-local/src/index.ts b/packages/adapters/codex-local/src/index.ts new file mode 100644 index 00000000..4538896b --- /dev/null +++ b/packages/adapters/codex-local/src/index.ts @@ -0,0 +1,8 @@ +export const type = "codex_local"; +export const label = "Codex (local)"; + +export const models = [ + { id: "o4-mini", label: "o4-mini" }, + { id: "o3", label: "o3" }, + { id: "codex-mini-latest", label: "Codex Mini" }, +]; diff --git a/server/src/adapters/codex-local/execute.ts b/packages/adapters/codex-local/src/server/execute.ts similarity index 97% rename from server/src/adapters/codex-local/execute.ts rename to packages/adapters/codex-local/src/server/execute.ts index 46286f25..75ef0815 100644 --- a/server/src/adapters/codex-local/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -1,4 +1,4 @@ -import type { AdapterExecutionContext, AdapterExecutionResult } from "../types.js"; +import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils"; import { asString, asNumber, @@ -12,7 +12,7 @@ import { ensurePathInEnv, renderTemplate, runChildProcess, -} from "../utils.js"; +} from "@paperclip/adapter-utils/server-utils"; import { parseCodexJsonl } from "./parse.js"; export async function execute(ctx: AdapterExecutionContext): Promise { diff --git a/packages/adapters/codex-local/src/server/index.ts b/packages/adapters/codex-local/src/server/index.ts new file mode 100644 index 00000000..32b56e7c --- /dev/null +++ b/packages/adapters/codex-local/src/server/index.ts @@ -0,0 +1,2 @@ +export { execute } from "./execute.js"; +export { parseCodexJsonl } from "./parse.js"; diff --git a/server/src/adapters/codex-local/parse.ts b/packages/adapters/codex-local/src/server/parse.ts similarity index 92% rename from server/src/adapters/codex-local/parse.ts rename to packages/adapters/codex-local/src/server/parse.ts index 3e232a29..0d2b6dbb 100644 --- a/server/src/adapters/codex-local/parse.ts +++ b/packages/adapters/codex-local/src/server/parse.ts @@ -1,4 +1,4 @@ -import { asString, asNumber, parseObject, parseJson } from "../utils.js"; +import { asString, asNumber, parseObject, parseJson } from "@paperclip/adapter-utils/server-utils"; export function parseCodexJsonl(stdout: string) { let sessionId: string | null = null; diff --git a/ui/src/adapters/codex-local/build-config.ts b/packages/adapters/codex-local/src/ui/build-config.ts similarity index 94% rename from ui/src/adapters/codex-local/build-config.ts rename to packages/adapters/codex-local/src/ui/build-config.ts index 45625169..fbbdb7f6 100644 --- a/ui/src/adapters/codex-local/build-config.ts +++ b/packages/adapters/codex-local/src/ui/build-config.ts @@ -1,4 +1,4 @@ -import type { CreateConfigValues } from "../../components/AgentConfigForm"; +import type { CreateConfigValues } from "@paperclip/adapter-utils"; function parseCommaArgs(value: string): string[] { return value diff --git a/packages/adapters/codex-local/src/ui/index.ts b/packages/adapters/codex-local/src/ui/index.ts new file mode 100644 index 00000000..ba222502 --- /dev/null +++ b/packages/adapters/codex-local/src/ui/index.ts @@ -0,0 +1,2 @@ +export { parseCodexStdoutLine } from "./parse-stdout.js"; +export { buildCodexLocalConfig } from "./build-config.js"; diff --git a/ui/src/adapters/codex-local/parse-stdout.ts b/packages/adapters/codex-local/src/ui/parse-stdout.ts similarity index 96% rename from ui/src/adapters/codex-local/parse-stdout.ts rename to packages/adapters/codex-local/src/ui/parse-stdout.ts index 721be0f1..93537f07 100644 --- a/ui/src/adapters/codex-local/parse-stdout.ts +++ b/packages/adapters/codex-local/src/ui/parse-stdout.ts @@ -1,4 +1,4 @@ -import type { TranscriptEntry } from "../types"; +import type { TranscriptEntry } from "@paperclip/adapter-utils"; function safeJsonParse(text: string): unknown { try { diff --git a/packages/adapters/codex-local/tsconfig.json b/packages/adapters/codex-local/tsconfig.json new file mode 100644 index 00000000..2f355cfe --- /dev/null +++ b/packages/adapters/codex-local/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9618258..a95a6e22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,15 @@ importers: '@clack/prompts': specifier: ^0.10.0 version: 0.10.1 + '@paperclip/adapter-claude-local': + specifier: workspace:* + version: link:../packages/adapters/claude-local + '@paperclip/adapter-codex-local': + specifier: workspace:* + version: link:../packages/adapters/codex-local + '@paperclip/adapter-utils': + specifier: workspace:* + version: link:../packages/adapter-utils '@paperclip/db': specifier: workspace:* version: link:../packages/db @@ -43,6 +52,38 @@ importers: specifier: ^5.7.3 version: 5.9.3 + packages/adapter-utils: + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/adapters/claude-local: + dependencies: + '@paperclip/adapter-utils': + specifier: workspace:* + version: link:../../adapter-utils + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/adapters/codex-local: + dependencies: + '@paperclip/adapter-utils': + specifier: workspace:* + version: link:../../adapter-utils + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages/db: dependencies: '@paperclip/shared': @@ -80,6 +121,15 @@ importers: server: dependencies: + '@paperclip/adapter-claude-local': + specifier: workspace:* + version: link:../packages/adapters/claude-local + '@paperclip/adapter-codex-local': + specifier: workspace:* + version: link:../packages/adapters/codex-local + '@paperclip/adapter-utils': + specifier: workspace:* + version: link:../packages/adapter-utils '@paperclip/db': specifier: workspace:* version: link:../packages/db @@ -139,6 +189,15 @@ importers: ui: dependencies: + '@paperclip/adapter-claude-local': + specifier: workspace:* + version: link:../packages/adapters/claude-local + '@paperclip/adapter-codex-local': + specifier: workspace:* + version: link:../packages/adapters/codex-local + '@paperclip/adapter-utils': + specifier: workspace:* + version: link:../packages/adapter-utils '@paperclip/shared': specifier: workspace:* version: link:../packages/shared diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 411fe263..014e1412 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - packages/* + - packages/adapters/* - server - ui - cli diff --git a/server/package.json b/server/package.json index a759ed63..d43f4797 100644 --- a/server/package.json +++ b/server/package.json @@ -10,6 +10,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@paperclip/adapter-claude-local": "workspace:*", + "@paperclip/adapter-codex-local": "workspace:*", + "@paperclip/adapter-utils": "workspace:*", "@paperclip/db": "workspace:*", "@paperclip/shared": "workspace:*", "detect-port": "^2.1.0", diff --git a/server/src/adapters/claude-local/index.ts b/server/src/adapters/claude-local/index.ts deleted file mode 100644 index f5a50b80..00000000 --- a/server/src/adapters/claude-local/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ServerAdapterModule } from "../types.js"; -import { execute } from "./execute.js"; - -export const claudeLocalAdapter: ServerAdapterModule = { - type: "claude_local", - execute, - models: [ - { id: "claude-opus-4-6", label: "Claude Opus 4.6" }, - { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5" }, - { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" }, - ], -}; diff --git a/server/src/adapters/codex-local/index.ts b/server/src/adapters/codex-local/index.ts deleted file mode 100644 index 3375432f..00000000 --- a/server/src/adapters/codex-local/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ServerAdapterModule } from "../types.js"; -import { execute } from "./execute.js"; - -export const codexLocalAdapter: ServerAdapterModule = { - type: "codex_local", - execute, - models: [ - { id: "o4-mini", label: "o4-mini" }, - { id: "o3", label: "o3" }, - { id: "codex-mini-latest", label: "Codex Mini" }, - ], -}; diff --git a/server/src/adapters/index.ts b/server/src/adapters/index.ts index b314cbb0..6b973404 100644 --- a/server/src/adapters/index.ts +++ b/server/src/adapters/index.ts @@ -5,7 +5,7 @@ export type { AdapterExecutionResult, AdapterInvocationMeta, UsageSummary, - AgentRecord, - AgentRuntimeStateRecord, -} from "./types.js"; + AdapterAgent, + AdapterRuntime, +} from "@paperclip/adapter-utils"; export { runningProcesses } from "./utils.js"; diff --git a/server/src/adapters/registry.ts b/server/src/adapters/registry.ts index 1ff84357..585cb37b 100644 --- a/server/src/adapters/registry.ts +++ b/server/src/adapters/registry.ts @@ -1,9 +1,23 @@ import type { ServerAdapterModule } from "./types.js"; -import { claudeLocalAdapter } from "./claude-local/index.js"; -import { codexLocalAdapter } from "./codex-local/index.js"; +import { execute as claudeExecute } from "@paperclip/adapter-claude-local/server"; +import { models as claudeModels } from "@paperclip/adapter-claude-local"; +import { execute as codexExecute } from "@paperclip/adapter-codex-local/server"; +import { models as codexModels } from "@paperclip/adapter-codex-local"; import { processAdapter } from "./process/index.js"; import { httpAdapter } from "./http/index.js"; +const claudeLocalAdapter: ServerAdapterModule = { + type: "claude_local", + execute: claudeExecute, + models: claudeModels, +}; + +const codexLocalAdapter: ServerAdapterModule = { + type: "codex_local", + execute: codexExecute, + models: codexModels, +}; + const adaptersByType = new Map( [claudeLocalAdapter, codexLocalAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]), ); diff --git a/server/src/adapters/types.ts b/server/src/adapters/types.ts index 906e2977..3cd72846 100644 --- a/server/src/adapters/types.ts +++ b/server/src/adapters/types.ts @@ -1,51 +1,12 @@ -import type { agents, agentRuntimeState } from "@paperclip/db"; - -export interface UsageSummary { - inputTokens: number; - outputTokens: number; - cachedInputTokens?: number; -} - -export interface AdapterExecutionResult { - exitCode: number | null; - signal: string | null; - timedOut: boolean; - errorMessage?: string | null; - usage?: UsageSummary; - sessionId?: string | null; - provider?: string | null; - model?: string | null; - costUsd?: number | null; - resultJson?: Record | null; - summary?: string | null; - clearSession?: boolean; -} - -export interface AdapterInvocationMeta { - adapterType: string; - command: string; - cwd?: string; - commandArgs?: string[]; - env?: Record; - prompt?: string; - context?: Record; -} - -export type AgentRecord = typeof agents.$inferSelect; -export type AgentRuntimeStateRecord = typeof agentRuntimeState.$inferSelect; - -export interface AdapterExecutionContext { - runId: string; - agent: AgentRecord; - runtime: AgentRuntimeStateRecord; - config: Record; - context: Record; - onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; - onMeta?: (meta: AdapterInvocationMeta) => Promise; -} - -export interface ServerAdapterModule { - type: string; - execute(ctx: AdapterExecutionContext): Promise; - models?: { id: string; label: string }[]; -} +// Re-export all types from the shared adapter-utils package. +// This file is kept as a convenience shim so existing in-tree +// imports (process/, http/, heartbeat.ts) don't need rewriting. +export type { + AdapterAgent, + AdapterRuntime, + UsageSummary, + AdapterExecutionResult, + AdapterInvocationMeta, + AdapterExecutionContext, + ServerAdapterModule, +} from "@paperclip/adapter-utils"; diff --git a/server/src/adapters/utils.ts b/server/src/adapters/utils.ts index 7c34029d..da1090f5 100644 --- a/server/src/adapters/utils.ts +++ b/server/src/adapters/utils.ts @@ -1,169 +1,32 @@ -import { spawn, type ChildProcess } from "node:child_process"; -import { constants as fsConstants, promises as fs } from "node:fs"; -import path from "node:path"; +// Re-export everything from the shared adapter-utils/server-utils package. +// This file is kept as a convenience shim so existing in-tree +// imports (process/, http/, heartbeat.ts) don't need rewriting. import { logger } from "../middleware/logger.js"; +export { + type RunProcessResult, + runningProcesses, + MAX_CAPTURE_BYTES, + MAX_EXCERPT_BYTES, + parseObject, + asString, + asNumber, + asBoolean, + asStringArray, + parseJson, + appendWithCap, + resolvePathValue, + renderTemplate, + redactEnvForLogs, + buildPaperclipEnv, + defaultPathForPlatform, + ensurePathInEnv, + ensureAbsoluteDirectory, + ensureCommandResolvable, +} from "@paperclip/adapter-utils/server-utils"; -export interface RunProcessResult { - exitCode: number | null; - signal: string | null; - timedOut: boolean; - stdout: string; - stderr: string; -} - -interface RunningProcess { - child: ChildProcess; - graceSec: number; -} - -export const runningProcesses = new Map(); -export const MAX_CAPTURE_BYTES = 4 * 1024 * 1024; -export const MAX_EXCERPT_BYTES = 32 * 1024; -const SENSITIVE_ENV_KEY = /(key|token|secret|password|passwd|authorization|cookie)/i; - -export function parseObject(value: unknown): Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) { - return {}; - } - return value as Record; -} - -export function asString(value: unknown, fallback: string): string { - return typeof value === "string" && value.length > 0 ? value : fallback; -} - -export function asNumber(value: unknown, fallback: number): number { - return typeof value === "number" && Number.isFinite(value) ? value : fallback; -} - -export function asBoolean(value: unknown, fallback: boolean): boolean { - return typeof value === "boolean" ? value : fallback; -} - -export function asStringArray(value: unknown): string[] { - return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; -} - -export function parseJson(value: string): Record | null { - try { - return JSON.parse(value) as Record; - } catch { - return null; - } -} - -export function appendWithCap(prev: string, chunk: string, cap = MAX_CAPTURE_BYTES) { - const combined = prev + chunk; - return combined.length > cap ? combined.slice(combined.length - cap) : combined; -} - -export function resolvePathValue(obj: Record, dottedPath: string) { - const parts = dottedPath.split("."); - let cursor: unknown = obj; - - for (const part of parts) { - if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) { - return ""; - } - cursor = (cursor as Record)[part]; - } - - if (cursor === null || cursor === undefined) return ""; - if (typeof cursor === "string") return cursor; - if (typeof cursor === "number" || typeof cursor === "boolean") return String(cursor); - - try { - return JSON.stringify(cursor); - } catch { - return ""; - } -} - -export function renderTemplate(template: string, data: Record) { - return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path)); -} - -export function redactEnvForLogs(env: Record): Record { - const redacted: Record = {}; - for (const [key, value] of Object.entries(env)) { - redacted[key] = SENSITIVE_ENV_KEY.test(key) ? "***REDACTED***" : value; - } - return redacted; -} - -export function buildPaperclipEnv(agent: { id: string; companyId: string }): Record { - const vars: Record = { - PAPERCLIP_AGENT_ID: agent.id, - PAPERCLIP_COMPANY_ID: agent.companyId, - }; - const apiUrl = process.env.PAPERCLIP_API_URL ?? `http://localhost:${process.env.PORT ?? 3100}`; - vars.PAPERCLIP_API_URL = apiUrl; - return vars; -} - -export function defaultPathForPlatform() { - if (process.platform === "win32") { - return "C:\\Windows\\System32;C:\\Windows;C:\\Windows\\System32\\Wbem"; - } - return "/usr/local/bin:/opt/homebrew/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin"; -} - -export function ensurePathInEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { - if (typeof env.PATH === "string" && env.PATH.length > 0) return env; - if (typeof env.Path === "string" && env.Path.length > 0) return env; - return { ...env, PATH: defaultPathForPlatform() }; -} - -export async function ensureAbsoluteDirectory(cwd: string) { - if (!path.isAbsolute(cwd)) { - throw new Error(`Working directory must be an absolute path: "${cwd}"`); - } - - let stats; - try { - stats = await fs.stat(cwd); - } catch { - throw new Error(`Working directory does not exist: "${cwd}"`); - } - - if (!stats.isDirectory()) { - throw new Error(`Working directory is not a directory: "${cwd}"`); - } -} - -export async function ensureCommandResolvable(command: string, cwd: string, env: NodeJS.ProcessEnv) { - const hasPathSeparator = command.includes("/") || command.includes("\\"); - if (hasPathSeparator) { - const absolute = path.isAbsolute(command) ? command : path.resolve(cwd, command); - try { - await fs.access(absolute, fsConstants.X_OK); - } catch { - throw new Error(`Command is not executable: "${command}" (resolved: "${absolute}")`); - } - return; - } - - const pathValue = env.PATH ?? env.Path ?? ""; - const delimiter = process.platform === "win32" ? ";" : ":"; - const dirs = pathValue.split(delimiter).filter(Boolean); - const windowsExt = process.platform === "win32" - ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") - : [""]; - - for (const dir of dirs) { - for (const ext of windowsExt) { - const candidate = path.join(dir, process.platform === "win32" ? `${command}${ext}` : command); - try { - await fs.access(candidate, fsConstants.X_OK); - return; - } catch { - // continue scanning PATH - } - } - } - - throw new Error(`Command not found in PATH: "${command}"`); -} +// Re-export runChildProcess with the server's pino logger wired in. +import { runChildProcess as _runChildProcess } from "@paperclip/adapter-utils/server-utils"; +import type { RunProcessResult } from "@paperclip/adapter-utils/server-utils"; export async function runChildProcess( runId: string, @@ -177,72 +40,8 @@ export async function runChildProcess( onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; }, ): Promise { - return new Promise((resolve, reject) => { - const mergedEnv = ensurePathInEnv({ ...process.env, ...opts.env }); - const child = spawn(command, args, { - cwd: opts.cwd, - env: mergedEnv, - shell: false, - stdio: ["ignore", "pipe", "pipe"], - }); - - runningProcesses.set(runId, { child, graceSec: opts.graceSec }); - - let timedOut = false; - let stdout = ""; - let stderr = ""; - let logChain: Promise = Promise.resolve(); - - const timeout = setTimeout(() => { - timedOut = true; - child.kill("SIGTERM"); - setTimeout(() => { - if (!child.killed) { - child.kill("SIGKILL"); - } - }, Math.max(1, opts.graceSec) * 1000); - }, Math.max(1, opts.timeoutSec) * 1000); - - child.stdout?.on("data", (chunk) => { - const text = String(chunk); - stdout = appendWithCap(stdout, text); - logChain = logChain - .then(() => opts.onLog("stdout", text)) - .catch((err) => logger.warn({ err, runId }, "failed to append stdout log chunk")); - }); - - child.stderr?.on("data", (chunk) => { - const text = String(chunk); - stderr = appendWithCap(stderr, text); - logChain = logChain - .then(() => opts.onLog("stderr", text)) - .catch((err) => logger.warn({ err, runId }, "failed to append stderr log chunk")); - }); - - child.on("error", (err) => { - clearTimeout(timeout); - runningProcesses.delete(runId); - const errno = (err as NodeJS.ErrnoException).code; - const pathValue = mergedEnv.PATH ?? mergedEnv.Path ?? ""; - const msg = - errno === "ENOENT" - ? `Failed to start command "${command}" in "${opts.cwd}". Verify adapter command, working directory, and PATH (${pathValue}).` - : `Failed to start command "${command}" in "${opts.cwd}": ${err.message}`; - reject(new Error(msg)); - }); - - child.on("close", (code, signal) => { - clearTimeout(timeout); - runningProcesses.delete(runId); - void logChain.finally(() => { - resolve({ - exitCode: code, - signal, - timedOut, - stdout, - stderr, - }); - }); - }); + return _runChildProcess(runId, command, args, { + ...opts, + onLogError: (err, id, msg) => logger.warn({ err, runId: id }, msg), }); } diff --git a/ui/package.json b/ui/package.json index c35dd406..fc243aec 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,9 @@ "typecheck": "tsc -b" }, "dependencies": { + "@paperclip/adapter-claude-local": "workspace:*", + "@paperclip/adapter-codex-local": "workspace:*", + "@paperclip/adapter-utils": "workspace:*", "@paperclip/shared": "workspace:*", "@radix-ui/react-slot": "^1.2.4", "@tanstack/react-query": "^5.90.21", diff --git a/ui/src/adapters/claude-local/index.ts b/ui/src/adapters/claude-local/index.ts index b6efdb67..c310a873 100644 --- a/ui/src/adapters/claude-local/index.ts +++ b/ui/src/adapters/claude-local/index.ts @@ -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", diff --git a/ui/src/adapters/codex-local/index.ts b/ui/src/adapters/codex-local/index.ts index 7eeb4a7c..2aeabd76 100644 --- a/ui/src/adapters/codex-local/index.ts +++ b/ui/src/adapters/codex-local/index.ts @@ -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", diff --git a/ui/src/adapters/types.ts b/ui/src/adapters/types.ts index 8b83362b..59a65d6a 100644 --- a/ui/src/adapters/types.ts +++ b/ui/src/adapters/types.ts @@ -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; buildAdapterConfig: (values: CreateConfigValues) => Record; } diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 56d511e2..a569c243 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -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",