@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.
- package/README.md +14 -3
- package/index.ts +15 -31
- 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
|
|
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
|
|
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
|
|
70
|
+
const modelProviderNames: Record<QuotaState['provider'], string> = {
|
|
78
71
|
anthropic: 'anthropic',
|
|
79
72
|
'openai-codex': 'openai',
|
|
80
73
|
};
|
|
81
74
|
|
|
82
|
-
const
|
|
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) =>
|
|
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.
|
|
221
|
-
'
|
|
222
|
-
|
|
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 !==
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
"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": "
|
|
28
|
-
"lint:fix": "
|
|
29
|
-
"format": "
|
|
30
|
-
"format: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
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
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"
|