@hashimkarim/usagestat-linux-x64-gnu 2.0.0-alpha.3 → 2.0.0-alpha.4

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 (142) hide show
  1. package/native-manifest.json +731 -65
  2. package/package.json +112 -1
  3. package/plugins/UPSTREAM-LICENSES.md +6 -2
  4. package/plugins/_provider-icons/LICENSE +21 -0
  5. package/plugins/_provider-icons/NOTICE +12 -0
  6. package/plugins/_provider-icons/licenses/CodexBar-MIT.txt +21 -0
  7. package/plugins/_provider-icons/licenses/LobeHub-MIT.txt +21 -0
  8. package/plugins/_provider-icons/licenses/UsageStat-Bar-MIT.txt +21 -0
  9. package/plugins/_provider-icons/manifest.json +1219 -0
  10. package/plugins/_provider-icons/provenance.json +269 -0
  11. package/plugins/_provider-icons/source.json +4 -0
  12. package/plugins/abacus-ai/icon-color.svg +18 -0
  13. package/plugins/aixy/LICENSE +21 -0
  14. package/plugins/aixy/plugin.js +261 -0
  15. package/plugins/aixy/plugin.json +24 -0
  16. package/plugins/antigravity/plugin.js +136 -46
  17. package/plugins/antigravity-cli/icon-color.svg +1 -0
  18. package/plugins/antigravity-cli/icon.svg +1 -3
  19. package/plugins/antigravity-cli/plugin.js +11 -15
  20. package/plugins/antigravity-ide/icon-color.svg +1 -0
  21. package/plugins/antigravity-ide/icon.svg +1 -3
  22. package/plugins/atlascloud/LICENSE +21 -0
  23. package/plugins/atlascloud/icon-color.svg +1 -0
  24. package/plugins/atlascloud/icon.svg +1 -0
  25. package/plugins/atlascloud/plugin.js +49 -0
  26. package/plugins/atlascloud/plugin.json +23 -0
  27. package/plugins/augment/icon-color.svg +14 -0
  28. package/plugins/bifrost/LICENSE +21 -0
  29. package/plugins/bifrost/plugin.js +361 -0
  30. package/plugins/bifrost/plugin.json +23 -0
  31. package/plugins/claude/plugin.js +302 -152
  32. package/plugins/claude/plugin.json +1 -1
  33. package/plugins/clinepass/plugin.js +19 -3
  34. package/plugins/codebuff/icon-color.svg +92 -0
  35. package/plugins/coderabbit/plugin.js +25 -0
  36. package/plugins/coderabbit/plugin.json +12 -0
  37. package/plugins/codex/plugin.js +1 -0
  38. package/plugins/command-code/icon-color.svg +3 -0
  39. package/plugins/command-code/plugin.js +7 -3
  40. package/plugins/copilot/icon.svg +1 -1
  41. package/plugins/crof/icon-color.svg +3 -0
  42. package/plugins/crofai/icon-color.svg +3 -0
  43. package/plugins/crofai/icon.svg +2 -15
  44. package/plugins/cursor/icon-color.svg +1 -0
  45. package/plugins/deepinfra/icon-color.svg +1 -0
  46. package/plugins/deepinfra/icon.svg +1 -4
  47. package/plugins/deepseek/plugin.js +150 -59
  48. package/plugins/deepseek/plugin.json +3 -1
  49. package/plugins/devin/plugin.js +3 -0
  50. package/plugins/devpass/LICENSE +21 -0
  51. package/plugins/devpass/plugin.js +115 -0
  52. package/plugins/devpass/plugin.json +23 -0
  53. package/plugins/elevenlabs/icon-color.svg +4 -0
  54. package/plugins/factory/icon-color.svg +1 -0
  55. package/plugins/fireworks-ai/icon-color.svg +1 -0
  56. package/plugins/fireworks-ai/icon.svg +1 -6
  57. package/plugins/gemini-apps/LICENSE +22 -0
  58. package/plugins/gemini-apps/icon-color.svg +1 -0
  59. package/plugins/gemini-apps/icon.svg +1 -0
  60. package/plugins/gemini-apps/plugin.js +470 -0
  61. package/plugins/gemini-apps/plugin.json +19 -0
  62. package/plugins/gitkraken/LICENSE +21 -0
  63. package/plugins/gitkraken/plugin.js +153 -0
  64. package/plugins/gitkraken/plugin.json +24 -0
  65. package/plugins/grok/icon-color.svg +1 -0
  66. package/plugins/groqcloud/icon-color.svg +1 -0
  67. package/plugins/helmcode/LICENSE +21 -0
  68. package/plugins/helmcode/plugin.js +188 -0
  69. package/plugins/helmcode/plugin.json +24 -0
  70. package/plugins/huggingface/LICENSE +21 -0
  71. package/plugins/huggingface/icon-color.svg +1 -0
  72. package/plugins/huggingface/icon.svg +1 -0
  73. package/plugins/huggingface/plugin.js +293 -0
  74. package/plugins/huggingface/plugin.json +25 -0
  75. package/plugins/hyper/LICENSE +21 -0
  76. package/plugins/hyper/plugin.js +131 -0
  77. package/plugins/hyper/plugin.json +26 -0
  78. package/plugins/kilo/icon-color.svg +1 -0
  79. package/plugins/litellm/plugin.js +54 -3
  80. package/plugins/llmman/LICENSE +21 -0
  81. package/plugins/llmman/plugin.js +133 -0
  82. package/plugins/llmman/plugin.json +23 -0
  83. package/plugins/longcat/icon-color.svg +1 -0
  84. package/plugins/longcat/icon.svg +1 -4
  85. package/plugins/manus/icon-color.svg +6 -0
  86. package/plugins/mimo/icon-color.svg +4 -0
  87. package/plugins/mimo/icon.svg +4 -1
  88. package/plugins/mistral/plugin.js +110 -5
  89. package/plugins/moonshot/icon-color.svg +1 -0
  90. package/plugins/muse/LICENSE +21 -0
  91. package/plugins/muse/plugin.js +319 -0
  92. package/plugins/muse/plugin.json +26 -0
  93. package/plugins/nous/LICENSE +21 -0
  94. package/plugins/nous/icon-color.svg +1 -0
  95. package/plugins/nous/icon.svg +1 -0
  96. package/plugins/nous/plugin.js +124 -0
  97. package/plugins/nous/plugin.json +24 -0
  98. package/plugins/ollama/icon-color.svg +1 -0
  99. package/plugins/openai-api/icon-color.svg +1 -0
  100. package/plugins/opencode/icon-color.svg +1 -0
  101. package/plugins/opencode-go/icon-color.svg +1 -0
  102. package/plugins/opencode-go/plugin.js +30 -12
  103. package/plugins/openrouter/icon-color.svg +1 -0
  104. package/plugins/pi/plugin.js +113 -0
  105. package/plugins/pi/plugin.json +11 -0
  106. package/plugins/qwencloud/icon-color.svg +1 -0
  107. package/plugins/qwencloud/icon.svg +1 -4
  108. package/plugins/raycast/LICENSE +21 -0
  109. package/plugins/raycast/plugin.js +174 -0
  110. package/plugins/raycast/plugin.json +24 -0
  111. package/plugins/replicate/LICENSE +21 -0
  112. package/plugins/replicate/icon-color.svg +1 -0
  113. package/plugins/replicate/icon.svg +1 -0
  114. package/plugins/replicate/plugin.js +165 -0
  115. package/plugins/replicate/plugin.json +24 -0
  116. package/plugins/synthetic/icon-color.svg +14 -0
  117. package/plugins/typesafe/LICENSE +21 -0
  118. package/plugins/typesafe/plugin.js +225 -0
  119. package/plugins/typesafe/plugin.json +24 -0
  120. package/plugins/v0/LICENSE +21 -0
  121. package/plugins/v0/icon-color.svg +1 -0
  122. package/plugins/v0/icon.svg +1 -0
  123. package/plugins/v0/plugin.js +214 -0
  124. package/plugins/v0/plugin.json +24 -0
  125. package/plugins/venice/plugin.js +50 -1
  126. package/plugins/venice/plugin.json +2 -1
  127. package/plugins/vercel/LICENSE +21 -0
  128. package/plugins/vercel/icon-color.svg +1 -0
  129. package/plugins/vercel/icon.svg +1 -0
  130. package/plugins/vercel/plugin.js +55 -0
  131. package/plugins/vercel/plugin.json +23 -0
  132. package/plugins/windsurf/icon-color.svg +1 -0
  133. package/plugins/xai/icon-color.svg +1 -0
  134. package/plugins/xai/icon.svg +1 -7
  135. package/plugins/xkiro/LICENSE +21 -0
  136. package/plugins/xkiro/plugin.js +99 -0
  137. package/plugins/xkiro/plugin.json +23 -0
  138. package/plugins/zai/icon-color.svg +1 -0
  139. package/plugins/zenmux/icon-color.svg +1 -0
  140. package/plugins/zenmux/icon.svg +1 -3
  141. package/usagestat +0 -0
  142. package/usagestatd +0 -0
