diff --git a/src/claude-usage.ts b/src/claude-usage.ts index 4c863e1..0f265b4 100644 --- a/src/claude-usage.ts +++ b/src/claude-usage.ts @@ -8,6 +8,8 @@ import { logger } from './logger.js'; import { getCurrentToken, getAllTokens } from './token-rotation.js'; +const PROFILE_ENDPOINT = 'https://api.anthropic.com/api/oauth/profile'; + export interface ClaudeUsageData { five_hour?: { utilization: number; resets_at: string }; seven_day?: { utilization: number; resets_at: string }; @@ -99,6 +101,66 @@ export async function fetchClaudeUsage(): Promise { return fetchUsageForToken(token); } +export interface ClaudeAccountProfile { + email: string; + planType: string; // "max", "pro", "free" +} + +const profileCache = new Map(); + +async function fetchProfileForToken(token: string): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + try { + const res = await fetch(PROFILE_ENDPOINT, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'anthropic-beta': 'oauth-2025-04-20', + }, + signal: controller.signal, + }); + if (!res.ok) return null; + const data = await res.json() as { + account?: { email?: string; has_claude_max?: boolean; has_claude_pro?: boolean }; + organization?: { organization_type?: string }; + }; + const orgType = data.organization?.organization_type || ''; + const planType = data.account?.has_claude_max ? 'max' + : data.account?.has_claude_pro ? 'pro' + : orgType.replace('claude_', '') || 'free'; + return { + email: data.account?.email || '?', + planType, + }; + } catch { + return null; + } finally { + clearTimeout(timer); + } +} + +/** + * Fetch profiles for all Claude tokens (cached, called once on startup). + */ +export async function fetchAllClaudeProfiles(): Promise { + const allTokens = getAllTokens(); + for (const t of allTokens) { + const profile = await fetchProfileForToken(t.token); + if (profile) { + profileCache.set(t.index, profile); + logger.info( + { account: t.index + 1, plan: profile.planType, email: profile.email }, + `Claude account #${t.index + 1}: ${profile.planType}`, + ); + } + } +} + +export function getClaudeProfile(index: number): ClaudeAccountProfile | undefined { + return profileCache.get(index); +} + export interface ClaudeAccountUsage { index: number; masked: string; diff --git a/src/unified-dashboard.ts b/src/unified-dashboard.ts index 4adc783..f26fdac 100644 --- a/src/unified-dashboard.ts +++ b/src/unified-dashboard.ts @@ -7,6 +7,8 @@ import { STATUS_SHOW_ROOMS, USAGE_DASHBOARD_ENABLED } from './config.js'; import { fetchClaudeUsage as fetchClaudeUsageApi, fetchAllClaudeUsage, + fetchAllClaudeProfiles, + getClaudeProfile, type ClaudeUsageData, type ClaudeAccountUsage, } from './claude-usage.js'; @@ -600,12 +602,14 @@ async function buildUsageContent(): Promise { if (!u) continue; const h5 = u.five_hour; const d7 = u.seven_day; + const profile = getClaudeProfile(account.index); + const planSuffix = profile ? ` ${profile.planType}` : ''; const label = claudeAccounts.length > 1 - ? `Claude${account.index + 1}${account.isActive ? '*' : ''}${account.isRateLimited ? '!' : ''}` + ? `Claude${account.index + 1}${account.isActive ? '*' : ''}${account.isRateLimited ? '!' : ''}${planSuffix}` : claudeUsageIsCached - ? 'Claude*' - : 'Claude'; + ? `Claude*${planSuffix}` + : `Claude${planSuffix}`; rows.push({ name: label, h5pct: h5 @@ -863,6 +867,7 @@ export async function startUnifiedDashboard( } if (isRenderer) { + await fetchAllClaudeProfiles(); await refreshUsageCache(); }