2026-02-18 15:29:24 -06:00
import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import { fileURLToPath } from "node:url" ;
2026-03-03 08:45:26 -06:00
import type { AdapterExecutionContext , AdapterExecutionResult } from "@paperclipai/adapter-utils" ;
import type { RunProcessResult } from "@paperclipai/adapter-utils/server-utils" ;
2026-02-18 13:53:03 -06:00
import {
asString ,
asNumber ,
asBoolean ,
asStringArray ,
parseObject ,
parseJson ,
buildPaperclipEnv ,
2026-03-15 07:05:01 -05:00
readPaperclipRuntimeSkillEntries ,
2026-03-13 08:49:11 -05:00
joinPromptSections ,
2026-03-28 15:42:14 -05:00
buildInvocationEnvForLogs ,
2026-02-18 13:53:03 -06:00
ensureAbsoluteDirectory ,
ensureCommandResolvable ,
ensurePathInEnv ,
2026-03-28 15:42:14 -05:00
resolveCommandForLogs ,
2026-02-18 13:53:03 -06:00
renderTemplate ,
runChildProcess ,
2026-03-03 08:45:26 -06:00
} from "@paperclipai/adapter-utils/server-utils" ;
2026-02-23 14:40:44 -06:00
import {
parseClaudeStreamJson ,
describeClaudeFailure ,
detectClaudeLoginRequired ,
2026-02-26 16:33:10 -06:00
isClaudeMaxTurnsResult ,
2026-02-23 14:40:44 -06:00
isClaudeUnknownSessionError ,
} from "./parse.js" ;
2026-03-13 22:49:42 -05:00
import { resolveClaudeDesiredSkillNames } from "./skills.js" ;
2026-02-18 13:53:03 -06:00
2026-03-03 16:06:12 -06:00
const __moduleDir = path . dirname ( fileURLToPath ( import . meta . url ) ) ;
2026-02-18 15:29:24 -06:00
/ * *
* 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 .
* /
2026-03-13 22:49:42 -05:00
async function buildSkillsDir ( config : Record < string , unknown > ) : Promise < string > {
2026-02-18 15:29:24 -06:00
const tmp = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-skills-" ) ) ;
const target = path . join ( tmp , ".claude" , "skills" ) ;
await fs . mkdir ( target , { recursive : true } ) ;
2026-03-15 07:05:01 -05:00
const availableEntries = await readPaperclipRuntimeSkillEntries ( config , __moduleDir ) ;
2026-03-13 22:49:42 -05:00
const desiredNames = new Set (
resolveClaudeDesiredSkillNames (
config ,
2026-03-15 07:05:01 -05:00
availableEntries ,
2026-03-13 22:49:42 -05:00
) ,
) ;
for ( const entry of availableEntries ) {
2026-03-16 18:27:20 -05:00
if ( ! desiredNames . has ( entry . key ) ) continue ;
2026-03-13 22:49:42 -05:00
await fs . symlink (
entry . source ,
2026-03-16 18:27:20 -05:00
path . join ( target , entry . runtimeName ) ,
2026-03-13 22:49:42 -05:00
) ;
2026-02-18 15:29:24 -06:00
}
return tmp ;
}
2026-02-23 14:40:44 -06:00
interface ClaudeExecutionInput {
runId : string ;
agent : AdapterExecutionContext [ "agent" ] ;
config : Record < string , unknown > ;
context : Record < string , unknown > ;
authToken? : string ;
}
2026-02-18 13:53:03 -06:00
2026-02-23 14:40:44 -06:00
interface ClaudeRuntimeConfig {
command : string ;
2026-03-28 15:42:14 -05:00
resolvedCommand : string ;
2026-02-23 14:40:44 -06:00
cwd : string ;
2026-02-25 08:38:58 -06:00
workspaceId : string | null ;
workspaceRepoUrl : string | null ;
workspaceRepoRef : string | null ;
2026-02-23 14:40:44 -06:00
env : Record < string , string > ;
2026-03-28 15:42:14 -05:00
loggedEnv : Record < string , string > ;
2026-02-23 14:40:44 -06:00
timeoutSec : number ;
graceSec : number ;
extraArgs : string [ ] ;
}
2026-02-18 13:53:03 -06:00
2026-02-23 14:40:44 -06:00
function buildLoginResult ( input : {
proc : RunProcessResult ;
loginUrl : string | null ;
} ) {
return {
exitCode : input.proc.exitCode ,
signal : input.proc.signal ,
timedOut : input.proc.timedOut ,
stdout : input.proc.stdout ,
stderr : input.proc.stderr ,
loginUrl : input.loginUrl ,
} ;
}
2026-02-25 21:35:44 -06:00
function hasNonEmptyEnvValue ( env : Record < string , string > , key : string ) : boolean {
const raw = env [ key ] ;
return typeof raw === "string" && raw . trim ( ) . length > 0 ;
}
function resolveClaudeBillingType ( env : Record < string , string > ) : "api" | "subscription" {
// Claude uses API-key auth when ANTHROPIC_API_KEY is present; otherwise rely on local login/session auth.
return hasNonEmptyEnvValue ( env , "ANTHROPIC_API_KEY" ) ? "api" : "subscription" ;
}
2026-02-23 14:40:44 -06:00
async function buildClaudeRuntimeConfig ( input : ClaudeExecutionInput ) : Promise < ClaudeRuntimeConfig > {
const { runId , agent , config , context , authToken } = input ;
const command = asString ( config . command , "claude" ) ;
2026-02-25 08:38:58 -06:00
const workspaceContext = parseObject ( context . paperclipWorkspace ) ;
const workspaceCwd = asString ( workspaceContext . cwd , "" ) ;
const workspaceSource = asString ( workspaceContext . source , "" ) ;
2026-03-10 10:58:38 -05:00
const workspaceStrategy = asString ( workspaceContext . strategy , "" ) ;
2026-02-25 08:38:58 -06:00
const workspaceId = asString ( workspaceContext . workspaceId , "" ) || null ;
const workspaceRepoUrl = asString ( workspaceContext . repoUrl , "" ) || null ;
const workspaceRepoRef = asString ( workspaceContext . repoRef , "" ) || null ;
2026-03-10 10:58:38 -05:00
const workspaceBranch = asString ( workspaceContext . branchName , "" ) || null ;
const workspaceWorktreePath = asString ( workspaceContext . worktreePath , "" ) || null ;
2026-03-14 00:36:53 -07:00
const agentHome = asString ( workspaceContext . agentHome , "" ) || null ;
2026-02-25 21:35:44 -06:00
const workspaceHints = Array . isArray ( context . paperclipWorkspaces )
? context . paperclipWorkspaces . filter (
( value ) : value is Record < string , unknown > = > typeof value === "object" && value !== null ,
)
: [ ] ;
2026-03-10 10:58:38 -05:00
const runtimeServiceIntents = Array . isArray ( context . paperclipRuntimeServiceIntents )
? context . paperclipRuntimeServiceIntents . filter (
( value ) : value is Record < string , unknown > = > typeof value === "object" && value !== null ,
)
: [ ] ;
const runtimeServices = Array . isArray ( context . paperclipRuntimeServices )
? context . paperclipRuntimeServices . filter (
( value ) : value is Record < string , unknown > = > typeof value === "object" && value !== null ,
)
: [ ] ;
const runtimePrimaryUrl = asString ( context . paperclipRuntimePrimaryUrl , "" ) ;
2026-02-25 21:35:44 -06:00
const configuredCwd = asString ( config . cwd , "" ) ;
const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd . length > 0 ;
const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd ;
const cwd = effectiveWorkspaceCwd || configuredCwd || process . cwd ( ) ;
2026-03-03 12:29:32 -06:00
await ensureAbsoluteDirectory ( cwd , { createIfMissing : true } ) ;
2026-02-23 14:40:44 -06:00
2026-02-18 13:53:03 -06:00
const envConfig = parseObject ( config . env ) ;
2026-02-18 16:46:45 -06:00
const hasExplicitApiKey =
typeof envConfig . PAPERCLIP_API_KEY === "string" && envConfig . PAPERCLIP_API_KEY . trim ( ) . length > 0 ;
2026-02-18 13:53:03 -06:00
const env : Record < string , string > = { . . . buildPaperclipEnv ( agent ) } ;
2026-02-19 09:09:50 -06:00
env . PAPERCLIP_RUN_ID = runId ;
2026-02-23 14:40:44 -06:00
2026-02-19 09:09:50 -06:00
const wakeTaskId =
( typeof context . taskId === "string" && context . taskId . trim ( ) . length > 0 && context . taskId . trim ( ) ) ||
( typeof context . issueId === "string" && context . issueId . trim ( ) . length > 0 && context . issueId . trim ( ) ) ||
null ;
const wakeReason =
typeof context . wakeReason === "string" && context . wakeReason . trim ( ) . length > 0
? context . wakeReason . trim ( )
: null ;
2026-02-20 10:32:07 -06:00
const wakeCommentId =
( typeof context . wakeCommentId === "string" && context . wakeCommentId . trim ( ) . length > 0 && context . wakeCommentId . trim ( ) ) ||
( typeof context . commentId === "string" && context . commentId . trim ( ) . length > 0 && context . commentId . trim ( ) ) ||
null ;
2026-02-19 13:02:53 -06:00
const approvalId =
typeof context . approvalId === "string" && context . approvalId . trim ( ) . length > 0
? context . approvalId . trim ( )
: null ;
const approvalStatus =
typeof context . approvalStatus === "string" && context . approvalStatus . trim ( ) . length > 0
? context . approvalStatus . trim ( )
: null ;
const linkedIssueIds = Array . isArray ( context . issueIds )
? context . issueIds . filter ( ( value ) : value is string = > typeof value === "string" && value . trim ( ) . length > 0 )
: [ ] ;
2026-02-23 14:40:44 -06:00
2026-02-19 09:09:50 -06:00
if ( wakeTaskId ) {
env . PAPERCLIP_TASK_ID = wakeTaskId ;
}
if ( wakeReason ) {
env . PAPERCLIP_WAKE_REASON = wakeReason ;
}
2026-02-20 10:32:07 -06:00
if ( wakeCommentId ) {
env . PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId ;
}
2026-02-19 13:02:53 -06:00
if ( approvalId ) {
env . PAPERCLIP_APPROVAL_ID = approvalId ;
}
if ( approvalStatus ) {
env . PAPERCLIP_APPROVAL_STATUS = approvalStatus ;
}
if ( linkedIssueIds . length > 0 ) {
env . PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds . join ( "," ) ;
}
2026-02-25 21:35:44 -06:00
if ( effectiveWorkspaceCwd ) {
env . PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd ;
2026-02-25 08:38:58 -06:00
}
if ( workspaceSource ) {
env . PAPERCLIP_WORKSPACE_SOURCE = workspaceSource ;
}
2026-03-10 10:58:38 -05:00
if ( workspaceStrategy ) {
env . PAPERCLIP_WORKSPACE_STRATEGY = workspaceStrategy ;
}
2026-02-25 08:38:58 -06:00
if ( workspaceId ) {
env . PAPERCLIP_WORKSPACE_ID = workspaceId ;
}
if ( workspaceRepoUrl ) {
env . PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl ;
}
if ( workspaceRepoRef ) {
env . PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef ;
}
2026-03-10 10:58:38 -05:00
if ( workspaceBranch ) {
env . PAPERCLIP_WORKSPACE_BRANCH = workspaceBranch ;
}
if ( workspaceWorktreePath ) {
env . PAPERCLIP_WORKSPACE_WORKTREE_PATH = workspaceWorktreePath ;
}
2026-03-14 00:36:53 -07:00
if ( agentHome ) {
env . AGENT_HOME = agentHome ;
}
2026-02-25 21:35:44 -06:00
if ( workspaceHints . length > 0 ) {
env . PAPERCLIP_WORKSPACES_JSON = JSON . stringify ( workspaceHints ) ;
}
2026-03-10 10:58:38 -05:00
if ( runtimeServiceIntents . length > 0 ) {
env . PAPERCLIP_RUNTIME_SERVICE_INTENTS_JSON = JSON . stringify ( runtimeServiceIntents ) ;
}
if ( runtimeServices . length > 0 ) {
env . PAPERCLIP_RUNTIME_SERVICES_JSON = JSON . stringify ( runtimeServices ) ;
}
if ( runtimePrimaryUrl ) {
env . PAPERCLIP_RUNTIME_PRIMARY_URL = runtimePrimaryUrl ;
}
2026-02-23 14:40:44 -06:00
for ( const [ key , value ] of Object . entries ( envConfig ) ) {
if ( typeof value === "string" ) env [ key ] = value ;
2026-02-18 13:53:03 -06:00
}
2026-02-23 14:40:44 -06:00
2026-02-18 16:46:45 -06:00
if ( ! hasExplicitApiKey && authToken ) {
env . PAPERCLIP_API_KEY = authToken ;
}
2026-02-23 14:40:44 -06:00
2026-02-18 13:53:03 -06:00
const runtimeEnv = ensurePathInEnv ( { . . . process . env , . . . env } ) ;
await ensureCommandResolvable ( command , cwd , runtimeEnv ) ;
2026-03-28 15:42:14 -05:00
const resolvedCommand = await resolveCommandForLogs ( command , cwd , runtimeEnv ) ;
const loggedEnv = buildInvocationEnvForLogs ( env , {
runtimeEnv ,
includeRuntimeKeys : [ "HOME" , "CLAUDE_CONFIG_DIR" ] ,
resolvedCommand ,
} ) ;
2026-02-18 13:53:03 -06:00
2026-02-20 10:32:07 -06:00
const timeoutSec = asNumber ( config . timeoutSec , 0 ) ;
2026-02-18 13:53:03 -06:00
const graceSec = asNumber ( config . graceSec , 20 ) ;
const extraArgs = ( ( ) = > {
const fromExtraArgs = asStringArray ( config . extraArgs ) ;
if ( fromExtraArgs . length > 0 ) return fromExtraArgs ;
return asStringArray ( config . args ) ;
} ) ( ) ;
2026-02-23 14:40:44 -06:00
return {
command ,
2026-03-28 15:42:14 -05:00
resolvedCommand ,
2026-02-23 14:40:44 -06:00
cwd ,
2026-02-25 08:38:58 -06:00
workspaceId ,
workspaceRepoUrl ,
workspaceRepoRef ,
2026-02-23 14:40:44 -06:00
env ,
2026-03-28 15:42:14 -05:00
loggedEnv ,
2026-02-23 14:40:44 -06:00
timeoutSec ,
graceSec ,
extraArgs ,
} ;
}
export async function runClaudeLogin ( input : {
runId : string ;
agent : AdapterExecutionContext [ "agent" ] ;
config : Record < string , unknown > ;
context? : Record < string , unknown > ;
authToken? : string ;
onLog ? : ( stream : "stdout" | "stderr" , chunk : string ) = > Promise < void > ;
} ) {
const onLog = input . onLog ? ? ( async ( ) = > { } ) ;
const runtime = await buildClaudeRuntimeConfig ( {
runId : input.runId ,
agent : input.agent ,
config : input.config ,
context : input.context ? ? { } ,
authToken : input.authToken ,
} ) ;
const proc = await runChildProcess ( input . runId , runtime . command , [ "login" ] , {
cwd : runtime.cwd ,
env : runtime.env ,
timeoutSec : runtime.timeoutSec ,
graceSec : runtime.graceSec ,
onLog ,
} ) ;
const loginMeta = detectClaudeLoginRequired ( {
parsed : null ,
stdout : proc.stdout ,
stderr : proc.stderr ,
} ) ;
return buildLoginResult ( {
proc ,
loginUrl : loginMeta.loginUrl ,
} ) ;
}
export async function execute ( ctx : AdapterExecutionContext ) : Promise < AdapterExecutionResult > {
2026-03-19 11:20:36 -05:00
const { runId , agent , runtime , config , context , onLog , onMeta , onSpawn , authToken } = ctx ;
2026-02-23 14:40:44 -06:00
const promptTemplate = asString (
config . promptTemplate ,
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work." ,
) ;
const model = asString ( config . model , "" ) ;
const effort = asString ( config . effort , "" ) ;
2026-02-26 16:33:10 -06:00
const chrome = asBoolean ( config . chrome , false ) ;
2026-02-23 14:40:44 -06:00
const maxTurns = asNumber ( config . maxTurnsPerRun , 0 ) ;
const dangerouslySkipPermissions = asBoolean ( config . dangerouslySkipPermissions , false ) ;
2026-02-26 16:34:15 -06:00
const instructionsFilePath = asString ( config . instructionsFilePath , "" ) . trim ( ) ;
const instructionsFileDir = instructionsFilePath ? ` ${ path . dirname ( instructionsFilePath ) } / ` : "" ;
2026-03-02 16:43:59 -06:00
const commandNotes = instructionsFilePath
? [
` Injected agent instructions via --append-system-prompt-file ${ instructionsFilePath } (with path directive appended) ` ,
]
: [ ] ;
2026-02-23 14:40:44 -06:00
const runtimeConfig = await buildClaudeRuntimeConfig ( {
runId ,
agent ,
config ,
context ,
authToken ,
} ) ;
2026-02-25 08:38:58 -06:00
const {
command ,
2026-03-28 15:42:14 -05:00
resolvedCommand ,
2026-02-25 08:38:58 -06:00
cwd ,
workspaceId ,
workspaceRepoUrl ,
workspaceRepoRef ,
env ,
2026-03-28 15:42:14 -05:00
loggedEnv ,
2026-02-25 08:38:58 -06:00
timeoutSec ,
graceSec ,
extraArgs ,
} = runtimeConfig ;
2026-03-14 22:00:12 -05:00
const effectiveEnv = Object . fromEntries (
Object . entries ( { . . . process . env , . . . env } ) . filter (
( entry ) : entry is [ string , string ] = > typeof entry [ 1 ] === "string" ,
) ,
) ;
const billingType = resolveClaudeBillingType ( effectiveEnv ) ;
2026-03-13 22:49:42 -05:00
const skillsDir = await buildSkillsDir ( config ) ;
2026-02-18 13:53:03 -06:00
2026-03-02 16:08:55 -06:00
// When instructionsFilePath is configured, create a combined temp file that
// includes both the file content and the path directive, so we only need
// --append-system-prompt-file (Claude CLI forbids using both flags together).
2026-03-10 16:46:48 -07:00
let effectiveInstructionsFilePath : string | undefined = instructionsFilePath ;
2026-03-02 16:08:55 -06:00
if ( instructionsFilePath ) {
2026-03-10 16:46:48 -07:00
try {
const instructionsContent = await fs . readFile ( instructionsFilePath , "utf-8" ) ;
const pathDirective = ` \ nThe above agent instructions were loaded from ${ instructionsFilePath } . Resolve any relative file references from ${ instructionsFileDir } . ` ;
const combinedPath = path . join ( skillsDir , "agent-instructions.md" ) ;
await fs . writeFile ( combinedPath , instructionsContent + pathDirective , "utf-8" ) ;
effectiveInstructionsFilePath = combinedPath ;
} catch ( err ) {
const reason = err instanceof Error ? err.message : String ( err ) ;
await onLog (
"stderr" ,
` [paperclip] Warning: could not read agent instructions file " ${ instructionsFilePath } ": ${ reason } \ n ` ,
) ;
effectiveInstructionsFilePath = undefined ;
}
2026-03-02 16:08:55 -06:00
}
2026-02-19 14:01:58 -06:00
const runtimeSessionParams = parseObject ( runtime . sessionParams ) ;
const runtimeSessionId = asString ( runtimeSessionParams . sessionId , runtime . sessionId ? ? "" ) ;
const runtimeSessionCwd = asString ( runtimeSessionParams . cwd , "" ) ;
const canResumeSession =
runtimeSessionId . length > 0 &&
( runtimeSessionCwd . length === 0 || path . resolve ( runtimeSessionCwd ) === path . resolve ( cwd ) ) ;
const sessionId = canResumeSession ? runtimeSessionId : null ;
if ( runtimeSessionId && ! canResumeSession ) {
await onLog (
2026-03-18 21:16:37 -05:00
"stdout" ,
2026-02-19 14:01:58 -06:00
` [paperclip] Claude session " ${ runtimeSessionId } " was saved for cwd " ${ runtimeSessionCwd } " and will not be resumed in " ${ cwd } ". \ n ` ,
) ;
}
2026-03-13 08:49:11 -05:00
const bootstrapPromptTemplate = asString ( config . bootstrapPromptTemplate , "" ) ;
const templateData = {
2026-02-19 14:39:37 -06:00
agentId : agent.id ,
companyId : agent.companyId ,
runId ,
2026-02-18 13:53:03 -06:00
company : { id : agent.companyId } ,
agent ,
run : { id : runId , source : "on_demand" } ,
context ,
2026-03-13 08:49:11 -05:00
} ;
const renderedPrompt = renderTemplate ( promptTemplate , templateData ) ;
const renderedBootstrapPrompt =
! sessionId && bootstrapPromptTemplate . trim ( ) . length > 0
? renderTemplate ( bootstrapPromptTemplate , templateData ) . trim ( )
: "" ;
const sessionHandoffNote = asString ( context . paperclipSessionHandoffMarkdown , "" ) . trim ( ) ;
const prompt = joinPromptSections ( [
renderedBootstrapPrompt ,
sessionHandoffNote ,
renderedPrompt ,
] ) ;
const promptMetrics = {
promptChars : prompt.length ,
bootstrapPromptChars : renderedBootstrapPrompt.length ,
sessionHandoffChars : sessionHandoffNote.length ,
heartbeatPromptChars : renderedPrompt.length ,
} ;
2026-02-18 13:53:03 -06:00
const buildClaudeArgs = ( resumeSessionId : string | null ) = > {
2026-02-18 15:29:24 -06:00
const args = [ "--print" , "-" , "--output-format" , "stream-json" , "--verbose" ] ;
2026-02-18 13:53:03 -06:00
if ( resumeSessionId ) args . push ( "--resume" , resumeSessionId ) ;
if ( dangerouslySkipPermissions ) args . push ( "--dangerously-skip-permissions" ) ;
2026-02-26 16:33:10 -06:00
if ( chrome ) args . push ( "--chrome" ) ;
2026-02-18 13:53:03 -06:00
if ( model ) args . push ( "--model" , model ) ;
2026-02-20 10:32:07 -06:00
if ( effort ) args . push ( "--effort" , effort ) ;
2026-02-18 13:53:03 -06:00
if ( maxTurns > 0 ) args . push ( "--max-turns" , String ( maxTurns ) ) ;
2026-03-02 16:08:55 -06:00
if ( effectiveInstructionsFilePath ) {
args . push ( "--append-system-prompt-file" , effectiveInstructionsFilePath ) ;
2026-02-26 16:34:15 -06:00
}
2026-02-18 15:29:24 -06:00
args . push ( "--add-dir" , skillsDir ) ;
2026-02-18 13:53:03 -06:00
if ( extraArgs . length > 0 ) args . push ( . . . extraArgs ) ;
return args ;
} ;
const parseFallbackErrorMessage = ( proc : RunProcessResult ) = > {
const stderrLine =
proc . stderr
. split ( /\r?\n/ )
. map ( ( line ) = > line . trim ( ) )
. find ( Boolean ) ? ? "" ;
if ( ( proc . exitCode ? ? 0 ) === 0 ) {
return "Failed to parse claude JSON output" ;
}
return stderrLine
? ` Claude exited with code ${ proc . exitCode ? ? - 1 } : ${ stderrLine } `
: ` Claude exited with code ${ proc . exitCode ? ? - 1 } ` ;
} ;
const runAttempt = async ( resumeSessionId : string | null ) = > {
const args = buildClaudeArgs ( resumeSessionId ) ;
if ( onMeta ) {
await onMeta ( {
adapterType : "claude_local" ,
2026-03-28 15:42:14 -05:00
command : resolvedCommand ,
2026-02-18 13:53:03 -06:00
cwd ,
2026-02-18 15:29:24 -06:00
commandArgs : args ,
2026-03-02 16:43:59 -06:00
commandNotes ,
2026-03-28 15:42:14 -05:00
env : loggedEnv ,
2026-02-18 13:53:03 -06:00
prompt ,
2026-03-13 08:49:11 -05:00
promptMetrics ,
2026-02-18 13:53:03 -06:00
context ,
} ) ;
}
const proc = await runChildProcess ( runId , command , args , {
cwd ,
env ,
2026-02-18 15:29:24 -06:00
stdin : prompt ,
2026-02-18 13:53:03 -06:00
timeoutSec ,
graceSec ,
2026-03-19 11:20:36 -05:00
onSpawn ,
2026-02-18 13:53:03 -06:00
onLog ,
} ) ;
const parsedStream = parseClaudeStreamJson ( proc . stdout ) ;
const parsed = parsedStream . resultJson ? ? parseJson ( proc . stdout ) ;
return { proc , parsedStream , parsed } ;
} ;
const toAdapterResult = (
attempt : {
proc : RunProcessResult ;
parsedStream : ReturnType < typeof parseClaudeStreamJson > ;
parsed : Record < string , unknown > | null ;
} ,
opts : { fallbackSessionId : string | null ; clearSessionOnMissingSession? : boolean } ,
) : AdapterExecutionResult = > {
const { proc , parsedStream , parsed } = attempt ;
2026-02-23 14:40:44 -06:00
const loginMeta = detectClaudeLoginRequired ( {
parsed ,
stdout : proc.stdout ,
stderr : proc.stderr ,
} ) ;
const errorMeta =
loginMeta . loginUrl != null
? {
loginUrl : loginMeta.loginUrl ,
}
: undefined ;
2026-02-18 13:53:03 -06:00
if ( proc . timedOut ) {
return {
exitCode : proc.exitCode ,
signal : proc.signal ,
timedOut : true ,
errorMessage : ` Timed out after ${ timeoutSec } s ` ,
2026-02-23 14:40:44 -06:00
errorCode : "timeout" ,
errorMeta ,
2026-02-18 13:53:03 -06:00
clearSession : Boolean ( opts . clearSessionOnMissingSession ) ,
} ;
}
if ( ! parsed ) {
return {
exitCode : proc.exitCode ,
signal : proc.signal ,
timedOut : false ,
errorMessage : parseFallbackErrorMessage ( proc ) ,
2026-02-23 14:40:44 -06:00
errorCode : loginMeta.requiresLogin ? "claude_auth_required" : null ,
errorMeta ,
2026-02-18 13:53:03 -06:00
resultJson : {
stdout : proc.stdout ,
stderr : proc.stderr ,
} ,
clearSession : Boolean ( opts . clearSessionOnMissingSession ) ,
} ;
}
const usage =
parsedStream . usage ? ?
( ( ) = > {
const usageObj = parseObject ( parsed . usage ) ;
return {
inputTokens : asNumber ( usageObj . input_tokens , 0 ) ,
cachedInputTokens : asNumber ( usageObj . cache_read_input_tokens , 0 ) ,
outputTokens : asNumber ( usageObj . output_tokens , 0 ) ,
} ;
} ) ( ) ;
const resolvedSessionId =
parsedStream . sessionId ? ?
( asString ( parsed . session_id , opts . fallbackSessionId ? ? "" ) || opts . fallbackSessionId ) ;
2026-02-19 14:01:58 -06:00
const resolvedSessionParams = resolvedSessionId
2026-02-25 08:38:58 -06:00
? ( {
sessionId : resolvedSessionId ,
cwd ,
. . . ( workspaceId ? { workspaceId } : { } ) ,
. . . ( workspaceRepoUrl ? { repoUrl : workspaceRepoUrl } : { } ) ,
. . . ( workspaceRepoRef ? { repoRef : workspaceRepoRef } : { } ) ,
} as Record < string , unknown > )
2026-02-19 14:01:58 -06:00
: null ;
2026-02-26 16:33:10 -06:00
const clearSessionForMaxTurns = isClaudeMaxTurnsResult ( parsed ) ;
2026-02-18 13:53:03 -06:00
return {
exitCode : proc.exitCode ,
signal : proc.signal ,
timedOut : false ,
errorMessage :
( proc . exitCode ? ? 0 ) === 0
? null
: describeClaudeFailure ( parsed ) ? ? ` Claude exited with code ${ proc . exitCode ? ? - 1 } ` ,
2026-02-23 14:40:44 -06:00
errorCode : loginMeta.requiresLogin ? "claude_auth_required" : null ,
errorMeta ,
2026-02-18 13:53:03 -06:00
usage ,
sessionId : resolvedSessionId ,
2026-02-19 14:01:58 -06:00
sessionParams : resolvedSessionParams ,
sessionDisplayId : resolvedSessionId ,
2026-02-18 13:53:03 -06:00
provider : "anthropic" ,
2026-03-14 22:00:12 -05:00
biller : "anthropic" ,
2026-02-18 13:53:03 -06:00
model : parsedStream.model || asString ( parsed . model , model ) ,
2026-02-25 21:35:44 -06:00
billingType ,
2026-02-18 13:53:03 -06:00
costUsd : parsedStream.costUsd ? ? asNumber ( parsed . total_cost_usd , 0 ) ,
resultJson : parsed ,
summary : parsedStream.summary || asString ( parsed . result , "" ) ,
2026-02-26 16:33:10 -06:00
clearSession : clearSessionForMaxTurns || Boolean ( opts . clearSessionOnMissingSession && ! resolvedSessionId ) ,
2026-02-18 13:53:03 -06:00
} ;
} ;
2026-02-18 15:29:24 -06:00
try {
const initial = await runAttempt ( sessionId ? ? null ) ;
if (
sessionId &&
! initial . proc . timedOut &&
( initial . proc . exitCode ? ? 0 ) !== 0 &&
initial . parsed &&
isClaudeUnknownSessionError ( initial . parsed )
) {
await onLog (
2026-03-18 21:16:37 -05:00
"stdout" ,
2026-02-18 15:29:24 -06:00
` [paperclip] Claude resume session " ${ sessionId } " is unavailable; retrying with a fresh session. \ n ` ,
) ;
const retry = await runAttempt ( null ) ;
return toAdapterResult ( retry , { fallbackSessionId : null , clearSessionOnMissingSession : true } ) ;
}
2026-02-18 13:53:03 -06:00
2026-02-19 14:01:58 -06:00
return toAdapterResult ( initial , { fallbackSessionId : runtimeSessionId || runtime . sessionId } ) ;
2026-02-18 15:29:24 -06:00
} finally {
fs . rm ( skillsDir , { recursive : true , force : true } ) . catch ( ( ) = > { } ) ;
}
2026-02-18 13:53:03 -06:00
}