@hashimkarim/usagestat-linux-x64-gnu 2.0.0-alpha.1 → 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 (384) hide show
  1. package/LICENSE +0 -0
  2. package/native-manifest.json +1088 -134
  3. package/package.json +160 -1
  4. package/plugins/UPSTREAM-LICENSES.md +113 -0
  5. package/plugins/_provider-icons/LICENSE +21 -0
  6. package/plugins/_provider-icons/NOTICE +12 -0
  7. package/plugins/_provider-icons/licenses/CodexBar-MIT.txt +21 -0
  8. package/plugins/_provider-icons/licenses/LobeHub-MIT.txt +21 -0
  9. package/plugins/_provider-icons/licenses/UsageStat-Bar-MIT.txt +21 -0
  10. package/plugins/_provider-icons/manifest.json +1219 -0
  11. package/plugins/_provider-icons/provenance.json +269 -0
  12. package/plugins/_provider-icons/source.json +4 -0
  13. package/plugins/abacus-ai/icon-color.svg +18 -0
  14. package/plugins/abacus-ai/icon.svg +0 -0
  15. package/plugins/abacus-ai/plugin.js +0 -0
  16. package/plugins/abacus-ai/plugin.json +0 -0
  17. package/plugins/aiand/icon.svg +6 -0
  18. package/plugins/aiand/plugin.js +108 -0
  19. package/plugins/aiand/plugin.json +22 -0
  20. package/plugins/aixy/LICENSE +21 -0
  21. package/plugins/aixy/plugin.js +261 -0
  22. package/plugins/aixy/plugin.json +24 -0
  23. package/plugins/alibaba/icon-color.svg +0 -0
  24. package/plugins/alibaba/icon.svg +0 -0
  25. package/plugins/alibaba/plugin.js +0 -0
  26. package/plugins/alibaba/plugin.json +0 -0
  27. package/plugins/alibaba-token-plan/icon-color.svg +0 -0
  28. package/plugins/alibaba-token-plan/icon.svg +0 -0
  29. package/plugins/alibaba-token-plan/plugin.js +0 -0
  30. package/plugins/alibaba-token-plan/plugin.json +0 -0
  31. package/plugins/amp/icon-color.svg +0 -0
  32. package/plugins/amp/icon-conflict.svg +0 -0
  33. package/plugins/amp/icon.svg +0 -0
  34. package/plugins/amp/plugin.js +0 -0
  35. package/plugins/amp/plugin.json +0 -0
  36. package/plugins/antigravity/icon-color.svg +0 -0
  37. package/plugins/antigravity/icon-conflict.svg +0 -0
  38. package/plugins/antigravity/icon.svg +0 -0
  39. package/plugins/antigravity/plugin.js +136 -46
  40. package/plugins/antigravity/plugin.json +0 -0
  41. package/plugins/antigravity-cli/icon-color.svg +1 -0
  42. package/plugins/antigravity-cli/icon.svg +1 -3
  43. package/plugins/antigravity-cli/plugin.js +11 -15
  44. package/plugins/antigravity-cli/plugin.json +0 -0
  45. package/plugins/antigravity-ide/icon-color.svg +1 -0
  46. package/plugins/antigravity-ide/icon.svg +1 -3
  47. package/plugins/antigravity-ide/plugin.js +0 -0
  48. package/plugins/antigravity-ide/plugin.json +0 -0
  49. package/plugins/atlascloud/LICENSE +21 -0
  50. package/plugins/atlascloud/icon-color.svg +1 -0
  51. package/plugins/atlascloud/icon.svg +1 -0
  52. package/plugins/atlascloud/plugin.js +49 -0
  53. package/plugins/atlascloud/plugin.json +23 -0
  54. package/plugins/augment/icon-color.svg +14 -0
  55. package/plugins/augment/icon.svg +0 -0
  56. package/plugins/augment/plugin.js +0 -0
  57. package/plugins/augment/plugin.json +0 -0
  58. package/plugins/aws-bedrock/icon-color.svg +0 -0
  59. package/plugins/aws-bedrock/icon.svg +0 -0
  60. package/plugins/aws-bedrock/plugin.js +0 -0
  61. package/plugins/aws-bedrock/plugin.json +0 -0
  62. package/plugins/azure-openai/icon-color.svg +0 -0
  63. package/plugins/azure-openai/icon.svg +0 -0
  64. package/plugins/azure-openai/plugin.js +0 -0
  65. package/plugins/azure-openai/plugin.json +0 -0
  66. package/plugins/bifrost/LICENSE +21 -0
  67. package/plugins/bifrost/plugin.js +361 -0
  68. package/plugins/bifrost/plugin.json +23 -0
  69. package/plugins/chutes/icon.svg +0 -0
  70. package/plugins/chutes/plugin.js +0 -0
  71. package/plugins/chutes/plugin.json +0 -0
  72. package/plugins/claude/icon-color.svg +0 -0
  73. package/plugins/claude/icon.svg +0 -0
  74. package/plugins/claude/plugin.js +358 -161
  75. package/plugins/claude/plugin.json +1 -1
  76. package/plugins/clawrouter/icon.svg +7 -0
  77. package/plugins/clawrouter/plugin.js +45 -0
  78. package/plugins/clawrouter/plugin.json +22 -0
  79. package/plugins/clinepass/icon.svg +4 -0
  80. package/plugins/clinepass/plugin.js +87 -0
  81. package/plugins/clinepass/plugin.json +23 -0
  82. package/plugins/codebuddy/plugin.js +331 -0
  83. package/plugins/codebuddy/plugin.json +30 -0
  84. package/plugins/codebuff/icon-color.svg +92 -0
  85. package/plugins/codebuff/icon.svg +0 -0
  86. package/plugins/codebuff/plugin.js +0 -0
  87. package/plugins/codebuff/plugin.json +0 -0
  88. package/plugins/coderabbit/plugin.js +25 -0
  89. package/plugins/coderabbit/plugin.json +12 -0
  90. package/plugins/codex/icon-color.svg +0 -0
  91. package/plugins/codex/icon.svg +0 -0
  92. package/plugins/codex/plugin.js +8 -2
  93. package/plugins/codex/plugin.json +0 -0
  94. package/plugins/command-code/icon-color.svg +3 -0
  95. package/plugins/command-code/icon.svg +0 -0
  96. package/plugins/command-code/plugin.js +14 -3
  97. package/plugins/command-code/plugin.json +0 -0
  98. package/plugins/copilot/icon-color.svg +0 -0
  99. package/plugins/copilot/icon.svg +1 -1
  100. package/plugins/copilot/plugin.js +61 -9
  101. package/plugins/copilot/plugin.json +4 -1
  102. package/plugins/crof/icon-color.svg +3 -0
  103. package/plugins/crof/icon.svg +0 -0
  104. package/plugins/crof/plugin.js +0 -0
  105. package/plugins/crof/plugin.json +0 -0
  106. package/plugins/crofai/icon-color.svg +3 -0
  107. package/plugins/crofai/icon.svg +2 -15
  108. package/plugins/crofai/plugin.js +0 -0
  109. package/plugins/crofai/plugin.json +0 -0
  110. package/plugins/cursor/icon-color.svg +1 -0
  111. package/plugins/cursor/icon-conflict.svg +0 -0
  112. package/plugins/cursor/icon.svg +0 -0
  113. package/plugins/cursor/plugin.js +46 -9
  114. package/plugins/cursor/plugin.json +3 -2
  115. package/plugins/cursor-nightly/icon.png +0 -0
  116. package/plugins/cursor-nightly/plugin.js +45 -8
  117. package/plugins/cursor-nightly/plugin.json +3 -2
  118. package/plugins/deepgram/icon.svg +0 -0
  119. package/plugins/deepgram/plugin.js +0 -0
  120. package/plugins/deepgram/plugin.json +0 -0
  121. package/plugins/deepinfra/icon-color.svg +1 -0
  122. package/plugins/deepinfra/icon.svg +1 -0
  123. package/plugins/deepinfra/plugin.js +97 -0
  124. package/plugins/deepinfra/plugin.json +28 -0
  125. package/plugins/deepseek/icon-color.svg +0 -0
  126. package/plugins/deepseek/icon.svg +0 -0
  127. package/plugins/deepseek/plugin.js +150 -59
  128. package/plugins/deepseek/plugin.json +3 -1
  129. package/plugins/devin/icon.svg +0 -0
  130. package/plugins/devin/plugin.js +114 -0
  131. package/plugins/devin/plugin.json +2 -1
  132. package/plugins/devpass/LICENSE +21 -0
  133. package/plugins/devpass/plugin.js +115 -0
  134. package/plugins/devpass/plugin.json +23 -0
  135. package/plugins/doubao/icon-color.svg +0 -0
  136. package/plugins/doubao/icon.svg +0 -0
  137. package/plugins/doubao/plugin.js +0 -0
  138. package/plugins/doubao/plugin.json +0 -0
  139. package/plugins/droid/icon.svg +0 -0
  140. package/plugins/droid/plugin.json +2 -1
  141. package/plugins/elevenlabs/icon-color.svg +4 -0
  142. package/plugins/elevenlabs/icon.svg +0 -0
  143. package/plugins/elevenlabs/plugin.js +17 -2
  144. package/plugins/elevenlabs/plugin.json +0 -0
  145. package/plugins/factory/icon-color.svg +1 -0
  146. package/plugins/factory/icon-conflict.svg +0 -0
  147. package/plugins/factory/icon.svg +0 -0
  148. package/plugins/factory/plugin.js +38 -14
  149. package/plugins/factory/plugin.json +2 -1
  150. package/plugins/fireworks-ai/icon-color.svg +1 -0
  151. package/plugins/fireworks-ai/icon.svg +1 -6
  152. package/plugins/fireworks-ai/plugin.js +0 -0
  153. package/plugins/fireworks-ai/plugin.json +0 -0
  154. package/plugins/gemini/icon-color.svg +0 -0
  155. package/plugins/gemini/icon.svg +0 -0
  156. package/plugins/gemini/plugin.js +0 -0
  157. package/plugins/gemini/plugin.json +0 -0
  158. package/plugins/gemini-apps/LICENSE +22 -0
  159. package/plugins/gemini-apps/icon-color.svg +1 -0
  160. package/plugins/gemini-apps/icon.svg +1 -0
  161. package/plugins/gemini-apps/plugin.js +470 -0
  162. package/plugins/gemini-apps/plugin.json +19 -0
  163. package/plugins/gitkraken/LICENSE +21 -0
  164. package/plugins/gitkraken/plugin.js +153 -0
  165. package/plugins/gitkraken/plugin.json +24 -0
  166. package/plugins/grok/icon-color.svg +1 -0
  167. package/plugins/grok/icon.svg +0 -0
  168. package/plugins/grok/plugin.js +30 -5
  169. package/plugins/grok/plugin.json +3 -1
  170. package/plugins/groqcloud/icon-color.svg +1 -0
  171. package/plugins/groqcloud/icon.svg +0 -0
  172. package/plugins/groqcloud/plugin.js +0 -0
  173. package/plugins/groqcloud/plugin.json +0 -0
  174. package/plugins/helmcode/LICENSE +21 -0
  175. package/plugins/helmcode/plugin.js +188 -0
  176. package/plugins/helmcode/plugin.json +24 -0
  177. package/plugins/huggingface/LICENSE +21 -0
  178. package/plugins/huggingface/icon-color.svg +1 -0
  179. package/plugins/huggingface/icon.svg +1 -0
  180. package/plugins/huggingface/plugin.js +293 -0
  181. package/plugins/huggingface/plugin.json +25 -0
  182. package/plugins/hyper/LICENSE +21 -0
  183. package/plugins/hyper/plugin.js +131 -0
  184. package/plugins/hyper/plugin.json +26 -0
  185. package/plugins/ibmbob/icon.svg +23 -0
  186. package/plugins/ibmbob/plugin.js +61 -0
  187. package/plugins/ibmbob/plugin.json +22 -0
  188. package/plugins/jetbrains-ai-assistant/icon-color.svg +0 -0
  189. package/plugins/jetbrains-ai-assistant/icon-conflict.svg +0 -0
  190. package/plugins/jetbrains-ai-assistant/icon.svg +0 -0
  191. package/plugins/jetbrains-ai-assistant/plugin.js +0 -0
  192. package/plugins/jetbrains-ai-assistant/plugin.json +0 -0
  193. package/plugins/kilo/icon-color.svg +1 -0
  194. package/plugins/kilo/icon.svg +0 -0
  195. package/plugins/kilo/plugin.js +0 -0
  196. package/plugins/kilo/plugin.json +0 -0
  197. package/plugins/kimi/icon-color.svg +0 -0
  198. package/plugins/kimi/icon-conflict.svg +0 -0
  199. package/plugins/kimi/icon.svg +0 -0
  200. package/plugins/kimi/plugin.js +122 -141
  201. package/plugins/kimi/plugin.json +5 -2
  202. package/plugins/kimi-k2/icon-color.svg +0 -0
  203. package/plugins/kimi-k2/icon.svg +0 -0
  204. package/plugins/kimi-k2/plugin.js +0 -0
  205. package/plugins/kimi-k2/plugin.json +0 -0
  206. package/plugins/kiro/icon-color.svg +0 -0
  207. package/plugins/kiro/icon-conflict.svg +0 -0
  208. package/plugins/kiro/icon.svg +0 -0
  209. package/plugins/kiro/plugin.js +49 -10
  210. package/plugins/kiro/plugin.json +3 -1
  211. package/plugins/litellm/icon.svg +0 -0
  212. package/plugins/litellm/plugin.js +54 -3
  213. package/plugins/litellm/plugin.json +0 -0
  214. package/plugins/llm-proxy/icon.svg +0 -0
  215. package/plugins/llm-proxy/plugin.js +0 -0
  216. package/plugins/llm-proxy/plugin.json +0 -0
  217. package/plugins/llmman/LICENSE +21 -0
  218. package/plugins/llmman/plugin.js +133 -0
  219. package/plugins/llmman/plugin.json +23 -0
  220. package/plugins/longcat/icon-color.svg +1 -0
  221. package/plugins/longcat/icon.svg +1 -0
  222. package/plugins/longcat/plugin.js +182 -0
  223. package/plugins/longcat/plugin.json +23 -0
  224. package/plugins/manus/icon-color.svg +6 -0
  225. package/plugins/manus/icon.svg +0 -0
  226. package/plugins/manus/plugin.js +0 -0
  227. package/plugins/manus/plugin.json +0 -0
  228. package/plugins/mimo/icon-color.svg +4 -0
  229. package/plugins/mimo/icon.svg +4 -1
  230. package/plugins/mimo/plugin.js +0 -0
  231. package/plugins/mimo/plugin.json +0 -0
  232. package/plugins/minimax/icon-color.svg +0 -0
  233. package/plugins/minimax/icon-conflict.svg +0 -0
  234. package/plugins/minimax/icon.svg +0 -0
  235. package/plugins/minimax/plugin.js +25 -31
  236. package/plugins/minimax/plugin.json +0 -0
  237. package/plugins/mistral/icon-color.svg +0 -0
  238. package/plugins/mistral/icon.svg +0 -0
  239. package/plugins/mistral/plugin.js +110 -5
  240. package/plugins/mistral/plugin.json +0 -0
  241. package/plugins/moonshot/icon-color.svg +1 -0
  242. package/plugins/moonshot/icon.svg +0 -0
  243. package/plugins/moonshot/plugin.js +8 -6
  244. package/plugins/moonshot/plugin.json +0 -0
  245. package/plugins/muse/LICENSE +21 -0
  246. package/plugins/muse/plugin.js +319 -0
  247. package/plugins/muse/plugin.json +26 -0
  248. package/plugins/nanogpt/icon-color.svg +0 -0
  249. package/plugins/nanogpt/icon.svg +0 -0
  250. package/plugins/nanogpt/plugin.js +0 -0
  251. package/plugins/nanogpt/plugin.json +0 -0
  252. package/plugins/neuralwatt/icon.svg +0 -0
  253. package/plugins/neuralwatt/plugin.js +0 -0
  254. package/plugins/neuralwatt/plugin.json +0 -0
  255. package/plugins/notion/icon.svg +1 -0
  256. package/plugins/notion/plugin.js +282 -0
  257. package/plugins/notion/plugin.json +29 -0
  258. package/plugins/nous/LICENSE +21 -0
  259. package/plugins/nous/icon-color.svg +1 -0
  260. package/plugins/nous/icon.svg +1 -0
  261. package/plugins/nous/plugin.js +124 -0
  262. package/plugins/nous/plugin.json +24 -0
  263. package/plugins/ollama/icon-color.svg +1 -0
  264. package/plugins/ollama/icon.svg +0 -0
  265. package/plugins/ollama/plugin.js +257 -56
  266. package/plugins/ollama/plugin.json +6 -3
  267. package/plugins/openai-api/icon-color.svg +1 -0
  268. package/plugins/openai-api/icon.svg +0 -0
  269. package/plugins/openai-api/plugin.js +0 -0
  270. package/plugins/openai-api/plugin.json +0 -0
  271. package/plugins/opencode/icon-color.svg +1 -0
  272. package/plugins/opencode/icon.svg +0 -0
  273. package/plugins/opencode/plugin.js +0 -0
  274. package/plugins/opencode/plugin.json +0 -0
  275. package/plugins/opencode-go/icon-color.svg +1 -0
  276. package/plugins/opencode-go/icon-conflict.svg +0 -0
  277. package/plugins/opencode-go/icon.svg +0 -0
  278. package/plugins/opencode-go/plugin.js +103 -17
  279. package/plugins/opencode-go/plugin.json +5 -2
  280. package/plugins/openrouter/icon-color.svg +1 -0
  281. package/plugins/openrouter/icon.svg +0 -0
  282. package/plugins/openrouter/plugin.js +371 -114
  283. package/plugins/openrouter/plugin.json +6 -0
  284. package/plugins/perplexity/icon-color.svg +0 -0
  285. package/plugins/perplexity/icon-conflict.svg +0 -0
  286. package/plugins/perplexity/icon.svg +0 -0
  287. package/plugins/perplexity/plugin.js +0 -0
  288. package/plugins/perplexity/plugin.json +0 -0
  289. package/plugins/pi/plugin.js +113 -0
  290. package/plugins/pi/plugin.json +11 -0
  291. package/plugins/poe/icon.svg +0 -0
  292. package/plugins/poe/plugin.js +24 -18
  293. package/plugins/poe/plugin.json +2 -0
  294. package/plugins/qoder/icon.svg +3 -0
  295. package/plugins/qoder/plugin.js +227 -0
  296. package/plugins/qoder/plugin.json +23 -0
  297. package/plugins/qwencloud/icon-color.svg +1 -0
  298. package/plugins/qwencloud/icon.svg +1 -0
  299. package/plugins/qwencloud/plugin.js +677 -0
  300. package/plugins/qwencloud/plugin.json +29 -0
  301. package/plugins/raycast/LICENSE +21 -0
  302. package/plugins/raycast/plugin.js +174 -0
  303. package/plugins/raycast/plugin.json +24 -0
  304. package/plugins/replicate/LICENSE +21 -0
  305. package/plugins/replicate/icon-color.svg +1 -0
  306. package/plugins/replicate/icon.svg +1 -0
  307. package/plugins/replicate/plugin.js +165 -0
  308. package/plugins/replicate/plugin.json +24 -0
  309. package/plugins/sakana/icon.svg +5 -0
  310. package/plugins/sakana/plugin.js +156 -0
  311. package/plugins/sakana/plugin.json +23 -0
  312. package/plugins/stepfun/icon-color.svg +0 -0
  313. package/plugins/stepfun/icon.svg +0 -0
  314. package/plugins/stepfun/plugin.js +0 -0
  315. package/plugins/stepfun/plugin.json +0 -0
  316. package/plugins/sub2api/icon.svg +4 -0
  317. package/plugins/sub2api/plugin.js +191 -0
  318. package/plugins/sub2api/plugin.json +16 -0
  319. package/plugins/synthetic/icon-color.svg +14 -0
  320. package/plugins/synthetic/icon-conflict.svg +0 -0
  321. package/plugins/synthetic/icon.svg +0 -0
  322. package/plugins/synthetic/plugin.js +0 -0
  323. package/plugins/synthetic/plugin.json +0 -0
  324. package/plugins/t3chat/icon-color.svg +0 -0
  325. package/plugins/t3chat/icon.svg +0 -0
  326. package/plugins/t3chat/plugin.js +0 -0
  327. package/plugins/t3chat/plugin.json +0 -0
  328. package/plugins/typesafe/LICENSE +21 -0
  329. package/plugins/typesafe/plugin.js +225 -0
  330. package/plugins/typesafe/plugin.json +24 -0
  331. package/plugins/v0/LICENSE +21 -0
  332. package/plugins/v0/icon-color.svg +1 -0
  333. package/plugins/v0/icon.svg +1 -0
  334. package/plugins/v0/plugin.js +214 -0
  335. package/plugins/v0/plugin.json +24 -0
  336. package/plugins/venice/icon-color.svg +0 -0
  337. package/plugins/venice/icon.svg +0 -0
  338. package/plugins/venice/plugin.js +50 -1
  339. package/plugins/venice/plugin.json +2 -1
  340. package/plugins/vercel/LICENSE +21 -0
  341. package/plugins/vercel/icon-color.svg +1 -0
  342. package/plugins/vercel/icon.svg +1 -0
  343. package/plugins/vercel/plugin.js +55 -0
  344. package/plugins/vercel/plugin.json +23 -0
  345. package/plugins/vertex-ai/icon-color.svg +0 -0
  346. package/plugins/vertex-ai/icon.svg +0 -0
  347. package/plugins/vertex-ai/plugin.js +0 -0
  348. package/plugins/vertex-ai/plugin.json +0 -0
  349. package/plugins/warp/icon-color.svg +0 -0
  350. package/plugins/warp/icon.svg +0 -0
  351. package/plugins/warp/plugin.js +0 -0
  352. package/plugins/warp/plugin.json +0 -0
  353. package/plugins/wayfinder/icon.svg +5 -0
  354. package/plugins/wayfinder/plugin.js +47 -0
  355. package/plugins/wayfinder/plugin.json +16 -0
  356. package/plugins/windsurf/icon-color.svg +1 -0
  357. package/plugins/windsurf/icon-conflict.svg +0 -0
  358. package/plugins/windsurf/icon.svg +0 -0
  359. package/plugins/windsurf/plugin.js +0 -0
  360. package/plugins/windsurf/plugin.json +0 -0
  361. package/plugins/xai/icon-color.svg +1 -0
  362. package/plugins/xai/icon.svg +1 -0
  363. package/plugins/xai/plugin.js +162 -0
  364. package/plugins/xai/plugin.json +28 -0
  365. package/plugins/xkiro/LICENSE +21 -0
  366. package/plugins/xkiro/plugin.js +99 -0
  367. package/plugins/xkiro/plugin.json +23 -0
  368. package/plugins/zai/icon-color.svg +1 -0
  369. package/plugins/zai/icon-conflict.svg +0 -0
  370. package/plugins/zai/icon.svg +0 -0
  371. package/plugins/zai/plugin.js +111 -161
  372. package/plugins/zai/plugin.json +4 -1
  373. package/plugins/zed/icon.svg +0 -0
  374. package/plugins/zed/plugin.js +0 -0
  375. package/plugins/zed/plugin.json +0 -0
  376. package/plugins/zenmux/icon-color.svg +1 -0
  377. package/plugins/zenmux/icon.svg +1 -0
  378. package/plugins/zenmux/plugin.js +102 -0
  379. package/plugins/zenmux/plugin.json +23 -0
  380. package/plugins/zoommate/icon.svg +1 -0
  381. package/plugins/zoommate/plugin.js +244 -0
  382. package/plugins/zoommate/plugin.json +29 -0
  383. package/usagestat +0 -0
  384. package/usagestatd +0 -0
