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