@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,1009 @@
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 CREDIT_USD_RATE = 0.04
9
+ const REFRESH_AGE_MS = 8 * 24 * 60 * 60 * 1000
10
+ const ACCESS_TOKEN_REFRESH_WINDOW_MS = 5 * 60 * 1000
11
+ const ERR_NOT_LOGGED_IN = "Not logged in. Run `codex` to authenticate."
12
+ const ERR_SESSION_EXPIRED = "Session expired. Run `codex` to log in again."
13
+ const ERR_TOKEN_CONFLICT = "Token conflict. Run `codex` to log in again."
14
+ const ERR_TOKEN_REVOKED = "Token revoked. Run `codex` to log in again."
15
+ const ERR_TOKEN_EXPIRED = "Token expired. Run `codex` to log in again."
16
+ const ERR_USAGE_API_KEY = "Usage not available for API key."
17
+ const ERR_USAGE_CONNECTION = "Usage request failed. Check your connection."
18
+ const ERR_USAGE_AFTER_REFRESH = "Usage request failed after refresh. Try again."
19
+
20
+ function joinPath(base, leaf) {
21
+ return base.replace(/[\\/]+$/, "") + "/" + leaf
22
+ }
23
+
24
+ function readCodexHome(ctx) {
25
+ if (!ctx.host.env || typeof ctx.host.env.get !== "function") {
26
+ return null
27
+ }
28
+
29
+ try {
30
+ const value = ctx.host.env.get("CODEX_HOME")
31
+ if (typeof value !== "string") return null
32
+ const trimmed = value.trim()
33
+ return trimmed || null
34
+ } catch (e) {
35
+ ctx.host.log.warn("CODEX_HOME read failed: " + String(e))
36
+ return null
37
+ }
38
+ }
39
+
40
+ function readEnvUrl(ctx, name, fallback) {
41
+ try {
42
+ const value = ctx.host.env.get(name)
43
+ if (typeof value === "string" && value.trim()) return value.trim()
44
+ } catch {}
45
+ return fallback
46
+ }
47
+
48
+ function decodeHexUtf8(hex) {
49
+ try {
50
+ const bytes = []
51
+ for (let i = 0; i < hex.length; i += 2) {
52
+ bytes.push(parseInt(hex.slice(i, i + 2), 16))
53
+ }
54
+
55
+ if (typeof TextDecoder !== "undefined") {
56
+ try {
57
+ return new TextDecoder("utf-8", { fatal: false }).decode(new Uint8Array(bytes))
58
+ } catch {}
59
+ }
60
+
61
+ let escaped = ""
62
+ for (const b of bytes) {
63
+ const h = b.toString(16)
64
+ escaped += "%" + (h.length === 1 ? "0" + h : h)
65
+ }
66
+ return decodeURIComponent(escaped)
67
+ } catch {
68
+ return null
69
+ }
70
+ }
71
+
72
+ function tryParseAuthJson(ctx, text) {
73
+ if (!text) return null
74
+ const parsed = ctx.util.tryParseJson(text)
75
+ if (parsed) return parsed
76
+
77
+ // Some keychain payloads can be returned as hex-encoded UTF-8 bytes.
78
+ let hex = String(text).trim()
79
+ if (hex.startsWith("0x") || hex.startsWith("0X")) hex = hex.slice(2)
80
+ if (!hex || hex.length % 2 !== 0) return null
81
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null
82
+
83
+ const decoded = decodeHexUtf8(hex)
84
+ if (!decoded) return null
85
+ return ctx.util.tryParseJson(decoded)
86
+ }
87
+
88
+ function resolveAuthPaths(ctx) {
89
+ const codexHome = readCodexHome(ctx)
90
+
91
+ // If CODEX_HOME is set, use it
92
+ if (codexHome) {
93
+ return [joinPath(codexHome, AUTH_FILE)]
94
+ }
95
+
96
+ return CONFIG_AUTH_PATHS.map((basePath) => joinPath(basePath, AUTH_FILE))
97
+ }
98
+
99
+ function hasTokenLikeAuth(auth) {
100
+ if (!auth || typeof auth !== "object") return false
101
+ if (auth.tokens && auth.tokens.access_token) return true
102
+ if (auth.OPENAI_API_KEY) return true
103
+ return false
104
+ }
105
+
106
+ function hasAccessTokenAuth(auth) {
107
+ return !!(auth && auth.tokens && auth.tokens.access_token)
108
+ }
109
+
110
+ function isAuthFallbackError(e) {
111
+ if (typeof e !== "string") return false
112
+ return (
113
+ e === ERR_SESSION_EXPIRED ||
114
+ e === ERR_TOKEN_CONFLICT ||
115
+ e === ERR_TOKEN_REVOKED ||
116
+ e === ERR_TOKEN_EXPIRED
117
+ )
118
+ }
119
+
120
+ function loadAuthFromKeychain(ctx) {
121
+ if (!ctx.host.keychain || typeof ctx.host.keychain.readGenericPassword !== "function") {
122
+ return null
123
+ }
124
+
125
+ try {
126
+ const value = ctx.host.keychain.readGenericPassword(KEYCHAIN_SERVICE)
127
+ if (!value) return null
128
+ const auth = tryParseAuthJson(ctx, value)
129
+ if (!hasTokenLikeAuth(auth)) {
130
+ ctx.host.log.warn("keychain has data but no codex auth payload")
131
+ return null
132
+ }
133
+ ctx.host.log.info("auth loaded from keychain: " + KEYCHAIN_SERVICE)
134
+ return { auth, authPath: null, source: "keychain" }
135
+ } catch (e) {
136
+ ctx.host.log.info("keychain read failed (may not exist): " + String(e))
137
+ return null
138
+ }
139
+ }
140
+
141
+ function nativeAuthStorage(ctx) {
142
+ const settings = (ctx.provider && ctx.provider.settings) || {}
143
+ return typeof settings.authStorage === "string" ? settings.authStorage : "auto"
144
+ }
145
+
146
+ function loadNativeAuth(ctx) {
147
+ return JSON.parse(ctx.host.codex.readAuth(nativeAuthStorage(ctx)))
148
+ }
149
+
150
+ function saveAuth(ctx, authState) {
151
+ const auth = authState && authState.auth ? authState.auth : null
152
+ if (!auth) return false
153
+
154
+ if (authState.source === "native") {
155
+ if (authState.readOnly) return false
156
+ ctx.host.codex.writeAuth(nativeAuthStorage(ctx), authState.profileKey, authState.revision, authState.storage, JSON.stringify(auth))
157
+ return true
158
+ }
159
+
160
+ if (authState.source === "file" && authState.authPath) {
161
+ ctx.host.fs.writeText(authState.authPath, JSON.stringify(auth, null, 2))
162
+ return true
163
+ }
164
+
165
+ if (authState.source === "keychain") {
166
+ if (!ctx.host.keychain || typeof ctx.host.keychain.writeGenericPassword !== "function") {
167
+ ctx.host.log.warn("keychain write unsupported in this host")
168
+ return false
169
+ }
170
+ // Use compact JSON to avoid newline-induced keychain encoding issues.
171
+ ctx.host.keychain.writeGenericPassword(KEYCHAIN_SERVICE, JSON.stringify(auth))
172
+ return true
173
+ }
174
+
175
+ return false
176
+ }
177
+
178
+ function loadFileAuthCandidates(ctx) {
179
+ const authPaths = resolveAuthPaths(ctx)
180
+ const candidates = []
181
+ const missingPaths = []
182
+ for (const authPath of authPaths) {
183
+ if (!ctx.host.fs.exists(authPath)) {
184
+ missingPaths.push(authPath)
185
+ continue
186
+ }
187
+ try {
188
+ const text = ctx.host.fs.readText(authPath)
189
+ const auth = tryParseAuthJson(ctx, text)
190
+ if (!hasTokenLikeAuth(auth)) {
191
+ ctx.host.log.warn("auth file exists but no valid codex auth payload: " + authPath)
192
+ continue
193
+ }
194
+ ctx.host.log.info("auth loaded from file: " + authPath)
195
+ candidates.push({ auth, authPath, source: "file" })
196
+ } catch (e) {
197
+ ctx.host.log.warn("auth file read failed: " + authPath + ": " + String(e))
198
+ }
199
+ }
200
+
201
+ return { candidates, missingPaths }
202
+ }
203
+
204
+ function needsRefresh(ctx, auth, nowMs) {
205
+ const accessToken = auth.tokens && auth.tokens.access_token
206
+ if (accessToken && ctx.jwt && typeof ctx.jwt.decodePayload === "function") {
207
+ const payload = ctx.jwt.decodePayload(accessToken)
208
+ const expiresAtSeconds = payload && payload.exp
209
+ if (typeof expiresAtSeconds === "number" && Number.isFinite(expiresAtSeconds)) {
210
+ const expiresAtMs = expiresAtSeconds * 1000
211
+ return expiresAtMs <= nowMs + ACCESS_TOKEN_REFRESH_WINDOW_MS
212
+ }
213
+ }
214
+
215
+ if (!auth.last_refresh) return false
216
+ const lastMs = ctx.util.parseDateMs(auth.last_refresh)
217
+ if (lastMs === null) return false
218
+ return nowMs - lastMs > REFRESH_AGE_MS
219
+ }
220
+
221
+ function reloadAuthState(ctx, authState) {
222
+ let reloaded = null
223
+ if (authState.source === "file" && authState.authPath) {
224
+ try {
225
+ const auth = tryParseAuthJson(ctx, ctx.host.fs.readText(authState.authPath))
226
+ if (hasTokenLikeAuth(auth)) {
227
+ reloaded = { auth, authPath: authState.authPath, source: "file" }
228
+ }
229
+ } catch (e) {
230
+ ctx.host.log.warn("auth reload failed for file " + authState.authPath + ": " + String(e))
231
+ }
232
+ } else if (authState.source === "native") {
233
+ reloaded = loadNativeAuth(ctx)
234
+ } else if (authState.source === "keychain") {
235
+ reloaded = loadAuthFromKeychain(ctx)
236
+ }
237
+
238
+ if (!reloaded) return { status: "unchanged", authState }
239
+ if (!hasAccessTokenAuth(reloaded.auth)) {
240
+ return { status: "error", error: ERR_TOKEN_CONFLICT }
241
+ }
242
+
243
+ const expectedAccountId = authState.auth.tokens && authState.auth.tokens.account_id
244
+ const reloadedAccountId = reloaded.auth.tokens && reloaded.auth.tokens.account_id
245
+ if (expectedAccountId && reloadedAccountId !== expectedAccountId) {
246
+ return { status: "error", error: ERR_TOKEN_CONFLICT }
247
+ }
248
+
249
+ if (JSON.stringify(reloaded.auth) !== JSON.stringify(authState.auth)) {
250
+ ctx.host.log.info("auth changed during guarded reload, using updated credentials")
251
+ return { status: "changed", authState: reloaded }
252
+ }
253
+ return { status: "unchanged", authState }
254
+ }
255
+
256
+ function refreshToken(ctx, authState) {
257
+ // Rotating an encrypted store's refresh token without updating that store
258
+ // would invalidate the CLI session. Let the owning Codex CLI refresh it.
259
+ if (authState.readOnly) throw ERR_SESSION_EXPIRED
260
+ const auth = authState.auth
261
+ if (!auth.tokens || !auth.tokens.refresh_token) {
262
+ ctx.host.log.warn("refresh skipped: no refresh token")
263
+ return null
264
+ }
265
+
266
+ ctx.host.log.info("attempting token refresh")
267
+ try {
268
+ const resp = ctx.util.request({
269
+ method: "POST",
270
+ url: readEnvUrl(ctx, "CODEX_REFRESH_URL", REFRESH_URL),
271
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
272
+ bodyText:
273
+ "grant_type=refresh_token" +
274
+ "&client_id=" + encodeURIComponent(CLIENT_ID) +
275
+ "&refresh_token=" + encodeURIComponent(auth.tokens.refresh_token),
276
+ timeoutMs: 15000,
277
+ })
278
+
279
+ if (resp.status === 400 || resp.status === 401) {
280
+ let code = null
281
+ const body = ctx.util.tryParseJson(resp.bodyText)
282
+ if (body) {
283
+ code = body.error?.code || body.error || body.code
284
+ }
285
+ ctx.host.log.error("refresh failed: status=" + resp.status + " code=" + String(code))
286
+ if (code === "refresh_token_expired") {
287
+ throw ERR_SESSION_EXPIRED
288
+ }
289
+ if (code === "refresh_token_reused") {
290
+ throw ERR_TOKEN_CONFLICT
291
+ }
292
+ if (code === "refresh_token_invalidated") {
293
+ throw ERR_TOKEN_REVOKED
294
+ }
295
+ throw ERR_TOKEN_EXPIRED
296
+ }
297
+ if (resp.status < 200 || resp.status >= 300) {
298
+ ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
299
+ return null
300
+ }
301
+
302
+ const body = ctx.util.tryParseJson(resp.bodyText)
303
+ if (!body) {
304
+ ctx.host.log.warn("refresh response not valid JSON")
305
+ return null
306
+ }
307
+ const newAccessToken = body.access_token
308
+ if (!newAccessToken) {
309
+ ctx.host.log.warn("refresh response missing access_token")
310
+ return null
311
+ }
312
+
313
+ auth.tokens.access_token = newAccessToken
314
+ if (body.refresh_token) auth.tokens.refresh_token = body.refresh_token
315
+ if (body.id_token) auth.tokens.id_token = body.id_token
316
+ auth.last_refresh = new Date().toISOString()
317
+
318
+ try {
319
+ const saved = saveAuth(ctx, authState)
320
+ if (saved) {
321
+ ctx.host.log.info("refresh succeeded, auth persisted to " + authState.source)
322
+ } else {
323
+ ctx.host.log.warn("refresh succeeded but auth persistence was not possible")
324
+ }
325
+ } catch (e) {
326
+ ctx.host.log.warn("refresh succeeded but failed to save auth: " + String(e))
327
+ }
328
+
329
+ return newAccessToken
330
+ } catch (e) {
331
+ if (typeof e === "string") throw e
332
+ ctx.host.log.error("refresh exception: " + String(e))
333
+ return null
334
+ }
335
+ }
336
+
337
+ function fetchUsage(ctx, accessToken, accountId) {
338
+ const headers = {
339
+ Authorization: "Bearer " + accessToken,
340
+ Accept: "application/json",
341
+ "User-Agent": "OpenUsage",
342
+ }
343
+ if (accountId) {
344
+ headers["ChatGPT-Account-Id"] = accountId
345
+ }
346
+ return ctx.util.request({
347
+ method: "GET",
348
+ url: readEnvUrl(ctx, "CODEX_USAGE_URL", USAGE_URL),
349
+ headers,
350
+ timeoutMs: 10000,
351
+ })
352
+ }
353
+
354
+ function readPercent(value) {
355
+ const n = Number(value)
356
+ return Number.isFinite(n) ? n : null
357
+ }
358
+
359
+ function readNumber(value) {
360
+ const n = Number(value)
361
+ return Number.isFinite(n) ? n : null
362
+ }
363
+
364
+ function readCreditsRemaining(resp, data) {
365
+ const credits = data && data.credits && typeof data.credits === "object" ? data.credits : null
366
+ if (credits) {
367
+ const bodyBalance = readNumber(credits.balance)
368
+ if (bodyBalance !== null) return bodyBalance
369
+ if (credits.has_credits === false) return 0
370
+ }
371
+
372
+ return readNumber(resp.headers["x-codex-credits-balance"])
373
+ }
374
+
375
+ function formatCodexPlan(ctx, planType) {
376
+ const rawPlan = typeof planType === "string" ? planType.trim() : ""
377
+ if (!rawPlan) return null
378
+ if (rawPlan.toLowerCase() === "prolite") return "Pro 5x"
379
+ if (rawPlan.toLowerCase() === "pro") return "Pro 20x"
380
+ return ctx.fmt.planLabel(rawPlan) || null
381
+ }
382
+
383
+ function getResetsAtIso(ctx, nowSec, window) {
384
+ if (!window) return null
385
+ if (typeof window.reset_at === "number") {
386
+ return ctx.util.toIso(window.reset_at)
387
+ }
388
+ if (typeof window.reset_after_seconds === "number") {
389
+ return ctx.util.toIso(nowSec + window.reset_after_seconds)
390
+ }
391
+ return null
392
+ }
393
+
394
+ // Period durations in milliseconds
395
+ var PERIOD_SESSION_MS = 5 * 60 * 60 * 1000 // 5 hours
396
+ var PERIOD_WEEKLY_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
397
+
398
+ function queryTokenUsage(ctx) {
399
+ if (!ctx.host.ccusage || typeof ctx.host.ccusage.query !== "function") {
400
+ return { status: "no_runner", data: null }
401
+ }
402
+
403
+ const since = new Date()
404
+ // Inclusive range: today + previous 30 days = 31 calendar days.
405
+ since.setDate(since.getDate() - 30)
406
+ const y = since.getFullYear()
407
+ const m = since.getMonth() + 1
408
+ const d = since.getDate()
409
+ const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d
410
+ const queryOpts = { provider: "codex", since: sinceStr }
411
+ const codexHome = readCodexHome(ctx)
412
+ if (codexHome) {
413
+ queryOpts.homePath = codexHome
414
+ }
415
+
416
+ const result = ctx.host.ccusage.query(queryOpts)
417
+ if (!result || typeof result !== "object" || typeof result.status !== "string") {
418
+ return { status: "runner_failed", data: null }
419
+ }
420
+ if (result.status !== "ok") {
421
+ return { status: result.status, data: null }
422
+ }
423
+ if (!result.data || !Array.isArray(result.data.daily)) {
424
+ return { status: "runner_failed", data: null }
425
+ }
426
+ return { status: "ok", data: result.data }
427
+ }
428
+
429
+ function fmtTokens(n) {
430
+ const abs = Math.abs(n)
431
+ const sign = n < 0 ? "-" : ""
432
+ const units = [
433
+ { threshold: 1e9, divisor: 1e9, suffix: "B" },
434
+ { threshold: 1e6, divisor: 1e6, suffix: "M" },
435
+ { threshold: 1e3, divisor: 1e3, suffix: "K" },
436
+ ]
437
+ for (let i = 0; i < units.length; i++) {
438
+ const unit = units[i]
439
+ if (abs >= unit.threshold) {
440
+ const scaled = abs / unit.divisor
441
+ const formatted = scaled >= 10
442
+ ? Math.round(scaled).toString()
443
+ : scaled.toFixed(1).replace(/\.0$/, "")
444
+ return sign + formatted + unit.suffix
445
+ }
446
+ }
447
+ return sign + Math.round(abs).toString()
448
+ }
449
+
450
+ function dayKeyFromDate(date) {
451
+ const year = date.getFullYear()
452
+ const month = date.getMonth() + 1
453
+ const day = date.getDate()
454
+ return year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day
455
+ }
456
+
457
+ function dayKeyFromUsageDate(rawDate) {
458
+ if (typeof rawDate !== "string") return null
459
+ const value = rawDate.trim()
460
+ if (!value) return null
461
+
462
+ const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/)
463
+ if (isoMatch) {
464
+ return isoMatch[1] + "-" + isoMatch[2] + "-" + isoMatch[3]
465
+ }
466
+
467
+ const isoDatePrefixMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})(?:[Tt\s]|$)/)
468
+ if (isoDatePrefixMatch) {
469
+ return isoDatePrefixMatch[1] + "-" + isoDatePrefixMatch[2] + "-" + isoDatePrefixMatch[3]
470
+ }
471
+
472
+ const compactMatch = value.match(/^(\d{4})(\d{2})(\d{2})$/)
473
+ if (compactMatch) {
474
+ return compactMatch[1] + "-" + compactMatch[2] + "-" + compactMatch[3]
475
+ }
476
+
477
+ const ms = Date.parse(value)
478
+ if (!Number.isFinite(ms)) return null
479
+ return dayKeyFromDate(new Date(ms))
480
+ }
481
+
482
+ function usageCostUsd(day) {
483
+ if (!day || typeof day !== "object") return null
484
+
485
+ if (day.totalCost != null) {
486
+ const totalCost = Number(day.totalCost)
487
+ if (Number.isFinite(totalCost)) return totalCost
488
+ }
489
+
490
+ if (day.costUSD != null) {
491
+ const costUSD = Number(day.costUSD)
492
+ if (Number.isFinite(costUSD)) return costUSD
493
+ }
494
+
495
+ return null
496
+ }
497
+
498
+ function costAndTokensLabel(data, opts) {
499
+ const includeZeroTokens = !!(opts && opts.includeZeroTokens)
500
+ const parts = []
501
+ if (data.costUSD != null) parts.push("$" + data.costUSD.toFixed(2))
502
+ if (data.tokens > 0 || (includeZeroTokens && data.tokens === 0)) {
503
+ parts.push(fmtTokens(data.tokens) + " tokens")
504
+ }
505
+ return parts.join(" · ")
506
+ }
507
+
508
+ function modelTokenCount(modelUsage) {
509
+ if (!modelUsage || typeof modelUsage !== "object") return 0
510
+ const total = Number(modelUsage.totalTokens)
511
+ if (Number.isFinite(total) && total > 0) return total
512
+
513
+ const fields = [
514
+ "inputTokens",
515
+ "cachedInputTokens",
516
+ "cacheCreationTokens",
517
+ "cacheReadTokens",
518
+ "outputTokens",
519
+ "reasoningOutputTokens",
520
+ ]
521
+ let sum = 0
522
+ for (let i = 0; i < fields.length; i++) {
523
+ const n = Number(modelUsage[fields[i]])
524
+ if (Number.isFinite(n) && n > 0) sum += n
525
+ }
526
+ return sum
527
+ }
528
+
529
+ function collectModelUsage(daily) {
530
+ const totals = {}
531
+ let totalTokens = 0
532
+ for (let i = 0; i < daily.length; i++) {
533
+ const day = daily[i]
534
+ const models = day && day.models
535
+ if (models && typeof models === "object") {
536
+ const names = Object.keys(models)
537
+ for (let j = 0; j < names.length; j++) {
538
+ const name = names[j]
539
+ const tokens = modelTokenCount(models[name])
540
+ if (tokens <= 0) continue
541
+ totals[name] = (totals[name] || 0) + tokens
542
+ totalTokens += tokens
543
+ }
544
+ }
545
+
546
+ const breakdowns = day && day.modelBreakdowns
547
+ if (Array.isArray(breakdowns)) {
548
+ for (let j = 0; j < breakdowns.length; j++) {
549
+ const breakdown = breakdowns[j]
550
+ const name = String(
551
+ (breakdown && (breakdown.modelName || breakdown.name || breakdown.model)) || ""
552
+ ).trim()
553
+ if (!name) continue
554
+ const tokens = modelTokenCount(breakdown)
555
+ if (tokens <= 0) continue
556
+ totals[name] = (totals[name] || 0) + tokens
557
+ totalTokens += tokens
558
+ }
559
+ }
560
+ }
561
+
562
+ if (totalTokens <= 0) return []
563
+ return Object.keys(totals)
564
+ .map((name) => ({ name, tokens: totals[name], percent: (totals[name] / totalTokens) * 100 }))
565
+ .sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name))
566
+ }
567
+
568
+ function percentLabel(value) {
569
+ if (value > 0 && value < 0.1) return "<0.1%"
570
+ const rounded = Math.round(value * 10) / 10
571
+ return (rounded % 1 === 0 ? String(Math.round(rounded)) : String(rounded)) + "%"
572
+ }
573
+
574
+ function pushModelUsageLines(lines, ctx, daily) {
575
+ const models = collectModelUsage(daily)
576
+ for (let i = 0; i < models.length; i++) {
577
+ const model = models[i]
578
+ lines.push(ctx.line.text({
579
+ label: model.name,
580
+ value: percentLabel(model.percent),
581
+ }))
582
+ }
583
+ }
584
+
585
+ function usageDayLabel(rawDate) {
586
+ const key = dayKeyFromUsageDate(rawDate)
587
+ if (!key) return String(rawDate || "").slice(0, 10) || "Usage"
588
+ const month = Number(key.slice(5, 7))
589
+ const day = Number(key.slice(8, 10))
590
+ return month + "/" + day
591
+ }
592
+
593
+ function collectUsageChartPoints(daily) {
594
+ const points = []
595
+ for (let i = 0; i < daily.length; i++) {
596
+ const day = daily[i]
597
+ const tokens = Number(day && day.totalTokens)
598
+ if (!Number.isFinite(tokens) || tokens < 0) continue
599
+ const key = dayKeyFromUsageDate(day.date)
600
+ if (!key) continue
601
+ points.push({
602
+ key: key,
603
+ label: usageDayLabel(day.date),
604
+ value: tokens,
605
+ valueLabel: fmtTokens(tokens) + " tokens",
606
+ })
607
+ }
608
+ return points
609
+ .sort((a, b) => a.key.localeCompare(b.key))
610
+ .slice(-31)
611
+ .map((point) => ({
612
+ label: point.label,
613
+ value: point.value,
614
+ valueLabel: point.valueLabel,
615
+ }))
616
+ }
617
+
618
+ function pushUsageChartLine(lines, ctx, daily) {
619
+ const points = collectUsageChartPoints(daily)
620
+ if (points.length === 0) return
621
+ lines.push(ctx.line.barChart({
622
+ label: "Usage Trend",
623
+ points: points,
624
+ note: "Estimated from local Codex logs for the selected account.",
625
+ color: "#74AA9C",
626
+ }))
627
+ }
628
+
629
+ function persistUsageDaily(ctx, daily, displayName) {
630
+ if (!ctx.host.usageDaily || typeof ctx.host.usageDaily.ingest !== "function") return
631
+ if (!daily || !daily.length) return
632
+ try {
633
+ ctx.host.usageDaily.ingest({ displayName: displayName, daily: daily })
634
+ } catch (e) { /* ignore */ }
635
+ }
636
+
637
+ function pushDayUsageLine(lines, ctx, label, dayEntry) {
638
+ const tokens = Number(dayEntry && dayEntry.totalTokens) || 0
639
+ const cost = usageCostUsd(dayEntry)
640
+ if (tokens > 0) {
641
+ lines.push(ctx.line.text({
642
+ label: label,
643
+ value: costAndTokensLabel({ tokens: tokens, costUSD: cost })
644
+ }))
645
+ return
646
+ }
647
+
648
+ lines.push(ctx.line.text({
649
+ label: label,
650
+ value: costAndTokensLabel({ tokens: 0, costUSD: 0 }, { includeZeroTokens: true })
651
+ }))
652
+ }
653
+
654
+ function probeWithAuthState(ctx, initialAuthState) {
655
+ let authState = initialAuthState
656
+ let auth = authState.auth
657
+
658
+ if (auth.tokens && auth.tokens.access_token) {
659
+ const nowMs = Date.now()
660
+ let accessToken = auth.tokens.access_token
661
+ let accountId = auth.tokens.account_id
662
+ let proactiveRefreshAuthError = null
663
+
664
+ if (needsRefresh(ctx, auth, nowMs)) {
665
+ ctx.host.log.info("token needs refresh")
666
+ const reload = reloadAuthState(ctx, authState)
667
+ if (reload.status === "error") throw reload.error
668
+ authState = reload.authState
669
+ auth = authState.auth
670
+ accessToken = auth.tokens.access_token
671
+ accountId = auth.tokens.account_id
672
+ let refreshed = null
673
+ if (needsRefresh(ctx, auth, nowMs)) {
674
+ try {
675
+ refreshed = refreshToken(ctx, authState)
676
+ } catch (e) {
677
+ if (!isAuthFallbackError(e)) throw e
678
+ proactiveRefreshAuthError = e
679
+ ctx.host.log.warn("proactive refresh failed, trying existing token: " + String(e))
680
+ }
681
+ }
682
+ if (refreshed) {
683
+ accessToken = refreshed
684
+ } else if (!proactiveRefreshAuthError) {
685
+ ctx.host.log.warn("proactive refresh failed, trying with existing token")
686
+ }
687
+ }
688
+
689
+ let resp
690
+ let didRefresh = false
691
+ let didReloadAuth = false
692
+ try {
693
+ resp = ctx.util.retryOnceOnAuth({
694
+ request: (token) => {
695
+ try {
696
+ return fetchUsage(ctx, token || accessToken, accountId)
697
+ } catch (e) {
698
+ ctx.host.log.error("usage request exception: " + String(e))
699
+ if (didRefresh) {
700
+ throw ERR_USAGE_AFTER_REFRESH
701
+ }
702
+ throw ERR_USAGE_CONNECTION
703
+ }
704
+ },
705
+ refresh: () => {
706
+ const reload = reloadAuthState(ctx, authState)
707
+ if (reload.status === "error") throw reload.error
708
+ if (reload.status === "changed") {
709
+ authState = reload.authState
710
+ auth = authState.auth
711
+ accessToken = auth.tokens.access_token
712
+ accountId = auth.tokens.account_id
713
+ proactiveRefreshAuthError = null
714
+ didReloadAuth = true
715
+ ctx.host.log.info("usage returned 401, retrying with reloaded auth")
716
+ return accessToken
717
+ }
718
+ if (proactiveRefreshAuthError) throw proactiveRefreshAuthError
719
+ ctx.host.log.info("usage returned 401, attempting refresh")
720
+ didRefresh = true
721
+ return refreshToken(ctx, authState)
722
+ },
723
+ })
724
+ } catch (e) {
725
+ if (typeof e === "string") throw e
726
+ ctx.host.log.error("usage request failed: " + String(e))
727
+ throw ERR_USAGE_CONNECTION
728
+ }
729
+
730
+ if (didReloadAuth && ctx.util.isAuthStatus(resp.status)) {
731
+ ctx.host.log.info("reloaded auth returned 401, attempting refresh")
732
+ didRefresh = true
733
+ const refreshed = refreshToken(ctx, authState)
734
+ if (refreshed) {
735
+ try {
736
+ resp = fetchUsage(ctx, refreshed, accountId)
737
+ } catch (e) {
738
+ ctx.host.log.error("usage request exception after reloaded auth refresh: " + String(e))
739
+ throw ERR_USAGE_AFTER_REFRESH
740
+ }
741
+ }
742
+ }
743
+
744
+ if (ctx.util.isAuthStatus(resp.status)) {
745
+ ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
746
+ throw ERR_TOKEN_EXPIRED
747
+ }
748
+
749
+ if (resp.status < 200 || resp.status >= 300) {
750
+ ctx.host.log.error("usage returned error: status=" + resp.status)
751
+ throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
752
+ }
753
+
754
+ ctx.host.log.info("usage fetch succeeded")
755
+
756
+ const data = ctx.util.tryParseJson(resp.bodyText)
757
+ if (data === null) {
758
+ throw "Usage response invalid. Try again later."
759
+ }
760
+
761
+ const lines = []
762
+ const nowSec = Math.floor(Date.now() / 1000)
763
+ const rateLimit = data.rate_limit || null
764
+ const primaryWindow = rateLimit && rateLimit.primary_window ? rateLimit.primary_window : null
765
+ const secondaryWindow = rateLimit && rateLimit.secondary_window ? rateLimit.secondary_window : null
766
+ const reviewWindow =
767
+ data.code_review_rate_limit && data.code_review_rate_limit.primary_window
768
+ ? data.code_review_rate_limit.primary_window
769
+ : null
770
+
771
+ const headerPrimary = readPercent(resp.headers["x-codex-primary-used-percent"])
772
+ const headerSecondary = readPercent(resp.headers["x-codex-secondary-used-percent"])
773
+
774
+ if (headerPrimary !== null) {
775
+ lines.push(ctx.line.progress({
776
+ label: "Session",
777
+ used: headerPrimary,
778
+ limit: 100,
779
+ format: { kind: "percent" },
780
+ resetsAt: getResetsAtIso(ctx, nowSec, primaryWindow),
781
+ periodDurationMs: PERIOD_SESSION_MS
782
+ }))
783
+ }
784
+ if (headerSecondary !== null) {
785
+ lines.push(ctx.line.progress({
786
+ label: "Weekly",
787
+ used: headerSecondary,
788
+ limit: 100,
789
+ format: { kind: "percent" },
790
+ resetsAt: getResetsAtIso(ctx, nowSec, secondaryWindow),
791
+ periodDurationMs: PERIOD_WEEKLY_MS
792
+ }))
793
+ }
794
+
795
+ if (lines.length === 0 && data.rate_limit) {
796
+ if (data.rate_limit.primary_window && typeof data.rate_limit.primary_window.used_percent === "number") {
797
+ lines.push(ctx.line.progress({
798
+ label: "Session",
799
+ used: data.rate_limit.primary_window.used_percent,
800
+ limit: 100,
801
+ format: { kind: "percent" },
802
+ resetsAt: getResetsAtIso(ctx, nowSec, primaryWindow),
803
+ periodDurationMs: PERIOD_SESSION_MS
804
+ }))
805
+ }
806
+ if (data.rate_limit.secondary_window && typeof data.rate_limit.secondary_window.used_percent === "number") {
807
+ lines.push(ctx.line.progress({
808
+ label: "Weekly",
809
+ used: data.rate_limit.secondary_window.used_percent,
810
+ limit: 100,
811
+ format: { kind: "percent" },
812
+ resetsAt: getResetsAtIso(ctx, nowSec, secondaryWindow),
813
+ periodDurationMs: PERIOD_WEEKLY_MS
814
+ }))
815
+ }
816
+ }
817
+
818
+ if (Array.isArray(data.additional_rate_limits)) {
819
+ for (const entry of data.additional_rate_limits) {
820
+ if (!entry || !entry.rate_limit) continue
821
+ const name = typeof entry.limit_name === "string" ? entry.limit_name : ""
822
+ let shortName = name.replace(/^GPT-[\d.]+-Codex-/, "")
823
+ if (!shortName) shortName = name || "Model"
824
+ const rl = entry.rate_limit
825
+ if (rl.primary_window && typeof rl.primary_window.used_percent === "number") {
826
+ lines.push(ctx.line.progress({
827
+ label: shortName,
828
+ used: rl.primary_window.used_percent,
829
+ limit: 100,
830
+ format: { kind: "percent" },
831
+ resetsAt: getResetsAtIso(ctx, nowSec, rl.primary_window),
832
+ periodDurationMs: typeof rl.primary_window.limit_window_seconds === "number"
833
+ ? rl.primary_window.limit_window_seconds * 1000
834
+ : PERIOD_SESSION_MS
835
+ }))
836
+ }
837
+ if (rl.secondary_window && typeof rl.secondary_window.used_percent === "number") {
838
+ lines.push(ctx.line.progress({
839
+ label: shortName + " Weekly",
840
+ used: rl.secondary_window.used_percent,
841
+ limit: 100,
842
+ format: { kind: "percent" },
843
+ resetsAt: getResetsAtIso(ctx, nowSec, rl.secondary_window),
844
+ periodDurationMs: typeof rl.secondary_window.limit_window_seconds === "number"
845
+ ? rl.secondary_window.limit_window_seconds * 1000
846
+ : PERIOD_WEEKLY_MS
847
+ }))
848
+ }
849
+ }
850
+ }
851
+
852
+ if (reviewWindow) {
853
+ const used = reviewWindow.used_percent
854
+ if (typeof used === "number") {
855
+ lines.push(ctx.line.progress({
856
+ label: "Reviews",
857
+ used: used,
858
+ limit: 100,
859
+ format: { kind: "percent" },
860
+ resetsAt: getResetsAtIso(ctx, nowSec, reviewWindow),
861
+ periodDurationMs: PERIOD_WEEKLY_MS // code_review_rate_limit is a 7-day window
862
+ }))
863
+ }
864
+ }
865
+
866
+ const resetCredits =
867
+ data.rate_limit_reset_credits &&
868
+ typeof data.rate_limit_reset_credits === "object" &&
869
+ data.rate_limit_reset_credits.available_count != null
870
+ ? readNumber(data.rate_limit_reset_credits.available_count)
871
+ : null
872
+ if (resetCredits !== null && resetCredits >= 0) {
873
+ lines.push(ctx.line.text({
874
+ label: "Rate Limit Resets",
875
+ value: Math.floor(resetCredits) + " available",
876
+ }))
877
+ }
878
+
879
+ const creditsRemaining = readCreditsRemaining(resp, data)
880
+ if (creditsRemaining !== null) {
881
+ const remaining = Math.max(0, Math.floor(creditsRemaining))
882
+ const usdValue = (remaining * CREDIT_USD_RATE).toFixed(2)
883
+ lines.push(ctx.line.text({
884
+ label: "Credits",
885
+ value: "$" + usdValue + " · " + remaining + " credits",
886
+ }))
887
+ }
888
+
889
+ let plan = null
890
+ if (data.plan_type) {
891
+ const planLabel = formatCodexPlan(ctx, data.plan_type)
892
+ if (planLabel) {
893
+ plan = planLabel
894
+ }
895
+ }
896
+
897
+ try {
898
+ const tokenUsageResult = queryTokenUsage(ctx)
899
+ if (tokenUsageResult.status === "ok") {
900
+ const tokenUsage = tokenUsageResult.data
901
+ const now = new Date()
902
+ const todayKey = dayKeyFromDate(now)
903
+ const yesterday = new Date(now.getTime())
904
+ yesterday.setDate(yesterday.getDate() - 1)
905
+ const yesterdayKey = dayKeyFromDate(yesterday)
906
+
907
+ let todayEntry = null
908
+ let yesterdayEntry = null
909
+ for (let i = 0; i < tokenUsage.daily.length; i++) {
910
+ const usageDayKey = dayKeyFromUsageDate(tokenUsage.daily[i].date)
911
+ if (usageDayKey === todayKey) {
912
+ todayEntry = tokenUsage.daily[i]
913
+ continue
914
+ }
915
+ if (usageDayKey === yesterdayKey) {
916
+ yesterdayEntry = tokenUsage.daily[i]
917
+ }
918
+ }
919
+
920
+ pushDayUsageLine(lines, ctx, "Today", todayEntry)
921
+ pushDayUsageLine(lines, ctx, "Yesterday", yesterdayEntry)
922
+
923
+ let totalTokens = 0
924
+ let totalCostNanos = 0
925
+ let hasCost = false
926
+ for (let i = 0; i < tokenUsage.daily.length; i++) {
927
+ const day = tokenUsage.daily[i]
928
+ const dayTokens = Number(day.totalTokens)
929
+ if (Number.isFinite(dayTokens)) {
930
+ totalTokens += dayTokens
931
+ }
932
+
933
+ const dayCost = usageCostUsd(day)
934
+ if (dayCost != null) {
935
+ totalCostNanos += Math.round(dayCost * 1e9)
936
+ hasCost = true
937
+ }
938
+ }
939
+
940
+ if (totalTokens > 0) {
941
+ lines.push(ctx.line.text({
942
+ label: "Last 30 Days",
943
+ value: costAndTokensLabel({ tokens: totalTokens, costUSD: hasCost ? totalCostNanos / 1e9 : null })
944
+ }))
945
+ }
946
+
947
+ pushUsageChartLine(lines, ctx, tokenUsage.daily)
948
+ persistUsageDaily(ctx, tokenUsage.daily, "Codex")
949
+ }
950
+ } catch (e) {
951
+ ctx.host.log.warn("local Codex token usage failed: " + String(e))
952
+ }
953
+
954
+ if (lines.length === 0) {
955
+ lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
956
+ }
957
+
958
+ return { plan: plan, lines: lines }
959
+ }
960
+
961
+ if (auth.OPENAI_API_KEY) {
962
+ throw ERR_USAGE_API_KEY
963
+ }
964
+
965
+ throw ERR_NOT_LOGGED_IN
966
+ }
967
+
968
+ function probe(ctx) {
969
+ if (ctx.host.codex && typeof ctx.host.codex.readAuth === "function") {
970
+ return probeWithAuthState(ctx, loadNativeAuth(ctx))
971
+ }
972
+ const fileAuth = loadFileAuthCandidates(ctx)
973
+ let lastAuthFallbackError = null
974
+ for (let i = 0; i < fileAuth.candidates.length; i++) {
975
+ const authState = fileAuth.candidates[i]
976
+ try {
977
+ return probeWithAuthState(ctx, authState)
978
+ } catch (e) {
979
+ if (!isAuthFallbackError(e)) {
980
+ throw e
981
+ }
982
+ lastAuthFallbackError = e
983
+ ctx.host.log.warn("auth failed for file " + authState.authPath + ", trying next auth source: " + String(e))
984
+ }
985
+ }
986
+
987
+ const keychainAuth = loadAuthFromKeychain(ctx)
988
+ if (keychainAuth) {
989
+ try {
990
+ return probeWithAuthState(ctx, keychainAuth)
991
+ } catch (e) {
992
+ if (!isAuthFallbackError(e)) throw e
993
+ lastAuthFallbackError = e
994
+ ctx.host.log.warn("keychain auth failed: " + String(e))
995
+ }
996
+ }
997
+
998
+ if (lastAuthFallbackError) throw lastAuthFallbackError
999
+
1000
+ for (const authPath of fileAuth.missingPaths) {
1001
+ ctx.host.log.warn("auth file not found: " + authPath)
1002
+ }
1003
+
1004
+ ctx.host.log.error("probe failed: not logged in")
1005
+ throw ERR_NOT_LOGGED_IN
1006
+ }
1007
+
1008
+ globalThis.__openusage_plugin = { id: "codex", probe }
1009
+ })()