@@ -411,6 +411,8 @@
411
411
 
412
412
  const envAccessToken = readEnvText(ctx, "CLAUDE_CODE_OAUTH_TOKEN")
413
413
  const stored = loadStoredCredentials(ctx, !!envAccessToken)
414
+ // setup-token credentials cannot read quotas; prefer the signed-in profile.
415
+ if (stored && stored.oauth && stored.oauth.accessToken && hasProfileScope(stored)) return stored
414
416
  if (!envAccessToken) {
415
417
  return stored
416
418
  }
@@ -594,15 +596,21 @@
594
596
 
595
597
  function fetchUsage(ctx, accessToken) {
596
598
  const oauthConfig = getOauthConfig(ctx)
599
+ let version = "2.1.0"
600
+ try {
601
+ const result = ctx.host.command.run({program: "claude", args: ["--version"], timeoutMs: 3000})
602
+ const match = result.status === 0 && /^\s*(\d+\.\d+\.\d+)(?:\s|$)/.exec(result.stdout || "")
603
+ if (match) version = match[1]
604
+ } catch (_) { /* The OAuth source also works without a local CLI. */ }
597
605
  return ctx.util.request({
598
606
  method: "GET",
599
- url: oauthConfig.usageUrl,
607
+ url: oauthConfig.usageUrl + "?cedar_ember=1&skip_spend=1",
600
608
  headers: {
601
609
  Authorization: "Bearer " + accessToken.trim(),
602
610
  Accept: "application/json",
603
611
  "Content-Type": "application/json",
604
612
  "anthropic-beta": "oauth-2025-04-20",
605
- "User-Agent": "claude-code/2.1.69",
613
+ "User-Agent": "claude-code/" + version,
606
614
  },
607
615
  timeoutMs: 10000,
608
616
  })
@@ -626,6 +634,29 @@
626
634
  return null
627
635
  }
628
636
 
