@monotykamary/pi-better-grok 0.2.2 → 0.3.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/README.md +5 -2
- package/index.ts +65 -10
- package/package.json +1 -1
- package/src/config.ts +13 -0
- package/src/resets.ts +19 -2
- package/src/usage-controller.ts +20 -4
- package/src/usage.ts +41 -6
package/README.md
CHANGED
|
@@ -36,7 +36,9 @@ SuperGrok plans earn banked rate-limit reset tokens: redeeming one restores the
|
|
|
36
36
|
|
|
37
37
|
The inventory and redeem calls use the grok.com consumer billing gRPC-Web service (`prod_mc_billing.ConsumerUiSvc/GetRemainingResets` and `RedeemReset`) that the web usage page itself calls, authenticated with the same xAI OAuth token as the usage meter. This surface is undocumented; request shapes are pinned in `src/resets.ts` and schema drift is expected.
|
|
38
38
|
|
|
39
|
-
**
|
|
39
|
+
**Cloudflare requirement (worked around):** grok.com fronts this RPC with a managed challenge that non-browser clients cannot solve — even a perfect Chrome TLS impersonation (curl-impersonate) is challenged, because the site requires a `cf_clearance` cookie issued after a browser solves the challenge once. The cookie is bound to the browser's user-agent and IP. This is the same mechanism community Grok proxies (e.g. grok2api) rely on.
|
|
40
|
+
|
|
41
|
+
Because pi runs on the same machine (and IP) as your browser, supplying both values in config unlocks the surface. To get them: open grok.com in your browser, DevTools → Application → Cookies → copy `cf_clearance`; Network → any request → copy the `User-Agent` request header. When the clearance expires, `/grok-resets` says so explicitly and you re-copy the cookie. Without a clearance configured, the widget hides the count and `/grok-resets` explains the challenge instead of misreporting it as an auth failure.
|
|
40
42
|
|
|
41
43
|
## pi-multiprovider
|
|
42
44
|
|
|
@@ -58,7 +60,8 @@ JSON config at `~/.pi/agent/extensions/pi-better-grok.json` (global) or `<projec
|
|
|
58
60
|
"showResetTimes": true,
|
|
59
61
|
"showBankedResets": true
|
|
60
62
|
},
|
|
61
|
-
"footer": { "mode": "status" }
|
|
63
|
+
"footer": { "mode": "status" },
|
|
64
|
+
"resets": { "cookies": "", "userAgent": "" }
|
|
62
65
|
}
|
|
63
66
|
```
|
|
64
67
|
|
package/index.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* cli-chat-proxy.grok.com identity-first billing surface using pi's native
|
|
8
8
|
* xAI OAuth credential.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type ExtensionContext,
|
|
13
|
+
type Theme,
|
|
14
|
+
type ThemeColor,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
16
|
import { Container, SettingsList, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
12
17
|
import { CONFIG_BASENAME, STATUS_KEY } from "./src/identity.ts";
|
|
13
18
|
import {
|
|
@@ -29,6 +34,7 @@ import {
|
|
|
29
34
|
formatGrokResetOutcome,
|
|
30
35
|
redeemGrokResetForSession,
|
|
31
36
|
selectGrokResetToken,
|
|
37
|
+
setGrokResetNetworkProfile,
|
|
32
38
|
} from "./src/resets.ts";
|
|
33
39
|
import { ResetController } from "./src/reset-controller.ts";
|
|
34
40
|
import {
|
|
@@ -55,6 +61,8 @@ import {
|
|
|
55
61
|
parseUsageSnapshot,
|
|
56
62
|
requestGrokUsage,
|
|
57
63
|
UsageError,
|
|
64
|
+
type UsageSegment,
|
|
65
|
+
type UsageSeverity,
|
|
58
66
|
type UsageSnapshot,
|
|
59
67
|
} from "./src/usage.ts";
|
|
60
68
|
import {
|
|
@@ -84,6 +92,46 @@ const requireSettingsListTheme = (): SettingsListTheme => {
|
|
|
84
92
|
return loadedSettingsListTheme;
|
|
85
93
|
};
|
|
86
94
|
|
|
95
|
+
const USAGE_SEVERITY_COLORS: Record<UsageSeverity, ThemeColor> = {
|
|
96
|
+
ok: "success",
|
|
97
|
+
warning: "warning",
|
|
98
|
+
critical: "error",
|
|
99
|
+
muted: "dim",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Keep a coloured segment on one line. Unlike sanitizeStatusText this leaves the
|
|
104
|
+
* spacing shared by neighbouring segments ("5h: " + "90%") intact.
|
|
105
|
+
*/
|
|
106
|
+
function flattenSegment(text: string): string {
|
|
107
|
+
return text.replace(/[\r\n\t]+/g, " ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Colour a usage line segment by segment: the percentage carries the severity
|
|
112
|
+
* colour while the label, countdown and banked resets stay dim.
|
|
113
|
+
*/
|
|
114
|
+
function colorizeUsageSegments(segments: UsageSegment[], theme: Theme): string {
|
|
115
|
+
return segments
|
|
116
|
+
.map((segment) =>
|
|
117
|
+
theme.fg(USAGE_SEVERITY_COLORS[segment.severity], flattenSegment(segment.text)),
|
|
118
|
+
)
|
|
119
|
+
.join("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Pair the fast-mode segment with the usage segments for the status widget. */
|
|
123
|
+
function statusWidgetParts(
|
|
124
|
+
fast: string | undefined,
|
|
125
|
+
usage: UsageSegment[] | undefined,
|
|
126
|
+
): UsageSegment[] | undefined {
|
|
127
|
+
const parts: UsageSegment[] = fast ? [{ text: fast, severity: "muted" }] : [];
|
|
128
|
+
if (usage?.length) {
|
|
129
|
+
if (parts.length > 0) parts.push({ text: " · ", severity: "muted" });
|
|
130
|
+
parts.push(...usage);
|
|
131
|
+
}
|
|
132
|
+
return parts.length > 0 ? parts : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
87
135
|
class DynamicBorder {
|
|
88
136
|
readonly #color: (text: string) => string;
|
|
89
137
|
|
|
@@ -194,6 +242,11 @@ export default function betterGrok(pi: ExtensionAPI): void {
|
|
|
194
242
|
|
|
195
243
|
function refresh(ctx: ExtensionContext): ResolvedConfig {
|
|
196
244
|
cachedConfig = resolveConfig(ctx.cwd || process.cwd());
|
|
245
|
+
setGrokResetNetworkProfile(
|
|
246
|
+
cachedConfig.resets.cookies || cachedConfig.resets.userAgent
|
|
247
|
+
? { cookies: cachedConfig.resets.cookies, userAgent: cachedConfig.resets.userAgent }
|
|
248
|
+
: undefined,
|
|
249
|
+
);
|
|
197
250
|
return cachedConfig;
|
|
198
251
|
}
|
|
199
252
|
|
|
@@ -542,8 +595,10 @@ export default function betterGrok(pi: ExtensionAPI): void {
|
|
|
542
595
|
parts.push(contextText);
|
|
543
596
|
|
|
544
597
|
const cfg = config(ctx);
|
|
545
|
-
const
|
|
546
|
-
const usageLine =
|
|
598
|
+
const usageStatusSegments = usageController.statusSegments(ctx, cfg, usingSubscription);
|
|
599
|
+
const usageLine = usageStatusSegments
|
|
600
|
+
? colorizeUsageSegments(usageStatusSegments, theme)
|
|
601
|
+
: undefined;
|
|
547
602
|
|
|
548
603
|
let statsLeft = parts.join(" ");
|
|
549
604
|
let statsLeftWidth = visibleWidth(statsLeft);
|
|
@@ -624,22 +679,22 @@ export default function betterGrok(pi: ExtensionAPI): void {
|
|
|
624
679
|
statusInstalled = text !== undefined;
|
|
625
680
|
}
|
|
626
681
|
|
|
627
|
-
function setStatusWidget(ctx: ExtensionContext,
|
|
628
|
-
if (!
|
|
682
|
+
function setStatusWidget(ctx: ExtensionContext, parts: UsageSegment[] | undefined): void {
|
|
683
|
+
if (!parts && !statusWidgetInstalled) return;
|
|
629
684
|
ctx.ui.setWidget(
|
|
630
685
|
STATUS_KEY,
|
|
631
|
-
|
|
686
|
+
parts
|
|
632
687
|
? (_tui, theme) => ({
|
|
633
688
|
invalidate() {},
|
|
634
689
|
render(width: number): string[] {
|
|
635
|
-
const line =
|
|
690
|
+
const line = colorizeUsageSegments(parts, theme);
|
|
636
691
|
return [truncateToWidth(line, width, theme.fg("dim", "..."))];
|
|
637
692
|
},
|
|
638
693
|
})
|
|
639
694
|
: undefined,
|
|
640
695
|
{ placement: "belowEditor" },
|
|
641
696
|
);
|
|
642
|
-
statusWidgetInstalled =
|
|
697
|
+
statusWidgetInstalled = parts !== undefined;
|
|
643
698
|
}
|
|
644
699
|
|
|
645
700
|
function updateFooter(ctx: ExtensionContext): void {
|
|
@@ -672,8 +727,8 @@ export default function betterGrok(pi: ExtensionAPI): void {
|
|
|
672
727
|
}
|
|
673
728
|
|
|
674
729
|
const fast = fastController.statusSegment(ctx, cfg);
|
|
675
|
-
const usage = usageController.
|
|
676
|
-
setStatusWidget(ctx,
|
|
730
|
+
const usage = usageController.statusSegments(ctx, cfg);
|
|
731
|
+
setStatusWidget(ctx, statusWidgetParts(fast, usage));
|
|
677
732
|
}
|
|
678
733
|
|
|
679
734
|
pi.on("session_start", (_event, ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monotykamary/pi-better-grok",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Improve Grok/xAI in pi with fast mode, subscription usage stats, banked reset redemption, multiprovider pools, footer polish, and settings — mirroring pi-better-openai.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"footer",
|
package/src/config.ts
CHANGED
|
@@ -29,6 +29,14 @@ export type UsageConfig = {
|
|
|
29
29
|
};
|
|
30
30
|
export type FooterConfig = { mode?: FooterMode };
|
|
31
31
|
|
|
32
|
+
// Cloudflare fronts the grok.com billing gRPC with a managed challenge that
|
|
33
|
+
// non-browser runtimes cannot solve. A clearance cookie captured from a grok.com
|
|
34
|
+
// browser session (bound to the browser user-agent and IP) unlocks the fetch.
|
|
35
|
+
export type ResetConfig = {
|
|
36
|
+
cookies?: string;
|
|
37
|
+
userAgent?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
32
40
|
export interface ConfigFile {
|
|
33
41
|
persistState?: boolean;
|
|
34
42
|
active?: boolean;
|
|
@@ -37,6 +45,7 @@ export interface ConfigFile {
|
|
|
37
45
|
fast?: FastConfig;
|
|
38
46
|
usage?: UsageConfig;
|
|
39
47
|
footer?: FooterConfig;
|
|
48
|
+
resets?: ResetConfig;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
export interface SupportedModel {
|
|
@@ -57,6 +66,7 @@ export interface ResolvedConfig {
|
|
|
57
66
|
fast: Required<FastConfig>;
|
|
58
67
|
usage: Required<UsageConfig>;
|
|
59
68
|
footer: Required<FooterConfig>;
|
|
69
|
+
resets: Required<ResetConfig>;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
export const DEFAULT_FAST_CONFIG: Required<FastConfig> = { effort: "low" };
|
|
@@ -68,6 +78,7 @@ export const DEFAULT_USAGE_CONFIG: Required<UsageConfig> = {
|
|
|
68
78
|
showBankedResets: true,
|
|
69
79
|
};
|
|
70
80
|
export const DEFAULT_FOOTER_CONFIG: Required<FooterConfig> = { mode: "status" };
|
|
81
|
+
export const DEFAULT_RESET_CONFIG: Required<ResetConfig> = { cookies: "", userAgent: "" };
|
|
71
82
|
export const DEFAULT_CONFIG: ConfigFile = {
|
|
72
83
|
persistState: true,
|
|
73
84
|
active: false,
|
|
@@ -76,6 +87,7 @@ export const DEFAULT_CONFIG: ConfigFile = {
|
|
|
76
87
|
fast: DEFAULT_FAST_CONFIG,
|
|
77
88
|
usage: DEFAULT_USAGE_CONFIG,
|
|
78
89
|
footer: DEFAULT_FOOTER_CONFIG,
|
|
90
|
+
resets: DEFAULT_RESET_CONFIG,
|
|
79
91
|
};
|
|
80
92
|
|
|
81
93
|
type SettingsOptionSection = "footer" | "usage" | "fast";
|
|
@@ -322,5 +334,6 @@ export function resolveConfig(cwd: string, home = homedir(), env = process.env):
|
|
|
322
334
|
fast: mergedSection(DEFAULT_FAST_CONFIG, globalRaw.fast, projectRaw.fast),
|
|
323
335
|
usage: mergedSection(DEFAULT_USAGE_CONFIG, globalRaw.usage, projectRaw.usage),
|
|
324
336
|
footer: mergedSection(DEFAULT_FOOTER_CONFIG, globalRaw.footer, projectRaw.footer),
|
|
337
|
+
resets: mergedSection(DEFAULT_RESET_CONFIG, globalRaw.resets, projectRaw.resets),
|
|
325
338
|
};
|
|
326
339
|
}
|
package/src/resets.ts
CHANGED
|
@@ -360,13 +360,28 @@ export function mapGrokRedeemStatus(
|
|
|
360
360
|
);
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
export type ResetNetworkProfile = { cookies?: string; userAgent?: string };
|
|
364
|
+
|
|
365
|
+
let networkProfile: ResetNetworkProfile | undefined;
|
|
366
|
+
|
|
367
|
+
// Optional Cloudflare clearance profile: cookies captured from a grok.com
|
|
368
|
+
// browser session (at minimum cf_clearance) plus the user-agent that solved
|
|
369
|
+
// the challenge. Cloudflare binds the clearance to the user-agent and IP, and
|
|
370
|
+
// pi runs on the same machine as the browser, so only the user-agent must match.
|
|
371
|
+
export function setGrokResetNetworkProfile(profile: ResetNetworkProfile | undefined): void {
|
|
372
|
+
networkProfile = profile?.cookies || profile?.userAgent ? profile : undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
363
375
|
function resetHeaders(credential: GrokCredential): Record<string, string> {
|
|
364
|
-
|
|
376
|
+
const headers: Record<string, string> = {
|
|
365
377
|
Authorization: `Bearer ${credential.token}`,
|
|
366
378
|
"X-XAI-Token-Auth": GROK_CLI_AUTH_HEADER,
|
|
367
379
|
"Content-Type": GRPC_WEB_CONTENT_TYPE,
|
|
368
380
|
"x-grpc-web": "1",
|
|
369
381
|
};
|
|
382
|
+
if (networkProfile?.cookies) headers.Cookie = networkProfile.cookies;
|
|
383
|
+
if (networkProfile?.userAgent) headers["User-Agent"] = networkProfile.userAgent;
|
|
384
|
+
return headers;
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
async function postGrokRpc(
|
|
@@ -393,7 +408,9 @@ async function postGrokRpc(
|
|
|
393
408
|
if (response.headers.get("cf-mitigated") === "challenge") {
|
|
394
409
|
throw new ResetError(
|
|
395
410
|
"challenge",
|
|
396
|
-
|
|
411
|
+
networkProfile?.cookies
|
|
412
|
+
? `grok.com answered with a Cloudflare challenge even with the configured clearance cookie (HTTP ${response.status}); it likely expired or no longer matches the browser user-agent. Re-copy it from a grok.com browser session.`
|
|
413
|
+
: `grok.com is serving a Cloudflare browser challenge (HTTP ${response.status}); banked reset data cannot be fetched from a non-browser client. Set resets.cookies and resets.userAgent in the Better Grok config from a grok.com browser session to enable it.`,
|
|
397
414
|
response.status,
|
|
398
415
|
);
|
|
399
416
|
}
|
package/src/usage-controller.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type { ResolvedConfig } from "./config.ts";
|
|
|
3
3
|
import { isXaiProvider, readPiStoredOAuthToken } from "./grok-auth.ts";
|
|
4
4
|
import { sanitizeDiagnosticError } from "./format.ts";
|
|
5
5
|
import { currentModelKey } from "./fast-controller.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type UsageSegment,
|
|
8
|
+
type UsageSnapshot,
|
|
9
|
+
formatUsageDetail,
|
|
10
|
+
formatUsageSnapshot,
|
|
11
|
+
usageSegments,
|
|
12
|
+
} from "./usage.ts";
|
|
7
13
|
|
|
8
14
|
export function isGrokSubscriptionModel(
|
|
9
15
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
@@ -77,22 +83,32 @@ export class UsageController {
|
|
|
77
83
|
return this.usageSnapshot;
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
statusSegments(
|
|
81
87
|
ctx: ExtensionContext,
|
|
82
88
|
cfg = this.getConfig(ctx),
|
|
83
89
|
isUsingOAuth?: boolean,
|
|
84
|
-
):
|
|
90
|
+
): UsageSegment[] | undefined {
|
|
85
91
|
return this.usageSnapshot &&
|
|
86
92
|
!this.usageError &&
|
|
87
93
|
cfg.usage.enabled &&
|
|
88
94
|
isGrokSubscriptionModel(ctx, cfg, isUsingOAuth)
|
|
89
|
-
?
|
|
95
|
+
? usageSegments(this.usageSnapshot, {
|
|
90
96
|
...cfg.usage,
|
|
91
97
|
bankedResets: this.getBankedResets?.() ?? null,
|
|
92
98
|
})
|
|
93
99
|
: undefined;
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
statusLine(
|
|
103
|
+
ctx: ExtensionContext,
|
|
104
|
+
cfg = this.getConfig(ctx),
|
|
105
|
+
isUsingOAuth?: boolean,
|
|
106
|
+
): string | undefined {
|
|
107
|
+
return this.statusSegments(ctx, cfg, isUsingOAuth)
|
|
108
|
+
?.map((segment) => segment.text)
|
|
109
|
+
.join("");
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
formatStatus(ctx: ExtensionContext): string {
|
|
97
113
|
const cfg = this.getConfig(ctx);
|
|
98
114
|
if (!cfg.usage.enabled) return "Usage display is disabled.";
|
package/src/usage.ts
CHANGED
|
@@ -241,24 +241,59 @@ export function formatBankedResetsSuffix(count: number | null | undefined): stri
|
|
|
241
241
|
return `${count} banked reset${count === 1 ? "" : "s"}`;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
export
|
|
244
|
+
export type UsageSeverity = "ok" | "warning" | "critical" | "muted";
|
|
245
|
+
|
|
246
|
+
export type UsageSegment = {
|
|
247
|
+
text: string;
|
|
248
|
+
severity: UsageSeverity;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/** Remaining budget at or below these thresholds turns the percentage amber/red. */
|
|
252
|
+
const WARNING_LEFT_PERCENT = 30;
|
|
253
|
+
const CRITICAL_LEFT_PERCENT = 10;
|
|
254
|
+
|
|
255
|
+
export function severityForLeftPercent(percent: number | null): UsageSeverity {
|
|
256
|
+
if (percent === null) return "muted";
|
|
257
|
+
if (percent <= CRITICAL_LEFT_PERCENT) return "critical";
|
|
258
|
+
if (percent <= WARNING_LEFT_PERCENT) return "warning";
|
|
259
|
+
return "ok";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function usageSegments(
|
|
245
263
|
snapshot: UsageSnapshot,
|
|
246
264
|
options: UsageStatusOptions,
|
|
247
265
|
now = Date.now(),
|
|
248
|
-
):
|
|
266
|
+
): UsageSegment[] {
|
|
249
267
|
const used = snapshot.creditUsagePercent;
|
|
250
268
|
const left = used === null ? null : clampPercent(100 - used);
|
|
251
|
-
const
|
|
269
|
+
const segments: UsageSegment[] = [
|
|
270
|
+
{ text: "Usage: ", severity: "muted" },
|
|
271
|
+
{ text: formatPercent(left), severity: severityForLeftPercent(left) },
|
|
272
|
+
{ text: " left", severity: "muted" },
|
|
273
|
+
];
|
|
252
274
|
if (options.showResetTimes) {
|
|
253
275
|
const seconds = periodSecondsLeft(snapshot, now);
|
|
254
276
|
const countdown = formatResetCountdown(seconds);
|
|
255
277
|
const clock = formatResetClock(seconds, now);
|
|
256
|
-
if (countdown && clock)
|
|
278
|
+
if (countdown && clock) {
|
|
279
|
+
segments.push({ text: ` · ↺ ${countdown} - ${clock}`, severity: "muted" });
|
|
280
|
+
}
|
|
257
281
|
}
|
|
258
282
|
const banked =
|
|
259
283
|
options.showBankedResets === false ? null : formatBankedResetsSuffix(options.bankedResets);
|
|
260
|
-
if (banked)
|
|
261
|
-
return
|
|
284
|
+
if (banked) segments.push({ text: ` · ${banked}`, severity: "muted" });
|
|
285
|
+
return segments;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Flat text form of {@link usageSegments}, used for status lines and notifications. */
|
|
289
|
+
export function formatUsageSnapshot(
|
|
290
|
+
snapshot: UsageSnapshot,
|
|
291
|
+
options: UsageStatusOptions,
|
|
292
|
+
now = Date.now(),
|
|
293
|
+
): string {
|
|
294
|
+
return usageSegments(snapshot, options, now)
|
|
295
|
+
.map((segment) => segment.text)
|
|
296
|
+
.join("");
|
|
262
297
|
}
|
|
263
298
|
|
|
264
299
|
export function formatUsageDetail(snapshot: UsageSnapshot): string {
|