@timo972/cc-router 0.11.0 → 0.12.0-rc.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.
@@ -110,9 +110,11 @@ export function renameAccountRecordById(oldId, newId) {
110
110
  return target;
111
111
  }
112
112
  function normalizeAccountProvider(record) {
113
- return record.provider === "openai_subscription"
114
- ? "openai_subscription"
115
- : "anthropic_subscription";
113
+ if (record.provider === "openai_subscription")
114
+ return "openai_subscription";
115
+ if (record.provider === "xai_subscription")
116
+ return "xai_subscription";
117
+ return "anthropic_subscription";
116
118
  }
117
119
  export function migrateLegacyAccountProviders(path = ACCOUNTS_PATH) {
118
120
  const records = readRawFromPath(path);
@@ -184,6 +186,20 @@ export function saveOpenAIAccountsToPath(accounts, path) {
184
186
  export function saveOpenAIAccounts(accounts) {
185
187
  saveOpenAIAccountsToPath(accounts, ACCOUNTS_PATH);
186
188
  }
189
+ export function loadXaiAccounts(path) {
190
+ const records = readRawFromPath(path ?? ACCOUNTS_PATH);
191
+ return records
192
+ .filter(a => a.provider === "xai_subscription")
193
+ .map(a => ({
194
+ id: a.id,
195
+ provider: "xai_subscription",
196
+ accessToken: a.accessToken,
197
+ refreshToken: a.refreshToken,
198
+ expiresAt: a.expiresAt,
199
+ enabled: a.enabled !== false,
200
+ ...(Array.isArray(a.scopes) ? { scopes: a.scopes } : {}),
201
+ }));
202
+ }
187
203
  function parseProxyConfig(raw) {
188
204
  const parsed = JSON.parse(raw);
189
205
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
@@ -5,6 +5,7 @@ export const CONFIG_DIR = path.join(os.homedir(), ".cc-router");
5
5
  export const ACCOUNTS_PATH = process.env["ACCOUNTS_PATH"] ??
6
6
  path.join(CONFIG_DIR, "accounts.json");
7
7
  export const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
8
+ export const CODEX_CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
8
9
  export const PROXY_PORT = parseInt(process.env["PORT"] ?? "3456", 10);
9
10
  export const LITELLM_PORT = 4000;
10
11
  // When set, the server forwards to LiteLLM instead of Anthropic directly
@@ -20,7 +20,7 @@ function cleanModel(model) {
20
20
  * costs the Anthropic path nothing. Everything else unprefixed still goes to
21
21
  * Anthropic, which is what existing setups rely on.
22
22
  */
23
- function isBareOpenAIModel(publicModel) {
23
+ export function isBareOpenAIModel(publicModel) {
24
24
  return publicModel.toLowerCase().startsWith("gpt-");
25
25
  }
26
26
  export function parseModelRef(model, config = {}) {
@@ -132,8 +132,11 @@ export function applyCodexRateLimits(account, update, nowMs) {
132
132
  }
133
133
  if (update.credits)
134
134
  limits.credits = update.credits;
135
- if (update.buckets.length > 0 || update.credits)
135
+ if (update.resetCredits)
136
+ limits.resetCredits = update.resetCredits;
137
+ if (update.buckets.length > 0 || update.credits || update.resetCredits) {
136
138
  limits.lastUpdated = nowMs;
139
+ }
137
140
  }
138
141
  function normalizeModelSlug(model) {
139
142
  const normalized = model?.trim().toLowerCase();
@@ -246,7 +246,20 @@ export function parseCodexUsagePayload(value, nowMs) {
246
246
  };
247
247
  }
248
248
  }
249
- return { buckets, ...(credits ? { credits } : {}) };
249
+ let resetCredits;
250
+ const rawResetCredits = payload["rate_limit_reset_credits"];
251
+ if (typeof rawResetCredits === "object" && rawResetCredits !== null) {
252
+ const record = rawResetCredits;
253
+ const count = usageNumber(record["available_count"]) ?? usageNumber(record["available"]);
254
+ if (count !== undefined) {
255
+ resetCredits = { available: Math.max(0, Math.min(99, Math.floor(count))) };
256
+ }
257
+ }
258
+ return {
259
+ buckets,
260
+ ...(credits ? { credits } : {}),
261
+ ...(resetCredits ? { resetCredits } : {}),
262
+ };
250
263
  }
251
264
  export function resolveActiveLimit(headers) {
252
265
  const raw = headerString(headers, "x-codex-active-limit")?.trim();
@@ -0,0 +1,42 @@
1
+ export const XAI_PROVIDER = "xai_subscription";
2
+ export const XAI_DEFAULT_SCOPES = [
3
+ "openid",
4
+ "profile",
5
+ "email",
6
+ "offline_access",
7
+ "grok-cli:access",
8
+ "api:access",
9
+ ];
10
+ function parseExpiresAt(value) {
11
+ const parsed = typeof value === "number" ? value : Number(value);
12
+ if (!Number.isFinite(parsed) || parsed <= 0) {
13
+ throw new Error("expiresAt must be a positive Unix timestamp in milliseconds");
14
+ }
15
+ return parsed;
16
+ }
17
+ function parseScopes(value) {
18
+ if (Array.isArray(value))
19
+ return value.filter(Boolean);
20
+ if (typeof value === "string")
21
+ return value.split(/\s+/).filter(Boolean);
22
+ return [...XAI_DEFAULT_SCOPES];
23
+ }
24
+ export function createXaiAccountRecord(input) {
25
+ const id = input.id.trim();
26
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
27
+ throw new Error("Only letters, numbers, _ and - allowed in account ID");
28
+ }
29
+ if (!input.accessToken.trim())
30
+ throw new Error("Access token is required");
31
+ if (!input.refreshToken.trim())
32
+ throw new Error("Refresh token is required");
33
+ return {
34
+ id,
35
+ provider: XAI_PROVIDER,
36
+ accessToken: input.accessToken.trim(),
37
+ refreshToken: input.refreshToken.trim(),
38
+ expiresAt: parseExpiresAt(input.expiresAt),
39
+ scopes: parseScopes(input.scopes),
40
+ enabled: input.enabled ?? true,
41
+ };
42
+ }
@@ -0,0 +1,116 @@
1
+ import { createXaiAccountRecord, XAI_DEFAULT_SCOPES } from "./account-record.js";
2
+ const DEFAULT_ISSUER = "https://auth.x.ai";
3
+ const DEFAULT_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
4
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
5
+ function issuerOf(opts) {
6
+ return (opts.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
7
+ }
8
+ function clientIdOf(opts) {
9
+ return opts.clientId ?? DEFAULT_CLIENT_ID;
10
+ }
11
+ function fetchOf(opts) {
12
+ return opts.fetchImpl ?? fetch;
13
+ }
14
+ async function readError(res) {
15
+ try {
16
+ return await res.text();
17
+ }
18
+ catch {
19
+ return "";
20
+ }
21
+ }
22
+ function parseAccessTokenExpiry(accessToken, expiresIn, now = Date.now()) {
23
+ const parts = accessToken.split(".");
24
+ if (parts[1]) {
25
+ try {
26
+ const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
27
+ if (typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0) {
28
+ return claims.exp * 1000;
29
+ }
30
+ }
31
+ catch { /* fall through */ }
32
+ }
33
+ if (typeof expiresIn === "number" && Number.isFinite(expiresIn) && expiresIn > 0) {
34
+ return now + expiresIn * 1000;
35
+ }
36
+ throw new Error("xAI access token has no usable expiry");
37
+ }
38
+ export async function requestXaiDeviceCode(opts = {}) {
39
+ const issuer = issuerOf(opts);
40
+ const form = new URLSearchParams({
41
+ client_id: clientIdOf(opts),
42
+ scope: XAI_DEFAULT_SCOPES.join(" "),
43
+ });
44
+ const res = await fetchOf(opts)(`${issuer}/oauth2/device/code`, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/x-www-form-urlencoded",
48
+ Accept: "application/json",
49
+ },
50
+ body: form.toString(),
51
+ });
52
+ if (!res.ok) {
53
+ throw new Error(`xAI device code request failed (${res.status}): ${await readError(res)}`);
54
+ }
55
+ const body = await res.json();
56
+ if (!body.device_code || !body.user_code || !body.verification_uri) {
57
+ throw new Error("xAI device code response is missing device_code, user_code, or verification_uri");
58
+ }
59
+ return {
60
+ verificationUrl: body.verification_uri_complete ?? body.verification_uri,
61
+ userCode: body.user_code,
62
+ deviceCode: body.device_code,
63
+ intervalSeconds: Number(body.interval ?? 5),
64
+ };
65
+ }
66
+ export async function exchangeXaiDeviceCodeForTokens(opts) {
67
+ const issuer = issuerOf(opts);
68
+ const sleep = opts.sleep ?? ((ms) => new Promise(resolve => setTimeout(resolve, ms)));
69
+ const now = opts.now ?? Date.now;
70
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
71
+ const started = now();
72
+ while (now() - started <= timeoutMs) {
73
+ const form = new URLSearchParams({
74
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
75
+ device_code: opts.deviceCode.deviceCode,
76
+ client_id: clientIdOf(opts),
77
+ });
78
+ const res = await fetchOf(opts)(`${issuer}/oauth2/token`, {
79
+ method: "POST",
80
+ headers: {
81
+ "Content-Type": "application/x-www-form-urlencoded",
82
+ Accept: "application/json",
83
+ },
84
+ body: form.toString(),
85
+ });
86
+ const body = await res.json().catch(() => ({}));
87
+ if (res.ok && body.access_token && body.refresh_token) {
88
+ return {
89
+ accessToken: body.access_token,
90
+ refreshToken: body.refresh_token,
91
+ expiresAt: parseAccessTokenExpiry(body.access_token, body.expires_in, now()),
92
+ };
93
+ }
94
+ const error = body.error ?? "";
95
+ if (error === "authorization_pending" || error === "slow_down" || res.status === 400 && !error) {
96
+ const wait = error === "slow_down"
97
+ ? Math.max(1, opts.deviceCode.intervalSeconds) + 5
98
+ : Math.max(1, opts.deviceCode.intervalSeconds);
99
+ await sleep(wait * 1000);
100
+ continue;
101
+ }
102
+ throw new Error(`xAI device authorization failed (${res.status}): ${body.error_description ?? (error || await readError(res))}`);
103
+ }
104
+ throw new Error("xAI device authorization timed out");
105
+ }
106
+ export async function loginXaiWithDeviceCode(opts) {
107
+ const deviceCode = await requestXaiDeviceCode(opts);
108
+ opts.onDeviceCode?.(deviceCode);
109
+ const tokens = await exchangeXaiDeviceCodeForTokens({ ...opts, deviceCode });
110
+ return createXaiAccountRecord({
111
+ id: opts.accountId,
112
+ accessToken: tokens.accessToken,
113
+ refreshToken: tokens.refreshToken,
114
+ expiresAt: tokens.expiresAt,
115
+ });
116
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import path from "path";
3
+ import { grokHomeDir } from "./overview.js";
4
+ import { createXaiAccountRecord } from "./account-record.js";
5
+ /**
6
+ * Copy the Grok CLI OIDC login (~/.grok/auth.json) into an accounts.json record.
7
+ * Does not write the file itself — the caller persists via upsert.
8
+ */
9
+ export function importGrokCliAuth(opts = {}) {
10
+ const grokHome = opts.grokHome ?? grokHomeDir();
11
+ const fileExists = opts.fileExists ?? existsSync;
12
+ const readFile = opts.readFile ?? ((filePath) => readFileSync(filePath, "utf-8"));
13
+ const authPath = path.join(grokHome, "auth.json");
14
+ if (!fileExists(authPath)) {
15
+ throw new Error(`No Grok CLI login at ${authPath}. Run: grok login`);
16
+ }
17
+ let raw;
18
+ try {
19
+ raw = JSON.parse(readFile(authPath));
20
+ }
21
+ catch {
22
+ throw new Error(`Could not parse ${authPath}`);
23
+ }
24
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
25
+ throw new Error(`${authPath} is not a Grok auth map`);
26
+ }
27
+ const entry = firstOidcEntry(raw);
28
+ if (!entry) {
29
+ throw new Error("No OIDC Grok login found in auth.json. Run: grok login");
30
+ }
31
+ const accessToken = typeof entry.key === "string" ? entry.key.trim() : "";
32
+ const refreshToken = typeof entry.refresh_token === "string" ? entry.refresh_token.trim() : "";
33
+ const expiresAt = parseExpiresAt(entry.expires_at, accessToken);
34
+ const id = opts.accountId?.trim() || "grok";
35
+ return createXaiAccountRecord({
36
+ id,
37
+ accessToken,
38
+ refreshToken,
39
+ expiresAt,
40
+ });
41
+ }
42
+ function firstOidcEntry(raw) {
43
+ for (const value of Object.values(raw)) {
44
+ if (value === null || typeof value !== "object" || Array.isArray(value))
45
+ continue;
46
+ const entry = value;
47
+ if (entry.auth_mode === "oidc" && typeof entry.key === "string" && typeof entry.refresh_token === "string") {
48
+ return entry;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ function parseExpiresAt(value, accessToken) {
54
+ if (typeof value === "string" && value.trim()) {
55
+ const parsed = Date.parse(value);
56
+ if (Number.isFinite(parsed) && parsed > 0)
57
+ return parsed;
58
+ }
59
+ const parts = accessToken.split(".");
60
+ if (parts[1]) {
61
+ try {
62
+ const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
63
+ if (typeof claims.exp === "number" && Number.isFinite(claims.exp) && claims.exp > 0) {
64
+ return claims.exp * 1000;
65
+ }
66
+ }
67
+ catch { /* fall through */ }
68
+ }
69
+ throw new Error("Grok login is missing a usable expiry");
70
+ }
@@ -0,0 +1,279 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { loadXaiAccounts } from "../../config/manager.js";
5
+ import { fetchGrokSubscription } from "./subscription-fetch.js";
6
+ function loadConfiguredXaiAccounts() {
7
+ // Read the override at call time. This keeps the overview aligned with the
8
+ // manager's documented ACCOUNTS_PATH override even when the environment is
9
+ // installed after this module has been imported (as in embedded callers and
10
+ // tests); without it the paths module's import-time snapshot can hide stored
11
+ // xAI accounts from the dashboard.
12
+ return loadXaiAccounts(process.env["ACCOUNTS_PATH"] || undefined);
13
+ }
14
+ export function grokHomeDir(homeDir = os.homedir()) {
15
+ const fromEnv = process.env["GROK_HOME"]?.trim();
16
+ if (fromEnv)
17
+ return fromEnv;
18
+ return path.join(homeDir, ".grok");
19
+ }
20
+ export function loadGrokAccountSnapshots(opts = {}) {
21
+ const grokHome = opts.grokHome ?? grokHomeDir();
22
+ const now = opts.now ?? Date.now;
23
+ const isProcessAlive = opts.isProcessAlive ?? defaultIsProcessAlive;
24
+ const readFile = opts.readFile ?? ((filePath) => readFileSync(filePath, "utf-8"));
25
+ const fileExists = opts.fileExists ?? existsSync;
26
+ const authPath = path.join(grokHome, "auth.json");
27
+ if (!fileExists(authPath))
28
+ return [];
29
+ let raw;
30
+ try {
31
+ raw = JSON.parse(readFile(authPath));
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
37
+ return [];
38
+ const liveSessions = readLiveSessions(path.join(grokHome, "active_sessions.json"), { readFile, fileExists, isProcessAlive });
39
+ const nowMs = now();
40
+ const usedIds = new Set();
41
+ const snapshots = [];
42
+ for (const entry of Object.values(raw)) {
43
+ const snapshot = snapshotFromAuthEntry(entry, liveSessions.length, nowMs, usedIds);
44
+ if (snapshot)
45
+ snapshots.push(snapshot);
46
+ }
47
+ return snapshots;
48
+ }
49
+ /** Stored xAI accounts if any, otherwise the live Grok CLI login. */
50
+ export function loadGrokHealthSnapshots(opts = {}) {
51
+ const overlay = loadGrokAccountSnapshots(opts);
52
+ const liveSessions = overlay.reduce((sum, account) => Math.max(sum, account.activeSessions), 0);
53
+ const stored = loadConfiguredXaiAccounts();
54
+ if (stored.length === 0)
55
+ return overlay;
56
+ const now = (opts.now ?? Date.now)();
57
+ return stored.map(account => {
58
+ const tier = grokTierFromAccessToken(account.accessToken);
59
+ return {
60
+ id: account.id,
61
+ provider: "xai_subscription",
62
+ enabled: true,
63
+ healthy: account.enabled !== false && account.expiresAt > now,
64
+ busy: liveSessions > 0,
65
+ inFlightRequests: 0,
66
+ activeSessions: liveSessions,
67
+ requestCount: 0,
68
+ errorCount: 0,
69
+ expiresInMs: account.expiresAt - now,
70
+ lastUsedMs: 0,
71
+ lastRefreshMs: 0,
72
+ ...(tier !== undefined ? { tier } : {}),
73
+ };
74
+ });
75
+ }
76
+ /**
77
+ * Sync snapshots enriched with the live plan name + code-access flag. Each
78
+ * stored xAI account gets one `/v1/user` lookup (matched by id); a failed or
79
+ * missing lookup leaves the snapshot on its access-token `tier` fallback, so an
80
+ * offline dashboard degrades to the coarse tier instead of dropping the row.
81
+ */
82
+ export async function loadGrokHealthSnapshotsWithSubscription(opts = {}) {
83
+ const base = loadGrokHealthSnapshots(opts);
84
+ if (base.length === 0)
85
+ return base;
86
+ const accounts = opts.accounts ?? loadConfiguredXaiAccounts();
87
+ const fetchSubscription = opts.fetchSubscription
88
+ ?? ((accessToken) => fetchGrokSubscription({ accessToken }));
89
+ return Promise.all(base.map(async (snapshot) => {
90
+ const account = accounts.find(candidate => candidate.id === snapshot.id);
91
+ if (!account)
92
+ return snapshot;
93
+ const result = await fetchSubscription(account.accessToken);
94
+ if (!result.ok)
95
+ return snapshot;
96
+ return {
97
+ ...snapshot,
98
+ ...(result.subscriptionTier !== undefined ? { subscriptionTier: result.subscriptionTier } : {}),
99
+ ...(result.hasCodeAccess !== undefined ? { hasCodeAccess: result.hasCodeAccess } : {}),
100
+ };
101
+ }));
102
+ }
103
+ export function grokSnapshotAsHealthAccount(snapshot) {
104
+ const xai = {
105
+ ...(snapshot.tier !== undefined ? { tier: snapshot.tier } : {}),
106
+ ...(snapshot.subscriptionTier !== undefined ? { subscriptionTier: snapshot.subscriptionTier } : {}),
107
+ ...(snapshot.hasCodeAccess !== undefined ? { hasCodeAccess: snapshot.hasCodeAccess } : {}),
108
+ };
109
+ return {
110
+ id: snapshot.id,
111
+ provider: "xai_subscription",
112
+ enabled: true,
113
+ healthy: snapshot.healthy,
114
+ busy: snapshot.busy,
115
+ inFlightRequests: 0,
116
+ activeSessions: snapshot.activeSessions,
117
+ requestCount: snapshot.requestCount,
118
+ errorCount: snapshot.errorCount,
119
+ expiresInMs: snapshot.expiresInMs,
120
+ lastUsedMs: snapshot.lastUsedMs,
121
+ lastRefreshMs: snapshot.lastRefreshMs,
122
+ ...(Object.keys(xai).length > 0 ? { xai } : {}),
123
+ };
124
+ }
125
+ export function mergeGrokIntoHealth(health, snapshots = loadGrokHealthSnapshots()) {
126
+ // The proxy daemon already emits the Grok row from the sync (network-free)
127
+ // path, so it carries only the coarse `tier`. Rather than skip enrichment,
128
+ // overlay the live plan name / code-access flag from the async snapshots the
129
+ // caller polled — otherwise the daemon-served row would stay stuck on "tier N".
130
+ if (health.accounts.some(account => account.provider === "xai_subscription")) {
131
+ return enrichExistingGrokAccounts(health, snapshots);
132
+ }
133
+ const grokAccounts = snapshots.map(grokSnapshotAsHealthAccount);
134
+ if (grokAccounts.length === 0)
135
+ return health;
136
+ const healthy = grokAccounts.filter(account => account.healthy).length;
137
+ const xai = {
138
+ configured: true,
139
+ accounts: grokAccounts.length,
140
+ healthy,
141
+ enabled: grokAccounts.length,
142
+ };
143
+ return {
144
+ ...health,
145
+ accounts: [...health.accounts, ...grokAccounts],
146
+ ...(health.operational
147
+ ? { operational: { ...health.operational, providers: { ...health.operational.providers, xai } } }
148
+ : {}),
149
+ };
150
+ }
151
+ /**
152
+ * Overlay the live plan name / code-access flag from freshly-polled snapshots
153
+ * onto the tier-only Grok rows the proxy daemon serves. Matches by account id,
154
+ * with a single-account fallback (the common one-Grok-login case). Returns the
155
+ * same object untouched when nothing changed, so the fast poll path stays cheap.
156
+ */
157
+ function enrichExistingGrokAccounts(health, snapshots) {
158
+ if (snapshots.length === 0)
159
+ return health;
160
+ const byId = new Map(snapshots.map(snapshot => [snapshot.id, snapshot]));
161
+ let changed = false;
162
+ const accounts = health.accounts.map(account => {
163
+ if (account.provider !== "xai_subscription")
164
+ return account;
165
+ const snapshot = (account.id !== undefined ? byId.get(account.id) : undefined)
166
+ ?? (snapshots.length === 1 ? snapshots[0] : undefined);
167
+ if (!snapshot)
168
+ return account;
169
+ const enrichment = {
170
+ ...(snapshot.tier !== undefined ? { tier: snapshot.tier } : {}),
171
+ ...(snapshot.subscriptionTier !== undefined ? { subscriptionTier: snapshot.subscriptionTier } : {}),
172
+ ...(snapshot.hasCodeAccess !== undefined ? { hasCodeAccess: snapshot.hasCodeAccess } : {}),
173
+ };
174
+ if (Object.keys(enrichment).length === 0)
175
+ return account;
176
+ changed = true;
177
+ return { ...account, xai: { ...account.xai, ...enrichment } };
178
+ });
179
+ return changed ? { ...health, accounts } : health;
180
+ }
181
+ function snapshotFromAuthEntry(value, liveSessionCount, nowMs, usedIds) {
182
+ if (value === null || typeof value !== "object" || Array.isArray(value))
183
+ return undefined;
184
+ const entry = value;
185
+ if (entry.auth_mode !== "oidc" && typeof entry.key !== "string")
186
+ return undefined;
187
+ const claims = typeof entry.key === "string" ? decodeJwtPayload(entry.key) : null;
188
+ const id = uniqueAccountId("grok", usedIds);
189
+ const expiresAt = parseExpiresAt(entry.expires_at, claims);
190
+ const expiresInMs = expiresAt > 0 ? expiresAt - nowMs : 0;
191
+ const healthy = expiresInMs > 0;
192
+ const tier = numberClaim(claims, "tier");
193
+ return {
194
+ id,
195
+ provider: "xai_subscription",
196
+ enabled: true,
197
+ healthy,
198
+ busy: liveSessionCount > 0,
199
+ inFlightRequests: 0,
200
+ activeSessions: liveSessionCount,
201
+ requestCount: 0,
202
+ errorCount: 0,
203
+ expiresInMs,
204
+ lastUsedMs: 0,
205
+ lastRefreshMs: 0,
206
+ ...(tier !== undefined ? { tier } : {}),
207
+ };
208
+ }
209
+ function readLiveSessions(sessionsPath, opts) {
210
+ if (!opts.fileExists(sessionsPath))
211
+ return [];
212
+ let raw;
213
+ try {
214
+ raw = JSON.parse(opts.readFile(sessionsPath));
215
+ }
216
+ catch {
217
+ return [];
218
+ }
219
+ if (!Array.isArray(raw))
220
+ return [];
221
+ return raw.filter((row) => {
222
+ if (row === null || typeof row !== "object")
223
+ return false;
224
+ const pid = row.pid;
225
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && opts.isProcessAlive(pid);
226
+ });
227
+ }
228
+ function uniqueAccountId(base, used) {
229
+ if (!used.has(base)) {
230
+ used.add(base);
231
+ return base;
232
+ }
233
+ let n = 2;
234
+ while (used.has(`${base}-${n}`))
235
+ n += 1;
236
+ const id = `${base}-${n}`;
237
+ used.add(id);
238
+ return id;
239
+ }
240
+ function parseExpiresAt(value, claims) {
241
+ if (typeof value === "string" && value.trim()) {
242
+ const parsed = Date.parse(value);
243
+ if (Number.isFinite(parsed) && parsed > 0)
244
+ return parsed;
245
+ }
246
+ const exp = numberClaim(claims, "exp");
247
+ return exp !== undefined && exp > 0 ? exp * 1000 : 0;
248
+ }
249
+ export function grokTierFromAccessToken(accessToken) {
250
+ return numberClaim(decodeJwtPayload(accessToken), "tier");
251
+ }
252
+ function decodeJwtPayload(token) {
253
+ const parts = token.split(".");
254
+ if (parts.length < 2 || !parts[1])
255
+ return null;
256
+ try {
257
+ const json = Buffer.from(parts[1], "base64url").toString("utf-8");
258
+ const parsed = JSON.parse(json);
259
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
260
+ return null;
261
+ return parsed;
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ }
267
+ function numberClaim(claims, key) {
268
+ const value = claims?.[key];
269
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
270
+ }
271
+ function defaultIsProcessAlive(pid) {
272
+ try {
273
+ process.kill(pid, 0);
274
+ return true;
275
+ }
276
+ catch {
277
+ return false;
278
+ }
279
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Live subscription lookup for the Grok dashboard row.
3
+ *
4
+ * The Grok CLI's own backend is `https://cli-chat-proxy.grok.com/v1`. A
5
+ * bearer-only GET on `/v1/user?include=subscription` answers with the account
6
+ * identity plus `subscriptionTier` ("GrokPro", …) and `hasGrokCodeAccess` — the
7
+ * only quota-relevant signals xAI exposes to the OIDC CLI token. The
8
+ * `?include=subscription` query is REQUIRED: the bare `/v1/user` omits
9
+ * `subscriptionTier` entirely (verified live 2026-08-21).
10
+ *
11
+ * Probed 2026-08-21 against the live endpoint: there is NO usage/limit/reset
12
+ * data here. `/v1/usage`, `/v1/rate-limits`, `/v1/quota` and friends all 404;
13
+ * no `x-ratelimit-*` response headers. xAI's weekly usage pool lives only in the
14
+ * web app's Settings → Usage surface behind a different auth context, so the
15
+ * dashboard shows the plan, not a percentage. This is why the Grok row has no
16
+ * Claude-style 5h/7d windows — they do not exist.
17
+ */
18
+ export const GROK_USER_ENDPOINT = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
19
+ export async function fetchGrokSubscription(account, options = {}) {
20
+ const fetchImpl = options.fetch ?? globalThis.fetch;
21
+ let response;
22
+ try {
23
+ response = await fetchImpl(GROK_USER_ENDPOINT, {
24
+ headers: { authorization: `Bearer ${account.accessToken}`, accept: "application/json" },
25
+ signal: AbortSignal.timeout(10_000),
26
+ });
27
+ }
28
+ catch {
29
+ return { ok: false, reason: "network" };
30
+ }
31
+ if (response.status === 401 || response.status === 403)
32
+ return { ok: false, reason: "auth" };
33
+ if (!response.ok)
34
+ return { ok: false, reason: "http" };
35
+ let body;
36
+ try {
37
+ body = await response.json();
38
+ }
39
+ catch {
40
+ return { ok: false, reason: "malformed" };
41
+ }
42
+ return parseGrokUserPayload(body);
43
+ }
44
+ export function parseGrokUserPayload(body) {
45
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
46
+ return { ok: false, reason: "malformed" };
47
+ }
48
+ const record = body;
49
+ const tier = record["subscriptionTier"];
50
+ const codeAccess = record["hasGrokCodeAccess"];
51
+ return {
52
+ ok: true,
53
+ ...(typeof tier === "string" && tier.trim() ? { subscriptionTier: tier.trim() } : {}),
54
+ ...(typeof codeAccess === "boolean" ? { hasCodeAccess: codeAccess } : {}),
55
+ };
56
+ }