@juanbenjumea/pi-provider-usage-fetcher 0.3.3 → 0.3.5

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/README.md CHANGED
@@ -16,12 +16,15 @@ Despite the name (historical), this package covers more than OpenCode Go:
16
16
  | **Kimi Coding** | `KIMI_API_KEY`, `auth.json`, explicit apiKey | `GET https://api.kimi.com/coding/v1/usages` |
17
17
  | **Synthetic** | `SYNTHETIC_API_KEY`, `auth.json`, explicit apiKey | `GET https://api.synthetic.new/v2/quotas` |
18
18
  | **z.ai / GLM** | Region-specific environment key, `auth.json`, explicit apiKey | `GET {api.z.ai or open.bigmodel.cn}/api/monitor/usage/quota/limit` |
19
+ | **Grok / SuperGrok** | `GROK_OAUTH_TOKEN`, `GROK_ACCESS_TOKEN`, `auth.json` (`grok`), or `~/.grok/auth.json` | Grok CLI billing proxy (`cli-chat-proxy.grok.com/v1/billing?format=credits`) |
19
20
  | **Cursor** | `quota-sessions.json`, env, CodexBar, Cursor app DB | `cursor.com/api/usage-summary` |
20
21
  | **CommandCode** | `quota-sessions.json`, env, CodexBar | `api.commandcode.ai/internal/billing/*` |
21
22
  | **Openference** | API key from caller, `OPENFERENCE_API_KEY`, or `auth.json` | `GET https://openference.com/api/user/me` |
22
23
 
23
24
  Legacy OpenCode Go dashboard cookie scraping remains as a fallback when no API key is configured.
24
25
 
26
+ Grok reports **subscription credits**, not xAI API spend. Its quota adapter follows the Grok CLI billing route observed in CodexBar; this is a private CLI endpoint and may change. It does not use xAI inference API keys or xAI Management API spend endpoints.
27
+
25
28
  ## Install
26
29
 