637
+ function livePlanLabel(ctx, creds) {
638
+ const token = creds.oauth.accessToken
639
+ const tokenHash = ctx.host.crypto.sha256(token)
640
+ const cached = readLiveUsageCache(ctx)
641
+ if (cached && cached.profileTokenHash === tokenHash) return cached.livePlan || null
642
+ let plan = null
643
+ try {
644
+ const result = ctx.util.requestJson({method: "GET", url: getOauthConfig(ctx).baseApiUrl + "/api/oauth/profile",
645
+ headers: {Authorization: "Bearer " + token, Accept: "application/json", "anthropic-beta": "oauth-2025-04-20"}, timeoutMs: 3000})
646
+ const org = result.resp.status === 200 && result.json && result.json.organization
647
+ if (org && typeof org === "object") {
648
+ const type = cleanText(org.organization_type || org.organizationType)
649
+ const tier = cleanText(org.rate_limit_tier || org.rateLimitTier) || creds.oauth.rateLimitTier
650
+ if (type) plan = oauthPlanLabel(ctx, {oauth: {subscriptionType: type.replace(/^claude_/, ""), rateLimitTier: tier}})
651
+ }
652
+ } catch (_) {
653
+ ctx.host.log.warn("Claude profile unavailable; retaining credential plan.")
654
+ }
655
+ // Include failed lookups, so a profile outage cannot add a request on every poll.
656
+ writeLiveUsageCache(ctx, {profileTokenHash: tokenHash, livePlan: plan})
657
+ return plan
658
+ }
659
+
629
660
  function fmtRateLimitMinutes(seconds) {
630
661
  if (seconds <= 0) return "now"
631
662
  const mins = Math.ceil(seconds / 60)
@@ -1149,9 +1180,9 @@
1149
1180
 
1150
1181
  function getSessionKey(ctx) {
1151
1182
  const candidates = [
1183
+ providerCookieHeader(ctx),
1152
1184
  readEnvText(ctx, "CLAUDE_AI_SESSION_KEY"),
1153
1185
  readEnvText(ctx, "CLAUDE_WEB_SESSION_KEY"),
1154
- providerCookieHeader(ctx),
1155
1186
  ]
1156
1187
  for (let i = 0; i < candidates.length; i++) {
1157
1188
  const key = extractSessionKey(candidates[i])
@@ -1160,9 +1191,12 @@
1160
1191
  return null
1161
1192
  }
1162
1193
 
1163
- function buildWebHeaders(sessionKey) {
1194
+ function buildWebHeaders(ctx, sessionKey) {
1195
+ const configured = providerCookieHeader(ctx)
1196
+ const cookie = configured && extractSessionKey(configured) === sessionKey && configured.includes("=")
1197
+ ? configured : "sessionKey=" + sessionKey
1164
1198
  return {
1165
- Cookie: "sessionKey=" + sessionKey,
1199
+ Cookie: cookie,
1166
1200
  Accept: "application/json",
1167
1201
  Origin: "https://claude.ai",
1168
1202
  Referer: "https://claude.ai/settings/usage",
@@ -1171,8 +1205,22 @@
1171
1205
  }
1172
1206
  }
1173
1207
 
1208
+ function checkWebResponse(resp) {
1209
+ const headers = resp.headers || {}
1210
+ const challenge = String(headers["cf-mitigated"] || headers["Cf-Mitigated"] || "").toLowerCase() === "challenge"
1211
+ || /<title>\s*just a moment|\/cdn-cgi\/challenge-platform\//i.test(String(resp.bodyText || "").slice(0,65536))
1212
+ if (resp.status === 403 && challenge) {
1213
+ throw "Claude web usage is blocked by a Cloudflare browser challenge. Your Claude login has not been rejected."
1214
+ }
1215
+ if (resp.status === 429) {
1216
+ const error = new Error("Claude web usage updates are temporarily rate-limited, not your message allowance.")
1217
+ error.retryAfterSeconds = parseRetryAfterSeconds(headers)
1218
+ throw error
1219
+ }
1220
+ }
1221
+
1174
1222
  function fetchWebUsage(ctx, sessionKey) {
1175
- const headers = buildWebHeaders(sessionKey)
1223
+ const headers = buildWebHeaders(ctx, sessionKey)
1176
1224
 
1177
1225
  let orgResp
1178
1226
  try {
@@ -1187,6 +1235,7 @@
1187
1235
  throw "Web usage request failed. Check your connection."
1188
1236
  }
1189
1237
 
1238
+ checkWebResponse(orgResp)
1190
1239
  if (orgResp.status === 401 || orgResp.status === 403) {
1191
1240
  throw "Web session invalid or expired. Set CLAUDE_AI_SESSION_KEY to your claude.ai sessionKey cookie."
1192
1241
  }
@@ -1219,6 +1268,7 @@
1219
1268
  }
1220
1269
 
1221
1270
  lastUsageStatus = usageResp.status
1271
+ checkWebResponse(usageResp)
1222
1272
  if (usageResp.status === 401 || usageResp.status === 403) {
1223
1273
  ctx.host.log.info("web mode: usage unavailable for org " + candidateOrgId + " (HTTP " + usageResp.status + ")")
1224
1274
  continue
@@ -1265,33 +1315,46 @@
1265
1315
 
1266
1316
  function loadWebUsageData(ctx, sessionKey, nowMs) {
1267
1317
  const liveCache = readLiveUsageCache(ctx)
1318
+ if (liveCache && Number(liveCache.webRetryAtMs) > nowMs) {
1319
+ throw liveCache.webError || "Claude web usage updates are temporarily unavailable."
1320
+ }
1268
1321
  const cachedLastWebUsageFetchMs = Number(liveCache && liveCache.lastWebUsageFetchMs) || 0
1269
1322
  const effectiveLastWebUsageFetchMs = Math.max(lastWebUsageFetchMs, cachedLastWebUsageFetchMs)
1270
- const cached = cachedWebUsageData || cachedLiveUsageData(liveCache, nowMs)
1323
+ const cached = cachedWebUsageData || (liveCache && liveCache.usageSource === "web" && cachedLiveUsageData(liveCache, nowMs))
1271
1324
  if (cached && nowMs - effectiveLastWebUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1272
1325
  lastWebUsageFetchMs = effectiveLastWebUsageFetchMs
1273
1326
  ctx.host.log.info(
1274
1327
  "web usage fetch skipped: last fetch was " +
1275
1328
  Math.round((nowMs - effectiveLastWebUsageFetchMs) / 1000) + "s ago"
1276
1329
  )
1277
- return { data: cached, plan: null }
1330
+ return { data: cached, plan: liveCache && liveCache.webPlan || null }
1278
1331
  }
1279
1332
 
1280
1333
  lastWebUsageFetchMs = nowMs
1281
- const result = fetchWebUsage(ctx, sessionKey)
1282
- const validLines = []
1283
- addUsageWindowLines(ctx, result.usageData, validLines)
1284
- if (!validLines.length) throw "Web usage response invalid. Try again later."
1334
+ let result
1335
+ try {
1336
+ result = fetchWebUsage(ctx, sessionKey)
1337
+ const validLines = []
1338
+ addUsageWindowLines(ctx, result.usageData, validLines)
1339
+ if (!validLines.length) throw "Web usage response invalid. Try again later."
1340
+ } catch (error) {
1341
+ const delay = error && Number.isFinite(error.retryAfterSeconds) ? error.retryAfterSeconds * 1000 : MIN_USAGE_FETCH_INTERVAL_MS
1342
+ writeLiveUsageCache(ctx, {lastWebUsageFetchMs: nowMs, webRetryAtMs: nowMs + Math.max(MIN_USAGE_FETCH_INTERVAL_MS, delay),
1343
+ webError: error && error.message || String(error)})
1344
+ throw error
1345
+ }
1285
1346
  cachedWebUsageData = result.usageData
1347
+ let plan = null
1348
+ if (result.accountInfo && result.accountInfo.rate_limit_tier) {
1349
+ plan = webTierToPlanLabel(result.accountInfo.rate_limit_tier)
1350
+ }
1286
1351
  writeLiveUsageCache(ctx, {
1287
1352
  usageData: result.usageData,
1353
+ usageSource: "web",
1288
1354
  usageFetchedAtMs: nowMs,
1289
1355
  lastWebUsageFetchMs: nowMs,
1356
+ webRetryAtMs: 0, webError: null, webPlan: plan,
1290
1357
  })
1291
- let plan = null
1292
- if (result.accountInfo && result.accountInfo.rate_limit_tier) {
1293
- plan = webTierToPlanLabel(result.accountInfo.rate_limit_tier)
1294
- }
1295
1358
  return { data: result.usageData, plan: plan }
1296
1359
  }
1297
1360
 
@@ -1439,6 +1502,24 @@
1439
1502
  }))
1440
1503
  }
