tokentracker-cli 0.5.101 → 0.6.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/package.json +1 -1
- package/src/lib/codex-token-refresh.js +112 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +59 -16
- package/src/lib/subscriptions.js +41 -0
- package/src/lib/usage-limits.js +119 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, oh-my-pi, Craft Agents)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
// Public Codex OAuth client. Same id used by the official `codex` CLI. Aligned with
|
|
5
|
+
// steipete/CodexBar's CodexTokenRefresher.swift — neither is sensitive (it's a public client).
|
|
6
|
+
const REFRESH_ENDPOINT = "https://auth.openai.com/oauth/token";
|
|
7
|
+
const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
8
|
+
|
|
9
|
+
// CodexBar refreshes when last_refresh > 8 days. We mirror that — actual TTL is shorter so
|
|
10
|
+
// we always refresh before the access token can expire while users have the app running.
|
|
11
|
+
const REFRESH_THRESHOLD_MS = 8 * 24 * 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
function isTokenStale(lastRefreshIso, nowMs = Date.now()) {
|
|
14
|
+
if (!lastRefreshIso) return true;
|
|
15
|
+
const ts = Date.parse(lastRefreshIso);
|
|
16
|
+
if (!Number.isFinite(ts)) return true;
|
|
17
|
+
return nowMs - ts > REFRESH_THRESHOLD_MS;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function refreshCodexTokens({ refreshToken, fetchImpl = fetch }) {
|
|
21
|
+
if (typeof refreshToken !== "string" || refreshToken.length === 0) {
|
|
22
|
+
const err = new Error("Codex refresh skipped: no refresh_token in auth.json");
|
|
23
|
+
err.code = "NO_REFRESH_TOKEN";
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const res = await fetchImpl(REFRESH_ENDPOINT, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
Accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
client_id: CODEX_CLIENT_ID,
|
|
35
|
+
grant_type: "refresh_token",
|
|
36
|
+
refresh_token: refreshToken,
|
|
37
|
+
scope: "openid profile email",
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (res.status === 401) {
|
|
42
|
+
let openaiErrorCode = null;
|
|
43
|
+
try {
|
|
44
|
+
const body = await res.json();
|
|
45
|
+
openaiErrorCode =
|
|
46
|
+
(body && typeof body.error === "object" && body.error.code) ||
|
|
47
|
+
(typeof body?.error === "string" ? body.error : null) ||
|
|
48
|
+
body?.code ||
|
|
49
|
+
null;
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
// Ignore parse failure — surface the generic reason.
|
|
52
|
+
}
|
|
53
|
+
const err = new Error(
|
|
54
|
+
"Codex refresh token expired or revoked. Run `codex` to re-authenticate.",
|
|
55
|
+
);
|
|
56
|
+
err.code = "REFRESH_TOKEN_EXPIRED";
|
|
57
|
+
err.openaiErrorCode = openaiErrorCode;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const err = new Error(`Codex token refresh failed: ${res.status}`);
|
|
63
|
+
err.code = "REFRESH_HTTP_ERROR";
|
|
64
|
+
err.status = res.status;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
if (!body || typeof body.access_token !== "string" || body.access_token.length === 0) {
|
|
70
|
+
const err = new Error("Codex token refresh response missing access_token");
|
|
71
|
+
err.code = "REFRESH_INVALID_RESPONSE";
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
access_token: body.access_token,
|
|
77
|
+
refresh_token:
|
|
78
|
+
typeof body.refresh_token === "string" && body.refresh_token.length > 0
|
|
79
|
+
? body.refresh_token
|
|
80
|
+
: refreshToken,
|
|
81
|
+
id_token: typeof body.id_token === "string" ? body.id_token : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Atomic write so a process kill mid-write doesn't corrupt auth.json (which would force the
|
|
86
|
+
// user to re-run `codex` login).
|
|
87
|
+
async function persistRefreshedAuth(authPath, currentAuth, newTokens) {
|
|
88
|
+
const merged = {
|
|
89
|
+
...currentAuth,
|
|
90
|
+
tokens: {
|
|
91
|
+
...(currentAuth.tokens || {}),
|
|
92
|
+
access_token: newTokens.access_token,
|
|
93
|
+
refresh_token: newTokens.refresh_token,
|
|
94
|
+
id_token: newTokens.id_token || currentAuth?.tokens?.id_token || null,
|
|
95
|
+
},
|
|
96
|
+
last_refresh: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const tmp = `${authPath}.tmp.${process.pid}.${Date.now()}`;
|
|
100
|
+
await fs.promises.writeFile(tmp, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
101
|
+
await fs.promises.rename(tmp, authPath);
|
|
102
|
+
return merged;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
REFRESH_ENDPOINT,
|
|
107
|
+
CODEX_CLIENT_ID,
|
|
108
|
+
REFRESH_THRESHOLD_MS,
|
|
109
|
+
isTokenStale,
|
|
110
|
+
refreshCodexTokens,
|
|
111
|
+
persistRefreshedAuth,
|
|
112
|
+
};
|