diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index fe47b99a..78938f4b 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -175,6 +175,7 @@ export async function runChildProcess( graceSec: number; onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; onLogError?: (err: unknown, runId: string, message: string) => void; + stdin?: string; }, ): Promise { const onLogError = opts.onLogError ?? ((err, id, msg) => console.warn({ err, runId: id }, msg)); @@ -185,9 +186,14 @@ export async function runChildProcess( cwd: opts.cwd, env: mergedEnv, shell: false, - stdio: ["ignore", "pipe", "pipe"], + stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"], }); + if (opts.stdin != null && child.stdin) { + child.stdin.write(opts.stdin); + child.stdin.end(); + } + runningProcesses.set(runId, { child, graceSec: opts.graceSec }); let timedOut = false; diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index c5bf7895..4f83a350 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -72,6 +72,7 @@ export interface ServerAdapterModule { export type TranscriptEntry = | { kind: "assistant"; ts: string; text: string } | { kind: "tool_call"; ts: string; name: string; input: unknown } + | { kind: "tool_result"; ts: string; toolUseId: string; content: string; isError: boolean } | { 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 } diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index 592052b2..078d0163 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -1,3 +1,7 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip/adapter-utils"; import type { RunProcessResult } from "@paperclip/adapter-utils/server-utils"; import { @@ -17,6 +21,32 @@ import { } from "@paperclip/adapter-utils/server-utils"; import { parseClaudeStreamJson, describeClaudeFailure, isClaudeUnknownSessionError } from "./parse.js"; +const PAPERCLIP_SKILLS_DIR = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../../../../../skills", +); + +/** + * Create a tmpdir with `.claude/skills/` containing symlinks to skills from + * the repo's `skills/` directory, so `--add-dir` makes Claude Code discover + * them as proper registered skills. + */ +async function buildSkillsDir(): Promise { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-skills-")); + const target = path.join(tmp, ".claude", "skills"); + await fs.mkdir(target, { recursive: true }); + const entries = await fs.readdir(PAPERCLIP_SKILLS_DIR, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + await fs.symlink( + path.join(PAPERCLIP_SKILLS_DIR, entry.name), + path.join(target, entry.name), + ); + } + } + return tmp; +} + export async function execute(ctx: AdapterExecutionContext): Promise { const { runId, agent, runtime, config, context, onLog, onMeta } = ctx; @@ -47,6 +77,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) return fromExtraArgs; return asStringArray(config.args); })(); + const skillsDir = await buildSkillsDir(); const sessionId = runtime.sessionId; const template = sessionId ? promptTemplate : bootstrapTemplate; @@ -58,11 +89,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise { - const args = ["--print", prompt, "--output-format", "stream-json", "--verbose"]; + const args = ["--print", "-", "--output-format", "stream-json", "--verbose"]; if (resumeSessionId) args.push("--resume", resumeSessionId); if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions"); if (model) args.push("--model", model); if (maxTurns > 0) args.push("--max-turns", String(maxTurns)); + args.push("--add-dir", skillsDir); if (extraArgs.length > 0) args.push(...extraArgs); return args; }; @@ -90,7 +122,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise (idx === 1 ? `` : value)), + commandArgs: args, env: redactEnvForLogs(env), prompt, context, @@ -100,6 +132,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise {}); + } } diff --git a/packages/adapters/claude-local/src/ui/parse-stdout.ts b/packages/adapters/claude-local/src/ui/parse-stdout.ts index 15ee761f..737700dc 100644 --- a/packages/adapters/claude-local/src/ui/parse-stdout.ts +++ b/packages/adapters/claude-local/src/ui/parse-stdout.ts @@ -75,6 +75,35 @@ export function parseClaudeStdoutLine(line: string, ts: string): TranscriptEntry return entries.length > 0 ? entries : [{ kind: "stdout", ts, text: line }]; } + if (type === "user") { + const message = asRecord(parsed.message) ?? {}; + const content = Array.isArray(message.content) ? message.content : []; + const entries: TranscriptEntry[] = []; + for (const blockRaw of content) { + const block = asRecord(blockRaw); + if (!block) continue; + const blockType = typeof block.type === "string" ? block.type : ""; + if (blockType === "tool_result") { + const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : ""; + const isError = block.is_error === true; + let text = ""; + if (typeof block.content === "string") { + text = block.content; + } else if (Array.isArray(block.content)) { + const parts: string[] = []; + for (const part of block.content) { + const p = asRecord(part); + if (p && typeof p.text === "string") parts.push(p.text); + } + text = parts.join("\n"); + } + entries.push({ kind: "tool_result", ts, toolUseId, content: text, isError }); + } + } + if (entries.length > 0) return entries; + // fall through to stdout for user messages without tool_result blocks + } + if (type === "result") { const usage = asRecord(parsed.usage) ?? {}; const inputTokens = asNumber(usage.input_tokens);