// The agent and model a profile runs, which every report names. export interface ProfileIdentity { type: "CLAUDE" | "CODEX" | "OPENCODE"; model: string; } export interface BotProfile extends ProfileIdentity { token?: string; } // The lowercase name a profile type reads as in posted comments. function profileName(type: ProfileIdentity["type"]): string { switch (type) { case "CLAUDE": return "claude"; case "CODEX": return "codex"; case "OPENCODE": return "opencode"; } } export function profileFooter(profile: ProfileIdentity): string { return `> Profile: \`${profileName(profile.type)}\` · \`${profile.model}\``; } export function withProfile(body: string, profile: ProfileIdentity): string { return `${body}\n\n${profileFooter(profile)}`; } export type FailureReason = "SIGN_IN_FAILED" | "USAGE_EXHAUSTED" | "FAILED"; // The words a failure reason reads as in logs and posted comments. function toMessage(reason: FailureReason): string { switch (reason) { case "SIGN_IN_FAILED": return "sign-in failed"; case "USAGE_EXHAUSTED": return "usage exhausted"; case "FAILED": return "failed"; } } export interface ProfileFailure extends ProfileIdentity { reason: FailureReason; } // Where a profile's isolated user keeps its home. interface ProfileHome { type: ProfileIdentity["type"]; home: string; } // Raised once an agent's credentials are rejected and any re-login is spent. export class AgentAuthError extends Error { constructor(message: string, options?: ErrorOptions) { super(message, options); this.name = "AgentAuthError"; } } function isUsageExhausted(error: unknown): boolean { return /(?:usage|rate) limit|hit your (?:weekly )?limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/iu.test( String(error), ); } function failureReason(error: unknown): FailureReason { if (error instanceof AgentAuthError) { return "SIGN_IN_FAILED"; } return isUsageExhausted(error) ? "USAGE_EXHAUSTED" : "FAILED"; } export function failureFooter(failures: ProfileFailure[]): string { if (failures.length === 0) { return "> Profile: none"; } return `> Tried: ${failures .map( ({ type, model, reason }) => `\`${profileName(type)}\` · \`${model}\` (${toMessage(reason)})`, ) .join(", ")}`; } // Tries the profiles in order, moving on only after a sign-in failure or // exhausted usage, and records every profile it tried. export async function runProfiles( profiles: P[], attempt: (profile: P) => Promise, failures: ProfileFailure[] = [], ): Promise { for (const [index, profile] of profiles.entries()) { try { return await attempt(profile); } catch (error) { const reason = failureReason(error); failures.push({ type: profile.type, model: profile.model, reason }); if (reason === "FAILED" || index === profiles.length - 1) { throw error; } console.error(`${profile.type} ${toMessage(reason)}; trying next profile`); } } throw new Error("profiles must contain at least one profile"); } // Environments are maps from variable name to value, converted to the object // form only where a child process is spawned. export function sanitizedEnvironment( environment: ReadonlyMap, ): Map { const result = new Map(); for (const name of [ "PATH", "HOME", "LANG", "LANGUAGE", "LC_ALL", "TERM", "COLORTERM", "CI", "TMPDIR", "GITEA_TOKEN", "GITEA_API_URL", "GITEA_REPOSITORY", "GITHUB_WORKSPACE", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "all_proxy", "no_proxy", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS", ]) { const value = environment.get(name); if (value !== undefined) { result.set(name, value); } } return result; } // The environment variable each agent CLI reads its credential from. function tokenVariable(type: ProfileIdentity["type"]): string { switch (type) { case "CLAUDE": return "CLAUDE_CODE_OAUTH_TOKEN"; case "CODEX": return "OPENAI_API_KEY"; case "OPENCODE": return "OPENROUTER_API_KEY"; } } export function agentEnvironment( environment: ReadonlyMap, profile: ProfileHome, token?: string, ): Map { const result = sanitizedEnvironment(environment); result.set("HOME", profile.home); if (profile.type === "CODEX") { result.set("CODEX_HOME", `${profile.home}/.codex`); } if (token !== undefined && token !== "") { const tokenName = tokenVariable(profile.type); result.set(tokenName, token); } return result; }