tokentracker-cli 0.94.0 → 0.95.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 (130) hide show
  1. package/README.de.md +9 -7
  2. package/README.ja.md +8 -6
  3. package/README.ko.md +8 -6
  4. package/README.md +23 -10
  5. package/README.zh-CN.md +8 -6
  6. package/dashboard/dist/assets/{AchievementBadge-DPqwk2zI.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CELHoMQt.js → AchievementsPage-C4WLT8Rl.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DdAC0VCb.js → AchievementsSection-ilvR2ssr.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Dd4I1SpM.js → BadgeDetailModal-CjEeToxa.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-DL2-4mnm.js → BotAnimated-DDf9pBuK.js} +1 -1
  11. package/dashboard/dist/assets/{Card-B_MpSRse.js → Card-DNous84-.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BLZZ4rG6.js → ClawdAnimated-BLMIRntb.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-B-vfdIbH.js → CommandPalette-vVekhoFO.js} +1 -1
  14. package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
  15. package/dashboard/dist/assets/{ConfirmModal-DLnRwUXC.js → ConfirmModal-DFdpcx12.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-FZ8wEN5w.js → Controls-RNhqybmE.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-D2IzPc5o.js → DevicePage-DIhh-YAk.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-BaqQaUUk.js → DialogClose-ZqxYVIzl.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-DrbKjtuS.js → DialogTitle-BiRcejlu.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-GddKDE8i.js → FadeIn-BDBQpeGe.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-DUu-Z5mE.js → HeaderGithubStar-CztIwtKT.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-BmDkY35A.js → HoverTooltip-Cfso7LTC.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-CGJyqWk2.js → IpCheckPage-CNE7dLs3.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-Bd91ALUj.js → LandingPage-B2a3cweM.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-BGGUwXFd.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BMEqHwif.js → LeaderboardPage-DkTHj3fB.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-BSweJqm2.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CRrI3RFz.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-XIRDv46K.js → LimitsPage-BIZ1-6BB.js} +2 -2
  32. package/dashboard/dist/assets/{LocalOnlyNotice-CCD-lGWG.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-lDuqSv4f.js → LoginCard-yXLA9geu.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DStt5VXN.js → LoginModal-BF2Pfkqa.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-dmVPSL72.js → LoginPage-DuOypZap.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-ybcZRctF.js → PetPage-BH3xDVV9.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-Dar_UjxF.js → PopoverPopup-CewF_cGz.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-Ct9tILpz.js → ProviderIcon-Dg0K0RPn.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-B4yLh5zx.js → ResetPasswordPage-CYwt85O3.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-BOE1CEzq.js → SegmentedControl-BJkXXomQ.js} +1 -1
  41. package/dashboard/dist/assets/{Select-D-bSh7Zz.js → Select-B3_WH0Qe.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-DMYjXXq4.js → SelectItemText-BUby_DUu.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-CaleS-6_.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
  44. package/dashboard/dist/assets/SessionsPage-TaC9nahK.js +2 -0
  45. package/dashboard/dist/assets/{SettingsPage-iJH0Yyah.js → SettingsPage-CudcQh6m.js} +2 -2
  46. package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-CuXKoMPg.js → TokenGalaxy-DtYzUsXU.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-Dx5GB7F5.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
  49. package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
  50. package/dashboard/dist/assets/{WidgetsPage-DUaIyGtu.js → WidgetsPage-vzfGccWb.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-BzqlcAUC.js → WrappedPage-DaKuIkYE.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-BHz2N7Ih.css → __vite-browser-external-CCMdjCX2.css} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-Dnxl50k1.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-BdWWxza6.js → agent-logos-B7nzn3n2.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-CANSzmm-.js → arrow-up-right-CsxyvCER.js} +1 -1
  56. package/dashboard/dist/assets/{check-BnJpzXRZ.js → check-hYcC7MdH.js} +1 -1
  57. package/dashboard/dist/assets/{composite-Ccj6KQmh.js → composite-C79LNbex.js} +1 -1
  58. package/dashboard/dist/assets/{copy-C2Yc8Evu.js → copy-Cz4qzE62.js} +1 -1
  59. package/dashboard/dist/assets/{download-B52JTk6Y.js → download-CUaSNE2E.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-DayTwZx-.js → external-link-vq7kIyKK.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B8GEX2yT.js → flame-CB6K4eR6.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-mhkbohkb.js → format-tokens-KI-e_JQ-.js} +1 -1
  63. package/dashboard/dist/assets/{icons-o8bPXqRP.js → icons-3maItkrp.js} +1 -1
  64. package/dashboard/dist/assets/{index-MBVWUKpA.js → index-BRXoZ7Jj.js} +1 -1
  65. package/dashboard/dist/assets/{index-CK_CduLm.js → index-C5WrfmLB.js} +1 -1
  66. package/dashboard/dist/assets/{index-Bp0ZHDtz.js → index-CUbC50Zp.js} +1 -1
  67. package/dashboard/dist/assets/{info-D94QM_vX.js → info-r0yGihUe.js} +1 -1
  68. package/dashboard/dist/assets/{limits-providers-BLzMyCiy.js → limits-providers-BN_94oe9.js} +1 -1
  69. package/dashboard/dist/assets/{link-2-CqBmFaDI.js → link-2-J9t0VXWM.js} +1 -1
  70. package/dashboard/dist/assets/{loader-circle-SaxUpDR6.js → loader-circle-DCvOsPze.js} +1 -1
  71. package/dashboard/dist/assets/{maximize-2-BoXqARks.js → maximize-2-CZ9JhGjf.js} +1 -1
  72. package/dashboard/dist/assets/{modifiers.esm-Conh_-BD.js → modifiers.esm-DJ0_WKg0.js} +1 -1
  73. package/dashboard/dist/assets/module-xpSFyNBK.js +3 -0
  74. package/dashboard/dist/assets/{monitor-smartphone-D6qLR9qd.js → monitor-smartphone-nqbWjJx_.js} +1 -1
  75. package/dashboard/dist/assets/{plus-D51mjIc2.js → plus-Cg7wd2jK.js} +1 -1
  76. package/dashboard/dist/assets/{provider-display-DpHPjd_s.js → provider-display-B9L1nRY9.js} +2 -2
  77. package/dashboard/dist/assets/{react-D_AcI466.js → react-D2AD6Pk2.js} +1 -1
  78. package/dashboard/dist/assets/{search-CwLcNdvZ.js → search-uNiWba1h.js} +1 -1
  79. package/dashboard/dist/assets/{skills-api-Dqi7Mjgo.js → skills-api-BBYuAhiL.js} +1 -1
  80. package/dashboard/dist/assets/{sortable.esm-BPDlUxPb.js → sortable.esm-CzPxc949.js} +1 -1
  81. package/dashboard/dist/assets/{store-ikeRIRGd.js → store-D4A7EUwE.js} +1 -1
  82. package/dashboard/dist/assets/{terminal-BfeX3L5T.js → terminal-Crmm_yKF.js} +1 -1
  83. package/dashboard/dist/assets/{trash-2-5_Qlp1k7.js → trash-2-ia9VjRnU.js} +1 -1
  84. package/dashboard/dist/assets/{use-community-stats-D90Fy2_j.js → use-community-stats-KaGCAxaF.js} +1 -1
  85. package/dashboard/dist/assets/use-latest-request-guard-Dy8gSDkk.js +1 -0
  86. package/dashboard/dist/assets/{use-limits-display-prefs-D4BYFaa-.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
  87. package/dashboard/dist/assets/{use-ordered-list-DJxLNdXY.js → use-ordered-list-C4JnHdE0.js} +1 -1
  88. package/dashboard/dist/assets/use-reduced-motion-AQE0ummK.js +1 -0
  89. package/dashboard/dist/assets/{use-session-efficiency-pref-CAX_GqXc.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
  90. package/dashboard/dist/assets/{use-transform-CeW6Z5R0.js → use-transform-C7-jSJ9k.js} +1 -1
  91. package/dashboard/dist/assets/use-usage-limits-B3YYqn-v.js +1 -0
  92. package/dashboard/dist/assets/{useCurrency-BarGeFyK.js → useCurrency-CK_gcb3J.js} +1 -1
  93. package/dashboard/dist/assets/{useTokenFormat-DUJ9iT9H.js → useTokenFormat-C_a-UkCA.js} +1 -1
  94. package/dashboard/dist/assets/{zap--m0IWoI3.js → zap-BIJNn-Ic.js} +1 -1
  95. package/dashboard/dist/brand-logos/lmstudio.svg +12 -0
  96. package/dashboard/dist/brand-logos/unsloth.svg +1 -0
  97. package/dashboard/dist/index.html +16 -12
  98. package/dashboard/dist/ip-check.html +2 -2
  99. package/dashboard/dist/leaderboard.html +2 -2
  100. package/dashboard/dist/llms.txt +4 -2
  101. package/dashboard/dist/share.html +2 -2
  102. package/package.json +2 -2
  103. package/src/commands/device-login.js +5 -10
  104. package/src/commands/init.js +28 -1
  105. package/src/commands/serve.js +6 -0
  106. package/src/commands/status.js +24 -0
  107. package/src/commands/sync.js +114 -18
  108. package/src/lib/cloud-account.js +102 -29
  109. package/src/lib/fs.js +30 -0
  110. package/src/lib/grok-context-breakdown.js +24 -33
  111. package/src/lib/grok-usage.js +95 -0
  112. package/src/lib/local-api.js +101 -13
  113. package/src/lib/pricing/index.js +15 -0
  114. package/src/lib/pricing/matcher.js +15 -0
  115. package/src/lib/pricing/seed-snapshot.json +1 -1
  116. package/src/lib/rollout.js +1094 -72
  117. package/src/lib/session-analytics.js +100 -40
  118. package/src/lib/usage-limits.js +507 -55
  119. package/src/lib/zcode-limits.js +65 -8
  120. package/dashboard/dist/assets/CommunityStatsModal-aAico565.js +0 -1
  121. package/dashboard/dist/assets/DashboardPage-BByRBsbt.js +0 -1
  122. package/dashboard/dist/assets/DialogDescription-BUrh0Jod.js +0 -1
  123. package/dashboard/dist/assets/SessionsPage-DOaY6_E9.js +0 -2
  124. package/dashboard/dist/assets/SkillsPage-BkzJOBLk.js +0 -1
  125. package/dashboard/dist/assets/TrendMonitor-BuKmsznP.js +0 -19
  126. package/dashboard/dist/assets/__vite-browser-external-CVIGknXl.js +0 -45
  127. package/dashboard/dist/assets/chevron-right-DJm0mbG0.js +0 -1
  128. package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
  129. package/dashboard/dist/assets/use-reduced-motion-yE3T3cat.js +0 -1
  130. package/dashboard/dist/assets/use-usage-limits-5HP8k9a3.js +0 -1
@@ -55,6 +55,38 @@ const DEFAULT_PROVIDER_TIMEOUT_MS = 15_000;
55
55
  const ANTIGRAVITY_LIMITS_CACHE_FILE = "usage-limits-cache.json";
56
56
  const ANTIGRAVITY_LIMITS_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
57
57
  const ANTIGRAVITY_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
58
+ // Public installed-app OAuth client used by Antigravity / agy (same id PokeTokenBar
59
+ // and the agy binary embed). Not confidential — installed-app clients cannot keep a
60
+ // secret. Used to refresh the on-disk Google token so quota can be read like
61
+ // Claude/Codex without the IDE process running.
62
+ const ANTIGRAVITY_OAUTH_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
63
+ const ANTIGRAVITY_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
64
+ const ANTIGRAVITY_LOAD_CODE_ASSIST_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
65
+ // The daily- host is what the Antigravity app uses. The unprefixed host often
66
+ // returns remainingFraction=1 for Gemini buckets (looks like 100% free).
67
+ const ANTIGRAVITY_QUOTA_SUMMARY_URLS = Object.freeze([
68
+ "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
69
+ "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
70
+ ]);
71
+ const ANTIGRAVITY_USER_AGENT = "antigravity/2.9.1";
72
+ const ANTIGRAVITY_TOKEN_REFRESH_SKEW_MS = 60 * 1000;
73
+ // Per-step ceilings for the Antigravity serial chain. `providerTimeoutMs` bounds the
74
+ // whole chain (see fetchAntigravityLimits); these only cap a single step, so one slow
75
+ // call cannot spend a budget that later steps — and the cache fallback — still need.
76
+ // Every step's effective timeout is min(ceiling, remaining budget − guard).
77
+ const ANTIGRAVITY_REMOTE_TIMEOUT_MS = 8000;
78
+ const ANTIGRAVITY_PROCESS_SCAN_TIMEOUT_MS = 4000;
79
+ const ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS = 8000;
80
+ // Reserved tail of the provider budget. Without it the chain can consume the budget
81
+ // right up to the outer race, leaving no time for readAntigravityLimitsCache — the
82
+ // user would get a red error instead of the stale-but-usable bars the cache exists for.
83
+ // Proportional with an absolute cap: a flat reserve larger than the whole budget would
84
+ // skip every step and guarantee the error it exists to prevent.
85
+ const ANTIGRAVITY_BUDGET_GUARD_MS = 1500;
86
+ const ANTIGRAVITY_BUDGET_GUARD_FRACTION = 0.15;
87
+ const ANTIGRAVITY_BUDGET_EXHAUSTED_MESSAGE = "Antigravity quota lookup timed out.";
88
+ const ANTIGRAVITY_AUTH_EXPIRED_MESSAGE = "Not logged in to Antigravity. Launch Antigravity once to authenticate.";
89
+ const ANTIGRAVITY_NOT_RUNNING_MESSAGE = "Antigravity IDE is not running. Launch Antigravity to see usage limits.";
58
90
  // Claude shares its OAuth usage endpoint budget with Claude Code itself, so a transient
59
91
  // 429 is common. Persist the last successful read so the panel can keep showing it instead
60
92
  // of flashing a red error. Separate file from Antigravity's (whose writer rewrites the whole
@@ -2113,7 +2145,12 @@ function parseWindowsProcesses(output) {
2113
2145
  .filter((entry) => Number.isFinite(entry.pid) && entry.command);
2114
2146
  }
2115
2147
 
2116
- async function detectAntigravityProcess({ commandRunner, platform = process.platform } = {}) {
2148
+ async function detectAntigravityProcess({
2149
+ commandRunner,
2150
+ platform = process.platform,
2151
+ timeoutMs = ANTIGRAVITY_PROCESS_SCAN_TIMEOUT_MS,
2152
+ signal,
2153
+ } = {}) {
2117
2154
  let processes;
2118
2155
  if (platform === "win32") {
2119
2156
  const script = [
@@ -2125,12 +2162,13 @@ async function detectAntigravityProcess({ commandRunner, platform = process.plat
2125
2162
  commandRunner,
2126
2163
  "powershell.exe",
2127
2164
  ["-NoProfile", "-NonInteractive", "-Command", script],
2128
- { timeout: 4000 },
2165
+ { timeout: timeoutMs, signal },
2129
2166
  );
2130
2167
  processes = parseWindowsProcesses(result?.stdout);
2131
2168
  } else {
2132
2169
  const result = await runCommand(commandRunner, "/bin/ps", ["-ax", "-o", "pid=,command="], {
2133
- timeout: 4000,
2170
+ timeout: timeoutMs,
2171
+ signal,
2134
2172
  });
2135
2173
  processes = String(result?.stdout || "")
2136
2174
  .split("\n")
@@ -2594,13 +2632,18 @@ function parseWindowsListeningPorts(output, pid) {
2594
2632
  return Array.from(ports).sort((a, b) => a - b);
2595
2633
  }
2596
2634
 
2597
- async function listAntigravityPorts(pid, { commandRunner, platform = process.platform } = {}) {
2635
+ async function listAntigravityPorts(pid, {
2636
+ commandRunner,
2637
+ platform = process.platform,
2638
+ timeoutMs = ANTIGRAVITY_PROCESS_SCAN_TIMEOUT_MS,
2639
+ signal,
2640
+ } = {}) {
2598
2641
  if (platform === "win32") {
2599
2642
  const result = await runCommand(
2600
2643
  commandRunner,
2601
2644
  "netstat.exe",
2602
2645
  ["-ano", "-p", "tcp"],
2603
- { timeout: 4000 },
2646
+ { timeout: timeoutMs, signal },
2604
2647
  );
2605
2648
  const ports = parseWindowsListeningPorts(result?.stdout, pid);
2606
2649
  if (!ports.length) {
@@ -2616,7 +2659,7 @@ async function listAntigravityPorts(pid, { commandRunner, platform = process.pla
2616
2659
  commandRunner,
2617
2660
  lsof,
2618
2661
  ["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", String(pid)],
2619
- { timeout: 4000 },
2662
+ { timeout: timeoutMs, signal },
2620
2663
  );
2621
2664
  const ports = parseListeningPorts(result?.stdout);
2622
2665
  if (!ports.length) {
@@ -2660,11 +2703,12 @@ function requestLocalJson({
2660
2703
  path,
2661
2704
  body,
2662
2705
  csrfToken,
2663
- timeoutMs = 8000,
2706
+ timeoutMs = ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS,
2664
2707
  requestFn,
2708
+ signal,
2665
2709
  }) {
2666
2710
  if (typeof requestFn === "function") {
2667
- return requestFn({ scheme, port, path, body, csrfToken, timeoutMs });
2711
+ return requestFn({ scheme, port, path, body, csrfToken, timeoutMs, signal });
2668
2712
  }
2669
2713
 
2670
2714
  const client = scheme === "https" ? https : http;
@@ -2687,6 +2731,9 @@ function requestLocalJson({
2687
2731
  rejectUnauthorized: false,
2688
2732
  timeout: timeoutMs,
2689
2733
  headers,
2734
+ // Without this the provider race only stops *waiting* — the socket stays
2735
+ // open past the deadline. http.request aborts and emits 'error' instead.
2736
+ signal,
2690
2737
  },
2691
2738
  (res) => {
2692
2739
  let data = "";
@@ -2838,11 +2885,17 @@ function normalizeAntigravityResponse(body, { fallbackToConfigs = false } = {})
2838
2885
  }
2839
2886
 
2840
2887
  function normalizeAntigravityQuotaSummary(body) {
2841
- if (!antigravityCodeIsOk(body?.code)) {
2888
+ if (body?.code !== undefined && body?.code !== null && !antigravityCodeIsOk(body.code)) {
2842
2889
  throw new Error(`Antigravity API error: ${body?.code}`);
2843
2890
  }
2844
2891
 
2845
- const groups = body?.response?.groups;
2892
+ // Local Connect-RPC wraps the payload in `response`; the Cloud Code HTTP
2893
+ // endpoint returns `groups` at the top level.
2894
+ const groups = Array.isArray(body?.response?.groups)
2895
+ ? body.response.groups
2896
+ : Array.isArray(body?.groups)
2897
+ ? body.groups
2898
+ : null;
2846
2899
  if (!Array.isArray(groups) || !groups.length) {
2847
2900
  throw new Error("Could not parse Antigravity quota summary: no groups.");
2848
2901
  }
@@ -2850,7 +2903,10 @@ function normalizeAntigravityQuotaSummary(body) {
2850
2903
  // Map bucketId → window, regardless of which group they live in
2851
2904
  const makeWindow = (remainingFraction, resetTime) => {
2852
2905
  if (typeof remainingFraction !== "number") return null;
2853
- return buildWindow({ usedPercent: 100 - remainingFraction * 100, resetAt: resetTime || null });
2906
+ return buildWindow({
2907
+ usedPercent: Math.round((1 - remainingFraction) * 100),
2908
+ resetAt: parseAntigravityDate(resetTime) || (typeof resetTime === "string" ? resetTime : null),
2909
+ });
2854
2910
  };
2855
2911
 
2856
2912
  // Collect all buckets into a flat bucketId-keyed map
@@ -2884,7 +2940,7 @@ function normalizeAntigravityQuotaSummary(body) {
2884
2940
  };
2885
2941
  }
2886
2942
 
2887
- async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn, scheme = "https" } = {}) {
2943
+ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn, scheme = "https", signal } = {}) {
2888
2944
  try {
2889
2945
  await requestLocalJson({
2890
2946
  scheme,
@@ -2894,6 +2950,7 @@ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn, sch
2894
2950
  csrfToken,
2895
2951
  timeoutMs,
2896
2952
  requestFn,
2953
+ signal,
2897
2954
  });
2898
2955
  return true;
2899
2956
  } catch (_error) {
@@ -2913,7 +2970,343 @@ function hasAntigravityInstallEvidence({ home } = {}) {
2913
2970
  });
2914
2971
  }
2915
2972
 
2916
- async function fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImpl = fetch, timeoutMs = 8000, nowMs = Date.now(), platform = process.platform } = {}) {
2973
+ function listAntigravityCredentialPaths(home) {
2974
+ const geminiHome = path.join(home || os.homedir(), ".gemini");
2975
+ return [
2976
+ path.join(geminiHome, "jetski-standalone-oauth-token"),
2977
+ path.join(geminiHome, "antigravity", "jetski-standalone-oauth-token"),
2978
+ path.join(geminiHome, "antigravity", "antigravity-oauth-token"),
2979
+ path.join(geminiHome, "antigravity-ide", "antigravity-oauth-token"),
2980
+ path.join(geminiHome, "antigravity-cli", "antigravity-oauth-token"),
2981
+ ];
2982
+ }
2983
+
2984
+ function parseAntigravityExpiryMs(value) {
2985
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
2986
+ return value < 1e12 ? value * 1000 : value;
2987
+ }
2988
+ if (typeof value === "string" && value) {
2989
+ const ms = Date.parse(value);
2990
+ if (Number.isFinite(ms) && ms > 0) return ms;
2991
+ }
2992
+ return null;
2993
+ }
2994
+
2995
+ function parseAntigravityCredentialPayload(raw) {
2996
+ if (typeof raw !== "string" || !raw.trim()) return null;
2997
+ let text = raw.trim();
2998
+ if (text.startsWith("go-keyring-base64:")) {
2999
+ try {
3000
+ text = Buffer.from(text.slice("go-keyring-base64:".length), "base64").toString("utf8").trim();
3001
+ } catch {
3002
+ return null;
3003
+ }
3004
+ }
3005
+ let parsed;
3006
+ try {
3007
+ parsed = JSON.parse(text);
3008
+ } catch {
3009
+ return null;
3010
+ }
3011
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3012
+
3013
+ const tokenObj = parsed.token && typeof parsed.token === "object" && !Array.isArray(parsed.token)
3014
+ ? parsed.token
3015
+ : parsed;
3016
+ const accessToken = typeof tokenObj.access_token === "string" && tokenObj.access_token
3017
+ ? tokenObj.access_token
3018
+ : (typeof parsed.token === "string" && parsed.token ? parsed.token : null);
3019
+ if (!accessToken) return null;
3020
+ const refreshToken = typeof tokenObj.refresh_token === "string" && tokenObj.refresh_token
3021
+ ? tokenObj.refresh_token
3022
+ : (typeof parsed.refresh_token === "string" && parsed.refresh_token ? parsed.refresh_token : null);
3023
+ return {
3024
+ accessToken,
3025
+ refreshToken,
3026
+ expiryMs: parseAntigravityExpiryMs(
3027
+ tokenObj.expiry ?? tokenObj.expiry_date ?? parsed.expiry ?? parsed.expiry_date,
3028
+ ),
3029
+ raw: parsed,
3030
+ };
3031
+ }
3032
+
3033
+ function loadAntigravityCredentialsFromFiles({ home } = {}) {
3034
+ for (const credPath of listAntigravityCredentialPaths(home)) {
3035
+ try {
3036
+ const parsed = parseAntigravityCredentialPayload(fs.readFileSync(credPath, "utf8"));
3037
+ if (parsed) {
3038
+ return { ...parsed, source: "file", path: credPath };
3039
+ }
3040
+ } catch {
3041
+ // missing or unreadable
3042
+ }
3043
+ }
3044
+ return null;
3045
+ }
3046
+
3047
+ function readAntigravityKeychainRaw({ securityRunner, timeoutMs = 2000 } = {}) {
3048
+ const runner = typeof securityRunner === "function" ? securityRunner : cp.spawnSync;
3049
+ if (runner === cp.spawnSync) {
3050
+ if (process.platform !== "darwin" || !fs.existsSync(MACOS_SECURITY_BIN)) return null;
3051
+ }
3052
+ try {
3053
+ const result = runner(
3054
+ MACOS_SECURITY_BIN,
3055
+ ["find-generic-password", "-s", "gemini", "-a", "antigravity", "-w"],
3056
+ {
3057
+ encoding: "utf8",
3058
+ timeout: timeoutMs,
3059
+ stdio: ["ignore", "pipe", "ignore"],
3060
+ },
3061
+ );
3062
+ if (!result || result.error || result.status !== 0) return null;
3063
+ const stdout = typeof result.stdout === "string"
3064
+ ? result.stdout
3065
+ : Buffer.isBuffer(result.stdout)
3066
+ ? result.stdout.toString("utf8")
3067
+ : "";
3068
+ const trimmed = stdout.trim();
3069
+ return trimmed || null;
3070
+ } catch {
3071
+ return null;
3072
+ }
3073
+ }
3074
+
3075
+ function loadAntigravityCredentials({ home, platform = process.platform, securityRunner } = {}) {
3076
+ const fromFile = loadAntigravityCredentialsFromFiles({ home });
3077
+ if (fromFile) return fromFile;
3078
+ if (platform !== "darwin" && typeof securityRunner !== "function") return null;
3079
+ const raw = readAntigravityKeychainRaw({ securityRunner });
3080
+ const parsed = parseAntigravityCredentialPayload(raw);
3081
+ if (!parsed) return null;
3082
+ return { ...parsed, source: "keychain", path: null };
3083
+ }
3084
+
3085
+ function persistAntigravityCredentials(creds, next, { nowMs = Date.now() } = {}) {
3086
+ if (creds?.source !== "file" || !creds.path) return;
3087
+ const expiresIn = Number.isFinite(next?.expiresIn) && next.expiresIn > 0 ? next.expiresIn : 3600;
3088
+ const expiry = new Date(nowMs + expiresIn * 1000).toISOString();
3089
+ try {
3090
+ const payload = creds.raw && typeof creds.raw === "object" ? { ...creds.raw } : {};
3091
+ if (payload.token && typeof payload.token === "object") {
3092
+ payload.token = {
3093
+ ...payload.token,
3094
+ access_token: next.accessToken,
3095
+ expiry,
3096
+ };
3097
+ if (next.refreshToken) payload.token.refresh_token = next.refreshToken;
3098
+ } else {
3099
+ payload.access_token = next.accessToken;
3100
+ payload.expiry = expiry;
3101
+ if (next.refreshToken) payload.refresh_token = next.refreshToken;
3102
+ }
3103
+ const tmpPath = `${creds.path}.${process.pid}.tmp`;
3104
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
3105
+ fs.renameSync(tmpPath, creds.path);
3106
+ } catch (_error) {}
3107
+ }
3108
+
3109
+ async function refreshAntigravityAccessToken(refreshToken, { fetchImpl = fetch, signal } = {}) {
3110
+ if (typeof refreshToken !== "string" || !refreshToken) {
3111
+ const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
3112
+ err.code = "AUTH_EXPIRED";
3113
+ throw err;
3114
+ }
3115
+ const res = await fetchImpl(ANTIGRAVITY_OAUTH_TOKEN_URL, {
3116
+ method: "POST",
3117
+ headers: {
3118
+ "Content-Type": "application/x-www-form-urlencoded",
3119
+ Accept: "application/json",
3120
+ },
3121
+ body: new URLSearchParams({
3122
+ client_id: ANTIGRAVITY_OAUTH_CLIENT_ID,
3123
+ grant_type: "refresh_token",
3124
+ refresh_token: refreshToken,
3125
+ }),
3126
+ signal,
3127
+ });
3128
+ if (!res.ok) {
3129
+ const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
3130
+ err.code = "AUTH_EXPIRED";
3131
+ throw err;
3132
+ }
3133
+ const json = await res.json();
3134
+ if (typeof json?.access_token !== "string" || !json.access_token) {
3135
+ throw new Error("Could not parse Antigravity token refresh response");
3136
+ }
3137
+ return {
3138
+ accessToken: json.access_token,
3139
+ refreshToken: typeof json.refresh_token === "string" && json.refresh_token
3140
+ ? json.refresh_token
3141
+ : refreshToken,
3142
+ expiresIn: Number(json.expires_in),
3143
+ };
3144
+ }
3145
+
3146
+ async function resolveAntigravityAccessToken(creds, { fetchImpl = fetch, nowMs = Date.now(), forceRefresh = false, signal } = {}) {
3147
+ const expired = creds.expiryMs != null && creds.expiryMs <= nowMs + ANTIGRAVITY_TOKEN_REFRESH_SKEW_MS;
3148
+ if ((forceRefresh || expired || !creds.accessToken) && creds.refreshToken) {
3149
+ const next = await refreshAntigravityAccessToken(creds.refreshToken, { fetchImpl, signal });
3150
+ persistAntigravityCredentials(creds, next, { nowMs });
3151
+ return next.accessToken;
3152
+ }
3153
+ if (!creds.accessToken) {
3154
+ const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
3155
+ err.code = "AUTH_EXPIRED";
3156
+ throw err;
3157
+ }
3158
+ return creds.accessToken;
3159
+ }
3160
+
3161
+ function postAntigravityCloudCode(fetchImpl, url, accessToken, body, signal) {
3162
+ return fetchImpl(url, {
3163
+ method: "POST",
3164
+ headers: {
3165
+ Authorization: `Bearer ${accessToken}`,
3166
+ "Content-Type": "application/json",
3167
+ Accept: "application/json",
3168
+ "User-Agent": ANTIGRAVITY_USER_AGENT,
3169
+ },
3170
+ body: JSON.stringify(body ?? {}),
3171
+ signal,
3172
+ });
3173
+ }
3174
+
3175
+ async function fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal) {
3176
+ let lastError = null;
3177
+ for (const url of ANTIGRAVITY_QUOTA_SUMMARY_URLS) {
3178
+ try {
3179
+ const res = await postAntigravityCloudCode(fetchImpl, url, accessToken, {}, signal);
3180
+ if (res.status === 401 || res.status === 403) {
3181
+ const err = new Error(ANTIGRAVITY_AUTH_EXPIRED_MESSAGE);
3182
+ err.code = "AUTH_EXPIRED";
3183
+ err.status = res.status;
3184
+ throw err;
3185
+ }
3186
+ if (!res.ok) {
3187
+ lastError = new Error(`Antigravity API error: HTTP ${res.status}`);
3188
+ continue;
3189
+ }
3190
+ return await res.json();
3191
+ } catch (error) {
3192
+ if (error?.code === "AUTH_EXPIRED") throw error;
3193
+ lastError = error;
3194
+ }
3195
+ }
3196
+ throw lastError || new Error("Antigravity quota request failed.");
3197
+ }
3198
+
3199
+ async function fetchAntigravityPlanLabel(fetchImpl, accessToken, signal) {
3200
+ try {
3201
+ const res = await postAntigravityCloudCode(fetchImpl, ANTIGRAVITY_LOAD_CODE_ASSIST_URL, accessToken, {
3202
+ metadata: { ideType: "ANTIGRAVITY" },
3203
+ }, signal);
3204
+ if (!res.ok) return null;
3205
+ const json = await res.json();
3206
+ const paid = json?.paidTier;
3207
+ if (typeof paid?.name === "string" && paid.name.trim()) return paid.name.trim();
3208
+ if (typeof paid?.id === "string" && paid.id.trim()) return paid.id.trim();
3209
+ return null;
3210
+ } catch {
3211
+ return null;
3212
+ }
3213
+ }
3214
+
3215
+ async function fetchAntigravityRemoteLimits({
3216
+ home,
3217
+ platform = process.platform,
3218
+ securityRunner,
3219
+ fetchImpl = fetch,
3220
+ nowMs = Date.now(),
3221
+ signal,
3222
+ } = {}) {
3223
+ const creds = loadAntigravityCredentials({ home, platform, securityRunner });
3224
+ if (!creds) return null;
3225
+
3226
+ const loadWithToken = async (accessToken) => {
3227
+ const payload = await fetchAntigravityQuotaSummaryJson(fetchImpl, accessToken, signal);
3228
+ const windows = normalizeAntigravityQuotaSummary(payload);
3229
+ const accountPlan = windows.account_plan || await fetchAntigravityPlanLabel(fetchImpl, accessToken, signal);
3230
+ return {
3231
+ configured: true,
3232
+ error: null,
3233
+ account_email: windows.account_email || null,
3234
+ account_plan: accountPlan || null,
3235
+ primary_window: windows.primary_window,
3236
+ secondary_window: windows.secondary_window,
3237
+ tertiary_window: windows.tertiary_window,
3238
+ quaternary_window: windows.quaternary_window,
3239
+ };
3240
+ };
3241
+
3242
+ let accessToken = await resolveAntigravityAccessToken(creds, { fetchImpl, nowMs, signal });
3243
+ try {
3244
+ return await loadWithToken(accessToken);
3245
+ } catch (error) {
3246
+ if (error?.code !== "AUTH_EXPIRED" || !creds.refreshToken) throw error;
3247
+ accessToken = await resolveAntigravityAccessToken(creds, { fetchImpl, nowMs, forceRefresh: true, signal });
3248
+ return await loadWithToken(accessToken);
3249
+ }
3250
+ }
3251
+
3252
+ function antigravityUnavailableResult({ home, nowMs, platform, securityRunner, remoteError } = {}) {
3253
+ const cached = readAntigravityLimitsCache({ home, nowMs });
3254
+ if (cached) return cached;
3255
+ const creds = loadAntigravityCredentials({ home, platform, securityRunner });
3256
+ if (!hasAntigravityInstallEvidence({ home }) && !creds) {
3257
+ return { configured: false };
3258
+ }
3259
+ if (remoteError) {
3260
+ const raw = remoteError.message || "Unknown error";
3261
+ const message = raw === "timeout" || /timed out/i.test(raw)
3262
+ ? "Antigravity quota request timed out."
3263
+ : raw;
3264
+ return { configured: true, error: message };
3265
+ }
3266
+ if (creds) {
3267
+ return { configured: true, error: ANTIGRAVITY_AUTH_EXPIRED_MESSAGE };
3268
+ }
3269
+ return { configured: true, error: ANTIGRAVITY_NOT_RUNNING_MESSAGE };
3270
+ }
3271
+
3272
+ /**
3273
+ * `providerTimeoutMs` bounds the WHOLE serial chain — remote quota attempt, process
3274
+ * scan, port scan, port probes and local RPCs — not any single call. Mirrors
3275
+ * fetchArkCodingPlanLimits / the codex remaining-budget pattern: each step's timeout is
3276
+ * clamped to what is left of the budget, and a step whose share has run out is skipped
3277
+ * rather than issued, so the chain can never outrun the outer provider race and the
3278
+ * disk-cache fallback still resolves inside it.
3279
+ *
3280
+ * Before this was budgeted, `timeoutMs` was used as a PER-CALL timeout while the call
3281
+ * site passed it as a TOTAL budget: one 15s budget bought 15s remote + 4s ps + 4s lsof
3282
+ * + 15s per probed port + 15s per local RPC (~83s for a single port).
3283
+ */
3284
+ async function fetchAntigravityLimits({
3285
+ home,
3286
+ commandRunner,
3287
+ requestFn,
3288
+ fetchImpl = fetch,
3289
+ providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
3290
+ nowMs = Date.now(),
3291
+ platform = process.platform,
3292
+ securityRunner,
3293
+ signal,
3294
+ } = {}) {
3295
+ const startedAtMs = performance.now();
3296
+ // min(this step's ceiling, budget left after reserving the fallback guard).
3297
+ // 0 means "no time left" — the caller must skip the call, not issue it.
3298
+ const budgetedTimeoutMs = (stepCeilingMs) => {
3299
+ if (!Number.isFinite(providerTimeoutMs) || providerTimeoutMs <= 0) return stepCeilingMs;
3300
+ const guardMs = Math.min(
3301
+ ANTIGRAVITY_BUDGET_GUARD_MS,
3302
+ providerTimeoutMs * ANTIGRAVITY_BUDGET_GUARD_FRACTION,
3303
+ );
3304
+ const remainingMs = providerTimeoutMs - (performance.now() - startedAtMs);
3305
+ const guardedMs = Math.floor(remainingMs - guardMs);
3306
+ if (guardedMs <= 0) return 0;
3307
+ return Math.min(stepCeilingMs, guardedMs);
3308
+ };
3309
+
2917
3310
  const finalize = (payload, normalizeOptions) => {
2918
3311
  const result = {
2919
3312
  configured: true,
@@ -2934,32 +3327,67 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImp
2934
3327
  return result;
2935
3328
  };
2936
3329
 
3330
+ let remoteError = null;
3331
+ const remoteTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_REMOTE_TIMEOUT_MS);
3332
+ if (remoteTimeoutMs > 0) {
3333
+ try {
3334
+ const remote = await withProviderTimeout(
3335
+ fetchAntigravityRemoteLimits({ home, platform, securityRunner, fetchImpl, nowMs, signal }),
3336
+ "Antigravity",
3337
+ remoteTimeoutMs,
3338
+ );
3339
+ if (remote?.configured && !remote.error && hasAntigravityWindow(remote)) {
3340
+ writeAntigravityLimitsCache(remote, { home, nowMs });
3341
+ return remote;
3342
+ }
3343
+ } catch (error) {
3344
+ remoteError = error;
3345
+ }
3346
+ }
3347
+
2937
3348
  try {
2938
- const processInfo = await detectAntigravityProcess({ commandRunner, platform });
3349
+ const processScanTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_PROCESS_SCAN_TIMEOUT_MS);
3350
+ if (processScanTimeoutMs <= 0) {
3351
+ throw new Error(ANTIGRAVITY_BUDGET_EXHAUSTED_MESSAGE);
3352
+ }
3353
+ const processInfo = await detectAntigravityProcess({
3354
+ commandRunner,
3355
+ platform,
3356
+ timeoutMs: processScanTimeoutMs,
3357
+ signal,
3358
+ });
2939
3359
  if (!processInfo.configured) {
2940
- const cached = readAntigravityLimitsCache({ home, nowMs });
2941
- if (cached) return cached;
2942
- // No install evidence → user likely doesn't have Antigravity at all.
2943
- // Return configured:false so the card stays neutral (like other providers).
2944
- if (!hasAntigravityInstallEvidence({ home })) {
2945
- return { configured: false };
2946
- }
2947
- return { configured: true, error: "Antigravity IDE is not running. Launch Antigravity to see usage limits." };
3360
+ return antigravityUnavailableResult({ home, nowMs, platform, securityRunner, remoteError });
2948
3361
  }
2949
3362
  if (processInfo.error) {
2950
3363
  return { configured: true, error: processInfo.error };
2951
3364
  }
2952
- const ports = await listAntigravityPorts(processInfo.pid, { commandRunner, platform });
3365
+ const portScanTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_PROCESS_SCAN_TIMEOUT_MS);
3366
+ if (portScanTimeoutMs <= 0) {
3367
+ throw new Error(ANTIGRAVITY_BUDGET_EXHAUSTED_MESSAGE);
3368
+ }
3369
+ const ports = await listAntigravityPorts(processInfo.pid, {
3370
+ commandRunner,
3371
+ platform,
3372
+ timeoutMs: portScanTimeoutMs,
3373
+ signal,
3374
+ });
2953
3375
  let workingPort = null;
2954
3376
  let workingScheme = "https";
3377
+ // Each probe draws from the same budget, so a long port list cannot multiply it:
3378
+ // once the guard is reached the loop stops and the cache fallback runs instead.
2955
3379
  for (const port of ports) {
2956
- if (await probeAntigravityPort(port, processInfo.csrfToken, { timeoutMs, requestFn })) {
3380
+ const probeTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS);
3381
+ if (probeTimeoutMs <= 0) break;
3382
+ if (await probeAntigravityPort(port, processInfo.csrfToken, { timeoutMs: probeTimeoutMs, requestFn, signal })) {
2957
3383
  workingPort = port;
2958
3384
  break;
2959
3385
  }
2960
3386
  // agy CLI serves both HTTPS and HTTP; no CSRF needed
2961
3387
  if (!processInfo.csrfToken) {
2962
- if (await probeAntigravityPort(port, null, { timeoutMs, requestFn, scheme: "http" })) {
3388
+ const httpProbeTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS);
3389
+ if (httpProbeTimeoutMs <= 0) break;
3390
+ if (await probeAntigravityPort(port, null, { timeoutMs: httpProbeTimeoutMs, requestFn, scheme: "http", signal })) {
2963
3391
  workingPort = port;
2964
3392
  workingScheme = "http";
2965
3393
  break;
@@ -2970,21 +3398,29 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImp
2970
3398
  throw new Error("Antigravity port detection failed: no working API port found");
2971
3399
  }
2972
3400
 
2973
- try {
2974
- const quotaSummary = await requestLocalJson({
2975
- scheme: workingScheme,
2976
- port: workingPort,
2977
- path: "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary",
2978
- body: antigravityDefaultBody(),
2979
- csrfToken: processInfo.csrfToken,
2980
- timeoutMs,
2981
- requestFn,
2982
- });
2983
- return finalizeQuotaSummary(quotaSummary);
2984
- } catch (_quotaError) {
2985
- // quota summary not available (IDE servers return 404) → fall back to GetUserStatus
3401
+ const quotaTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS);
3402
+ if (quotaTimeoutMs > 0) {
3403
+ try {
3404
+ const quotaSummary = await requestLocalJson({
3405
+ scheme: workingScheme,
3406
+ port: workingPort,
3407
+ path: "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary",
3408
+ body: antigravityDefaultBody(),
3409
+ csrfToken: processInfo.csrfToken,
3410
+ timeoutMs: quotaTimeoutMs,
3411
+ requestFn,
3412
+ signal,
3413
+ });
3414
+ return finalizeQuotaSummary(quotaSummary);
3415
+ } catch (_quotaError) {
3416
+ // quota summary not available (IDE servers return 404) → fall back to GetUserStatus
3417
+ }
2986
3418
  }
2987
3419
 
3420
+ const userStatusTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS);
3421
+ if (userStatusTimeoutMs <= 0) {
3422
+ throw new Error(ANTIGRAVITY_BUDGET_EXHAUSTED_MESSAGE);
3423
+ }
2988
3424
  try {
2989
3425
  const userStatus = await requestLocalJson({
2990
3426
  scheme: workingScheme,
@@ -2992,11 +3428,14 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImp
2992
3428
  path: "/exa.language_server_pb.LanguageServerService/GetUserStatus",
2993
3429
  body: antigravityDefaultBody(),
2994
3430
  csrfToken: processInfo.csrfToken,
2995
- timeoutMs,
3431
+ timeoutMs: userStatusTimeoutMs,
2996
3432
  requestFn,
3433
+ signal,
2997
3434
  });
2998
3435
  return finalize(userStatus);
2999
3436
  } catch (primaryError) {
3437
+ const configsTimeoutMs = budgetedTimeoutMs(ANTIGRAVITY_LOCAL_REQUEST_TIMEOUT_MS);
3438
+ if (configsTimeoutMs <= 0) throw primaryError;
3000
3439
  const fallbackPort =
3001
3440
  Number.isFinite(processInfo.extensionPort) && processInfo.extensionPort > 0
3002
3441
  ? processInfo.extensionPort
@@ -3011,26 +3450,20 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImp
3011
3450
  path: "/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs",
3012
3451
  body: antigravityDefaultBody(),
3013
3452
  csrfToken: processInfo.csrfToken,
3014
- timeoutMs,
3453
+ timeoutMs: configsTimeoutMs,
3015
3454
  requestFn,
3455
+ signal,
3016
3456
  });
3017
3457
  return finalize(modelConfigs, { fallbackToConfigs: true });
3018
3458
  }
3019
3459
  } catch (error) {
3020
- const cached = readAntigravityLimitsCache({ home, nowMs });
3021
- if (cached) return cached;
3022
- // If there's no install evidence, this error is likely from a system that
3023
- // never had Antigravity — return neutral state like other providers.
3024
- if (!hasAntigravityInstallEvidence({ home })) {
3025
- return { configured: false };
3026
- }
3027
- const message = error?.message === "timeout"
3028
- ? "Antigravity quota request timed out."
3029
- : error?.message || "Unknown error";
3030
- return {
3031
- configured: true,
3032
- error: message,
3033
- };
3460
+ return antigravityUnavailableResult({
3461
+ home,
3462
+ nowMs,
3463
+ platform,
3464
+ securityRunner,
3465
+ remoteError: remoteError || error,
3466
+ });
3034
3467
  }
3035
3468
  }
3036
3469
 
@@ -3210,7 +3643,24 @@ async function fetchUsageLimitsUncached({
3210
3643
  withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
3211
3644
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
3212
3645
  fetchKiroLimits({ commandRunner, now, platform, home }),
3213
- fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImpl: providerFetch, nowMs }),
3646
+ // Antigravity's own budget keeps the serial chain inside providerTimeoutMs; this
3647
+ // outer race is the enforcing backstop every other provider already has, and the
3648
+ // signal makes a fired race actually kill the spawned scans and open sockets.
3649
+ withAbortableProviderTimeout(
3650
+ (signal) => fetchAntigravityLimits({
3651
+ home,
3652
+ commandRunner,
3653
+ requestFn,
3654
+ fetchImpl: providerFetch,
3655
+ nowMs,
3656
+ platform,
3657
+ securityRunner,
3658
+ providerTimeoutMs,
3659
+ signal,
3660
+ }),
3661
+ "Antigravity",
3662
+ providerTimeoutMs,
3663
+ ).catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
3214
3664
  withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
3215
3665
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
3216
3666
  withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch }), "Grok Build", providerTimeoutMs)
@@ -3501,6 +3951,8 @@ module.exports = {
3501
3951
  readKiroCreditsSummary,
3502
3952
  fetchKiroLimits,
3503
3953
  normalizeAntigravityResponse,
3954
+ normalizeAntigravityQuotaSummary,
3955
+ loadAntigravityCredentials,
3504
3956
  parseListeningPorts,
3505
3957
  parseWindowsListeningPorts,
3506
3958
  listAntigravityPorts,