import type { ProviderId, QuotaResult } from "./types.js"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { fetchAntigravity, fetchCodex, fetchDeepSeek, fetchKimi, fetchOpenCodeGo, fetchOpenRouter, fetchXai, refreshKimiToken, } from "./providers/fetch.js"; import { homedir } from "node:os"; import { join } from "node:path"; interface AuthStorage { get: (provider: string) => unknown; getApiKey: (provider: string) => Promise | string | undefined; updateToken?: (provider: string, data: Record) => void; } /** Create an AuthStorage that reads from ~/.pi/agent/auth.json for standalone CLI execution. */ function createFileAuthStorage(customPath?: string): AuthStorage { const filePath = customPath || join(homedir(), ".pi", "agent", "auth.json"); let data: Record = {}; if (existsSync(filePath)) { try { data = JSON.parse(readFileSync(filePath, "utf8")); } catch { data = {}; } } return { get: (provider: string) => data[provider], getApiKey: (provider: string) => { const entry = data[provider]; if (typeof entry === "string") { return entry; } return entry?.key || entry?.access || entry?.token; }, updateToken: (provider: string, updated: Record) => { data[provider] = { ...data[provider], ...updated }; try { writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8"); } catch { // Ignore write failure } }, }; } /** Resolve credential from Pi's AuthStorage. */ async function token(auth: AuthStorage, provider: string): Promise { const val = await auth.getApiKey(provider); if (val) { return val; } // Fallback: some providers store in "access" or "key" or "token" field const cred = auth.get(provider) as Record | undefined; return (cred?.access as string) ?? (cred?.key as string) ?? (cred?.token as string) ?? undefined; } function codexAccountId(auth: AuthStorage): string | undefined { const cred = auth.get("openai-codex") as Record | undefined; if (typeof cred?.accountId === "string") { return cred.accountId; } try { const authPath = join(homedir(), ".codex", "auth.json"), data = JSON.parse(readFileSync(authPath, "utf8")) as any; return data?.tokens?.account_id ?? data?.tokens?.accountId; } catch { return undefined; } } type ProviderFetcher = (auth: AuthStorage, signal?: AbortSignal) => Promise; const FETCHERS: Record = { antigravity: async (auth, signal) => { const tok = await token(auth, "antigravity"); if (!tok) { return { error: "No token", ok: false, provider: "antigravity" }; } return fetchAntigravity(tok, signal); }, deepseek: async (auth, signal) => { const key = await token(auth, "deepseek"); if (!key) { return { error: "No API key", ok: false, provider: "deepseek" }; } return fetchDeepSeek(key, signal); }, "kimi-coding": async (auth, signal) => { let tok = await token(auth, "kimi-coding"); if (!tok) { return { error: "No token", ok: false, provider: "kimi-coding" }; } let res = await fetchKimi(tok, signal); if (!res.ok && res.error.includes("401")) { const cred = auth.get("kimi-coding") as Record | undefined; const rf = typeof cred?.refresh === "string" ? cred.refresh : undefined; if (rf) { const refreshed = await refreshKimiToken(rf); if (refreshed?.accessToken) { tok = refreshed.accessToken; auth.updateToken?.("kimi-coding", { access: refreshed.accessToken, expires: Date.now() + refreshed.expiresIn * 1000, refresh: refreshed.refreshToken || rf, }); res = await fetchKimi(tok, signal); } } } return res; }, "openai-codex": async (auth, signal) => { const tok = await token(auth, "openai-codex"); if (!tok) { return { error: "No token", ok: false, provider: "openai-codex" }; } const acct = codexAccountId(auth); if (!acct) { return { error: "No account ID", ok: false, provider: "openai-codex" }; } return fetchCodex(tok, acct, signal); }, "opencode-go": async (auth, signal) => { const key = await token(auth, "opencode-go"); if (!key) { return { error: "No API key", ok: false, provider: "opencode-go" }; } return fetchOpenCodeGo(key, signal); }, openrouter: async (auth, signal) => { const key = await token(auth, "openrouter"); if (!key) { return { error: "No API key", ok: false, provider: "openrouter" }; } return fetchOpenRouter(key, signal); }, xai: async (auth, signal) => { const tok = await token(auth, "xai"); if (!tok) { return { error: "No token", ok: false, provider: "xai" }; } return fetchXai(tok, signal); }, }, PROVIDER_IDS = Object.keys(FETCHERS) as ProviderId[], PROVIDER_LABELS: Record = { antigravity: "Antigravity", deepseek: "DeepSeek", "kimi-coding": "Kimi", "openai-codex": "Codex", "opencode-go": "OpenCode", openrouter: "OpenRouter", xai: "Grok", }, // ── Cache ──────────────────────────────────────────────── TTL_MS = 60_000, cache = new Map(); async function fetchQuota( auth: AuthStorage, provider: ProviderId, opts?: { force?: boolean; signal?: AbortSignal }, ): Promise { if (!opts?.force) { const cached = cache.get(provider); if (cached && Date.now() - cached.at < TTL_MS) { return cached.result; } } try { const result = await FETCHERS[provider](auth, opts?.signal); cache.set(provider, { at: Date.now(), result }); return result; } catch (error) { const msg = error instanceof Error ? error.message : String(error); return { error: msg, ok: false, provider }; } } async function fetchAllQuotas( auth: AuthStorage, opts?: { force?: boolean; signal?: AbortSignal }, ): Promise { return Promise.all(PROVIDER_IDS.map((p) => fetchQuota(auth, p, opts))); } function clearCache(provider?: ProviderId): void { if (provider) { cache.delete(provider); } else { cache.clear(); } } export type { AuthStorage }; export { PROVIDER_IDS, PROVIDER_LABELS, clearCache, createFileAuthStorage, fetchAllQuotas, fetchQuota, };