tokentracker-cli 0.60.3 → 0.62.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 (47) hide show
  1. package/dashboard/dist/assets/{ActivityHeatmap-cLPCmg1i.js → ActivityHeatmap-DiY-TQE6.js} +1 -1
  2. package/dashboard/dist/assets/{Card-_FGU06_g.js → Card-Dw2Ywh21.js} +1 -1
  3. package/dashboard/dist/assets/DashboardPage-DxFUSPgP.js +19 -0
  4. package/dashboard/dist/assets/{DevicePage-SEcI1kHh.js → DevicePage-BZ6FyruO.js} +1 -1
  5. package/dashboard/dist/assets/{DialogTitle-BXqPt5Ob.js → DialogTitle-By_-gdyZ.js} +1 -1
  6. package/dashboard/dist/assets/{FadeIn-By_JNfKb.js → FadeIn-DbTTDXlD.js} +1 -1
  7. package/dashboard/dist/assets/{HeaderGithubStar-BOfxPmYV.js → HeaderGithubStar-Ddp9MldL.js} +1 -1
  8. package/dashboard/dist/assets/{IpCheckPage-DofM0M8X.js → IpCheckPage-DpRpe-yJ.js} +1 -1
  9. package/dashboard/dist/assets/{LandingPage-Cs-U-Y5I.js → LandingPage-1-X9Dyop.js} +1 -1
  10. package/dashboard/dist/assets/{LeaderboardAvatar-CLLQCHHj.js → LeaderboardAvatar-WLkGqwTq.js} +1 -1
  11. package/dashboard/dist/assets/{LeaderboardPage-Ctco-Kdt.js → LeaderboardPage-hWZKXtzP.js} +3 -3
  12. package/dashboard/dist/assets/{LeaderboardProfileModal-DIu4iePI.js → LeaderboardProfileModal-DCKHaWyv.js} +3 -3
  13. package/dashboard/dist/assets/{LeaderboardProfilePage-BQJbJ4Zk.js → LeaderboardProfilePage-BPYxDyKG.js} +3 -3
  14. package/dashboard/dist/assets/LimitsPage-BBDDxGWY.js +3 -0
  15. package/dashboard/dist/assets/{LocalOnlyNotice-DF1lCLZT.js → LocalOnlyNotice-4EFHus0W.js} +1 -1
  16. package/dashboard/dist/assets/{LoginPage-Br4lL88q.js → LoginPage-D1O2Yp2H.js} +1 -1
  17. package/dashboard/dist/assets/{PopoverPopup-sfd3Ot7l.js → PopoverPopup-BJ1DYzia.js} +1 -1
  18. package/dashboard/dist/assets/{ResetPasswordPage-DPCimfFC.js → ResetPasswordPage-BYTCLfpG.js} +1 -1
  19. package/dashboard/dist/assets/{Select-BTE9IFaB.js → Select-BtUw6eA7.js} +1 -1
  20. package/dashboard/dist/assets/{SelectItemText-fTD-_D4r.js → SelectItemText-uBxS-TJY.js} +1 -1
  21. package/dashboard/dist/assets/SettingsPage-B8lgxPa3.js +1 -0
  22. package/dashboard/dist/assets/{SkillsPage-kLIOS3gm.js → SkillsPage-OalMj85c.js} +1 -1
  23. package/dashboard/dist/assets/WidgetsPage-1qfj_khV.js +1 -0
  24. package/dashboard/dist/assets/{WrappedPage-Bgjnpl0T.js → WrappedPage-DPIgHaEm.js} +1 -1
  25. package/dashboard/dist/assets/{agent-logos-D1CgjfSi.js → agent-logos--zTo3ETT.js} +1 -1
  26. package/dashboard/dist/assets/{arrow-up-right-BCwkxp9i.js → arrow-up-right-Cu9AGOq8.js} +1 -1
  27. package/dashboard/dist/assets/{download-DYn5lZdU.js → download-DfOwf6ht.js} +1 -1
  28. package/dashboard/dist/assets/{info-VYTQzNiq.js → info-B-6dkV8l.js} +1 -1
  29. package/dashboard/dist/assets/{main-ClN5xMKf.js → main-BxMBoQ4o.js} +39 -22
  30. package/dashboard/dist/assets/{main-x6DwHW58.css → main-CJDO0VC9.css} +1 -1
  31. package/dashboard/dist/assets/pencil-D_fU_ajz.js +1 -0
  32. package/dashboard/dist/assets/{use-limits-display-prefs-BZlxCSKp.js → use-limits-display-prefs-CeUpLkAH.js} +1 -1
  33. package/dashboard/dist/assets/{use-native-settings-Cbz2Ycln.js → use-native-settings-k36QjuqG.js} +1 -1
  34. package/dashboard/dist/assets/{use-usage-limits-BYwcAHmE.js → use-usage-limits-DUqHPGiW.js} +1 -1
  35. package/dashboard/dist/assets/{useCurrency-CgeoIGsJ.js → useCurrency-CGfsqycG.js} +1 -1
  36. package/dashboard/dist/assets/{useScrollLock-DqpLDNcl.js → useScrollLock-CPd8fHwH.js} +1 -1
  37. package/dashboard/dist/index.html +2 -2
  38. package/dashboard/dist/share.html +2 -2
  39. package/package.json +1 -1
  40. package/src/lib/opencode-go-limits.js +115 -3
  41. package/src/lib/pricing/seed-snapshot.json +1 -1
  42. package/src/lib/rollout.js +19 -0
  43. package/src/lib/usage-limits.js +212 -77
  44. package/dashboard/dist/assets/DashboardPage-sfNk7IcJ.js +0 -19
  45. package/dashboard/dist/assets/LimitsPage-B6cYrodR.js +0 -3
  46. package/dashboard/dist/assets/SettingsPage-DERfKxU0.js +0 -1
  47. package/dashboard/dist/assets/WidgetsPage-BIaj8VAc.js +0 -1
