import { parse } from "jsr:@std/yaml@1"; import { isAbsolute, relative, resolve } from "jsr:@std/path@1"; export type BotProfile = { type: "claude" | "codex"; model: string; token?: string; tokenEnv?: string; }; const DEFAULT_MODELS: Record = { claude: "claude-sonnet-5", codex: "gpt-5.6-terra", }; export type ProfileFileSystem = { realPath(path: string): Promise; stat(path: string): Promise<{ isFile: boolean }>; readTextFile(path: string): Promise; }; const systemFileSystem: ProfileFileSystem = { realPath: Deno.realPath, async stat(path) { return { isFile: (await Deno.stat(path)).isFile }; }, readTextFile: Deno.readTextFile, }; export async function readProfilesFile( file: string, workspace: string, fileSystem: ProfileFileSystem = systemFileSystem, ): Promise { if (isAbsolute(file)) { throw new Error("profiles-file must be repository-relative"); } const root = await fileSystem.realPath(workspace); const target = await fileSystem.realPath(resolve(root, file)); const pathFromRoot = relative(root, target); if ( pathFromRoot === "" || pathFromRoot === ".." || pathFromRoot.startsWith("../") || pathFromRoot.startsWith("..\\") || isAbsolute(pathFromRoot) ) { throw new Error("profiles-file must stay inside the repository"); } if (!(await fileSystem.stat(target)).isFile) { throw new Error("profiles-file must name a file"); } return await fileSystem.readTextFile(target); } export function parseProfiles( text: string, environment: Record, ): BotProfile[] { const value: unknown = parse(text); if (!Array.isArray(value) || value.length === 0) { throw new Error("profiles must contain at least one profile"); } return value.map((entry: unknown, index) => { const at = `profiles[${index}]`; if (typeof entry !== "object" || entry === null) { throw new Error(`${at}: expected a mapping`); } const allowed = new Set(["type", "model", "token-env"]); for (const key of Object.keys(entry)) { if (!allowed.has(key)) throw new Error(`${at}.${key}: unknown field`); } const { type, model, "token-env": tokenEnv } = entry as Record< string, unknown >; if (type !== "claude" && type !== "codex") { throw new Error(`${at}.type: expected "claude" or "codex"`); } if (model !== undefined && (typeof model !== "string" || model === "")) { throw new Error(`${at}.model: expected a model name`); } if ( tokenEnv !== undefined && (typeof tokenEnv !== "string" || !/^BOT_PROFILE_[A-Z0-9_]+$/.test(tokenEnv)) ) { throw new Error(`${at}.token-env: must start with BOT_PROFILE_`); } if (type === "claude" && tokenEnv === undefined) { throw new Error(`${at}.token-env: required for Claude`); } const token = tokenEnv === undefined ? undefined : environment[tokenEnv]; if (tokenEnv !== undefined && !token) { throw new Error(`${at}.token-env: ${tokenEnv} is not set`); } return { type, model: model ?? DEFAULT_MODELS[type], ...(token && { token }), ...(tokenEnv && { tokenEnv }), }; }); } function isUsageExhausted(error: unknown): boolean { return /(?:usage|rate) limit|hit your limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/i .test(String(error)); } export async function runProfiles( profiles: BotProfile[], attempt: (profile: BotProfile) => Promise, ): Promise { for (const [index, profile] of profiles.entries()) { try { return await attempt(profile); } catch (error) { if (!isUsageExhausted(error) || index === profiles.length - 1) { throw error; } console.error(`${profile.type} usage exhausted; trying next profile`); } } throw new Error("profiles must contain at least one profile"); } export function sanitizedEnvironment( environment: Record, profiles: BotProfile[], ): Record { const result = { ...environment }; for ( const name of [ "PROFILES", "REVIEWER_TOKEN", "BOT_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "OPENAI_API_KEY", "CODEX_API_KEY", "CODEX_ACCESS_TOKEN", ] ) { delete result[name]; } for (const name of Object.keys(result)) { if (name.startsWith("BOT_PROFILE_")) delete result[name]; } for (const profile of profiles) { if (profile.tokenEnv) delete result[profile.tokenEnv]; } return result; } export function agentEnvironment( environment: Record, profile: BotProfile, profiles: BotProfile[], ): Record { const result = sanitizedEnvironment(environment, profiles); if (profile.token) { result[ profile.type === "claude" ? "CLAUDE_CODE_OAUTH_TOKEN" : "OPENAI_API_KEY" ] = profile.token; } return result; }