@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.
- package/LICENSE +57744 -0
- package/native-manifest.json +1414 -0
- package/package.json +254 -0
- package/plugins/abacus-ai/icon.svg +18 -0
- package/plugins/abacus-ai/plugin.js +121 -0
- package/plugins/abacus-ai/plugin.json +17 -0
- package/plugins/alibaba/icon-color.svg +1 -0
- package/plugins/alibaba/icon.svg +1 -0
- package/plugins/alibaba/plugin.js +322 -0
- package/plugins/alibaba/plugin.json +20 -0
- package/plugins/alibaba-token-plan/icon-color.svg +1 -0
- package/plugins/alibaba-token-plan/icon.svg +1 -0
- package/plugins/alibaba-token-plan/plugin.js +544 -0
- package/plugins/alibaba-token-plan/plugin.json +19 -0
- package/plugins/amp/icon-color.svg +1 -0
- package/plugins/amp/icon-conflict.svg +6 -0
- package/plugins/amp/icon.svg +1 -0
- package/plugins/amp/plugin.js +175 -0
- package/plugins/amp/plugin.json +16 -0
- package/plugins/antigravity/icon-color.svg +1 -0
- package/plugins/antigravity/icon-conflict.svg +3 -0
- package/plugins/antigravity/icon.svg +1 -0
- package/plugins/antigravity/plugin.js +739 -0
- package/plugins/antigravity/plugin.json +31 -0
- package/plugins/antigravity-cli/icon.svg +3 -0
- package/plugins/antigravity-cli/plugin.js +570 -0
- package/plugins/antigravity-cli/plugin.json +16 -0
- package/plugins/antigravity-ide/icon.svg +3 -0
- package/plugins/antigravity-ide/plugin.js +257 -0
- package/plugins/antigravity-ide/plugin.json +29 -0
- package/plugins/augment/icon.svg +14 -0
- package/plugins/augment/plugin.js +59 -0
- package/plugins/augment/plugin.json +18 -0
- package/plugins/aws-bedrock/icon-color.svg +1 -0
- package/plugins/aws-bedrock/icon.svg +1 -0
- package/plugins/aws-bedrock/plugin.js +206 -0
- package/plugins/aws-bedrock/plugin.json +18 -0
- package/plugins/azure-openai/icon-color.svg +1 -0
- package/plugins/azure-openai/icon.svg +1 -0
- package/plugins/azure-openai/plugin.js +145 -0
- package/plugins/azure-openai/plugin.json +17 -0
- package/plugins/chutes/icon.svg +7 -0
- package/plugins/chutes/plugin.js +236 -0
- package/plugins/chutes/plugin.json +19 -0
- package/plugins/claude/icon-color.svg +1 -0
- package/plugins/claude/icon.svg +1 -0
- package/plugins/claude/plugin.js +1723 -0
- package/plugins/claude/plugin.json +30 -0
- package/plugins/codebuff/icon.svg +92 -0
- package/plugins/codebuff/plugin.js +113 -0
- package/plugins/codebuff/plugin.json +17 -0
- package/plugins/codex/icon-color.svg +5 -0
- package/plugins/codex/icon.svg +1 -0
- package/plugins/codex/plugin.js +1009 -0
- package/plugins/codex/plugin.json +82 -0
- package/plugins/command-code/icon.svg +3 -0
- package/plugins/command-code/plugin.js +184 -0
- package/plugins/command-code/plugin.json +20 -0
- package/plugins/copilot/icon-color.svg +1 -0
- package/plugins/copilot/icon.svg +1 -0
- package/plugins/copilot/plugin.js +684 -0
- package/plugins/copilot/plugin.json +21 -0
- package/plugins/crof/icon.svg +3 -0
- package/plugins/crof/plugin.js +53 -0
- package/plugins/crof/plugin.json +17 -0
- package/plugins/crofai/icon.svg +16 -0
- package/plugins/crofai/plugin.js +81 -0
- package/plugins/crofai/plugin.json +18 -0
- package/plugins/cursor/icon-conflict.svg +3 -0
- package/plugins/cursor/icon.svg +1 -0
- package/plugins/cursor/plugin.js +1379 -0
- package/plugins/cursor/plugin.json +23 -0
- package/plugins/cursor-nightly/icon.png +0 -0
- package/plugins/cursor-nightly/plugin.js +1379 -0
- package/plugins/cursor-nightly/plugin.json +23 -0
- package/plugins/deepgram/icon.svg +1 -0
- package/plugins/deepgram/plugin.js +20 -0
- package/plugins/deepgram/plugin.json +17 -0
- package/plugins/deepseek/icon-color.svg +1 -0
- package/plugins/deepseek/icon.svg +1 -0
- package/plugins/deepseek/plugin.js +71 -0
- package/plugins/deepseek/plugin.json +17 -0
- package/plugins/devin/icon.svg +5 -0
- package/plugins/devin/plugin.js +341 -0
- package/plugins/devin/plugin.json +16 -0
- package/plugins/doubao/icon-color.svg +1 -0
- package/plugins/doubao/icon.svg +1 -0
- package/plugins/doubao/plugin.js +92 -0
- package/plugins/doubao/plugin.json +17 -0
- package/plugins/droid/icon.svg +1 -0
- package/plugins/droid/plugin.json +20 -0
- package/plugins/elevenlabs/icon.svg +4 -0
- package/plugins/elevenlabs/plugin.js +48 -0
- package/plugins/elevenlabs/plugin.json +18 -0
- package/plugins/factory/icon-conflict.svg +1 -0
- package/plugins/factory/icon.svg +1 -0
- package/plugins/factory/plugin.js +487 -0
- package/plugins/factory/plugin.json +15 -0
- package/plugins/fireworks-ai/icon.svg +6 -0
- package/plugins/fireworks-ai/plugin.js +370 -0
- package/plugins/fireworks-ai/plugin.json +19 -0
- package/plugins/gemini/icon-color.svg +1 -0
- package/plugins/gemini/icon.svg +1 -0
- package/plugins/gemini/plugin.js +460 -0
- package/plugins/gemini/plugin.json +16 -0
- package/plugins/grok/icon.svg +1 -0
- package/plugins/grok/plugin.js +297 -0
- package/plugins/grok/plugin.json +19 -0
- package/plugins/groqcloud/icon.svg +1 -0
- package/plugins/groqcloud/plugin.js +117 -0
- package/plugins/groqcloud/plugin.json +19 -0
- package/plugins/jetbrains-ai-assistant/icon-color.svg +14 -0
- package/plugins/jetbrains-ai-assistant/icon-conflict.svg +3 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +14 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +313 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +16 -0
- package/plugins/kilo/icon.svg +1 -0
- package/plugins/kilo/plugin.js +111 -0
- package/plugins/kilo/plugin.json +19 -0
- package/plugins/kimi/icon-color.svg +1 -0
- package/plugins/kimi/icon-conflict.svg +3 -0
- package/plugins/kimi/icon.svg +1 -0
- package/plugins/kimi/plugin.js +358 -0
- package/plugins/kimi/plugin.json +15 -0
- package/plugins/kimi-k2/icon-color.svg +1 -0
- package/plugins/kimi-k2/icon.svg +1 -0
- package/plugins/kimi-k2/plugin.js +58 -0
- package/plugins/kimi-k2/plugin.json +18 -0
- package/plugins/kiro/icon-color.svg +5 -0
- package/plugins/kiro/icon-conflict.svg +1 -0
- package/plugins/kiro/icon.svg +4 -0
- package/plugins/kiro/plugin.js +390 -0
- package/plugins/kiro/plugin.json +16 -0
- package/plugins/litellm/icon.svg +16 -0
- package/plugins/litellm/plugin.js +199 -0
- package/plugins/litellm/plugin.json +16 -0
- package/plugins/llm-proxy/icon.svg +1 -0
- package/plugins/llm-proxy/plugin.js +40 -0
- package/plugins/llm-proxy/plugin.json +17 -0
- package/plugins/manus/icon.svg +6 -0
- package/plugins/manus/plugin.js +50 -0
- package/plugins/manus/plugin.json +17 -0
- package/plugins/mimo/icon.svg +1 -0
- package/plugins/mimo/plugin.js +177 -0
- package/plugins/mimo/plugin.json +19 -0
- package/plugins/minimax/icon-color.svg +1 -0
- package/plugins/minimax/icon-conflict.svg +4 -0
- package/plugins/minimax/icon.svg +1 -0
- package/plugins/minimax/plugin.js +453 -0
- package/plugins/minimax/plugin.json +14 -0
- package/plugins/mistral/icon-color.svg +1 -0
- package/plugins/mistral/icon.svg +1 -0
- package/plugins/mistral/plugin.js +152 -0
- package/plugins/mistral/plugin.json +18 -0
- package/plugins/moonshot/icon.svg +1 -0
- package/plugins/moonshot/plugin.js +43 -0
- package/plugins/moonshot/plugin.json +16 -0
- package/plugins/nanogpt/icon-color.svg +17 -0
- package/plugins/nanogpt/icon.svg +10 -0
- package/plugins/nanogpt/plugin.js +80 -0
- package/plugins/nanogpt/plugin.json +17 -0
- package/plugins/neuralwatt/icon.svg +3 -0
- package/plugins/neuralwatt/plugin.js +152 -0
- package/plugins/neuralwatt/plugin.json +17 -0
- package/plugins/ollama/icon.svg +1 -0
- package/plugins/ollama/plugin.js +92 -0
- package/plugins/ollama/plugin.json +20 -0
- package/plugins/openai-api/icon.svg +1 -0
- package/plugins/openai-api/plugin.js +533 -0
- package/plugins/openai-api/plugin.json +26 -0
- package/plugins/opencode/icon.svg +1 -0
- package/plugins/opencode/plugin.js +342 -0
- package/plugins/opencode/plugin.json +19 -0
- package/plugins/opencode-go/icon-conflict.svg +1 -0
- package/plugins/opencode-go/icon.svg +1 -0
- package/plugins/opencode-go/plugin.js +617 -0
- package/plugins/opencode-go/plugin.json +50 -0
- package/plugins/openrouter/icon.svg +1 -0
- package/plugins/openrouter/plugin.js +156 -0
- package/plugins/openrouter/plugin.json +8 -0
- package/plugins/perplexity/icon-color.svg +1 -0
- package/plugins/perplexity/icon-conflict.svg +1 -0
- package/plugins/perplexity/icon.svg +1 -0
- package/plugins/perplexity/plugin.js +425 -0
- package/plugins/perplexity/plugin.json +19 -0
- package/plugins/poe/icon.svg +1 -0
- package/plugins/poe/plugin.js +190 -0
- package/plugins/poe/plugin.json +19 -0
- package/plugins/stepfun/icon-color.svg +1 -0
- package/plugins/stepfun/icon.svg +1 -0
- package/plugins/stepfun/plugin.js +155 -0
- package/plugins/stepfun/plugin.json +18 -0
- package/plugins/synthetic/icon-conflict.svg +12 -0
- package/plugins/synthetic/icon.svg +14 -0
- package/plugins/synthetic/plugin.js +331 -0
- package/plugins/synthetic/plugin.json +23 -0
- package/plugins/t3chat/icon-color.svg +28 -0
- package/plugins/t3chat/icon.svg +4 -0
- package/plugins/t3chat/plugin.js +243 -0
- package/plugins/t3chat/plugin.json +20 -0
- package/plugins/venice/icon-color.svg +1 -0
- package/plugins/venice/icon.svg +1 -0
- package/plugins/venice/plugin.js +69 -0
- package/plugins/venice/plugin.json +16 -0
- package/plugins/vertex-ai/icon-color.svg +1 -0
- package/plugins/vertex-ai/icon.svg +1 -0
- package/plugins/vertex-ai/plugin.js +172 -0
- package/plugins/vertex-ai/plugin.json +18 -0
- package/plugins/warp/icon-color.svg +10 -0
- package/plugins/warp/icon.svg +6 -0
- package/plugins/warp/plugin.js +108 -0
- package/plugins/warp/plugin.json +18 -0
- package/plugins/windsurf/icon-conflict.svg +3 -0
- package/plugins/windsurf/icon.svg +1 -0
- package/plugins/windsurf/plugin.js +208 -0
- package/plugins/windsurf/plugin.json +16 -0
- package/plugins/zai/icon-conflict.svg +5 -0
- package/plugins/zai/icon.svg +1 -0
- package/plugins/zai/plugin.js +188 -0
- package/plugins/zai/plugin.json +16 -0
- package/plugins/zed/icon.svg +4 -0
- package/plugins/zed/plugin.js +270 -0
- package/plugins/zed/plugin.json +19 -0
- package/usagestat +0 -0
- 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
|
+
})()
|