opencode-antigravity-auth 1.4.2 → 1.4.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +56 -54
  2. package/dist/src/plugin/accounts.d.ts +46 -3
  3. package/dist/src/plugin/accounts.d.ts.map +1 -1
  4. package/dist/src/plugin/accounts.js +187 -8
  5. package/dist/src/plugin/accounts.js.map +1 -1
  6. package/dist/src/plugin/config/loader.d.ts.map +1 -1
  7. package/dist/src/plugin/config/loader.js +7 -4
  8. package/dist/src/plugin/config/loader.js.map +1 -1
  9. package/dist/src/plugin/config/schema.d.ts +19 -0
  10. package/dist/src/plugin/config/schema.d.ts.map +1 -1
  11. package/dist/src/plugin/config/schema.js +55 -0
  12. package/dist/src/plugin/config/schema.js.map +1 -1
  13. package/dist/src/plugin/debug.d.ts +26 -0
  14. package/dist/src/plugin/debug.d.ts.map +1 -1
  15. package/dist/src/plugin/debug.js +69 -1
  16. package/dist/src/plugin/debug.js.map +1 -1
  17. package/dist/src/plugin/quota.d.ts +10 -0
  18. package/dist/src/plugin/quota.d.ts.map +1 -1
  19. package/dist/src/plugin/quota.js +88 -7
  20. package/dist/src/plugin/quota.js.map +1 -1
  21. package/dist/src/plugin/recovery.d.ts.map +1 -1
  22. package/dist/src/plugin/recovery.js +2 -0
  23. package/dist/src/plugin/recovery.js.map +1 -1
  24. package/dist/src/plugin/request.d.ts.map +1 -1
  25. package/dist/src/plugin/request.js +30 -15
  26. package/dist/src/plugin/request.js.map +1 -1
  27. package/dist/src/plugin/storage.d.ts +19 -0
  28. package/dist/src/plugin/storage.d.ts.map +1 -1
  29. package/dist/src/plugin/storage.js +44 -4
  30. package/dist/src/plugin/storage.js.map +1 -1
  31. package/dist/src/plugin/transform/model-resolver.d.ts +7 -9
  32. package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
  33. package/dist/src/plugin/transform/model-resolver.js +12 -37
  34. package/dist/src/plugin/transform/model-resolver.js.map +1 -1
  35. package/dist/src/plugin/transform/types.d.ts +1 -1
  36. package/dist/src/plugin/transform/types.d.ts.map +1 -1
  37. package/dist/src/plugin.d.ts +7 -0
  38. package/dist/src/plugin.d.ts.map +1 -1
  39. package/dist/src/plugin.js +262 -50
  40. package/dist/src/plugin.js.map +1 -1
  41. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { exec } from "node:child_process";
2
2
  import { tool } from "@opencode-ai/plugin";
3
- import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_PROVIDER_ID } from "./constants";
3
+ import { ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_ENDPOINT_PROD, ANTIGRAVITY_PROVIDER_ID } from "./constants";
4
4
  import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth";
5
5
  import { accessTokenExpired, isOAuthAuth, parseRefreshParts } from "./plugin/auth";
6
6
  import { promptAddAnotherAccount, promptLoginMode, promptProjectId } from "./plugin/cli";
@@ -13,7 +13,7 @@ import { EmptyResponseError } from "./plugin/errors";
13
13
  import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token";
14
14
  import { startOAuthListener } from "./plugin/server";
15
15
  import { clearAccounts, loadAccounts, saveAccounts } from "./plugin/storage";
16
- import { AccountManager, parseRateLimitReason, calculateBackoffMs } from "./plugin/accounts";
16
+ import { AccountManager, parseRateLimitReason, calculateBackoffMs, computeSoftQuotaCacheTtlMs } from "./plugin/accounts";
17
17
  import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker";
18
18
  import { loadConfig, initRuntimeConfig } from "./plugin/config";
19
19
  import { createSessionRecoveryHook, getRecoverySuccessToast } from "./plugin/recovery";
