openusage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/bin/openusage +91 -0
  2. package/package.json +33 -0
  3. package/plugins/amp/icon.svg +6 -0
  4. package/plugins/amp/plugin.js +175 -0
  5. package/plugins/amp/plugin.json +20 -0
  6. package/plugins/amp/plugin.test.js +365 -0
  7. package/plugins/antigravity/icon.svg +3 -0
  8. package/plugins/antigravity/plugin.js +484 -0
  9. package/plugins/antigravity/plugin.json +17 -0
  10. package/plugins/antigravity/plugin.test.js +1356 -0
  11. package/plugins/claude/icon.svg +3 -0
  12. package/plugins/claude/plugin.js +565 -0
  13. package/plugins/claude/plugin.json +28 -0
  14. package/plugins/claude/plugin.test.js +1012 -0
  15. package/plugins/codex/icon.svg +3 -0
  16. package/plugins/codex/plugin.js +673 -0
  17. package/plugins/codex/plugin.json +30 -0
  18. package/plugins/codex/plugin.test.js +1071 -0
  19. package/plugins/copilot/icon.svg +3 -0
  20. package/plugins/copilot/plugin.js +264 -0
  21. package/plugins/copilot/plugin.json +20 -0
  22. package/plugins/copilot/plugin.test.js +529 -0
  23. package/plugins/cursor/icon.svg +3 -0
  24. package/plugins/cursor/plugin.js +526 -0
  25. package/plugins/cursor/plugin.json +24 -0
  26. package/plugins/cursor/plugin.test.js +1168 -0
  27. package/plugins/factory/icon.svg +1 -0
  28. package/plugins/factory/plugin.js +407 -0
  29. package/plugins/factory/plugin.json +19 -0
  30. package/plugins/factory/plugin.test.js +833 -0
  31. package/plugins/gemini/icon.svg +4 -0
  32. package/plugins/gemini/plugin.js +413 -0
  33. package/plugins/gemini/plugin.json +20 -0
  34. package/plugins/gemini/plugin.test.js +735 -0
  35. package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
  36. package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
  37. package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
  38. package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
  39. package/plugins/kimi/icon.svg +3 -0
  40. package/plugins/kimi/plugin.js +358 -0
  41. package/plugins/kimi/plugin.json +19 -0
  42. package/plugins/kimi/plugin.test.js +619 -0
  43. package/plugins/minimax/icon.svg +4 -0
  44. package/plugins/minimax/plugin.js +388 -0
  45. package/plugins/minimax/plugin.json +17 -0
  46. package/plugins/minimax/plugin.test.js +943 -0
  47. package/plugins/perplexity/icon.svg +1 -0
  48. package/plugins/perplexity/plugin.js +378 -0
  49. package/plugins/perplexity/plugin.json +15 -0
  50. package/plugins/perplexity/plugin.test.js +602 -0
  51. package/plugins/windsurf/icon.svg +3 -0
  52. package/plugins/windsurf/plugin.js +218 -0
  53. package/plugins/windsurf/plugin.json +16 -0
  54. package/plugins/windsurf/plugin.test.js +455 -0
  55. package/plugins/zai/icon.svg +5 -0
  56. package/plugins/zai/plugin.js +156 -0
  57. package/plugins/zai/plugin.json +18 -0
  58. package/plugins/zai/plugin.test.js +396 -0
