@monotykamary/pi-better-grok 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/LICENSE +21 -0
- package/README.md +65 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/index.ts +666 -0
- package/package.json +66 -0
- package/src/config.ts +314 -0
- package/src/fast-controller.ts +82 -0
- package/src/format.ts +66 -0
- package/src/grok-auth.ts +242 -0
- package/src/identity.ts +7 -0
- package/src/paths.ts +17 -0
- package/src/usage-controller.ts +210 -0
- package/src/usage.ts +351 -0
package/src/usage.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { GrokCredential } from "./grok-auth.ts";
|
|
2
|
+
|
|
3
|
+
// Revision-pinned Grok subscription surface. Same unofficial contract used by
|
|
4
|
+
// pi-grok-usage and pi-xai /xai-usage: authenticated GET /v1/user, then
|
|
5
|
+
// GET /v1/billing?format=credits carrying the x-userid identity header.
|
|
6
|
+
export const XAI_CLI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user";
|
|
7
|
+
export const XAI_CLI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
8
|
+
|
|
9
|
+
export const GROK_CLIENT_IDENTIFIER = process.env.PI_XAI_CLIENT_NAME || "grok-shell";
|
|
10
|
+
export const GROK_CLIENT_VERSION = process.env.PI_XAI_CLIENT_VERSION || "0.2.101";
|
|
11
|
+
|
|
12
|
+
const USAGE_TIMEOUT_MS = 15_000;
|
|
13
|
+
const MAX_RESPONSE_BYTES = 256 * 1024;
|
|
14
|
+
const MAX_CENTS = 1_000_000_000_000;
|
|
15
|
+
const MAX_USER_ID_LENGTH = 256;
|
|
16
|
+
const MAX_LABEL_LENGTH = 80;
|
|
17
|
+
|
|
18
|
+
export type ProductUsage = { product: string; usagePercent: number };
|
|
19
|
+
|
|
20
|
+
export type UsageSnapshot = {
|
|
21
|
+
capturedAt: number;
|
|
22
|
+
creditUsagePercent: number | null;
|
|
23
|
+
periodType: string | null;
|
|
24
|
+
periodStart: string | null;
|
|
25
|
+
periodEnd: string | null;
|
|
26
|
+
subscriptionTier: string | null;
|
|
27
|
+
isUnifiedBillingUser: boolean | null;
|
|
28
|
+
productUsage: ProductUsage[];
|
|
29
|
+
usedCents: number | null;
|
|
30
|
+
monthlyLimitCents: number | null;
|
|
31
|
+
prepaidBalanceCents: number | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type UsageErrorCode = "auth" | "http" | "invalid" | "oversize" | "transport";
|
|
35
|
+
|
|
36
|
+
export class UsageError extends Error {
|
|
37
|
+
readonly code: UsageErrorCode;
|
|
38
|
+
readonly status?: number;
|
|
39
|
+
|
|
40
|
+
constructor(code: UsageErrorCode, message: string, status?: number) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "UsageError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.status = status;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function objectValue(value: unknown): Record<string, unknown> | undefined {
|
|
49
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
50
|
+
? (value as Record<string, unknown>)
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function clampPercent(value: number): number {
|
|
55
|
+
return Math.min(100, Math.max(0, value));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function boundedPercent(value: unknown): number | undefined {
|
|
59
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100
|
|
60
|
+
? value
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function boundedCents(value: unknown): number | undefined {
|
|
65
|
+
const wrapper = objectValue(value);
|
|
66
|
+
if (!wrapper) return undefined;
|
|
67
|
+
const cents = wrapper.val;
|
|
68
|
+
return typeof cents === "number" &&
|
|
69
|
+
Number.isSafeInteger(cents) &&
|
|
70
|
+
cents >= 0 &&
|
|
71
|
+
cents <= MAX_CENTS
|
|
72
|
+
? cents
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasControlCharacters(value: string): boolean {
|
|
77
|
+
for (const char of value) {
|
|
78
|
+
const code = char.charCodeAt(0);
|
|
79
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function boundedLabel(value: unknown): string | undefined {
|
|
85
|
+
if (typeof value !== "string") return undefined;
|
|
86
|
+
const label = value.trim();
|
|
87
|
+
return label && label.length <= MAX_LABEL_LENGTH && !hasControlCharacters(label)
|
|
88
|
+
? label
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function boundedTimestamp(value: unknown): string | undefined {
|
|
93
|
+
if (typeof value !== "string") return undefined;
|
|
94
|
+
const timestamp = value.trim();
|
|
95
|
+
if (!timestamp || timestamp.length > 64) return undefined;
|
|
96
|
+
return Number.isFinite(Date.parse(timestamp)) ? timestamp : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isPrintableAscii(value: string): boolean {
|
|
100
|
+
for (const char of value) {
|
|
101
|
+
const code = char.charCodeAt(0);
|
|
102
|
+
if (code < 0x21 || code > 0x7e) return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseUserId(value: unknown): string {
|
|
108
|
+
const userId = objectValue(value)?.userId;
|
|
109
|
+
if (
|
|
110
|
+
typeof userId !== "string" ||
|
|
111
|
+
!userId ||
|
|
112
|
+
userId.length > MAX_USER_ID_LENGTH ||
|
|
113
|
+
!isPrintableAscii(userId)
|
|
114
|
+
) {
|
|
115
|
+
throw new UsageError(
|
|
116
|
+
"invalid",
|
|
117
|
+
"xAI account identity could not be verified; billing was not requested.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return userId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function parseUsageSnapshot(data: unknown, now = Date.now()): UsageSnapshot {
|
|
124
|
+
const root = objectValue(data);
|
|
125
|
+
if (!root) throw new UsageError("invalid", "xAI usage returned an invalid response.");
|
|
126
|
+
const config = objectValue(root.config);
|
|
127
|
+
const snapshot: UsageSnapshot = {
|
|
128
|
+
capturedAt: now,
|
|
129
|
+
creditUsagePercent: null,
|
|
130
|
+
periodType: null,
|
|
131
|
+
periodStart: null,
|
|
132
|
+
periodEnd: null,
|
|
133
|
+
subscriptionTier: null,
|
|
134
|
+
isUnifiedBillingUser: null,
|
|
135
|
+
productUsage: [],
|
|
136
|
+
usedCents: null,
|
|
137
|
+
monthlyLimitCents: null,
|
|
138
|
+
prepaidBalanceCents: null,
|
|
139
|
+
};
|
|
140
|
+
if (!config) return snapshot;
|
|
141
|
+
|
|
142
|
+
const percent = boundedPercent(config.creditUsagePercent);
|
|
143
|
+
if (percent !== undefined) snapshot.creditUsagePercent = percent;
|
|
144
|
+
|
|
145
|
+
const period = objectValue(config.currentPeriod);
|
|
146
|
+
if (period) {
|
|
147
|
+
snapshot.periodType = boundedLabel(period.type) ?? null;
|
|
148
|
+
snapshot.periodStart = boundedTimestamp(period.start) ?? null;
|
|
149
|
+
snapshot.periodEnd = boundedTimestamp(period.end) ?? null;
|
|
150
|
+
}
|
|
151
|
+
if (!snapshot.periodStart) {
|
|
152
|
+
snapshot.periodStart = boundedTimestamp(config.billingPeriodStart) ?? null;
|
|
153
|
+
}
|
|
154
|
+
if (!snapshot.periodEnd) {
|
|
155
|
+
snapshot.periodEnd = boundedTimestamp(config.billingPeriodEnd) ?? null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
snapshot.subscriptionTier = boundedLabel(root.subscriptionTier) ?? null;
|
|
159
|
+
if (typeof config.isUnifiedBillingUser === "boolean") {
|
|
160
|
+
snapshot.isUnifiedBillingUser = config.isUnifiedBillingUser;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(config.productUsage)) {
|
|
164
|
+
for (const entry of config.productUsage.slice(0, 16)) {
|
|
165
|
+
const record = objectValue(entry);
|
|
166
|
+
if (!record) continue;
|
|
167
|
+
const product = boundedLabel(record.product);
|
|
168
|
+
const usagePercent = boundedPercent(record.usagePercent);
|
|
169
|
+
if (product && usagePercent !== undefined) {
|
|
170
|
+
snapshot.productUsage.push({ product, usagePercent });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const used = boundedCents(config.used);
|
|
176
|
+
if (used !== undefined) snapshot.usedCents = used;
|
|
177
|
+
const monthlyLimit = boundedCents(config.monthlyLimit);
|
|
178
|
+
if (monthlyLimit !== undefined) snapshot.monthlyLimitCents = monthlyLimit;
|
|
179
|
+
const prepaid = boundedCents(config.prepaidBalance);
|
|
180
|
+
if (prepaid !== undefined) snapshot.prepaidBalanceCents = prepaid;
|
|
181
|
+
|
|
182
|
+
return snapshot;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatPercent(value: number | null): string {
|
|
186
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
187
|
+
? `${Math.round(clampPercent(value))}%`
|
|
188
|
+
: "--";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function formatResetCountdown(seconds: number | null): string | null {
|
|
192
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return null;
|
|
193
|
+
const total = Math.max(0, Math.round(seconds));
|
|
194
|
+
const days = Math.floor(total / 86_400);
|
|
195
|
+
const hours = Math.floor((total % 86_400) / 3_600);
|
|
196
|
+
const minutes = Math.floor((total % 3_600) / 60);
|
|
197
|
+
const secs = total % 60;
|
|
198
|
+
if (days > 0) return `${days}d${hours}h`;
|
|
199
|
+
if (hours > 0) return `${hours}h${minutes}m`;
|
|
200
|
+
if (minutes > 0) return `${minutes}m`;
|
|
201
|
+
return `${secs}s`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function formatResetClock(seconds: number | null, now = Date.now()): string | null {
|
|
205
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds)) return null;
|
|
206
|
+
const reset = new Date(now + seconds * 1000);
|
|
207
|
+
const time = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(
|
|
208
|
+
reset,
|
|
209
|
+
);
|
|
210
|
+
if (reset.toDateString() === new Date(now).toDateString()) return time;
|
|
211
|
+
const weekday = new Intl.DateTimeFormat(undefined, { weekday: "short" }).format(reset);
|
|
212
|
+
return `${weekday} ${time}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function periodSecondsLeft(snapshot: UsageSnapshot, now = Date.now()): number | null {
|
|
216
|
+
if (!snapshot.periodEnd) return null;
|
|
217
|
+
const end = Date.parse(snapshot.periodEnd);
|
|
218
|
+
if (!Number.isFinite(end)) return null;
|
|
219
|
+
return Math.max(0, (end - now) / 1000);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function periodTypeLabel(periodType: string | null): string {
|
|
223
|
+
if (!periodType) return "period";
|
|
224
|
+
const prefix = "USAGE_PERIOD_TYPE_";
|
|
225
|
+
const short = periodType.startsWith(prefix) ? periodType.slice(prefix.length) : periodType;
|
|
226
|
+
return short.toLowerCase();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function formatCents(cents: number | null): string {
|
|
230
|
+
return typeof cents === "number" ? `$${(cents / 100).toFixed(2)}` : "--";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function formatUsageSnapshot(
|
|
234
|
+
snapshot: UsageSnapshot,
|
|
235
|
+
options: { showResetTimes: boolean },
|
|
236
|
+
now = Date.now(),
|
|
237
|
+
): string {
|
|
238
|
+
const used = snapshot.creditUsagePercent;
|
|
239
|
+
const left = used === null ? null : clampPercent(100 - used);
|
|
240
|
+
const parts = [`Usage: ${formatPercent(left)} left`];
|
|
241
|
+
if (options.showResetTimes) {
|
|
242
|
+
const seconds = periodSecondsLeft(snapshot, now);
|
|
243
|
+
const countdown = formatResetCountdown(seconds);
|
|
244
|
+
const clock = formatResetClock(seconds, now);
|
|
245
|
+
if (countdown && clock) parts.push(`↺ ${countdown} - ${clock}`);
|
|
246
|
+
}
|
|
247
|
+
return parts.join(" · ");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function formatUsageDetail(snapshot: UsageSnapshot): string {
|
|
251
|
+
const lines: string[] = [];
|
|
252
|
+
const used = snapshot.creditUsagePercent;
|
|
253
|
+
const left = used === null ? null : clampPercent(100 - used);
|
|
254
|
+
lines.push(`Included usage: ${formatPercent(used)} used · ${formatPercent(left)} left`);
|
|
255
|
+
if (snapshot.periodStart && snapshot.periodEnd) {
|
|
256
|
+
lines.push(
|
|
257
|
+
`${periodTypeLabel(snapshot.periodType)} period: ${snapshot.periodStart} → ${snapshot.periodEnd}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
for (const entry of snapshot.productUsage) {
|
|
261
|
+
lines.push(`${entry.product}: ${formatPercent(entry.usagePercent)} used`);
|
|
262
|
+
}
|
|
263
|
+
if (snapshot.subscriptionTier) lines.push(`Tier: ${snapshot.subscriptionTier}`);
|
|
264
|
+
if (snapshot.usedCents !== null || snapshot.monthlyLimitCents !== null) {
|
|
265
|
+
lines.push(
|
|
266
|
+
`Monthly: ${formatCents(snapshot.usedCents)} of ${formatCents(snapshot.monthlyLimitCents)}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (snapshot.prepaidBalanceCents !== null && snapshot.prepaidBalanceCents > 0) {
|
|
270
|
+
lines.push(`Prepaid balance: ${formatCents(snapshot.prepaidBalanceCents)}`);
|
|
271
|
+
}
|
|
272
|
+
lines.push(`Captured: ${new Date(snapshot.capturedAt).toLocaleTimeString()}`);
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function usageHeaders(accessToken: string, userId?: string): Record<string, string> {
|
|
277
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
278
|
+
return {
|
|
279
|
+
Authorization: `Bearer ${accessToken}`,
|
|
280
|
+
"X-XAI-Token-Auth": "xai-grok-cli",
|
|
281
|
+
"x-grok-client-identifier": GROK_CLIENT_IDENTIFIER,
|
|
282
|
+
"x-grok-client-version": GROK_CLIENT_VERSION,
|
|
283
|
+
"x-grok-client-mode": interactive ? "interactive" : "headless",
|
|
284
|
+
...(userId ? { "x-userid": userId } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function httpError(status: number): UsageError {
|
|
289
|
+
if (status === 401 || status === 403) {
|
|
290
|
+
return new UsageError(
|
|
291
|
+
"auth",
|
|
292
|
+
"xAI authentication was rejected. Run /login xai and try again.",
|
|
293
|
+
status,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (status === 429) {
|
|
297
|
+
return new UsageError("http", "xAI usage is rate limited. Try again later.", status);
|
|
298
|
+
}
|
|
299
|
+
return new UsageError("http", `xAI usage request failed with status ${status}.`, status);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function requestSignal(signal?: AbortSignal): AbortSignal {
|
|
303
|
+
const timeout = AbortSignal.timeout(USAGE_TIMEOUT_MS);
|
|
304
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function fetchJson(
|
|
308
|
+
url: string,
|
|
309
|
+
headers: Record<string, string>,
|
|
310
|
+
signal?: AbortSignal,
|
|
311
|
+
): Promise<unknown> {
|
|
312
|
+
let response: Response;
|
|
313
|
+
try {
|
|
314
|
+
response = await fetch(url, { headers, signal: requestSignal(signal) });
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
+
throw new UsageError("transport", `xAI usage request failed: ${message}`);
|
|
318
|
+
}
|
|
319
|
+
if (!response.ok) throw httpError(response.status);
|
|
320
|
+
const declaredLength = Number(response.headers.get("content-length") ?? "0");
|
|
321
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
|
|
322
|
+
throw new UsageError("oversize", "xAI usage returned an oversized response.");
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
return (await response.json()) as unknown;
|
|
326
|
+
} catch (error) {
|
|
327
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
328
|
+
throw new UsageError("invalid", `xAI usage returned an invalid response body: ${message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export async function requestGrokUsage(
|
|
333
|
+
credential: GrokCredential,
|
|
334
|
+
options: { signal?: AbortSignal } = {},
|
|
335
|
+
): Promise<{ userId: string; snapshot: UsageSnapshot }> {
|
|
336
|
+
if (!credential.token) {
|
|
337
|
+
throw new UsageError("auth", "xAI OAuth credentials are required. Run /login xai first.");
|
|
338
|
+
}
|
|
339
|
+
const userPayload = await fetchJson(
|
|
340
|
+
XAI_CLI_USER_URL,
|
|
341
|
+
usageHeaders(credential.token),
|
|
342
|
+
options.signal,
|
|
343
|
+
);
|
|
344
|
+
const userId = parseUserId(userPayload);
|
|
345
|
+
const billingPayload = await fetchJson(
|
|
346
|
+
XAI_CLI_BILLING_URL,
|
|
347
|
+
usageHeaders(credential.token, userId),
|
|
348
|
+
options.signal,
|
|
349
|
+
);
|
|
350
|
+
return { userId, snapshot: parseUsageSnapshot(billingPayload) };
|
|
351
|
+
}
|