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:
@@ -13,6 +13,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.10.0",
|
"@clack/prompts": "^0.10.0",
|
||||||
|
"@paperclip/adapter-claude-local": "workspace:*",
|
||||||
|
"@paperclip/adapter-codex-local": "workspace:*",
|
||||||
|
"@paperclip/adapter-utils": "workspace:*",
|
||||||
"@paperclip/db": "workspace:*",
|
"@paperclip/db": "workspace:*",
|
||||||
"@paperclip/shared": "workspace:*",
|
"@paperclip/shared": "workspace:*",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CLIAdapterModule } from "../types.js";
|
import type { CLIAdapterModule } from "@paperclip/adapter-utils";
|
||||||
import { printHttpStdoutEvent } from "./format-event.js";
|
import { printHttpStdoutEvent } from "./format-event.js";
|
||||||
|
|
||||||
export const httpCLIAdapter: CLIAdapterModule = {
|
export const httpCLIAdapter: CLIAdapterModule = {
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { getCLIAdapter } from "./registry.js";
|
export { getCLIAdapter } from "./registry.js";
|
||||||
export type { CLIAdapterModule } from "./types.js";
|
export type { CLIAdapterModule } from "@paperclip/adapter-utils";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CLIAdapterModule } from "../types.js";
|
import type { CLIAdapterModule } from "@paperclip/adapter-utils";
|
||||||
import { printProcessStdoutEvent } from "./format-event.js";
|
import { printProcessStdoutEvent } from "./format-event.js";
|
||||||
|
|
||||||
export const processCLIAdapter: CLIAdapterModule = {
|
export const processCLIAdapter: CLIAdapterModule = {
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import type { CLIAdapterModule } from "./types.js";
|
import type { CLIAdapterModule } from "@paperclip/adapter-utils";
|
||||||
import { claudeLocalCLIAdapter } from "./claude-local/index.js";
|
import { printClaudeStreamEvent } from "@paperclip/adapter-claude-local/cli";
|
||||||
import { codexLocalCLIAdapter } from "./codex-local/index.js";
|
import { printCodexStreamEvent } from "@paperclip/adapter-codex-local/cli";
|
||||||
import { processCLIAdapter } from "./process/index.js";
|
import { processCLIAdapter } from "./process/index.js";
|
||||||
import { httpCLIAdapter } from "./http/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>(
|
const adaptersByType = new Map<string, CLIAdapterModule>(
|
||||||
[claudeLocalCLIAdapter, codexLocalCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
|
[claudeLocalCLIAdapter, codexLocalCLIAdapter, processCLIAdapter, httpCLIAdapter].map((a) => [a.type, a]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface CLIAdapterModule {
|
|
||||||
type: string;
|
|
||||||
formatStdoutEvent: (line: string, debug: boolean) => void;
|
|
||||||
}
|
|
||||||
16
packages/adapter-utils/package.json
Normal file
16
packages/adapter-utils/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/adapter-utils/src/index.ts
Normal file
13
packages/adapter-utils/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export type {
|
||||||
|
AdapterAgent,
|
||||||
|
AdapterRuntime,
|
||||||
|
UsageSummary,
|
||||||
|
AdapterExecutionResult,
|
||||||
|
AdapterInvocationMeta,
|
||||||
|
AdapterExecutionContext,
|
||||||
|
ServerAdapterModule,
|
||||||
|
TranscriptEntry,
|
||||||
|
StdoutLineParser,
|
||||||
|
CLIAdapterModule,
|
||||||
|
CreateConfigValues,
|
||||||
|
} from "./types.js";
|
||||||
250
packages/adapter-utils/src/server-utils.ts
Normal file
250
packages/adapter-utils/src/server-utils.ts
Normal file
@@ -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<string, RunningProcess>();
|
||||||
|
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<string, unknown> {
|
||||||
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as Record<string, unknown>;
|
||||||
|
} 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<string, unknown>, 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<string, unknown>)[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<string, unknown>) {
|
||||||
|
return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
|
||||||
|
const redacted: Record<string, string> = {};
|
||||||
|
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<string, string> {
|
||||||
|
const vars: Record<string, string> = {
|
||||||
|
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<string, string>;
|
||||||
|
timeoutSec: number;
|
||||||
|
graceSec: number;
|
||||||
|
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||||
|
onLogError?: (err: unknown, runId: string, message: string) => void;
|
||||||
|
},
|
||||||
|
): Promise<RunProcessResult> {
|
||||||
|
const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg));
|
||||||
|
|
||||||
|
return new Promise<RunProcessResult>((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<void> = 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
113
packages/adapter-utils/src/types.ts
Normal file
113
packages/adapter-utils/src/types.ts
Normal file
@@ -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<string, unknown> | null;
|
||||||
|
summary?: string | null;
|
||||||
|
clearSession?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdapterInvocationMeta {
|
||||||
|
adapterType: string;
|
||||||
|
command: string;
|
||||||
|
cwd?: string;
|
||||||
|
commandArgs?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
prompt?: string;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdapterExecutionContext {
|
||||||
|
runId: string;
|
||||||
|
agent: AdapterAgent;
|
||||||
|
runtime: AdapterRuntime;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
context: Record<string, unknown>;
|
||||||
|
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||||
|
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerAdapterModule {
|
||||||
|
type: string;
|
||||||
|
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
8
packages/adapter-utils/tsconfig.json
Normal file
8
packages/adapter-utils/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
22
packages/adapters/claude-local/package.json
Normal file
22
packages/adapters/claude-local/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/adapters/claude-local/src/cli/index.ts
Normal file
1
packages/adapters/claude-local/src/cli/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { printClaudeStreamEvent } from "./format-event.js";
|
||||||
8
packages/adapters/claude-local/src/index.ts
Normal file
8
packages/adapters/claude-local/src/index.ts
Normal file
@@ -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" },
|
||||||
|
];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "../types.js";
|
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils";
|
||||||
import type { RunProcessResult } from "../utils.js";
|
import type { RunProcessResult } from "@paperclip/adapter-utils/server-utils";
|
||||||
import {
|
import {
|
||||||
asString,
|
asString,
|
||||||
asNumber,
|
asNumber,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "../utils.js";
|
} from "@paperclip/adapter-utils/server-utils";
|
||||||
import { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js";
|
import { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js";
|
||||||
|
|
||||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||||
2
packages/adapters/claude-local/src/server/index.ts
Normal file
2
packages/adapters/claude-local/src/server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { execute } from "./execute.js";
|
||||||
|
export { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { UsageSummary } from "../types.js";
|
import type { UsageSummary } from "@paperclip/adapter-utils";
|
||||||
import { asString, asNumber, parseObject, parseJson } from "../utils.js";
|
import { asString, asNumber, parseObject, parseJson } from "@paperclip/adapter-utils/server-utils";
|
||||||
|
|
||||||
export function parseClaudeStreamJson(stdout: string) {
|
export function parseClaudeStreamJson(stdout: string) {
|
||||||
let sessionId: string | null = null;
|
let sessionId: string | null = null;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CreateConfigValues } from "../../components/AgentConfigForm";
|
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
function parseCommaArgs(value: string): string[] {
|
function parseCommaArgs(value: string): string[] {
|
||||||
return value
|
return value
|
||||||
2
packages/adapters/claude-local/src/ui/index.ts
Normal file
2
packages/adapters/claude-local/src/ui/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { parseClaudeStdoutLine } from "./parse-stdout.js";
|
||||||
|
export { buildClaudeLocalConfig } from "./build-config.js";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TranscriptEntry } from "../types";
|
import type { TranscriptEntry } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
||||||
8
packages/adapters/claude-local/tsconfig.json
Normal file
8
packages/adapters/claude-local/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
22
packages/adapters/codex-local/package.json
Normal file
22
packages/adapters/codex-local/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/adapters/codex-local/src/cli/index.ts
Normal file
1
packages/adapters/codex-local/src/cli/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { printCodexStreamEvent } from "./format-event.js";
|
||||||
8
packages/adapters/codex-local/src/index.ts
Normal file
8
packages/adapters/codex-local/src/index.ts
Normal file
@@ -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" },
|
||||||
|
];
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "../types.js";
|
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils";
|
||||||
import {
|
import {
|
||||||
asString,
|
asString,
|
||||||
asNumber,
|
asNumber,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "../utils.js";
|
} from "@paperclip/adapter-utils/server-utils";
|
||||||
import { parseCodexJsonl } from "./parse.js";
|
import { parseCodexJsonl } from "./parse.js";
|
||||||
|
|
||||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||||
2
packages/adapters/codex-local/src/server/index.ts
Normal file
2
packages/adapters/codex-local/src/server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { execute } from "./execute.js";
|
||||||
|
export { parseCodexJsonl } from "./parse.js";
|
||||||
@@ -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) {
|
export function parseCodexJsonl(stdout: string) {
|
||||||
let sessionId: string | null = null;
|
let sessionId: string | null = null;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CreateConfigValues } from "../../components/AgentConfigForm";
|
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
function parseCommaArgs(value: string): string[] {
|
function parseCommaArgs(value: string): string[] {
|
||||||
return value
|
return value
|
||||||
2
packages/adapters/codex-local/src/ui/index.ts
Normal file
2
packages/adapters/codex-local/src/ui/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { parseCodexStdoutLine } from "./parse-stdout.js";
|
||||||
|
export { buildCodexLocalConfig } from "./build-config.js";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TranscriptEntry } from "../types";
|
import type { TranscriptEntry } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
function safeJsonParse(text: string): unknown {
|
function safeJsonParse(text: string): unknown {
|
||||||
try {
|
try {
|
||||||
8
packages/adapters/codex-local/tsconfig.json
Normal file
8
packages/adapters/codex-local/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@@ -20,6 +20,15 @@ importers:
|
|||||||
'@clack/prompts':
|
'@clack/prompts':
|
||||||
specifier: ^0.10.0
|
specifier: ^0.10.0
|
||||||
version: 0.10.1
|
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':
|
'@paperclip/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/db
|
version: link:../packages/db
|
||||||
@@ -43,6 +52,38 @@ importers:
|
|||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.9.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:
|
packages/db:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@paperclip/shared':
|
'@paperclip/shared':
|
||||||
@@ -80,6 +121,15 @@ importers:
|
|||||||
|
|
||||||
server:
|
server:
|
||||||
dependencies:
|
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':
|
'@paperclip/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/db
|
version: link:../packages/db
|
||||||
@@ -139,6 +189,15 @@ importers:
|
|||||||
|
|
||||||
ui:
|
ui:
|
||||||
dependencies:
|
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':
|
'@paperclip/shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/shared
|
version: link:../packages/shared
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
packages:
|
packages:
|
||||||
- packages/*
|
- packages/*
|
||||||
|
- packages/adapters/*
|
||||||
- server
|
- server
|
||||||
- ui
|
- ui
|
||||||
- cli
|
- cli
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@paperclip/adapter-claude-local": "workspace:*",
|
||||||
|
"@paperclip/adapter-codex-local": "workspace:*",
|
||||||
|
"@paperclip/adapter-utils": "workspace:*",
|
||||||
"@paperclip/db": "workspace:*",
|
"@paperclip/db": "workspace:*",
|
||||||
"@paperclip/shared": "workspace:*",
|
"@paperclip/shared": "workspace:*",
|
||||||
"detect-port": "^2.1.0",
|
"detect-port": "^2.1.0",
|
||||||
|
|||||||
@@ -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" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -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" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -5,7 +5,7 @@ export type {
|
|||||||
AdapterExecutionResult,
|
AdapterExecutionResult,
|
||||||
AdapterInvocationMeta,
|
AdapterInvocationMeta,
|
||||||
UsageSummary,
|
UsageSummary,
|
||||||
AgentRecord,
|
AdapterAgent,
|
||||||
AgentRuntimeStateRecord,
|
AdapterRuntime,
|
||||||
} from "./types.js";
|
} from "@paperclip/adapter-utils";
|
||||||
export { runningProcesses } from "./utils.js";
|
export { runningProcesses } from "./utils.js";
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import type { ServerAdapterModule } from "./types.js";
|
import type { ServerAdapterModule } from "./types.js";
|
||||||
import { claudeLocalAdapter } from "./claude-local/index.js";
|
import { execute as claudeExecute } from "@paperclip/adapter-claude-local/server";
|
||||||
import { codexLocalAdapter } from "./codex-local/index.js";
|
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 { processAdapter } from "./process/index.js";
|
||||||
import { httpAdapter } from "./http/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<string, ServerAdapterModule>(
|
const adaptersByType = new Map<string, ServerAdapterModule>(
|
||||||
[claudeLocalAdapter, codexLocalAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]),
|
[claudeLocalAdapter, codexLocalAdapter, processAdapter, httpAdapter].map((a) => [a.type, a]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,51 +1,12 @@
|
|||||||
import type { agents, agentRuntimeState } from "@paperclip/db";
|
// Re-export all types from the shared adapter-utils package.
|
||||||
|
// This file is kept as a convenience shim so existing in-tree
|
||||||
export interface UsageSummary {
|
// imports (process/, http/, heartbeat.ts) don't need rewriting.
|
||||||
inputTokens: number;
|
export type {
|
||||||
outputTokens: number;
|
AdapterAgent,
|
||||||
cachedInputTokens?: number;
|
AdapterRuntime,
|
||||||
}
|
UsageSummary,
|
||||||
|
AdapterExecutionResult,
|
||||||
export interface AdapterExecutionResult {
|
AdapterInvocationMeta,
|
||||||
exitCode: number | null;
|
AdapterExecutionContext,
|
||||||
signal: string | null;
|
ServerAdapterModule,
|
||||||
timedOut: boolean;
|
} from "@paperclip/adapter-utils";
|
||||||
errorMessage?: string | null;
|
|
||||||
usage?: UsageSummary;
|
|
||||||
sessionId?: string | null;
|
|
||||||
provider?: string | null;
|
|
||||||
model?: string | null;
|
|
||||||
costUsd?: number | null;
|
|
||||||
resultJson?: Record<string, unknown> | null;
|
|
||||||
summary?: string | null;
|
|
||||||
clearSession?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdapterInvocationMeta {
|
|
||||||
adapterType: string;
|
|
||||||
command: string;
|
|
||||||
cwd?: string;
|
|
||||||
commandArgs?: string[];
|
|
||||||
env?: Record<string, string>;
|
|
||||||
prompt?: string;
|
|
||||||
context?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AgentRecord = typeof agents.$inferSelect;
|
|
||||||
export type AgentRuntimeStateRecord = typeof agentRuntimeState.$inferSelect;
|
|
||||||
|
|
||||||
export interface AdapterExecutionContext {
|
|
||||||
runId: string;
|
|
||||||
agent: AgentRecord;
|
|
||||||
runtime: AgentRuntimeStateRecord;
|
|
||||||
config: Record<string, unknown>;
|
|
||||||
context: Record<string, unknown>;
|
|
||||||
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
|
||||||
onMeta?: (meta: AdapterInvocationMeta) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerAdapterModule {
|
|
||||||
type: string;
|
|
||||||
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
|
||||||
models?: { id: string; label: string }[];
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,169 +1,32 @@
|
|||||||
import { spawn, type ChildProcess } from "node:child_process";
|
// Re-export everything from the shared adapter-utils/server-utils package.
|
||||||
import { constants as fsConstants, promises as fs } from "node:fs";
|
// This file is kept as a convenience shim so existing in-tree
|
||||||
import path from "node:path";
|
// imports (process/, http/, heartbeat.ts) don't need rewriting.
|
||||||
import { logger } from "../middleware/logger.js";
|
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 {
|
// Re-export runChildProcess with the server's pino logger wired in.
|
||||||
exitCode: number | null;
|
import { runChildProcess as _runChildProcess } from "@paperclip/adapter-utils/server-utils";
|
||||||
signal: string | null;
|
import type { RunProcessResult } from "@paperclip/adapter-utils/server-utils";
|
||||||
timedOut: boolean;
|
|
||||||
stdout: string;
|
|
||||||
stderr: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RunningProcess {
|
|
||||||
child: ChildProcess;
|
|
||||||
graceSec: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const runningProcesses = new Map<string, RunningProcess>();
|
|
||||||
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<string, unknown> {
|
|
||||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return value as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, unknown> | null {
|
|
||||||
try {
|
|
||||||
return JSON.parse(value) as Record<string, unknown>;
|
|
||||||
} 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<string, unknown>, 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<string, unknown>)[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<string, unknown>) {
|
|
||||||
return template.replace(/{{\s*([a-zA-Z0-9_.-]+)\s*}}/g, (_, path) => resolvePathValue(data, path));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redactEnvForLogs(env: Record<string, string>): Record<string, string> {
|
|
||||||
const redacted: Record<string, string> = {};
|
|
||||||
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<string, string> {
|
|
||||||
const vars: Record<string, string> = {
|
|
||||||
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(
|
export async function runChildProcess(
|
||||||
runId: string,
|
runId: string,
|
||||||
@@ -177,72 +40,8 @@ export async function runChildProcess(
|
|||||||
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
|
||||||
},
|
},
|
||||||
): Promise<RunProcessResult> {
|
): Promise<RunProcessResult> {
|
||||||
return new Promise<RunProcessResult>((resolve, reject) => {
|
return _runChildProcess(runId, command, args, {
|
||||||
const mergedEnv = ensurePathInEnv({ ...process.env, ...opts.env });
|
...opts,
|
||||||
const child = spawn(command, args, {
|
onLogError: (err, id, msg) => logger.warn({ err, runId: id }, msg),
|
||||||
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<void> = 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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
"typecheck": "tsc -b"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@paperclip/adapter-claude-local": "workspace:*",
|
||||||
|
"@paperclip/adapter-codex-local": "workspace:*",
|
||||||
|
"@paperclip/adapter-utils": "workspace:*",
|
||||||
"@paperclip/shared": "workspace:*",
|
"@paperclip/shared": "workspace:*",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { UIAdapterModule } from "../types";
|
import type { UIAdapterModule } from "../types";
|
||||||
import { parseClaudeStdoutLine } from "./parse-stdout";
|
import { parseClaudeStdoutLine } from "@paperclip/adapter-claude-local/ui";
|
||||||
import { ClaudeLocalConfigFields } from "./config-fields";
|
import { ClaudeLocalConfigFields } from "./config-fields";
|
||||||
import { buildClaudeLocalConfig } from "./build-config";
|
import { buildClaudeLocalConfig } from "@paperclip/adapter-claude-local/ui";
|
||||||
|
|
||||||
export const claudeLocalUIAdapter: UIAdapterModule = {
|
export const claudeLocalUIAdapter: UIAdapterModule = {
|
||||||
type: "claude_local",
|
type: "claude_local",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { UIAdapterModule } from "../types";
|
import type { UIAdapterModule } from "../types";
|
||||||
import { parseCodexStdoutLine } from "./parse-stdout";
|
import { parseCodexStdoutLine } from "@paperclip/adapter-codex-local/ui";
|
||||||
import { CodexLocalConfigFields } from "./config-fields";
|
import { CodexLocalConfigFields } from "./config-fields";
|
||||||
import { buildCodexLocalConfig } from "./build-config";
|
import { buildCodexLocalConfig } from "@paperclip/adapter-codex-local/ui";
|
||||||
|
|
||||||
export const codexLocalUIAdapter: UIAdapterModule = {
|
export const codexLocalUIAdapter: UIAdapterModule = {
|
||||||
type: "codex_local",
|
type: "codex_local",
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import type { CreateConfigValues } from "../components/AgentConfigForm";
|
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
|
|
||||||
export type TranscriptEntry =
|
// Re-export shared types so local consumers don't need to change imports
|
||||||
| { kind: "assistant"; ts: string; text: string }
|
export type { TranscriptEntry, StdoutLineParser, CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
| { 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[];
|
|
||||||
|
|
||||||
export interface AdapterConfigFieldsProps {
|
export interface AdapterConfigFieldsProps {
|
||||||
mode: "create" | "edit";
|
mode: "create" | "edit";
|
||||||
@@ -33,7 +25,7 @@ export interface AdapterConfigFieldsProps {
|
|||||||
export interface UIAdapterModule {
|
export interface UIAdapterModule {
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
parseStdoutLine: StdoutLineParser;
|
parseStdoutLine: (line: string, ts: string) => import("@paperclip/adapter-utils").TranscriptEntry[];
|
||||||
ConfigFields: ComponentType<AdapterConfigFieldsProps>;
|
ConfigFields: ComponentType<AdapterConfigFieldsProps>;
|
||||||
buildAdapterConfig: (values: CreateConfigValues) => Record<string, unknown>;
|
buildAdapterConfig: (values: CreateConfigValues) => Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,24 +29,10 @@ import { ClaudeLocalAdvancedFields } from "../adapters/claude-local/config-field
|
|||||||
|
|
||||||
/* ---- Create mode values ---- */
|
/* ---- Create mode values ---- */
|
||||||
|
|
||||||
export interface CreateConfigValues {
|
// Canonical type lives in @paperclip/adapter-utils; re-exported here
|
||||||
adapterType: string;
|
// so existing imports from this file keep working.
|
||||||
cwd: string;
|
export type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
promptTemplate: string;
|
import type { CreateConfigValues } from "@paperclip/adapter-utils";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultCreateValues: CreateConfigValues = {
|
export const defaultCreateValues: CreateConfigValues = {
|
||||||
adapterType: "claude_local",
|
adapterType: "claude_local",
|
||||||
|
|||||||
Reference in New Issue
Block a user