@@ -0,0 +1,3 @@
1
+ <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M25.7146 63.2153L41.4393 54.3917L41.7025 53.6226L41.4393 53.1976H40.6705L38.0394 53.0359L29.054 52.7929L21.2624 52.4691L13.7134 52.0644L11.8111 51.6594L10.0303 49.3118L10.2123 48.138L11.8111 47.0657L14.0981 47.2681L19.1574 47.6119L26.7467 48.138L32.2516 48.4618L40.4073 49.3118H41.7025L41.8846 48.7857L41.4393 48.4618L41.0955 48.138L33.243 42.8155L24.7432 37.1894L20.2909 33.9513L17.8824 32.3119L16.6684 30.774L16.1422 27.4147L18.328 25.0062L21.2624 25.2088L22.0112 25.4112L24.9861 27.6979L31.3407 32.616L39.6381 38.7273L40.8525 39.7391L41.3381 39.395L41.399 39.1523L40.8525 38.2415L36.3394 30.0858L31.5227 21.7883L29.3775 18.3478L28.811 16.2837C28.6087 15.4334 28.4669 14.7252 28.4669 13.8549L30.9563 10.4753L32.3321 10.0303L35.6515 10.4756L37.0479 11.6897L39.112 16.4052L42.4513 23.8327L47.6321 33.9313L49.15 36.9265L49.9594 39.6991L50.2632 40.5491H50.7894V40.0632L51.2141 34.3766L52.0035 27.3944L52.7726 18.4087L53.0358 15.8793L54.2905 12.8435L56.7795 11.2041L58.7224 12.135L60.3212 14.422L60.0986 15.899L59.1474 22.0718L57.2857 31.7458L56.0713 38.2218H56.7795L57.5892 37.4121L60.8677 33.061L66.3723 26.18L68.801 23.448L71.6342 20.4325L73.4556 18.9957H76.8962L79.4255 22.7601L78.2926 26.6456L74.7509 31.1384L71.8163 34.943L67.607 40.6097L64.9758 45.1431L65.2188 45.5072L65.8464 45.4466L75.358 43.4228L80.4984 42.4917L86.6304 41.4393L89.4033 42.7346L89.7065 44.0502L88.6135 46.7419L82.0566 48.3607L74.3662 49.8989L62.9118 52.6109L62.77 52.7121L62.9321 52.9144L68.0925 53.4L70.2987 53.5214H75.7021L85.7601 54.2702L88.3912 56.0108L89.9697 58.1358L89.7065 59.7545L85.6589 61.8189L80.1949 60.5236L67.4452 57.4881L63.0735 56.3952H62.4665V56.7596L66.1093 60.3213L72.7877 66.3523L81.1461 74.1236L81.5707 76.0462L80.4984 77.5638L79.3649 77.4021L72.0186 71.8772L69.1854 69.3879L62.77 63.9844H62.3453V64.5509L63.8223 66.7164L71.6342 78.4544L72.0389 82.0567L71.4725 83.2308L69.4487 83.939L67.2222 83.534L62.6485 77.1189L57.9333 69.8937L54.1284 63.4177L53.6631 63.6809L51.4167 87.8651L50.3644 89.0995L47.9356 90.0303L45.9121 88.4924L44.8392 86.0031L45.9118 81.0852L47.2071 74.6701L48.2594 69.5699L49.2106 63.2356L49.7773 61.131L49.7367 60.9892L49.2715 61.0498L44.4954 67.607L37.23 77.4224L31.4825 83.5746L30.1063 84.1211L27.7181 82.8864L27.9408 80.6805L29.2763 78.7177L37.2297 68.5988L42.026 62.3248L45.1227 58.7025L45.1024 58.176H44.9204L23.7917 71.8975L20.0274 72.3831L18.4083 70.8655L18.6106 68.3761L19.3798 67.5664L25.7343 63.195L25.7146 63.2153Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,565 @@
1
+ (function () {
2
+ const CRED_FILE = "~/.claude/.credentials.json"
3
+ const KEYCHAIN_SERVICE = "Claude Code-credentials"
4
+ const USAGE_URL = "https://api.anthropic.com/api/oauth/usage"
5
+ const REFRESH_URL = "https://platform.claude.com/v1/oauth/token"
6
+ const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
7
+ const SCOPES = "user:profile user:inference user:sessions:claude_code user:mcp_servers"
8
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000 // refresh 5 minutes before expiration
9
+
10
+ function utf8DecodeBytes(bytes) {
11
+ // Prefer native TextDecoder when available (QuickJS may not expose it).
12
+ if (typeof TextDecoder !== "undefined") {
13
+ try {
14
+ return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes))
15
+ } catch {}
16
+ }
17
+
18
+ // Minimal UTF-8 decoder (replacement char on invalid sequences).
19
+ let out = ""
20
+ for (let i = 0; i < bytes.length; ) {
21
+ const b0 = bytes[i] & 0xff
22
+ if (b0 < 0x80) {
23
+ out += String.fromCharCode(b0)
24
+ i += 1
25
+ continue
26
+ }
27
+
28
+ // 2-byte
29
+ if (b0 >= 0xc2 && b0 <= 0xdf) {
30
+ if (i + 1 >= bytes.length) {
31
+ out += "\ufffd"
32
+ break
33
+ }
34
+ const b1 = bytes[i + 1] & 0xff
35
+ if ((b1 & 0xc0) !== 0x80) {
36
+ out += "\ufffd"
37
+ i += 1
38
+ continue
39
+ }
40
+ const cp = ((b0 & 0x1f) << 6) | (b1 & 0x3f)
41
+ out += String.fromCharCode(cp)
42
+ i += 2
43
+ continue
44
+ }
45
+
46
+ // 3-byte
47
+ if (b0 >= 0xe0 && b0 <= 0xef) {
48
+ if (i + 2 >= bytes.length) {
49
+ out += "\ufffd"
50
+ break
51
+ }
52
+ const b1 = bytes[i + 1] & 0xff
53
+ const b2 = bytes[i + 2] & 0xff
54
+ const validCont = (b1 & 0xc0) === 0x80 && (b2 & 0xc0) === 0x80
55
+ const notOverlong = !(b0 === 0xe0 && b1 < 0xa0)
56
+ const notSurrogate = !(b0 === 0xed && b1 >= 0xa0)
57
+ if (!validCont || !notOverlong || !notSurrogate) {
58
+ out += "\ufffd"
59
+ i += 1
60
+ continue
61
+ }
62
+ const cp = ((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f)
63
+ out += String.fromCharCode(cp)
64
+ i += 3
65
+ continue
66
+ }
67
+
68
+ // 4-byte
69
+ if (b0 >= 0xf0 && b0 <= 0xf4) {
70
+ if (i + 3 >= bytes.length) {
71
+ out += "\ufffd"
72
+ break
73
+ }
74
+ const b1 = bytes[i + 1] & 0xff
75
+ const b2 = bytes[i + 2] & 0xff
76
+ const b3 = bytes[i + 3] & 0xff
77
+ const validCont = (b1 & 0xc0) === 0x80 && (b2 & 0xc0) === 0x80 && (b3 & 0xc0) === 0x80
78
+ const notOverlong = !(b0 === 0xf0 && b1 < 0x90)
79
+ const notTooHigh = !(b0 === 0xf4 && b1 > 0x8f)
80
+ if (!validCont || !notOverlong || !notTooHigh) {
81
+ out += "\ufffd"
82
+ i += 1
83
+ continue
84
+ }
85
+ const cp =
86
+ ((b0 & 0x07) << 18) | ((b1 & 0x3f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f)
87
+ const n = cp - 0x10000
88
+ out += String.fromCharCode(0xd800 + ((n >> 10) & 0x3ff), 0xdc00 + (n & 0x3ff))
89
+ i += 4
90
+ continue
91
+ }
92
+
93
+ out += "\ufffd"
94
+ i += 1
95
+ }
96
+ return out
97
+ }
98
+
99
+ function tryParseCredentialJSON(ctx, text) {
100
+ if (!text) return null
101
+ const parsed = ctx.util.tryParseJson(text)
102
+ if (parsed) return parsed
103
+
104
+ // Some macOS keychain items are returned by `security ... -w` as hex-encoded UTF-8 bytes.
105
+ // Example prefix: "7b0a" ( "{\\n" ).
106
+ // Support both plain hex and "0x..." forms.
107
+ let hex = String(text).trim()
108
+ if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
109
+ if (!hex || hex.length % 2 !== 0) return null
110
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null
111
+ try {
112
+ const bytes = []
113
+ for (let i = 0; i < hex.length; i += 2) {
114
+ bytes.push(parseInt(hex.slice(i, i + 2), 16))
115
+ }
116
+ const decoded = utf8DecodeBytes(bytes)
117
+ const decodedParsed = ctx.util.tryParseJson(decoded)
118
+ if (decodedParsed) return decodedParsed
119
+ } catch {}
120
+
121
+ return null
122
+ }
123
+
124
+ function loadCredentials(ctx) {
125
+ // Try file first
126
+ if (ctx.host.fs.exists(CRED_FILE)) {
127
+ try {
128
+ const text = ctx.host.fs.readText(CRED_FILE)
129
+ const parsed = tryParseCredentialJSON(ctx, text)
130
+ if (parsed) {
131
+ const oauth = parsed.claudeAiOauth
132
+ if (oauth && oauth.accessToken) {
133
+ ctx.host.log.info("credentials loaded from file")
134
+ return { oauth, source: "file", fullData: parsed }
135
+ }
136
+ }
137
+ ctx.host.log.warn("credentials file exists but no valid oauth data")
138
+ } catch (e) {
139
+ ctx.host.log.warn("credentials file read failed: " + String(e))
140
+ }
141
+ }
142
+
143
+ // Try keychain fallback
144
+ try {
145
+ const keychainValue = ctx.host.keychain.readGenericPassword(KEYCHAIN_SERVICE)
146
+ if (keychainValue) {
147
+ const parsed = tryParseCredentialJSON(ctx, keychainValue)
148
+ if (parsed) {
149
+ const oauth = parsed.claudeAiOauth
150
+ if (oauth && oauth.accessToken) {
151
+ ctx.host.log.info("credentials loaded from keychain")
152
+ return { oauth, source: "keychain", fullData: parsed }
153
+ }
154
+ }
155
+ ctx.host.log.warn("keychain has data but no valid oauth")
156
+ }
157
+ } catch (e) {
158
+ ctx.host.log.info("keychain read failed (may not exist): " + String(e))
159
+ }
160
+
161
+ ctx.host.log.warn("no credentials found")
162
+ return null
163
+ }
164
+
165
+ function saveCredentials(ctx, source, fullData) {
166
+ // MUST use minified JSON - macOS `security -w` hex-encodes values with newlines,
167
+ // which Claude Code can't read back, causing it to invalidate the session.
168
+ const text = JSON.stringify(fullData)
169
+ if (source === "file") {
170
+ try {
171
+ ctx.host.fs.writeText(CRED_FILE, text)
172
+ } catch (e) {
173
+ ctx.host.log.error("Failed to write Claude credentials file: " + String(e))
174
+ }
175
+ } else if (source === "keychain") {
176
+ try {
177
+ ctx.host.keychain.writeGenericPassword(KEYCHAIN_SERVICE, text)
178
+ } catch (e) {
179
+ ctx.host.log.error("Failed to write Claude credentials keychain: " + String(e))
180
+ }
181
+ }
182
+ }
183
+
184
+ function needsRefresh(ctx, oauth, nowMs) {
185
+ return ctx.util.needsRefreshByExpiry({
186
+ nowMs,
187
+ expiresAtMs: oauth.expiresAt,
188
+ bufferMs: REFRESH_BUFFER_MS,
189
+ })
190
+ }
191
+
192
+ function refreshToken(ctx, creds) {
193
+ const { oauth, source, fullData } = creds
194
+ if (!oauth.refreshToken) {
195
+ ctx.host.log.warn("refresh skipped: no refresh token")
196
+ return null
197
+ }
198
+
199
+ ctx.host.log.info("attempting token refresh")
200
+ try {
201
+ const resp = ctx.util.request({
202
+ method: "POST",
203
+ url: REFRESH_URL,
204
+ headers: { "Content-Type": "application/json" },
205
+ bodyText: JSON.stringify({
206
+ grant_type: "refresh_token",
207
+ refresh_token: oauth.refreshToken,
208
+ client_id: CLIENT_ID,
209
+ scope: SCOPES,
210
+ }),
211
+ timeoutMs: 15000,
212
+ })
213
+
214
+ if (resp.status === 400 || resp.status === 401) {
215
+ let errorCode = null
216
+ const body = ctx.util.tryParseJson(resp.bodyText)
217
+ if (body) errorCode = body.error || body.error_description
218
+ ctx.host.log.error("refresh failed: status=" + resp.status + " error=" + String(errorCode))
219
+ if (errorCode === "invalid_grant") {
220
+ throw "Session expired. Run `claude` to log in again."
221
+ }
222
+ throw "Token expired. Run `claude` to log in again."
223
+ }
224
+ if (resp.status < 200 || resp.status >= 300) {
225
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
226
+ return null
227
+ }
228
+
229
+ const body = ctx.util.tryParseJson(resp.bodyText)
230
+ if (!body) {
231
+ ctx.host.log.warn("refresh response not valid JSON")
232
+ return null
233
+ }
234
+ const newAccessToken = body.access_token
235
+ if (!newAccessToken) {
236
+ ctx.host.log.warn("refresh response missing access_token")
237
+ return null
238
+ }
239
+
240
+ // Update oauth credentials
241
+ oauth.accessToken = newAccessToken
242
+ if (body.refresh_token) oauth.refreshToken = body.refresh_token
243
+ if (typeof body.expires_in === "number") {
244
+ oauth.expiresAt = Date.now() + body.expires_in * 1000
245
+ }
246
+
247
+ // Persist updated credentials
248
+ fullData.claudeAiOauth = oauth
249
+ saveCredentials(ctx, source, fullData)
250
+
251
+ ctx.host.log.info("refresh succeeded, new token expires in " + (body.expires_in || "unknown") + "s")
252
+ return newAccessToken
253
+ } catch (e) {
254
+ if (typeof e === "string") throw e
255
+ ctx.host.log.error("refresh exception: " + String(e))
256
+ return null
257
+ }
258
+ }
259
+
260
+ function fetchUsage(ctx, accessToken) {
261
+ return ctx.util.request({
262
+ method: "GET",
263
+ url: USAGE_URL,
264
+ headers: {
265
+ Authorization: "Bearer " + accessToken.trim(),
266
+ Accept: "application/json",
267
+ "Content-Type": "application/json",
268
+ "anthropic-beta": "oauth-2025-04-20",
269
+ "User-Agent": "OpenUsage",
270
+ },
271
+ timeoutMs: 10000,
272
+ })
273
+ }
274
+
275
+ function queryTokenUsage(ctx) {
276
+ const since = new Date()
277
+ // Inclusive range: today + previous 30 days = 31 calendar days.
278
+ since.setDate(since.getDate() - 30)
279
+ const y = since.getFullYear()
280
+ const m = since.getMonth() + 1
281
+ const d = since.getDate()
282
+ const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d
283
+
284
+ const result = ctx.host.ccusage.query({ since: sinceStr })
285
+ if (!result || typeof result !== "object" || typeof result.status !== "string") {
286
+ return { status: "runner_failed", data: null }
287
+ }
288
+ if (result.status !== "ok") {
289
+ return { status: result.status, data: null }
290
+ }
291
+ if (!result.data || !Array.isArray(result.data.daily)) {
292
+ return { status: "runner_failed", data: null }
293
+ }
294
+ return { status: "ok", data: result.data }
295
+ }
296
+
297
+ function fmtTokens(n) {
298
+ const abs = Math.abs(n)
299
+ const sign = n < 0 ? "-" : ""
300
+ const units = [
301
+ { threshold: 1e9, divisor: 1e9, suffix: "B" },
302
+ { threshold: 1e6, divisor: 1e6, suffix: "M" },
303
+ { threshold: 1e3, divisor: 1e3, suffix: "K" },
304
+ ]
305
+ for (let i = 0; i < units.length; i++) {
306
+ const unit = units[i]
307
+ if (abs >= unit.threshold) {
308
+ const scaled = abs / unit.divisor
309
+ const formatted = scaled >= 10
310
+ ? Math.round(scaled).toString()
311
+ : scaled.toFixed(1).replace(/\.0$/, "")
312
+ return sign + formatted + unit.suffix
313
+ }
314
+ }
315
+ return sign + Math.round(abs).toString()
316
+ }
317
+
318
+ function dayKeyFromDate(date) {
319
+ const year = date.getFullYear()
320
+ const month = date.getMonth() + 1
321
+ const day = date.getDate()
322
+ return year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day
323
+ }
324
+
325
+ function dayKeyFromUsageDate(rawDate) {
326
+ if (typeof rawDate !== "string") return null
327
+ const value = rawDate.trim()
328
+ if (!value) return null
329
+
330
+ const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/)
331
+ if (isoMatch) {
332
+ return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3]
333
+ }
334
+
335
+ const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/)
336
+ if (compactMatch) {
337
+ return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3]
338
+ }
339
+
340
+ const ms = Date.parse(value)
341
+ if (!Number.isFinite(ms)) return null
342
+ return dayKeyFromDate(new Date(ms))
343
+ }
344
+
345
+ function usageCostUsd(day) {
346
+ if (!day || typeof day !== "object") return null
347
+
348
+ if (day.totalCost != null) {
349
+ const totalCost = Number(day.totalCost)
350
+ if (Number.isFinite(totalCost)) return totalCost
351
+ }
352
+
353
+ if (day.costUSD != null) {
354
+ const costUSD = Number(day.costUSD)
355
+ if (Number.isFinite(costUSD)) return costUSD
356
+ }
357
+
358
+ return null
359
+ }
360
+
361
+ function costAndTokensLabel(data, opts) {
362
+ const includeZeroTokens = !!(opts && opts.includeZeroTokens)
363
+ const parts = []
364
+ if (data.costUSD != null) parts.push("$" + data.costUSD.toFixed(2))
365
+ if (data.tokens > 0 || (includeZeroTokens && data.tokens === 0)) {
366
+ parts.push(fmtTokens(data.tokens) + " tokens")
367
+ }
368
+ return parts.join(" \u00b7 ")
369
+ }
370
+
371
+ function pushDayUsageLine(lines, ctx, label, dayEntry) {
372
+ const tokens = Number(dayEntry && dayEntry.totalTokens) || 0
373
+ const cost = usageCostUsd(dayEntry)
374
+ if (tokens > 0) {
375
+ lines.push(ctx.line.text({
376
+ label: label,
377
+ value: costAndTokensLabel({ tokens: tokens, costUSD: cost })
378
+ }))
379
+ return
380
+ }
381
+
382
+ lines.push(ctx.line.text({
383
+ label: label,
384
+ value: costAndTokensLabel({ tokens: 0, costUSD: 0 }, { includeZeroTokens: true })
385
+ }))
386
+ }
387
+
388
+ function probe(ctx) {
389
+ const creds = loadCredentials(ctx)
390
+ if (!creds || !creds.oauth || !creds.oauth.accessToken || !creds.oauth.accessToken.trim()) {
391
+ ctx.host.log.error("probe failed: not logged in")
392
+ throw "Not logged in. Run `claude` to authenticate."
393
+ }
394
+
395
+ const nowMs = Date.now()
396
+ let accessToken = creds.oauth.accessToken
397
+
398
+ // Proactively refresh if token is expired or about to expire
399
+ if (needsRefresh(ctx, creds.oauth, nowMs)) {
400
+ ctx.host.log.info("token needs refresh (expired or expiring soon)")
401
+ const refreshed = refreshToken(ctx, creds)
402
+ if (refreshed) {
403
+ accessToken = refreshed
404
+ } else {
405
+ ctx.host.log.warn("proactive refresh failed, trying with existing token")
406
+ }
407
+ }
408
+
409
+ let resp
410
+ let didRefresh = false
411
+ try {
412
+ resp = ctx.util.retryOnceOnAuth({
413
+ request: (token) => {
414
+ try {
415
+ return fetchUsage(ctx, token || accessToken)
416
+ } catch (e) {
417
+ ctx.host.log.error("usage request exception: " + String(e))
418
+ if (didRefresh) {
419
+ throw "Usage request failed after refresh. Try again."
420
+ }
421
+ throw "Usage request failed. Check your connection."
422
+ }
423
+ },
424
+ refresh: () => {
425
+ ctx.host.log.info("usage returned 401, attempting refresh")
426
+ didRefresh = true
427
+ return refreshToken(ctx, creds)
428
+ },
429
+ })
430
+ } catch (e) {
431
+ if (typeof e === "string") throw e
432
+ ctx.host.log.error("usage request failed: " + String(e))
433
+ throw "Usage request failed. Check your connection."
434
+ }
435
+
436
+ if (ctx.util.isAuthStatus(resp.status)) {
437
+ ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
438
+ throw "Token expired. Run `claude` to log in again."
439
+ }
440
+
441
+ if (resp.status < 200 || resp.status >= 300) {
442
+ ctx.host.log.error("usage returned error: status=" + resp.status)
443
+ throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
444
+ }
445
+
446
+ ctx.host.log.info("usage fetch succeeded")
447
+
448
+ let data
449
+ data = ctx.util.tryParseJson(resp.bodyText)
450
+ if (data === null) {
451
+ throw "Usage response invalid. Try again later."
452
+ }
453
+
454
+ const lines = []
455
+ let plan = null
456
+ if (creds.oauth.subscriptionType) {
457
+ const planLabel = ctx.fmt.planLabel(creds.oauth.subscriptionType)
458
+ if (planLabel) {
459
+ plan = planLabel
460
+ }
461
+ }
462
+
463
+ if (data.five_hour && typeof data.five_hour.utilization === "number") {
464
+ lines.push(ctx.line.progress({
465
+ label: "Session",
466
+ used: data.five_hour.utilization,
467
+ limit: 100,
468
+ format: { kind: "percent" },
469
+ resetsAt: ctx.util.toIso(data.five_hour.resets_at),
470
+ periodDurationMs: 5 * 60 * 60 * 1000 // 5 hours
471
+ }))
472
+ }
473
+ if (data.seven_day && typeof data.seven_day.utilization === "number") {
474
+ lines.push(ctx.line.progress({
475
+ label: "Weekly",
476
+ used: data.seven_day.utilization,
477
+ limit: 100,
478
+ format: { kind: "percent" },
479
+ resetsAt: ctx.util.toIso(data.seven_day.resets_at),
480
+ periodDurationMs: 7 * 24 * 60 * 60 * 1000 // 7 days
481
+ }))
482
+ }
483
+ if (data.seven_day_sonnet && typeof data.seven_day_sonnet.utilization === "number") {
484
+ lines.push(ctx.line.progress({
485
+ label: "Sonnet",
486
+ used: data.seven_day_sonnet.utilization,
487
+ limit: 100,
488
+ format: { kind: "percent" },
489
+ resetsAt: ctx.util.toIso(data.seven_day_sonnet.resets_at),
490
+ periodDurationMs: 7 * 24 * 60 * 60 * 1000 // 7 days
491
+ }))
492
+ }
493
+
494
+ if (data.extra_usage && data.extra_usage.is_enabled) {
495
+ const used = data.extra_usage.used_credits
496
+ const limit = data.extra_usage.monthly_limit
497
+ if (typeof used === "number" && typeof limit === "number" && limit > 0) {
498
+ lines.push(ctx.line.progress({
499
+ label: "Extra usage spent",
500
+ used: ctx.fmt.dollars(used),
501
+ limit: ctx.fmt.dollars(limit),
502
+ format: { kind: "dollars" }
503
+ }))
504
+ } else if (typeof used === "number" && used > 0) {
505
+ lines.push(ctx.line.text({ label: "Extra usage spent", value: "$" + String(ctx.fmt.dollars(used)) }))
506
+ }
507
+ }
508
+
509
+ const usageResult = queryTokenUsage(ctx)
510
+ if (usageResult.status === "ok") {
511
+ const usage = usageResult.data
512
+ const now = new Date()
513
+ const todayKey = dayKeyFromDate(now)
514
+ const yesterday = new Date(now.getTime())
515
+ yesterday.setDate(yesterday.getDate() - 1)
516
+ const yesterdayKey = dayKeyFromDate(yesterday)
517
+
518
+ let todayEntry = null
519
+ let yesterdayEntry = null
520
+ for (let i = 0; i < usage.daily.length; i++) {
521
+ const usageDayKey = dayKeyFromUsageDate(usage.daily[i].date)
522
+ if (usageDayKey === todayKey) {
523
+ todayEntry = usage.daily[i]
524
+ continue
525
+ }
526
+ if (usageDayKey === yesterdayKey) {
527
+ yesterdayEntry = usage.daily[i]
528
+ }
529
+ }
530
+
531
+ pushDayUsageLine(lines, ctx, "Today", todayEntry)
532
+ pushDayUsageLine(lines, ctx, "Yesterday", yesterdayEntry)
533
+
534
+ let totalTokens = 0
535
+ let totalCostNanos = 0
536
+ let hasCost = false
537
+ for (let i = 0; i < usage.daily.length; i++) {
538
+ const day = usage.daily[i]
539
+ const dayTokens = Number(day.totalTokens)
540
+ if (Number.isFinite(dayTokens)) {
541
+ totalTokens += dayTokens
542
+ }
543
+ const dayCost = usageCostUsd(day)
544
+ if (dayCost != null) {
545
+ totalCostNanos += Math.round(dayCost * 1e9)
546
+ hasCost = true
547
+ }
548
+ }
549
+ if (totalTokens > 0) {
550
+ lines.push(ctx.line.text({
551
+ label: "Last 30 Days",
552
+ value: costAndTokensLabel({ tokens: totalTokens, costUSD: hasCost ? totalCostNanos / 1e9 : null })
553
+ }))
554
+ }
555
+ }
556
+
557
+ if (lines.length === 0) {
558
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
559
+ }
560
+
561
+ return { plan: plan, lines: lines }
562
+ }
563
+
564
+ globalThis.__openusage_plugin = { id: "claude", probe }
565
+ })()
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "claude",
4
+ "name": "Claude",
5
+ "version": "0.0.1",
6
+ "entry": "plugin.js",
7
+ "icon": "icon.svg",
8
+ "brandColor": "#DE7356",
9
+ "cli": {
10
+ "category": "cli",
11
+ "binaryName": "claude",
12
+ "installCmd": "curl -fsSL https://claude.ai/install.sh | sh",
13
+ "loginCmd": "claude auth login"
14
+ },
15
+ "links": [
16
+ { "label": "Status", "url": "https://status.anthropic.com/" },
17
+ { "label": "Console", "url": "https://console.anthropic.com/" }
18
+ ],
19
+ "lines": [
20
+ { "type": "progress", "label": "Session", "scope": "overview", "primaryOrder": 1 },
21
+ { "type": "progress", "label": "Weekly", "scope": "overview" },
22
+ { "type": "progress", "label": "Sonnet", "scope": "detail" },
23
+ { "type": "progress", "label": "Extra usage spent", "scope": "detail" },
24
+ { "type": "text", "label": "Today", "scope": "detail" },
25
+ { "type": "text", "label": "Yesterday", "scope": "detail" },
26
+ { "type": "text", "label": "Last 30 Days", "scope": "detail" }
27
+ ]
28
+ }