@@ -20,13 +20,24 @@
20
20
  const MIN_USAGE_FETCH_INTERVAL_MS = 5 * 60 * 1000 // never poll more than once per 5 min
21
21
  const DEFAULT_RATE_LIMIT_BACKOFF_MS = 5 * 60 * 1000 // fallback when no Retry-After header
22
22
  const LIVE_USAGE_CACHE_FILE = "live-usage-cache.json"
23
- const LIVE_USAGE_CACHE_VERSION = 1
23
+ const LIVE_USAGE_CACHE_VERSION = 2
24
24
  const LIVE_USAGE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
25
25
  let rateLimitedUntilMs = 0 // epoch ms; 0 = not rate-limited
26
26
  let lastUsageFetchMs = 0 // epoch ms of the most-recent OAuth API attempt
27
27
  let cachedUsageData = null // last successful OAuth API response body (parsed JSON)
28
28
  let lastWebUsageFetchMs = 0 // epoch ms of the most-recent Web API attempt
29
29
  let cachedWebUsageData = null // last successful Web API response body (parsed JSON)
30
+ let activeCacheIdentity = null
31
+
32
+ function bindUsageCache(ctx, creds, sessionKey) {
33
+ const identity = ctx.host.crypto.sha256(JSON.stringify([
34
+ creds && creds.oauth && creds.oauth.accessToken || "", sessionKey || "",
35
+ ctx.provider && ctx.provider.workspaceId || "",
36
+ ]))
37
+ if (activeCacheIdentity !== identity) _resetState()
38
+ activeCacheIdentity = identity
39
+ ctx.claudeCacheIdentity = identity
40
+ }
30
41
 
