@bacnh85/pi-sub 0.1.8 → 0.1.9
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 +21 -16
- package/index.ts +78 -15
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -30,32 +30,31 @@ pi -e ./extensions/pi-sub
|
|
|
30
30
|
|
|
31
31
|
The footer status appears after Pi's built-in status/token usage line and includes:
|
|
32
32
|
|
|
33
|
-
- active account email;
|
|
34
|
-
- subscription plan, such as `Plus
|
|
35
|
-
- 5-hour
|
|
36
|
-
- weekly
|
|
37
|
-
- active Codex model id.
|
|
33
|
+
- active account email/account label;
|
|
34
|
+
- subscription plan, such as `Plus` in `/sub` details;
|
|
35
|
+
- 5-hour remaining quota and reset countdown;
|
|
36
|
+
- weekly remaining quota and reset countdown.
|
|
38
37
|
|
|
39
38
|
Example subscription line:
|
|
40
39
|
|
|
41
40
|
```text
|
|
42
|
-
|
|
41
|
+
(user@example.com) R:15%/2H W:20%/3D
|
|
43
42
|
```
|
|
44
43
|
|
|
45
44
|
### OpenCode Go
|
|
46
45
|
|
|
47
|
-
OpenCode Go does not expose usage
|
|
46
|
+
OpenCode Go does not expose a public usage-window API, so the footer shows the active account/key label and accumulated session cost:
|
|
48
47
|
|
|
49
48
|
```text
|
|
50
|
-
OpenCode Go $0.23
|
|
49
|
+
OpenCode Go (OpenCode Go key#1a2b3c4d) $0.23
|
|
51
50
|
```
|
|
52
51
|
|
|
53
52
|
### Z.ai
|
|
54
53
|
|
|
55
|
-
Z.ai (GLM Coding Plan) shows 5-hour rolling and weekly quota
|
|
54
|
+
Z.ai (GLM Coding Plan) shows the active account/key label plus 5-hour rolling and weekly remaining quota with reset countdowns:
|
|
56
55
|
|
|
57
56
|
```text
|
|
58
|
-
R:55
|
|
57
|
+
(Z.ai key#1a2b3c4d) R:55%/2H W:80%/3D
|
|
59
58
|
```
|
|
60
59
|
|
|
61
60
|
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
@@ -71,16 +70,21 @@ When the current model provider is not supported, `pi-sub` clears its subscripti
|
|
|
71
70
|
When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
|
|
72
71
|
|
|
73
72
|
```text
|
|
74
|
-
ACCOUNT PLAN
|
|
75
|
-
* user@example.com Plus
|
|
73
|
+
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
74
|
+
* user@example.com Plus 15%/2H 20%/3D Now
|
|
76
75
|
```
|
|
77
76
|
|
|
78
|
-
For OpenCode Go, `/sub` shows the provider/model and session cost:
|
|
77
|
+
For OpenCode Go, `/sub` shows the provider/model, active account/key label, and session cost:
|
|
79
78
|
|
|
80
79
|
```text
|
|
81
80
|
Provider: OpenCode Go · Model: kimi-k2.6 · Fetched: 14:23
|
|
82
|
-
OpenCode Go does not expose usage windows.
|
|
83
81
|
Session cost: $0.23
|
|
82
|
+
|
|
83
|
+
ACCOUNT PLAN LAST ACTIVITY
|
|
84
|
+
------------------------------------------------------
|
|
85
|
+
* OpenCode Go key#1a2b3c4d Go Now
|
|
86
|
+
|
|
87
|
+
OpenCode Go does not expose usage windows.
|
|
84
88
|
```
|
|
85
89
|
|
|
86
90
|
For Z.ai, `/sub` shows the rolling and weekly quota windows:
|
|
@@ -90,7 +94,7 @@ Provider: Z.ai · Model: glm-5.2 · Fetched: 14:23
|
|
|
90
94
|
|
|
91
95
|
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
92
96
|
------------------------------------------------------------------------
|
|
93
|
-
* Z.ai
|
|
97
|
+
* Z.ai key#1a2b3c4d Pro 55%/2H 80%/3D Now
|
|
94
98
|
```
|
|
95
99
|
|
|
96
100
|
## Refresh behavior
|
|
@@ -110,8 +114,9 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
|
110
114
|
## Requirements and troubleshooting
|
|
111
115
|
|
|
112
116
|
- **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.
|
|
117
|
+
- **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
118
|
- **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`).
|
|
119
|
+
- 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
120
|
- `pi-sub` redacts auth/token-related errors and never prints credentials.
|
|
116
121
|
|
|
117
122
|
## Design notes
|
package/index.ts
CHANGED
|
@@ -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";
|
|
@@ -38,6 +39,10 @@ type PiAuthEntry = {
|
|
|
38
39
|
expires?: number;
|
|
39
40
|
accountId?: string;
|
|
40
41
|
key?: string;
|
|
42
|
+
email?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
env?: Record<string, string>;
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
interface UsageWindow {
|
|
@@ -140,6 +145,46 @@ function accountFromPiAuth(entry: PiAuthEntry): SubscriptionAccountSnapshot {
|
|
|
140
145
|
};
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
function firstString(...values: unknown[]): string | undefined {
|
|
149
|
+
for (const value of values) {
|
|
150
|
+
if (typeof value !== "string") continue;
|
|
151
|
+
const trimmed = value.trim();
|
|
152
|
+
if (trimmed.length > 0) return trimmed;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function authEntryLabel(entry: PiAuthEntry | undefined): string | undefined {
|
|
158
|
+
return firstString(entry?.email, entry?.label, entry?.name, entry?.accountId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function keyFingerprint(key: string | undefined): string | undefined {
|
|
162
|
+
if (!key) return undefined;
|
|
163
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 8);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function authAccountLabel(providerLabel: string, entry: PiAuthEntry | undefined): string {
|
|
167
|
+
const label = authEntryLabel(entry);
|
|
168
|
+
if (label) return label;
|
|
169
|
+
const fingerprint = keyFingerprint(entry?.key);
|
|
170
|
+
return fingerprint ? `${providerLabel} key#${fingerprint}` : `${providerLabel} account`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function authAccountSnapshot(providerLabel: string, entry: PiAuthEntry | undefined, defaults: Partial<SubscriptionAccountSnapshot> = {}): SubscriptionAccountSnapshot {
|
|
174
|
+
return {
|
|
175
|
+
id: firstString(entry?.accountId),
|
|
176
|
+
isActive: true,
|
|
177
|
+
accountLabel: authAccountLabel(providerLabel, entry),
|
|
178
|
+
lastActivity: "Now",
|
|
179
|
+
...defaults,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatFooterAccount(account: SubscriptionAccountSnapshot | undefined): string | undefined {
|
|
184
|
+
const label = firstString(account?.accountLabel);
|
|
185
|
+
return label ? `(${label})` : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
143
188
|
function getCodexAccountId(entry: PiAuthEntry | undefined): string | undefined {
|
|
144
189
|
if (!entry) return undefined;
|
|
145
190
|
if (typeof entry.accountId === "string" && entry.accountId.length > 0) return entry.accountId;
|
|
@@ -244,29 +289,32 @@ async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
|
244
289
|
return { ...entry, accountId };
|
|
245
290
|
}
|
|
246
291
|
|
|
247
|
-
async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string }> {
|
|
292
|
+
async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string; account: SubscriptionAccountSnapshot }> {
|
|
248
293
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
249
294
|
const entry = auth[OPC_PROVIDER];
|
|
250
295
|
if (!entry?.key && !entry?.accountId) throw new Error("Missing opencode-go API key or accountId in Pi auth");
|
|
251
|
-
|
|
252
|
-
|
|
296
|
+
const account = authAccountSnapshot("OpenCode Go", entry, { plan: "Go" });
|
|
297
|
+
if (typeof entry.key === "string") return { key: entry.key, account };
|
|
298
|
+
return { accountId: typeof entry.accountId === "string" ? entry.accountId : undefined, account };
|
|
253
299
|
}
|
|
254
300
|
|
|
255
|
-
async function readZaiAuth(): Promise<string> {
|
|
301
|
+
async function readZaiAuth(): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
256
302
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
257
303
|
const entry = auth[ZAI_PROVIDER];
|
|
258
304
|
if (!entry?.key) throw new Error("Missing zai API key in Pi auth");
|
|
259
|
-
return entry.key;
|
|
305
|
+
return { key: entry.key, account: authAccountSnapshot("Z.ai", entry) };
|
|
260
306
|
}
|
|
261
307
|
|
|
262
308
|
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
309
|
+
const accountId = getCodexAccountId(entry) ?? entry.accountId;
|
|
310
|
+
if (!accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
263
311
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
264
312
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
265
313
|
const response = await fetch(USAGE_ENDPOINT, {
|
|
266
314
|
headers: {
|
|
267
315
|
Accept: "application/json",
|
|
268
316
|
Authorization: `Bearer ${entry.access}`,
|
|
269
|
-
"ChatGPT-Account-Id":
|
|
317
|
+
"ChatGPT-Account-Id": accountId,
|
|
270
318
|
"User-Agent": "pi-sub/0.1.0",
|
|
271
319
|
},
|
|
272
320
|
signal: combinedSignal,
|
|
@@ -312,11 +360,12 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
312
360
|
|
|
313
361
|
async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
314
362
|
try {
|
|
315
|
-
await readOpenCodeGoAuth();
|
|
363
|
+
const { account } = await readOpenCodeGoAuth();
|
|
316
364
|
return {
|
|
317
365
|
providerId: OPC_PROVIDER,
|
|
318
366
|
providerDisplayName: "OpenCode Go",
|
|
319
|
-
accounts: [],
|
|
367
|
+
accounts: [account],
|
|
368
|
+
activeAccount: account,
|
|
320
369
|
fetchedAt: Date.now(),
|
|
321
370
|
};
|
|
322
371
|
} catch (error) {
|
|
@@ -343,6 +392,10 @@ interface ZaiLimitEntry {
|
|
|
343
392
|
interface ZaiUsageApiResponse {
|
|
344
393
|
data?: {
|
|
345
394
|
limits?: ZaiLimitEntry[];
|
|
395
|
+
planName?: string;
|
|
396
|
+
plan?: string;
|
|
397
|
+
plan_type?: string;
|
|
398
|
+
packageName?: string;
|
|
346
399
|
};
|
|
347
400
|
}
|
|
348
401
|
|
|
@@ -370,9 +423,14 @@ function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
|
370
423
|
};
|
|
371
424
|
}
|
|
372
425
|
|
|
426
|
+
function zaiPlanLabel(response: ZaiUsageApiResponse): string | undefined {
|
|
427
|
+
const data = response.data;
|
|
428
|
+
return planLabel(firstString(data?.planName, data?.plan, data?.plan_type, data?.packageName));
|
|
429
|
+
}
|
|
430
|
+
|
|
373
431
|
async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
374
432
|
try {
|
|
375
|
-
const apiKey = await readZaiAuth();
|
|
433
|
+
const { key: apiKey, account: authAccount } = await readZaiAuth();
|
|
376
434
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
377
435
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
378
436
|
|
|
@@ -408,11 +466,10 @@ async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSna
|
|
|
408
466
|
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
409
467
|
|
|
410
468
|
const account: SubscriptionAccountSnapshot = {
|
|
411
|
-
|
|
412
|
-
|
|
469
|
+
...authAccount,
|
|
470
|
+
plan: zaiPlanLabel(parsed) ?? authAccount.plan,
|
|
413
471
|
fiveHour,
|
|
414
472
|
weekly,
|
|
415
|
-
lastActivity: "Now",
|
|
416
473
|
};
|
|
417
474
|
|
|
418
475
|
return {
|
|
@@ -521,9 +578,11 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
|
521
578
|
color = "warning";
|
|
522
579
|
} else {
|
|
523
580
|
const account = snapshot.activeAccount;
|
|
524
|
-
const
|
|
581
|
+
const windowParts = windowSegments(account);
|
|
582
|
+
const accountPart = formatFooterAccount(account);
|
|
583
|
+
const segments = accountPart ? [accountPart, ...windowParts] : [...windowParts];
|
|
525
584
|
const cost = snapshot.cost;
|
|
526
|
-
const hasWindows =
|
|
585
|
+
const hasWindows = windowParts.length > 0;
|
|
527
586
|
if (cost !== undefined && cost > 0) segments.push(`$${cost.toFixed(2)}`);
|
|
528
587
|
if (segments.length === 0) {
|
|
529
588
|
line = `Sub ${state.adapter.displayName}`;
|
|
@@ -654,7 +713,11 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
654
713
|
});
|
|
655
714
|
|
|
656
715
|
const costLine = snapshot.cost !== undefined ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
|
|
657
|
-
|
|
716
|
+
const lines = [`Provider: ${snapshot.providerDisplayName} · Model: ${state.model?.id ?? "unknown-model"} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}${costLine}`, "", header, sep, ...body];
|
|
717
|
+
if (!hasFiveHour && !hasWeekly && !hasMonthly) {
|
|
718
|
+
lines.push("", `${snapshot.providerDisplayName} does not expose usage windows.`);
|
|
719
|
+
}
|
|
720
|
+
return lines.join("\n");
|
|
658
721
|
}
|
|
659
722
|
|
|
660
723
|
export default function (pi: ExtensionAPI) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-sub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Pi extension showing subscription usage for supported model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"./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": "*"
|