@bacnh85/pi-sub 0.1.0 → 0.1.2
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 +13 -19
- package/index.ts +198 -116
- 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
|
-
Sub Plus user@example.com 5H 85
|
|
40
|
+
Sub · Plus · user@example.com · 5H 85% (18:12) · W 80% (08:00 on 2 Jul) · 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,177 @@ 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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
const day = date.toLocaleDateString(undefined, { day: "numeric" });
|
|
150
|
+
const month = date.toLocaleDateString(undefined, { month: "short" });
|
|
151
|
+
return `${time} on ${day} ${month}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function usageWindowFromApi(window: UsageApiWindow | undefined): UsageWindow | undefined {
|
|
155
|
+
if (!window || typeof window.used_percent !== "number") return undefined;
|
|
156
|
+
const percent = Math.round(window.used_percent);
|
|
157
|
+
const resetLabel = formatReset(window.reset_at);
|
|
158
|
+
return {
|
|
159
|
+
percent,
|
|
160
|
+
resetLabel,
|
|
161
|
+
resetsAt: window.reset_at,
|
|
162
|
+
label: resetLabel ? `${percent}% (${resetLabel})` : `${percent}%`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
105
165
|
|
|
166
|
+
function mergeUsageIntoAccount(account: SubscriptionAccountSnapshot, usage: UsageApiSnapshot | undefined): SubscriptionAccountSnapshot {
|
|
167
|
+
if (!usage) return account;
|
|
106
168
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
fetchedAt: Date.now(),
|
|
169
|
+
...account,
|
|
170
|
+
plan: planLabel(usage.plan_type) ?? account.plan,
|
|
171
|
+
fiveHour: usageWindowFromApi(usage.primary) ?? account.fiveHour,
|
|
172
|
+
weekly: usageWindowFromApi(usage.secondary) ?? account.weekly,
|
|
112
173
|
};
|
|
113
174
|
}
|
|
114
175
|
|
|
176
|
+
function parseUsageResponse(body: unknown): UsageApiSnapshot | undefined {
|
|
177
|
+
if (!body || typeof body !== "object") return undefined;
|
|
178
|
+
const root = body as any;
|
|
179
|
+
const rateLimit = root.rate_limit;
|
|
180
|
+
if (!rateLimit || typeof rateLimit !== "object") return undefined;
|
|
181
|
+
const parseWindow = (window: any): UsageApiWindow | undefined => {
|
|
182
|
+
if (!window || typeof window !== "object" || typeof window.used_percent !== "number") return undefined;
|
|
183
|
+
return {
|
|
184
|
+
used_percent: window.used_percent,
|
|
185
|
+
limit_window_seconds: typeof window.limit_window_seconds === "number" ? window.limit_window_seconds : undefined,
|
|
186
|
+
reset_at: typeof window.reset_at === "number" ? window.reset_at : undefined,
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
primary: parseWindow(rateLimit.primary_window),
|
|
191
|
+
secondary: parseWindow(rateLimit.secondary_window),
|
|
192
|
+
plan_type: typeof root.plan_type === "string" ? root.plan_type : undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function readPiCodexAuth(): Promise<PiAuthEntry> {
|
|
197
|
+
const auth = await readJsonFile<PiAuthFile>(piAuthPath());
|
|
198
|
+
const entry = auth[PI_SUB_PROVIDER];
|
|
199
|
+
if (!entry?.access || !entry.accountId) throw new Error("Missing openai-codex OAuth entry in Pi auth");
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function fetchUsageFromPiAuth(entry: PiAuthEntry, signal?: AbortSignal): Promise<UsageApiSnapshot | undefined> {
|
|
204
|
+
const timeoutSignal = AbortSignal.timeout(7_000);
|
|
205
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
206
|
+
const response = await fetch(USAGE_ENDPOINT, {
|
|
207
|
+
headers: {
|
|
208
|
+
Accept: "application/json",
|
|
209
|
+
Authorization: `Bearer ${entry.access}`,
|
|
210
|
+
"ChatGPT-Account-Id": entry.accountId!,
|
|
211
|
+
"User-Agent": "pi-sub/0.1.0",
|
|
212
|
+
},
|
|
213
|
+
signal: combinedSignal,
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) throw new Error(`usage request failed with HTTP ${response.status}`);
|
|
216
|
+
return parseUsageResponse(await response.json());
|
|
217
|
+
}
|
|
218
|
+
|
|
115
219
|
function redactedError(error: unknown): string {
|
|
116
220
|
const message = error instanceof Error ? error.message : String(error || "Unknown error");
|
|
117
|
-
if (/
|
|
118
|
-
if (/
|
|
119
|
-
if (/
|
|
221
|
+
if (/ENOENT|no such file/i.test(message)) return "Pi auth not found";
|
|
222
|
+
if (/missing openai-codex/i.test(message)) return "openai-codex auth not found";
|
|
223
|
+
if (/timed out|timeout|aborted/i.test(message)) return "Codex usage refresh timed out";
|
|
224
|
+
if (/401|403|auth|token|unauthorized|forbidden/i.test(message)) return "Codex auth unavailable";
|
|
120
225
|
return "Codex usage unavailable";
|
|
121
226
|
}
|
|
122
227
|
|
|
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"));
|
|
228
|
+
async function fetchCodexUsage(signal?: AbortSignal): Promise<SubscriptionUsageSnapshot> {
|
|
229
|
+
try {
|
|
230
|
+
const entry = await readPiCodexAuth();
|
|
231
|
+
let activeAccount = accountFromPiAuth(entry);
|
|
232
|
+
const usage = await fetchUsageFromPiAuth(entry, signal);
|
|
233
|
+
activeAccount = mergeUsageIntoAccount(activeAccount, usage);
|
|
234
|
+
return {
|
|
235
|
+
providerId: PI_SUB_PROVIDER,
|
|
236
|
+
providerDisplayName: "Codex",
|
|
237
|
+
accounts: [activeAccount],
|
|
238
|
+
activeAccount,
|
|
239
|
+
fetchedAt: Date.now(),
|
|
142
240
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
providerId: PI_SUB_PROVIDER,
|
|
244
|
+
providerDisplayName: "Codex",
|
|
245
|
+
accounts: [],
|
|
246
|
+
fetchedAt: Date.now(),
|
|
247
|
+
error: redactedError(error),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
154
250
|
}
|
|
155
251
|
|
|
156
252
|
const codexAdapter: SubscriptionProviderAdapter = {
|
|
157
|
-
id:
|
|
253
|
+
id: PI_SUB_PROVIDER,
|
|
158
254
|
displayName: "Codex",
|
|
159
255
|
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
|
-
},
|
|
256
|
+
fetchUsage: fetchCodexUsage,
|
|
173
257
|
};
|
|
174
258
|
|
|
175
259
|
const adapters = [codexAdapter];
|
|
@@ -178,48 +262,46 @@ function supportedAdapter(model: ModelLike): SubscriptionProviderAdapter | undef
|
|
|
178
262
|
return adapters.find((adapter) => adapter.isModelSupported(model));
|
|
179
263
|
}
|
|
180
264
|
|
|
181
|
-
function formatWindow(window: UsageWindow | undefined,
|
|
265
|
+
function formatWindow(window: UsageWindow | undefined, _compact = true): string {
|
|
182
266
|
if (!window) return "?";
|
|
183
|
-
if (window.percent !== undefined && window.resetLabel) return
|
|
267
|
+
if (window.percent !== undefined && window.resetLabel) return `${window.percent}% (${window.resetLabel})`;
|
|
184
268
|
if (window.label) return window.label;
|
|
185
269
|
if (window.percent !== undefined) return `${window.percent}%`;
|
|
186
270
|
if (window.used !== undefined && window.limit !== undefined) return `${window.used}/${window.limit}`;
|
|
187
271
|
return "?";
|
|
188
272
|
}
|
|
189
273
|
|
|
190
|
-
function compactResetLabel(label: string): string {
|
|
191
|
-
return label.replace(/^0?(\d+:\d+) on (\d+ \w+)$/i, "$2 $1");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
274
|
function maxPercent(account: SubscriptionAccountSnapshot | undefined): number {
|
|
195
275
|
return Math.max(account?.fiveHour?.percent ?? 0, account?.weekly?.percent ?? 0);
|
|
196
276
|
}
|
|
197
277
|
|
|
198
|
-
function
|
|
278
|
+
function renderSubscriptionLine(ctx: ExtensionContext, state: State): void {
|
|
199
279
|
if (!state.adapter) {
|
|
200
|
-
ctx.ui.
|
|
280
|
+
ctx.ui.setWidget(STATUS_KEY, undefined);
|
|
201
281
|
return;
|
|
202
282
|
}
|
|
203
283
|
const theme = ctx.ui.theme;
|
|
204
284
|
const snapshot = state.snapshot;
|
|
205
285
|
const modelId = state.model?.id ?? "unknown-model";
|
|
286
|
+
let line: string;
|
|
287
|
+
let color: "dim" | "warning" | "error" = "dim";
|
|
206
288
|
if (!snapshot) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
289
|
+
line = `Sub ${state.adapter.displayName} loading · ${modelId}`;
|
|
290
|
+
} else if (snapshot.error) {
|
|
291
|
+
line = `Sub ${snapshot.error} · ${modelId}`;
|
|
292
|
+
color = "warning";
|
|
293
|
+
} else {
|
|
294
|
+
const account = snapshot.activeAccount;
|
|
295
|
+
const pieces = ["Sub"];
|
|
296
|
+
if (account?.plan) pieces.push(account.plan);
|
|
297
|
+
pieces.push(account?.accountLabel ?? "unknown account");
|
|
298
|
+
pieces.push(`5H ${formatWindow(account?.fiveHour)}`);
|
|
299
|
+
pieces.push(`W ${formatWindow(account?.weekly)}`);
|
|
300
|
+
pieces.push(modelId);
|
|
301
|
+
line = pieces.join(" · ");
|
|
302
|
+
color = maxPercent(account) >= 90 ? "error" : maxPercent(account) >= 80 ? "warning" : "dim";
|
|
213
303
|
}
|
|
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(" ")));
|
|
304
|
+
ctx.ui.setWidget(STATUS_KEY, [theme.fg(color, line)], { placement: "belowEditor" });
|
|
223
305
|
}
|
|
224
306
|
|
|
225
307
|
function startTimer(ctx: ExtensionContext, state: State): void {
|
|
@@ -244,23 +326,23 @@ function updateActiveAdapter(ctx: ExtensionContext, state: State, model: ModelLi
|
|
|
244
326
|
state.lastRefreshAt = 0;
|
|
245
327
|
stopTimer(state);
|
|
246
328
|
}
|
|
247
|
-
|
|
329
|
+
renderSubscriptionLine(ctx, state);
|
|
248
330
|
if (state.adapter) startTimer(ctx, state);
|
|
249
331
|
}
|
|
250
332
|
|
|
251
333
|
async function refreshUsage(ctx: ExtensionContext, state: State, force: boolean): Promise<SubscriptionUsageSnapshot | undefined> {
|
|
252
334
|
const adapter = state.adapter;
|
|
253
335
|
if (!adapter) {
|
|
254
|
-
|
|
336
|
+
renderSubscriptionLine(ctx, state);
|
|
255
337
|
return undefined;
|
|
256
338
|
}
|
|
257
339
|
if (!force && state.snapshot && Date.now() - state.lastRefreshAt < REFRESH_TTL_MS) return state.snapshot;
|
|
258
340
|
if (state.inFlight) return state.inFlight;
|
|
259
|
-
|
|
341
|
+
renderSubscriptionLine(ctx, state);
|
|
260
342
|
state.inFlight = adapter.fetchUsage(ctx.signal).then((snapshot) => {
|
|
261
343
|
state.snapshot = snapshot;
|
|
262
344
|
state.lastRefreshAt = Date.now();
|
|
263
|
-
|
|
345
|
+
renderSubscriptionLine(ctx, state);
|
|
264
346
|
return snapshot;
|
|
265
347
|
}).finally(() => {
|
|
266
348
|
state.inFlight = undefined;
|
|
@@ -331,7 +413,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
331
413
|
|
|
332
414
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
333
415
|
stopTimer(state);
|
|
334
|
-
ctx.ui.
|
|
416
|
+
ctx.ui.setWidget(STATUS_KEY, undefined);
|
|
335
417
|
});
|
|
336
418
|
|
|
337
419
|
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.2",
|
|
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": "*"
|