@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.
Files changed (3) hide show
  1. package/README.md +21 -16
  2. package/index.ts +78 -15
  3. 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 usage percentage and reset time;
36
- - weekly usage percentage and reset time/date;
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
- Sub · Plus · user@example.com · 5H 85% (18:12) · W 80% (08:00 on 2 Jul) · gpt-5-codex
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 windows, so the footer shows only the accumulated session cost:
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 percentages with reset times:
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%(18:12) W:80%(08:00 on 2 Jul)
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 5H USAGE WEEKLY USAGE LAST ACTIVITY
75
- * user@example.com Plus 85% (18:12) 80% (08:00 on 2 Jul) Now
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 subscription ? 55%/18:12 80%/08:00 on 2 Jul Now
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
- if (typeof entry.key === "string") return { key: entry.key };
252
- return { accountId: typeof entry.accountId === "string" ? entry.accountId : undefined };
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": getCodexAccountId(entry) ?? entry.accountId,
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
- isActive: true,
412
- accountLabel: "Z.ai subscription",
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 segments = windowSegments(account);
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 = segments.length > 0;
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
- return [`Provider: ${snapshot.providerDisplayName} · Model: ${state.model?.id ?? "unknown-model"} · Fetched: ${new Date(snapshot.fetchedAt).toLocaleTimeString()}${costLine}`, "", header, sep, ...body].join("\n");
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.8",
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": { "node": ">=20.3.0" },
36
+ "engines": {
37
+ "node": ">=20.3.0"
38
+ },
37
39
  "peerDependencies": {
38
40
  "@earendil-works/pi-coding-agent": "*",
39
41
  "@earendil-works/pi-tui": "*"