@hashimkarim/usagestat-linux-x64-gnu 2.0.0-alpha.1

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 (225) hide show
  1. package/LICENSE +57744 -0
  2. package/native-manifest.json +1414 -0
  3. package/package.json +254 -0
  4. package/plugins/abacus-ai/icon.svg +18 -0
  5. package/plugins/abacus-ai/plugin.js +121 -0
  6. package/plugins/abacus-ai/plugin.json +17 -0
  7. package/plugins/alibaba/icon-color.svg +1 -0
  8. package/plugins/alibaba/icon.svg +1 -0
  9. package/plugins/alibaba/plugin.js +322 -0
  10. package/plugins/alibaba/plugin.json +20 -0
  11. package/plugins/alibaba-token-plan/icon-color.svg +1 -0
  12. package/plugins/alibaba-token-plan/icon.svg +1 -0
  13. package/plugins/alibaba-token-plan/plugin.js +544 -0
  14. package/plugins/alibaba-token-plan/plugin.json +19 -0
  15. package/plugins/amp/icon-color.svg +1 -0
  16. package/plugins/amp/icon-conflict.svg +6 -0
  17. package/plugins/amp/icon.svg +1 -0
  18. package/plugins/amp/plugin.js +175 -0
  19. package/plugins/amp/plugin.json +16 -0
  20. package/plugins/antigravity/icon-color.svg +1 -0
  21. package/plugins/antigravity/icon-conflict.svg +3 -0
  22. package/plugins/antigravity/icon.svg +1 -0
  23. package/plugins/antigravity/plugin.js +739 -0
  24. package/plugins/antigravity/plugin.json +31 -0
  25. package/plugins/antigravity-cli/icon.svg +3 -0
  26. package/plugins/antigravity-cli/plugin.js +570 -0
  27. package/plugins/antigravity-cli/plugin.json +16 -0
  28. package/plugins/antigravity-ide/icon.svg +3 -0
  29. package/plugins/antigravity-ide/plugin.js +257 -0
  30. package/plugins/antigravity-ide/plugin.json +29 -0
  31. package/plugins/augment/icon.svg +14 -0
  32. package/plugins/augment/plugin.js +59 -0
  33. package/plugins/augment/plugin.json +18 -0
  34. package/plugins/aws-bedrock/icon-color.svg +1 -0
  35. package/plugins/aws-bedrock/icon.svg +1 -0
  36. package/plugins/aws-bedrock/plugin.js +206 -0
  37. package/plugins/aws-bedrock/plugin.json +18 -0
  38. package/plugins/azure-openai/icon-color.svg +1 -0
  39. package/plugins/azure-openai/icon.svg +1 -0
  40. package/plugins/azure-openai/plugin.js +145 -0
  41. package/plugins/azure-openai/plugin.json +17 -0
  42. package/plugins/chutes/icon.svg +7 -0
  43. package/plugins/chutes/plugin.js +236 -0
  44. package/plugins/chutes/plugin.json +19 -0
  45. package/plugins/claude/icon-color.svg +1 -0
  46. package/plugins/claude/icon.svg +1 -0
  47. package/plugins/claude/plugin.js +1723 -0
  48. package/plugins/claude/plugin.json +30 -0
  49. package/plugins/codebuff/icon.svg +92 -0
  50. package/plugins/codebuff/plugin.js +113 -0
  51. package/plugins/codebuff/plugin.json +17 -0
  52. package/plugins/codex/icon-color.svg +5 -0
  53. package/plugins/codex/icon.svg +1 -0
  54. package/plugins/codex/plugin.js +1009 -0
  55. package/plugins/codex/plugin.json +82 -0
  56. package/plugins/command-code/icon.svg +3 -0
  57. package/plugins/command-code/plugin.js +184 -0
  58. package/plugins/command-code/plugin.json +20 -0
  59. package/plugins/copilot/icon-color.svg +1 -0
  60. package/plugins/copilot/icon.svg +1 -0
  61. package/plugins/copilot/plugin.js +684 -0
  62. package/plugins/copilot/plugin.json +21 -0
  63. package/plugins/crof/icon.svg +3 -0
  64. package/plugins/crof/plugin.js +53 -0
  65. package/plugins/crof/plugin.json +17 -0
  66. package/plugins/crofai/icon.svg +16 -0
  67. package/plugins/crofai/plugin.js +81 -0
  68. package/plugins/crofai/plugin.json +18 -0
  69. package/plugins/cursor/icon-conflict.svg +3 -0
  70. package/plugins/cursor/icon.svg +1 -0
  71. package/plugins/cursor/plugin.js +1379 -0
  72. package/plugins/cursor/plugin.json +23 -0
  73. package/plugins/cursor-nightly/icon.png +0 -0
  74. package/plugins/cursor-nightly/plugin.js +1379 -0
  75. package/plugins/cursor-nightly/plugin.json +23 -0
  76. package/plugins/deepgram/icon.svg +1 -0
  77. package/plugins/deepgram/plugin.js +20 -0
  78. package/plugins/deepgram/plugin.json +17 -0
  79. package/plugins/deepseek/icon-color.svg +1 -0
  80. package/plugins/deepseek/icon.svg +1 -0
  81. package/plugins/deepseek/plugin.js +71 -0
  82. package/plugins/deepseek/plugin.json +17 -0
  83. package/plugins/devin/icon.svg +5 -0
  84. package/plugins/devin/plugin.js +341 -0
  85. package/plugins/devin/plugin.json +16 -0
  86. package/plugins/doubao/icon-color.svg +1 -0
  87. package/plugins/doubao/icon.svg +1 -0
  88. package/plugins/doubao/plugin.js +92 -0
  89. package/plugins/doubao/plugin.json +17 -0
  90. package/plugins/droid/icon.svg +1 -0
  91. package/plugins/droid/plugin.json +20 -0
  92. package/plugins/elevenlabs/icon.svg +4 -0
  93. package/plugins/elevenlabs/plugin.js +48 -0
  94. package/plugins/elevenlabs/plugin.json +18 -0
  95. package/plugins/factory/icon-conflict.svg +1 -0
  96. package/plugins/factory/icon.svg +1 -0
  97. package/plugins/factory/plugin.js +487 -0
  98. package/plugins/factory/plugin.json +15 -0
  99. package/plugins/fireworks-ai/icon.svg +6 -0
  100. package/plugins/fireworks-ai/plugin.js +370 -0
  101. package/plugins/fireworks-ai/plugin.json +19 -0
  102. package/plugins/gemini/icon-color.svg +1 -0
  103. package/plugins/gemini/icon.svg +1 -0
  104. package/plugins/gemini/plugin.js +460 -0
  105. package/plugins/gemini/plugin.json +16 -0
  106. package/plugins/grok/icon.svg +1 -0
  107. package/plugins/grok/plugin.js +297 -0
  108. package/plugins/grok/plugin.json +19 -0
  109. package/plugins/groqcloud/icon.svg +1 -0
  110. package/plugins/groqcloud/plugin.js +117 -0
  111. package/plugins/groqcloud/plugin.json +19 -0
  112. package/plugins/jetbrains-ai-assistant/icon-color.svg +14 -0
  113. package/plugins/jetbrains-ai-assistant/icon-conflict.svg +3 -0
  114. package/plugins/jetbrains-ai-assistant/icon.svg +14 -0
  115. package/plugins/jetbrains-ai-assistant/plugin.js +313 -0
  116. package/plugins/jetbrains-ai-assistant/plugin.json +16 -0
  117. package/plugins/kilo/icon.svg +1 -0
  118. package/plugins/kilo/plugin.js +111 -0
  119. package/plugins/kilo/plugin.json +19 -0
  120. package/plugins/kimi/icon-color.svg +1 -0
  121. package/plugins/kimi/icon-conflict.svg +3 -0
  122. package/plugins/kimi/icon.svg +1 -0
  123. package/plugins/kimi/plugin.js +358 -0
  124. package/plugins/kimi/plugin.json +15 -0
  125. package/plugins/kimi-k2/icon-color.svg +1 -0
  126. package/plugins/kimi-k2/icon.svg +1 -0
  127. package/plugins/kimi-k2/plugin.js +58 -0
  128. package/plugins/kimi-k2/plugin.json +18 -0
  129. package/plugins/kiro/icon-color.svg +5 -0
  130. package/plugins/kiro/icon-conflict.svg +1 -0
  131. package/plugins/kiro/icon.svg +4 -0
  132. package/plugins/kiro/plugin.js +390 -0
  133. package/plugins/kiro/plugin.json +16 -0
  134. package/plugins/litellm/icon.svg +16 -0
  135. package/plugins/litellm/plugin.js +199 -0
  136. package/plugins/litellm/plugin.json +16 -0
  137. package/plugins/llm-proxy/icon.svg +1 -0
  138. package/plugins/llm-proxy/plugin.js +40 -0
  139. package/plugins/llm-proxy/plugin.json +17 -0
  140. package/plugins/manus/icon.svg +6 -0
  141. package/plugins/manus/plugin.js +50 -0
  142. package/plugins/manus/plugin.json +17 -0
  143. package/plugins/mimo/icon.svg +1 -0
  144. package/plugins/mimo/plugin.js +177 -0
  145. package/plugins/mimo/plugin.json +19 -0
  146. package/plugins/minimax/icon-color.svg +1 -0
  147. package/plugins/minimax/icon-conflict.svg +4 -0
  148. package/plugins/minimax/icon.svg +1 -0
  149. package/plugins/minimax/plugin.js +453 -0
  150. package/plugins/minimax/plugin.json +14 -0
  151. package/plugins/mistral/icon-color.svg +1 -0
  152. package/plugins/mistral/icon.svg +1 -0
  153. package/plugins/mistral/plugin.js +152 -0
  154. package/plugins/mistral/plugin.json +18 -0
  155. package/plugins/moonshot/icon.svg +1 -0
  156. package/plugins/moonshot/plugin.js +43 -0
  157. package/plugins/moonshot/plugin.json +16 -0
  158. package/plugins/nanogpt/icon-color.svg +17 -0
  159. package/plugins/nanogpt/icon.svg +10 -0
  160. package/plugins/nanogpt/plugin.js +80 -0
  161. package/plugins/nanogpt/plugin.json +17 -0
  162. package/plugins/neuralwatt/icon.svg +3 -0
  163. package/plugins/neuralwatt/plugin.js +152 -0
  164. package/plugins/neuralwatt/plugin.json +17 -0
  165. package/plugins/ollama/icon.svg +1 -0
  166. package/plugins/ollama/plugin.js +92 -0
  167. package/plugins/ollama/plugin.json +20 -0
  168. package/plugins/openai-api/icon.svg +1 -0
  169. package/plugins/openai-api/plugin.js +533 -0
  170. package/plugins/openai-api/plugin.json +26 -0
  171. package/plugins/opencode/icon.svg +1 -0
  172. package/plugins/opencode/plugin.js +342 -0
  173. package/plugins/opencode/plugin.json +19 -0
  174. package/plugins/opencode-go/icon-conflict.svg +1 -0
  175. package/plugins/opencode-go/icon.svg +1 -0
  176. package/plugins/opencode-go/plugin.js +617 -0
  177. package/plugins/opencode-go/plugin.json +50 -0
  178. package/plugins/openrouter/icon.svg +1 -0
  179. package/plugins/openrouter/plugin.js +156 -0
  180. package/plugins/openrouter/plugin.json +8 -0
  181. package/plugins/perplexity/icon-color.svg +1 -0
  182. package/plugins/perplexity/icon-conflict.svg +1 -0
  183. package/plugins/perplexity/icon.svg +1 -0
  184. package/plugins/perplexity/plugin.js +425 -0
  185. package/plugins/perplexity/plugin.json +19 -0
  186. package/plugins/poe/icon.svg +1 -0
  187. package/plugins/poe/plugin.js +190 -0
  188. package/plugins/poe/plugin.json +19 -0
  189. package/plugins/stepfun/icon-color.svg +1 -0
  190. package/plugins/stepfun/icon.svg +1 -0
  191. package/plugins/stepfun/plugin.js +155 -0
  192. package/plugins/stepfun/plugin.json +18 -0
  193. package/plugins/synthetic/icon-conflict.svg +12 -0
  194. package/plugins/synthetic/icon.svg +14 -0
  195. package/plugins/synthetic/plugin.js +331 -0
  196. package/plugins/synthetic/plugin.json +23 -0
  197. package/plugins/t3chat/icon-color.svg +28 -0
  198. package/plugins/t3chat/icon.svg +4 -0
  199. package/plugins/t3chat/plugin.js +243 -0
  200. package/plugins/t3chat/plugin.json +20 -0
  201. package/plugins/venice/icon-color.svg +1 -0
  202. package/plugins/venice/icon.svg +1 -0
  203. package/plugins/venice/plugin.js +69 -0
  204. package/plugins/venice/plugin.json +16 -0
  205. package/plugins/vertex-ai/icon-color.svg +1 -0
  206. package/plugins/vertex-ai/icon.svg +1 -0
  207. package/plugins/vertex-ai/plugin.js +172 -0
  208. package/plugins/vertex-ai/plugin.json +18 -0
  209. package/plugins/warp/icon-color.svg +10 -0
  210. package/plugins/warp/icon.svg +6 -0
  211. package/plugins/warp/plugin.js +108 -0
  212. package/plugins/warp/plugin.json +18 -0
  213. package/plugins/windsurf/icon-conflict.svg +3 -0
  214. package/plugins/windsurf/icon.svg +1 -0
  215. package/plugins/windsurf/plugin.js +208 -0
  216. package/plugins/windsurf/plugin.json +16 -0
  217. package/plugins/zai/icon-conflict.svg +5 -0
  218. package/plugins/zai/icon.svg +1 -0
  219. package/plugins/zai/plugin.js +188 -0
  220. package/plugins/zai/plugin.json +16 -0
  221. package/plugins/zed/icon.svg +4 -0
  222. package/plugins/zed/plugin.js +270 -0
  223. package/plugins/zed/plugin.json +19 -0
  224. package/usagestat +0 -0
  225. package/usagestatd +0 -0
