@ravshansbox/pi-quota 0.3.2 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +14 -3
  2. package/index.ts +15 -31
  3. package/package.json +10 -12
package/README.md CHANGED
@@ -8,13 +8,14 @@ Anthropic and OpenAI Codex quota status extension for pi.
8
8
  pi install npm:@ravshansbox/pi-quota
9
9
  ```
10
10
 
11
+ Add `-l` to install it in project settings.
12
+
11
13
  ## Usage
12
14
 
13
- Pi loads the extension from `./index.ts` and shows the remaining quota for the provider backing the active model in the footer status line.
15
+ Pi loads the extension from `./index.ts` and shows the remaining quota for the provider backing the active model above the editor.
14
16
 
15
17
  - Polls Anthropic and OpenAI Codex usage endpoints at every 10-minute wall-clock mark (`HH:00`, `HH:10`, `HH:20`, `HH:30`, `HH:40`, `HH:50`); polls immediately on session start, then aligns to the next mark
16
- - Shows a footer status for the provider backing the **active model**, and only when quota data for that provider has been polled; switching models re-renders it immediately
17
- - The status shares the footer line with other extensions, so it uses no extra terminal row
18
+ - Shows a widget above the editor for the provider backing the **active model**, and only when quota data for that provider has been polled; switching models re-renders it immediately
18
19
  - The text stays compact, e.g. `93% 1h29m · 67% 3d13h`, where the 5-hour window comes first and the 7-day window second, and each window shows its remaining percentage followed by time until reset
19
20
  - Available resets, when present, are appended as `· 2x 5h` (count, then time until the soonest expires)
20
21
  - Both providers are still polled whenever their credentials exist, so quota is already fresh when the active model changes
@@ -25,6 +26,16 @@ Pi loads the extension from `./index.ts` and shows the remaining quota for the p
25
26
 
26
27
  No configuration is required. OAuth credentials for Anthropic and OpenAI Codex are read from `~/.pi/agent/auth.json`.
27
28
 
29
+ To redeem a saved OpenAI Codex reset automatically when the 7-day window is exhausted, add this to `~/.pi/agent/settings.json`:
30
+
31
+ ```json
32
+ {
33
+ "quota": {
34
+ "codexResets": { "autoRedeem": true }
35
+ }
36
+ }
37
+ ```
38
+
28
39
  ## Development
29
40
 
30
41
  ```bash
package/index.ts CHANGED
@@ -1,11 +1,3 @@
1
- /**
2
- * pi-quota — tracks Anthropic and OpenAI Codex subscription quota and renders a footer status.
3
- *
4
- * NOTE: This extension deliberately has no automated tests. It is a single-file extension whose
5
- * behaviour is dominated by external HTTP APIs (Anthropic, OpenAI Codex) and pi runtime events;
6
- * it is verified manually by running it in pi. Do not add a test suite here.
7
- */
8
-
9
1
  import { spawn } from 'node:child_process';
10
2
  import { readFile, writeFile, appendFile } from 'node:fs/promises';
11
3
  import { join } from 'node:path';
@@ -14,6 +6,7 @@ import {
14
6
  type ExtensionAPI,
15
7
  type ExtensionContext,
16
8
  } from '@earendil-works/pi-coding-agent';
9
+ import { Text } from '@earendil-works/pi-tui';
17
10
 
18
11
  interface QuotaConfig {
19
12
  codexResets?: { autoRedeem?: boolean };
@@ -74,12 +67,12 @@ type CodexResetCreditList = {
74
67
 
75
68
  type CodexResetConsumeResponse = { code?: string };
76
69
 
77
- const MODEL_PROVIDER_NAMES: Record<QuotaState['provider'], string> = {
70
+ const modelProviderNames: Record<QuotaState['provider'], string> = {
78
71
  anthropic: 'anthropic',
79
72
  'openai-codex': 'openai',
80
73
  };
81
74
 
82
- const REQUEST_TIMEOUT_MS = 30_000;
75
+ const requestTimeoutMs = 30_000;
83
76
 
84
77
  function nextMarkAfter(time: Date): Date {
85
78
  const next = new Date(time.getTime());
@@ -186,11 +179,9 @@ export default function (pi: ExtensionAPI) {
186
179
  }
187
180
 
188
181
  function buildStatusText(): string | undefined {
189
- // Only ever render the provider backing the active model; anything else
190
- // would not apply to the next request.
191
182
  const activeProvider = ctxRef?.model?.provider;
192
183
  if (!activeProvider) return undefined;
193
- const state = states.find((s) => MODEL_PROVIDER_NAMES[s.provider] === activeProvider);
184
+ const state = states.find((s) => modelProviderNames[s.provider] === activeProvider);
194
185
 
195
186
  if (!state) return undefined;
196
187
 
@@ -216,11 +207,11 @@ export default function (pi: ExtensionAPI) {
216
207
  if (!ctxRef) return;
217
208
 
218
209
  const text = buildStatusText();
210
+ const line = text === undefined ? undefined : ctxRef.ui.theme.fg('muted', text);
219
211
 
220
- ctxRef.ui.setStatus(
221
- 'pi-quota',
222
- text === undefined ? undefined : ctxRef.ui.theme.fg('muted', text),
223
- );
212
+ ctxRef.ui.setWidget('pi-quota', line === undefined ? undefined : () => new Text(line, 0, 0), {
213
+ placement: 'aboveEditor',
214
+ });
224
215
  }
225
216
 
226
217
  function windowResetDetected(
@@ -255,7 +246,7 @@ export default function (pi: ExtensionAPI) {
255
246
  function pingProviderOnReset(provider: QuotaState['provider']) {
256
247
  const ctx = ctxRef;
257
248
  if (!ctx?.model) return;
258
- if (ctx.model.provider !== MODEL_PROVIDER_NAMES[provider]) return;
249
+ if (ctx.model.provider !== modelProviderNames[provider]) return;
259
250
 
260
251
  const child = spawn(
261
252
  'pi',
@@ -316,7 +307,7 @@ export default function (pi: ExtensionAPI) {
316
307
  refresh_token: record.refresh,
317
308
  client_id: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
318
309
  }),
319
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
310
+ signal: AbortSignal.timeout(requestTimeoutMs),
320
311
  });
321
312
 
322
313
  if (!response.ok) {
@@ -350,7 +341,7 @@ export default function (pi: ExtensionAPI) {
350
341
  refresh_token: record.refresh,
351
342
  client_id: 'app_EMoamEEZ73f0CkXaXp7hrann',
352
343
  }),
353
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
344
+ signal: AbortSignal.timeout(requestTimeoutMs),
354
345
  });
355
346
 
356
347
  if (!response.ok) {
@@ -381,7 +372,7 @@ export default function (pi: ExtensionAPI) {
381
372
  'User-Agent': 'pi-quota/1.0',
382
373
  accept: 'application/json',
383
374
  },
384
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
375
+ signal: AbortSignal.timeout(requestTimeoutMs),
385
376
  },
386
377
  );
387
378
 
@@ -431,7 +422,7 @@ export default function (pi: ExtensionAPI) {
431
422
  credit_id: creditId,
432
423
  redeem_request_id: redeemRequestId,
433
424
  }),
434
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
425
+ signal: AbortSignal.timeout(requestTimeoutMs),
435
426
  },
436
427
  );
