import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { ProviderQuotaResult, QuotaWindow } from "@paperclipai/adapter-utils"; export function claudeConfigDir(): string { const fromEnv = process.env.CLAUDE_CONFIG_DIR; if (typeof fromEnv === "string" && fromEnv.trim().length > 0) return fromEnv.trim(); return path.join(os.homedir(), ".claude"); } export async function readClaudeToken(): Promise { const credPath = path.join(claudeConfigDir(), "credentials.json"); let raw: string; try { raw = await fs.readFile(credPath, "utf8"); } catch { return null; } let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return null; } if (typeof parsed !== "object" || parsed === null) return null; const obj = parsed as Record; const oauth = obj["claudeAiOauth"]; if (typeof oauth !== "object" || oauth === null) return null; const token = (oauth as Record)["accessToken"]; return typeof token === "string" && token.length > 0 ? token : null; } interface AnthropicUsageWindow { utilization?: number | null; resets_at?: string | null; } interface AnthropicUsageResponse { five_hour?: AnthropicUsageWindow | null; seven_day?: AnthropicUsageWindow | null; seven_day_sonnet?: AnthropicUsageWindow | null; seven_day_opus?: AnthropicUsageWindow | null; } /** Convert a 0-1 utilization fraction to a 0-100 integer percent. Returns null for null/undefined input. */ export function toPercent(utilization: number | null | undefined): number | null { if (utilization == null) return null; // utilization is 0-1 fraction; clamp to 100 in case of floating-point overshoot return Math.min(100, Math.round(utilization * 100)); } /** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */ export async function fetchWithTimeout(url: string, init: RequestInit, ms = 8000): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), ms); try { return await fetch(url, { ...init, signal: controller.signal }); } finally { clearTimeout(timer); } } export async function fetchClaudeQuota(token: string): Promise { const resp = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", { headers: { "Authorization": `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20", }, }); if (!resp.ok) throw new Error(`anthropic usage api returned ${resp.status}`); const body = (await resp.json()) as AnthropicUsageResponse; const windows: QuotaWindow[] = []; if (body.five_hour != null) { windows.push({ label: "5h", usedPercent: toPercent(body.five_hour.utilization), resetsAt: body.five_hour.resets_at ?? null, valueLabel: null, }); } if (body.seven_day != null) { windows.push({ label: "7d", usedPercent: toPercent(body.seven_day.utilization), resetsAt: body.seven_day.resets_at ?? null, valueLabel: null, }); } if (body.seven_day_sonnet != null) { windows.push({ label: "Sonnet 7d", usedPercent: toPercent(body.seven_day_sonnet.utilization), resetsAt: body.seven_day_sonnet.resets_at ?? null, valueLabel: null, }); } if (body.seven_day_opus != null) { windows.push({ label: "Opus 7d", usedPercent: toPercent(body.seven_day_opus.utilization), resetsAt: body.seven_day_opus.resets_at ?? null, valueLabel: null, }); } return windows; } export async function getQuotaWindows(): Promise { const token = await readClaudeToken(); if (!token) { return { provider: "anthropic", ok: false, error: "no local claude auth token", windows: [] }; } const windows = await fetchClaudeQuota(token); return { provider: "anthropic", ok: true, windows }; }