@@ -8910,6 +8910,9 @@ async function parseAntigravityFile({
8910
8910
  const resumed = canResume && (cachedTokens > 0 || cachedModel !== null);
8911
8911
  const scanStart = resumed ? lastLine : 0;
8912
8912
  let currentModel = resumed ? cachedModel : null;
8913
+ if (!currentModel) {
8914
+ currentModel = await readAntigravityDefaultModel(filePath);
8915
+ }
8913
8916
  let contextTokens = resumed ? cachedTokens : 0;
8914
8917
  // Snapshot of contextTokens at the last PLANNER_RESPONSE we billed for. Only
8915
8918
  // tokens accumulated AFTER that point count as new input on the next planner
@@ -9021,6 +9024,22 @@ async function parseAntigravityFile({
9021
9024
  };
9022
9025
  }
9023
9026
 
9027
+ async function readAntigravityDefaultModel(filePath) {
9028
+ try {
9029
+ // filePath: …/antigravity-cli/brain/<uuid>/.system_generated/logs/transcript.jsonl
9030
+ // Go up 5 levels to reach the variant root (e.g. …/antigravity-cli/)
9031
+ let dir = filePath;
9032
+ for (let i = 0; i < 5; i++) dir = path.dirname(dir);
9033
+ const raw = await fs.readFile(path.join(dir, "settings.json"), "utf8");
9034
+ const settings = JSON.parse(raw);
9035
+ if (settings.model && typeof settings.model === "string") {
9036
+ return normalizeAntigravityTranscriptModel(settings.model);
9037
+ }
9038
+ } catch (_) {
9039
+ }
9040
+ return null;
9041
+ }
9042
+
9024
9043
  function parseAntigravityModelSelection(content) {
9025
9044
  if (typeof content !== "string" || !content) return null;
9026
9045
  const match = content.match(
@@ -1025,8 +1025,18 @@ function normalizeGeminiQuotaResponse({ buckets, email, tier }) {
1025
1025
 
1026
1026
  async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner } = {}) {
1027
1027
  const settings = loadGeminiSettings({ home, env });
1028
+ const credentials = loadGeminiCredentials({ home, env });
1029
+ // Gemini is "configured" only if there are real OAuth credentials OR the
1030
+ // gemini CLI is installed. A bare ~/.gemini/settings.json is not enough:
1031
+ // sibling products (Antigravity) also live under ~/.gemini and would
1032
+ // otherwise surface a spurious "Not logged in to Gemini" card. Do NOT
1033
+ // require the binary when credentials exist — authenticated users on a
1034
+ // minimal launchd PATH have no `gemini` on PATH (issue #224).
1035
+ if (!credentials && !(await isBinaryAvailable("gemini", { commandRunner }))) {
1036
+ return { configured: false };
1037
+ }
1028
1038
  const selectedType = settings?.security?.auth?.selectedType ?? null;
1029
- if (!settings && !loadGeminiCredentials({ home, env })) {
1039
+ if (!settings && !credentials) {
1030
1040
  return { configured: false };
1031
1041
  }
1032
1042
  if (selectedType === "api-key") {
@@ -1036,7 +1046,7 @@ async function fetchGeminiLimits({ home, env, fetchImpl = fetch, commandRunner }
1036
1046
  return { configured: true, error: "Gemini Vertex AI auth not supported. Use Google account (OAuth) instead." };
1037
1047
  }
1038
1048
 
1039
- const creds = loadGeminiCredentials({ home, env });
1049
+ const creds = credentials;
1040
1050
  if (!creds?.access_token) {
1041
1051
  return { configured: true, error: "Not logged in to Gemini. Run 'gemini' in Terminal to authenticate." };
1042
1052
  }
@@ -1612,15 +1622,38 @@ function parseProcessLine(line) {
1612
1622
  };
1613
1623
  }
1614
1624
 
1625
+ function firstCommandToken(command) {
1626
+ const trimmed = String(command || "").trimStart();
1627
+ const quote = trimmed[0];
1628
+ if (quote === '"' || quote === "'") {
1629
+ const end = trimmed.indexOf(quote, 1);
1630
+ return end >= 0 ? trimmed.slice(1, end) : trimmed.slice(1);
1631
+ }
1632
+ return trimmed.split(/\s+/, 1)[0] || "";
1633
+ }
1634
+
1615
1635
  function isAntigravityCommandLine(command) {
1616
- const lower = String(command || "").toLowerCase();
1617
- return lower.includes("language_server")
1618
- && (
1619
- (lower.includes("--app_data_dir") && lower.includes("antigravity"))
1620
- || lower.includes("/antigravity/")
1621
- || lower.includes("\\antigravity\\")
1622
- || lower.includes("--override_ide_name antigravity")
1623
- );
1636
+ const raw = String(command || "");
1637
+ const lower = raw.toLowerCase();
1638
+ const executable = firstCommandToken(raw).split(/[\\/]/).pop() || "";
1639
+
1640
+ // The agy CLI binary itself runs as a server with Connect-RPC endpoints.
1641
+ // Match only the executable token's basename so absolute paths work without
1642
+ // treating arbitrary arguments like `vim /tmp/agy` as an Antigravity server.
1643
+ if (/^agy(?:\.exe)?$/i.test(executable)) return true;
1644
+
1645
+ // IDE language_server — only return true when accompanied by Antigravity-specific
1646
+ // markers so sibling Codeium products (Windsurf, etc.) are not misidentified.
1647
+ const hasLangServerBinary = /^language_server(?:_[a-z0-9]+)*(?:\.exe)?$/i.test(executable);
1648
+
1649
+ const hasAntigravityMarker =
1650
+ (lower.includes("--app_data_dir") && lower.includes("antigravity")) ||
1651
+ lower.includes("/antigravity/") ||
1652
+ lower.includes("/antigravity.app/") ||
1653
+ lower.includes("\\antigravity\\") ||
1654
+ /--override_ide_name(?:=|\s+)["']?antigravity\b/i.test(raw);
1655
+
1656
+ return hasLangServerBinary && hasAntigravityMarker;
1624
1657
  }
1625
1658
 
1626
1659
  function extractCommandFlag(command, flag) {
@@ -1641,8 +1674,7 @@ async function detectAntigravityProcess({ commandRunner } = {}) {
1641
1674
  if (!parsed) continue;
1642
1675
  if (!isAntigravityCommandLine(parsed.command)) continue;
1643
1676
  sawProcess = true;
1644
- const csrfToken = extractCommandFlag(parsed.command, "--csrf_token");
1645
- if (!csrfToken) continue;
1677
+ const csrfToken = extractCommandFlag(parsed.command, "--csrf_token") || null;
1646
1678
  const extensionPort = extractFirstNumber(extractCommandFlag(parsed.command, "--extension_server_port"));
1647
1679
  return {
1648
1680
  configured: true,
@@ -1677,7 +1709,7 @@ function isCacheWindowUsable(window, { cachedAtMs, nowMs } = {}) {
1677
1709
  }
1678
1710
 
1679
1711
  function hasAntigravityWindow(limits) {
1680
- return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window);
1712
+ return Boolean(limits?.primary_window || limits?.secondary_window || limits?.tertiary_window || limits?.quaternary_window);
1681
1713
  }
1682
1714
 
1683
1715
  function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
@@ -1694,6 +1726,7 @@ function normalizeAntigravityCachedLimits(raw, { nowMs = Date.now() } = {}) {
1694
1726
  primary_window: isCacheWindowUsable(raw?.primary_window, { cachedAtMs, nowMs }) ? raw.primary_window : null,
1695
1727
  secondary_window: isCacheWindowUsable(raw?.secondary_window, { cachedAtMs, nowMs }) ? raw.secondary_window : null,
1696
1728
  tertiary_window: isCacheWindowUsable(raw?.tertiary_window, { cachedAtMs, nowMs }) ? raw.tertiary_window : null,
1729
+ quaternary_window: isCacheWindowUsable(raw?.quaternary_window, { cachedAtMs, nowMs }) ? raw.quaternary_window : null,
1697
1730
  cached: true,
1698
1731
  cached_at: raw.cached_at,
1699
1732
  };
@@ -1720,6 +1753,7 @@ function writeAntigravityLimitsCache(limits, { home, nowMs = Date.now() } = {})
1720
1753
  primary_window: limits.primary_window || null,
1721
1754
  secondary_window: limits.secondary_window || null,
1722
1755
  tertiary_window: limits.tertiary_window || null,
1756
+ quaternary_window: limits.quaternary_window || null,
1723
1757
  cached_at: new Date(nowMs).toISOString(),
1724
1758
  },
1725
1759
  };
@@ -1935,6 +1969,14 @@ function requestLocalJson({
1935
1969
  const client = scheme === "https" ? https : http;
1936
1970
  return new Promise((resolve, reject) => {
1937
1971
  const rawBody = JSON.stringify(body);
1972
+ const headers = {
1973
+ "Content-Type": "application/json",
1974
+ "Content-Length": Buffer.byteLength(rawBody),
1975
+ "Connect-Protocol-Version": "1",
1976
+ };
1977
+ if (csrfToken) {
1978
+ headers["X-Codeium-Csrf-Token"] = csrfToken;
1979
+ }
1938
1980
  const req = client.request(
1939
1981
  {
1940
1982
  hostname: "127.0.0.1",
@@ -1943,12 +1985,7 @@ function requestLocalJson({
1943
1985
  method: "POST",
1944
1986
  rejectUnauthorized: false,
1945
1987
  timeout: timeoutMs,
1946
- headers: {
1947
- "Content-Type": "application/json",
1948
- "Content-Length": Buffer.byteLength(rawBody),
1949
- "Connect-Protocol-Version": "1",
1950
- "X-Codeium-Csrf-Token": csrfToken,
1951
- },
1988
+ headers,
1952
1989
  },
1953
1990
  (res) => {
1954
1991
  let data = "";
@@ -2039,36 +2076,6 @@ function antigravityPriority(model) {
2039
2076
  return 0;
2040
2077
  }
2041
2078
 
2042
- function chooseAntigravityModel(models, family) {
2043
- const candidates = models
2044
- .filter((model) => antigravityFamily(model) === family)
2045
- .map((model) => ({
2046
- model,
2047
- priority: antigravityPriority(model),
2048
- remaining:
2049
- typeof model.remaining_fraction === "number"
2050
- ? model.remaining_fraction
2051
- : Number.POSITIVE_INFINITY,
2052
- }))
2053
- .filter((entry) => entry.priority !== null);
2054
-
2055
- if (!candidates.length) return null;
2056
- candidates.sort((a, b) => {
2057
- if (a.priority !== b.priority) return a.priority - b.priority;
2058
- return a.remaining - b.remaining;
2059
- });
2060
- return candidates[0].model;
2061
- }
2062
-
2063
- function makeAntigravityWindow(model) {
2064
- if (!model) return null;
2065
- const remaining = typeof model.remaining_fraction === "number" ? model.remaining_fraction * 100 : 0;
2066
- return buildWindow({
2067
- usedPercent: 100 - remaining,
2068
- resetAt: model.reset_at,
2069
- });
2070
- }
2071
-
2072
2079
  function normalizeAntigravityResponse(body, { fallbackToConfigs = false } = {}) {
2073
2080
  if (!antigravityCodeIsOk(body?.code)) {
2074
2081
  throw new Error(`Antigravity API error: ${body?.code}`);
@@ -2078,21 +2085,40 @@ function normalizeAntigravityResponse(body, { fallbackToConfigs = false } = {})
2078
2085
  const configs = fallbackToConfigs
2079
2086
  ? body?.clientModelConfigs
2080
2087
  : userStatus?.cascadeModelConfigData?.clientModelConfigs;
2081
- const models = parseAntigravityModelConfigs(configs);
2082
- if (!models.length) {
2088
+ const allModels = parseAntigravityModelConfigs(configs);
2089
+ if (!allModels.length) {
2083
2090
  throw new Error("Could not parse Antigravity quota: no quota models available.");
2084
2091
  }
2085
2092
 
2086
- const primary = chooseAntigravityModel(models, "claude");
2087
- const secondary = chooseAntigravityModel(models, "gemini_pro");
2088
- const tertiary = chooseAntigravityModel(models, "gemini_flash");
2089
- const fallback = !primary && !secondary && !tertiary
2090
- ? [...models].sort((a, b) => {
2091
- const aRemaining = typeof a.remaining_fraction === "number" ? a.remaining_fraction : Number.POSITIVE_INFINITY;
2092
- const bRemaining = typeof b.remaining_fraction === "number" ? b.remaining_fraction : Number.POSITIVE_INFINITY;
2093
- return aRemaining - bRemaining;
2094
- })[0]
2095
- : null;
2093
+ // Keep only chat models (skip autocomplete/lite/tab models)
2094
+ const chatModels = allModels.filter((m) => antigravityPriority(m) !== null);
2095
+ const models = chatModels.length ? chatModels : allModels;
2096
+
2097
+ // Group chat models and pick the most-used (lowest remaining weekly quota)
2098
+ // and least-used (highest remaining 5h rolling quota) per family.
2099
+ const claudeModels = models.filter((m) => antigravityFamily(m) === "claude");
2100
+ const geminiModels = models.filter(
2101
+ (m) => antigravityFamily(m) === "gemini_pro" || antigravityFamily(m) === "gemini_flash",
2102
+ );
2103
+
2104
+ const pickMin = (list) => {
2105
+ const sorted = [...list].filter((m) => typeof m.remaining_fraction === "number");
2106
+ if (!sorted.length) return list[0] || null;
2107
+ sorted.sort((a, b) => a.remaining_fraction - b.remaining_fraction);
2108
+ return sorted[0];
2109
+ };
2110
+ const pickMax = (list) => {
2111
+ const sorted = [...list].filter((m) => typeof m.remaining_fraction === "number");
2112
+ if (!sorted.length) return list[list.length - 1] || null;
2113
+ sorted.sort((a, b) => b.remaining_fraction - a.remaining_fraction);
2114
+ return sorted[0];
2115
+ };
2116
+
2117
+ const makeWindow = (model) => {
2118
+ if (!model) return null;
2119
+ const remaining = typeof model.remaining_fraction === "number" ? model.remaining_fraction * 100 : 0;
2120
+ return buildWindow({ usedPercent: 100 - remaining, resetAt: model.reset_at });
2121
+ };
2096
2122
 
2097
2123
  return {
2098
2124
  account_email: typeof userStatus?.email === "string" ? userStatus.email : null,
@@ -2103,16 +2129,64 @@ function normalizeAntigravityResponse(body, { fallbackToConfigs = false } = {})
2103
2129
  || userStatus?.planStatus?.planInfo?.planName
2104
2130
  || userStatus?.planStatus?.planInfo?.planShortName
2105
2131
  || null,
2106
- primary_window: makeAntigravityWindow(primary || fallback),
2107
- secondary_window: makeAntigravityWindow(secondary),
2108
- tertiary_window: makeAntigravityWindow(tertiary),
2132
+ primary_window: makeWindow(claudeModels.length ? pickMin(claudeModels) : pickMin(models)),
2133
+ secondary_window: makeWindow(claudeModels.length ? pickMax(claudeModels) : null),
2134
+ tertiary_window: makeWindow(geminiModels.length ? pickMin(geminiModels) : pickMin(models)),
2135
+ quaternary_window: makeWindow(geminiModels.length ? pickMax(geminiModels) : null),
2136
+ };
2137
+ }
2138
+
2139
+ function normalizeAntigravityQuotaSummary(body) {
2140
+ if (!antigravityCodeIsOk(body?.code)) {
2141
+ throw new Error(`Antigravity API error: ${body?.code}`);
2142
+ }
2143
+
2144
+ const groups = body?.response?.groups;
2145
+ if (!Array.isArray(groups) || !groups.length) {
2146
+ throw new Error("Could not parse Antigravity quota summary: no groups.");
2147
+ }
2148
+
2149
+ // Map bucketId → window, regardless of which group they live in
2150
+ const makeWindow = (remainingFraction, resetTime) => {
2151
+ if (typeof remainingFraction !== "number") return null;
2152
+ return buildWindow({ usedPercent: 100 - remainingFraction * 100, resetAt: resetTime || null });
2153
+ };
2154
+
2155
+ // Collect all buckets into a flat bucketId-keyed map
2156
+ const buckets = {};
2157
+ for (const group of groups) {
2158
+ if (!Array.isArray(group.buckets)) continue;
2159
+ for (const b of group.buckets) {
2160
+ buckets[b.bucketId] = b;
2161
+ }
2162
+ }
2163
+
2164
+ const windows = {
2165
+ primary_window: makeWindow(buckets["3p-weekly"]?.remainingFraction, buckets["3p-weekly"]?.resetTime),
2166
+ secondary_window: makeWindow(buckets["3p-5h"]?.remainingFraction, buckets["3p-5h"]?.resetTime),
2167
+ tertiary_window: makeWindow(buckets["gemini-weekly"]?.remainingFraction, buckets["gemini-weekly"]?.resetTime),
2168
+ quaternary_window: makeWindow(buckets["gemini-5h"]?.remainingFraction, buckets["gemini-5h"]?.resetTime),
2169
+ };
2170
+
2171
+ // If the groups parsed but none of the known bucketIds matched (upstream
2172
+ // renamed them), treat it as a parse failure so the caller falls back to
2173
+ // GetUserStatus rather than rendering an empty, error-free card.
2174
+ if (!windows.primary_window && !windows.secondary_window
2175
+ && !windows.tertiary_window && !windows.quaternary_window) {
2176
+ throw new Error("Could not parse Antigravity quota summary: no known buckets matched.");
2177
+ }
2178
+
2179
+ return {
2180
+ account_email: null, // quota summary doesn't include email
2181
+ account_plan: null, // quota summary doesn't include plan info
2182
+ ...windows,
2109
2183
  };
2110
2184
  }
2111
2185
 
2112
- async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn } = {}) {
2186
+ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn, scheme = "https" } = {}) {
2113
2187
  try {
2114
2188
  await requestLocalJson({
2115
- scheme: "https",
2189
+ scheme,
2116
2190
  port,
2117
2191
  path: "/exa.language_server_pb.LanguageServerService/GetUnleashData",
2118
2192
  body: antigravityUnleashBody(),
@@ -2126,15 +2200,19 @@ async function probeAntigravityPort(port, csrfToken, { timeoutMs, requestFn } =
2126
2200
  }
2127
2201
  }
2128
2202
 
2129
- async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutMs = 8000, nowMs = Date.now() } = {}) {
2130
- const processInfo = await detectAntigravityProcess({ commandRunner });
2131
- if (!processInfo.configured) {
2132
- return readAntigravityLimitsCache({ home, nowMs }) || { configured: false };
2133
- }
2134
- if (processInfo.error) {
2135
- return { configured: true, error: processInfo.error };
2136
- }
2203
+ function hasAntigravityInstallEvidence({ home } = {}) {
2204
+ const geminiHome = path.join(home || os.homedir(), ".gemini");
2205
+ return ["antigravity", "antigravity-ide", "antigravity-cli"]
2206
+ .some((name) => {
2207
+ try {
2208
+ return fs.statSync(path.join(geminiHome, name)).isDirectory();
2209
+ } catch {
2210
+ return false;
2211
+ }
2212
+ });
2213
+ }
2137
2214
 
2215
+ async function fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImpl = fetch, timeoutMs = 8000, nowMs = Date.now() } = {}) {
2138
2216
  const finalize = (payload, normalizeOptions) => {
2139
2217
  const result = {
2140
2218
  configured: true,
@@ -2145,22 +2223,70 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutM
2145
2223
  return result;
2146
2224
  };
2147
2225
 
2226
+ const finalizeQuotaSummary = (payload) => {
2227
+ const result = {
2228
+ configured: true,
2229
+ error: null,
2230
+ ...normalizeAntigravityQuotaSummary(payload),
2231
+ };
2232
+ writeAntigravityLimitsCache(result, { home, nowMs });
2233
+ return result;
2234
+ };
2235
+
2148
2236
  try {
2237
+ const processInfo = await detectAntigravityProcess({ commandRunner });
2238
+ if (!processInfo.configured) {
2239
+ const cached = readAntigravityLimitsCache({ home, nowMs });
2240
+ if (cached) return cached;
2241
+ // No install evidence → user likely doesn't have Antigravity at all.
2242
+ // Return configured:false so the card stays neutral (like other providers).
2243
+ if (!hasAntigravityInstallEvidence({ home })) {
2244
+ return { configured: false };
2245
+ }
2246
+ return { configured: true, error: "Antigravity IDE is not running. Launch Antigravity to see usage limits." };
2247
+ }
2248
+ if (processInfo.error) {
2249
+ return { configured: true, error: processInfo.error };
2250
+ }
2149
2251
  const ports = await listAntigravityPorts(processInfo.pid, { commandRunner });
2150
2252
  let workingPort = null;
2253
+ let workingScheme = "https";
2151
2254
  for (const port of ports) {
2152
2255
  if (await probeAntigravityPort(port, processInfo.csrfToken, { timeoutMs, requestFn })) {
2153
2256
  workingPort = port;
2154
2257
  break;
2155
2258
  }
2259
+ // agy CLI serves both HTTPS and HTTP; no CSRF needed
2260
+ if (!processInfo.csrfToken) {
2261
+ if (await probeAntigravityPort(port, null, { timeoutMs, requestFn, scheme: "http" })) {
2262
+ workingPort = port;
2263
+ workingScheme = "http";
2264
+ break;
2265
+ }
2266
+ }
2156
2267
  }
2157
2268
  if (!workingPort) {
2158
2269
  throw new Error("Antigravity port detection failed: no working API port found");
2159
2270
  }
2160
2271
 
2272
+ try {
2273
+ const quotaSummary = await requestLocalJson({
2274
+ scheme: workingScheme,
2275
+ port: workingPort,
2276
+ path: "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary",
2277
+ body: antigravityDefaultBody(),
2278
+ csrfToken: processInfo.csrfToken,
2279
+ timeoutMs,
2280
+ requestFn,
2281
+ });
2282
+ return finalizeQuotaSummary(quotaSummary);
2283
+ } catch (_quotaError) {
2284
+ // quota summary not available (IDE servers return 404) → fall back to GetUserStatus
2285
+ }
2286
+
2161
2287
  try {
2162
2288
  const userStatus = await requestLocalJson({
2163
- scheme: "https",
2289
+ scheme: workingScheme,
2164
2290
  port: workingPort,
2165
2291
  path: "/exa.language_server_pb.LanguageServerService/GetUserStatus",
2166
2292
  body: antigravityDefaultBody(),
@@ -2174,8 +2300,12 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutM
2174
2300
  Number.isFinite(processInfo.extensionPort) && processInfo.extensionPort > 0
2175
2301
  ? processInfo.extensionPort
2176
2302
  : workingPort;
2303
+ // agy has no extension server port; try HTTP on the same port
2304
+ const fallbackScheme = !processInfo.csrfToken && fallbackPort === workingPort
2305
+ ? (workingScheme === "https" ? "http" : "https")
2306
+ : (fallbackPort === workingPort ? "https" : "http");
2177
2307
  const modelConfigs = await requestLocalJson({
2178
- scheme: fallbackPort === workingPort ? "https" : "http",
2308
+ scheme: fallbackScheme,
2179
2309
  port: fallbackPort,
2180
2310
  path: "/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs",
2181
2311
  body: antigravityDefaultBody(),
@@ -2188,6 +2318,11 @@ async function fetchAntigravityLimits({ home, commandRunner, requestFn, timeoutM
2188
2318
  } catch (error) {
2189
2319
  const cached = readAntigravityLimitsCache({ home, nowMs });
2190
2320
  if (cached) return cached;
2321
+ // If there's no install evidence, this error is likely from a system that
2322
+ // never had Antigravity — return neutral state like other providers.
2323
+ if (!hasAntigravityInstallEvidence({ home })) {
2324
+ return { configured: false };
2325
+ }
2191
2326
  const message = error?.message === "timeout"
2192
2327
  ? "Antigravity quota request timed out."
2193
2328
  : error?.message || "Unknown error";
@@ -2335,7 +2470,7 @@ async function fetchUsageLimitsUncached({
2335
2470
  withProviderTimeout(fetchGeminiLimits({ home, env, fetchImpl: providerFetch, commandRunner }), "Gemini", providerTimeoutMs)
2336
2471
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2337
2472
  fetchKiroLimits({ commandRunner, now }),
2338
- fetchAntigravityLimits({ home, commandRunner, requestFn, nowMs }),
2473
+ fetchAntigravityLimits({ home, commandRunner, requestFn, fetchImpl: providerFetch, nowMs }),
2339
2474
  withProviderTimeout(fetchCopilotLimits({ home, env, fetchImpl: providerFetch, platform, securityRunner }), "GitHub Copilot", providerTimeoutMs)
2340
2475
  .catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
2341
2476
  withProviderTimeout(fetchGrokLimits({ home, env, fetchImpl: providerFetch }), "Grok Build", providerTimeoutMs)