pi-codex-footer 0.1.0
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/LICENSE +21 -0
- package/README.md +108 -0
- package/extensions/index.ts +337 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# pi-codex-footer
|
|
2
|
+
|
|
3
|
+
A publishable Pi package that adds a readable 2-line footer with live OpenAI Codex quota information.
|
|
4
|
+
|
|
5
|
+
## What it shows
|
|
6
|
+
|
|
7
|
+
Line 1:
|
|
8
|
+
- current folder
|
|
9
|
+
- git branch
|
|
10
|
+
- provider + model
|
|
11
|
+
- thinking level
|
|
12
|
+
- context usage as `used/window`
|
|
13
|
+
- total session cost
|
|
14
|
+
|
|
15
|
+
Line 2:
|
|
16
|
+
- tokens/sec for the last assistant response
|
|
17
|
+
- real Codex 5h usage
|
|
18
|
+
- real Codex 7d usage
|
|
19
|
+
- real 5h reset countdown
|
|
20
|
+
- real 7d reset countdown
|
|
21
|
+
|
|
22
|
+
For OpenAI Codex, quota data comes from:
|
|
23
|
+
|
|
24
|
+
- `https://chatgpt.com/backend-api/codex/usage`
|
|
25
|
+
|
|
26
|
+
using your existing Pi OAuth auth from:
|
|
27
|
+
|
|
28
|
+
- `~/.pi/agent/auth.json`
|
|
29
|
+
|
|
30
|
+
## Privacy / secrets
|
|
31
|
+
|
|
32
|
+
This package **does not bundle or publish any secrets**.
|
|
33
|
+
|
|
34
|
+
It reads your existing local `openai-codex` OAuth token and account id from `~/.pi/agent/auth.json` at runtime, then makes a request to OpenAI's ChatGPT backend to fetch live quota data.
|
|
35
|
+
|
|
36
|
+
It does **not**:
|
|
37
|
+
- write your token into the package
|
|
38
|
+
- store your token in the repo
|
|
39
|
+
- print your token in UI output
|
|
40
|
+
- require manual config edits
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
### From a local folder
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pi install ./pi-codex-footer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### From npm
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pi install npm:pi-codex-footer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### From git
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pi install git:github.com/glnarayanan/pi-codex-footer
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then reload Pi:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/reload
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
- `/codex-footer-on` — reload Pi and enable the footer
|
|
71
|
+
- `/codex-footer-off` — restore Pi's default footer
|
|
72
|
+
- `/codex-footer-status` — show whether live Codex quota data is available
|
|
73
|
+
|
|
74
|
+
## Behavior
|
|
75
|
+
|
|
76
|
+
- If the active provider is `openai-codex`, the footer shows live quota numbers.
|
|
77
|
+
- If the active provider is something else, quota fields show `n/a`.
|
|
78
|
+
- Quota data refreshes roughly once per minute.
|
|
79
|
+
- Reset countdowns update every second.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
Try it locally without publishing:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pi install ./pi-codex-footer
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
or:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pi -e ./pi-codex-footer/extensions/index.ts
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Publish to npm
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd pi-codex-footer
|
|
99
|
+
npm publish --access public
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Publish from git
|
|
103
|
+
|
|
104
|
+
Push this folder to a repo, then install with:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pi install git:github.com/glnarayanan/pi-codex-footer
|
|
108
|
+
```
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { request as httpsRequest } from "node:https";
|
|
6
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
7
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
9
|
+
|
|
10
|
+
const AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
11
|
+
const QUOTA_REFRESH_MS = 60 * 1000;
|
|
12
|
+
const MAX_RESPONSE_BYTES = 64 * 1024; // 64 KB — plenty for the quota JSON
|
|
13
|
+
|
|
14
|
+
interface OpenAICodexAuth {
|
|
15
|
+
type: "oauth";
|
|
16
|
+
access: string;
|
|
17
|
+
accountId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CodexUsageWindow {
|
|
21
|
+
used_percent: number;
|
|
22
|
+
limit_window_seconds: number;
|
|
23
|
+
reset_after_seconds: number;
|
|
24
|
+
reset_at: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CodexUsageResponse {
|
|
28
|
+
plan_type?: string;
|
|
29
|
+
rate_limit?: {
|
|
30
|
+
allowed: boolean;
|
|
31
|
+
limit_reached: boolean;
|
|
32
|
+
primary_window?: CodexUsageWindow | null;
|
|
33
|
+
secondary_window?: CodexUsageWindow | null;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Distinguishes auth failures from other errors so callers can notify the user. */
|
|
38
|
+
interface CodexFetchResult {
|
|
39
|
+
usage: CodexUsageResponse | null;
|
|
40
|
+
authExpired: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fmtInt(n: number): string {
|
|
44
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
45
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
46
|
+
return `${Math.round(n)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fmtMoney(n: number): string {
|
|
50
|
+
return `$${n.toFixed(3)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fmtUsedOfTotal(used: number, total?: number): string {
|
|
54
|
+
if (!total || total <= 0) return fmtInt(used);
|
|
55
|
+
return `${fmtInt(used)}/${fmtInt(total)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fmtWindowUsage(window?: CodexUsageWindow | null): string {
|
|
59
|
+
if (!window) return "n/a";
|
|
60
|
+
const used = Math.max(0, window.used_percent);
|
|
61
|
+
const left = Math.max(0, 100 - used);
|
|
62
|
+
return `${used.toFixed(1)}% used / ${left.toFixed(1)}% left`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fmtDuration(ms: number): string {
|
|
66
|
+
if (ms <= 0) return "now";
|
|
67
|
+
const totalSeconds = Math.ceil(ms / 1000);
|
|
68
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
69
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
70
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
71
|
+
if (days > 0) return `${days}d ${hours}h`;
|
|
72
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
73
|
+
return `${minutes}m`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getAssistantMessages(ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]) {
|
|
77
|
+
return ctx.sessionManager
|
|
78
|
+
.getBranch()
|
|
79
|
+
.filter((e): e is typeof e & { type: "message"; message: AssistantMessage } => e.type === "message" && e.message.role === "assistant")
|
|
80
|
+
.map((e) => e.message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readOpenAICodexAuth(): OpenAICodexAuth | null {
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(AUTH_PATH)) return null;
|
|
86
|
+
const data = JSON.parse(readFileSync(AUTH_PATH, "utf8")) as Record<string, unknown>;
|
|
87
|
+
const cred = data["openai-codex"] as Partial<OpenAICodexAuth> | undefined;
|
|
88
|
+
if (!cred || cred.type !== "oauth" || !cred.access || !cred.accountId) return null;
|
|
89
|
+
return cred as OpenAICodexAuth;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Validates that the parsed JSON structurally matches CodexUsageResponse. */
|
|
96
|
+
function isValidCodexResponse(obj: unknown): obj is CodexUsageResponse {
|
|
97
|
+
if (!obj || typeof obj !== "object") return false;
|
|
98
|
+
const record = obj as Record<string, unknown>;
|
|
99
|
+
// rate_limit is the only field we actually consume — require it to be present and object-shaped
|
|
100
|
+
if (record.rate_limit !== undefined && (typeof record.rate_limit !== "object" || record.rate_limit === null)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function fetchCodexUsage(): Promise<CodexFetchResult> {
|
|
107
|
+
const cred = readOpenAICodexAuth();
|
|
108
|
+
if (!cred) return { usage: null, authExpired: false };
|
|
109
|
+
|
|
110
|
+
return await new Promise<CodexFetchResult>((resolve) => {
|
|
111
|
+
let destroyed = false;
|
|
112
|
+
const req = httpsRequest(
|
|
113
|
+
"https://chatgpt.com/backend-api/codex/usage",
|
|
114
|
+
{
|
|
115
|
+
method: "GET",
|
|
116
|
+
headers: {
|
|
117
|
+
Authorization: `Bearer ${cred.access}`,
|
|
118
|
+
"chatgpt-account-id": cred.accountId,
|
|
119
|
+
"OpenAI-Beta": "responses=experimental",
|
|
120
|
+
"User-Agent": "pi-codex-footer"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
(res) => {
|
|
124
|
+
let body = "";
|
|
125
|
+
res.setEncoding("utf8");
|
|
126
|
+
res.on("data", (chunk) => {
|
|
127
|
+
body += chunk;
|
|
128
|
+
// Cap accumulated body to prevent memory exhaustion
|
|
129
|
+
if (body.length > MAX_RESPONSE_BYTES) {
|
|
130
|
+
destroyed = true;
|
|
131
|
+
req.destroy();
|
|
132
|
+
resolve({ usage: null, authExpired: false });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
res.on("end", () => {
|
|
136
|
+
if (destroyed) return;
|
|
137
|
+
const status = res.statusCode ?? 500;
|
|
138
|
+
|
|
139
|
+
// Surface auth failures distinctly so the UI can notify the user
|
|
140
|
+
if (status === 401 || status === 403) {
|
|
141
|
+
resolve({ usage: null, authExpired: true });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (status < 200 || status >= 300) {
|
|
145
|
+
resolve({ usage: null, authExpired: false });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const parsed: unknown = JSON.parse(body);
|
|
150
|
+
if (isValidCodexResponse(parsed)) {
|
|
151
|
+
resolve({ usage: parsed, authExpired: false });
|
|
152
|
+
} else {
|
|
153
|
+
resolve({ usage: null, authExpired: false });
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
resolve({ usage: null, authExpired: false });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
req.on("error", () => resolve({ usage: null, authExpired: false }));
|
|
162
|
+
req.setTimeout(15000, () => {
|
|
163
|
+
req.destroy();
|
|
164
|
+
resolve({ usage: null, authExpired: false });
|
|
165
|
+
});
|
|
166
|
+
req.end();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default function (pi: ExtensionAPI) {
|
|
171
|
+
let lastAssistantStart: number | null = null;
|
|
172
|
+
let lastTokensPerSecond: number | null = null;
|
|
173
|
+
let lastKnownCodexUsage: CodexUsageResponse | null = null;
|
|
174
|
+
let authExpiredNotified = false;
|
|
175
|
+
|
|
176
|
+
pi.on("message_start", async (event) => {
|
|
177
|
+
if (event.message.role === "assistant") lastAssistantStart = Date.now();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
pi.on("message_end", async (event) => {
|
|
181
|
+
if (event.message.role !== "assistant") return;
|
|
182
|
+
const m = event.message as AssistantMessage;
|
|
183
|
+
if (lastAssistantStart) {
|
|
184
|
+
const elapsedSeconds = Math.max((Date.now() - lastAssistantStart) / 1000, 0.001);
|
|
185
|
+
lastTokensPerSecond = m.usage.output / elapsedSeconds;
|
|
186
|
+
}
|
|
187
|
+
lastAssistantStart = null;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
191
|
+
let codexUsage: CodexUsageResponse | null = null;
|
|
192
|
+
let codexUsageFetchedAt = 0;
|
|
193
|
+
let codexUsageInFlight: Promise<void> | null = null;
|
|
194
|
+
|
|
195
|
+
const handleFetchResult = (result: CodexFetchResult) => {
|
|
196
|
+
codexUsage = result.usage;
|
|
197
|
+
lastKnownCodexUsage = result.usage;
|
|
198
|
+
codexUsageFetchedAt = Date.now();
|
|
199
|
+
if (result.authExpired && !authExpiredNotified) {
|
|
200
|
+
authExpiredNotified = true;
|
|
201
|
+
ctx.ui.notify("Codex quota: auth token expired or forbidden — re-authenticate with Pi to restore live quota data.", "warning");
|
|
202
|
+
} else if (!result.authExpired && result.usage) {
|
|
203
|
+
// Successful fetch clears the flag so a future expiry is reported again
|
|
204
|
+
authExpiredNotified = false;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const primeCodexUsage = async () => {
|
|
209
|
+
if (ctx.model?.provider !== "openai-codex") return;
|
|
210
|
+
if (codexUsageInFlight) return;
|
|
211
|
+
codexUsageInFlight = (async () => {
|
|
212
|
+
try {
|
|
213
|
+
handleFetchResult(await fetchCodexUsage());
|
|
214
|
+
} catch {
|
|
215
|
+
codexUsage = null;
|
|
216
|
+
lastKnownCodexUsage = null;
|
|
217
|
+
codexUsageFetchedAt = Date.now();
|
|
218
|
+
} finally {
|
|
219
|
+
codexUsageInFlight = null;
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
await codexUsageInFlight;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
void primeCodexUsage();
|
|
226
|
+
|
|
227
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
228
|
+
const refreshCodexUsage = () => {
|
|
229
|
+
if (codexUsageInFlight) return;
|
|
230
|
+
codexUsageInFlight = (async () => {
|
|
231
|
+
try {
|
|
232
|
+
handleFetchResult(await fetchCodexUsage());
|
|
233
|
+
} catch {
|
|
234
|
+
codexUsage = null;
|
|
235
|
+
lastKnownCodexUsage = null;
|
|
236
|
+
codexUsageFetchedAt = Date.now();
|
|
237
|
+
} finally {
|
|
238
|
+
codexUsageInFlight = null;
|
|
239
|
+
tui.requestRender();
|
|
240
|
+
}
|
|
241
|
+
})();
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
245
|
+
const interval = setInterval(() => tui.requestRender(), 1000);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
dispose: () => {
|
|
249
|
+
unsub();
|
|
250
|
+
clearInterval(interval);
|
|
251
|
+
},
|
|
252
|
+
invalidate() {},
|
|
253
|
+
render(width: number): string[] {
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const cwdName = basename(ctx.cwd) || ctx.cwd;
|
|
256
|
+
const branch = footerData.getGitBranch() || "no-git";
|
|
257
|
+
const provider = ctx.model?.provider || "no-provider";
|
|
258
|
+
const model = ctx.model?.id || "no-model";
|
|
259
|
+
const thinking = pi.getThinkingLevel();
|
|
260
|
+
const context = ctx.getContextUsage();
|
|
261
|
+
const isCodex = provider === "openai-codex";
|
|
262
|
+
|
|
263
|
+
if (isCodex && Date.now() - codexUsageFetchedAt > QUOTA_REFRESH_MS && !codexUsageInFlight) {
|
|
264
|
+
refreshCodexUsage();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const messages = getAssistantMessages(ctx);
|
|
268
|
+
const totalCost = messages.reduce((sum, m) => sum + (m.usage.cost.total || 0), 0);
|
|
269
|
+
const livePrimary = codexUsage?.rate_limit?.primary_window;
|
|
270
|
+
const liveSecondary = codexUsage?.rate_limit?.secondary_window;
|
|
271
|
+
const fiveHourResetText = isCodex && livePrimary
|
|
272
|
+
? fmtDuration(Math.max(0, livePrimary.reset_at * 1000 - now))
|
|
273
|
+
: "n/a";
|
|
274
|
+
const weeklyResetText = isCodex && liveSecondary
|
|
275
|
+
? fmtDuration(Math.max(0, liveSecondary.reset_at * 1000 - now))
|
|
276
|
+
: "n/a";
|
|
277
|
+
|
|
278
|
+
const sep = ` ${theme.fg("dim", "•")} `;
|
|
279
|
+
const line1 = [
|
|
280
|
+
`${theme.fg("dim", "📁")} ${cwdName}`,
|
|
281
|
+
`${theme.fg("dim", "")} ${branch}`,
|
|
282
|
+
`${theme.fg("dim", "🤖")} ${provider} - ${model}`,
|
|
283
|
+
`${theme.fg("dim", "🧠")} ${thinking}`,
|
|
284
|
+
`${theme.fg("dim", "◔")} ${context ? fmtUsedOfTotal(context.tokens || 0, context.contextWindow) : "n/a"}`,
|
|
285
|
+
`${theme.fg("dim", "💵")} ${fmtMoney(totalCost)}`
|
|
286
|
+
].join(sep);
|
|
287
|
+
|
|
288
|
+
const line2 = [
|
|
289
|
+
`${theme.fg("dim", "⚡")} ${lastTokensPerSecond ? `${lastTokensPerSecond.toFixed(1)} tok/s` : "n/a"}`,
|
|
290
|
+
`${theme.fg("dim", "5h")} ${isCodex ? fmtWindowUsage(livePrimary) : "n/a"}`,
|
|
291
|
+
`${theme.fg("dim", "7d")} ${isCodex ? fmtWindowUsage(liveSecondary) : "n/a"}`,
|
|
292
|
+
`${theme.fg("dim", "↺5h")} ${fiveHourResetText}`,
|
|
293
|
+
`${theme.fg("dim", "↺7d")} ${weeklyResetText}`
|
|
294
|
+
].join(sep);
|
|
295
|
+
|
|
296
|
+
return [truncateToWidth(line1, width), truncateToWidth(line2, width)];
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
pi.registerCommand("codex-footer-status", {
|
|
303
|
+
description: "Show live Codex footer quota status",
|
|
304
|
+
handler: async (_args, commandCtx) => {
|
|
305
|
+
const auth = readOpenAICodexAuth();
|
|
306
|
+
const result = await fetchCodexUsage();
|
|
307
|
+
const usage = result.usage ?? lastKnownCodexUsage;
|
|
308
|
+
const primary = usage?.rate_limit?.primary_window;
|
|
309
|
+
const secondary = usage?.rate_limit?.secondary_window;
|
|
310
|
+
const lines = [
|
|
311
|
+
`provider: ${commandCtx.model?.provider || "unknown"}`,
|
|
312
|
+
`auth: ${auth ? (result.authExpired ? "expired" : "found") : "missing"}`,
|
|
313
|
+
`plan: ${usage?.plan_type || "unknown"}`,
|
|
314
|
+
`5h: ${fmtWindowUsage(primary)}`,
|
|
315
|
+
`7d: ${fmtWindowUsage(secondary)}`,
|
|
316
|
+
`5h reset: ${primary ? fmtDuration(Math.max(0, primary.reset_at * 1000 - Date.now())) : "n/a"}`,
|
|
317
|
+
`7d reset: ${secondary ? fmtDuration(Math.max(0, secondary.reset_at * 1000 - Date.now())) : "n/a"}`
|
|
318
|
+
];
|
|
319
|
+
commandCtx.ui.notify(lines.join("\n"), usage ? "info" : "warning");
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
pi.registerCommand("codex-footer-off", {
|
|
324
|
+
description: "Restore Pi's default footer",
|
|
325
|
+
handler: async (_args, ctx) => {
|
|
326
|
+
ctx.ui.setFooter(undefined);
|
|
327
|
+
ctx.ui.notify("Default footer restored", "info");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
pi.registerCommand("codex-footer-on", {
|
|
332
|
+
description: "Enable the Codex quota footer",
|
|
333
|
+
handler: async (_args, ctx) => {
|
|
334
|
+
await ctx.reload();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-codex-footer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that adds a 2-line footer with live OpenAI Codex 5h/7d quota usage and reset timers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "glnarayanan",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/glnarayanan/pi-codex-footer.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/glnarayanan/pi-codex-footer#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/glnarayanan/pi-codex-footer/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi-extension",
|
|
18
|
+
"openai",
|
|
19
|
+
"codex",
|
|
20
|
+
"chatgpt",
|
|
21
|
+
"footer"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"files": [
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"extensions"
|
|
28
|
+
],
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./extensions/index.ts"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@mariozechner/pi-ai": "*",
|
|
36
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
37
|
+
"@mariozechner/pi-tui": "*"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "echo 'nothing to build'",
|
|
41
|
+
"check": "echo 'load in pi with pi -e ./extensions/index.ts or pi install ./pi-codex-footer'"
|
|
42
|
+
}
|
|
43
|
+
}
|