437
428
 
@@ -460,7 +451,7 @@ export default function (pi: ExtensionAPI) {
460
451
  'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',
461
452
  accept: 'application/json',
462
453
  },
463
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
454
+ signal: AbortSignal.timeout(requestTimeoutMs),
464
455
  });
465
456
 
466
457
  if (response.ok) {
@@ -490,7 +481,7 @@ export default function (pi: ExtensionAPI) {
490
481
  'User-Agent': 'pi-quota/1.0',
491
482
  accept: 'application/json',
492
483
  },
493
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
484
+ signal: AbortSignal.timeout(requestTimeoutMs),
494
485
  });
495
486
 
496
487
  if (response.ok) {
@@ -498,8 +489,6 @@ export default function (pi: ExtensionAPI) {
498
489
  if (data.rate_limit) {
499
490
  const primary = data.rate_limit.primary_window;
500
491
  const secondary = data.rate_limit.secondary_window;
501
- // During OpenAI's temporary 5h-quota removal, the only returned
502
- // window is the weekly quota, still named `primary_window`.
503
492
  const fiveHour = secondary ? primary : undefined;
504
493
  const sevenDay = secondary ?? primary;
505
494
  const resetsAvailable = data.rate_limit_reset_credits?.available_count;
@@ -533,12 +522,10 @@ export default function (pi: ExtensionAPI) {
533
522
  const codexState = states.find((s) => s.provider === 'openai-codex');
534
523
  if (!codexState) return;
535
524
 
536
- // Reset the flag when the weekly window recovers
537
525
  if (codexState.sevenDayRemaining !== null && codexState.sevenDayRemaining > 0) {
538
526
  codexRedeemAttempted = false;
539
527
  }
540
528
 
541
- // Only redeem if weekly is exhausted and we haven't tried yet
542
529
  if (codexState.sevenDayRemaining !== 0) return;
543
530
  if (codexState.resetsAvailable === 0) return;
544
531
  if (codexRedeemAttempted) return;
@@ -614,13 +601,10 @@ export default function (pi: ExtensionAPI) {
614
601
  ctxRef = ctx;
615
602
  codexRedeemAttempted = false;
616
603
 
617
- // Do not await: quota polling is network-bound and would block startup.
618
604
  void runCycle();
619
605
  scheduleCheck();
620
606
  });
621
607
 
622
- // The status tracks the active model's provider, so re-render on switch
623
- // rather than waiting for the next poll (up to 10 minutes away).
624
608
  pi.on('model_select', (_event, ctx) => {
625
609
  ctxRef = ctx;
626
610
  updateStatus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravshansbox/pi-quota",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Anthropic and OpenAI Codex quota status extension for pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,23 +24,21 @@
24
24
  },
25
25
  "scripts": {
26
26
  "typecheck": "tsc --noEmit",
27
- "lint": "eslint .",
28
- "lint:fix": "eslint . --fix",
29
- "format": "prettier --write .",
30
- "format:check": "prettier --check .",
27
+ "lint": "oxlint --deny-warnings",
28
+ "lint:fix": "oxlint --fix",
29
+ "format": "oxfmt",
30
+ "format:check": "oxfmt --check",
31
31
  "check": "npm run typecheck && npm run lint && npm run format:check"
32
32
  },
33
33
  "peerDependencies": {
34
- "@earendil-works/pi-coding-agent": "*"
34
+ "@earendil-works/pi-coding-agent": "*",
35
+ "@earendil-works/pi-tui": "*"
35
36
  },
36
37
  "devDependencies": {
37
- "@eslint/js": "^10.0.1",
38
38
  "@types/node": "^26.6.5",
39
- "eslint": "^10.13.0",
40
- "eslint-config-prettier": "^10.1.8",
41
- "prettier": "^3.9.10",
42
- "typescript": "^6.0.3",
43
- "typescript-eslint": "^8.72.0"
39
+ "oxfmt": "^0.74.0",
40
+ "oxlint": "^1.89.0",
41
+ "typescript": "^7.0.2"
44
42
  },
45
43
  "files": [
46
44
  "index.ts"