31
42
  function utf8DecodeBytes(bytes) {
32
43
  // Prefer native TextDecoder when available (QuickJS may not expose it).
@@ -194,7 +205,7 @@
194
205
  if (!path || !ctx.host.fs.exists(path)) return null
195
206
  try {
196
207
  const parsed = ctx.util.tryParseJson(ctx.host.fs.readText(path))
197
- if (!parsed || typeof parsed !== "object") return null
208
+ if (!parsed || typeof parsed !== "object" || parsed.accountHash !== ctx.claudeCacheIdentity) return null
198
209
  return parsed
199
210
  } catch {
200
211
  return null
@@ -210,6 +221,7 @@
210
221
  cache[key] = updates[key]
211
222
  }
212
223
  cache.version = LIVE_USAGE_CACHE_VERSION
224
+ cache.accountHash = ctx.claudeCacheIdentity
213
225
  cache.updatedAtMs = Date.now()
214
226
  ctx.host.fs.writeText(path, JSON.stringify(cache, null, 2))
215
227
  } catch (e) {
@@ -399,6 +411,8 @@
399
411
 
400
412
  const envAccessToken = readEnvText(ctx, "CLAUDE_CODE_OAUTH_TOKEN")
401
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
402
416
  if (!envAccessToken) {
403
417
  return stored
404
418
  }
@@ -569,6 +583,7 @@
569
583
  // Persist updated credentials back to the same source we read from.
570
584
  fullData.claudeAiOauth = oauth
571
585
  saveCredentials(ctx, source, creds.serviceName, fullData)
586
+ bindUsageCache(ctx, creds, getSessionKey(ctx))
572
587
 
573
588
  ctx.host.log.info("refresh succeeded, new token expires in " + (body.expires_in || "unknown") + "s")
574
589
  return newAccessToken
@@ -581,15 +596,21 @@
581
596
 
582
597
  function fetchUsage(ctx, accessToken) {
583
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. */ }
584
605
  return ctx.util.request({
585
606
  method: "GET",
586
- url: oauthConfig.usageUrl,
607
+ url: oauthConfig.usageUrl + "?cedar_ember=1&skip_spend=1",
587
608
  headers: {
588
609
  Authorization: "Bearer " + accessToken.trim(),
589
610
  Accept: "application/json",
590
611
  "Content-Type": "application/json",
591
612
  "anthropic-beta": "oauth-2025-04-20",
592
- "User-Agent": "claude-code/2.1.69",
613
+ "User-Agent": "claude-code/" + version,
593
614
  },
594
615
  timeoutMs: 10000,
595
616
  })
@@ -603,8 +624,8 @@
603
624
  if (!str) return null
604
625
  // Retry-After can be a delay-seconds or HTTP-date (RFC 7231).
605
626
  // 0 means "retry immediately" — return 0 as a valid value.
606
- const seconds = parseInt(str, 10)
607
- if (Number.isFinite(seconds) && seconds >= 0) return seconds
627
+ const seconds = /^\d+$/.test(str) ? Number(str) : NaN
628
+ if (Number.isSafeInteger(seconds)) return seconds
608
629
  const dateMs = Date.parse(str)
609
630
  if (Number.isFinite(dateMs)) {
610
631
  const delay = Math.ceil((dateMs - Date.now()) / 1000)
@@ -613,6 +634,29 @@
613
634
  return null
614
635
  }
615
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
+
616
660
  function fmtRateLimitMinutes(seconds) {
617
661
  if (seconds <= 0) return "now"
618
662
  const mins = Math.ceil(seconds / 60)
@@ -1136,9 +1180,9 @@
1136
1180
 
1137
1181
  function getSessionKey(ctx) {
1138
1182
  const candidates = [
1183
+ providerCookieHeader(ctx),
1139
1184
  readEnvText(ctx, "CLAUDE_AI_SESSION_KEY"),
1140
1185
  readEnvText(ctx, "CLAUDE_WEB_SESSION_KEY"),
1141
- providerCookieHeader(ctx),
1142
1186
  ]
1143
1187
  for (let i = 0; i < candidates.length; i++) {
1144
1188
  const key = extractSessionKey(candidates[i])
@@ -1147,9 +1191,12 @@
1147
1191
  return null
1148
1192
  }
1149
1193
 
1150
- 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
1151
1198
  return {
1152
- Cookie: "sessionKey=" + sessionKey,
1199
+ Cookie: cookie,
1153
1200
  Accept: "application/json",
1154
1201
  Origin: "https://claude.ai",
1155
1202
  Referer: "https://claude.ai/settings/usage",
@@ -1158,8 +1205,22 @@
1158
1205
  }
1159
1206
  }
1160
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
+
1161
1222
  function fetchWebUsage(ctx, sessionKey) {
1162
- const headers = buildWebHeaders(sessionKey)
1223
+ const headers = buildWebHeaders(ctx, sessionKey)
1163
1224
 
1164
1225
  let orgResp
1165
1226
  try {
@@ -1174,6 +1235,7 @@
1174
1235
  throw "Web usage request failed. Check your connection."
1175
1236
  }
1176
1237
 
1238
+ checkWebResponse(orgResp)
1177
1239
  if (orgResp.status === 401 || orgResp.status === 403) {
1178
1240
  throw "Web session invalid or expired. Set CLAUDE_AI_SESSION_KEY to your claude.ai sessionKey cookie."
1179
1241
  }
@@ -1206,6 +1268,7 @@
1206
1268
  }
1207
1269
 
1208
1270
  lastUsageStatus = usageResp.status
1271
+ checkWebResponse(usageResp)
1209
1272
  if (usageResp.status === 401 || usageResp.status === 403) {
1210
1273
  ctx.host.log.info("web mode: usage unavailable for org " + candidateOrgId + " (HTTP " + usageResp.status + ")")
1211
1274
  continue
@@ -1252,29 +1315,46 @@
1252
1315
 
1253
1316
  function loadWebUsageData(ctx, sessionKey, nowMs) {
1254
1317
  const liveCache = readLiveUsageCache(ctx)
1318
+ if (liveCache && Number(liveCache.webRetryAtMs) > nowMs) {
1319
+ throw liveCache.webError || "Claude web usage updates are temporarily unavailable."
1320
+ }
1255
1321
  const cachedLastWebUsageFetchMs = Number(liveCache && liveCache.lastWebUsageFetchMs) || 0
1256
1322
  const effectiveLastWebUsageFetchMs = Math.max(lastWebUsageFetchMs, cachedLastWebUsageFetchMs)
1257
- if (nowMs - effectiveLastWebUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1323
+ const cached = cachedWebUsageData || (liveCache && liveCache.usageSource === "web" && cachedLiveUsageData(liveCache, nowMs))
1324
+ if (cached && nowMs - effectiveLastWebUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1258
1325
  lastWebUsageFetchMs = effectiveLastWebUsageFetchMs
1259
1326
  ctx.host.log.info(
1260
1327
  "web usage fetch skipped: last fetch was " +
1261
1328
  Math.round((nowMs - effectiveLastWebUsageFetchMs) / 1000) + "s ago"
1262
1329
  )
1263
- return { data: cachedWebUsageData || cachedLiveUsageData(liveCache, nowMs), plan: null }
1330
+ return { data: cached, plan: liveCache && liveCache.webPlan || null }
1264
1331
  }
1265
1332
 
1266
1333
  lastWebUsageFetchMs = nowMs
1267
- const result = fetchWebUsage(ctx, sessionKey)
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
+ }
1268
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
+ }
1269
1351
  writeLiveUsageCache(ctx, {
1270
1352
  usageData: result.usageData,
1353
+ usageSource: "web",
1271
1354
  usageFetchedAtMs: nowMs,
1272
1355
  lastWebUsageFetchMs: nowMs,
1356
+ webRetryAtMs: 0, webError: null, webPlan: plan,
1273
1357
  })
1274
- let plan = null
1275
- if (result.accountInfo && result.accountInfo.rate_limit_tier) {
1276
- plan = webTierToPlanLabel(result.accountInfo.rate_limit_tier)
1277
- }
1278
1358
  return { data: result.usageData, plan: plan }
1279
1359
  }
1280
1360
 
@@ -1282,7 +1362,9 @@
1282
1362
  const t = (tier || "").toLowerCase()
1283
1363
  if (t === "free") return "Claude Free"
1284
1364
  if (t === "pro" || t === "claude_pro") return "Claude Pro"
1285
- if (t === "max" || t === "claude_max_5" || t === "claude_max_20") return "Claude Max"
1365
+ if (t === "claude_max_5" || t === "claude_max_5x") return "Claude Max 5x"
1366
+ if (t === "claude_max_20" || t === "claude_max_20x") return "Claude Max 20x"
1367
+ if (t === "max") return "Claude Max"
1286
1368
  if (t === "team") return "Claude Team"
1287
1369
  if (t === "enterprise") return "Claude Enterprise"
1288
1370
  return "Claude (" + tier + ")"
@@ -1299,6 +1381,8 @@
1299
1381
  }
1300
1382
 
1301
1383
  function moneyMajorUnits(value) {
1384
+ if (typeof value !== "number" && typeof value !== "string") return null
1385
+ if (typeof value === "string" && !value.trim()) return null
1302
1386
  const n = Number(value)
1303
1387
  if (!Number.isFinite(n)) return null
1304
1388
  // Claude usage APIs return Extra usage amounts in minor units (cents).
@@ -1333,9 +1417,18 @@
1333
1417
  return months[d.getUTCMonth()] + " " + String(d.getUTCDate())
1334
1418
  }
1335
1419
 
1420
+ function currentUsageWindow(data, kind, fallback) {
1421
+ const entries = Array.isArray(data.limits) ? data.limits.filter((entry) =>
1422
+ entry && entry.kind === kind && Number.isFinite(entry.percent) &&
1423
+ !(entry.scope && (entry.scope.model || entry.scope.surface))) : []
1424
+ const entry = entries.find((item) => item.is_active === true) || entries[0]
1425
+ return entry ? { utilization: entry.percent, resets_at: entry.resets_at } : fallback
1426
+ }
1427
+
1336
1428
  function addUsageWindowLines(ctx, data, lines) {
1337
- const fiveHour = data.five_hour
1338
- if (fiveHour && typeof fiveHour.utilization === "number") {
1429
+ const settings = ctx.provider && ctx.provider.settings || {}
1430
+ const fiveHour = currentUsageWindow(data, "session", data.five_hour)
1431
+ if (fiveHour && Number.isFinite(fiveHour.utilization)) {
1339
1432
  lines.push(ctx.line.progress({
1340
1433
  label: "Session",
1341
1434
  used: fiveHour.utilization,
@@ -1346,8 +1439,8 @@
1346
1439
  }))
1347
1440
  }
1348
1441
 
1349
- const sevenDay = data.seven_day
1350
- if (sevenDay && typeof sevenDay.utilization === "number") {
1442
+ const sevenDay = currentUsageWindow(data, "weekly_all", data.seven_day)
1443
+ if (sevenDay && Number.isFinite(sevenDay.utilization)) {
1351
1444
  lines.push(ctx.line.progress({
1352
1445
  label: "Weekly",
1353
1446
  used: sevenDay.utilization,
@@ -1359,7 +1452,7 @@
1359
1452
  }
1360
1453
 
1361
1454
  const sevenDaySonnet = data.seven_day_sonnet
1362
- if (sevenDaySonnet && typeof sevenDaySonnet.utilization === "number") {
1455
+ if (settings.showModelQuotas === true && sevenDaySonnet && Number.isFinite(sevenDaySonnet.utilization)) {
1363
1456
  lines.push(ctx.line.progress({
1364
1457
  label: "Sonnet",
1365
1458
  used: sevenDaySonnet.utilization,
@@ -1371,7 +1464,7 @@
1371
1464
  }
1372
1465
 
1373
1466
  const sevenDayOpus = data.seven_day_opus
1374
- if (sevenDayOpus && typeof sevenDayOpus.utilization === "number") {
1467
+ if (settings.showModelQuotas === true && sevenDayOpus && Number.isFinite(sevenDayOpus.utilization)) {
1375
1468
  lines.push(ctx.line.progress({
1376
1469
  label: "Opus",
1377
1470
  used: sevenDayOpus.utilization,
@@ -1382,13 +1475,23 @@
1382
1475
  }))
1383
1476
  }
1384
1477
 
1478
+ if (settings.showModelQuotas === true && Array.isArray(data.limits)) {
1479
+ for (const entry of data.limits) {
1480
+ if (!entry || entry.kind !== "weekly_scoped" || !Number.isFinite(entry.percent)) continue
1481
+ const model = entry.scope && entry.scope.model && entry.scope.model.display_name
1482
+ if (typeof model !== "string" || !model.trim() || lines.some((line) => line.label === model.trim())) continue
1483
+ lines.push(ctx.line.progress({ label: model.trim(), used: entry.percent, limit: 100,
1484
+ format: { kind: "percent" }, resetsAt: ctx.util.toIso(entry.resets_at), periodDurationMs: 7 * 24 * 60 * 60 * 1000 }))
1485
+ }
1486
+ }
1487
+
1385
1488
  // Routines: check canonical name first, then legacy aliases
1386
1489
  const routinesWindow = pickUsageWindow(data, [
1387
1490
  "seven_day_routines",
1388
1491
  "seven_day_cowork",
1389
1492
  "seven_day_claude_routines",
1390
1493
  ])
1391
- if (routinesWindow && typeof routinesWindow.utilization === "number") {
1494
+ if (settings.hideDailyRoutines !== true && routinesWindow && Number.isFinite(routinesWindow.utilization)) {
1392
1495
  lines.push(ctx.line.progress({
1393
1496
  label: "Routines",
1394
1497
  used: routinesWindow.utilization,
@@ -1399,6 +1502,24 @@
1399
1502
  }))
1400
1503
  }
1401
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
+
1402
1523
  if (data.extra_usage && data.extra_usage.is_enabled) {
1403
1524
  const used = moneyMajorUnits(data.extra_usage.used_credits)
1404
1525
  const limit = moneyMajorUnits(data.extra_usage.monthly_limit)
@@ -1448,6 +1569,29 @@
1448
1569
 
1449
1570
  // ── probe() ────────────────────────────────────────────────────────────────
1450
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
+
1451
1595
  function probe(ctx) {
1452
1596
  const creds = loadCredentials(ctx)
1453
1597
  const sessionKey = getSessionKey(ctx)
@@ -1457,6 +1601,7 @@
1457
1601
  const wantsAuto = sourceMode === "auto"
1458
1602
  const wantsWeb = sourceMode === "web"
1459
1603
  const wantsOAuth = sourceMode === "oauth"
1604
+ const wantsCli = sourceMode === "cli"
1460
1605
  const wantsLocal = sourceMode === "local"
1461
1606
  const wantsApi = sourceMode === "api"
1462
1607
 
@@ -1472,15 +1617,14 @@
1472
1617
  ctx.host.log.error("api mode requested but no Claude Admin API key found")
1473
1618
  throw "Claude API usage needs an Anthropic Admin API key. Set ANTHROPIC_ADMIN_KEY."
1474
1619
  }
1475
- if (!wantsLocal && !wantsApi && !hasOAuth && !sessionKey && !adminApiKey) {
1476
- ctx.host.log.error("probe failed: no credentials or session key")
1477
- throw "Not logged in. Run `claude` to authenticate, set CLAUDE_AI_SESSION_KEY, or set ANTHROPIC_ADMIN_KEY."
1478
- }
1479
-
1620
+ const noLiveCredentials = !hasOAuth && !sessionKey && !adminApiKey
1621
+ bindUsageCache(ctx, creds, sessionKey)
1480
1622
  const nowMs = Date.now()
1481
1623
  const liveCache = readLiveUsageCache(ctx)
1482
1624
  const homePath = getClaudeHomeOverride(ctx)
1483
1625
  let data = null
1626
+ let quotaSource = null
1627
+ let quotaFetchedAtMs = null
1484
1628
  let lines = []
1485
1629
  let plan = null
1486
1630
  let rateLimited = false
@@ -1488,145 +1632,194 @@
1488
1632
 
1489
1633
  if (hasOAuth) {
1490
1634
  plan = oauthPlanLabel(ctx, creds)
1635
+ if (liveCache && liveCache.profileTokenHash === ctx.host.crypto.sha256(creds.oauth.accessToken) && liveCache.livePlan) plan = liveCache.livePlan
1491
1636
  }
1492
1637
 
1493
- if (wantsLocal) {
1494
- ctx.host.log.info("local mode requested; skipping live usage fetch")
1495
- } else if (wantsApi) {
1496
- ctx.host.log.info("api mode requested; skipping live quota fetch")
1497
- } else if (hasOAuth && !wantsWeb) {
1498
- // ── OAuth mode ───────────────────────────────────────────────────────
1499
- let accessToken = creds.oauth.accessToken
1500
- const canFetchLiveUsage = hasProfileScope(creds)
1501
-
1502
- if (canFetchLiveUsage) {
1503
- const cachedData = cachedUsageData || cachedLiveUsageData(liveCache, nowMs)
1504
- const cachedRateLimitedUntilMs = Number(liveCache && liveCache.rateLimitedUntilMs) || 0
1505
- const effectiveRateLimitedUntilMs = Math.max(rateLimitedUntilMs, cachedRateLimitedUntilMs)
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
+ }
1506
1651
 
1507
- if (nowMs < effectiveRateLimitedUntilMs) {
1508
- // Still within a rate-limit window from a previous probe call — skip the
1509
- // API request entirely and surface the remaining wait time to the user.
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) {
1510
1664
  rateLimited = true
1511
- rateLimitedUntilMs = effectiveRateLimitedUntilMs
1512
- retryAfterSeconds = Math.ceil((effectiveRateLimitedUntilMs - nowMs) / 1000)
1513
- data = cachedData
1514
- 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)
1515
1667
  } else {
1516
- // Rate-limit window has expired (or was never set). Check whether we were
1517
- // previously rate-limited so we can bypass the min-interval guard: a short
1518
- // Retry-After (< 5 min) must not be swallowed by the normal poll throttle.
1519
- const wasRateLimited = effectiveRateLimitedUntilMs > 0
1520
- rateLimitedUntilMs = 0
1521
-
1522
- const cachedLastUsageFetchMs = Number(liveCache && liveCache.lastUsageFetchMs) || 0
1523
- const effectiveLastUsageFetchMs = Math.max(lastUsageFetchMs, cachedLastUsageFetchMs)
1524
- if (!wasRateLimited && nowMs - effectiveLastUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1525
- // Polled too recently in normal operation — reuse last cached response.
1526
- 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)
1527
1687
  data = cachedData
1528
- ctx.host.log.info(
1529
- "usage fetch skipped: last fetch was " +
1530
- Math.round((nowMs - effectiveLastUsageFetchMs) / 1000) + "s ago (min interval " +
1531
- MIN_USAGE_FETCH_INTERVAL_MS / 1000 + "s)"
1532
- )
1688
+ ctx.host.log.info("usage fetch skipped: rate-limited for " + retryAfterSeconds + "s more")
1533
1689
  } else {
1534
- // Proactively refresh if token is expired or about to expire
1535
- if (needsRefresh(ctx, creds.oauth, nowMs)) {
1536
- ctx.host.log.info("token needs refresh (expired or expiring soon)")
1537
- const refreshed = refreshToken(ctx, creds)
1538
- if (refreshed) {
1539
- accessToken = refreshed
1540
- } else {
1541
- ctx.host.log.warn("proactive refresh failed, trying with existing token")
1542
- }
1543
- }
1544
-
1545
- lastUsageFetchMs = nowMs
1546
- let resp
1547
- let didRefresh = false
1548
- try {
1549
- resp = ctx.util.retryOnceOnAuth({
1550
- request: (token) => {
1551
- try {
1552
- return fetchUsage(ctx, token || accessToken)
1553
- } catch (e) {
1554
- ctx.host.log.error("usage request exception: " + String(e))
1555
- if (didRefresh) {
1556
- throw "Usage request failed after refresh. Try again."
1557
- }
1558
- throw "Usage request failed. Check your connection."
1559
- }
1560
- },
1561
- refresh: () => {
1562
- ctx.host.log.info("usage returned 401, attempting refresh")
1563
- didRefresh = true
1564
- return refreshToken(ctx, creds)
1565
- },
1566
- })
1567
- } catch (e) {
1568
- if (typeof e === "string") throw e
1569
- ctx.host.log.error("usage request failed: " + String(e))
1570
- throw "Usage request failed. Check your connection."
1571
- }
1572
-
1573
- if (ctx.util.isAuthStatus(resp.status)) {
1574
- ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
1575
- throw "Token expired. Run `claude` to log in again."
1576
- }
1577
-
1578
- if (resp.status === 429) {
1579
- rateLimited = true
1580
- retryAfterSeconds = parseRetryAfterSeconds(resp.headers)
1581
- const backoffMs = retryAfterSeconds !== null
1582
- ? retryAfterSeconds * 1000
1583
- : DEFAULT_RATE_LIMIT_BACKOFF_MS
1584
- 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
1585
1701
  data = cachedData
1586
- writeLiveUsageCache(ctx, {
1587
- rateLimitedUntilMs: rateLimitedUntilMs,
1588
- lastUsageFetchMs: nowMs,
1589
- usageData: data,
1590
- usageFetchedAtMs: Number(liveCache && liveCache.usageFetchedAtMs) || null,
1591
- lastRateLimitedAtMs: nowMs,
1592
- })
1593
- ctx.host.log.warn(
1594
- "usage rate limited (429), backing off for " +
1595
- 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)"
1596
1706
  )
1597
- } else if (resp.status < 200 || resp.status >= 300) {
1598
- ctx.host.log.error("usage returned error: status=" + resp.status)
1599
- throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
1600
1707
  } else {
1601
- ctx.host.log.info("usage fetch succeeded")
1602
- data = ctx.util.tryParseJson(resp.bodyText)
1603
- if (data === null) {
1604
- 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."
1605
1750
  }
1606
- cachedUsageData = data
1607
- rateLimitedUntilMs = 0
1608
- writeLiveUsageCache(ctx, {
1609
- rateLimitedUntilMs: 0,
1610
- lastUsageFetchMs: nowMs,
1611
- usageData: data,
1612
- usageFetchedAtMs: nowMs,
1613
- lastRateLimitedAtMs: null,
1614
- })
1615
- }
1616
- } // end fetch else-branch
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
1796
+ }
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
1617
1809
  }
1618
1810
  } else {
1619
- ctx.host.log.info("skipping live usage fetch for inference-only token")
1620
- }
1621
- } else if (sessionKey) {
1622
- // ── Web mode ─────────────────────────────────────────────────────────
1623
- const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
1624
- data = webResult.data
1625
- if (webResult.plan) {
1626
- plan = webResult.plan
1811
+ ctx.host.log.info("auto mode using available Claude usage history")
1627
1812
  }
1628
- } else {
1629
- ctx.host.log.info("auto mode using Claude Admin API history only")
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
1630
1823
  }
1631
1824
 
1632
1825
  if (rateLimited && wantsAuto && sessionKey) {
@@ -1635,6 +1828,7 @@
1635
1828
  const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
1636
1829
  if (webResult.data) {
1637
1830
  data = webResult.data
1831
+ quotaSource = "web"
1638
1832
  rateLimited = false
1639
1833
  retryAfterSeconds = null
1640
1834
  if (webResult.plan) {
@@ -1695,18 +1889,21 @@
1695
1889
  ? fmtRateLimitMinutes(retryAfterSeconds)
1696
1890
  : null
1697
1891
  const waitText = retryText
1698
- ? "Rate limited, retry in ~" + retryText
1699
- : "Rate limited, try again later"
1892
+ ? "Usage updates paused; retry in ~" + retryText
1893
+ : "Usage updates temporarily unavailable"
1700
1894
  lines.unshift(ctx.line.badge({ label: "Status", text: waitText, color: "#f59e0b" }))
1701
- const noteText = retryText
1702
- ? "Live usage rate limited — retry in ~" + retryText
1703
- : "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." : "")
1704
1896
  lines.push(ctx.line.text({ label: "Note", value: noteText }))
1705
1897
  } else if (lines.length === 0) {
1898
+ if (noLiveCredentials && wantsAuto) throw "Not logged in and no local Claude usage found. Run `claude` to authenticate."
1706
1899
  lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
1707
1900
  }
1708
1901
 
1709
- return { plan: plan, lines: lines }
1902
+ const finalCache = readLiveUsageCache(ctx)
1903
+ const fetchedAt = data && ctx.util.toIso(quotaFetchedAtMs || finalCache && finalCache.usageFetchedAtMs)
1904
+ return { plan: plan, lines: lines, fetchedAt: fetchedAt || ctx.nowIso,
1905
+ state: rateLimited ? "failed" : lines.length ? "ready" : "no-data",
1906
+ source: data ? (rateLimited ? "cached" : quotaSource) : wantsApi ? "api" : "local" }
1710
1907
  }
1711
1908
 
1712
1909
  // _resetState is a testing hook — resets module-scope rate-limit state between tests.