@ravshansbox/pi-quota 0.3.1 → 0.4.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/README.md +14 -3
- package/index.ts +21 -34
- package/package.json +8 -11
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,16 +1,11 @@
|
|
|
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';
|
|
12
|
-
import {
|
|
13
|
-
|
|
4
|
+
import {
|
|
5
|
+
getAgentDir,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionContext,
|
|
8
|
+
} from '@earendil-works/pi-coding-agent';
|
|
14
9
|
|
|
15
10
|
interface QuotaConfig {
|
|
16
11
|
codexResets?: { autoRedeem?: boolean };
|
|
@@ -71,12 +66,12 @@ type CodexResetCreditList = {
|
|
|
71
66
|
|
|
72
67
|
type CodexResetConsumeResponse = { code?: string };
|
|
73
68
|
|
|
74
|
-
const
|
|
69
|
+
const modelProviderNames: Record<QuotaState['provider'], string> = {
|
|
75
70
|
anthropic: 'anthropic',
|
|
76
71
|
'openai-codex': 'openai',
|
|
77
72
|
};
|
|
78
73
|
|
|
79
|
-
const
|
|
74
|
+
const requestTimeoutMs = 30_000;
|
|
80
75
|
|
|
81
76
|
function nextMarkAfter(time: Date): Date {
|
|
82
77
|
const next = new Date(time.getTime());
|
|
@@ -119,7 +114,7 @@ function formatResetTime(reset: Date): string {
|
|
|
119
114
|
|
|
120
115
|
async function loadConfig(): Promise<QuotaConfig | null> {
|
|
121
116
|
try {
|
|
122
|
-
const settingsPath = join(
|
|
117
|
+
const settingsPath = join(getAgentDir(), 'settings.json');
|
|
123
118
|
const settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
124
119
|
return settings.quota ?? null;
|
|
125
120
|
} catch {
|
|
@@ -128,7 +123,7 @@ async function loadConfig(): Promise<QuotaConfig | null> {
|
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
function authPath() {
|
|
131
|
-
return join(
|
|
126
|
+
return join(getAgentDir(), 'auth.json');
|
|
132
127
|
}
|
|
133
128
|
|
|
134
129
|
async function loadAuth(): Promise<AuthFile | null> {
|
|
@@ -150,7 +145,7 @@ async function persistAuthRecord(provider: string, record: OAuthAuthRecord): Pro
|
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
function logPath() {
|
|
153
|
-
return join(
|
|
148
|
+
return join(getAgentDir(), 'pi-quota.log');
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
async function logError(message: string, error?: unknown): Promise<void> {
|
|
@@ -183,11 +178,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
183
178
|
}
|
|
184
179
|
|
|
185
180
|
function buildStatusText(): string | undefined {
|
|
186
|
-
// Only ever render the provider backing the active model; anything else
|
|
187
|
-
// would not apply to the next request.
|
|
188
181
|
const activeProvider = ctxRef?.model?.provider;
|
|
189
182
|
if (!activeProvider) return undefined;
|
|
190
|
-
const state = states.find((s) =>
|
|
183
|
+
const state = states.find((s) => modelProviderNames[s.provider] === activeProvider);
|
|
191
184
|
|
|
192
185
|
if (!state) return undefined;
|
|
193
186
|
|
|
@@ -214,9 +207,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
214
207
|
|
|
215
208
|
const text = buildStatusText();
|
|
216
209
|
|
|
217
|
-
ctxRef.ui.
|
|
210
|
+
ctxRef.ui.setWidget(
|
|
218
211
|
'pi-quota',
|
|
219
|
-
text === undefined ? undefined : ctxRef.ui.theme.fg('muted', text),
|
|
212
|
+
text === undefined ? undefined : [ctxRef.ui.theme.fg('muted', text)],
|
|
213
|
+
{ placement: 'aboveEditor' },
|
|
220
214
|
);
|
|
221
215
|
}
|
|
222
216
|
|
|
@@ -252,7 +246,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
252
246
|
function pingProviderOnReset(provider: QuotaState['provider']) {
|
|
253
247
|
const ctx = ctxRef;
|
|
254
248
|
if (!ctx?.model) return;
|
|
255
|
-
if (ctx.model.provider !==
|
|
249
|
+
if (ctx.model.provider !== modelProviderNames[provider]) return;
|
|
256
250
|
|
|
257
251
|
const child = spawn(
|
|
258
252
|
'pi',
|
|
@@ -313,7 +307,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
313
307
|
refresh_token: record.refresh,
|
|
314
308
|
client_id: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
315
309
|
}),
|
|
316
|
-
signal: AbortSignal.timeout(
|
|
310
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
317
311
|
});
|
|
318
312
|
|
|
319
313
|
if (!response.ok) {
|
|
@@ -347,7 +341,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
347
341
|
refresh_token: record.refresh,
|
|
348
342
|
client_id: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
349
343
|
}),
|
|
350
|
-
signal: AbortSignal.timeout(
|
|
344
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
351
345
|
});
|
|
352
346
|
|
|
353
347
|
if (!response.ok) {
|
|
@@ -378,7 +372,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
372
|
'User-Agent': 'pi-quota/1.0',
|
|
379
373
|
accept: 'application/json',
|
|
380
374
|
},
|
|
381
|
-
signal: AbortSignal.timeout(
|
|
375
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
382
376
|
},
|
|
383
377
|
);
|
|
384
378
|
|
|
@@ -428,7 +422,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
428
422
|
credit_id: creditId,
|
|
429
423
|
redeem_request_id: redeemRequestId,
|
|
430
424
|
}),
|
|
431
|
-
signal: AbortSignal.timeout(
|
|
425
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
432
426
|
},
|
|
433
427
|
);
|
|
434
428
|
|
|
@@ -457,7 +451,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
457
451
|
'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',
|
|
458
452
|
accept: 'application/json',
|
|
459
453
|
},
|
|
460
|
-
signal: AbortSignal.timeout(
|
|
454
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
461
455
|
});
|
|
462
456
|
|
|
463
457
|
if (response.ok) {
|
|
@@ -487,7 +481,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
487
481
|
'User-Agent': 'pi-quota/1.0',
|
|
488
482
|
accept: 'application/json',
|
|
489
483
|
},
|
|
490
|
-
signal: AbortSignal.timeout(
|
|
484
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
491
485
|
});
|
|
492
486
|
|
|
493
487
|
if (response.ok) {
|
|
@@ -495,8 +489,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
495
489
|
if (data.rate_limit) {
|
|
496
490
|
const primary = data.rate_limit.primary_window;
|
|
497
491
|
const secondary = data.rate_limit.secondary_window;
|
|
498
|
-
// During OpenAI's temporary 5h-quota removal, the only returned
|
|
499
|
-
// window is the weekly quota, still named `primary_window`.
|
|
500
492
|
const fiveHour = secondary ? primary : undefined;
|
|
501
493
|
const sevenDay = secondary ?? primary;
|
|
502
494
|
const resetsAvailable = data.rate_limit_reset_credits?.available_count;
|
|
@@ -530,12 +522,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
530
522
|
const codexState = states.find((s) => s.provider === 'openai-codex');
|
|
531
523
|
if (!codexState) return;
|
|
532
524
|
|
|
533
|
-
// Reset the flag when the weekly window recovers
|
|
534
525
|
if (codexState.sevenDayRemaining !== null && codexState.sevenDayRemaining > 0) {
|
|
535
526
|
codexRedeemAttempted = false;
|
|
536
527
|
}
|
|
537
528
|
|
|
538
|
-
// Only redeem if weekly is exhausted and we haven't tried yet
|
|
539
529
|
if (codexState.sevenDayRemaining !== 0) return;
|
|
540
530
|
if (codexState.resetsAvailable === 0) return;
|
|
541
531
|
if (codexRedeemAttempted) return;
|
|
@@ -611,13 +601,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
611
601
|
ctxRef = ctx;
|
|
612
602
|
codexRedeemAttempted = false;
|
|
613
603
|
|
|
614
|
-
// Do not await: quota polling is network-bound and would block startup.
|
|
615
604
|
void runCycle();
|
|
616
605
|
scheduleCheck();
|
|
617
606
|
});
|
|
618
607
|
|
|
619
|
-
// The status tracks the active model's provider, so re-render on switch
|
|
620
|
-
// rather than waiting for the next poll (up to 10 minutes away).
|
|
621
608
|
pi.on('model_select', (_event, ctx) => {
|
|
622
609
|
ctxRef = ctx;
|
|
623
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.0",
|
|
4
4
|
"description": "Anthropic and OpenAI Codex quota status extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,23 +24,20 @@
|
|
|
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
34
|
"@earendil-works/pi-coding-agent": "*"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@eslint/js": "^10.0.1",
|
|
38
37
|
"@types/node": "^26.6.5",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"typescript": "^6.0.3",
|
|
43
|
-
"typescript-eslint": "^8.72.0"
|
|
38
|
+
"oxfmt": "^0.74.0",
|
|
39
|
+
"oxlint": "^1.89.0",
|
|
40
|
+
"typescript": "^7.0.2"
|
|
44
41
|
},
|
|
45
42
|
"files": [
|
|
46
43
|
"index.ts"
|