@bacnh85/pi-sub 0.1.0 → 0.1.1
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 +12 -18
- package/index.ts +193 -109
- package/package.json +1 -4
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
|
-
V1 supports OpenAI Codex models
|
|
5
|
+
V1 supports OpenAI Codex models by reading Pi's auth state from `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`) and displays a separate subscription line below the editor only while the active Pi model provider is `openai-codex`.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -34,13 +34,13 @@ The footer status includes:
|
|
|
34
34
|
- weekly usage percentage and reset time/date;
|
|
35
35
|
- active Codex model id.
|
|
36
36
|
|
|
37
|
-
Example
|
|
37
|
+
Example subscription line:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
40
|
Sub Plus user@example.com 5H 85%→18:12 W 80%→2 Jul 08:00 · gpt-5-codex
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
When the current model provider is not supported, `pi-sub` clears its
|
|
43
|
+
When the current model provider is not supported, `pi-sub` clears its subscription line and does not refresh subscription data.
|
|
44
44
|
|
|
45
45
|
## Commands
|
|
46
46
|
|
|
@@ -50,12 +50,11 @@ When the current model provider is not supported, `pi-sub` clears its footer sta
|
|
|
50
50
|
| `/sub status` | Same as `/sub`. |
|
|
51
51
|
| `/sub refresh` | Force a usage refresh, then show details. |
|
|
52
52
|
|
|
53
|
-
When Codex
|
|
53
|
+
When Pi OpenAI Codex auth is available, `/sub` shows the active account usage:
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
56
|
ACCOUNT PLAN 5H USAGE WEEKLY USAGE LAST ACTIVITY
|
|
57
57
|
* hangdanchi@gmail.com Plus 85% (18:12) 80% (08:00 on 2 Jul) Now
|
|
58
|
-
mail@bacnh.com Plus 75% (14:21) 20% (14:23 on 29 Jun) Now
|
|
59
58
|
```
|
|
60
59
|
|
|
61
60
|
## Refresh behavior
|
|
@@ -68,23 +67,18 @@ ACCOUNT PLAN 5H USAGE WEEKLY USAGE LAST ACTIVITY
|
|
|
68
67
|
- periodically while a supported provider remains active;
|
|
69
68
|
- when `/sub refresh` is run.
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
`pi-sub` reads the `openai-codex` OAuth entry from Pi's auth file and refreshes live usage directly against ChatGPT's usage endpoint. It does not execute the `codex-auth` CLI and does not assume a separate Codex CLI installation exists.
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- `@loongphy/codex-auth` is installed as a runtime dependency of this package.
|
|
76
|
-
- Codex account data must already be configured for `codex-auth list` to return usage.
|
|
77
|
-
- Node.js 22+ is required by `@loongphy/codex-auth`.
|
|
78
|
-
- `pi-sub` redacts auth/token-related errors in the footer and never prints Codex tokens.
|
|
72
|
+
Refreshes are cached briefly to avoid excessive usage endpoint calls.
|
|
79
73
|
|
|
80
|
-
|
|
74
|
+
## Requirements and troubleshooting
|
|
81
75
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
- Pi auth must contain an `openai-codex` OAuth entry in `~/.pi/agent/auth.json` or `$PI_CODING_AGENT_DIR/auth.json`.
|
|
77
|
+
- The entry must include `access` and `accountId` fields.
|
|
78
|
+
- `pi-sub` redacts auth/token-related errors and never prints Codex tokens.
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
If usage is unavailable, verify that Pi can use the `openai-codex` provider and that the auth file exists.
|
|
87
81
|
|
|
88
82
|
## Design notes
|
|
89
83
|
|
|
90
|
-
The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters. V1 intentionally supports only OpenAI Codex.
|
|
84
|
+
The extension is named `pi-sub` rather than `pi-codex-usage` so future subscription providers can be added as separate adapters. V1 intentionally supports only OpenAI Codex and vendors only the small amount of behavior needed for this extension instead of invoking `codex-auth` directly.
|
package/index.ts
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
5
6
|
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
7
|
const STATUS_KEY = "pi-sub";
|
|
8
8
|
const MESSAGE_TYPE = "pi-sub-status";
|
|
9
|
+
const USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage";
|
|
9
10
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
10
11
|
const REFRESH_TTL_MS = 30_000;
|
|
11
12
|
const REFRESH_DEBOUNCE_MS = 2_000;
|
|
13
|
+
const PI_SUB_PROVIDER = "openai-codex";
|
|
12
14
|
|
|
13
15
|
type ModelLike = { provider?: string; id?: string } | undefined;
|
|
14
16
|
|
|
17
|
+
type UsageApiWindow = {
|
|
18
|
+
used_percent?: number;
|
|
19
|
+
limit_window_seconds?: number;
|
|
20
|
+
reset_at?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type UsageApiSnapshot = {
|
|
24
|
+
primary?: UsageApiWindow;
|
|
25
|
+
secondary?: UsageApiWindow;
|
|
26
|
+
plan_type?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type PiAuthFile = Record<string, PiAuthEntry | undefined>;
|
|
30
|
+
|
|
31
|
+
type PiAuthEntry = {
|
|
32
|
+
type?: string;
|
|
33
|
+
access?: string;
|
|
34
|
+
refresh?: string;
|
|
35
|
+
expires?: number;
|
|
36
|
+
accountId?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
15
39
|
interface UsageWindow {
|
|
16
40
|
used?: number;
|
|
17
41
|
limit?: number;
|
|
@@ -59,117 +83,175 @@ interface State {
|
|
|
59
83
|
|
|
60
84
|
function isCodexModel(model: ModelLike): boolean {
|
|
61
85
|
const provider = model?.provider?.toLowerCase() ?? "";
|
|
62
|
-
return provider ===
|
|
86
|
+
return provider === PI_SUB_PROVIDER || provider.includes(PI_SUB_PROVIDER);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function piAuthPath(): string {
|
|
90
|
+
const configDir = process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
91
|
+
return path.join(configDir, "auth.json");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function readJsonFile<T>(file: string): Promise<T> {
|
|
95
|
+
return JSON.parse(await fs.readFile(file, "utf8")) as T;
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
function decodeJwtPayload(token: string | undefined): Record<string, any> | undefined {
|
|
99
|
+
if (!token) return undefined;
|
|
100
|
+
const parts = token.split(".");
|
|
101
|
+
if (parts.length < 2) return undefined;
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")) as Record<string, any>;
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
68
107
|
}
|
|
69
108
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
109
|
+
function accountFromPiAuth(entry: PiAuthEntry): SubscriptionAccountSnapshot {
|
|
110
|
+
const claims = decodeJwtPayload(entry.access);
|
|
111
|
+
const profile = claims?.["https://api.openai.com/profile"];
|
|
112
|
+
const auth = claims?.["https://api.openai.com/auth"];
|
|
113
|
+
const email = typeof profile?.email === "string" ? profile.email : undefined;
|
|
114
|
+
const plan = typeof auth?.chatgpt_plan_type === "string" ? planLabel(auth.chatgpt_plan_type) : undefined;
|
|
115
|
+
const accountId = typeof entry.accountId === "string" ? entry.accountId : typeof auth?.chatgpt_account_id === "string" ? auth.chatgpt_account_id : undefined;
|
|
76
116
|
return {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
id: accountId,
|
|
118
|
+
isActive: true,
|
|
119
|
+
accountLabel: email ?? accountId ?? "openai-codex account",
|
|
120
|
+
plan,
|
|
121
|
+
lastActivity: "Now",
|
|
80
122
|
};
|
|
81
123
|
}
|
|
82
124
|
|
|
83
|
-
function
|
|
84
|
-
|
|
125
|
+
function planLabel(plan: string | undefined): string | undefined {
|
|
126
|
+
if (!plan) return undefined;
|
|
127
|
+
const normalized = plan.toLowerCase().replace(/[_-]+/g, " ");
|
|
128
|
+
const labels: Record<string, string> = {
|
|
129
|
+
free: "Free",
|
|
130
|
+
plus: "Plus",
|
|
131
|
+
prolite: "Pro Lite",
|
|
132
|
+
"pro lite": "Pro Lite",
|
|
133
|
+
pro: "Pro",
|
|
134
|
+
team: "Business",
|
|
135
|
+
business: "Business",
|
|
136
|
+
enterprise: "Enterprise",
|
|
137
|
+
edu: "Edu",
|
|
138
|
+
unknown: "Unknown",
|
|
139
|
+
};
|
|
140
|
+
return labels[normalized] ?? plan;
|
|
85
141
|
}
|
|
86
142
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
143
|
+
function formatReset(timestampSeconds: number | undefined): string | undefined {
|
|
144
|
+
if (!timestampSeconds) return undefined;
|
|
145
|
+
const date = new Date(timestampSeconds * 1000);
|
|
146
|
+
const now = new Date();
|
|
147
|
+
const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
148
|
+
if (date.toDateString() === now.toDateString()) return time;
|
|
149
|
+
return `${time} on ${date.toLocaleDateString(undefined, { day: "numeric", month: "short" })}`;
|
|
150
|
+
}
|
|
91
151
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
152
|
+
function usageWindowFromApi(window: UsageApiWindow | undefined): UsageWindow | undefined {
|
|
153
|
+
if (!window || typeof window.used_percent !== "number") return undefined;
|
|
154
|
+
const percent = Math.round(window.used_percent);
|
|
155
|
+
const resetLabel = formatReset(window.reset_at);
|
|
156
|
+
return {
|
|
157
|
+
percent,
|
|
158
|
+
resetLabel,
|
|
159
|
+
resetsAt: window.reset_at,
|
|
160
|
+
label: resetLabel ? `${percent}% (${resetLabel})` : `${percent}%`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mergeUsageIntoAccount(account: SubscriptionAccountSnapshot, usage: UsageApiSnapshot | undefined): SubscriptionAccountSnapshot {
|
|
165
|
+
if (!usage) return account;
|
|
166
|
+
return {
|
|
167
|
+
...account,
|
|
168
|
+
plan: planLabel(usage.plan_type) ?? account.plan,
|
|
169
|
+
fiveHour: usageWindowFromApi(usage.primary) ?? account.fiveHour,
|
|
170
|
+
weekly: usageWindowFromApi(usage.secondary) ?? account.weekly,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
105
173
|
|
|
174
|
+
function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
175
|
+
if (!body || typeof body !== "object") return undefined;
|
|
176
|
+
const root = body as any;
|
|
177
|
+
const rateLimit = root.rate_limit;
|
|
178
|
+
if (!rateLimit || typeof rateLimit !== "object") return undefined;
|
|
179
|
+
const parseWindow = (window: any): UsageApiWindow | undefined => {
|
|
180
|
+
if (!window || typeof window !== "object" || typeof window.used_percent !== "number") return undefined;
|
|
181
|
+
return {
|
|
182
|
+
used_percent: window.used_percent,
|
|
183
|
+
limit_window_seconds: typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : undefined,
|
|
184
|
+
reset_at: typeof window.reset_at === "number" ? window.reset_at : undefined,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
106
187
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
activeAccount: accounts.find((account) => account.isActive) ?? accounts[0],
|
|
111
|
-
fetchedAt: Date.now(),
|
|
188
|
+
primary: parseWindow(rateLimit.primary_window),
|
|
189
|
+
secondary: parseWindow(rateLimit.secondary_window),
|
|
190
|
+
plan_type: typeof root.plan_type === "string" ? root.plan_type : undefined,
|
|
112
191
|
};
|
|
113
192
|
}
|
|
114
193
|
|
|
194
|
+
async function readPiCodexAuth(): Promise<PiAuthEntry> {
|
|
195
|
+
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
196
|
+
const entry = auth[PI_SUB_PROVIDER];
|
|
197
|
+
if (!entry?.access || !entry.accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
198
|
+
return entry;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
202
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
203
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
204
|
+
const response = await fetch(USAGE_ENDPOINT, {
|
|
205
|
+
headers: {
|
|
206
|
+
Accept: "application/json",
|
|
207
|
+
Authorization: `Bearer ${entry.access}`,
|
|
208
|
+
"ChatGPT-Account-Id": entry.accountId!,
|
|
209
|
+
"User-Agent": "pi-sub/0.1.0",
|
|
210
|
+
},
|
|
211
|
+
signal: combinedSignal,
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) throw new Error(`usage request failed with HTTP ${response.status}`);
|
|
214
|
+
return parseUsageResponse(await response.json());
|
|
215
|
+
}
|
|
216
|
+
|
|
115
217
|
function redactedError(error: unknown): string {
|
|
116
218
|
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
|
117
|
-
if (/
|
|
118
|
-
if (/
|
|
119
|
-
if (/
|
|
219
|
+
if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
|
|
220
|
+
if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
|
|
221
|
+
if (/timed out|timeout|aborted/i.test(message)) return "Codex usage refresh timed out";
|
|
222
|
+
if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return "Codex auth unavailable";
|
|
120
223
|
return "Codex usage unavailable";
|
|
121
224
|
}
|
|
122
225
|
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const timeout = setTimeout(() => {
|
|
136
|
-
child.kill("SIGTERM");
|
|
137
|
-
reject(new Error("codex-auth list timed out"));
|
|
138
|
-
}, 20_000);
|
|
139
|
-
const onAbort = () => {
|
|
140
|
-
child.kill("SIGTERM");
|
|
141
|
-
reject(signal?.reason ?? new Error("Operation aborted"));
|
|
226
|
+
async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
227
|
+
try {
|
|
228
|
+
const entry = await readPiCodexAuth();
|
|
229
|
+
let activeAccount = accountFromPiAuth(entry);
|
|
230
|
+
const usage = await fetchUsageFromPiAuth(entry, signal);
|
|
231
|
+
activeAccount = mergeUsageIntoAccount(activeAccount, usage);
|
|
232
|
+
return {
|
|
233
|
+
providerId: PI_SUB_PROVIDER,
|
|
234
|
+
providerDisplayName: "Codex",
|
|
235
|
+
accounts: [activeAccount],
|
|
236
|
+
activeAccount,
|
|
237
|
+
fetchedAt: Date.now(),
|
|
142
238
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
providerId: PI_SUB_PROVIDER,
|
|
242
|
+
providerDisplayName: "Codex",
|
|
243
|
+
accounts: [],
|
|
244
|
+
fetchedAt: Date.now(),
|
|
245
|
+
error: redactedError(error),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
154
248
|
}
|
|
155
249
|
|
|
156
250
|
const codexAdapter: SubscriptionProviderAdapter = {
|
|
157
|
-
id:
|
|
251
|
+
id: PI_SUB_PROVIDER,
|
|
158
252
|
displayName: "Codex",
|
|
159
253
|
isModelSupported: isCodexModel,
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
return parseCodexAuthList(await runCodexAuthList(signal));
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return {
|
|
165
|
-
providerId: "openai-codex",
|
|
166
|
-
providerDisplayName: "Codex",
|
|
167
|
-
accounts: [],
|
|
168
|
-
fetchedAt: Date.now(),
|
|
169
|
-
error: redactedError(error),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
},
|
|
254
|
+
fetchUsage: fetchCodexUsage,
|
|
173
255
|
};
|
|
174
256
|
|
|
175
257
|
const adapters = [codexAdapter];
|
|
@@ -195,31 +277,33 @@ function maxPercent(account: SubscriptionAccountSnapshot | undefined): number {
|
|
|
195
277
|
return Math.max(account?.fiveHour?.percent ?? 0, account?.weekly?.percent ?? 0);
|
|
196
278
|
}
|
|
197
279
|
|
|
198
|
-
function
|
|
280
|
+
function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
199
281
|
if (!state.adapter) {
|
|
200
|
-
ctx.ui.
|
|
282
|
+
ctx.ui.setWidget(STATUS_KEY, undefined);
|
|
201
283
|
return;
|
|
202
284
|
}
|
|
203
285
|
const theme = ctx.ui.theme;
|
|
204
286
|
const snapshot = state.snapshot;
|
|
205
287
|
const modelId = state.model?.id ?? "unknown-model";
|
|
288
|
+
let line: string;
|
|
289
|
+
let color: "dim" | "warning" | "error" = "dim";
|
|
206
290
|
if (!snapshot) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
291
|
+
line = `Sub ${state.adapter.displayName} loading · ${modelId}`;
|
|
292
|
+
} else if (snapshot.error) {
|
|
293
|
+
line = `Sub ${snapshot.error} · ${modelId}`;
|
|
294
|
+
color = "warning";
|
|
295
|
+
} else {
|
|
296
|
+
const account = snapshot.activeAccount;
|
|
297
|
+
const pieces = ["Sub"];
|
|
298
|
+
if (account?.plan) pieces.push(account.plan);
|
|
299
|
+
pieces.push(account?.accountLabel ?? "unknown account");
|
|
300
|
+
pieces.push(`5H ${formatWindow(account?.fiveHour)}`);
|
|
301
|
+
pieces.push(`W ${formatWindow(account?.weekly)}`);
|
|
302
|
+
pieces.push("·", modelId);
|
|
303
|
+
line = pieces.join(" ");
|
|
304
|
+
color = maxPercent(account) >= 90 ? "error" : maxPercent(account) >= 80 ? "warning" : "dim";
|
|
213
305
|
}
|
|
214
|
-
|
|
215
|
-
const pieces = ["Sub"];
|
|
216
|
-
if (account?.plan) pieces.push(account.plan);
|
|
217
|
-
pieces.push(account?.accountLabel ?? "unknown account");
|
|
218
|
-
pieces.push(`5H ${formatWindow(account?.fiveHour)}`);
|
|
219
|
-
pieces.push(`W ${formatWindow(account?.weekly)}`);
|
|
220
|
-
pieces.push("·", modelId);
|
|
221
|
-
const color = maxPercent(account) >= 90 ? "error" : maxPercent(account) >= 80 ? "warning" : "dim";
|
|
222
|
-
ctx.ui.setStatus(STATUS_KEY, theme.fg(color, pieces.join(" ")));
|
|
306
|
+
ctx.ui.setWidget(STATUS_KEY, [theme.fg(color, line)], { placement: "belowEditor" });
|
|
223
307
|
}
|
|
224
308
|
|
|
225
309
|
function startTimer(ctx: ExtensionContext, state: State): void {
|
|
@@ -244,23 +328,23 @@ function updateActiveAdapter(ctx: ExtensionContext, state: State, model: ModelLi
|
|
|
244
328
|
state.lastRefreshAt = 0;
|
|
245
329
|
stopTimer(state);
|
|
246
330
|
}
|
|
247
|
-
|
|
331
|
+
renderSubscriptionLine(ctx, state);
|
|
248
332
|
if (state.adapter) startTimer(ctx, state);
|
|
249
333
|
}
|
|
250
334
|
|
|
251
335
|
async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean): Promise<SubscriptionUsageSnapshot | undefined> {
|
|
252
336
|
const adapter = state.adapter;
|
|
253
337
|
if (!adapter) {
|
|
254
|
-
|
|
338
|
+
renderSubscriptionLine(ctx, state);
|
|
255
339
|
return undefined;
|
|
256
340
|
}
|
|
257
341
|
if (!force && state.snapshot && Date.now() - state.lastRefreshAt < REFRESH_TTL_MS) return state.snapshot;
|
|
258
342
|
if (state.inFlight) return state.inFlight;
|
|
259
|
-
|
|
343
|
+
renderSubscriptionLine(ctx, state);
|
|
260
344
|
state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
|
|
261
345
|
state.snapshot = snapshot;
|
|
262
346
|
state.lastRefreshAt = Date.now();
|
|
263
|
-
|
|
347
|
+
renderSubscriptionLine(ctx, state);
|
|
264
348
|
return snapshot;
|
|
265
349
|
}).finally(() => {
|
|
266
350
|
state.inFlight = undefined;
|
|
@@ -331,7 +415,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
331
415
|
|
|
332
416
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
333
417
|
stopTimer(state);
|
|
334
|
-
ctx.ui.
|
|
418
|
+
ctx.ui.setWidget(STATUS_KEY, undefined);
|
|
335
419
|
});
|
|
336
420
|
|
|
337
421
|
pi.registerCommand("sub", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-sub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Pi extension showing subscription usage for supported model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,9 +33,6 @@
|
|
|
33
33
|
"./index.ts"
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"@loongphy/codex-auth": "^0.2.10"
|
|
38
|
-
},
|
|
39
36
|
"peerDependencies": {
|
|
40
37
|
"@earendil-works/pi-coding-agent": "*",
|
|
41
38
|
"@earendil-works/pi-tui": "*"
|