27
30
  ```bash
package/dist/index.d.ts CHANGED
@@ -21,4 +21,5 @@ export * from "./lib/gemini/gemini.ts";
21
21
  export * from "./lib/kimi/kimi.ts";
22
22
  export * from "./lib/synthetic/synthetic.ts";
23
23
  export * from "./lib/zai/zai.ts";
24
+ export * from "./lib/grok/grok.ts";
24
25
  export * from "./lib/provider/dispatch.ts";
package/dist/index.js CHANGED
@@ -21,4 +21,5 @@ export * from "./lib/gemini/gemini.js";
21
21
  export * from "./lib/kimi/kimi.js";
22
22
  export * from "./lib/synthetic/synthetic.js";
23
23
  export * from "./lib/zai/zai.js";
24
+ export * from "./lib/grok/grok.js";
24
25
  export * from "./lib/provider/dispatch.js";
@@ -0,0 +1,5 @@
1
+ import type { QuotaFetchOptions, QuotaSnapshot } from "../shared/quota-types.ts";
2
+ export declare const GROK_CREDITS_ENDPOINT = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
3
+ /** Parse the scope-keyed auth.json written by `grok login`, preferring SuperGrok OIDC. */
4
+ export declare function parseGrokAuthFile(contents: string): string | undefined;
5
+ export declare function fetchGrokUsage(options?: QuotaFetchOptions): Promise<QuotaSnapshot>;
@@ -0,0 +1,129 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { authCredential, resolveAuthValue } from "../auth/auth.js";
5
+ import { clampPercent, formatResetTime, safeError } from "../shared/format.js";
6
+ import { fetchJson } from "../shared/http.js";
7
+ export const GROK_CREDITS_ENDPOINT = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
8
+ const OIDC_SCOPE_PREFIX = "https://auth.x.ai::";
9
+ const LEGACY_SCOPE = "https://accounts.x.ai/sign-in";
10
+ /** Parse the scope-keyed auth.json written by `grok login`, preferring SuperGrok OIDC. */
11
+ export function parseGrokAuthFile(contents) {
12
+ try {
13
+ const parsed = JSON.parse(contents);
14
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
15
+ return undefined;
16
+ const entries = Object.entries(parsed).filter((entry) => !!entry[1] && typeof entry[1] === "object" && !Array.isArray(entry[1]));
17
+ const candidates = [
18
+ ...entries.filter(([scope]) => scope.startsWith(OIDC_SCOPE_PREFIX)),
19
+ ...entries.filter(([scope]) => scope === LEGACY_SCOPE || scope.includes("/sign-in")),
20
+ ];
21
+ for (const [, entry] of candidates) {
22
+ const token = typeof entry.key === "string" ? normalizeBearer(entry.key) : undefined;
23
+ if (token)
24
+ return token;
25
+ }
26
+ }
27
+ catch {
28
+ // Treat absent, malformed, or partial CLI credentials as unavailable.
29
+ }
30
+ return undefined;
31
+ }
32
+ function normalizeBearer(value) {
33
+ let token = value?.trim() ?? "";
34
+ if (token.toLowerCase().startsWith("bearer "))
35
+ token = token.slice(7).trim();
36
+ // xAI inference keys and cookies are not SuperGrok OAuth credentials.
37
+ if (!token ||
38
+ token.toLowerCase().startsWith("xai-") ||
39
+ token.toLowerCase().startsWith("cookie:") ||
40
+ token.includes("=")) {
41
+ return undefined;
42
+ }
43
+ return token;
44
+ }
45
+ function grokCliToken() {
46
+ const configuredHome = process.env.GROK_HOME?.trim();
47
+ const home = configuredHome
48
+ ? resolve(configuredHome.replace(/^~(?=\/|$)/, homedir()))
49
+ : join(homedir(), ".grok");
50
+ const path = join(home, "auth.json");
51
+ try {
52
+ if (existsSync(path))
53
+ return parseGrokAuthFile(readFileSync(path, "utf8"));
54
+ }
55
+ catch {
56
+ // Treat inaccessible credentials as unavailable; never expose file errors or contents.
57
+ }
58
+ return undefined;
59
+ }
60
+ function grokToken(apiKey) {
61
+ return (normalizeBearer(apiKey) ||
62
+ normalizeBearer(resolveAuthValue(process.env.GROK_OAUTH_TOKEN)) ||
63
+ normalizeBearer(resolveAuthValue(process.env.GROK_ACCESS_TOKEN)) ||
64
+ normalizeBearer(authCredential("grok", "supergrok")) ||
65
+ grokCliToken());
66
+ }
67
+ function finiteNumber(value) {
68
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
69
+ }
70
+ function parseDate(value) {
71
+ if (typeof value !== "string")
72
+ return undefined;
73
+ const date = new Date(value);
74
+ return Number.isFinite(date.getTime()) ? date : undefined;
75
+ }
76
+ function windowLabel(start, end) {
77
+ if (!start || !end || end <= start)
78
+ return "Credits";
79
+ const days = (end.getTime() - start.getTime()) / 86_400_000;
80
+ if (days >= 4 && days <= 12)
81
+ return "Week";
82
+ if (days >= 20 && days <= 45)
83
+ return "Month";
84
+ return "Credits";
85
+ }
86
+ export async function fetchGrokUsage(options = {}) {
87
+ const token = grokToken(options.apiKey);
88
+ if (!token)
89
+ return { provider: "Grok", windows: [], error: "no-auth", fetchedAt: Date.now() };
90
+ try {
91
+ const { data } = await fetchJson(GROK_CREDITS_ENDPOINT, {
92
+ headers: {
93
+ Authorization: `Bearer ${token}`,
94
+ "x-xai-token-auth": "xai-grok-cli",
95
+ Accept: "application/json",
96
+ },
97
+ }, options.timeoutMs ?? 10_000, options.fetchImpl ?? fetch);
98
+ const config = data?.config;
99
+ if (!config || typeof config !== "object")
100
+ throw new Error("invalid-response");
101
+ const creditPercent = finiteNumber(config.creditUsagePercent);
102
+ const cap = finiteNumber(config.onDemandCap?.val);
103
+ const used = finiteNumber(config.onDemandUsed?.val);
104
+ const derivedPercent = cap !== undefined && cap > 0 && used !== undefined ? (used / cap) * 100 : undefined;
105
+ const usedPercent = creditPercent ?? derivedPercent;
106
+ if (usedPercent === undefined)
107
+ throw new Error("invalid-response");
108
+ const period = config.currentPeriod;
109
+ const currentPeriodEnd = parseDate(period?.end);
110
+ const end = currentPeriodEnd ?? parseDate(config.billingPeriodEnd);
111
+ // Do not combine a start from the current-period object with a fallback end
112
+ // from another period; duration is used only to label the quota window.
113
+ const start = currentPeriodEnd
114
+ ? parseDate(period?.start)
115
+ : parseDate(config.billingPeriodStart);
116
+ const resetDate = end && end.getTime() > Date.now() ? end : undefined;
117
+ const window = {
118
+ label: windowLabel(start, end),
119
+ usedPercent: clampPercent(usedPercent),
120
+ ...(resetDate
121
+ ? { resetsAt: resetDate.toISOString(), resetsIn: formatResetTime(resetDate) }
122
+ : {}),
123
+ };
124
+ return { provider: "Grok", windows: [window], fetchedAt: Date.now() };
125
+ }
126
+ catch (error) {
127
+ return { provider: "Grok", windows: [], error: safeError(error), fetchedAt: Date.now() };
128
+ }
129
+ }
@@ -7,6 +7,7 @@ import { fetchGeminiUsage } from "../gemini/gemini.js";
7
7
  import { fetchKimiUsage } from "../kimi/kimi.js";
8
8
  import { fetchSyntheticUsage } from "../synthetic/synthetic.js";
9
9
  import { fetchZaiUsage } from "../zai/zai.js";
10
+ import { fetchGrokUsage } from "../grok/grok.js";
10
11
  import { fetchCursorUsage } from "../cursor/cursor.js";
11
12
  import { fetchCommandCodeUsage } from "../commandcode/commandcode.js";
12
13
  import { fetchOpenferenceUsage } from "../openference/openference.js";
@@ -34,6 +35,9 @@ export const PROVIDER_MAP = {
34
35
  zai: "zai",
35
36
  "z.ai": "zai",
36
37
  "glm-coding-plan": "zai",
38
+ grok: "grok",
39
+ supergrok: "grok",
40
+ "grok-cli": "grok",
37
41
  cursor: "cursor",
38
42
  commandcode: "commandcode",
39
43
  openference: "openference",
@@ -55,6 +59,7 @@ export const PROVIDER_FETCHERS = {
55
59
  kimi: (options) => fetchKimiUsage(options),
56
60
  synthetic: fetchSyntheticUsage,
57
61
  zai: fetchZaiUsage,
62
+ grok: fetchGrokUsage,
58
63
  cursor: () => fetchCursorUsage(),
59
64
  commandcode: () => fetchCommandCodeUsage(),
60
65
  openference: (options) => fetchOpenferenceUsage(options?.apiKey, options),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@juanbenjumea/pi-provider-usage-fetcher",
3
- "version": "0.3.3",
4
- "description": "Multi-provider usage fetchers for coding assistant subscriptions, including OpenCode Go, Claude, Codex, Cursor, Copilot, Gemini, Kimi, and more.",
3
+ "version": "0.3.5",
4
+ "description": "Multi-provider usage fetchers for coding assistant subscriptions, including OpenCode Go, Claude, Codex, Grok, Cursor, Copilot, Gemini, Kimi, and more.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Juan Benjumea",
@@ -55,7 +55,8 @@
55
55
  "./gemini.ts": "./dist/lib/gemini/gemini.js",
56
56
  "./kimi.ts": "./dist/lib/kimi/kimi.js",
57
57
  "./synthetic.ts": "./dist/lib/synthetic/synthetic.js",
58
- "./zai.ts": "./dist/lib/zai/zai.js"
58
+ "./zai.ts": "./dist/lib/zai/zai.js",
59
+ "./grok.ts": "./dist/lib/grok/grok.js"
59
60
  },
60
61
  "files": [
61
62
  "dist/",