@monotykamary/pi-better-grok 0.2.0 → 0.2.2

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
@@ -28,7 +28,7 @@ Data comes from the same revision-pinned Grok subscription surface used by the c
28
28
  1. `GET https://cli-chat-proxy.grok.com/v1/user` (identity)
29
29
  2. `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` (with the `x-userid` header)
30
30
 
31
- Status widget line: `Usage: 66% left · ↺ 5d5h - Mon 5:34 PM` (weekly period + reset clock). Defaults to the widget area below the editor, like pi-better-openai; set `"footer": {"mode": "replace"}` for the full custom footer.
31
+ Status widget line: `Usage: 66% left · ↺ 5d5h - Mon 5:34 PM · 1 banked reset` (weekly period, reset clock, and available banked reset count). Defaults to the widget area below the editor, like pi-better-openai; set `"footer": {"mode": "replace"}` for the full custom footer.
32
32
 
33
33
  ## Banked resets
34
34
 
@@ -36,6 +36,8 @@ 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
+ **Known limitation:** grok.com fronts this RPC with a Cloudflare managed challenge, which browser-fingerprinted clients (Electron apps) pass but plain CLI runtimes cannot — the edge answers with `403 · cf-mitigated: challenge` regardless of headers or credentials. When that happens the widget hides the count and `/grok-resets` explains the challenge instead of misreporting it as an auth failure. The wiring is live end to end, so the count appears in any environment where the fetch can succeed.
40
+
39
41
  ## pi-multiprovider
40
42
 
