@bacnh85/pi-sub 0.1.8 → 0.1.10
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 +23 -22
- package/{index.ts → extensions/index.ts} +82 -75
- package/extensions/package.json +3 -0
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -15,13 +15,9 @@ pi install npm:@bacnh85/pi-sub
|
|
|
15
15
|
From this repository checkout:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
19
|
-
npm install
|
|
20
|
-
cd ../..
|
|
21
|
-
|
|
22
|
-
pi install ./extensions/pi-sub
|
|
18
|
+
pi install ./pi-sub
|
|
23
19
|
# or test directly
|
|
24
|
-
pi -e ./
|
|
20
|
+
pi -e ./pi-sub
|
|
25
21
|
```
|
|
26
22
|
|
|
27
23
|
## What it shows
|
|
@@ -30,32 +26,31 @@ pi -e ./extensions/pi-sub
|
|
|
30
26
|
|
|
31
27
|
The footer status appears after Pi's built-in status/token usage line and includes:
|
|
32
28
|
|
|
33
|
-
- active account email;
|
|
34
|
-
- subscription plan, such as `Plus
|
|
35
|
-
- 5-hour
|
|
36
|
-
- weekly
|
|
37
|
-
- active Codex model id.
|
|
29
|
+
- active account email/account label;
|
|
30
|
+
- subscription plan, such as `Plus` in `/sub` details;
|
|
31
|
+
- 5-hour remaining quota and reset countdown;
|
|
32
|
+
- weekly remaining quota and reset countdown.
|
|
38
33
|
|
|
39
34
|
Example subscription line:
|
|
40
35
|
|
|
41
36
|
```text
|
|
42
|
-
|
|
37
|
+
(user@example.com) R:15%/2H W:20%/3D
|
|
43
38
|
```
|
|
44
39
|
|
|
45
40
|
### OpenCode Go
|
|
46
41
|
|
|
47
|
-
OpenCode Go does not expose usage
|
|
42
|
+
OpenCode Go does not expose a public usage-window API, so the footer shows the active account/key label and accumulated session cost:
|
|
48
43
|
|
|
49
44
|
```text
|
|
50
|
-
OpenCode Go $0.23
|
|
45
|
+
OpenCode Go (OpenCode Go key#1a2b3c4d) $0.23
|
|
51
46
|
```
|
|
52
47
|
|
|
53
48
|
### Z.ai
|
|
54
49
|
|
|
55
|
-
Z.ai (GLM Coding Plan) shows 5-hour rolling and weekly quota
|
|
50
|
+
Z.ai (GLM Coding Plan) shows the active account/key label plus 5-hour rolling and weekly remaining quota with reset countdowns:
|
|
56
51
|
|
|
57
52
|
```text
|
|
58
|
-
R:55
|
|
53
|
+
(Z.ai key#1a2b3c4d) R:55%/2H W:80%/3D
|
|
59
54
|
```
|
|
60
55
|
|
|
61
56
|
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
@@ -71,16 +66,21 @@ When the current model provider is not supported, `pi-sub` clears its subscripti
|
|
|
71
66
|
When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
|
|
72
67
|
|
|
73
68
|
```text
|
|
74
|
-
ACCOUNT PLAN
|
|
75
|
-
* user@example.com Plus
|
|
69
|
+
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
70
|
+
* user@example.com Plus 15%/2H 20%/3D Now
|
|
76
71
|
```
|
|
77
72
|
|
|
78
|
-
For OpenCode Go, `/sub` shows the provider/model and session cost:
|
|
73
|
+
For OpenCode Go, `/sub` shows the provider/model, active account/key label, and session cost:
|
|
79
74
|
|
|
80
75
|
```text
|
|
81
76
|
Provider: OpenCode Go · Model: kimi-k2.6 · Fetched: 14:23
|
|
82
|
-
OpenCode Go does not expose usage windows.
|
|
83
77
|
Session cost: $0.23
|
|
78
|
+
|
|
79
|
+
ACCOUNT PLAN LAST ACTIVITY
|
|
80
|
+
------------------------------------------------------
|
|
81
|
+
* OpenCode Go key#1a2b3c4d Go Now
|
|
82
|
+
|
|
83
|
+
OpenCode Go does not expose usage windows.
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
For Z.ai, `/sub` shows the rolling and weekly quota windows:
|
|
@@ -90,7 +90,7 @@ Provider: Z.ai · Model: glm-5.2 · Fetched: 14:23
|
|
|
90
90
|
|
|
91
91
|
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
92
92
|
------------------------------------------------------------------------
|
|
93
|
-
* Z.ai
|
|
93
|
+
* Z.ai key#1a2b3c4d Pro 55%/2H 80%/3D Now
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
## Refresh behavior
|
|
@@ -110,8 +110,9 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
|
110
110
|
## Requirements and troubleshooting
|
|
111
111
|
|
|
112
112
|
- **OpenAI Codex**: Pi auth must contain an `openai-codex` OAuth entry in `~/.pi/agent/auth.json` or `$PI_CODING_AGENT_DIR/auth.json`. The entry must include `access` and `accountId` fields.
|
|
113
|
-
- **OpenCode Go**: Pi auth must contain an `opencode-go` API key entry (via `/login` or env var). The entry must have a `key` field or an `accountId` field.
|
|
113
|
+
- **OpenCode Go**: Pi auth must contain an `opencode-go` API key entry (via `/login` or env var). The entry must have a `key` field or an `accountId` field. OpenCode Go/Zen usage windows and Zen balance are not shown because no public API is currently documented for those values.
|
|
114
114
|
- **Z.ai**: Pi auth must contain a `zai` entry in `auth.json` with a `key` field (the same API key used for Z.ai model access via `@czottmann/pi-zai-api`). The Z.ai provider must be registered (e.g., `pi install npm:@czottmann/pi-zai-api`).
|
|
115
|
+
- For API-key-only providers, account labels come from stored auth metadata (`email`, `label`, `name`, or `accountId`) when available; otherwise `pi-sub` displays a non-secret SHA-256 key fingerprint such as `Z.ai key#1a2b3c4d`.
|
|
115
116
|
- `pi-sub` redacts auth/token-related errors and never prints credentials.
|
|
116
117
|
|
|
117
118
|
## Design notes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
@@ -19,7 +20,6 @@ type ModelLike = { provider?: string; id?: string } | undefined;
|
|
|
19
20
|
|
|
20
21
|
type UsageApiWindow = {
|
|
21
22
|
used_percent?: number;
|
|
22
|
-
limit_window_seconds?: number;
|
|
23
23
|
reset_at?: number;
|
|
24
24
|
};
|
|
25
25
|
|
|
@@ -38,17 +38,17 @@ type PiAuthEntry = {
|
|
|
38
38
|
expires?: number;
|
|
39
39
|
accountId?: string;
|
|
40
40
|
key?: string;
|
|
41
|
+
email?: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
env?: Record<string, string>;
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
interface UsageWindow {
|
|
44
|
-
used?: number;
|
|
45
|
-
limit?: number;
|
|
46
48
|
percent?: number;
|
|
47
49
|
remaining?: number;
|
|
48
50
|
remainingLabel?: string;
|
|
49
51
|
resetLabel?: string;
|
|
50
|
-
resetsAt?: string | number | Date;
|
|
51
|
-
label?: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
interface SubscriptionAccountSnapshot {
|
|
@@ -58,12 +58,10 @@ interface SubscriptionAccountSnapshot {
|
|
|
58
58
|
plan?: string;
|
|
59
59
|
fiveHour?: UsageWindow;
|
|
60
60
|
weekly?: UsageWindow;
|
|
61
|
-
monthly?: UsageWindow;
|
|
62
61
|
lastActivity?: string;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
interface SubscriptionUsageSnapshot {
|
|
66
|
-
providerId: string;
|
|
67
65
|
providerDisplayName: string;
|
|
68
66
|
accounts: SubscriptionAccountSnapshot[];
|
|
69
67
|
activeAccount?: SubscriptionAccountSnapshot;
|
|
@@ -72,12 +70,11 @@ interface SubscriptionUsageSnapshot {
|
|
|
72
70
|
cost?: number;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
type SubscriptionProviderAdapter = {
|
|
76
74
|
id: string;
|
|
77
75
|
displayName: string;
|
|
78
|
-
isModelSupported(model: ModelLike): boolean;
|
|
79
76
|
fetchUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot>;
|
|
80
|
-
}
|
|
77
|
+
};
|
|
81
78
|
|
|
82
79
|
interface State {
|
|
83
80
|
model?: ModelLike;
|
|
@@ -140,6 +137,46 @@ function accountFromPiAuth(entry: PiAuthEntry): SubscriptionAccountSnapshot {
|
|
|
140
137
|
};
|
|
141
138
|
}
|
|
142
139
|
|
|
140
|
+
function firstString(...values: unknown[]): string | undefined {
|
|
141
|
+
for (const value of values) {
|
|
142
|
+
if (typeof value !== "string") continue;
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
if (trimmed.length > 0) return trimmed;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function authEntryLabel(entry: PiAuthEntry | undefined): string | undefined {
|
|
150
|
+
return firstString(entry?.email, entry?.label, entry?.name, entry?.accountId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function keyFingerprint(key: string | undefined): string | undefined {
|
|
154
|
+
if (!key) return undefined;
|
|
155
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 8);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function authAccountLabel(providerLabel: string, entry: PiAuthEntry | undefined): string {
|
|
159
|
+
const label = authEntryLabel(entry);
|
|
160
|
+
if (label) return label;
|
|
161
|
+
const fingerprint = keyFingerprint(entry?.key);
|
|
162
|
+
return fingerprint ? `${providerLabel} key#${fingerprint}` : `${providerLabel} account`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function authAccountSnapshot(providerLabel: string, entry: PiAuthEntry | undefined, defaults: Partial<SubscriptionAccountSnapshot> = {}): SubscriptionAccountSnapshot {
|
|
166
|
+
return {
|
|
167
|
+
id: firstString(entry?.accountId),
|
|
168
|
+
isActive: true,
|
|
169
|
+
accountLabel: authAccountLabel(providerLabel, entry),
|
|
170
|
+
lastActivity: "Now",
|
|
171
|
+
...defaults,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatFooterAccount(account: SubscriptionAccountSnapshot | undefined): string | undefined {
|
|
176
|
+
const label = firstString(account?.accountLabel);
|
|
177
|
+
return label ? `(${label})` : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
143
180
|
function getCodexAccountId(entry: PiAuthEntry | undefined): string | undefined {
|
|
144
181
|
if (!entry) return undefined;
|
|
145
182
|
if (typeof entry.accountId === "string" && entry.accountId.length > 0) return entry.accountId;
|
|
@@ -201,8 +238,6 @@ function usageWindowFromApi(window: UsageApiWindow | undefined): UsageWindow | u
|
|
|
201
238
|
remaining,
|
|
202
239
|
remainingLabel,
|
|
203
240
|
resetLabel,
|
|
204
|
-
resetsAt: window.reset_at,
|
|
205
|
-
label: remainingLabel ? `${remaining}% (${remainingLabel})` : `${remaining}%`,
|
|
206
241
|
};
|
|
207
242
|
}
|
|
208
243
|
|
|
@@ -225,7 +260,6 @@ function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
|
225
260
|
if (!window || typeof window !== "object" || typeof window.used_percent !== "number") return undefined;
|
|
226
261
|
return {
|
|
227
262
|
used_percent: window.used_percent,
|
|
228
|
-
limit_window_seconds: typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : undefined,
|
|
229
263
|
reset_at: typeof window.reset_at === "number" ? window.reset_at : undefined,
|
|
230
264
|
};
|
|
231
265
|
};
|
|
@@ -244,29 +278,30 @@ async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
|
244
278
|
return { ...entry, accountId };
|
|
245
279
|
}
|
|
246
280
|
|
|
247
|
-
async function readOpenCodeGoAuth(): Promise<
|
|
281
|
+
async function readOpenCodeGoAuth(): Promise<SubscriptionAccountSnapshot> {
|
|
248
282
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
249
283
|
const entry = auth[OPC_PROVIDER];
|
|
250
284
|
if (!entry?.key && !entry?.accountId) throw new Error("Missing opencode-go API key or accountId in Pi auth");
|
|
251
|
-
|
|
252
|
-
return { accountId: typeof entry.accountId === "string" ? entry.accountId : undefined };
|
|
285
|
+
return authAccountSnapshot("OpenCode Go", entry, { plan: "Go" });
|
|
253
286
|
}
|
|
254
287
|
|
|
255
|
-
async function readZaiAuth(): Promise<string> {
|
|
288
|
+
async function readZaiAuth(): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
256
289
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
257
290
|
const entry = auth[ZAI_PROVIDER];
|
|
258
291
|
if (!entry?.key) throw new Error("Missing zai API key in Pi auth");
|
|
259
|
-
return entry.key;
|
|
292
|
+
return { key: entry.key, account: authAccountSnapshot("Z.ai", entry) };
|
|
260
293
|
}
|
|
261
294
|
|
|
262
295
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
296
|
+
const accountId = getCodexAccountId(entry) ?? entry.accountId;
|
|
297
|
+
if (!accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
263
298
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
264
299
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
265
300
|
const response = await fetch(USAGE_ENDPOINT, {
|
|
266
301
|
headers: {
|
|
267
302
|
Accept: "application/json",
|
|
268
303
|
Authorization: `Bearer ${entry.access}`,
|
|
269
|
-
"ChatGPT-Account-Id":
|
|
304
|
+
"ChatGPT-Account-Id": accountId,
|
|
270
305
|
"User-Agent": "pi-sub/0.1.0",
|
|
271
306
|
},
|
|
272
307
|
signal: combinedSignal,
|
|
@@ -293,7 +328,6 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
293
328
|
const usage = await fetchUsageFromPiAuth(entry, signal);
|
|
294
329
|
activeAccount = mergeUsageIntoAccount(activeAccount, usage);
|
|
295
330
|
return {
|
|
296
|
-
providerId: CODEX_PROVIDER,
|
|
297
331
|
providerDisplayName: "Codex",
|
|
298
332
|
accounts: [activeAccount],
|
|
299
333
|
activeAccount,
|
|
@@ -301,7 +335,6 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
301
335
|
};
|
|
302
336
|
} catch (error) {
|
|
303
337
|
return {
|
|
304
|
-
providerId: CODEX_PROVIDER,
|
|
305
338
|
providerDisplayName: "Codex",
|
|
306
339
|
accounts: [],
|
|
307
340
|
fetchedAt: Date.now(),
|
|
@@ -312,16 +345,15 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
312
345
|
|
|
313
346
|
async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
314
347
|
try {
|
|
315
|
-
await readOpenCodeGoAuth();
|
|
348
|
+
const account = await readOpenCodeGoAuth();
|
|
316
349
|
return {
|
|
317
|
-
providerId: OPC_PROVIDER,
|
|
318
350
|
providerDisplayName: "OpenCode Go",
|
|
319
|
-
accounts: [],
|
|
351
|
+
accounts: [account],
|
|
352
|
+
activeAccount: account,
|
|
320
353
|
fetchedAt: Date.now(),
|
|
321
354
|
};
|
|
322
355
|
} catch (error) {
|
|
323
356
|
return {
|
|
324
|
-
providerId: OPC_PROVIDER,
|
|
325
357
|
providerDisplayName: "OpenCode Go",
|
|
326
358
|
accounts: [],
|
|
327
359
|
fetchedAt: Date.now(),
|
|
@@ -343,6 +375,10 @@ interface ZaiLimitEntry {
|
|
|
343
375
|
interface ZaiUsageApiResponse {
|
|
344
376
|
data?: {
|
|
345
377
|
limits?: ZaiLimitEntry[];
|
|
378
|
+
planName?: string;
|
|
379
|
+
plan?: string;
|
|
380
|
+
plan_type?: string;
|
|
381
|
+
packageName?: string;
|
|
346
382
|
};
|
|
347
383
|
}
|
|
348
384
|
|
|
@@ -365,14 +401,17 @@ function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
|
365
401
|
remaining,
|
|
366
402
|
remainingLabel,
|
|
367
403
|
resetLabel,
|
|
368
|
-
resetsAt: limit.nextResetTime,
|
|
369
|
-
label: remainingLabel ? `${remaining}% (${remainingLabel})` : `${remaining}%`,
|
|
370
404
|
};
|
|
371
405
|
}
|
|
372
406
|
|
|
407
|
+
function zaiPlanLabel(response: ZaiUsageApiResponse): string | undefined {
|
|
408
|
+
const data = response.data;
|
|
409
|
+
return planLabel(firstString(data?.planName, data?.plan, data?.plan_type, data?.packageName));
|
|
410
|
+
}
|
|
411
|
+
|
|
373
412
|
async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
374
413
|
try {
|
|
375
|
-
const apiKey = await readZaiAuth();
|
|
414
|
+
const { key: apiKey, account: authAccount } = await readZaiAuth();
|
|
376
415
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
377
416
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
378
417
|
|
|
@@ -408,15 +447,13 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
|
|
|
408
447
|
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
409
448
|
|
|
410
449
|
const account: SubscriptionAccountSnapshot = {
|
|
411
|
-
|
|
412
|
-
|
|
450
|
+
...authAccount,
|
|
451
|
+
plan: zaiPlanLabel(parsed) ?? authAccount.plan,
|
|
413
452
|
fiveHour,
|
|
414
453
|
weekly,
|
|
415
|
-
lastActivity: "Now",
|
|
416
454
|
};
|
|
417
455
|
|
|
418
456
|
return {
|
|
419
|
-
providerId: ZAI_PROVIDER,
|
|
420
457
|
providerDisplayName: "Z.ai",
|
|
421
458
|
accounts: [account],
|
|
422
459
|
activeAccount: account,
|
|
@@ -424,7 +461,6 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
|
|
|
424
461
|
};
|
|
425
462
|
} catch (error) {
|
|
426
463
|
return {
|
|
427
|
-
providerId: ZAI_PROVIDER,
|
|
428
464
|
providerDisplayName: "Z.ai",
|
|
429
465
|
accounts: [],
|
|
430
466
|
fetchedAt: Date.now(),
|
|
@@ -433,31 +469,11 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
|
|
|
433
469
|
}
|
|
434
470
|
}
|
|
435
471
|
|
|
436
|
-
const codexAdapter: SubscriptionProviderAdapter = {
|
|
437
|
-
id: CODEX_PROVIDER,
|
|
438
|
-
displayName: "Codex",
|
|
439
|
-
isModelSupported: isCodexModel,
|
|
440
|
-
fetchUsage: fetchCodexUsage,
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
const openCodeGoAdapter: SubscriptionProviderAdapter = {
|
|
444
|
-
id: OPC_PROVIDER,
|
|
445
|
-
displayName: "OpenCode Go",
|
|
446
|
-
isModelSupported: isOpenCodeGoModel,
|
|
447
|
-
fetchUsage: fetchOpenCodeGoUsage,
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const zaiAdapter: SubscriptionProviderAdapter = {
|
|
451
|
-
id: ZAI_PROVIDER,
|
|
452
|
-
displayName: "Z.ai",
|
|
453
|
-
isModelSupported: isZaiModel,
|
|
454
|
-
fetchUsage: fetchZaiUsage,
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const adapters = [codexAdapter, openCodeGoAdapter, zaiAdapter];
|
|
458
|
-
|
|
459
472
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
460
|
-
|
|
473
|
+
if (isCodexModel(model)) return { id: CODEX_PROVIDER, displayName: "Codex", fetchUsage: fetchCodexUsage };
|
|
474
|
+
if (isOpenCodeGoModel(model)) return { id: OPC_PROVIDER, displayName: "OpenCode Go", fetchUsage: fetchOpenCodeGoUsage };
|
|
475
|
+
if (isZaiModel(model)) return { id: ZAI_PROVIDER, displayName: "Z.ai", fetchUsage: fetchZaiUsage };
|
|
476
|
+
return undefined;
|
|
461
477
|
}
|
|
462
478
|
|
|
463
479
|
function formatRemaining(window: UsageWindow | undefined): string {
|
|
@@ -468,20 +484,10 @@ function formatRemaining(window: UsageWindow | undefined): string {
|
|
|
468
484
|
return "?";
|
|
469
485
|
}
|
|
470
486
|
|
|
471
|
-
function formatWindow(window: UsageWindow | undefined, _compact = true): string {
|
|
472
|
-
if (!window) return "?";
|
|
473
|
-
if (window.percent !== undefined && window.resetLabel) return `${window.percent}% (${window.resetLabel})`;
|
|
474
|
-
if (window.label) return window.label;
|
|
475
|
-
if (window.percent !== undefined) return `${window.percent}%`;
|
|
476
|
-
if (window.used !== undefined && window.limit !== undefined) return `${window.used}/${window.limit}`;
|
|
477
|
-
return "?";
|
|
478
|
-
}
|
|
479
|
-
|
|
480
487
|
function minRemaining(account: SubscriptionAccountSnapshot | undefined): number {
|
|
481
488
|
const values: number[] = [];
|
|
482
489
|
if (account?.fiveHour?.remaining !== undefined) values.push(account.fiveHour.remaining);
|
|
483
490
|
if (account?.weekly?.remaining !== undefined) values.push(account.weekly.remaining);
|
|
484
|
-
if (account?.monthly?.remaining !== undefined) values.push(account.monthly.remaining);
|
|
485
491
|
if (values.length === 0) return 100;
|
|
486
492
|
return Math.min(...values);
|
|
487
493
|
}
|
|
@@ -491,7 +497,6 @@ function windowSegments(account: SubscriptionAccountSnapshot | undefined): strin
|
|
|
491
497
|
const segments: string[] = [];
|
|
492
498
|
if (account.fiveHour) segments.push(`R:${formatRemaining(account.fiveHour)}`);
|
|
493
499
|
if (account.weekly) segments.push(`W:${formatRemaining(account.weekly)}`);
|
|
494
|
-
if (account.monthly) segments.push(`M:${formatRemaining(account.monthly)}`);
|
|
495
500
|
return segments;
|
|
496
501
|
}
|
|
497
502
|
|
|
@@ -521,9 +526,11 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
|
521
526
|
color = "warning";
|
|
522
527
|
} else {
|
|
523
528
|
const account = snapshot.activeAccount;
|
|
524
|
-
const
|
|
529
|
+
const windowParts = windowSegments(account);
|
|
530
|
+
const accountPart = formatFooterAccount(account);
|
|
531
|
+
const segments = accountPart ? [accountPart, ...windowParts] : [...windowParts];
|
|
525
532
|
const cost = snapshot.cost;
|
|
526
|
-
const hasWindows =
|
|
533
|
+
const hasWindows = windowParts.length > 0;
|
|
527
534
|
if (cost !== undefined && cost > 0) segments.push(`$${cost.toFixed(2)}`);
|
|
528
535
|
if (segments.length === 0) {
|
|
529
536
|
line = `Sub ${state.adapter.displayName}`;
|
|
@@ -629,12 +636,8 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
629
636
|
|
|
630
637
|
const hasFiveHour = snapshot.accounts.some((a) => a.fiveHour);
|
|
631
638
|
const hasWeekly = snapshot.accounts.some((a) => a.weekly);
|
|
632
|
-
const hasMonthly = snapshot.accounts.some((a) => a.monthly);
|
|
633
|
-
|
|
634
639
|
if (hasFiveHour) columns.push({ key: "five", label: "ROLLING", get: (a) => formatRemaining(a.fiveHour) });
|
|
635
640
|
if (hasWeekly) columns.push({ key: "weekly", label: "WEEKLY", get: (a) => formatRemaining(a.weekly) });
|
|
636
|
-
if (hasMonthly) columns.push({ key: "monthly", label: "MONTHLY", get: (a) => formatRemaining(a.monthly) });
|
|
637
|
-
|
|
638
641
|
const rows = snapshot.accounts.map((account) => ({
|
|
639
642
|
active: account.isActive ? "*" : " ",
|
|
640
643
|
snapshot: account,
|
|
@@ -654,7 +657,11 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
654
657
|
});
|
|
655
658
|
|
|
656
659
|
const costLine = snapshot.cost !== undefined ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
|
|
657
|
-
|
|
660
|
+
const lines = [`Provider: ${snapshot.providerDisplayName} · Model: ${state.model?.id ?? "unknown-model"} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}${costLine}`, "", header, sep, ...body];
|
|
661
|
+
if (!hasFiveHour && !hasWeekly) {
|
|
662
|
+
lines.push("", `${snapshot.providerDisplayName} does not expose usage windows.`);
|
|
663
|
+
}
|
|
664
|
+
return lines.join("\n");
|
|
658
665
|
}
|
|
659
666
|
|
|
660
667
|
export default function (pi: ExtensionAPI) {
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-sub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Pi extension showing subscription usage for supported model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
-
"homepage": "https://github.com/bacnh85/
|
|
10
|
+
"homepage": "https://github.com/bacnh85/pi-extensions#readme",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/bacnh85/
|
|
14
|
-
"directory": "
|
|
13
|
+
"url": "git+https://github.com/bacnh85/pi-extensions.git",
|
|
14
|
+
"directory": "pi-sub"
|
|
15
15
|
},
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/bacnh85/
|
|
17
|
+
"url": "https://github.com/bacnh85/pi-extensions/issues"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"pi-package",
|
|
@@ -26,14 +26,16 @@
|
|
|
26
26
|
],
|
|
27
27
|
"files": [
|
|
28
28
|
"README.md",
|
|
29
|
-
"
|
|
29
|
+
"extensions/"
|
|
30
30
|
],
|
|
31
31
|
"pi": {
|
|
32
32
|
"extensions": [
|
|
33
|
-
"./index.ts"
|
|
33
|
+
"./extensions/index.ts"
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
|
-
"engines": {
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20.3.0"
|
|
38
|
+
},
|
|
37
39
|
"peerDependencies": {
|
|
38
40
|
"@earendil-works/pi-coding-agent": "*",
|
|
39
41
|
"@earendil-works/pi-tui": "*"
|