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="M83.7733 42.8087C84.6678 40.1149 84.9771 37.2613 84.6807 34.4385C84.3843 31.6156 83.489 28.8885 82.0544 26.4394C77.6908 18.8436 68.9203 14.9365 60.3548 16.7725C57.9831 14.1344 54.9591 12.1668 51.5864 11.0673C48.2137 9.96772 44.611 9.77498 41.1402 10.5084C37.6694 11.2418 34.4527 12.8755 31.8132 15.2455C29.1736 17.6155 27.204 20.6383 26.1024 24.0103C23.3212 24.5806 20.6938 25.738 18.3958 27.405C16.0977 29.0721 14.1819 31.2104 12.7765 33.6772C8.36538 41.2609 9.3669 50.8267 15.2527 57.3327C14.3549 60.0251 14.0424 62.8782 14.3361 65.7012C14.6298 68.5241 15.523 71.2518 16.9558 73.7017C21.325 81.3002 30.1011 85.207 38.6712 83.3686C40.5554 85.4904 42.8707 87.1858 45.4623 88.3416C48.0539 89.4975 50.8622 90.0871 53.6999 90.0713C62.4793 90.079 70.2575 84.4114 72.9393 76.0515C75.7201 75.4802 78.347 74.3225 80.6449 72.6555C82.9427 70.9886 84.8587 68.8507 86.2649 66.3846C90.6227 58.8145 89.6172 49.3005 83.7733 42.8087ZM53.6999 84.8356C50.1955 84.8411 46.801 83.6129 44.1116 81.3661L44.5848 81.098L60.5123 71.9043C60.9087 71.6718 61.2379 71.3402 61.4674 70.942C61.6969 70.5439 61.8189 70.0929 61.8215 69.6333V47.1769L68.5553 51.072C68.6225 51.1063 68.6694 51.1707 68.6814 51.2456V69.854C68.6641 78.1208 61.9667 84.8183 53.6999 84.8356ZM21.4977 71.0843C19.7402 68.0497 19.1092 64.4925 19.7156 61.0386L20.1885 61.3225L36.1321 70.5165C36.5266 70.748 36.9757 70.87 37.4331 70.87C37.8905 70.87 38.3396 70.748 38.7341 70.5165L58.21 59.2883V67.0628C58.2081 67.1031 58.1973 67.1424 58.1782 67.1779C58.1591 67.2134 58.1322 67.2441 58.0996 67.2678L41.9671 76.5722C34.798 80.7022 25.6388 78.2463 21.4977 71.0843ZM17.3026 36.3898C19.0723 33.3357 21.8655 31.0062 25.1878 29.8138V48.7376C25.1818 49.1949 25.2986 49.6453 25.5261 50.042C25.7535 50.4387 26.0833 50.7671 26.4809 50.9928L45.8622 62.1739L39.1283 66.069C39.0919 66.0883 39.0513 66.0984 39.0101 66.0984C38.9689 66.0984 38.9283 66.0883 38.8919 66.069L22.7908 56.7809C15.6359 52.6337 13.1822 43.4816 17.3026 36.3112V36.3898ZM72.624 49.2426L53.1792 37.9512L59.8976 34.0718C59.9341 34.0524 59.9747 34.0423 60.016 34.0423C60.0573 34.0423 60.0979 34.0524 60.1344 34.0718L76.2355 43.3761C78.6973 44.7966 80.7043 46.8882 82.0221 49.4065C83.3398 51.9249 83.914 54.7661 83.6775 57.5985C83.4411 60.431 82.4038 63.1377 80.6867 65.4027C78.9696 67.6677 76.6436 69.3975 73.9803 70.3901V51.466C73.9663 51.0096 73.834 50.5647 73.5962 50.1749C73.3584 49.7851 73.0234 49.4638 72.624 49.2426ZM79.3261 39.1657L78.8529 38.8815L62.9411 29.6089C62.5442 29.376 62.0924 29.2532 61.6322 29.2532C61.172 29.2532 60.7202 29.376 60.3233 29.6089L40.8629 40.8374V33.0628C40.8587 33.0233 40.8654 32.9834 40.882 32.9473C40.8987 32.9113 40.9248 32.8803 40.9575 32.8579L57.0586 23.5692C59.5263 22.1476 62.3478 21.458 65.193 21.5811C68.0382 21.7042 70.7896 22.6348 73.1253 24.2642C75.461 25.8936 77.2845 28.1543 78.3825 30.782C79.4806 33.4097 79.8077 36.2957 79.3257 39.1025V39.1657H79.3261ZM37.1888 52.9484L30.455 49.069C30.4213 49.0487 30.3925 49.0212 30.3707 48.9884C30.3488 48.9557 30.3345 48.9186 30.3286 48.8797V30.3188C30.3323 27.4714 31.1466 24.6839 32.6761 22.2822C34.2057 19.8805 36.3874 17.9639 38.9661 16.7564C41.5448 15.549 44.4139 15.1005 47.2381 15.4636C50.0622 15.8267 52.7247 16.9862 54.9141 18.8067L54.4409 19.0748L38.5134 28.2686C38.117 28.5011 37.7879 28.8327 37.5584 29.2308C37.329 29.629 37.207 30.0799 37.2045 30.5395L37.1888 52.9487V52.9484ZM40.8472 45.0632L49.5209 40.0643L58.21 45.0635V55.0615L49.5523 60.0608L40.8632 55.0615L40.8472 45.0632Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,673 @@
1
+ (function () {
2
+ const AUTH_FILE = "auth.json"
3
+ const CONFIG_AUTH_PATHS = ["~/.config/codex", "~/.codex"]
4
+ const KEYCHAIN_SERVICE = "Codex Auth"
5
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
6
+ const REFRESH_URL = "https://auth.openai.com/oauth/token"
7
+ const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"
8
+ const REFRESH_AGE_MS = 8 * 24 * 60 * 60 * 1000
9
+
10
+ function joinPath(base, leaf) {
11
+ return base.replace(/[\\/]+$/, "") + "/" + leaf
12
+ }
13
+
14
+ function readCodexHome(ctx) {
15
+ if (!ctx.host.env || typeof ctx.host.env.get !== "function") {
16
+ return null
17
+ }
18
+
19
+ try {
20
+ const value = ctx.host.env.get("CODEX_HOME")
21
+ if (typeof value !== "string") return null
22
+ const trimmed = value.trim()
23
+ return trimmed || null
24
+ } catch (e) {
25
+ ctx.host.log.warn("CODEX_HOME read failed: " + String(e))
26
+ return null
27
+ }
28
+ }
29
+
30
+ function decodeHexUtf8(hex) {
31
+ try {
32
+ const bytes = []
33
+ for (let i = 0; i < hex.length; i += 2) {
34
+ bytes.push(parseInt(hex.slice(i, i + 2), 16))
35
+ }
36
+
37
+ if (typeof TextDecoder !== "undefined") {
38
+ try {
39
+ return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes))
40
+ } catch {}
41
+ }
42
+
43
+ let escaped = ""
44
+ for (const b of bytes) {
45
+ const h = b.toString(16)
46
+ escaped += "%" + (h.length === 1 ? "0" + h : h)
47
+ }
48
+ return decodeURIComponent(escaped)
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ function tryParseAuthJson(ctx, text) {
55
+ if (!text) return null
56
+ const parsed = ctx.util.tryParseJson(text)
57
+ if (parsed) return parsed
58
+
59
+ // Some keychain payloads can be returned as hex-encoded UTF-8 bytes.
60
+ let hex = String(text).trim()
61
+ if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
62
+ if (!hex || hex.length % 2 !== 0) return null
63
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null
64
+
65
+ const decoded = decodeHexUtf8(hex)
66
+ if (!decoded) return null
67
+ return ctx.util.tryParseJson(decoded)
68
+ }
69
+
70
+ function resolveAuthPaths(ctx) {
71
+ const codexHome = readCodexHome(ctx)
72
+
73
+ // If CODEX_HOME is set, use it
74
+ if (codexHome) {
75
+ return [joinPath(codexHome, AUTH_FILE)]
76
+ }
77
+
78
+ return CONFIG_AUTH_PATHS.map((basePath) => joinPath(basePath, AUTH_FILE))
79
+ }
80
+
81
+ function hasTokenLikeAuth(auth) {
82
+ if (!auth || typeof auth !== "object") return false
83
+ if (auth.tokens && auth.tokens.access_token) return true
84
+ if (auth.OPENAI_API_KEY) return true
85
+ return false
86
+ }
87
+
88
+ function loadAuthFromKeychain(ctx) {
89
+ if (!ctx.host.keychain || typeof ctx.host.keychain.readGenericPassword !== "function") {
90
+ return null
91
+ }
92
+
93
+ try {
94
+ const value = ctx.host.keychain.readGenericPassword(KEYCHAIN_SERVICE)
95
+ if (!value) return null
96
+ const auth = tryParseAuthJson(ctx, value)
97
+ if (!hasTokenLikeAuth(auth)) {
98
+ ctx.host.log.warn("keychain has data but no codex auth payload")
99
+ return null
100
+ }
101
+ ctx.host.log.info("auth loaded from keychain: " + KEYCHAIN_SERVICE)
102
+ return { auth, authPath: null, source: "keychain" }
103
+ } catch (e) {
104
+ ctx.host.log.info("keychain read failed (may not exist): " + String(e))
105
+ return null
106
+ }
107
+ }
108
+
109
+ function saveAuth(ctx, authState) {
110
+ const auth = authState && authState.auth ? authState.auth : null
111
+ if (!auth) return false
112
+
113
+ if (authState.source === "file" && authState.authPath) {
114
+ ctx.host.fs.writeText(authState.authPath, JSON.stringify(auth, null, 2))
115
+ return true
116
+ }
117
+
118
+ if (authState.source === "keychain") {
119
+ if (!ctx.host.keychain || typeof ctx.host.keychain.writeGenericPassword !== "function") {
120
+ ctx.host.log.warn("keychain write unsupported in this host")
121
+ return false
122
+ }
123
+ // Use compact JSON to avoid newline-induced keychain encoding issues.
124
+ ctx.host.keychain.writeGenericPassword(KEYCHAIN_SERVICE, JSON.stringify(auth))
125
+ return true
126
+ }
127
+
128
+ return false
129
+ }
130
+
131
+ function loadAuth(ctx) {
132
+ const authPaths = resolveAuthPaths(ctx)
133
+ for (const authPath of authPaths) {
134
+ if (!ctx.host.fs.exists(authPath)) continue
135
+ try {
136
+ const text = ctx.host.fs.readText(authPath)
137
+ const auth = tryParseAuthJson(ctx, text)
138
+ if (!hasTokenLikeAuth(auth)) {
139
+ ctx.host.log.warn("auth file exists but no valid codex auth payload: " + authPath)
140
+ continue
141
+ }
142
+ ctx.host.log.info("auth loaded from file: " + authPath)
143
+ return { auth, authPath, source: "file" }
144
+ } catch (e) {
145
+ ctx.host.log.warn("auth file read failed: " + String(e))
146
+ }
147
+ }
148
+
149
+ const keychainAuth = loadAuthFromKeychain(ctx)
150
+ if (keychainAuth) return keychainAuth
151
+
152
+ if (authPaths.length > 0) {
153
+ for (const authPath of authPaths) {
154
+ if (!ctx.host.fs.exists(authPath)) {
155
+ ctx.host.log.warn("auth file not found: " + authPath)
156
+ }
157
+ }
158
+ }
159
+
160
+ return null
161
+ }
162
+
163
+ function needsRefresh(ctx, auth, nowMs) {
164
+ if (!auth.last_refresh) return true
165
+ const lastMs = ctx.util.parseDateMs(auth.last_refresh)
166
+ if (lastMs === null) return true
167
+ return nowMs - lastMs > REFRESH_AGE_MS
168
+ }
169
+
170
+ function refreshToken(ctx, authState) {
171
+ const auth = authState.auth
172
+ if (!auth.tokens || !auth.tokens.refresh_token) {
173
+ ctx.host.log.warn("refresh skipped: no refresh token")
174
+ return null
175
+ }
176
+
177
+ ctx.host.log.info("attempting token refresh")
178
+ try {
179
+ const resp = ctx.util.request({
180
+ method: "POST",
181
+ url: REFRESH_URL,
182
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
183
+ bodyText:
184
+ "grant_type=refresh_token" +
185
+ "&client_id=" + encodeURIComponent(CLIENT_ID) +
186
+ "&refresh_token=" + encodeURIComponent(auth.tokens.refresh_token),
187
+ timeoutMs: 15000,
188
+ })
189
+
190
+ if (resp.status === 400 || resp.status === 401) {
191
+ let code = null
192
+ const body = ctx.util.tryParseJson(resp.bodyText)
193
+ if (body) {
194
+ code = body.error?.code || body.error || body.code
195
+ }
196
+ ctx.host.log.error("refresh failed: status=" + resp.status + " code=" + String(code))
197
+ if (code === "refresh_token_expired") {
198
+ throw "Session expired. Run `codex` to log in again."
199
+ }
200
+ if (code === "refresh_token_reused") {
201
+ throw "Token conflict. Run `codex` to log in again."
202
+ }
203
+ if (code === "refresh_token_invalidated") {
204
+ throw "Token revoked. Run `codex` to log in again."
205
+ }
206
+ throw "Token expired. Run `codex` to log in again."
207
+ }
208
+ if (resp.status < 200 || resp.status >= 300) {
209
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
210
+ return null
211
+ }
212
+
213
+ const body = ctx.util.tryParseJson(resp.bodyText)
214
+ if (!body) {
215
+ ctx.host.log.warn("refresh response not valid JSON")
216
+ return null
217
+ }
218
+ const newAccessToken = body.access_token
219
+ if (!newAccessToken) {
220
+ ctx.host.log.warn("refresh response missing access_token")
221
+ return null
222
+ }
223
+
224
+ auth.tokens.access_token = newAccessToken
225
+ if (body.refresh_token) auth.tokens.refresh_token = body.refresh_token
226
+ if (body.id_token) auth.tokens.id_token = body.id_token
227
+ auth.last_refresh = new Date().toISOString()
228
+
229
+ try {
230
+ const saved = saveAuth(ctx, authState)
231
+ if (saved) {
232
+ ctx.host.log.info("refresh succeeded, auth persisted to " + authState.source)
233
+ } else {
234
+ ctx.host.log.warn("refresh succeeded but auth persistence was not possible")
235
+ }
236
+ } catch (e) {
237
+ ctx.host.log.warn("refresh succeeded but failed to save auth: " + String(e))
238
+ }
239
+
240
+ return newAccessToken
241
+ } catch (e) {
242
+ if (typeof e === "string") throw e
243
+ ctx.host.log.error("refresh exception: " + String(e))
244
+ return null
245
+ }
246
+ }
247
+
248
+ function fetchUsage(ctx, accessToken, accountId) {
249
+ const headers = {
250
+ Authorization: "Bearer " + accessToken,
251
+ Accept: "application/json",
252
+ "User-Agent": "OpenUsage",
253
+ }
254
+ if (accountId) {
255
+ headers["ChatGPT-Account-Id"] = accountId
256
+ }
257
+ return ctx.util.request({
258
+ method: "GET",
259
+ url: USAGE_URL,
260
+ headers,
261
+ timeoutMs: 10000,
262
+ })
263
+ }
264
+
265
+ function readPercent(value) {
266
+ const n = Number(value)
267
+ return Number.isFinite(n) ? n : null
268
+ }
269
+
270
+ function readNumber(value) {
271
+ const n = Number(value)
272
+ return Number.isFinite(n) ? n : null
273
+ }
274
+
275
+ function getResetsAtIso(ctx, nowSec, window) {
276
+ if (!window) return null
277
+ if (typeof window.reset_at === "number") {
278
+ return ctx.util.toIso(window.reset_at)
279
+ }
280
+ if (typeof window.reset_after_seconds === "number") {
281
+ return ctx.util.toIso(nowSec + window.reset_after_seconds)
282
+ }
283
+ return null
284
+ }
285
+
286
+ // Period durations in milliseconds
287
+ var PERIOD_SESSION_MS = 5 * 60 * 60 * 1000 // 5 hours
288
+ var PERIOD_WEEKLY_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
289
+
290
+ function queryTokenUsage(ctx) {
291
+ if (!ctx.host.ccusage || typeof ctx.host.ccusage.query !== "function") {
292
+ return { status: "no_runner", data: null }
293
+ }
294
+
295
+ const since = new Date()
296
+ // Inclusive range: today + previous 30 days = 31 calendar days.
297
+ since.setDate(since.getDate() - 30)
298
+ const y = since.getFullYear()
299
+ const m = since.getMonth() + 1
300
+ const d = since.getDate()
301
+ const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d
302
+ const queryOpts = { provider: "codex", since: sinceStr }
303
+ const codexHome = readCodexHome(ctx)
304
+ if (codexHome) {
305
+ queryOpts.homePath = codexHome
306
+ }
307
+
308
+ const result = ctx.host.ccusage.query(queryOpts)
309
+ if (!result || typeof result !== "object" || typeof result.status !== "string") {
310
+ return { status: "runner_failed", data: null }
311
+ }
312
+ if (result.status !== "ok") {
313
+ return { status: result.status, data: null }
314
+ }
315
+ if (!result.data || !Array.isArray(result.data.daily)) {
316
+ return { status: "runner_failed", data: null }
317
+ }
318
+ return { status: "ok", data: result.data }
319
+ }
320
+
321
+ function fmtTokens(n) {
322
+ const abs = Math.abs(n)
323
+ const sign = n < 0 ? "-" : ""
324
+ const units = [
325
+ { threshold: 1e9, divisor: 1e9, suffix: "B" },
326
+ { threshold: 1e6, divisor: 1e6, suffix: "M" },
327
+ { threshold: 1e3, divisor: 1e3, suffix: "K" },
328
+ ]
329
+ for (let i = 0; i < units.length; i++) {
330
+ const unit = units[i]
331
+ if (abs >= unit.threshold) {
332
+ const scaled = abs / unit.divisor
333
+ const formatted = scaled >= 10
334
+ ? Math.round(scaled).toString()
335
+ : scaled.toFixed(1).replace(/\.0$/, "")
336
+ return sign + formatted + unit.suffix
337
+ }
338
+ }
339
+ return sign + Math.round(abs).toString()
340
+ }
341
+
342
+ function dayKeyFromDate(date) {
343
+ const year = date.getFullYear()
344
+ const month = date.getMonth() + 1
345
+ const day = date.getDate()
346
+ return year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day
347
+ }
348
+
349
+ function dayKeyFromUsageDate(rawDate) {
350
+ if (typeof rawDate !== "string") return null
351
+ const value = rawDate.trim()
352
+ if (!value) return null
353
+
354
+ const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/)
355
+ if (isoMatch) {
356
+ return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3]
357
+ }
358
+
359
+ const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/)
360
+ if (compactMatch) {
361
+ return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3]
362
+ }
363
+
364
+ const ms = Date.parse(value)
365
+ if (!Number.isFinite(ms)) return null
366
+ return dayKeyFromDate(new Date(ms))
367
+ }
368
+
369
+ function usageCostUsd(day) {
370
+ if (!day || typeof day !== "object") return null
371
+
372
+ if (day.totalCost != null) {
373
+ const totalCost = Number(day.totalCost)
374
+ if (Number.isFinite(totalCost)) return totalCost
375
+ }
376
+
377
+ if (day.costUSD != null) {
378
+ const costUSD = Number(day.costUSD)
379
+ if (Number.isFinite(costUSD)) return costUSD
380
+ }
381
+
382
+ return null
383
+ }
384
+
385
+ function costAndTokensLabel(data, opts) {
386
+ const includeZeroTokens = !!(opts && opts.includeZeroTokens)
387
+ const parts = []
388
+ if (data.costUSD != null) parts.push("$" + data.costUSD.toFixed(2))
389
+ if (data.tokens > 0 || (includeZeroTokens && data.tokens === 0)) {
390
+ parts.push(fmtTokens(data.tokens) + " tokens")
391
+ }
392
+ return parts.join(" · ")
393
+ }
394
+
395
+ function pushDayUsageLine(lines, ctx, label, dayEntry) {
396
+ const tokens = Number(dayEntry && dayEntry.totalTokens) || 0
397
+ const cost = usageCostUsd(dayEntry)
398
+ if (tokens > 0) {
399
+ lines.push(ctx.line.text({
400
+ label: label,
401
+ value: costAndTokensLabel({ tokens: tokens, costUSD: cost })
402
+ }))
403
+ return
404
+ }
405
+
406
+ lines.push(ctx.line.text({
407
+ label: label,
408
+ value: costAndTokensLabel({ tokens: 0, costUSD: 0 }, { includeZeroTokens: true })
409
+ }))
410
+ }
411
+
412
+ function probe(ctx) {
413
+ const authState = loadAuth(ctx)
414
+ if (!authState || !authState.auth) {
415
+ ctx.host.log.error("probe failed: not logged in")
416
+ throw "Not logged in. Run `codex` to authenticate."
417
+ }
418
+ const auth = authState.auth
419
+
420
+ if (auth.tokens && auth.tokens.access_token) {
421
+ const nowMs = Date.now()
422
+ let accessToken = auth.tokens.access_token
423
+ const accountId = auth.tokens.account_id
424
+
425
+ if (needsRefresh(ctx, auth, nowMs)) {
426
+ ctx.host.log.info("token needs refresh (age > " + (REFRESH_AGE_MS / 1000 / 60 / 60 / 24) + " days)")
427
+ const refreshed = refreshToken(ctx, authState)
428
+ if (refreshed) {
429
+ accessToken = refreshed
430
+ } else {
431
+ ctx.host.log.warn("proactive refresh failed, trying with existing token")
432
+ }
433
+ }
434
+
435
+ let resp
436
+ let didRefresh = false
437
+ try {
438
+ resp = ctx.util.retryOnceOnAuth({
439
+ request: (token) => {
440
+ try {
441
+ return fetchUsage(ctx, token || accessToken, accountId)
442
+ } catch (e) {
443
+ ctx.host.log.error("usage request exception: " + String(e))
444
+ if (didRefresh) {
445
+ throw "Usage request failed after refresh. Try again."
446
+ }
447
+ throw "Usage request failed. Check your connection."
448
+ }
449
+ },
450
+ refresh: () => {
451
+ ctx.host.log.info("usage returned 401, attempting refresh")
452
+ didRefresh = true
453
+ return refreshToken(ctx, authState)
454
+ },
455
+ })
456
+ } catch (e) {
457
+ if (typeof e === "string") throw e
458
+ ctx.host.log.error("usage request failed: " + String(e))
459
+ throw "Usage request failed. Check your connection."
460
+ }
461
+
462
+ if (ctx.util.isAuthStatus(resp.status)) {
463
+ ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
464
+ throw "Token expired. Run `codex` to log in again."
465
+ }
466
+
467
+ if (resp.status < 200 || resp.status >= 300) {
468
+ ctx.host.log.error("usage returned error: status=" + resp.status)
469
+ throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
470
+ }
471
+
472
+ ctx.host.log.info("usage fetch succeeded")
473
+
474
+ const data = ctx.util.tryParseJson(resp.bodyText)
475
+ if (data === null) {
476
+ throw "Usage response invalid. Try again later."
477
+ }
478
+
479
+ const lines = []
480
+ const nowSec = Math.floor(Date.now() / 1000)
481
+ const rateLimit = data.rate_limit || null
482
+ const primaryWindow = rateLimit && rateLimit.primary_window ? rateLimit.primary_window : null
483
+ const secondaryWindow = rateLimit && rateLimit.secondary_window ? rateLimit.secondary_window : null
484
+ const reviewWindow =
485
+ data.code_review_rate_limit && data.code_review_rate_limit.primary_window
486
+ ? data.code_review_rate_limit.primary_window
487
+ : null
488
+
489
+ const headerPrimary = readPercent(resp.headers["x-codex-primary-used-percent"])
490
+ const headerSecondary = readPercent(resp.headers["x-codex-secondary-used-percent"])
491
+
492
+ if (headerPrimary !== null) {
493
+ lines.push(ctx.line.progress({
494
+ label: "Session",
495
+ used: headerPrimary,
496
+ limit: 100,
497
+ format: { kind: "percent" },
498
+ resetsAt: getResetsAtIso(ctx, nowSec, primaryWindow),
499
+ periodDurationMs: PERIOD_SESSION_MS
500
+ }))
501
+ }
502
+ if (headerSecondary !== null) {
503
+ lines.push(ctx.line.progress({
504
+ label: "Weekly",
505
+ used: headerSecondary,
506
+ limit: 100,
507
+ format: { kind: "percent" },
508
+ resetsAt: getResetsAtIso(ctx, nowSec, secondaryWindow),
509
+ periodDurationMs: PERIOD_WEEKLY_MS
510
+ }))
511
+ }
512
+
513
+ if (lines.length === 0 && data.rate_limit) {
514
+ if (data.rate_limit.primary_window && typeof data.rate_limit.primary_window.used_percent === "number") {
515
+ lines.push(ctx.line.progress({
516
+ label: "Session",
517
+ used: data.rate_limit.primary_window.used_percent,
518
+ limit: 100,
519
+ format: { kind: "percent" },
520
+ resetsAt: getResetsAtIso(ctx, nowSec, primaryWindow),
521
+ periodDurationMs: PERIOD_SESSION_MS
522
+ }))
523
+ }
524
+ if (data.rate_limit.secondary_window && typeof data.rate_limit.secondary_window.used_percent === "number") {
525
+ lines.push(ctx.line.progress({
526
+ label: "Weekly",
527
+ used: data.rate_limit.secondary_window.used_percent,
528
+ limit: 100,
529
+ format: { kind: "percent" },
530
+ resetsAt: getResetsAtIso(ctx, nowSec, secondaryWindow),
531
+ periodDurationMs: PERIOD_WEEKLY_MS
532
+ }))
533
+ }
534
+ }
535
+
536
+ if (Array.isArray(data.additional_rate_limits)) {
537
+ for (const entry of data.additional_rate_limits) {
538
+ if (!entry || !entry.rate_limit) continue
539
+ const name = typeof entry.limit_name === "string" ? entry.limit_name : ""
540
+ let shortName = name.replace(/^GPT-[\d.]+-Codex-/, "")
541
+ if (!shortName) shortName = name || "Model"
542
+ const rl = entry.rate_limit
543
+ if (rl.primary_window && typeof rl.primary_window.used_percent === "number") {
544
+ lines.push(ctx.line.progress({
545
+ label: shortName,
546
+ used: rl.primary_window.used_percent,
547
+ limit: 100,
548
+ format: { kind: "percent" },
549
+ resetsAt: getResetsAtIso(ctx, nowSec, rl.primary_window),
550
+ periodDurationMs: typeof rl.primary_window.limit_window_seconds === "number"
551
+ ? rl.primary_window.limit_window_seconds * 1000
552
+ : PERIOD_SESSION_MS
553
+ }))
554
+ }
555
+ if (rl.secondary_window && typeof rl.secondary_window.used_percent === "number") {
556
+ lines.push(ctx.line.progress({
557
+ label: shortName + " Weekly",
558
+ used: rl.secondary_window.used_percent,
559
+ limit: 100,
560
+ format: { kind: "percent" },
561
+ resetsAt: getResetsAtIso(ctx, nowSec, rl.secondary_window),
562
+ periodDurationMs: typeof rl.secondary_window.limit_window_seconds === "number"
563
+ ? rl.secondary_window.limit_window_seconds * 1000
564
+ : PERIOD_WEEKLY_MS
565
+ }))
566
+ }
567
+ }
568
+ }
569
+
570
+ if (reviewWindow) {
571
+ const used = reviewWindow.used_percent
572
+ if (typeof used === "number") {
573
+ lines.push(ctx.line.progress({
574
+ label: "Reviews",
575
+ used: used,
576
+ limit: 100,
577
+ format: { kind: "percent" },
578
+ resetsAt: getResetsAtIso(ctx, nowSec, reviewWindow),
579
+ periodDurationMs: PERIOD_WEEKLY_MS // code_review_rate_limit is a 7-day window
580
+ }))
581
+ }
582
+ }
583
+
584
+ const creditsBalance = resp.headers["x-codex-credits-balance"]
585
+ const creditsHeader = readNumber(creditsBalance)
586
+ const creditsData = data.credits ? readNumber(data.credits.balance) : null
587
+ const creditsRemaining = creditsHeader ?? creditsData
588
+ if (creditsRemaining !== null) {
589
+ const remaining = creditsRemaining
590
+ const limit = 1000
591
+ const used = Math.max(0, Math.min(limit, limit - remaining))
592
+ lines.push(ctx.line.progress({
593
+ label: "Credits",
594
+ used: used,
595
+ limit: limit,
596
+ format: { kind: "count", suffix: "credits" },
597
+ }))
598
+ }
599
+
600
+ let plan = null
601
+ if (data.plan_type) {
602
+ const planLabel = ctx.fmt.planLabel(data.plan_type)
603
+ if (planLabel) {
604
+ plan = planLabel
605
+ }
606
+ }
607
+
608
+ const tokenUsageResult = queryTokenUsage(ctx)
609
+ if (tokenUsageResult.status === "ok") {
610
+ const tokenUsage = tokenUsageResult.data
611
+ const now = new Date()
612
+ const todayKey = dayKeyFromDate(now)
613
+ const yesterday = new Date(now.getTime())
614
+ yesterday.setDate(yesterday.getDate() - 1)
615
+ const yesterdayKey = dayKeyFromDate(yesterday)
616
+
617
+ let todayEntry = null
618
+ let yesterdayEntry = null
619
+ for (let i = 0; i < tokenUsage.daily.length; i++) {
620
+ const usageDayKey = dayKeyFromUsageDate(tokenUsage.daily[i].date)
621
+ if (usageDayKey === todayKey) {
622
+ todayEntry = tokenUsage.daily[i]
623
+ continue
624
+ }
625
+ if (usageDayKey === yesterdayKey) {
626
+ yesterdayEntry = tokenUsage.daily[i]
627
+ }
628
+ }
629
+
630
+ pushDayUsageLine(lines, ctx, "Today", todayEntry)
631
+ pushDayUsageLine(lines, ctx, "Yesterday", yesterdayEntry)
632
+
633
+ let totalTokens = 0
634
+ let totalCostNanos = 0
635
+ let hasCost = false
636
+ for (let i = 0; i < tokenUsage.daily.length; i++) {
637
+ const day = tokenUsage.daily[i]
638
+ const dayTokens = Number(day.totalTokens)
639
+ if (Number.isFinite(dayTokens)) {
640
+ totalTokens += dayTokens
641
+ }
642
+
643
+ const dayCost = usageCostUsd(day)
644
+ if (dayCost != null) {
645
+ totalCostNanos += Math.round(dayCost * 1e9)
646
+ hasCost = true
647
+ }
648
+ }
649
+
650
+ if (totalTokens > 0) {
651
+ lines.push(ctx.line.text({
652
+ label: "Last 30 Days",
653
+ value: costAndTokensLabel({ tokens: totalTokens, costUSD: hasCost ? totalCostNanos / 1e9 : null })
654
+ }))
655
+ }
656
+ }
657
+
658
+ if (lines.length === 0) {
659
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
660
+ }
661
+
662
+ return { plan: plan, lines: lines }
663
+ }
664
+
665
+ if (auth.OPENAI_API_KEY) {
666
+ throw "Usage not available for API key."
667
+ }
668
+
669
+ throw "Not logged in. Run `codex` to authenticate."
670
+ }
671
+
672
+ globalThis.__openusage_plugin = { id: "codex", probe }
673
+ })()