1441
1504
 
1505
+ const grants = data.cedar_ember
1506
+ if (grants && typeof grants === "object" && !Array.isArray(grants)) {
1507
+ let count = 0
1508
+ const expiries = []
1509
+ if (grants.eligible === true && Array.isArray(grants.grants)) {
1510
+ for (const grant of grants.grants) {
1511
+ if (!grant || !Number.isSafeInteger(grant.resets_left) || grant.resets_left <= 0) continue
1512
+ const end = ctx.util.toIso(grant.ends_at)
1513
+ if (end && Date.parse(end) <= Date.parse(ctx.nowIso)) continue
1514
+ if (!Number.isSafeInteger(count + grant.resets_left)) continue
1515
+ count += grant.resets_left
1516
+ if (end) expiries.push(end + " (" + grant.resets_left + ")")
1517
+ }
1518
+ }
1519
+ lines.push(ctx.line.text({label: "Rate Limit Resets", value: count + " available",
1520
+ subtitle: expiries.length ? "Expires " + expiries.sort().join(", ") : undefined}))
1521
+ }
1522
+
1442
1523
  if (data.extra_usage && data.extra_usage.is_enabled) {
1443
1524
  const used = moneyMajorUnits(data.extra_usage.used_credits)
1444
1525
  const limit = moneyMajorUnits(data.extra_usage.monthly_limit)
@@ -1488,6 +1569,29 @@
1488
1569
 
1489
1570
  // ── probe() ────────────────────────────────────────────────────────────────
1490
1571
 
1572
+ function readNativeQuota(ctx, creds, probe) {
1573
+ // Never borrow the machine's ambient CLI account for a named account or an
1574
+ // inference-only token. The native host also verifies the profile token.
1575
+ if (!creds || creds.source !== "file" || !hasProfileScope(creds)) return null
1576
+ const host = ctx.host.claude
1577
+ const read = host && host[probe ? "probeQuota" : "readQuota"]
1578
+ if (typeof read !== "function") return null
1579
+ try {
1580
+ const raw = read(ctx.host.crypto.sha256(creds.oauth.accessToken), ctx.provider && ctx.provider.workspaceId || null)
1581
+ const result = typeof raw === "string" ? ctx.util.tryParseJson(raw) : raw
1582
+ if (!result || !result.data || !Number.isFinite(result.fetchedAtMs)) return null
1583
+ const age = Date.now() - result.fetchedAtMs
1584
+ if (age < 0 || age > MIN_USAGE_FETCH_INTERVAL_MS) return null
1585
+ if (result.source !== "cli" && result.source !== "cli-cache") return null
1586
+ const valid = []
1587
+ addUsageWindowLines(ctx, result.data, valid)
1588
+ return valid.length ? result : null
1589
+ } catch (e) {
1590
+ ctx.host.log.info("Claude CLI quota unavailable: " + String(e))
1591
+ return null
1592
+ }
1593
+ }
1594
+
1491
1595
  function probe(ctx) {
1492
1596
  const creds = loadCredentials(ctx)
1493
1597
  const sessionKey = getSessionKey(ctx)
@@ -1497,6 +1601,7 @@
1497
1601
  const wantsAuto = sourceMode === "auto"
1498
1602
  const wantsWeb = sourceMode === "web"
1499
1603
  const wantsOAuth = sourceMode === "oauth"
1604
+ const wantsCli = sourceMode === "cli"
1500
1605
  const wantsLocal = sourceMode === "local"
1501
1606
  const wantsApi = sourceMode === "api"
1502
1607
 
@@ -1519,6 +1624,7 @@
1519
1624
  const homePath = getClaudeHomeOverride(ctx)
1520
1625
  let data = null
1521
1626
  let quotaSource = null
1627
+ let quotaFetchedAtMs = null
1522
1628
  let lines = []
1523
1629
  let plan = null
1524
1630
  let rateLimited = false
@@ -1526,149 +1632,194 @@
1526
1632
 
1527
1633
  if (hasOAuth) {
1528
1634
  plan = oauthPlanLabel(ctx, creds)
1635
+ if (liveCache && liveCache.profileTokenHash === ctx.host.crypto.sha256(creds.oauth.accessToken) && liveCache.livePlan) plan = liveCache.livePlan
1529
1636
  }
1530
1637
 
1531
- if (wantsLocal) {
1532
- ctx.host.log.info("local mode requested; skipping live usage fetch")
1533
- } else if (wantsApi) {
1534
- ctx.host.log.info("api mode requested; skipping live quota fetch")
1535
- } else if (hasOAuth && !wantsWeb) {
1536
- quotaSource = "oauth"
1537
- // ── OAuth mode ───────────────────────────────────────────────────────
1538
- let accessToken = creds.oauth.accessToken
1539
- const canFetchLiveUsage = hasProfileScope(creds)
1540
-
1541
- if (canFetchLiveUsage) {
1542
- const cachedData = cachedUsageData || cachedLiveUsageData(liveCache, nowMs)
1543
- const cachedRateLimitedUntilMs = Number(liveCache && liveCache.rateLimitedUntilMs) || 0
1544
- const effectiveRateLimitedUntilMs = Math.max(rateLimitedUntilMs, cachedRateLimitedUntilMs)
1545
-
1546
- if (nowMs < effectiveRateLimitedUntilMs) {
1547
- // Still within a rate-limit window from a previous probe call — skip the
1548
- // API request entirely and surface the remaining wait time to the user.
1638
+ let nativeQuota = null
1639
+ if (wantsAuto || wantsCli) {
1640
+ nativeQuota = readNativeQuota(ctx, creds, false)
1641
+ const cooldownUntil = Math.max(rateLimitedUntilMs, Number(liveCache && liveCache.rateLimitedUntilMs) || 0)
1642
+ const apiFetchedAt = Number(liveCache && liveCache.usageFetchedAtMs) || 0
1643
+ const freshApi = cachedLiveUsageData(liveCache, nowMs) && apiFetchedAt <= nowMs && nowMs - apiFetchedAt < MIN_USAGE_FETCH_INTERVAL_MS
1644
+ if (!nativeQuota && wantsCli && nowMs >= cooldownUntil) {
1645
+ nativeQuota = readNativeQuota(ctx, creds, true)
1646
+ }
1647
+ // Prefer the newer complete observation; passive reads never clear an
1648
+ // HTTP Retry-After deadline or rewrite the REST cache's fetch timestamp.
1649
+ if (nativeQuota && !wantsCli && freshApi && apiFetchedAt > nativeQuota.fetchedAtMs) nativeQuota = null
1650
+ }
1651
+
1652
+ try {
1653
+ if (wantsLocal) {
1654
+ ctx.host.log.info("local mode requested; skipping live usage fetch")
1655
+ } else if (wantsApi) {
1656
+ ctx.host.log.info("api mode requested; skipping live quota fetch")
1657
+ } else if (nativeQuota) {
1658
+ data = nativeQuota.data
1659
+ quotaSource = nativeQuota.source
1660
+ quotaFetchedAtMs = nativeQuota.fetchedAtMs
1661
+ } else if (wantsCli) {
1662
+ const cooldownUntil = Math.max(rateLimitedUntilMs, Number(liveCache && liveCache.rateLimitedUntilMs) || 0)
1663
+ if (nowMs < cooldownUntil) {
1549
1664
  rateLimited = true
1550
- rateLimitedUntilMs = effectiveRateLimitedUntilMs
1551
- retryAfterSeconds = Math.ceil((effectiveRateLimitedUntilMs - nowMs) / 1000)
1552
- data = cachedData
1553
- ctx.host.log.info("usage fetch skipped: rate-limited for " + retryAfterSeconds + "s more")
1665
+ retryAfterSeconds = Math.ceil((cooldownUntil - nowMs) / 1000)
1666
+ data = cachedLiveUsageData(liveCache, nowMs)
1554
1667
  } else {
1555
- // Rate-limit window has expired (or was never set). Check whether we were
1556
- // previously rate-limited so we can bypass the min-interval guard: a short
1557
- // Retry-After (< 5 min) must not be swallowed by the normal poll throttle.
1558
- const wasRateLimited = effectiveRateLimitedUntilMs > 0
1559
- rateLimitedUntilMs = 0
1560
-
1561
- const cachedLastUsageFetchMs = Number(liveCache && liveCache.lastUsageFetchMs) || 0
1562
- const effectiveLastUsageFetchMs = Math.max(lastUsageFetchMs, cachedLastUsageFetchMs)
1563
- if (cachedData && !wasRateLimited && nowMs - effectiveLastUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1564
- // Polled too recently in normal operation — reuse last cached response.
1565
- lastUsageFetchMs = effectiveLastUsageFetchMs
1668
+ throw "Claude CLI returned no fresh quota data. Usage checks may be temporarily unavailable."
1669
+ }
1670
+ } else if (hasOAuth && !wantsWeb) {
1671
+ quotaSource = "oauth"
1672
+ // ── OAuth mode ───────────────────────────────────────────────────────
1673
+ let accessToken = creds.oauth.accessToken
1674
+ const canFetchLiveUsage = hasProfileScope(creds)
1675
+
1676
+ if (canFetchLiveUsage) {
1677
+ const cachedData = cachedUsageData || cachedLiveUsageData(liveCache, nowMs)
1678
+ const cachedRateLimitedUntilMs = Number(liveCache && liveCache.rateLimitedUntilMs) || 0
1679
+ const effectiveRateLimitedUntilMs = Math.max(rateLimitedUntilMs, cachedRateLimitedUntilMs)
1680
+
1681
+ if (nowMs < effectiveRateLimitedUntilMs) {
1682
+ // Still within a rate-limit window from a previous probe call — skip the
1683
+ // API request entirely and surface the remaining wait time to the user.
1684
+ rateLimited = true
1685
+ rateLimitedUntilMs = effectiveRateLimitedUntilMs
1686
+ retryAfterSeconds = Math.ceil((effectiveRateLimitedUntilMs - nowMs) / 1000)
1566
1687
  data = cachedData
1567
- ctx.host.log.info(
1568
- "usage fetch skipped: last fetch was " +
1569
- Math.round((nowMs - effectiveLastUsageFetchMs) / 1000) + "s ago (min interval " +
1570
- MIN_USAGE_FETCH_INTERVAL_MS / 1000 + "s)"
1571
- )
1688
+ ctx.host.log.info("usage fetch skipped: rate-limited for " + retryAfterSeconds + "s more")
1572
1689
  } else {
1573
- // Proactively refresh if token is expired or about to expire
1574
- if (needsRefresh(ctx, creds.oauth, nowMs)) {
1575
- ctx.host.log.info("token needs refresh (expired or expiring soon)")
1576
- const refreshed = refreshToken(ctx, creds)
1577
- if (refreshed) {
1578
- accessToken = refreshed
1579
- } else {
1580
- ctx.host.log.warn("proactive refresh failed, trying with existing token")
1581
- }
1582
- }
1583
-
1584
- lastUsageFetchMs = nowMs
1585
- let resp
1586
- let didRefresh = false
1587
- try {
1588
- resp = ctx.util.retryOnceOnAuth({
1589
- request: (token) => {
1590
- try {
1591
- return fetchUsage(ctx, token || accessToken)
1592
- } catch (e) {
1593
- ctx.host.log.error("usage request exception: " + String(e))
1594
- if (didRefresh) {
1595
- throw "Usage request failed after refresh. Try again."
1596
- }
1597
- throw "Usage request failed. Check your connection."
1598
- }
1599
- },
1600
- refresh: () => {
1601
- ctx.host.log.info("usage returned 401, attempting refresh")
1602
- didRefresh = true
1603
- return refreshToken(ctx, creds)
1604
- },
1605
- })
1606
- } catch (e) {
1607
- if (typeof e === "string") throw e
1608
- ctx.host.log.error("usage request failed: " + String(e))
1609
- throw "Usage request failed. Check your connection."
1610
- }
1611
-
1612
- if (ctx.util.isAuthStatus(resp.status)) {
1613
- ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
1614
- throw "Token expired. Run `claude` to log in again."
1615
- }
1616
-
1617
- if (resp.status === 429) {
1618
- rateLimited = true
1619
- retryAfterSeconds = parseRetryAfterSeconds(resp.headers)
1620
- const backoffMs = retryAfterSeconds !== null
1621
- ? retryAfterSeconds * 1000
1622
- : DEFAULT_RATE_LIMIT_BACKOFF_MS
1623
- rateLimitedUntilMs = nowMs + backoffMs
1690
+ // Rate-limit window has expired (or was never set). Check whether we were
1691
+ // previously rate-limited so we can bypass the min-interval guard: a short
1692
+ // Retry-After (< 5 min) must not be swallowed by the normal poll throttle.
1693
+ const wasRateLimited = effectiveRateLimitedUntilMs > 0
1694
+ rateLimitedUntilMs = 0
1695
+
1696
+ const cachedLastUsageFetchMs = Number(liveCache && liveCache.lastUsageFetchMs) || 0
1697
+ const effectiveLastUsageFetchMs = Math.max(lastUsageFetchMs, cachedLastUsageFetchMs)
1698
+ if (cachedData && !wasRateLimited && nowMs - effectiveLastUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1699
+ // Polled too recently in normal operation — reuse last cached response.
1700
+ lastUsageFetchMs = effectiveLastUsageFetchMs
1624
1701
  data = cachedData
1625
- writeLiveUsageCache(ctx, {
1626
- rateLimitedUntilMs: rateLimitedUntilMs,
1627
- lastUsageFetchMs: nowMs,
1628
- usageData: data,
1629
- usageFetchedAtMs: Number(liveCache && liveCache.usageFetchedAtMs) || null,
1630
- lastRateLimitedAtMs: nowMs,
1631
- })
1632
- ctx.host.log.warn(
1633
- "usage rate limited (429), backing off for " +
1634
- Math.round(backoffMs / 1000) + "s"
1702
+ ctx.host.log.info(
1703
+ "usage fetch skipped: last fetch was " +
1704
+ Math.round((nowMs - effectiveLastUsageFetchMs) / 1000) + "s ago (min interval " +
1705
+ MIN_USAGE_FETCH_INTERVAL_MS / 1000 + "s)"
1635
1706
  )
1636
- } else if (resp.status < 200 || resp.status >= 300) {
1637
- ctx.host.log.error("usage returned error: status=" + resp.status)
1638
- throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
1639
1707
  } else {
1640
- ctx.host.log.info("usage fetch succeeded")
1641
- data = ctx.util.tryParseJson(resp.bodyText)
1642
- const validLines = []
1643
- if (data && typeof data === "object") addUsageWindowLines(ctx, data, validLines)
1644
- if (!validLines.length) {
1645
- throw "Usage response invalid. Try again later."
1708
+ // Proactively refresh if token is expired or about to expire
1709
+ if (needsRefresh(ctx, creds.oauth, nowMs)) {
1710
+ ctx.host.log.info("token needs refresh (expired or expiring soon)")
1711
+ const refreshed = refreshToken(ctx, creds)
1712
+ if (refreshed) {
1713
+ accessToken = refreshed
1714
+ } else {
1715
+ ctx.host.log.warn("proactive refresh failed, trying with existing token")
1716
+ }
1717
+ }
1718
+
1719
+ lastUsageFetchMs = nowMs
1720
+ let resp
1721
+ let didRefresh = false
1722
+ try {
1723
+ resp = ctx.util.retryOnceOnAuth({
1724
+ request: (token) => {
1725
+ try {
1726
+ return fetchUsage(ctx, token || accessToken)
1727
+ } catch (e) {
1728
+ ctx.host.log.error("usage request exception: " + String(e))
1729
+ if (didRefresh) {
1730
+ throw "Usage request failed after refresh. Try again."
1731
+ }
1732
+ throw "Usage request failed. Check your connection."
1733
+ }
1734
+ },
1735
+ refresh: () => {
1736
+ ctx.host.log.info("usage returned 401, attempting refresh")
1737
+ didRefresh = true
1738
+ return refreshToken(ctx, creds)
1739
+ },
1740
+ })
1741
+ } catch (e) {
1742
+ if (typeof e === "string") throw e
1743
+ ctx.host.log.error("usage request failed: " + String(e))
1744
+ throw "Usage request failed. Check your connection."
1745
+ }
1746
+
1747
+ if (ctx.util.isAuthStatus(resp.status)) {
1748
+ ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
1749
+ throw "Token expired. Run `claude` to log in again."
1750
+ }
1751
+
1752
+ const responseData = ctx.util.tryParseJson(resp.bodyText)
1753
+ const inBandRateLimit = resp.status >= 200 && resp.status < 300
1754
+ && responseData && responseData.error && responseData.error.type === "rate_limit_error"
1755
+ if (resp.status === 429 || inBandRateLimit) {
1756
+ rateLimited = true
1757
+ retryAfterSeconds = parseRetryAfterSeconds(resp.headers)
1758
+ const backoffMs = retryAfterSeconds !== null
1759
+ ? retryAfterSeconds * 1000
1760
+ : DEFAULT_RATE_LIMIT_BACKOFF_MS
1761
+ rateLimitedUntilMs = nowMs + backoffMs
1762
+ data = cachedData
1763
+ writeLiveUsageCache(ctx, {
1764
+ rateLimitedUntilMs: rateLimitedUntilMs,
1765
+ lastUsageFetchMs: nowMs,
1766
+ usageData: data,
1767
+ usageFetchedAtMs: Number(liveCache && liveCache.usageFetchedAtMs) || null,
1768
+ lastRateLimitedAtMs: nowMs,
1769
+ })
1770
+ ctx.host.log.warn(
1771
+ "usage rate limited (429), backing off for " +
1772
+ Math.round(backoffMs / 1000) + "s"
1773
+ )
1774
+ } else if (resp.status < 200 || resp.status >= 300) {
1775
+ ctx.host.log.error("usage returned error: status=" + resp.status)
1776
+ throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
1777
+ } else {
1778
+ ctx.host.log.info("usage fetch succeeded")
1779
+ data = responseData
1780
+ const validLines = []
1781
+ if (data && typeof data === "object") addUsageWindowLines(ctx, data, validLines)
1782
+ if (!validLines.length) {
1783
+ throw "Usage response invalid. Try again later."
1784
+ }
1785
+ cachedUsageData = data
1786
+ rateLimitedUntilMs = 0
1787
+ writeLiveUsageCache(ctx, {
1788
+ rateLimitedUntilMs: 0,
1789
+ lastUsageFetchMs: nowMs,
1790
+ usageData: data,
1791
+ usageSource: "oauth",
1792
+ usageFetchedAtMs: nowMs,
1793
+ lastRateLimitedAtMs: null,
1794
+ })
1795
+ plan = livePlanLabel(ctx, creds) || plan
1646
1796
  }
1647
- cachedUsageData = data
1648
- rateLimitedUntilMs = 0
1649
- writeLiveUsageCache(ctx, {
1650
- rateLimitedUntilMs: 0,
1651
- lastUsageFetchMs: nowMs,
1652
- usageData: data,
1653
- usageFetchedAtMs: nowMs,
1654
- lastRateLimitedAtMs: null,
1655
- })
1656
- }
1657
- } // end fetch else-branch
1797
+ } // end fetch else-branch
1798
+ }
1799
+ } else {
1800
+ ctx.host.log.info("skipping live usage fetch for inference-only token")
1801
+ }
1802
+ } else if (sessionKey) {
1803
+ // ── Web mode ─────────────────────────────────────────────────────────
1804
+ const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
1805
+ data = webResult.data
1806
+ quotaSource = "web"
1807
+ if (webResult.plan) {
1808
+ plan = webResult.plan
1658
1809
  }
1659
1810
  } else {
1660
- ctx.host.log.info("skipping live usage fetch for inference-only token")
1661
- }
1662
- } else if (sessionKey) {
1663
- // ── Web mode ─────────────────────────────────────────────────────────
1664
- const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
1665
- data = webResult.data
1666
- quotaSource = "web"
1667
- if (webResult.plan) {
1668
- plan = webResult.plan
1811
+ ctx.host.log.info("auto mode using available Claude usage history")
1669
1812
  }
1670
- } else {
1671
- ctx.host.log.info("auto mode using available Claude usage history")
1813
+ } catch (primaryError) {
1814
+ const cooldownUntil = Math.max(rateLimitedUntilMs, Number(liveCache && liveCache.rateLimitedUntilMs) || 0)
1815
+ const fallback = wantsAuto && nowMs >= cooldownUntil &&
1816
+ (readNativeQuota(ctx, creds, false) || readNativeQuota(ctx, creds, true))
1817
+ if (!fallback) throw primaryError
1818
+ data = fallback.data
1819
+ quotaSource = fallback.source
1820
+ quotaFetchedAtMs = fallback.fetchedAtMs
1821
+ rateLimited = false
1822
+ retryAfterSeconds = null
1672
1823
  }
1673
1824
 
1674
1825
  if (rateLimited && wantsAuto && sessionKey) {
@@ -1738,12 +1889,10 @@
1738
1889
  ? fmtRateLimitMinutes(retryAfterSeconds)
1739
1890
  : null
1740
1891
  const waitText = retryText
1741
- ? "Rate limited, retry in ~" + retryText
1742
- : "Rate limited, try again later"
1892
+ ? "Usage updates paused; retry in ~" + retryText
1893
+ : "Usage updates temporarily unavailable"
1743
1894
  lines.unshift(ctx.line.badge({ label: "Status", text: waitText, color: "#f59e0b" }))
1744
- const noteText = retryText
1745
- ? "Live usage rate limited — retry in ~" + retryText
1746
- : "Live usage rate limited — data may be stale"
1895
+ const noteText = "Anthropic limited usage checks, not your message allowance." + (data ? " Showing the last successful reading." : "")
1747
1896
  lines.push(ctx.line.text({ label: "Note", value: noteText }))
1748
1897
  } else if (lines.length === 0) {
1749
1898
  if (noLiveCredentials && wantsAuto) throw "Not logged in and no local Claude usage found. Run `claude` to authenticate."
@@ -1751,8 +1900,9 @@
1751
1900
  }
1752
1901
 
1753
1902
  const finalCache = readLiveUsageCache(ctx)
1754
- const fetchedAt = data && finalCache && ctx.util.toIso(finalCache.usageFetchedAtMs)
1903
+ const fetchedAt = data && ctx.util.toIso(quotaFetchedAtMs || finalCache && finalCache.usageFetchedAtMs)
1755
1904
  return { plan: plan, lines: lines, fetchedAt: fetchedAt || ctx.nowIso,
1905
+ state: rateLimited ? "failed" : lines.length ? "ready" : "no-data",
1756
1906
  source: data ? (rateLimited ? "cached" : quotaSource) : wantsApi ? "api" : "local" }
1757
1907
  }
1758
1908
 
@@ -10,7 +10,7 @@
10
10
  { "label": "Status", "url": "https://status.anthropic.com/" },
11
11
  { "label": "Console", "url": "https://console.anthropic.com/" }
12
12
  ],
13
- "supportedModes": ["oauth", "web", "local", "api"],
13
+ "supportedModes": ["oauth", "cli", "web", "local", "api"],
14
14
  "autoMode": "oauth",
15
15
  "webUrl": "https://claude.ai",
16
16
  "usageDashboardUrl": "https://claude.ai/settings/usage",
@@ -14,7 +14,23 @@
14
14
  }
15
15
 
16
16
  function apiKey(ctx) {
17
- return clean(ctx.provider && ctx.provider.apiKey) || env(ctx, "CLINEPASS_API_KEY") || env(ctx, "CLINE_API_KEY");
17
+ const provider = ctx.provider || {};
18
+ if (Object.prototype.hasOwnProperty.call(provider, "apiKey")) return clean(provider.apiKey);
19
+ const scoped = provider.instanceId && provider.instanceId !== 'clinepass';
20
+ if (scoped) return null;
21
+ const explicit = env(ctx, "CLINEPASS_API_KEY") || env(ctx, "CLINE_API_KEY");
22
+ if (explicit) return explicit;
23
+ const settings = provider.settings || {};
24
+ const dir = env(ctx, 'CLINE_DATA_DIR') || (env(ctx, 'CLINE_DIR') || ctx.host.fs.homeDir + '/.cline') + '/data';
25
+ const file = clean(settings.authPath) || env(ctx, 'CLINE_PROVIDER_SETTINGS_PATH') || dir + '/settings/providers.json';
26
+ let saved;
27
+ try { saved = JSON.parse(ctx.host.fs.readText(file))?.providers?.cline?.settings; } catch (_) { return null; }
28
+ const access = clean(saved?.auth?.accessToken);
29
+ if (access) {
30
+ ctx.clineAuthSource = 'oauth';
31
+ return access.startsWith('workos:') ? access : 'workos:' + access;
32
+ }
33
+ return clean(saved?.apiKey) || clean(saved?.auth?.apiKey);
18
34
  }
19
35
 
20
36
  function windowFor(ctx, entry) {
@@ -39,7 +55,7 @@
39
55
 
40
56
  function probe(ctx) {
41
57
  var key = apiKey(ctx);
42
- if (!key) throw "ClinePass API key not found. Set CLINEPASS_API_KEY, CLINE_API_KEY, or provider apiKey.";
58
+ if (!key) throw "ClinePass credentials missing. Set an API key or run cline auth to sign in.";
43
59
 
44
60
  var result = ctx.util.requestJson({
45
61
  method: "GET",
@@ -64,7 +80,7 @@
64
80
  }
65
81
  }
66
82
  if (!metrics.length) throw "ClinePass response missing five_hour window.";
67
- return { displayName: "ClinePass", source: "api", plan: "API key", lines: metrics };
83
+ return { displayName: "ClinePass", source: ctx.clineAuthSource || "api", plan: ctx.clineAuthSource ? "Browser" : "API key", lines: metrics };
68
84
  }
69
85
 
70
86
  globalThis.__openusage_plugin = { id: "clinepass", probe: probe };