@@ -33,13 +33,18 @@ function getCapacityBackoffDelay(consecutiveFailures) {
33
33
  }
34
34
  const warmupAttemptedSessionIds = new Set();
35
35
  const warmupSucceededSessionIds = new Set();
36
+ // Track if this plugin instance is running in a child session (subagent, background task)
37
+ // Used to filter toasts based on toast_scope config
38
+ let isChildSession = false;
39
+ let childSessionParentID = undefined;
36
40
  const log = createLogger("plugin");
37
41
  // Module-level toast debounce to persist across requests (fixes toast spam)
38
42
  const rateLimitToastCooldowns = new Map();
39
43
  const RATE_LIMIT_TOAST_COOLDOWN_MS = 5000;
40
44
  const MAX_TOAST_COOLDOWN_ENTRIES = 100;
41
- // Track if "all accounts rate-limited" toast was shown to prevent spam in while loop
42
- let allAccountsRateLimitedToastShown = false;
45
+ // Track if "all accounts blocked" toasts were shown to prevent spam in while loop
46
+ let softQuotaToastShown = false;
47
+ let rateLimitToastShown = false;
43
48
  function cleanupToastCooldowns() {
44
49
  if (rateLimitToastCooldowns.size > MAX_TOAST_COOLDOWN_ENTRIES) {
45
50
  const now = Date.now();
@@ -61,8 +66,47 @@ function shouldShowRateLimitToast(message) {
61
66
  rateLimitToastCooldowns.set(toastKey, now);
62
67
  return true;
63
68
  }
64
- function resetAllAccountsRateLimitedToast() {
65
- allAccountsRateLimitedToastShown = false;
69
+ function resetAllAccountsBlockedToasts() {
70
+ softQuotaToastShown = false;
71
+ rateLimitToastShown = false;
72
+ }
73
+ const quotaRefreshInProgressByEmail = new Set();
74
+ async function triggerAsyncQuotaRefreshForAccount(accountManager, accountIndex, client, providerId, intervalMinutes) {
75
+ if (intervalMinutes <= 0)
76
+ return;
77
+ const accounts = accountManager.getAccounts();
78
+ const account = accounts[accountIndex];
79
+ if (!account || account.enabled === false)
80
+ return;
81
+ const accountKey = account.email ?? `idx-${accountIndex}`;
82
+ if (quotaRefreshInProgressByEmail.has(accountKey))
83
+ return;
84
+ const intervalMs = intervalMinutes * 60 * 1000;
85
+ const age = account.cachedQuotaUpdatedAt != null
86
+ ? Date.now() - account.cachedQuotaUpdatedAt
87
+ : Infinity;
88
+ if (age < intervalMs)
89
+ return;
90
+ quotaRefreshInProgressByEmail.add(accountKey);
91
+ try {
92
+ const accountsForCheck = accountManager.getAccountsForQuotaCheck();
93
+ const singleAccount = accountsForCheck[accountIndex];
94
+ if (!singleAccount) {
95
+ quotaRefreshInProgressByEmail.delete(accountKey);
96
+ return;
97
+ }
98
+ const results = await checkAccountsQuota([singleAccount], client, providerId);
99
+ if (results[0]?.status === "ok" && results[0]?.quota?.groups) {
100
+ accountManager.updateQuotaCache(accountIndex, results[0].quota.groups);
101
+ accountManager.requestSaveToDisk();
102
+ }
103
+ }
104
+ catch (err) {
105
+ log.debug(`quota-refresh-failed email=${accountKey}`, { error: String(err) });
106
+ }
107
+ finally {
108
+ quotaRefreshInProgressByEmail.delete(accountKey);
109
+ }
66
110
  }
67
111
  function trackWarmupAttempt(sessionId) {
68
112
  if (warmupSucceededSessionIds.has(sessionId)) {
@@ -650,6 +694,22 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
650
694
  const eventHandler = async (input) => {
651
695
  // Forward to update checker
652
696
  await updateChecker.event(input);
697
+ // Track if this is a child session (subagent, background task)
698
+ // This is used to filter toasts based on toast_scope config
699
+ if (input.event.type === "session.created") {
700
+ const props = input.event.properties;
701
+ if (props?.info?.parentID) {
702
+ isChildSession = true;
703
+ childSessionParentID = props.info.parentID;
704
+ log.debug("child-session-detected", { parentID: props.info.parentID });
705
+ }
706
+ else {
707
+ // Reset for root sessions - important when plugin instance is reused
708
+ isChildSession = false;
709
+ childSessionParentID = undefined;
710
+ log.debug("root-session-detected", {});
711
+ }
712
+ }
653
713
  // Handle session recovery
654
714
  if (sessionRecovery && input.event.type === "session.error") {
655
715
  const props = input.event.properties;
@@ -674,15 +734,18 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
674
734
  body: { parts: [{ type: "text", text: config.resume_text }] },
675
735
  query: { directory },
676
736
  }).catch(() => { });
677
- // Show success toast
737
+ // Show success toast (respects toast_scope for child sessions)
678
738
  const successToast = getRecoverySuccessToast();
679
- await client.tui.showToast({
680
- body: {
681
- title: successToast.title,
682
- message: successToast.message,
683
- variant: "success",
684
- },
685
- }).catch(() => { });
739
+ log.debug("recovery-toast", { ...successToast, isChildSession, toastScope: config.toast_scope });
740
+ if (!(config.toast_scope === "root_only" && isChildSession)) {
741
+ await client.tui.showToast({
742
+ body: {
743
+ title: successToast.title,
744
+ message: successToast.message,
745
+ variant: "success",
746
+ },
747
+ }).catch(() => { });
748
+ }
686
749
  }
687
750
  }
688
751
  }
@@ -822,12 +885,20 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
822
885
  // Use while(true) loop to handle rate limits with backoff
823
886
  // This ensures we wait and retry when all accounts are rate-limited
824
887
  const quietMode = config.quiet_mode;
825
- // Helper to show toast without blocking on abort (respects quiet_mode)
888
+ const toastScope = config.toast_scope;
889
+ // Helper to show toast without blocking on abort (respects quiet_mode and toast_scope)
826
890
  const showToast = async (message, variant) => {
891
+ // Always log to debug regardless of toast filtering
892
+ log.debug("toast", { message, variant, isChildSession, toastScope });
827
893
  if (quietMode)
828
894
  return;
829
895
  if (abortSignal?.aborted)
830
896
  return;
897
+ // Filter toasts for child sessions when toast_scope is "root_only"
898
+ if (toastScope === "root_only" && isChildSession) {
899
+ log.debug("toast-suppressed-child-session", { message, variant, parentID: childSessionParentID });
900
+ return;
901
+ }
831
902
  if (variant === "warning" && message.toLowerCase().includes("rate")) {
832
903
  if (!shouldShowRateLimitToast(message)) {
833
904
  return;
@@ -845,8 +916,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
845
916
  const hasOtherAccountWithAntigravity = (currentAccount) => {
846
917
  if (family !== "gemini")
847
918
  return false;
848
- const otherAccounts = accountManager.getAccounts().filter(acc => acc.index !== currentAccount.index);
849
- return otherAccounts.some(acc => !accountManager.isRateLimitedForHeaderStyle(acc, family, "antigravity", model));
919
+ // Use AccountManager method which properly checks for disabled/cooling-down accounts
920
+ return accountManager.hasOtherAccountWithAntigravityAvailable(currentAccount.index, family, model);
850
921
  };
851
922
  while (true) {
852
923
  // Check for abort at the start of each iteration
@@ -855,8 +926,29 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
855
926
  if (accountCount === 0) {
856
927
  throw new Error("No Antigravity accounts available. Run `opencode auth login`.");
857
928
  }
858
- const account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, 'antigravity', config.pid_offset_enabled);
929
+ const softQuotaCacheTtlMs = computeSoftQuotaCacheTtlMs(config.soft_quota_cache_ttl_minutes, config.quota_refresh_interval_minutes);
930
+ const account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, 'antigravity', config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
859
931
  if (!account) {
932
+ if (accountManager.areAllAccountsOverSoftQuota(family, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, model)) {
933
+ const threshold = config.soft_quota_threshold_percent;
934
+ const softQuotaWaitMs = accountManager.getMinWaitTimeForSoftQuota(family, threshold, softQuotaCacheTtlMs, model);
935
+ const maxWaitMs = (config.max_rate_limit_wait_seconds ?? 300) * 1000;
936
+ if (softQuotaWaitMs === null || (maxWaitMs > 0 && softQuotaWaitMs > maxWaitMs)) {
937
+ const waitTimeFormatted = softQuotaWaitMs ? formatWaitTime(softQuotaWaitMs) : "unknown";
938
+ await showToast(`All accounts over ${threshold}% quota threshold. Resets in ${waitTimeFormatted}.`, "error");
939
+ throw new Error(`Quota protection: All ${accountCount} account(s) are over ${threshold}% usage for ${family}. ` +
940
+ `Quota resets in ${waitTimeFormatted}. ` +
941
+ `Add more accounts, wait for quota reset, or set soft_quota_threshold_percent: 100 to disable.`);
942
+ }
943
+ const waitSecValue = Math.max(1, Math.ceil(softQuotaWaitMs / 1000));
944
+ pushDebug(`all-over-soft-quota family=${family} accounts=${accountCount} waitMs=${softQuotaWaitMs}`);
945
+ if (!softQuotaToastShown) {
946
+ await showToast(`All ${accountCount} account(s) over ${threshold}% quota. Waiting ${formatWaitTime(softQuotaWaitMs)}...`, "warning");
947
+ softQuotaToastShown = true;
948
+ }
949
+ await sleep(softQuotaWaitMs, abortSignal);
950
+ continue;
951
+ }
860
952
  const headerStyle = getHeaderStyleFromUrl(urlString, family);
861
953
  const explicitQuota = isExplicitQuotaFromUrl(urlString);
862
954
  // All accounts are rate-limited - wait and retry
@@ -882,16 +974,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
882
974
  `Quota resets in ${waitTimeFormatted}. ` +
883
975
  `Add more accounts with \`opencode auth login\` or wait and retry.`);
884
976
  }
885
- if (!allAccountsRateLimitedToastShown) {
977
+ if (!rateLimitToastShown) {
886
978
  await showToast(`All ${accountCount} account(s) rate-limited for ${family}. Waiting ${waitSecValue}s...`, "warning");
887
- allAccountsRateLimitedToastShown = true;
979
+ rateLimitToastShown = true;
888
980
  }
889
981
  // Wait for the rate-limit cooldown to expire, then retry
890
982
  await sleep(waitMs, abortSignal);
891
983
  continue;
892
984
  }
893
985
  // Account is available - reset the toast flag
894
- resetAllAccountsRateLimitedToast();
986
+ resetAllAccountsBlockedToasts();
895
987
  pushDebug(`selected idx=${account.index} email=${account.email ?? ""} family=${family} accounts=${accountCount} strategy=${config.account_selection_strategy}`);
896
988
  if (isDebugEnabled()) {
897
989
  logAccountContext("Selected", {
@@ -1054,9 +1146,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1054
1146
  // Try endpoint fallbacks with single header style based on model suffix
1055
1147
  let shouldSwitchAccount = false;
1056
1148
  // Determine header style from model suffix:
1057
- // - Models with :antigravity suffix -> use Antigravity quota
1058
- // - Models without suffix (default) -> use Gemini CLI quota
1059
- // - Claude models -> always use Antigravity
1149
+ // - Gemini models default to Antigravity
1150
+ // - Claude models always use Antigravity
1060
1151
  let headerStyle = getHeaderStyleFromUrl(urlString, family);
1061
1152
  const explicitQuota = isExplicitQuotaFromUrl(urlString);
1062
1153
  pushDebug(`headerStyle=${headerStyle} explicit=${explicitQuota}`);
@@ -1065,8 +1156,29 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1065
1156
  }
1066
1157
  // Check if this header style is rate-limited for this account
1067
1158
  if (accountManager.isRateLimitedForHeaderStyle(account, family, headerStyle, model)) {
1068
- // Quota fallback: try alternate quota on same account (if enabled and not explicit)
1069
- if (config.quota_fallback && !explicitQuota && family === "gemini") {
1159
+ // Antigravity-first fallback: exhaust antigravity across ALL accounts before gemini-cli
1160
+ if (config.quota_fallback && !explicitQuota && family === "gemini" && headerStyle === "antigravity") {
1161
+ // Check if ANY other account has antigravity available
1162
+ if (accountManager.hasOtherAccountWithAntigravityAvailable(account.index, family, model)) {
1163
+ // Switch to another account with antigravity (preserve antigravity priority)
1164
+ pushDebug(`antigravity rate-limited on account ${account.index}, but available on other accounts. Switching.`);
1165
+ shouldSwitchAccount = true;
1166
+ }
1167
+ else {
1168
+ // All accounts exhausted antigravity - fall back to gemini-cli on this account
1169
+ const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model);
1170
+ if (alternateStyle && alternateStyle !== headerStyle) {
1171
+ await showToast(`Antigravity quota exhausted on all accounts. Using Gemini CLI quota.`, "warning");
1172
+ headerStyle = alternateStyle;
1173
+ pushDebug(`all-accounts antigravity exhausted, quota fallback: ${headerStyle}`);
1174
+ }
1175
+ else {
1176
+ shouldSwitchAccount = true;
1177
+ }
1178
+ }
1179
+ }
1180
+ else if (config.quota_fallback && !explicitQuota && family === "gemini") {
1181
+ // gemini-cli rate-limited - try alternate style (antigravity) on same account
1070
1182
  const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model);
1071
1183
  if (alternateStyle && alternateStyle !== headerStyle) {
1072
1184
  const quotaName = headerStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
@@ -1098,6 +1210,12 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1098
1210
  lastEndpointIndex = i;
1099
1211
  }
1100
1212
  const currentEndpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i];
1213
+ // Skip sandbox endpoints for Gemini CLI models - they only work with Antigravity quota
1214
+ // Gemini CLI models must use production endpoint (cloudcode-pa.googleapis.com)
1215
+ if (headerStyle === "gemini-cli" && currentEndpoint !== ANTIGRAVITY_ENDPOINT_PROD) {
1216
+ pushDebug(`Skipping sandbox endpoint ${currentEndpoint} for gemini-cli headerStyle`);
1217
+ continue;
1218
+ }
1101
1219
  try {
1102
1220
  const prepared = prepareAntigravityRequest(input, init, accessToken, projectContext.effectiveProjectId, currentEndpoint, headerStyle, forceThinkingRecovery, {
1103
1221
  claudeToolHardening: config.claude_tool_hardening,
@@ -1333,6 +1451,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1333
1451
  account.consecutiveFailures = 0;
1334
1452
  getHealthTracker().recordSuccess(account.index);
1335
1453
  accountManager.markAccountUsed(account.index);
1454
+ void triggerAsyncQuotaRefreshForAccount(accountManager, account.index, client, providerId, config.quota_refresh_interval_minutes);
1336
1455
  }
1337
1456
  logAntigravityDebugResponse(debugContext, response, {
1338
1457
  note: response.ok ? "Success" : `Error ${response.status}`,
@@ -1509,39 +1628,129 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1509
1628
  });
1510
1629
  menuResult = await promptLoginMode(existingAccounts);
1511
1630
  if (menuResult.mode === "check") {
1512
- console.log("\nChecking quotas for all accounts...");
1631
+ console.log("\n📊 Checking quotas for all accounts...\n");
1513
1632
  const results = await checkAccountsQuota(existingStorage.accounts, client, providerId);
1633
+ let storageUpdated = false;
1514
1634
  for (const res of results) {
1515
1635
  const label = res.email || `Account ${res.index + 1}`;
1516
1636
  const disabledStr = res.disabled ? " (disabled)" : "";
1517
- console.log(`\n${res.index + 1}. ${label}${disabledStr}`);
1637
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
1638
+ console.log(` ${label}${disabledStr}`);
1639
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
1518
1640
  if (res.status === "error") {
1519
- console.log(` Error: ${res.error}`);
1641
+ console.log(`Error: ${res.error}\n`);
1520
1642
  continue;
1521
1643
  }
1522
- if (!res.quota || Object.keys(res.quota.groups).length === 0) {
1523
- console.log(" No quota information available.");
1524
- if (res.quota?.error)
1525
- console.log(` Error: ${res.quota.error}`);
1526
- continue;
1527
- }
1528
- const printGrp = (name, group) => {
1529
- if (!group)
1530
- return;
1531
- const remaining = typeof group.remainingFraction === 'number'
1532
- ? `${Math.round(group.remainingFraction * 100)}%`
1533
- : 'UNKNOWN';
1534
- const resetStr = group.resetTime ? `, resets in ${formatWaitTime(Date.parse(group.resetTime) - Date.now())}` : '';
1535
- console.log(` ${name}: ${remaining}${resetStr}`);
1644
+ // ANSI color codes
1645
+ const colors = {
1646
+ red: '\x1b[31m',
1647
+ orange: '\x1b[33m', // Yellow/orange
1648
+ green: '\x1b[32m',
1649
+ reset: '\x1b[0m',
1650
+ };
1651
+ // Get color based on remaining percentage
1652
+ const getColor = (remaining) => {
1653
+ if (typeof remaining !== 'number')
1654
+ return colors.reset;
1655
+ if (remaining < 0.2)
1656
+ return colors.red;
1657
+ if (remaining < 0.6)
1658
+ return colors.orange;
1659
+ return colors.green;
1536
1660
  };
1537
- printGrp("Claude", res.quota.groups.claude);
1538
- printGrp("Gemini 3 Pro", res.quota.groups["gemini-pro"]);
1539
- printGrp("Gemini 3 Flash", res.quota.groups["gemini-flash"]);
1661
+ // Helper to create colored progress bar
1662
+ const createProgressBar = (remaining, width = 20) => {
1663
+ if (typeof remaining !== 'number')
1664
+ return '░'.repeat(width) + ' ???';
1665
+ const filled = Math.round(remaining * width);
1666
+ const empty = width - filled;
1667
+ const color = getColor(remaining);
1668
+ const bar = `${color}${'█'.repeat(filled)}${colors.reset}${'░'.repeat(empty)}`;
1669
+ const pct = `${color}${Math.round(remaining * 100)}%${colors.reset}`.padStart(4 + color.length + colors.reset.length);
1670
+ return `${bar} ${pct}`;
1671
+ };
1672
+ // Helper to format reset time with days support
1673
+ const formatReset = (resetTime) => {
1674
+ if (!resetTime)
1675
+ return '';
1676
+ const ms = Date.parse(resetTime) - Date.now();
1677
+ if (ms <= 0)
1678
+ return ' (resetting...)';
1679
+ const hours = ms / (1000 * 60 * 60);
1680
+ if (hours >= 24) {
1681
+ const days = Math.floor(hours / 24);
1682
+ const remainingHours = Math.floor(hours % 24);
1683
+ if (remainingHours > 0) {
1684
+ return ` (resets in ${days}d ${remainingHours}h)`;
1685
+ }
1686
+ return ` (resets in ${days}d)`;
1687
+ }
1688
+ return ` (resets in ${formatWaitTime(ms)})`;
1689
+ };
1690
+ // Display Gemini CLI Quota first (as requested - swap order)
1691
+ const hasGeminiCli = res.geminiCliQuota && res.geminiCliQuota.models.length > 0;
1692
+ console.log(`\n ┌─ Gemini CLI Quota`);
1693
+ if (!hasGeminiCli) {
1694
+ const errorMsg = res.geminiCliQuota?.error || "No Gemini CLI quota available";
1695
+ console.log(` │ └─ ${errorMsg}`);
1696
+ }
1697
+ else {
1698
+ const models = res.geminiCliQuota.models;
1699
+ models.forEach((model, idx) => {
1700
+ const isLast = idx === models.length - 1;
1701
+ const connector = isLast ? "└─" : "├─";
1702
+ const bar = createProgressBar(model.remainingFraction);
1703
+ const reset = formatReset(model.resetTime);
1704
+ const modelName = model.modelId.padEnd(29);
1705
+ console.log(` │ ${connector} ${modelName} ${bar}${reset}`);
1706
+ });
1707
+ }
1708
+ // Display Antigravity Quota second
1709
+ const hasAntigravity = res.quota && Object.keys(res.quota.groups).length > 0;
1710
+ console.log(` │`);
1711
+ console.log(` └─ Antigravity Quota`);
1712
+ if (!hasAntigravity) {
1713
+ const errorMsg = res.quota?.error || "No quota information available";
1714
+ console.log(` └─ ${errorMsg}`);
1715
+ }
1716
+ else {
1717
+ const groups = res.quota.groups;
1718
+ const groupEntries = [
1719
+ { name: "Claude", data: groups.claude },
1720
+ { name: "Gemini 3 Pro", data: groups["gemini-pro"] },
1721
+ { name: "Gemini 3 Flash", data: groups["gemini-flash"] },
1722
+ ].filter(g => g.data);
1723
+ groupEntries.forEach((g, idx) => {
1724
+ const isLast = idx === groupEntries.length - 1;
1725
+ const connector = isLast ? "└─" : "├─";
1726
+ const bar = createProgressBar(g.data.remainingFraction);
1727
+ const reset = formatReset(g.data.resetTime);
1728
+ const modelName = g.name.padEnd(29);
1729
+ console.log(` ${connector} ${modelName} ${bar}${reset}`);
1730
+ });
1731
+ }
1732
+ console.log("");
1733
+ // Cache quota data for soft quota protection
1734
+ if (res.quota?.groups) {
1735
+ const acc = existingStorage.accounts[res.index];
1736
+ if (acc) {
1737
+ acc.cachedQuota = res.quota.groups;
1738
+ acc.cachedQuotaUpdatedAt = Date.now();
1739
+ storageUpdated = true;
1740
+ }
1741
+ }
1540
1742
  if (res.updatedAccount) {
1541
- existingStorage.accounts[res.index] = res.updatedAccount;
1542
- await saveAccounts(existingStorage);
1743
+ existingStorage.accounts[res.index] = {
1744
+ ...res.updatedAccount,
1745
+ cachedQuota: res.quota?.groups,
1746
+ cachedQuotaUpdatedAt: Date.now(),
1747
+ };
1748
+ storageUpdated = true;
1543
1749
  }
1544
1750
  }
1751
+ if (storageUpdated) {
1752
+ await saveAccounts(existingStorage);
1753
+ }
1545
1754
  console.log("");
1546
1755
  continue;
1547
1756
  }
@@ -1978,10 +2187,10 @@ function getHeaderStyleFromUrl(urlString, family) {
1978
2187
  }
1979
2188
  const modelWithSuffix = extractModelFromUrlWithSuffix(urlString);
1980
2189
  if (!modelWithSuffix) {
1981
- return "gemini-cli";
2190
+ return "antigravity";
1982
2191
  }
1983
2192
  const { quotaPreference } = resolveModelWithTier(modelWithSuffix);
1984
- return quotaPreference ?? "gemini-cli";
2193
+ return quotaPreference === "gemini-cli" ? "antigravity" : (quotaPreference ?? "antigravity");
1985
2194
  }
1986
2195
  function isExplicitQuotaFromUrl(urlString) {
1987
2196
  const modelWithSuffix = extractModelFromUrlWithSuffix(urlString);
@@ -1991,4 +2200,7 @@ function isExplicitQuotaFromUrl(urlString) {
1991
2200
  const { explicitQuota } = resolveModelWithTier(modelWithSuffix);
1992
2201
  return explicitQuota ?? false;
1993
2202
  }
2203
+ export const __testExports = {
2204
+ getHeaderStyleFromUrl,
2205
+ };
1994
2206
  //# sourceMappingURL=plugin.js.map