import fs from "node:fs/promises"; import path from "node:path"; import type { AdapterExecutionContext, AdapterExecutionResult } from "@agentcompany/adapter-utils"; import type { RunProcessResult } from "@agentcompany/adapter-utils/server-utils"; import { asString, asNumber, asBoolean, parseObject, parseJson, redactEnvForLogs, filterConflictingArgs, renderTemplate, runChildProcess, } from "@agentcompany/adapter-utils/server-utils"; import { parseClaudeStreamJson, describeClaudeFailure, detectClaudeLoginRequired, isClaudeMaxTurnsResult, isClaudeUnknownSessionError, } from "./parse.js"; import { buildSkillsDir, buildLoginResult, resolveClaudeBillingType, buildClaudeRuntimeConfig, parseManagedSkills, resolveSessionResumeState, } from "./execute-helpers.js"; export async function runClaudeLogin(input: { runId: string; agent: AdapterExecutionContext["agent"]; config: Record; context?: Record; authToken?: string; onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; }) { 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 { const { runId, agent, runtime, config, context, onLog, onMeta, authToken } = ctx; const promptTemplate = asString( config.promptTemplate, "{{context.taskTitle}}\n\n{{context.taskDescription}}", ); const model = asString(config.model, ""); const effort = asString(config.effort, ""); const chrome = asBoolean(config.chrome, false); const maxTurns = asNumber(config.maxTurnsPerRun, 0); const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, false); const instructionsFilePath = asString(config.instructionsFilePath, "").trim(); const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : ""; const commandNotes = instructionsFilePath ? [ `Injected agent instructions via --append-system-prompt-file ${instructionsFilePath} (with path directive appended)`, ] : []; const runtimeConfig = await buildClaudeRuntimeConfig({ runId, agent, config, context, authToken, }); const { command, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef, env, timeoutSec, graceSec, extraArgs, } = runtimeConfig; const billingType = resolveClaudeBillingType(env); const managedSkills = parseManagedSkills(config); const skillsDir = await buildSkillsDir(managedSkills, onLog); // 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). let effectiveInstructionsFilePath = instructionsFilePath; if (instructionsFilePath) { 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; } const sessionResume = resolveSessionResumeState(runtime, cwd); const { sessionId, runtimeSessionId, runtimeSessionCwd, canResumeSession } = sessionResume; if (runtimeSessionId && !canResumeSession) { await onLog( "stderr", `[agent-company] Claude session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${cwd}".\n`, ); } const prompt = renderTemplate(promptTemplate, { agentId: agent.id, companyId: agent.companyId, runId, company: { id: agent.companyId }, agent, run: { id: runId, source: "on_demand" }, context, }); const buildClaudeArgs = (resumeSessionId: string | null) => { const args = ["--print", "-", "--output-format", "stream-json", "--verbose"]; if (resumeSessionId) args.push("--resume", resumeSessionId); if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions"); if (chrome) args.push("--chrome"); if (model) args.push("--model", model); if (effort) args.push("--effort", effort); if (maxTurns > 0) args.push("--max-turns", String(maxTurns)); if (effectiveInstructionsFilePath) { args.push("--append-system-prompt-file", effectiveInstructionsFilePath); } args.push("--add-dir", skillsDir); if (extraArgs.length > 0) { // Filter out flags the adapter already manages to prevent duplicates // (e.g. user shell aliases that inject --dangerously-skip-permissions) const managedFlags = [ "--dangerously-skip-permissions", "--output-format", "--model", "--effort", "--max-turns", "--add-dir", "--append-system-prompt-file", "--teammate-mode", ]; const cleaned = filterConflictingArgs(extraArgs, managedFlags); if (cleaned.length > 0) args.push(...cleaned); } 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", command, cwd, commandArgs: args, commandNotes, env: redactEnvForLogs(env), prompt, context, }); } const proc = await runChildProcess(runId, command, args, { cwd, env, stdin: prompt, timeoutSec, graceSec, onLog, }); const parsedStream = parseClaudeStreamJson(proc.stdout); const parsed = parsedStream.resultJson ?? parseJson(proc.stdout); return { proc, parsedStream, parsed }; }; const toAdapterResult = ( attempt: { proc: RunProcessResult; parsedStream: ReturnType; parsed: Record | null; }, opts: { fallbackSessionId: string | null; clearSessionOnMissingSession?: boolean }, ): AdapterExecutionResult => { const { proc, parsedStream, parsed } = attempt; const loginMeta = detectClaudeLoginRequired({ parsed, stdout: proc.stdout, stderr: proc.stderr, }); const errorMeta = loginMeta.loginUrl != null ? { loginUrl: loginMeta.loginUrl, } : undefined; if (proc.timedOut) { return { exitCode: proc.exitCode, signal: proc.signal, timedOut: true, errorMessage: `Timed out after ${timeoutSec}s`, errorCode: "timeout", errorMeta, clearSession: Boolean(opts.clearSessionOnMissingSession), }; } if (!parsed) { return { exitCode: proc.exitCode, signal: proc.signal, timedOut: false, errorMessage: parseFallbackErrorMessage(proc), errorCode: loginMeta.requiresLogin ? "claude_auth_required" : null, errorMeta, 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); const resolvedSessionParams = resolvedSessionId ? ({ sessionId: resolvedSessionId, cwd, ...(workspaceId ? { workspaceId } : {}), ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}), ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}), } as Record) : null; const clearSessionForMaxTurns = isClaudeMaxTurnsResult(parsed); return { exitCode: proc.exitCode, signal: proc.signal, timedOut: false, errorMessage: (proc.exitCode ?? 0) === 0 ? null : describeClaudeFailure(parsed) ?? `Claude exited with code ${proc.exitCode ?? -1}`, errorCode: loginMeta.requiresLogin ? "claude_auth_required" : null, errorMeta, usage, sessionId: resolvedSessionId, sessionParams: resolvedSessionParams, sessionDisplayId: resolvedSessionId, provider: "anthropic", model: parsedStream.model || asString(parsed.model, model), billingType, costUsd: parsedStream.costUsd ?? asNumber(parsed.total_cost_usd, 0), resultJson: parsed, summary: parsedStream.summary || asString(parsed.result, ""), clearSession: clearSessionForMaxTurns || Boolean(opts.clearSessionOnMissingSession && !resolvedSessionId), }; }; try { const initial = await runAttempt(sessionId ?? null); if ( sessionId && !initial.proc.timedOut && (initial.proc.exitCode ?? 0) !== 0 && initial.parsed && isClaudeUnknownSessionError(initial.parsed) ) { await onLog( "stderr", `[agent-company] Claude resume session "${sessionId}" is unavailable; retrying with a fresh session.\n`, ); const retry = await runAttempt(null); return toAdapterResult(retry, { fallbackSessionId: null, clearSessionOnMissingSession: true }); } return toAdapterResult(initial, { fallbackSessionId: runtimeSessionId || runtime.sessionId }); } finally { fs.rm(skillsDir, { recursive: true, force: true }).catch(() => {}); } }