From 9a85cb272b0bef168188bc4c8c8de61d07644b2d Mon Sep 17 00:00:00 2001 From: Maxim Esipov Date: Sat, 24 Jan 2026 13:08:03 +0300 Subject: [PATCH] feat: add quota check and account management to auth login - Add enabled field to AccountMetadataV3 for account enable/disable - Add getEnabledAccounts/getTotalAccountCount methods to AccountManager - Add quota.ts with quota checking logic via fetchAvailableModels API - Integrate 'Check quotas' and 'Manage accounts' options into auth menu - Display quota status for Claude, Gemini 3 Pro, and Gemini 3 Flash - Add standalone check-quota.mjs script for manual verification - Update tsconfig to exclude temp_research folder --- scripts/check-quota.mjs | 218 +++++++++++++++++++++++++++++ src/plugin.ts | 108 +++++++++++---- src/plugin/accounts.ts | 12 ++ src/plugin/cli.ts | 24 +++- src/plugin/quota.ts | 273 +++++++++++++++++++++++++++++++++++++ src/plugin/storage.ts | 1 + src/plugin/ui/auth-menu.ts | 14 +- tsconfig.json | 2 + 8 files changed, 620 insertions(+), 32 deletions(-) create mode 100644 scripts/check-quota.mjs create mode 100644 src/plugin/quota.ts diff --git a/scripts/check-quota.mjs b/scripts/check-quota.mjs new file mode 100644 index 0000000..500bcb0 --- /dev/null +++ b/scripts/check-quota.mjs @@ -0,0 +1,218 @@ +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"; +const CLIENT_SECRET = "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"; +const CLOUD_CODE_BASE = "https://cloudcode-pa.googleapis.com"; +const USER_AGENT = "antigravity/windows/amd64"; +const FALLBACK_PROJECT_ID = "bamboo-precept-lgxtn"; + +function getDefaultAccountsPath() { + if (process.platform === "win32") { + const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming"); + return join(appData, "opencode", "antigravity-accounts.json"); + } + const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config"); + return join(xdgConfig, "opencode", "antigravity-accounts.json"); +} + +function parseArgs() { + const args = process.argv.slice(2); + let path = getDefaultAccountsPath(); + let accountIndex = null; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === "--path" && args[i + 1]) { + path = args[i + 1]; + i += 1; + continue; + } + if (arg === "--account" && args[i + 1]) { + const parsed = Number.parseInt(args[i + 1], 10); + if (!Number.isNaN(parsed)) { + accountIndex = parsed - 1; + } + i += 1; + } + } + return { path, accountIndex }; +} + +async function postJson(url, token, body, extraHeaders = {}) { + return fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "User-Agent": USER_AGENT, + ...extraHeaders, + }, + body: JSON.stringify(body), + }); +} + +async function refreshAccessToken(refreshToken) { + const response = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + }), + }); + + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`Token refresh failed (${response.status}): ${text.slice(0, 200)}`); + } + + const payload = await response.json(); + return payload.access_token; +} + +async function loadProjectId(accessToken) { + const body = { metadata: { ideType: "ANTIGRAVITY" } }; + const response = await postJson(`${CLOUD_CODE_BASE}/v1internal:loadCodeAssist`, accessToken, body); + if (!response.ok) { + return ""; + } + const payload = await response.json(); + if (typeof payload.cloudaicompanionProject === "string") { + return payload.cloudaicompanionProject; + } + if (payload.cloudaicompanionProject && typeof payload.cloudaicompanionProject.id === "string") { + return payload.cloudaicompanionProject.id; + } + return ""; +} + +function classifyGroup(modelName) { + const lower = modelName.toLowerCase(); + if (lower.includes("claude")) return "claude"; + if (!lower.includes("gemini-3")) return null; + if (lower.includes("flash")) return "gemini-flash"; + return "gemini-pro"; +} + +function updateGroup(groups, group, remainingFraction, resetTime) { + const entry = groups[group] || { count: 0 }; + entry.count += 1; + if (typeof remainingFraction === "number") { + if (entry.remaining === undefined) { + entry.remaining = remainingFraction; + } else { + entry.remaining = Math.min(entry.remaining, remainingFraction); + } + } + if (resetTime) { + const timestamp = Date.parse(resetTime); + if (Number.isFinite(timestamp)) { + if (!entry.resetTime) { + entry.resetTime = resetTime; + } else { + const existing = Date.parse(entry.resetTime); + if (!Number.isFinite(existing) || timestamp < existing) { + entry.resetTime = resetTime; + } + } + } + } + groups[group] = entry; +} + +function formatDuration(targetTime) { + const delta = targetTime - Date.now(); + if (delta <= 0) return "now"; + const totalSeconds = Math.round(delta / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + if (hours > 0) return `${hours}h ${minutes}m`; + return `${minutes}m`; +} + +function printGroup(label, entry) { + if (!entry || entry.count === 0) return; + const remaining = typeof entry.remaining === "number" ? Math.round(entry.remaining * 100) : null; + const status = remaining === null ? "UNKNOWN" : remaining <= 0 ? "LIMITED" : "OK"; + const details = []; + if (remaining !== null) details.push(`remaining ${remaining}%`); + if (entry.resetTime) { + const time = formatDuration(Date.parse(entry.resetTime)); + details.push(`resets in ${time}`); + } + const suffix = details.length ? ` (${details.join(", ")})` : ""; + console.log(` ${label}: ${status}${suffix}`); +} + +async function run() { + const { path, accountIndex } = parseArgs(); + const payload = JSON.parse(readFileSync(path, "utf8")); + const accounts = payload.accounts || []; + + if (accounts.length === 0) { + console.log("No accounts found."); + return; + } + + const selected = accountIndex === null + ? accounts.map((account, index) => ({ account, index })) + : accounts + .map((account, index) => ({ account, index })) + .filter((item) => item.index === accountIndex); + + for (const { account, index } of selected) { + const label = account.email || `Account ${index + 1}`; + const disabled = account.enabled === false ? " (disabled)" : ""; + console.log(`\n${index + 1}. ${label}${disabled}`); + + try { + const accessToken = await refreshAccessToken(account.refreshToken); + let projectId = await loadProjectId(accessToken); + if (!projectId) { + projectId = account.managedProjectId || account.projectId || FALLBACK_PROJECT_ID; + } + console.log(` project: ${projectId}`); + + const body = projectId ? { project: projectId } : {}; + const response = await postJson( + `${CLOUD_CODE_BASE}/v1internal:fetchAvailableModels`, + accessToken, + body, + ); + console.log(` fetchAvailableModels: ${response.status}`); + + if (!response.ok) { + const text = await response.text().catch(() => ""); + console.log(` error: ${text.trim().slice(0, 200)}`); + continue; + } + + const data = await response.json(); + const groups = {}; + const models = data.models || {}; + for (const [modelName, info] of Object.entries(models)) { + const group = classifyGroup(modelName); + if (!group) continue; + if (!info || !info.quotaInfo) continue; + const remaining = info.quotaInfo.remainingFraction ?? 0; + updateGroup(groups, group, remaining, info.quotaInfo.resetTime); + } + + printGroup("Claude", groups["claude"]); + printGroup("Gemini 3 Pro", groups["gemini-pro"]); + printGroup("Gemini 3 Flash", groups["gemini-flash"]); + } catch (error) { + console.log(` error: ${error instanceof Error ? error.message : String(error)}`); + } + } +} + +run().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/plugin.ts b/src/plugin.ts index 78203cc..14c74a4 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -37,6 +37,7 @@ import { AccountManager, type ModelFamily, parseRateLimitReason, calculateBackof import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker"; import { loadConfig, initRuntimeConfig, type AntigravityConfig } from "./plugin/config"; import { createSessionRecoveryHook, getRecoverySuccessToast } from "./plugin/recovery"; +import { checkAccountsQuota } from "./plugin/quota"; import { initDiskSignatureCache } from "./plugin/cache"; import { createProactiveRefreshQueue, type ProactiveRefreshQueue } from "./plugin/refresh-queue"; import { initLogger, createLogger } from "./plugin/logger"; @@ -306,6 +307,7 @@ async function persistAccountPool( managedProjectId: parts.managedProjectId, addedAt: now, lastUsed: now, + enabled: true, }); continue; } @@ -1830,39 +1832,93 @@ export const createAntigravityPlugin = (providerId: string) => async ( let refreshAccountIndex: number | undefined; const existingStorage = await loadAccounts(); if (existingStorage && existingStorage.accounts.length > 0) { - const now = Date.now(); - const existingAccounts = existingStorage.accounts.map((acc, idx) => { - let status: 'active' | 'rate-limited' | 'expired' | 'unknown' = 'unknown'; - - const rateLimits = acc.rateLimitResetTimes; - if (rateLimits) { - const isRateLimited = Object.values(rateLimits).some( - (resetTime) => typeof resetTime === 'number' && resetTime > now - ); - if (isRateLimited) { - status = 'rate-limited'; + let menuResult; + while (true) { + const now = Date.now(); + const existingAccounts = existingStorage.accounts.map((acc, idx) => { + let status: 'active' | 'rate-limited' | 'expired' | 'unknown' = 'unknown'; + + const rateLimits = acc.rateLimitResetTimes; + if (rateLimits) { + const isRateLimited = Object.values(rateLimits).some( + (resetTime) => typeof resetTime === 'number' && resetTime > now + ); + if (isRateLimited) { + status = 'rate-limited'; + } else { + status = 'active'; + } } else { status = 'active'; } - } else { - status = 'active'; + + if (acc.coolingDownUntil && acc.coolingDownUntil > now) { + status = 'rate-limited'; + } + + return { + email: acc.email, + index: idx, + addedAt: acc.addedAt, + lastUsed: acc.lastUsed, + status, + isCurrentAccount: idx === (existingStorage.activeIndex ?? 0), + enabled: acc.enabled !== false, + }; + }); + + menuResult = await promptLoginMode(existingAccounts); + + if (menuResult.mode === "check") { + console.log("\nChecking quotas for all accounts..."); + const results = await checkAccountsQuota(existingStorage.accounts, client, providerId); + for (const res of results) { + const label = res.email || `Account ${res.index + 1}`; + const disabledStr = res.disabled ? " (disabled)" : ""; + console.log(`\n${res.index + 1}. ${label}${disabledStr}`); + if (res.status === "error") { + console.log(` Error: ${res.error}`); + continue; + } + if (!res.quota || Object.keys(res.quota.groups).length === 0) { + console.log(" No quota information available."); + if (res.quota?.error) console.log(` Error: ${res.quota.error}`); + continue; + } + const printGrp = (name: string, group: any) => { + if (!group) return; + const remaining = typeof group.remainingFraction === 'number' + ? `${Math.round(group.remainingFraction * 100)}%` + : 'UNKNOWN'; + const resetStr = group.resetTime ? `, resets in ${formatWaitTime(Date.parse(group.resetTime) - Date.now())}` : ''; + console.log(` ${name}: ${remaining}${resetStr}`); + }; + printGrp("Claude", res.quota.groups.claude); + printGrp("Gemini 3 Pro", res.quota.groups["gemini-pro"]); + printGrp("Gemini 3 Flash", res.quota.groups["gemini-flash"]); + if (res.updatedAccount) { + existingStorage.accounts[res.index] = res.updatedAccount; + await saveAccounts(existingStorage); + } + } + console.log(""); + continue; } - if (acc.coolingDownUntil && acc.coolingDownUntil > now) { - status = 'rate-limited'; + if (menuResult.mode === "manage") { + if (menuResult.toggleAccountIndex !== undefined) { + const acc = existingStorage.accounts[menuResult.toggleAccountIndex]; + if (acc) { + acc.enabled = acc.enabled === false; + await saveAccounts(existingStorage); + console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? 'enabled' : 'disabled'}.\n`); + } + } + continue; } - return { - email: acc.email, - index: idx, - addedAt: acc.addedAt, - lastUsed: acc.lastUsed, - status, - isCurrentAccount: idx === (existingStorage.activeIndex ?? 0), - }; - }); - - const menuResult = await promptLoginMode(existingAccounts); + break; + } if (menuResult.mode === "cancel") { return { diff --git a/src/plugin/accounts.ts b/src/plugin/accounts.ts index d66f1fc..944f5fd 100644 --- a/src/plugin/accounts.ts +++ b/src/plugin/accounts.ts @@ -127,6 +127,7 @@ export interface ManagedAccount { parts: RefreshParts; access?: string; expires?: number; + enabled: boolean; rateLimitResetTimes: RateLimitStateV3; lastSwitchReason?: "rate-limit" | "initial" | "rotation"; coolingDownUntil?: number; @@ -277,6 +278,7 @@ export class AccountManager { }, access: matchesFallback ? authFallback?.access : undefined, expires: matchesFallback ? authFallback?.expires : undefined, + enabled: acc.enabled !== false, rateLimitResetTimes: acc.rateLimitResetTimes ?? {}, lastSwitchReason: acc.lastSwitchReason, coolingDownUntil: acc.coolingDownUntil, @@ -319,6 +321,7 @@ export class AccountManager { parts: authParts, access: authFallback.access, expires: authFallback.expires, + enabled: true, rateLimitResetTimes: {}, touchedForQuota: {}, }; @@ -342,6 +345,7 @@ export class AccountManager { parts, access: authFallback.access, expires: authFallback.expires, + enabled: true, rateLimitResetTimes: {}, touchedForQuota: {}, }, @@ -354,9 +358,17 @@ export class AccountManager { } getAccountCount(): number { + return this.getEnabledAccounts().length; + } + + getTotalAccountCount(): number { return this.accounts.length; } + getEnabledAccounts(): ManagedAccount[] { + return this.accounts.filter((account) => account.enabled); + } + getAccountsSnapshot(): ManagedAccount[] { return this.accounts.map((a) => ({ ...a, parts: { ...a.parts }, rateLimitResetTimes: { ...a.rateLimitResetTimes } })); } diff --git a/src/plugin/cli.ts b/src/plugin/cli.ts index c5a7c1d..ebc5a7f 100644 --- a/src/plugin/cli.ts +++ b/src/plugin/cli.ts @@ -29,7 +29,7 @@ export async function promptAddAnotherAccount(currentCount: number): Promise>; + modelCount: number; + error?: string; +} + +export type AccountQuotaStatus = "ok" | "disabled" | "error"; + +export interface AccountQuotaResult { + index: number; + email?: string; + status: AccountQuotaStatus; + error?: string; + disabled?: boolean; + quota?: QuotaSummary; + updatedAccount?: AccountMetadataV3; +} + +interface FetchAvailableModelsResponse { + models?: Record; +} + +interface FetchAvailableModelEntry { + quotaInfo?: { + remainingFraction?: number; + resetTime?: string; + }; + displayName?: string; + modelName?: string; +} + +function buildAuthFromAccount(account: AccountMetadataV3): OAuthAuthDetails { + return { + type: "oauth", + refresh: formatRefreshParts({ + refreshToken: account.refreshToken, + projectId: account.projectId, + managedProjectId: account.managedProjectId, + }), + access: undefined, + expires: undefined, + }; +} + +function normalizeRemainingFraction(value: unknown): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value)) { + return undefined; + } + if (value < 0) return 0; + if (value > 1) return 1; + return value; +} + +function parseResetTime(resetTime?: string): number | null { + if (!resetTime) return null; + const timestamp = Date.parse(resetTime); + if (!Number.isFinite(timestamp)) { + return null; + } + return timestamp; +} + +function classifyQuotaGroup(modelName: string, displayName?: string): QuotaGroup | null { + const combined = `${modelName} ${displayName ?? ""}`.toLowerCase(); + if (combined.includes("claude")) { + return "claude"; + } + const isGemini3 = combined.includes("gemini-3") || combined.includes("gemini 3"); + if (!isGemini3) { + return null; + } + const family = getModelFamily(modelName); + return family === "gemini-flash" ? "gemini-flash" : "gemini-pro"; +} + +function aggregateQuota(models?: Record): QuotaSummary { + const groups: Partial> = {}; + if (!models) { + return { groups, modelCount: 0 }; + } + + let totalCount = 0; + for (const [modelName, entry] of Object.entries(models)) { + const group = classifyQuotaGroup(modelName, entry.displayName ?? entry.modelName); + if (!group) { + continue; + } + const quotaInfo = entry.quotaInfo; + const remainingFraction = quotaInfo + ? normalizeRemainingFraction(quotaInfo.remainingFraction ?? 0) + : undefined; + const resetTime = quotaInfo?.resetTime; + const resetTimestamp = parseResetTime(resetTime); + + totalCount += 1; + + const existing = groups[group]; + const nextCount = (existing?.modelCount ?? 0) + 1; + const nextRemaining = + remainingFraction === undefined + ? existing?.remainingFraction + : existing?.remainingFraction === undefined + ? remainingFraction + : Math.min(existing.remainingFraction, remainingFraction); + + let nextResetTime = existing?.resetTime; + if (resetTimestamp !== null) { + if (!existing?.resetTime) { + nextResetTime = resetTime; + } else { + const existingTimestamp = parseResetTime(existing.resetTime); + if (existingTimestamp === null || resetTimestamp < existingTimestamp) { + nextResetTime = resetTime; + } + } + } + + groups[group] = { + remainingFraction: nextRemaining, + resetTime: nextResetTime, + modelCount: nextCount, + }; + } + + return { groups, modelCount: totalCount }; +} + +async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = FETCH_TIMEOUT_MS): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { ...options, signal: controller.signal }); + } finally { + clearTimeout(timeout); + } +} + +async function fetchAvailableModels( + accessToken: string, + projectId: string, +): Promise { + const endpoint = ANTIGRAVITY_ENDPOINT_PROD; + const quotaUserAgent = ANTIGRAVITY_HEADERS["User-Agent"] || "antigravity/windows/amd64"; + const errors: string[] = []; + + const body = projectId ? { project: projectId } : {}; + const response = await fetchWithTimeout(`${endpoint}/v1internal:fetchAvailableModels`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "User-Agent": quotaUserAgent, + }, + body: JSON.stringify(body), + }); + + if (response.ok) { + return (await response.json()) as FetchAvailableModelsResponse; + } + + const message = await response.text().catch(() => ""); + const snippet = message.trim().slice(0, 200); + errors.push( + `fetchAvailableModels ${response.status} at ${endpoint}${snippet ? `: ${snippet}` : ""}`, + ); + + throw new Error(errors.join("; ") || "fetchAvailableModels failed"); +} + +function applyAccountUpdates(account: AccountMetadataV3, auth: OAuthAuthDetails): AccountMetadataV3 | undefined { + const parts = parseRefreshParts(auth.refresh); + if (!parts.refreshToken) { + return undefined; + } + + const updated: AccountMetadataV3 = { + ...account, + refreshToken: parts.refreshToken, + projectId: parts.projectId ?? account.projectId, + managedProjectId: parts.managedProjectId ?? account.managedProjectId, + }; + + const changed = + updated.refreshToken !== account.refreshToken || + updated.projectId !== account.projectId || + updated.managedProjectId !== account.managedProjectId; + + return changed ? updated : undefined; +} + +export async function checkAccountsQuota( + accounts: AccountMetadataV3[], + client: PluginClient, + providerId = ANTIGRAVITY_PROVIDER_ID, +): Promise { + const results: AccountQuotaResult[] = []; + + for (const [index, account] of accounts.entries()) { + const disabled = account.enabled === false; + + let auth = buildAuthFromAccount(account); + + try { + if (accessTokenExpired(auth)) { + const refreshed = await refreshAccessToken(auth, client, providerId); + if (!refreshed) { + throw new Error("Token refresh failed"); + } + auth = refreshed; + } + + const projectContext = await ensureProjectContext(auth); + auth = projectContext.auth; + const updatedAccount = applyAccountUpdates(account, auth); + + let quotaResult: QuotaSummary; + try { + const response = await fetchAvailableModels( + auth.access ?? "", + projectContext.effectiveProjectId, + ); + quotaResult = aggregateQuota(response.models); + } catch (error) { + quotaResult = { + groups: {}, + modelCount: 0, + error: error instanceof Error ? error.message : String(error), + }; + } + + results.push({ + index, + email: account.email, + status: "ok", + disabled, + quota: quotaResult, + updatedAccount, + }); + } catch (error) { + results.push({ + index, + email: account.email, + status: "error", + disabled, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return results; +} diff --git a/src/plugin/storage.ts b/src/plugin/storage.ts index 2477af8..c91ef2b 100644 --- a/src/plugin/storage.ts +++ b/src/plugin/storage.ts @@ -182,6 +182,7 @@ export interface AccountMetadataV3 { managedProjectId?: string; addedAt: number; lastUsed: number; + enabled?: boolean; lastSwitchReason?: "rate-limit" | "initial" | "rotation"; rateLimitResetTimes?: RateLimitStateV3; coolingDownUntil?: number; diff --git a/src/plugin/ui/auth-menu.ts b/src/plugin/ui/auth-menu.ts index 9fd9825..77ba6de 100644 --- a/src/plugin/ui/auth-menu.ts +++ b/src/plugin/ui/auth-menu.ts @@ -11,15 +11,18 @@ export interface AccountInfo { lastUsed?: number; status?: AccountStatus; isCurrentAccount?: boolean; + enabled?: boolean; } export type AuthMenuAction = | { type: 'add' } | { type: 'select-account'; account: AccountInfo } | { type: 'delete-all' } + | { type: 'check' } + | { type: 'manage' } | { type: 'cancel' }; -export type AccountAction = 'back' | 'delete' | 'refresh' | 'cancel'; +export type AccountAction = 'back' | 'delete' | 'refresh' | 'toggle' | 'cancel'; function formatRelativeTime(timestamp: number | undefined): string { if (!timestamp) return 'never'; @@ -48,11 +51,14 @@ function getStatusBadge(status: AccountStatus | undefined): string { export async function showAuthMenu(accounts: AccountInfo[]): Promise { const items: MenuItem[] = [ { label: 'Add new account', value: { type: 'add' } }, + { label: 'Check quotas', value: { type: 'check' } }, + { label: 'Manage accounts (enable/disable)', value: { type: 'manage' } }, ...accounts.map(account => { const badge = getStatusBadge(account.status); + const disabledBadge = account.enabled === false ? ` ${ANSI.red}[disabled]${ANSI.reset}` : ''; const label = account.email || `Account ${account.index + 1}`; - const fullLabel = badge ? `${label} ${badge}` : label; + const fullLabel = `${label}${badge ? ' ' + badge : ''}${disabledBadge}`; return { label: fullLabel, @@ -84,9 +90,10 @@ export async function showAuthMenu(accounts: AccountInfo[]): Promise { const label = account.email || `Account ${account.index + 1}`; const badge = getStatusBadge(account.status); + const disabledBadge = account.enabled === false ? ` ${ANSI.red}[disabled]${ANSI.reset}` : ''; console.log(''); - console.log(`${ANSI.bold}Account: ${label}${badge ? ' ' + badge : ''}${ANSI.reset}`); + console.log(`${ANSI.bold}Account: ${label}${badge ? ' ' + badge : ''}${disabledBadge}${ANSI.reset}`); console.log(`${ANSI.dim}Added: ${formatDate(account.addedAt)}${ANSI.reset}`); console.log(`${ANSI.dim}Last used: ${formatRelativeTime(account.lastUsed)}${ANSI.reset}`); console.log(''); @@ -94,6 +101,7 @@ export async function showAccountDetails(account: AccountInfo): Promise