@bacnh85/pi-sub 0.1.7 → 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 +38 -14
- package/index.ts +197 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Pi extension that shows subscription usage for the currently selected supported model provider.
|
|
4
4
|
|
|
5
|
-
Supports OpenAI Codex (`openai-codex`) with live usage windows from ChatGPT's usage endpoint,
|
|
5
|
+
Supports OpenAI Codex (`openai-codex`) with live usage windows from ChatGPT's usage endpoint, OpenCode Go (`opencode-go`) with session cost tracking, and Z.ai (`zai`) with GLM Coding Plan quota monitoring. Displays a subscription footer status after Pi's built-in status/token usage line.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -30,24 +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
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Z.ai
|
|
53
|
+
|
|
54
|
+
Z.ai (GLM Coding Plan) shows the active account/key label plus 5-hour rolling and weekly remaining quota with reset countdowns:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
(Z.ai key#1a2b3c4d) R:55%/2H W:80%/3D
|
|
51
58
|
```
|
|
52
59
|
|
|
53
60
|
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
@@ -63,16 +70,31 @@ When the current model provider is not supported, `pi-sub` clears its subscripti
|
|
|
63
70
|
When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
|
|
64
71
|
|
|
65
72
|
```text
|
|
66
|
-
ACCOUNT PLAN
|
|
67
|
-
* user@example.com Plus
|
|
73
|
+
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
74
|
+
* user@example.com Plus 15%/2H 20%/3D Now
|
|
68
75
|
```
|
|
69
76
|
|
|
70
|
-
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:
|
|
71
78
|
|
|
72
79
|
```text
|
|
73
80
|
Provider: OpenCode Go · Model: kimi-k2.6 · Fetched: 14:23
|
|
74
|
-
OpenCode Go does not expose usage windows.
|
|
75
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.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For Z.ai, `/sub` shows the rolling and weekly quota windows:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
Provider: Z.ai · Model: glm-5.2 · Fetched: 14:23
|
|
94
|
+
|
|
95
|
+
ACCOUNT PLAN ROLLING WEEKLY LAST ACTIVITY
|
|
96
|
+
------------------------------------------------------------------------
|
|
97
|
+
* Z.ai key#1a2b3c4d Pro 55%/2H 80%/3D Now
|
|
76
98
|
```
|
|
77
99
|
|
|
78
100
|
## Refresh behavior
|
|
@@ -92,7 +114,9 @@ Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
|
92
114
|
## Requirements and troubleshooting
|
|
93
115
|
|
|
94
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.
|
|
95
|
-
- **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.
|
|
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`.
|
|
96
120
|
- `pi-sub` redacts auth/token-related errors and never prints credentials.
|
|
97
121
|
|
|
98
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";
|
|
@@ -12,6 +13,8 @@ const REFRESH_TTL_MS = 30_000;
|
|
|
12
13
|
const REFRESH_DEBOUNCE_MS = 2_000;
|
|
13
14
|
const CODEX_PROVIDER = "openai-codex";
|
|
14
15
|
const OPC_PROVIDER = "opencode-go";
|
|
16
|
+
const ZAI_PROVIDER = "zai";
|
|
17
|
+
const ZAI_USAGE_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
15
18
|
|
|
16
19
|
type ModelLike = { provider?: string; id?: string } | undefined;
|
|
17
20
|
|
|
@@ -36,6 +39,10 @@ type PiAuthEntry = {
|
|
|
36
39
|
expires?: number;
|
|
37
40
|
accountId?: string;
|
|
38
41
|
key?: string;
|
|
42
|
+
email?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
env?: Record<string, string>;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
interface UsageWindow {
|
|
@@ -98,6 +105,10 @@ function isOpenCodeGoModel(model: ModelLike): boolean {
|
|
|
98
105
|
return (model?.provider?.toLowerCase() ?? "") === OPC_PROVIDER;
|
|
99
106
|
}
|
|
100
107
|
|
|
108
|
+
function isZaiModel(model: ModelLike): boolean {
|
|
109
|
+
return (model?.provider?.toLowerCase() ?? "") === ZAI_PROVIDER;
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
function piAuthPath(): string {
|
|
102
113
|
const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
103
114
|
return path.join(configDir, "auth.json");
|
|
@@ -134,6 +145,46 @@ function accountFromPiAuth(entry: PiAuthEntry): SubscriptionAccountSnapshot {
|
|
|
134
145
|
};
|
|
135
146
|
}
|
|
136
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
|
+
|
|
137
188
|
function getCodexAccountId(entry: PiAuthEntry | undefined): string | undefined {
|
|
138
189
|
if (!entry) return undefined;
|
|
139
190
|
if (typeof entry.accountId === "string" && entry.accountId.length > 0) return entry.accountId;
|
|
@@ -238,22 +289,32 @@ async function readPiCodexAuth(): Promise<PiAuthEntry & { accountId: string }> {
|
|
|
238
289
|
return { ...entry, accountId };
|
|
239
290
|
}
|
|
240
291
|
|
|
241
|
-
async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string }> {
|
|
292
|
+
async function readOpenCodeGoAuth(): Promise<{ key?: string; accountId?: string; account: SubscriptionAccountSnapshot }> {
|
|
242
293
|
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
243
294
|
const entry = auth[OPC_PROVIDER];
|
|
244
295
|
if (!entry?.key && !entry?.accountId) throw new Error("Missing opencode-go API key or accountId in Pi auth");
|
|
245
|
-
|
|
246
|
-
|
|
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 };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function readZaiAuth(): Promise<{ key: string; account: SubscriptionAccountSnapshot }> {
|
|
302
|
+
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
303
|
+
const entry = auth[ZAI_PROVIDER];
|
|
304
|
+
if (!entry?.key) throw new Error("Missing zai API key in Pi auth");
|
|
305
|
+
return { key: entry.key, account: authAccountSnapshot("Z.ai", entry) };
|
|
247
306
|
}
|
|
248
307
|
|
|
249
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");
|
|
250
311
|
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
251
312
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
252
313
|
const response = await fetch(USAGE_ENDPOINT, {
|
|
253
314
|
headers: {
|
|
254
315
|
Accept: "application/json",
|
|
255
316
|
Authorization: `Bearer ${entry.access}`,
|
|
256
|
-
"ChatGPT-Account-Id":
|
|
317
|
+
"ChatGPT-Account-Id": accountId,
|
|
257
318
|
"User-Agent": "pi-sub/0.1.0",
|
|
258
319
|
},
|
|
259
320
|
signal: combinedSignal,
|
|
@@ -267,6 +328,7 @@ function redactedError(error: unknown, provider = "Codex"): string {
|
|
|
267
328
|
if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
|
|
268
329
|
if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
|
|
269
330
|
if (/missing opencode-go/i.test(message)) return "opencode-go auth not found";
|
|
331
|
+
if (/missing zai/i.test(message)) return "zai auth not found";
|
|
270
332
|
if (/timed out|timeout|aborted/i.test(message)) return `${provider} usage refresh timed out`;
|
|
271
333
|
if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return `${provider} auth unavailable`;
|
|
272
334
|
return `${provider} usage unavailable`;
|
|
@@ -298,11 +360,12 @@ async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageS
|
|
|
298
360
|
|
|
299
361
|
async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
300
362
|
try {
|
|
301
|
-
await readOpenCodeGoAuth();
|
|
363
|
+
const { account } = await readOpenCodeGoAuth();
|
|
302
364
|
return {
|
|
303
365
|
providerId: OPC_PROVIDER,
|
|
304
366
|
providerDisplayName: "OpenCode Go",
|
|
305
|
-
accounts: [],
|
|
367
|
+
accounts: [account],
|
|
368
|
+
activeAccount: account,
|
|
306
369
|
fetchedAt: Date.now(),
|
|
307
370
|
};
|
|
308
371
|
} catch (error) {
|
|
@@ -316,6 +379,117 @@ async function fetchOpenCodeGoUsage(_signal?: AbortSignal): Promise<Subscription
|
|
|
316
379
|
}
|
|
317
380
|
}
|
|
318
381
|
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Z.ai adapter
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
interface ZaiLimitEntry {
|
|
387
|
+
type: string;
|
|
388
|
+
percentage: number;
|
|
389
|
+
nextResetTime?: number;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface ZaiUsageApiResponse {
|
|
393
|
+
data?: {
|
|
394
|
+
limits?: ZaiLimitEntry[];
|
|
395
|
+
planName?: string;
|
|
396
|
+
plan?: string;
|
|
397
|
+
plan_type?: string;
|
|
398
|
+
packageName?: string;
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface ZaiUsageApiError {
|
|
403
|
+
code: number;
|
|
404
|
+
msg: string;
|
|
405
|
+
success: boolean;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function zaiLimitToUsageWindow(limit: ZaiLimitEntry): UsageWindow | undefined {
|
|
409
|
+
if (typeof limit.percentage !== "number") return undefined;
|
|
410
|
+
const percent = Math.round(limit.percentage);
|
|
411
|
+
const remaining = Math.max(0, 100 - percent);
|
|
412
|
+
// Z.ai returns nextResetTime in epoch milliseconds; format helpers expect seconds.
|
|
413
|
+
const resetAtSec = limit.nextResetTime ? limit.nextResetTime / 1000 : undefined;
|
|
414
|
+
const resetLabel = formatReset(resetAtSec);
|
|
415
|
+
const remainingLabel = formatRemainingTime(resetAtSec);
|
|
416
|
+
return {
|
|
417
|
+
percent,
|
|
418
|
+
remaining,
|
|
419
|
+
remainingLabel,
|
|
420
|
+
resetLabel,
|
|
421
|
+
resetsAt: limit.nextResetTime,
|
|
422
|
+
label: remainingLabel ? `${remaining}% (${remainingLabel})` : `${remaining}%`,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
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
|
+
|
|
431
|
+
async function fetchZaiUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
432
|
+
try {
|
|
433
|
+
const { key: apiKey, account: authAccount } = await readZaiAuth();
|
|
434
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
435
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
436
|
+
|
|
437
|
+
const response = await fetch(ZAI_USAGE_URL, {
|
|
438
|
+
headers: {
|
|
439
|
+
Accept: "application/json",
|
|
440
|
+
Authorization: `Bearer ${apiKey}`,
|
|
441
|
+
"User-Agent": "pi-sub/0.1.0",
|
|
442
|
+
},
|
|
443
|
+
signal: combinedSignal,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const body = await response.json();
|
|
447
|
+
|
|
448
|
+
// Z.ai returns HTTP 200 even on auth errors: {"code":401,"msg":"...","success":false}
|
|
449
|
+
const apiError = body as ZaiUsageApiError;
|
|
450
|
+
if (typeof apiError.success === "boolean" && !apiError.success && apiError.msg) {
|
|
451
|
+
throw new Error(`Z.ai API error: ${apiError.msg}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const parsed = body as ZaiUsageApiResponse;
|
|
455
|
+
const tokenLimits = (parsed.data?.limits ?? [])
|
|
456
|
+
.filter((l) => l.type === "TOKENS_LIMIT")
|
|
457
|
+
.sort((a, b) => (a.nextResetTime ?? 0) - (b.nextResetTime ?? 0));
|
|
458
|
+
|
|
459
|
+
if (tokenLimits.length === 0) {
|
|
460
|
+
throw new Error("No TOKENS_LIMIT entries in Z.ai usage response");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// The limit with the nearest reset is the 5-hour rolling window;
|
|
464
|
+
// the next one (if present) is the weekly window.
|
|
465
|
+
const fiveHour = zaiLimitToUsageWindow(tokenLimits[0]);
|
|
466
|
+
const weekly = tokenLimits.length >= 2 ? zaiLimitToUsageWindow(tokenLimits[1]) : undefined;
|
|
467
|
+
|
|
468
|
+
const account: SubscriptionAccountSnapshot = {
|
|
469
|
+
...authAccount,
|
|
470
|
+
plan: zaiPlanLabel(parsed) ?? authAccount.plan,
|
|
471
|
+
fiveHour,
|
|
472
|
+
weekly,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
providerId: ZAI_PROVIDER,
|
|
477
|
+
providerDisplayName: "Z.ai",
|
|
478
|
+
accounts: [account],
|
|
479
|
+
activeAccount: account,
|
|
480
|
+
fetchedAt: Date.now(),
|
|
481
|
+
};
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return {
|
|
484
|
+
providerId: ZAI_PROVIDER,
|
|
485
|
+
providerDisplayName: "Z.ai",
|
|
486
|
+
accounts: [],
|
|
487
|
+
fetchedAt: Date.now(),
|
|
488
|
+
error: redactedError(error, "Z.ai"),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
319
493
|
const codexAdapter: SubscriptionProviderAdapter = {
|
|
320
494
|
id: CODEX_PROVIDER,
|
|
321
495
|
displayName: "Codex",
|
|
@@ -330,7 +504,14 @@ const openCodeGoAdapter: SubscriptionProviderAdapter = {
|
|
|
330
504
|
fetchUsage: fetchOpenCodeGoUsage,
|
|
331
505
|
};
|
|
332
506
|
|
|
333
|
-
const
|
|
507
|
+
const zaiAdapter: SubscriptionProviderAdapter = {
|
|
508
|
+
id: ZAI_PROVIDER,
|
|
509
|
+
displayName: "Z.ai",
|
|
510
|
+
isModelSupported: isZaiModel,
|
|
511
|
+
fetchUsage: fetchZaiUsage,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const adapters = [codexAdapter, openCodeGoAdapter, zaiAdapter];
|
|
334
515
|
|
|
335
516
|
function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undefined {
|
|
336
517
|
return adapters.find((adapter) => adapter.isModelSupported(model));
|
|
@@ -397,9 +578,11 @@ function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
|
397
578
|
color = "warning";
|
|
398
579
|
} else {
|
|
399
580
|
const account = snapshot.activeAccount;
|
|
400
|
-
const
|
|
581
|
+
const windowParts = windowSegments(account);
|
|
582
|
+
const accountPart = formatFooterAccount(account);
|
|
583
|
+
const segments = accountPart ? [accountPart, ...windowParts] : [...windowParts];
|
|
401
584
|
const cost = snapshot.cost;
|
|
402
|
-
const hasWindows =
|
|
585
|
+
const hasWindows = windowParts.length > 0;
|
|
403
586
|
if (cost !== undefined && cost > 0) segments.push(`$${cost.toFixed(2)}`);
|
|
404
587
|
if (segments.length === 0) {
|
|
405
588
|
line = `Sub ${state.adapter.displayName}`;
|
|
@@ -530,7 +713,11 @@ function buildDetails(snapshot: SubscriptionUsageSnapshot | undefined, state: St
|
|
|
530
713
|
});
|
|
531
714
|
|
|
532
715
|
const costLine = snapshot.cost !== undefined ? `\nSession cost: $${snapshot.cost.toFixed(2)}` : "";
|
|
533
|
-
|
|
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");
|
|
534
721
|
}
|
|
535
722
|
|
|
536
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": "*"
|