@@ -0,0 +1,1723 @@
1
+ (function () {
2
+ const DEFAULT_CLAUDE_HOME = "~/.claude"
3
+ const CRED_FILE_NAME = ".credentials.json"
4
+ const KEYCHAIN_SERVICE_PREFIX = "Claude Code"
5
+ const PROD_BASE_API_URL = "https://api.anthropic.com"
6
+ const PROD_REFRESH_URL = "https://platform.claude.com/v1/oauth/token"
7
+ const ADMIN_COST_REPORT_URL = "https://api.anthropic.com/v1/organizations/cost_report"
8
+ const ADMIN_MESSAGES_USAGE_URL = "https://api.anthropic.com/v1/organizations/usage_report/messages"
9
+ const ANTHROPIC_VERSION = "2023-06-01"
10
+ const ADMIN_DAILY_BUCKETS = 31
11
+ const ADMIN_MAX_PAGES = 100
12
+ const PROD_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
13
+ const NON_PROD_CLIENT_ID = "22422756-60c9-4084-8eb7-27705fd5cf9a"
14
+ const SCOPES =
15
+ "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload"
16
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration
17
+ const CLAUDE_WEB_BASE_URL = "https://claude.ai/api"
18
+
19
+ // Rate-limit state persisted across probe() calls (module scope survives re-invocations).
20
+ const MIN_USAGE_FETCH_INTERVAL_MS = 5 * 60 * 1000 // never poll more than once per 5 min
21
+ const DEFAULT_RATE_LIMIT_BACKOFF_MS = 5 * 60 * 1000 // fallback when no Retry-After header
22
+ const LIVE_USAGE_CACHE_FILE = "live-usage-cache.json"
23
+ const LIVE_USAGE_CACHE_VERSION = 1
24
+ const LIVE_USAGE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
25
+ let rateLimitedUntilMs = 0 // epoch ms; 0 = not rate-limited
26
+ let lastUsageFetchMs = 0 // epoch ms of the most-recent OAuth API attempt
27
+ let cachedUsageData = null // last successful OAuth API response body (parsed JSON)
28
+ let lastWebUsageFetchMs = 0 // epoch ms of the most-recent Web API attempt
29
+ let cachedWebUsageData = null // last successful Web API response body (parsed JSON)
30
+
31
+ function utf8DecodeBytes(bytes) {
32
+ // Prefer native TextDecoder when available (QuickJS may not expose it).
33
+ if (typeof TextDecoder !== "undefined") {
34
+ try {
35
+ return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes))
36
+ } catch {}
37
+ }
38
+
39
+ // Minimal UTF-8 decoder (replacement char on invalid sequences).
40
+ let out = ""
41
+ for (let i = 0; i < bytes.length; ) {
42
+ const b0 = bytes[i] & 0xff
43
+ if (b0 < 0x80) {
44
+ out += String.fromCharCode(b0)
45
+ i += 1
46
+ continue
47
+ }
48
+
49
+ // 2-byte
50
+ if (b0 >= 0xc2 && b0 <= 0xdf) {
51
+ if (i + 1 >= bytes.length) {
52
+ out += "�"
53
+ break
54
+ }
55
+ const b1 = bytes[i + 1] & 0xff
56
+ if ((b1 & 0xc0) !== 0x80) {
57
+ out += "�"
58
+ i += 1
59
+ continue
60
+ }
61
+ const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f)
62
+ out += String.fromCharCode(cp)
63
+ i += 2
64
+ continue
65
+ }
66
+
67
+ // 3-byte
68
+ if (b0 >= 0xe0 && b0 <= 0xef) {
69
+ if (i + 2 >= bytes.length) {
70
+ out += "�"
71
+ break
72
+ }
73
+ const b1 = bytes[i + 1] & 0xff
74
+ const b2 = bytes[i + 2] & 0xff
75
+ const validCont = (b1 & 0xc0) === 0x80 && (b2 & 0xc0) === 0x80
76
+ const notOverlong = !(b0 === 0xe0 && b1 < 0xa0)
77
+ const notSurrogate = !(b0 === 0xed && b1 >= 0xa0)
78
+ if (!validCont || !notOverlong || !notSurrogate) {
79
+ out += "�"
80
+ i += 1
81
+ continue
82
+ }
83
+ const cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)
84
+ out += String.fromCharCode(cp)
85
+ i += 3
86
+ continue
87
+ }
88
+
89
+ // 4-byte
90
+ if (b0 >= 0xf0 && b0 <= 0xf4) {
91
+ if (i + 3 >= bytes.length) {
92
+ out += "�"
93
+ break
94
+ }
95
+ const b1 = bytes[i + 1] & 0xff
96
+ const b2 = bytes[i + 2] & 0xff
97
+ const b3 = bytes[i + 3] & 0xff
98
+ const validCont = (b1 & 0xc0) === 0x80 && (b2 & 0xc0) === 0x80 && (b3 & 0xc0) === 0x80
99
+ const notOverlong = !(b0 === 0xf0 && b1 < 0x90)
100
+ const notTooHigh = !(b0 === 0xf4 && b1 > 0x8f)
101
+ if (!validCont || !notOverlong || !notTooHigh) {
102
+ out += "�"
103
+ i += 1
104
+ continue
105
+ }
106
+ const cp =
107
+ ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f)
108
+ const n = cp - 0x10000
109
+ out += String.fromCharCode(0xd800 + ((n >> 10) & 0x3ff), 0xdc00 + (n & 0x3ff))
110
+ i += 4
111
+ continue
112
+ }
113
+
114
+ out += "�"
115
+ i += 1
116
+ }
117
+ return out
118
+ }
119
+
120
+ function tryParseCredentialJSON(ctx, text) {
121
+ if (!text) return null
122
+ const parsed = ctx.util.tryParseJson(text)
123
+ if (parsed) return parsed
124
+
125
+ // Some macOS keychain items are returned by `security ... -w` as hex-encoded UTF-8 bytes.
126
+ // Example prefix: "7b0a" ( "{\\n" ).
127
+ // Support both plain hex and "0x..." forms.
128
+ let hex = String(text).trim()
129
+ if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
130
+ if (!hex || hex.length % 2 !== 0) return null
131
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null
132
+ try {
133
+ const bytes = []
134
+ for (let i = 0; i < hex.length; i += 2) {
135
+ bytes.push(parseInt(hex.slice(i, i + 2), 16))
136
+ }
137
+ const decoded = utf8DecodeBytes(bytes)
138
+ const decodedParsed = ctx.util.tryParseJson(decoded)
139
+ if (decodedParsed) return decodedParsed
140
+ } catch {}
141
+
142
+ return null
143
+ }
144
+
145
+ function readEnvText(ctx, name) {
146
+ try {
147
+ const value = ctx.host.env.get(name)
148
+ if (value === null || value === undefined) return null
149
+ const text = name === "CLAUDE_CONFIG_DIR" ? String(value) : String(value).trim()
150
+ return text || null
151
+ } catch {
152
+ return null
153
+ }
154
+ }
155
+
156
+ function readEnvFlag(ctx, name) {
157
+ const value = readEnvText(ctx, name)
158
+ if (!value) return false
159
+ const lower = value.toLowerCase()
160
+ return lower !== "0" && lower !== "false" && lower !== "no" && lower !== "off"
161
+ }
162
+
163
+ function cleanText(value) {
164
+ if (value === null || value === undefined) return null
165
+ let text = String(value).trim()
166
+ if (!text) return null
167
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
168
+ text = text.slice(1, -1).trim()
169
+ }
170
+ return text || null
171
+ }
172
+
173
+ function readClaudeAdminApiKey(ctx) {
174
+ const configured = cleanText(ctx.provider && ctx.provider.apiKey)
175
+ if (configured) return configured
176
+
177
+ const envNames = ["ANTHROPIC_ADMIN_KEY", "ANTHROPIC_ADMIN_API_KEY"]
178
+ for (let i = 0; i < envNames.length; i++) {
179
+ const value = cleanText(readEnvText(ctx, envNames[i]))
180
+ if (value) return value
181
+ }
182
+ return null
183
+ }
184
+
185
+ function liveUsageCachePath(ctx) {
186
+ if (!ctx.app || typeof ctx.app.pluginDataDir !== "string" || !ctx.app.pluginDataDir.trim()) {
187
+ return null
188
+ }
189
+ return ctx.app.pluginDataDir + "/" + LIVE_USAGE_CACHE_FILE
190
+ }
191
+
192
+ function readLiveUsageCache(ctx) {
193
+ const path = liveUsageCachePath(ctx)
194
+ if (!path || !ctx.host.fs.exists(path)) return null
195
+ try {
196
+ const parsed = ctx.util.tryParseJson(ctx.host.fs.readText(path))
197
+ if (!parsed || typeof parsed !== "object") return null
198
+ return parsed
199
+ } catch {
200
+ return null
201
+ }
202
+ }
203
+
204
+ function writeLiveUsageCache(ctx, updates) {
205
+ const path = liveUsageCachePath(ctx)
206
+ if (!path) return
207
+ try {
208
+ const cache = readLiveUsageCache(ctx) || {}
209
+ for (const key in updates) {
210
+ cache[key] = updates[key]
211
+ }
212
+ cache.version = LIVE_USAGE_CACHE_VERSION
213
+ cache.updatedAtMs = Date.now()
214
+ ctx.host.fs.writeText(path, JSON.stringify(cache, null, 2))
215
+ } catch (e) {
216
+ ctx.host.log.warn("Claude live usage cache write failed: " + String(e))
217
+ }
218
+ }
219
+
220
+ function cachedLiveUsageData(cache, nowMs) {
221
+ if (!cache || !cache.usageData || typeof cache.usageData !== "object") return null
222
+ const fetchedAtMs = Number(cache.usageFetchedAtMs)
223
+ if (!Number.isFinite(fetchedAtMs) || fetchedAtMs <= 0) return null
224
+ if (nowMs - fetchedAtMs > LIVE_USAGE_CACHE_MAX_AGE_MS) return null
225
+ return cache.usageData
226
+ }
227
+
228
+ function getClaudeHomePath(ctx) {
229
+ return readEnvText(ctx, "CLAUDE_CONFIG_DIR") || DEFAULT_CLAUDE_HOME
230
+ }
231
+
232
+ function getClaudeHomeOverride(ctx) {
233
+ return readEnvText(ctx, "CLAUDE_CONFIG_DIR")
234
+ }
235
+
236
+ function getClaudeCredentialsPath(ctx) {
237
+ return getClaudeHomePath(ctx) + "/" + CRED_FILE_NAME
238
+ }
239
+
240
+ function getOauthConfig(ctx) {
241
+ let baseApiUrl = PROD_BASE_API_URL
242
+ let refreshUrl = PROD_REFRESH_URL
243
+ let clientId = PROD_CLIENT_ID
244
+ let oauthFileSuffix = ""
245
+
246
+ const isAntUser = readEnvText(ctx, "USER_TYPE") === "ant"
247
+ if (isAntUser && readEnvFlag(ctx, "USE_LOCAL_OAUTH")) {
248
+ const localApiBase = readEnvText(ctx, "CLAUDE_LOCAL_OAUTH_API_BASE")
249
+ baseApiUrl = (localApiBase || "http://localhost:8000").replace(/\/+$/, "")
250
+ refreshUrl = baseApiUrl + "/v1/oauth/token"
251
+ clientId = NON_PROD_CLIENT_ID
252
+ oauthFileSuffix = "-local-oauth"
253
+ } else if (isAntUser && readEnvFlag(ctx, "USE_STAGING_OAUTH")) {
254
+ baseApiUrl = "https://api-staging.anthropic.com"
255
+ refreshUrl = "https://platform.staging.ant.dev/v1/oauth/token"
256
+ clientId = NON_PROD_CLIENT_ID
257
+ oauthFileSuffix = "-staging-oauth"
258
+ }
259
+
260
+ const customOauthBase = readEnvText(ctx, "CLAUDE_CODE_CUSTOM_OAUTH_URL")
261
+ if (customOauthBase) {
262
+ const base = customOauthBase.replace(/\/+$/, "")
263
+ baseApiUrl = base
264
+ refreshUrl = base + "/v1/oauth/token"
265
+ oauthFileSuffix = "-custom-oauth"
266
+ }
267
+
268
+ const clientIdOverride = readEnvText(ctx, "CLAUDE_CODE_OAUTH_CLIENT_ID")
269
+ if (clientIdOverride) {
270
+ clientId = clientIdOverride
271
+ }
272
+
273
+ return {
274
+ baseApiUrl: baseApiUrl,
275
+ usageUrl: baseApiUrl + "/api/oauth/usage",
276
+ refreshUrl: refreshUrl,
277
+ clientId: clientId,
278
+ oauthFileSuffix: oauthFileSuffix,
279
+ }
280
+ }
281
+
282
+ function buildClaudeBaseKeychainService(ctx) {
283
+ return KEYCHAIN_SERVICE_PREFIX + getOauthConfig(ctx).oauthFileSuffix + "-credentials"
284
+ }
285
+
286
+ function computeKeychainHashSuffix(ctx) {
287
+ // Mirrors upstream Claude Code (decompiled from the binary):
288
+ // const suffix = !process.env.CLAUDE_CONFIG_DIR
289
+ // ? ""
290
+ // : "-" + sha256(CLAUDE_CONFIG_DIR.normalize("NFC")).slice(0, 8)
291
+ // The hash is ONLY appended when CLAUDE_CONFIG_DIR is explicitly set;
292
+ // when unset, upstream uses the legacy unhashed service name.
293
+ const explicitConfigDir = readEnvText(ctx, "CLAUDE_CONFIG_DIR")
294
+ if (!explicitConfigDir) return null
295
+ const sha256Hex = ctx.host && ctx.host.crypto && ctx.host.crypto.sha256Hex
296
+ if (typeof sha256Hex !== "function") return null
297
+ // Match upstream's `.normalize("NFC")` exactly.
298
+ const normalized =
299
+ typeof explicitConfigDir.normalize === "function"
300
+ ? explicitConfigDir.normalize("NFC")
301
+ : explicitConfigDir
302
+ const digest = sha256Hex(normalized)
303
+ if (typeof digest !== "string" || digest.length < 8) return null
304
+ return digest.slice(0, 8)
305
+ }
306
+
307
+ function getClaudeKeychainServiceCandidates(ctx) {
308
+ const base = buildClaudeBaseKeychainService(ctx)
309
+ if (!readEnvText(ctx, "CLAUDE_CONFIG_DIR")) return [base]
310
+ const hash = computeKeychainHashSuffix(ctx)
311
+ if (!hash) throw {code: "failed", message: "The host cannot resolve this Claude profile's credential service."}
312
+ return [base + "-" + hash]
313
+ }
314
+
315
+ function readKeychainCredentialText(ctx, service) {
316
+ const keychain = ctx.host.keychain
317
+ if (!keychain) return null
318
+ const currentUser = typeof keychain.readGenericPasswordForCurrentUser === "function"
319
+ const read = currentUser ? keychain.readGenericPasswordForCurrentUser : keychain.readGenericPassword
320
+ if (typeof read !== "function") return null
321
+ try {
322
+ const value = read.call(keychain, service)
323
+ return value ? {value, source: currentUser ? "keychain-current-user" : "keychain-legacy"} : null
324
+ } catch (e) {
325
+ // Missing auth can fall back to this profile's file. Denied/locked stores
326
+ // must remain distinguishable, and never broaden the account lookup.
327
+ if (/credential-(denied|unavailable|account-mismatch|malformed):/.test(String(e))) throw e
328
+ return null
329
+ }
330
+ }
331
+
332
+ function loadFileCredentials(ctx) {
333
+ const credFile = getClaudeCredentialsPath(ctx)
334
+ if (ctx.host.fs.exists(credFile)) {
335
+ try {
336
+ const text = ctx.host.fs.readText(credFile)
337
+ const parsed = tryParseCredentialJSON(ctx, text)
338
+ if (parsed) {
339
+ const oauth = parsed.claudeAiOauth
340
+ if (oauth && oauth.accessToken) {
341
+ ctx.host.log.info("credentials loaded from file")
342
+ return { oauth, source: "file", fullData: parsed }
343
+ }
344
+ }
345
+ ctx.host.log.warn("credentials file exists but no valid oauth data")
346
+ } catch (e) {
347
+ ctx.host.log.warn("credentials file read failed: " + String(e))
348
+ }
349
+ }
350
+
351
+ return null
352
+ }
353
+
354
+ function loadKeychainCredentials(ctx) {
355
+ // Upstream uses Keychain on macOS and profile files on Linux/Windows.
356
+ if (ctx.app.platform !== "macos") return null
357
+ // An explicit profile reads only its own hashed service.
358
+ for (const service of getClaudeKeychainServiceCandidates(ctx)) {
359
+ const keychainResult = readKeychainCredentialText(ctx, service)
360
+ if (keychainResult && keychainResult.value) {
361
+ const parsed = tryParseCredentialJSON(ctx, keychainResult.value)
362
+ if (parsed) {
363
+ const oauth = parsed.claudeAiOauth
364
+ if (oauth && oauth.accessToken) {
365
+ ctx.host.log.info("credentials loaded from keychain (service=" + service + ")")
366
+ return { oauth, source: keychainResult.source, serviceName: service, fullData: parsed }
367
+ }
368
+ }
369
+ ctx.host.log.warn("keychain has data for " + service + " but no valid oauth")
370
+ // Continue: a stale legacy entry shouldn't shadow a valid hashed one.
371
+ }
372
+ }
373
+
374
+ return null
375
+ }
376
+
377
+ function loadStoredCredentials(ctx, suppressMissingWarn) {
378
+ // Recent Claude Code versions keep the current session in Keychain and can
379
+ // leave a stale credentials file behind, so Keychain wins when valid.
380
+ let keychainError = null
381
+ let keychainCredentials = null
382
+ try { keychainCredentials = loadKeychainCredentials(ctx) } catch (e) { keychainError = e }
383
+ if (keychainCredentials) return keychainCredentials
384
+
385
+ const fileCredentials = loadFileCredentials(ctx)
386
+ if (fileCredentials) return fileCredentials
387
+
388
+ if (keychainError) throw keychainError
389
+
390
+ if (!suppressMissingWarn) {
391
+ ctx.host.log.warn("no credentials found")
392
+ }
393
+ return null
394
+ }
395
+
396
+ function loadCredentials(ctx) {
397
+ const injected = readInjectedCredential(ctx)
398
+ if (injected) return injected
399
+
400
+ const envAccessToken = readEnvText(ctx, "CLAUDE_CODE_OAUTH_TOKEN")
401
+ const stored = loadStoredCredentials(ctx, !!envAccessToken)
402
+ if (!envAccessToken) {
403
+ return stored
404
+ }
405
+
406
+ const oauth = stored && stored.oauth ? Object.assign({}, stored.oauth) : {}
407
+ oauth.accessToken = envAccessToken
408
+ return {
409
+ oauth: oauth,
410
+ source: stored ? stored.source : null,
411
+ serviceName: stored ? stored.serviceName : null,
412
+ fullData: stored ? stored.fullData : null,
413
+ inferenceOnly: true,
414
+ }
415
+ }
416
+
417
+ function readInjectedCredential(ctx) {
418
+ try {
419
+ if (!ctx.host.credentials || typeof ctx.host.credentials.get !== "function") return null
420
+ const raw = ctx.host.credentials.get()
421
+ if (!raw) return null
422
+ const credential = ctx.util.tryParseJson(String(raw))
423
+ if (!credential) return null
424
+ const accessToken = String(credential.accessToken || credential.sessionKey || "").trim()
425
+ const refreshToken = String(credential.refreshToken || "").trim()
426
+ if (!accessToken && !refreshToken) return null
427
+ return {
428
+ oauth: {
429
+ accessToken,
430
+ refreshToken,
431
+ expiresAt: typeof credential.expiresAt === "number" ? credential.expiresAt : null,
432
+ scopes: ["user:profile"],
433
+ },
434
+ source: "provider-account",
435
+ serviceName: null,
436
+ fullData: { claudeAiOauth: {} },
437
+ }
438
+ } catch (e) {
439
+ ctx.host.log.warn("provider account credential read failed: " + String(e))
440
+ return null
441
+ }
442
+ }
443
+
444
+ function hasProfileScope(creds) {
445
+ if (!creds || creds.inferenceOnly) {
446
+ return false
447
+ }
448
+ const scopes = creds.oauth && creds.oauth.scopes
449
+ if (Array.isArray(scopes) && scopes.length > 0) {
450
+ return scopes.indexOf("user:profile") !== -1
451
+ }
452
+ return true
453
+ }
454
+
455
+ function saveCredentials(ctx, source, serviceName, fullData) {
456
+ if (source === "provider-account") {
457
+ try {
458
+ if (ctx.host.credentials && typeof ctx.host.credentials.update === "function") {
459
+ const oauth = fullData && fullData.claudeAiOauth ? fullData.claudeAiOauth : {}
460
+ ctx.host.credentials.update(JSON.stringify({
461
+ accessToken: oauth.accessToken || null,
462
+ refreshToken: oauth.refreshToken || null,
463
+ expiresAt: oauth.expiresAt || null,
464
+ }))
465
+ }
466
+ } catch (e) {
467
+ ctx.host.log.error("Failed to update provider account credentials: " + String(e))
468
+ }
469
+ return
470
+ }
471
+ // MUST use minified JSON - macOS `security -w` hex-encodes values with newlines,
472
+ // which Claude Code can't read back, causing it to invalidate the session.
473
+ const text = JSON.stringify(fullData)
474
+ if (source === "file") {
475
+ try {
476
+ ctx.host.fs.writeText(getClaudeCredentialsPath(ctx), text)
477
+ } catch (e) {
478
+ ctx.host.log.error("Failed to write Claude credentials file: " + String(e))
479
+ }
480
+ return
481
+ }
482
+ if (!serviceName) {
483
+ ctx.host.log.error("Refusing keychain write: missing service name (source=" + source + ")")
484
+ return
485
+ }
486
+ if (source === "keychain-current-user") {
487
+ try {
488
+ if (typeof ctx.host.keychain.writeGenericPasswordForCurrentUser === "function") {
489
+ ctx.host.keychain.writeGenericPasswordForCurrentUser(serviceName, text)
490
+ } else {
491
+ ctx.host.keychain.writeGenericPassword(serviceName, text)
492
+ }
493
+ } catch (e) {
494
+ ctx.host.log.error("Failed to write Claude credentials keychain: " + String(e))
495
+ }
496
+ } else if (source === "keychain-legacy" || source === "keychain") {
497
+ try {
498
+ ctx.host.keychain.writeGenericPassword(serviceName, text)
499
+ } catch (e) {
500
+ ctx.host.log.error("Failed to write Claude credentials keychain: " + String(e))
501
+ }
502
+ }
503
+ }
504
+
505
+ function needsRefresh(ctx, oauth, nowMs) {
506
+ return ctx.util.needsRefreshByExpiry({
507
+ nowMs,
508
+ expiresAtMs: oauth.expiresAt,
509
+ bufferMs: REFRESH_BUFFER_MS,
510
+ })
511
+ }
512
+
513
+ function refreshToken(ctx, creds) {
514
+ const { oauth, source, fullData } = creds
515
+ if (!oauth.refreshToken) {
516
+ ctx.host.log.warn("refresh skipped: no refresh token")
517
+ return null
518
+ }
519
+
520
+ const oauthConfig = getOauthConfig(ctx)
521
+ ctx.host.log.info("attempting token refresh")
522
+ try {
523
+ const resp = ctx.util.request({
524
+ method: "POST",
525
+ url: oauthConfig.refreshUrl,
526
+ headers: { "Content-Type": "application/json" },
527
+ bodyText: JSON.stringify({
528
+ grant_type: "refresh_token",
529
+ refresh_token: oauth.refreshToken,
530
+ client_id: oauthConfig.clientId,
531
+ scope: SCOPES,
532
+ }),
533
+ timeoutMs: 15000,
534
+ })
535
+
536
+ if (resp.status === 400 || resp.status === 401) {
537
+ let errorCode = null
538
+ const body = ctx.util.tryParseJson(resp.bodyText)
539
+ if (body) errorCode = body.error || body.error_description
540
+ ctx.host.log.error("refresh failed: status=" + resp.status + " error=" + String(errorCode))
541
+ if (errorCode === "invalid_grant") {
542
+ throw "Session expired. Run `claude` to log in again."
543
+ }
544
+ throw "Token expired. Run `claude` to log in again."
545
+ }
546
+ if (resp.status < 200 || resp.status >= 300) {
547
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
548
+ return null
549
+ }
550
+
551
+ const body = ctx.util.tryParseJson(resp.bodyText)
552
+ if (!body) {
553
+ ctx.host.log.warn("refresh response not valid JSON")
554
+ return null
555
+ }
556
+ const newAccessToken = body.access_token
557
+ if (!newAccessToken) {
558
+ ctx.host.log.warn("refresh response missing access_token")
559
+ return null
560
+ }
561
+
562
+ // Update oauth credentials
563
+ oauth.accessToken = newAccessToken
564
+ if (body.refresh_token) oauth.refreshToken = body.refresh_token
565
+ if (typeof body.expires_in === "number") {
566
+ oauth.expiresAt = Date.now() + body.expires_in * 1000
567
+ }
568
+
569
+ // Persist updated credentials back to the same source we read from.
570
+ fullData.claudeAiOauth = oauth
571
+ saveCredentials(ctx, source, creds.serviceName, fullData)
572
+
573
+ ctx.host.log.info("refresh succeeded, new token expires in " + (body.expires_in || "unknown") + "s")
574
+ return newAccessToken
575
+ } catch (e) {
576
+ if (typeof e === "string") throw e
577
+ ctx.host.log.error("refresh exception: " + String(e))
578
+ return null
579
+ }
580
+ }
581
+
582
+ function fetchUsage(ctx, accessToken) {
583
+ const oauthConfig = getOauthConfig(ctx)
584
+ return ctx.util.request({
585
+ method: "GET",
586
+ url: oauthConfig.usageUrl,
587
+ headers: {
588
+ Authorization: "Bearer " + accessToken.trim(),
589
+ Accept: "application/json",
590
+ "Content-Type": "application/json",
591
+ "anthropic-beta": "oauth-2025-04-20",
592
+ "User-Agent": "claude-code/2.1.69",
593
+ },
594
+ timeoutMs: 10000,
595
+ })
596
+ }
597
+
598
+ function parseRetryAfterSeconds(headers) {
599
+ if (!headers) return null
600
+ const raw = headers["retry-after"] ?? headers["Retry-After"]
601
+ if (raw === undefined || raw === null) return null
602
+ const str = String(raw).trim()
603
+ if (!str) return null
604
+ // Retry-After can be a delay-seconds or HTTP-date (RFC 7231).
605
+ // 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
608
+ const dateMs = Date.parse(str)
609
+ if (Number.isFinite(dateMs)) {
610
+ const delay = Math.ceil((dateMs - Date.now()) / 1000)
611
+ return delay > 0 ? delay : 0
612
+ }
613
+ return null
614
+ }
615
+
616
+ function fmtRateLimitMinutes(seconds) {
617
+ if (seconds <= 0) return "now"
618
+ const mins = Math.ceil(seconds / 60)
619
+ return mins + "m"
620
+ }
621
+
622
+ function queryTokenUsage(ctx, homePath) {
623
+ if (!ctx.host.ccusage || typeof ctx.host.ccusage.query !== "function") {
624
+ return { status: "unavailable", data: null }
625
+ }
626
+
627
+ const since = new Date()
628
+ // Inclusive range: today + previous 30 days = 31 calendar days.
629
+ since.setDate(since.getDate() - 30)
630
+ const y = since.getFullYear()
631
+ const m = since.getMonth() + 1
632
+ const d = since.getDate()
633
+ const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d
634
+
635
+ const queryOpts = { since: sinceStr }
636
+ if (homePath) {
637
+ queryOpts.homePath = homePath
638
+ }
639
+
640
+ const result = ctx.host.ccusage.query(queryOpts)
641
+ if (!result || typeof result !== "object" || typeof result.status !== "string") {
642
+ return { status: "runner_failed", data: null }
643
+ }
644
+ if (result.status !== "ok") {
645
+ return { status: result.status, data: null }
646
+ }
647
+ if (!result.data || !Array.isArray(result.data.daily)) {
648
+ return { status: "runner_failed", data: null }
649
+ }
650
+ return { status: "ok", data: result.data }
651
+ }
652
+
653
+ function adminIsoString(ms) {
654
+ return new Date(ms).toISOString().replace(".000Z", "Z")
655
+ }
656
+
657
+ function adminRange(nowMs) {
658
+ const now = new Date(nowMs)
659
+ const todayStartMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
660
+ const startMs = todayStartMs - (ADMIN_DAILY_BUCKETS - 1) * 24 * 60 * 60 * 1000
661
+ const endMs = todayStartMs + 24 * 60 * 60 * 1000
662
+ return { startingAt: adminIsoString(startMs), endingAt: adminIsoString(endMs) }
663
+ }
664
+
665
+ function makeAdminUrl(baseUrl, params) {
666
+ const query = []
667
+ const keys = Object.keys(params)
668
+ for (let i = 0; i < keys.length; i++) {
669
+ const key = keys[i]
670
+ const value = params[key]
671
+ if (value === null || value === undefined || value === "") continue
672
+ query.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(value)))
673
+ }
674
+ return baseUrl + (query.length ? "?" + query.join("&") : "")
675
+ }
676
+
677
+ function makeAdminError(message, status, endpoint) {
678
+ return {
679
+ message,
680
+ status,
681
+ endpoint,
682
+ authRejected: status === 401 || status === 403,
683
+ }
684
+ }
685
+
686
+ function requestClaudeAdminJson(ctx, url, apiKey, endpoint) {
687
+ let resp
688
+ try {
689
+ resp = ctx.util.request({
690
+ method: "GET",
691
+ url,
692
+ headers: {
693
+ "x-api-key": apiKey,
694
+ "anthropic-version": ANTHROPIC_VERSION,
695
+ Accept: "application/json",
696
+ "User-Agent": "UsageStat/1.0",
697
+ },
698
+ timeoutMs: 20000,
699
+ })
700
+ } catch (e) {
701
+ throw makeAdminError("Claude Admin API " + endpoint + " request failed: " + String(e), 0, endpoint)
702
+ }
703
+
704
+ if (ctx.util.isAuthStatus(resp.status)) {
705
+ throw makeAdminError("Claude Admin API key invalid or missing required permissions.", resp.status, endpoint)
706
+ }
707
+ if (resp.status < 200 || resp.status >= 300) {
708
+ throw makeAdminError("Claude Admin API " + endpoint + " failed (HTTP " + resp.status + ").", resp.status, endpoint)
709
+ }
710
+
711
+ const json = ctx.util.tryParseJson(resp.bodyText)
712
+ if (!json) throw makeAdminError("Claude Admin API " + endpoint + " response invalid.", resp.status, endpoint)
713
+ return json
714
+ }
715
+
716
+ function fetchClaudeAdminReport(ctx, apiKey, url, endpoint, groupBy) {
717
+ const range = adminRange(Date.now())
718
+ const buckets = []
719
+ let nextPage = null
720
+ const seenPages = {}
721
+ let pageCount = 0
722
+
723
+ do {
724
+ pageCount += 1
725
+ if (pageCount > ADMIN_MAX_PAGES) {
726
+ throw makeAdminError("Claude Admin API " + endpoint + " pagination exceeded " + ADMIN_MAX_PAGES + " pages.", 200, endpoint)
727
+ }
728
+
729
+ const params = {
730
+ starting_at: range.startingAt,
731
+ ending_at: range.endingAt,
732
+ bucket_width: "1d",
733
+ limit: ADMIN_DAILY_BUCKETS,
734
+ "group_by[]": groupBy,
735
+ }
736
+ if (nextPage) params.page = nextPage
737
+
738
+ const json = requestClaudeAdminJson(ctx, makeAdminUrl(url, params), apiKey, endpoint)
739
+ if (Array.isArray(json.data)) {
740
+ for (let i = 0; i < json.data.length; i++) buckets.push(json.data[i])
741
+ }
742
+
743
+ if (!json.has_more) {
744
+ nextPage = null
745
+ continue
746
+ }
747
+ nextPage = cleanText(json.next_page)
748
+ if (!nextPage) {
749
+ throw makeAdminError("Claude Admin API " + endpoint + " pagination cursor missing.", 200, endpoint)
750
+ }
751
+ if (seenPages[nextPage]) {
752
+ throw makeAdminError("Claude Admin API " + endpoint + " pagination cursor repeated.", 200, endpoint)
753
+ }
754
+ seenPages[nextPage] = true
755
+ } while (nextPage)
756
+
757
+ return buckets
758
+ }
759
+
760
+ function numberValue(value) {
761
+ if (typeof value === "number" && Number.isFinite(value)) return value
762
+ if (typeof value === "string" && value.trim()) {
763
+ const parsed = Number(value.trim().replace(/,/g, ""))
764
+ if (Number.isFinite(parsed)) return parsed
765
+ }
766
+ return null
767
+ }
768
+
769
+ function intValue(value) {
770
+ const n = numberValue(value)
771
+ return n === null ? 0 : Math.max(0, Math.floor(n))
772
+ }
773
+
774
+ function displayName(raw, fallback) {
775
+ return cleanText(raw) || fallback
776
+ }
777
+
778
+ function dayKeyFromAdminDate(rawDate) {
779
+ const ms = Date.parse(rawDate)
780
+ if (!Number.isFinite(ms)) return null
781
+ return new Date(ms).toISOString().slice(0, 10)
782
+ }
783
+
784
+ function adminDailyBucket(map, startingAt, endingAt) {
785
+ const dayKey = dayKeyFromAdminDate(startingAt)
786
+ if (!dayKey) return null
787
+ if (!map[dayKey]) {
788
+ map[dayKey] = {
789
+ date: dayKey,
790
+ startingAt,
791
+ endingAt,
792
+ costUSD: 0,
793
+ inputTokens: 0,
794
+ cacheCreationTokens: 0,
795
+ cacheReadTokens: 0,
796
+ outputTokens: 0,
797
+ totalTokens: 0,
798
+ costItems: {},
799
+ models: {},
800
+ }
801
+ }
802
+ return map[dayKey]
803
+ }
804
+
805
+ function addAdminModel(day, name, input, cacheCreation, cacheRead, output, total) {
806
+ if (!day.models[name]) {
807
+ day.models[name] = {
808
+ modelName: name,
809
+ inputTokens: 0,
810
+ cacheCreationTokens: 0,
811
+ cacheReadTokens: 0,
812
+ outputTokens: 0,
813
+ totalTokens: 0,
814
+ }
815
+ }
816
+ const model = day.models[name]
817
+ model.inputTokens += input
818
+ model.cacheCreationTokens += cacheCreation
819
+ model.cacheReadTokens += cacheRead
820
+ model.outputTokens += output
821
+ model.totalTokens += total
822
+ }
823
+
824
+ function cacheCreationTokenCount(cacheCreation) {
825
+ if (!cacheCreation || typeof cacheCreation !== "object") return 0
826
+ const total = intValue(cacheCreation.total_input_tokens)
827
+ if (total > 0) return total
828
+ return intValue(cacheCreation.ephemeral_1h_input_tokens) +
829
+ intValue(cacheCreation.ephemeral_5m_input_tokens)
830
+ }
831
+
832
+ function makeClaudeAdminDaily(costBuckets, messageBuckets) {
833
+ const map = {}
834
+
835
+ for (let i = 0; i < costBuckets.length; i++) {
836
+ const bucket = costBuckets[i] || {}
837
+ const day = adminDailyBucket(map, bucket.starting_at, bucket.ending_at)
838
+ if (!day) continue
839
+ const results = Array.isArray(bucket.results) ? bucket.results : []
840
+ for (let j = 0; j < results.length; j++) {
841
+ const result = results[j] || {}
842
+ const amount = numberValue(result.amount)
843
+ const costUSD = amount === null ? 0 : amount / 100
844
+ day.costUSD += costUSD
845
+ const name = displayName(result.description || result.cost_type, "Claude API")
846
+ day.costItems[name] = (day.costItems[name] || 0) + costUSD
847
+ }
848
+ }
849
+
850
+ for (let i = 0; i < messageBuckets.length; i++) {
851
+ const bucket = messageBuckets[i] || {}
852
+ const day = adminDailyBucket(map, bucket.starting_at, bucket.ending_at)
853
+ if (!day) continue
854
+ const results = Array.isArray(bucket.results) ? bucket.results : []
855
+ for (let j = 0; j < results.length; j++) {
856
+ const result = results[j] || {}
857
+ const input = intValue(result.uncached_input_tokens)
858
+ const cacheCreation = cacheCreationTokenCount(result.cache_creation)
859
+ const cacheRead = intValue(result.cache_read_input_tokens)
860
+ const output = intValue(result.output_tokens)
861
+ const total = input + cacheCreation + cacheRead + output
862
+ day.inputTokens += input
863
+ day.cacheCreationTokens += cacheCreation
864
+ day.cacheReadTokens += cacheRead
865
+ day.outputTokens += output
866
+ day.totalTokens += total
867
+ addAdminModel(day, displayName(result.model, "Claude API"), input, cacheCreation, cacheRead, output, total)
868
+ }
869
+ }
870
+
871
+ return Object.keys(map)
872
+ .sort()
873
+ .map((key) => {
874
+ const day = map[key]
875
+ const modelBreakdowns = Object.keys(day.models)
876
+ .map((name) => day.models[name])
877
+ .sort((a, b) => b.totalTokens - a.totalTokens || a.modelName.localeCompare(b.modelName))
878
+ const costItems = Object.keys(day.costItems)
879
+ .map((name) => ({ name, costUSD: day.costItems[name] }))
880
+ .sort((a, b) => b.costUSD - a.costUSD || a.name.localeCompare(b.name))
881
+ return {
882
+ date: day.date,
883
+ inputTokens: day.inputTokens,
884
+ cacheCreationTokens: day.cacheCreationTokens,
885
+ cacheReadTokens: day.cacheReadTokens,
886
+ outputTokens: day.outputTokens,
887
+ totalTokens: day.totalTokens,
888
+ costUSD: day.costUSD,
889
+ totalCost: day.costUSD,
890
+ modelsUsed: modelBreakdowns.map((model) => model.modelName),
891
+ modelBreakdowns,
892
+ costItems,
893
+ }
894
+ })
895
+ }
896
+
897
+ function fetchClaudeAdminUsage(ctx, apiKey) {
898
+ const costs = fetchClaudeAdminReport(ctx, apiKey, ADMIN_COST_REPORT_URL, "cost_report", "description")
899
+ const messages = fetchClaudeAdminReport(ctx, apiKey, ADMIN_MESSAGES_USAGE_URL, "messages", "model")
900
+ return makeClaudeAdminDaily(costs, messages)
901
+ }
902
+
903
+ function fmtTokens(n) {
904
+ const abs = Math.abs(n)
905
+ const sign = n < 0 ? "-" : ""
906
+ const units = [
907
+ { threshold: 1e9, divisor: 1e9, suffix: "B" },
908
+ { threshold: 1e6, divisor: 1e6, suffix: "M" },
909
+ { threshold: 1e3, divisor: 1e3, suffix: "K" },
910
+ ]
911
+ for (let i = 0; i < units.length; i++) {
912
+ const unit = units[i]
913
+ if (abs >= unit.threshold) {
914
+ const scaled = abs / unit.divisor
915
+ const formatted = scaled >= 10
916
+ ? Math.round(scaled).toString()
917
+ : scaled.toFixed(1).replace(/\.0$/, "")
918
+ return sign + formatted + unit.suffix
919
+ }
920
+ }
921
+ return sign + Math.round(abs).toString()
922
+ }
923
+
924
+ function dayKeyFromDate(date) {
925
+ const year = date.getFullYear()
926
+ const month = date.getMonth() + 1
927
+ const day = date.getDate()
928
+ return year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day
929
+ }
930
+
931
+ function dayKeyFromUsageDate(rawDate) {
932
+ if (typeof rawDate !== "string") return null
933
+ const value = rawDate.trim()
934
+ if (!value) return null
935
+
936
+ const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/)
937
+ if (isoMatch) {
938
+ return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3]
939
+ }
940
+
941
+ const isoDatePrefixMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[Tt\s]|$)/)
942
+ if (isoDatePrefixMatch) {
943
+ return isoDatePrefixMatch[1] + "-" + isoDatePrefixMatch[2] + "-" + isoDatePrefixMatch[3]
944
+ }
945
+
946
+ const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/)
947
+ if (compactMatch) {
948
+ return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3]
949
+ }
950
+
951
+ const ms = Date.parse(value)
952
+ if (!Number.isFinite(ms)) return null
953
+ return dayKeyFromDate(new Date(ms))
954
+ }
955
+
956
+ function usageCostUsd(day) {
957
+ if (!day || typeof day !== "object") return null
958
+
959
+ if (day.totalCost != null) {
960
+ const totalCost = Number(day.totalCost)
961
+ if (Number.isFinite(totalCost)) return totalCost
962
+ }
963
+
964
+ if (day.costUSD != null) {
965
+ const costUSD = Number(day.costUSD)
966
+ if (Number.isFinite(costUSD)) return costUSD
967
+ }
968
+
969
+ return null
970
+ }
971
+
972
+ function costAndTokensLabel(data, opts) {
973
+ const includeZeroTokens = !!(opts && opts.includeZeroTokens)
974
+ const parts = []
975
+ if (data.costUSD != null) parts.push("$" + data.costUSD.toFixed(2))
976
+ if (data.tokens > 0 || (includeZeroTokens && data.tokens === 0)) {
977
+ parts.push(fmtTokens(data.tokens) + " tokens")
978
+ }
979
+ return parts.join(" · ")
980
+ }
981
+
982
+ function usageDayLabel(rawDate) {
983
+ const key = dayKeyFromUsageDate(rawDate)
984
+ if (!key) return String(rawDate || "").slice(0, 10) || "Usage"
985
+ const month = Number(key.slice(5, 7))
986
+ const day = Number(key.slice(8, 10))
987
+ return month + "/" + day
988
+ }
989
+
990
+ function collectUsageChartPoints(daily) {
991
+ const hasCost = daily.some((day) => {
992
+ const cost = usageCostUsd(day)
993
+ return cost !== null && cost > 0
994
+ })
995
+ const points = []
996
+ for (let i = 0; i < daily.length; i++) {
997
+ const day = daily[i]
998
+ const tokens = Number(day && day.totalTokens)
999
+ if (!Number.isFinite(tokens) || tokens < 0) continue
1000
+ const key = dayKeyFromUsageDate(day.date)
1001
+ if (!key) continue
1002
+ const cost = usageCostUsd(day) || 0
1003
+ points.push({
1004
+ key: key,
1005
+ label: usageDayLabel(day.date),
1006
+ value: hasCost ? cost : tokens,
1007
+ valueLabel: hasCost
1008
+ ? "$" + cost.toFixed(2) + " · " + fmtTokens(tokens) + " tokens"
1009
+ : fmtTokens(tokens) + " tokens",
1010
+ })
1011
+ }
1012
+ return points
1013
+ .sort((a, b) => a.key.localeCompare(b.key))
1014
+ .slice(-31)
1015
+ .map((point) => ({
1016
+ label: point.label,
1017
+ value: point.value,
1018
+ valueLabel: point.valueLabel,
1019
+ }))
1020
+ }
1021
+
1022
+ function pushUsageChartLine(lines, ctx, daily, note) {
1023
+ const points = collectUsageChartPoints(daily)
1024
+ if (points.length === 0) return
1025
+ lines.push(ctx.line.barChart({
1026
+ label: "Usage Trend",
1027
+ points: points,
1028
+ note: note || "Estimated from local Claude logs at API rates.",
1029
+ color: "#DE7356",
1030
+ }))
1031
+ }
1032
+
1033
+ function persistUsageDaily(ctx, daily, displayName, source) {
1034
+ if (!ctx.host.usageDaily || typeof ctx.host.usageDaily.ingest !== "function") return
1035
+ if (!daily || !daily.length) return
1036
+ try {
1037
+ const payload = { displayName: displayName, daily: daily }
1038
+ if (source) payload.source = source
1039
+ ctx.host.usageDaily.ingest(payload)
1040
+ } catch (e) { /* ignore */ }
1041
+ }
1042
+
1043
+ function pushDayUsageLine(lines, ctx, label, dayEntry) {
1044
+ const tokens = Number(dayEntry && dayEntry.totalTokens) || 0
1045
+ const cost = usageCostUsd(dayEntry)
1046
+ if (tokens > 0 || cost !== null) {
1047
+ lines.push(ctx.line.text({
1048
+ label: label,
1049
+ value: costAndTokensLabel(
1050
+ { tokens: tokens, costUSD: cost === null ? 0 : cost },
1051
+ { includeZeroTokens: true }
1052
+ )
1053
+ }))
1054
+ return
1055
+ }
1056
+
1057
+ lines.push(ctx.line.text({
1058
+ label: label,
1059
+ value: costAndTokensLabel({ tokens: 0, costUSD: 0 }, { includeZeroTokens: true })
1060
+ }))
1061
+ }
1062
+
1063
+ function summarizeUsageDaily(daily) {
1064
+ const summary = { tokens: 0, costUSD: 0, hasCost: false }
1065
+ const selectedDaily = (daily || []).slice(-30)
1066
+ for (let i = 0; i < selectedDaily.length; i++) {
1067
+ const day = selectedDaily[i]
1068
+ const dayTokens = Number(day && day.totalTokens)
1069
+ if (Number.isFinite(dayTokens)) summary.tokens += dayTokens
1070
+ const dayCost = usageCostUsd(day)
1071
+ if (dayCost !== null) {
1072
+ summary.costUSD += dayCost
1073
+ summary.hasCost = true
1074
+ }
1075
+ }
1076
+ return summary
1077
+ }
1078
+
1079
+ function appendUsageDailyLines(lines, ctx, daily, chartNote) {
1080
+ if (!daily || !daily.length) return false
1081
+ const now = new Date()
1082
+ const todayKey = dayKeyFromDate(now)
1083
+ const yesterday = new Date(now.getTime())
1084
+ yesterday.setDate(yesterday.getDate() - 1)
1085
+ const yesterdayKey = dayKeyFromDate(yesterday)
1086
+
1087
+ let todayEntry = null
1088
+ let yesterdayEntry = null
1089
+ for (let i = 0; i < daily.length; i++) {
1090
+ const usageDayKey = dayKeyFromUsageDate(daily[i].date)
1091
+ if (usageDayKey === todayKey) {
1092
+ todayEntry = daily[i]
1093
+ continue
1094
+ }
1095
+ if (usageDayKey === yesterdayKey) {
1096
+ yesterdayEntry = daily[i]
1097
+ }
1098
+ }
1099
+
1100
+ pushDayUsageLine(lines, ctx, "Today", todayEntry)
1101
+ pushDayUsageLine(lines, ctx, "Yesterday", yesterdayEntry)
1102
+
1103
+ const summary = summarizeUsageDaily(daily)
1104
+ if (summary.tokens > 0 || summary.hasCost) {
1105
+ lines.push(ctx.line.text({
1106
+ label: "Last 30 Days",
1107
+ value: costAndTokensLabel({
1108
+ tokens: summary.tokens,
1109
+ costUSD: summary.hasCost ? summary.costUSD : null,
1110
+ }, { includeZeroTokens: true })
1111
+ }))
1112
+ }
1113
+
1114
+ pushUsageChartLine(lines, ctx, daily, chartNote)
1115
+ return summary.tokens > 0 || summary.hasCost
1116
+ }
1117
+
1118
+ // ── Web mode helpers ───────────────────────────────────────────────────────
1119
+
1120
+ function extractSessionKey(value) {
1121
+ if (!value) return null
1122
+ const raw = String(value).trim()
1123
+ if (!raw) return null
1124
+ const cookieMatch = raw.match(/(?:^|;\s*)sessionKey=([^;]+)/i)
1125
+ if (cookieMatch && cookieMatch[1]) return cookieMatch[1].trim() || null
1126
+ const bare = raw.replace(/^sessionKey=/i, "").trim()
1127
+ return bare || null
1128
+ }
1129
+
1130
+ function providerCookieHeader(ctx) {
1131
+ if (ctx.provider && typeof ctx.provider.cookieHeader === "string") {
1132
+ return ctx.provider.cookieHeader
1133
+ }
1134
+ return null
1135
+ }
1136
+
1137
+ function getSessionKey(ctx) {
1138
+ const candidates = [
1139
+ readEnvText(ctx, "CLAUDE_AI_SESSION_KEY"),
1140
+ readEnvText(ctx, "CLAUDE_WEB_SESSION_KEY"),
1141
+ providerCookieHeader(ctx),
1142
+ ]
1143
+ for (let i = 0; i < candidates.length; i++) {
1144
+ const key = extractSessionKey(candidates[i])
1145
+ if (key) return key
1146
+ }
1147
+ return null
1148
+ }
1149
+
1150
+ function buildWebHeaders(sessionKey) {
1151
+ return {
1152
+ Cookie: "sessionKey=" + sessionKey,
1153
+ Accept: "application/json",
1154
+ Origin: "https://claude.ai",
1155
+ Referer: "https://claude.ai/settings/usage",
1156
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
1157
+ "anthropic-client-platform": "web_claude_ai",
1158
+ }
1159
+ }
1160
+
1161
+ function fetchWebUsage(ctx, sessionKey) {
1162
+ const headers = buildWebHeaders(sessionKey)
1163
+
1164
+ let orgResp
1165
+ try {
1166
+ orgResp = ctx.util.request({
1167
+ method: "GET",
1168
+ url: CLAUDE_WEB_BASE_URL + "/organizations",
1169
+ headers: headers,
1170
+ timeoutMs: 10000,
1171
+ })
1172
+ } catch (e) {
1173
+ ctx.host.log.error("web mode: org request failed: " + String(e))
1174
+ throw "Web usage request failed. Check your connection."
1175
+ }
1176
+
1177
+ if (orgResp.status === 401 || orgResp.status === 403) {
1178
+ throw "Web session invalid or expired. Set CLAUDE_AI_SESSION_KEY to your claude.ai sessionKey cookie."
1179
+ }
1180
+ if (orgResp.status < 200 || orgResp.status >= 300) {
1181
+ throw "Web usage request failed (HTTP " + orgResp.status + "). Try again later."
1182
+ }
1183
+
1184
+ const orgs = ctx.util.tryParseJson(orgResp.bodyText)
1185
+ if (!Array.isArray(orgs) || orgs.length === 0) {
1186
+ throw "No Claude organizations found in web response."
1187
+ }
1188
+ let orgId = null
1189
+ let usageData = null
1190
+ let lastUsageStatus = null
1191
+ for (let i = 0; i < orgs.length; i++) {
1192
+ const candidateOrgId = orgs[i] && orgs[i].uuid
1193
+ if (!candidateOrgId) continue
1194
+
1195
+ let usageResp
1196
+ try {
1197
+ usageResp = ctx.util.request({
1198
+ method: "GET",
1199
+ url: CLAUDE_WEB_BASE_URL + "/organizations/" + candidateOrgId + "/usage",
1200
+ headers: headers,
1201
+ timeoutMs: 10000,
1202
+ })
1203
+ } catch (e) {
1204
+ ctx.host.log.error("web mode: usage request failed: " + String(e))
1205
+ throw "Web usage request failed. Check your connection."
1206
+ }
1207
+
1208
+ lastUsageStatus = usageResp.status
1209
+ if (usageResp.status === 401 || usageResp.status === 403) {
1210
+ ctx.host.log.info("web mode: usage unavailable for org " + candidateOrgId + " (HTTP " + usageResp.status + ")")
1211
+ continue
1212
+ }
1213
+ if (usageResp.status < 200 || usageResp.status >= 300) {
1214
+ throw "Web usage request failed (HTTP " + usageResp.status + "). Try again later."
1215
+ }
1216
+
1217
+ const parsedUsageData = ctx.util.tryParseJson(usageResp.bodyText)
1218
+ if (!parsedUsageData) throw "Web usage response invalid. Try again later."
1219
+ orgId = candidateOrgId
1220
+ usageData = parsedUsageData
1221
+ break
1222
+ }
1223
+
1224
+ if (!usageData) {
1225
+ if (lastUsageStatus === 401) {
1226
+ throw "Web session invalid or expired. Set CLAUDE_AI_SESSION_KEY to your claude.ai sessionKey cookie."
1227
+ }
1228
+ if (lastUsageStatus === 403) {
1229
+ throw "No Claude organization with usage access found for this web session."
1230
+ }
1231
+ throw "Organization UUID missing in web response."
1232
+ }
1233
+
1234
+ let accountInfo = null
1235
+ try {
1236
+ const accResp = ctx.util.request({
1237
+ method: "GET",
1238
+ url: CLAUDE_WEB_BASE_URL + "/account",
1239
+ headers: headers,
1240
+ timeoutMs: 5000,
1241
+ })
1242
+ if (accResp.status >= 200 && accResp.status < 300) {
1243
+ accountInfo = ctx.util.tryParseJson(accResp.bodyText)
1244
+ }
1245
+ } catch {
1246
+ // Account info is optional
1247
+ }
1248
+
1249
+ ctx.host.log.info("web mode: usage fetched for org " + orgId)
1250
+ return { usageData: usageData, accountInfo: accountInfo }
1251
+ }
1252
+
1253
+ function loadWebUsageData(ctx, sessionKey, nowMs) {
1254
+ const liveCache = readLiveUsageCache(ctx)
1255
+ const cachedLastWebUsageFetchMs = Number(liveCache && liveCache.lastWebUsageFetchMs) || 0
1256
+ const effectiveLastWebUsageFetchMs = Math.max(lastWebUsageFetchMs, cachedLastWebUsageFetchMs)
1257
+ if (nowMs - effectiveLastWebUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
1258
+ lastWebUsageFetchMs = effectiveLastWebUsageFetchMs
1259
+ ctx.host.log.info(
1260
+ "web usage fetch skipped: last fetch was " +
1261
+ Math.round((nowMs - effectiveLastWebUsageFetchMs) / 1000) + "s ago"
1262
+ )
1263
+ return { data: cachedWebUsageData || cachedLiveUsageData(liveCache, nowMs), plan: null }
1264
+ }
1265
+
1266
+ lastWebUsageFetchMs = nowMs
1267
+ const result = fetchWebUsage(ctx, sessionKey)
1268
+ cachedWebUsageData = result.usageData
1269
+ writeLiveUsageCache(ctx, {
1270
+ usageData: result.usageData,
1271
+ usageFetchedAtMs: nowMs,
1272
+ lastWebUsageFetchMs: nowMs,
1273
+ })
1274
+ let plan = null
1275
+ if (result.accountInfo && result.accountInfo.rate_limit_tier) {
1276
+ plan = webTierToPlanLabel(result.accountInfo.rate_limit_tier)
1277
+ }
1278
+ return { data: result.usageData, plan: plan }
1279
+ }
1280
+
1281
+ function webTierToPlanLabel(tier) {
1282
+ const t = (tier || "").toLowerCase()
1283
+ if (t === "free") return "Claude Free"
1284
+ if (t === "pro" || t === "claude_pro") return "Claude Pro"
1285
+ if (t === "max" || t === "claude_max_5" || t === "claude_max_20") return "Claude Max"
1286
+ if (t === "team") return "Claude Team"
1287
+ if (t === "enterprise") return "Claude Enterprise"
1288
+ return "Claude (" + tier + ")"
1289
+ }
1290
+
1291
+ // ── Usage window rendering ─────────────────────────────────────────────────
1292
+
1293
+ function pickUsageWindow(data, aliases) {
1294
+ for (let i = 0; i < aliases.length; i++) {
1295
+ const w = data[aliases[i]]
1296
+ if (w && typeof w === "object") return w
1297
+ }
1298
+ return null
1299
+ }
1300
+
1301
+ function moneyMajorUnits(value) {
1302
+ const n = Number(value)
1303
+ if (!Number.isFinite(n)) return null
1304
+ // Claude usage APIs return Extra usage amounts in minor units (cents).
1305
+ return Math.round(n) / 100
1306
+ }
1307
+
1308
+ function currencySymbol(code) {
1309
+ const c = String(code || "").trim().toUpperCase()
1310
+ if (c === "EUR") return "€"
1311
+ if (c === "GBP") return "£"
1312
+ if (c === "JPY") return "¥"
1313
+ return "$"
1314
+ }
1315
+
1316
+ function formatMoneyAmount(value, code) {
1317
+ if (typeof value !== "number" || !Number.isFinite(value)) return ""
1318
+ const symbol = currencySymbol(code)
1319
+ return symbol + value.toFixed(2).replace(/\.00$/, "")
1320
+ }
1321
+
1322
+ function nextMonthResetIso(nowIso) {
1323
+ const now = new Date(nowIso || Date.now())
1324
+ if (Number.isNaN(now.getTime())) return null
1325
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0)).toISOString()
1326
+ }
1327
+
1328
+ function formatMonthDay(iso) {
1329
+ if (!iso) return null
1330
+ const d = new Date(iso)
1331
+ if (Number.isNaN(d.getTime())) return null
1332
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
1333
+ return months[d.getUTCMonth()] + " " + String(d.getUTCDate())
1334
+ }
1335
+
1336
+ function addUsageWindowLines(ctx, data, lines) {
1337
+ const fiveHour = data.five_hour
1338
+ if (fiveHour && typeof fiveHour.utilization === "number") {
1339
+ lines.push(ctx.line.progress({
1340
+ label: "Session",
1341
+ used: fiveHour.utilization,
1342
+ limit: 100,
1343
+ format: { kind: "percent" },
1344
+ resetsAt: ctx.util.toIso(fiveHour.resets_at),
1345
+ periodDurationMs: 5 * 60 * 60 * 1000,
1346
+ }))
1347
+ }
1348
+
1349
+ const sevenDay = data.seven_day
1350
+ if (sevenDay && typeof sevenDay.utilization === "number") {
1351
+ lines.push(ctx.line.progress({
1352
+ label: "Weekly",
1353
+ used: sevenDay.utilization,
1354
+ limit: 100,
1355
+ format: { kind: "percent" },
1356
+ resetsAt: ctx.util.toIso(sevenDay.resets_at),
1357
+ periodDurationMs: 7 * 24 * 60 * 60 * 1000,
1358
+ }))
1359
+ }
1360
+
1361
+ const sevenDaySonnet = data.seven_day_sonnet
1362
+ if (sevenDaySonnet && typeof sevenDaySonnet.utilization === "number") {
1363
+ lines.push(ctx.line.progress({
1364
+ label: "Sonnet",
1365
+ used: sevenDaySonnet.utilization,
1366
+ limit: 100,
1367
+ format: { kind: "percent" },
1368
+ resetsAt: ctx.util.toIso(sevenDaySonnet.resets_at),
1369
+ periodDurationMs: 7 * 24 * 60 * 60 * 1000,
1370
+ }))
1371
+ }
1372
+
1373
+ const sevenDayOpus = data.seven_day_opus
1374
+ if (sevenDayOpus && typeof sevenDayOpus.utilization === "number") {
1375
+ lines.push(ctx.line.progress({
1376
+ label: "Opus",
1377
+ used: sevenDayOpus.utilization,
1378
+ limit: 100,
1379
+ format: { kind: "percent" },
1380
+ resetsAt: ctx.util.toIso(sevenDayOpus.resets_at),
1381
+ periodDurationMs: 7 * 24 * 60 * 60 * 1000,
1382
+ }))
1383
+ }
1384
+
1385
+ // Routines: check canonical name first, then legacy aliases
1386
+ const routinesWindow = pickUsageWindow(data, [
1387
+ "seven_day_routines",
1388
+ "seven_day_cowork",
1389
+ "seven_day_claude_routines",
1390
+ ])
1391
+ if (routinesWindow && typeof routinesWindow.utilization === "number") {
1392
+ lines.push(ctx.line.progress({
1393
+ label: "Routines",
1394
+ used: routinesWindow.utilization,
1395
+ limit: 100,
1396
+ format: { kind: "percent" },
1397
+ resetsAt: ctx.util.toIso(routinesWindow.resets_at),
1398
+ periodDurationMs: 7 * 24 * 60 * 60 * 1000,
1399
+ }))
1400
+ }
1401
+
1402
+ if (data.extra_usage && data.extra_usage.is_enabled) {
1403
+ const used = moneyMajorUnits(data.extra_usage.used_credits)
1404
+ const limit = moneyMajorUnits(data.extra_usage.monthly_limit)
1405
+ const currency = data.extra_usage.currency || data.extra_usage.currency_code || data.extra_usage.currencyCode || "USD"
1406
+ const resetIso = ctx.util.toIso(
1407
+ data.extra_usage.resets_at ||
1408
+ data.extra_usage.reset_at ||
1409
+ data.extra_usage.resetsAt ||
1410
+ data.extra_usage.resetAt ||
1411
+ data.extra_usage.monthly_reset_at ||
1412
+ data.extra_usage.monthlyResetAt ||
1413
+ data.extra_usage.period_end ||
1414
+ data.extra_usage.periodEnd ||
1415
+ data.extra_usage.billing_cycle_end ||
1416
+ data.extra_usage.billingCycleEnd
1417
+ ) || nextMonthResetIso(ctx.nowIso)
1418
+ const resetText = formatMonthDay(resetIso)
1419
+
1420
+ if (typeof used === "number" && typeof limit === "number" && limit > 0) {
1421
+ const pct = Math.round((used / limit) * 100)
1422
+ let value = formatMoneyAmount(used, currency) + " / " + formatMoneyAmount(limit, currency)
1423
+ value += " (" + pct + "% used"
1424
+ if (resetText) value += ", resets " + resetText
1425
+ value += ")"
1426
+ lines.push(ctx.line.text({ label: "Extra usage spent", value: value }))
1427
+ } else if (typeof used === "number" && used > 0) {
1428
+ let value = formatMoneyAmount(used, currency)
1429
+ if (resetText) value += " (resets " + resetText + ")"
1430
+ lines.push(ctx.line.text({ label: "Extra usage spent", value: value }))
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ function oauthPlanLabel(ctx, creds) {
1436
+ if (!creds || !creds.oauth || !creds.oauth.subscriptionType) return null
1437
+ const basePlan = ctx.fmt.planLabel(creds.oauth.subscriptionType)
1438
+ if (!basePlan) return null
1439
+
1440
+ let tierSuffix = ""
1441
+ const rlt = String(creds.oauth.rateLimitTier || "")
1442
+ const tierMatch = rlt.match(/(\d+)x/)
1443
+ if (tierMatch) {
1444
+ tierSuffix = " " + tierMatch[1] + "x"
1445
+ }
1446
+ return basePlan + tierSuffix
1447
+ }
1448
+
1449
+ // ── probe() ────────────────────────────────────────────────────────────────
1450
+
1451
+ function probe(ctx) {
1452
+ const creds = loadCredentials(ctx)
1453
+ const sessionKey = getSessionKey(ctx)
1454
+ const adminApiKey = readClaudeAdminApiKey(ctx)
1455
+ const hasOAuth = !!(creds && creds.oauth && creds.oauth.accessToken && creds.oauth.accessToken.trim())
1456
+ const sourceMode = String(ctx.sourceMode || "auto").toLowerCase()
1457
+ const wantsAuto = sourceMode === "auto"
1458
+ const wantsWeb = sourceMode === "web"
1459
+ const wantsOAuth = sourceMode === "oauth"
1460
+ const wantsLocal = sourceMode === "local"
1461
+ const wantsApi = sourceMode === "api"
1462
+
1463
+ if (wantsWeb && !sessionKey) {
1464
+ ctx.host.log.error("web mode requested but no session key found")
1465
+ throw "Web session missing. Set CLAUDE_AI_SESSION_KEY to your claude.ai sessionKey cookie."
1466
+ }
1467
+ if (wantsOAuth && !hasOAuth) {
1468
+ ctx.host.log.error("oauth mode requested but no OAuth credentials found")
1469
+ throw "Not logged in. Run `claude` to authenticate."
1470
+ }
1471
+ if (wantsApi && !adminApiKey) {
1472
+ ctx.host.log.error("api mode requested but no Claude Admin API key found")
1473
+ throw "Claude API usage needs an Anthropic Admin API key. Set ANTHROPIC_ADMIN_KEY."
1474
+ }
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
+
1480
+ const nowMs = Date.now()
1481
+ const liveCache = readLiveUsageCache(ctx)
1482
+ const homePath = getClaudeHomeOverride(ctx)
1483
+ let data = null
1484
+ let lines = []
1485
+ let plan = null
1486
+ let rateLimited = false
1487
+ let retryAfterSeconds = null
1488
+
1489
+ if (hasOAuth) {
1490
+ plan = oauthPlanLabel(ctx, creds)
1491
+ }
1492
+
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)
1506
+
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.
1510
+ 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")
1515
+ } 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
1527
+ 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
+ )
1533
+ } 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
1585
+ 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"
1596
+ )
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
+ } 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."
1605
+ }
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
1617
+ }
1618
+ } 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
1627
+ }
1628
+ } else {
1629
+ ctx.host.log.info("auto mode using Claude Admin API history only")
1630
+ }
1631
+
1632
+ if (rateLimited && wantsAuto && sessionKey) {
1633
+ try {
1634
+ ctx.host.log.info("OAuth usage rate limited; trying Claude web fallback")
1635
+ const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
1636
+ if (webResult.data) {
1637
+ data = webResult.data
1638
+ rateLimited = false
1639
+ retryAfterSeconds = null
1640
+ if (webResult.plan) {
1641
+ plan = webResult.plan
1642
+ }
1643
+ }
1644
+ } catch (e) {
1645
+ ctx.host.log.warn("Claude web fallback failed after OAuth rate limit: " + String(e))
1646
+ }
1647
+ }
1648
+
1649
+ // ── Render usage windows ─────────────────────────────────────────────────
1650
+ if (data) {
1651
+ addUsageWindowLines(ctx, data, lines)
1652
+ }
1653
+
1654
+ // ── Token and cost history ───────────────────────────────────────────────
1655
+ let attachedUsageDaily = false
1656
+ if (!wantsLocal && adminApiKey && (wantsAuto || wantsApi)) {
1657
+ try {
1658
+ const adminDaily = fetchClaudeAdminUsage(ctx, adminApiKey)
1659
+ const summary = summarizeUsageDaily(adminDaily)
1660
+ if (summary.hasCost || summary.tokens > 0) {
1661
+ lines.push(ctx.line.text({
1662
+ label: "API Spend",
1663
+ value: summary.hasCost ? "$" + summary.costUSD.toFixed(2) : "$0.00",
1664
+ }))
1665
+ }
1666
+ attachedUsageDaily = appendUsageDailyLines(
1667
+ lines,
1668
+ ctx,
1669
+ adminDaily,
1670
+ "Claude Admin API organization usage."
1671
+ )
1672
+ persistUsageDaily(ctx, adminDaily, "Claude", "admin_billing")
1673
+ } catch (e) {
1674
+ const message = e && e.message ? e.message : String(e)
1675
+ if (wantsApi) throw message
1676
+ ctx.host.log.warn("Claude Admin API usage failed: " + message)
1677
+ }
1678
+ }
1679
+
1680
+ if (!attachedUsageDaily && !wantsApi) {
1681
+ try {
1682
+ const usageResult = queryTokenUsage(ctx, homePath)
1683
+ if (usageResult.status === "ok") {
1684
+ const usage = usageResult.data
1685
+ appendUsageDailyLines(lines, ctx, usage.daily, "Estimated from local Claude logs at API rates.")
1686
+ persistUsageDaily(ctx, usage.daily, "Claude")
1687
+ }
1688
+ } catch (e) {
1689
+ ctx.host.log.warn("local Claude token usage failed: " + String(e))
1690
+ }
1691
+ }
1692
+
1693
+ if (rateLimited) {
1694
+ const retryText = retryAfterSeconds !== null
1695
+ ? fmtRateLimitMinutes(retryAfterSeconds)
1696
+ : null
1697
+ const waitText = retryText
1698
+ ? "Rate limited, retry in ~" + retryText
1699
+ : "Rate limited, try again later"
1700
+ 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"
1704
+ lines.push(ctx.line.text({ label: "Note", value: noteText }))
1705
+ } else if (lines.length === 0) {
1706
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
1707
+ }
1708
+
1709
+ return { plan: plan, lines: lines }
1710
+ }
1711
+
1712
+ // _resetState is a testing hook — resets module-scope rate-limit state between tests.
1713
+ // The production host never calls this.
1714
+ function _resetState() {
1715
+ rateLimitedUntilMs = 0
1716
+ lastUsageFetchMs = 0
1717
+ cachedUsageData = null
1718
+ lastWebUsageFetchMs = 0
1719
+ cachedWebUsageData = null
1720
+ }
1721
+
1722
+ globalThis.__openusage_plugin = { id: "claude", probe, _resetState }
1723
+ })()