import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { AdapterExecutionContext } from "@paperclipai/adapter-utils"; const TRUTHY_ENV_RE = /^(1|true|yes|on)$/i; const COPIED_SHARED_FILES = ["config.json", "config.toml", "instructions.md"] as const; const SYMLINKED_SHARED_FILES = ["auth.json"] as const; const DEFAULT_PAPERCLIP_INSTANCE_ID = "default"; function nonEmpty(value: string | undefined): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } export async function pathExists(candidate: string): Promise { return fs.access(candidate).then(() => true).catch(() => false); } export function resolveSharedCodexHomeDir( env: NodeJS.ProcessEnv = process.env, ): string { const fromEnv = nonEmpty(env.CODEX_HOME); return fromEnv ? path.resolve(fromEnv) : path.join(os.homedir(), ".codex"); } function isWorktreeMode(env: NodeJS.ProcessEnv): boolean { return TRUTHY_ENV_RE.test(env.PAPERCLIP_IN_WORKTREE ?? ""); } export function resolveManagedCodexHomeDir( env: NodeJS.ProcessEnv, companyId?: string, ): string { const paperclipHome = nonEmpty(env.PAPERCLIP_HOME) ?? path.resolve(os.homedir(), ".paperclip"); const instanceId = nonEmpty(env.PAPERCLIP_INSTANCE_ID) ?? DEFAULT_PAPERCLIP_INSTANCE_ID; return companyId ? path.resolve(paperclipHome, "instances", instanceId, "companies", companyId, "codex-home") : path.resolve(paperclipHome, "instances", instanceId, "codex-home"); } async function ensureParentDir(target: string): Promise { await fs.mkdir(path.dirname(target), { recursive: true }); } async function ensureSymlink(target: string, source: string): Promise { const existing = await fs.lstat(target).catch(() => null); if (!existing) { await ensureParentDir(target); await fs.symlink(source, target); return; } if (!existing.isSymbolicLink()) { return; } const linkedPath = await fs.readlink(target).catch(() => null); if (!linkedPath) return; const resolvedLinkedPath = path.resolve(path.dirname(target), linkedPath); if (resolvedLinkedPath === source) return; await fs.unlink(target); await fs.symlink(source, target); } async function ensureCopiedFile(target: string, source: string): Promise { const existing = await fs.lstat(target).catch(() => null); if (existing) return; await ensureParentDir(target); await fs.copyFile(source, target); } /** * Writes an `auth.json` containing only `OPENAI_API_KEY` so the codex CLI can * authenticate via API key. Overwrites any existing file or symlink at that * path. Required because the codex CLI (>= 0.122) ignores the `OPENAI_API_KEY` * environment variable and only reads credentials from `$CODEX_HOME/auth.json`. */ export async function writeApiKeyAuthJson(home: string, apiKey: string): Promise { await fs.mkdir(home, { recursive: true }); const target = path.join(home, "auth.json"); await fs.rm(target, { force: true }); await fs.writeFile(target, JSON.stringify({ OPENAI_API_KEY: apiKey }), { mode: 0o600 }); } export async function prepareManagedCodexHome( env: NodeJS.ProcessEnv, onLog: AdapterExecutionContext["onLog"], companyId?: string, options: { apiKey?: string | null } = {}, ): Promise { const targetHome = resolveManagedCodexHomeDir(env, companyId); const apiKey = nonEmpty(options.apiKey ?? undefined); const sourceHome = resolveSharedCodexHomeDir(env); const seedFromShared = path.resolve(sourceHome) !== path.resolve(targetHome); await fs.mkdir(targetHome, { recursive: true }); // If a previous run wrote an apikey-mode auth.json (regular file) and this // run has no apiKey, remove it so the chatgpt-mode symlink can be restored. // Without this cleanup, ensureSymlink bails on a non-symlink and Codex keeps // authenticating with the stale key after it is removed from configuration. if (!apiKey && seedFromShared) { const authPath = path.join(targetHome, "auth.json"); const existing = await fs.lstat(authPath).catch(() => null); if (existing && !existing.isSymbolicLink()) { await fs.rm(authPath, { force: true }); } } if (seedFromShared) { for (const name of SYMLINKED_SHARED_FILES) { const source = path.join(sourceHome, name); if (!(await pathExists(source))) continue; await ensureSymlink(path.join(targetHome, name), source); } for (const name of COPIED_SHARED_FILES) { const source = path.join(sourceHome, name); if (!(await pathExists(source))) continue; await ensureCopiedFile(path.join(targetHome, name), source); } await onLog( "stdout", `[paperclip] Using ${isWorktreeMode(env) ? "worktree-isolated" : "Paperclip-managed"} Codex home "${targetHome}" (seeded from "${sourceHome}").\n`, ); } if (apiKey) { await writeApiKeyAuthJson(targetHome, apiKey); await onLog( "stdout", `[paperclip] Wrote API-key auth.json into Codex home "${targetHome}" from configured OPENAI_API_KEY.\n`, ); } return targetHome; }