41
43
  When [pi-multiprovider](https://github.com/monotykamary/pi-multiprovider) pools several `xai` accounts, the session's active account (chosen with `/switch-account`) is resolved first for usage display and banked resets, and the usage widget refreshes on every switch. Without that extension, credential resolution is unchanged: pi's native `xai` OAuth, then `xai-oauth`/`xai-auth` auth-file entries, then the Grok CLI store.
@@ -53,7 +55,8 @@ JSON config at `~/.pi/agent/extensions/pi-better-grok.json` (global) or `<projec
53
55
  "enabled": true,
54
56
  "refreshIntervalMs": 60000,
55
57
  "showOnlyOnSubscriptionModels": true,
56
- "showResetTimes": true
58
+ "showResetTimes": true,
59
+ "showBankedResets": true
57
60
  },
58
61
  "footer": { "mode": "status" }
59
62
  }
package/index.ts CHANGED
@@ -152,7 +152,12 @@ export default function betterGrok(pi: ExtensionAPI): void {
152
152
  let sessionNameCached = false;
153
153
  let cachedSessionNameLeafId: string | null | undefined;
154
154
  let cachedSessionName: string | undefined;
155
- const usageController = new UsageController(config, updateFooter, fetchUsageSnapshot);
155
+ const usageController = new UsageController(
156
+ config,
157
+ updateFooter,
158
+ fetchUsageSnapshot,
159
+ () => resetController.snapshot?.credits.availableCount ?? null,
160
+ );
156
161
  const resetController = new ResetController();
157
162
  let multiproviderService: MultiproviderService | undefined;
158
163
  let unsubscribeMultiprovider: (() => void) | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-better-grok",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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
@@ -25,6 +25,7 @@ export type UsageConfig = {
25
25
  refreshIntervalMs?: number;
26
26
  showOnlyOnSubscriptionModels?: boolean;
27
27
  showResetTimes?: boolean;
28
+ showBankedResets?: boolean;
28
29
  };
29
30
  export type FooterConfig = { mode?: FooterMode };
30
31
 
@@ -64,6 +65,7 @@ export const DEFAULT_USAGE_CONFIG: Required<UsageConfig> = {
64
65
  refreshIntervalMs: 60_000,
65
66
  showOnlyOnSubscriptionModels: true,
66
67
  showResetTimes: true,
68
+ showBankedResets: true,
67
69
  };
68
70
  export const DEFAULT_FOOTER_CONFIG: Required<FooterConfig> = { mode: "status" };
69
71
  export const DEFAULT_CONFIG: ConfigFile = {
@@ -193,6 +195,16 @@ export const USAGE_SETTING_DESCRIPTORS: SettingsOptionDescriptor[] = [
193
195
  parse: (rawValue) => rawValue === "true",
194
196
  current: (config) => String(config.usage.showResetTimes),
195
197
  },
198
+ {
199
+ id: "usage.showBankedResets",
200
+ section: "usage",
201
+ key: "showBankedResets",
202
+ label: "Banked reset count",
203
+ description: "Show the available banked SuperGrok reset count in the usage status line.",
204
+ values: ["true", "false"],
205
+ parse: (rawValue) => rawValue === "true",
206
+ current: (config) => String(config.usage.showBankedResets),
207
+ },
196
208
  ];
197
209
 
198
210
  export const FAST_SETTING_DESCRIPTORS: SettingsOptionDescriptor[] = [
package/src/resets.ts CHANGED
@@ -42,7 +42,14 @@ export type GrokRedeemCode = "reset" | "no_credit" | "already_redeemed";
42
42
 
43
43
  export type GrokRedeemResult = { code: GrokRedeemCode };
44
44
 
45
- export type ResetErrorCode = "auth" | "http" | "grpc" | "invalid" | "oversize" | "transport";
45
+ export type ResetErrorCode =
46
+ | "auth"
47
+ | "challenge"
48
+ | "http"
49
+ | "grpc"
50
+ | "invalid"
51
+ | "oversize"
52
+ | "transport";
46
53
 
47
54
  export class ResetError extends Error {
48
55
  readonly code: ResetErrorCode;
@@ -383,6 +390,13 @@ async function postGrokRpc(
383
390
  throw new ResetError("transport", `Grok reset request failed: ${message}`);
384
391
  }
385
392
  if (!response.ok) {
393
+ if (response.headers.get("cf-mitigated") === "challenge") {
394
+ throw new ResetError(
395
+ "challenge",
396
+ `grok.com is serving a Cloudflare browser challenge (HTTP ${response.status}); banked reset data cannot be fetched from a non-browser client.`,
397
+ response.status,
398
+ );
399
+ }
386
400
  if (response.status === 401 || response.status === 403) {
387
401
  throw new ResetError(
388
402
  "auth",
@@ -59,15 +59,18 @@ export class UsageController {
59
59
  private readonly getConfig: (ctx: ExtensionContext) => ResolvedConfig;
60
60
  private readonly updateFooter: (ctx: ExtensionContext) => void;
61
61
  private readonly fetchSnapshot: FetchUsageSnapshot;
62
+ private readonly getBankedResets: (() => number | null) | undefined;
62
63
 
63
64
  constructor(
64
65
  getConfig: (ctx: ExtensionContext) => ResolvedConfig,
65
66
  updateFooter: (ctx: ExtensionContext) => void,
66
67
  fetchSnapshot: FetchUsageSnapshot,
68
+ getBankedResets?: () => number | null,
67
69
  ) {
68
70
  this.getConfig = getConfig;
69
71
  this.updateFooter = updateFooter;
70
72
  this.fetchSnapshot = fetchSnapshot;
73
+ this.getBankedResets = getBankedResets;
71
74
  }
72
75
 
73
76
  get snapshot(): UsageSnapshot | undefined {
@@ -83,7 +86,10 @@ export class UsageController {
83
86
  !this.usageError &&
84
87
  cfg.usage.enabled &&
85
88
  isGrokSubscriptionModel(ctx, cfg, isUsingOAuth)
86
- ? formatUsageSnapshot(this.usageSnapshot, cfg.usage)
89
+ ? formatUsageSnapshot(this.usageSnapshot, {
90
+ ...cfg.usage,
91
+ bankedResets: this.getBankedResets?.() ?? null,
92
+ })
87
93
  : undefined;
88
94
  }
89
95
 
@@ -99,7 +105,10 @@ export class UsageController {
99
105
  this.usageUpdatedAt && Date.now() - this.usageUpdatedAt > cfg.usage.refreshIntervalMs * 2
100
106
  ? ` · stale`
101
107
  : "";
102
- return `${formatUsageSnapshot(this.usageSnapshot, cfg.usage)}${stale}`;
108
+ return `${formatUsageSnapshot(this.usageSnapshot, {
109
+ ...cfg.usage,
110
+ bankedResets: this.getBankedResets?.() ?? null,
111
+ })}${stale}`;
103
112
  }
104
113
 
105
114
  formatDetail(ctx: ExtensionContext): string {
package/src/usage.ts CHANGED
@@ -230,9 +230,20 @@ export function formatCents(cents: number | null): string {
230
230
  return typeof cents === "number" ? `$${(cents / 100).toFixed(2)}` : "--";
231
231
  }
232
232
 
233
+ export type UsageStatusOptions = {
234
+ showResetTimes: boolean;
235
+ showBankedResets?: boolean;
236
+ bankedResets?: number | null;
237
+ };
238
+
239
+ export function formatBankedResetsSuffix(count: number | null | undefined): string | null {
240
+ if (typeof count !== "number" || !Number.isInteger(count) || count <= 0) return null;
241
+ return `${count} banked reset${count === 1 ? "" : "s"}`;
242
+ }
243
+
233
244
  export function formatUsageSnapshot(
234
245
  snapshot: UsageSnapshot,
235
- options: { showResetTimes: boolean },
246
+ options: UsageStatusOptions,
236
247
  now = Date.now(),
237
248
  ): string {
238
249
  const used = snapshot.creditUsagePercent;
@@ -244,6 +255,9 @@ export function formatUsageSnapshot(
244
255
  const clock = formatResetClock(seconds, now);
245
256
  if (countdown && clock) parts.push(`↺ ${countdown} - ${clock}`);
246
257
  }
258
+ const banked =
259
+ options.showBankedResets === false ? null : formatBankedResetsSuffix(options.bankedResets);
260
+ if (banked) parts.push(banked);
247
261
  return parts.join(" · ");
248
262
  }
249
263