@diegopetrucci/pi-extensions 0.1.10 → 0.1.11

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 CHANGED
@@ -1,39 +1,33 @@
1
1
  # pi-extensions
2
2
 
3
- A collection of [pi](https://github.com/badlogic/pi-mono) agent extensions I made.
3
+ A collection of [pi](https://github.com/badlogic/pi-mono) agent extensions I made:
4
4
 
5
- ## Included extensions
5
+ - [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal two-line layout: branch/repo on the first line, context/model on the second, plus OpenAI Codex 5-hour and 7-day usage when available.
6
+ - [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings, sets reasoning to xhigh by default, and shows live status while running.
7
+ - [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
8
+ - [`confirm-destructive`](./extensions/confirm-destructive): Confirms before destructive session actions like clear, switch, and fork.
9
+ - [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
6
10
 
7
- | Extension | Description |
8
- |---|---|
9
- | [`minimal-footer`](./extensions/minimal-footer) | Replaces pi's built-in footer with a minimal two-line layout: branch/repo on the first line, context/model on the second. |
10
- | [`oracle`](./extensions/oracle) | Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings, sets reasoning to xhigh by default, and shows live status while running. |
11
- | [`permission-gate`](./extensions/permission-gate) | Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`. |
12
- | [`confirm-destructive`](./extensions/confirm-destructive) | Confirms before destructive session actions like clear, switch, and fork. |
13
- | [`notify`](./extensions/notify) | Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input. |
11
+ (For the full list of pi extensions I use, [check out my dotfiles](https://github.com/diegopetrucci/dot/blob/main/.pi/agent/settings.json).)
14
12
 
15
13
  ## Install
16
14
 
17
- ### GitHub
18
-
19
- Install the repo as a pi package:
15
+ Full collection:
20
16
 
21
17
  ```bash
22
- pi install git:github.com/diegopetrucci/pi-extensions
18
+ pi install npm:@diegopetrucci/pi-extensions
23
19
  ```
24
20
 
25
- Or pin to a tagged version:
21
+ Or pin the GitHub package to this release:
26
22
 
27
23
  ```bash
28
- pi install git:github.com/diegopetrucci/pi-extensions@v0.1.10
24
+ pi install git:github.com/diegopetrucci/pi-extensions@v0.1.11
29
25
  ```
30
26
 
31
- ### npm
32
-
33
- Install the full collection from npm:
27
+ Or a specific extension:
34
28
 
35
29
  ```bash
36
- pi install npm:@diegopetrucci/pi-extensions
30
+ pi install npm:@diegopetrucci/pi-oracle
37
31
  ```
38
32
 
39
33
  Then reload pi:
@@ -41,105 +35,3 @@ Then reload pi:
41
35
  ```text
42
36
  /reload
43
37
  ```
44
-
45
- ## Install only one extension
46
-
47
- If you only want one extension, you have two options.
48
-
49
- ### Option 1: install the standalone npm package
50
-
51
- ```bash
52
- pi install npm:@diegopetrucci/pi-minimal-footer
53
- ```
54
-
55
- ```bash
56
- pi install npm:@diegopetrucci/pi-oracle
57
- ```
58
-
59
- ```bash
60
- pi install npm:@diegopetrucci/pi-permission-gate
61
- ```
62
-
63
- ```bash
64
- pi install npm:@diegopetrucci/pi-confirm-destructive
65
- ```
66
-
67
- ```bash
68
- pi install npm:@diegopetrucci/pi-notify
69
- ```
70
-
71
- ### Option 2: filter the repo package
72
-
73
- If you prefer the collection package, you can filter it in your pi settings.
74
-
75
- Minimal footer only:
76
-
77
- ```json
78
- {
79
- "packages": [
80
- {
81
- "source": "npm:@diegopetrucci/pi-extensions",
82
- "extensions": ["extensions/minimal-footer/index.ts"]
83
- }
84
- ]
85
- }
86
- ```
87
-
88
- Oracle only:
89
-
90
- ```json
91
- {
92
- "packages": [
93
- {
94
- "source": "npm:@diegopetrucci/pi-extensions",
95
- "extensions": ["extensions/oracle/index.ts"]
96
- }
97
- ]
98
- }
99
- ```
100
-
101
- Permission gate only:
102
-
103
- ```json
104
- {
105
- "packages": [
106
- {
107
- "source": "npm:@diegopetrucci/pi-extensions",
108
- "extensions": ["extensions/permission-gate/index.ts"]
109
- }
110
- ]
111
- }
112
- ```
113
-
114
- Confirm destructive only:
115
-
116
- ```json
117
- {
118
- "packages": [
119
- {
120
- "source": "npm:@diegopetrucci/pi-extensions",
121
- "extensions": ["extensions/confirm-destructive/index.ts"]
122
- }
123
- ]
124
- }
125
- ```
126
-
127
- Notify only:
128
-
129
- ```json
130
- {
131
- "packages": [
132
- {
133
- "source": "npm:@diegopetrucci/pi-extensions",
134
- "extensions": ["extensions/notify/index.ts"]
135
- }
136
- ]
137
- }
138
- ```
139
-
140
- ## npm publishing
141
-
142
- The repo is set up to support both:
143
-
144
- - the collection package: `@diegopetrucci/pi-extensions`
145
- - standalone extension packages like `@diegopetrucci/pi-minimal-footer`, `@diegopetrucci/pi-oracle`, `@diegopetrucci/pi-permission-gate`, `@diegopetrucci/pi-confirm-destructive`, and `@diegopetrucci/pi-notify`
Binary file
@@ -10,6 +10,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
10
10
  - current repo name
11
11
  - current context percentage
12
12
  - current model and thinking level
13
+ - OpenAI Codex 5-hour and 7-day usage when available
13
14
 
14
15
  ## Layout
15
16
 
@@ -27,6 +28,12 @@ fix/remove-detached-image-tasks SendItToMy
27
28
  44.1% gpt-5.4 high
28
29
  ```
29
30
 
31
+ When using `openai-codex`, the bottom-left line also includes subscription usage:
32
+
33
+ ```text
34
+ 44.1% · 5h 12% · 7d 38%
35
+ ```
36
+
30
37
  On narrow terminals it falls back to one item per line.
31
38
 
32
39
  ## Install
@@ -60,6 +67,7 @@ Then reload pi:
60
67
  - **Top left:** current git branch
61
68
  - **Top right:** current repo directory name
62
69
  - **Bottom left:** current context usage percentage
70
+ - **Bottom left on `openai-codex`:** current context usage percentage plus 5-hour and 7-day Codex usage
63
71
  - **Bottom right:** model id and thinking level
64
72
 
65
73
  ## Publishing notes
@@ -72,3 +80,5 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
72
80
  - Uses pi footer data for git branch updates.
73
81
  - Shows only context percentage, not context window size.
74
82
  - Shows the model id rather than a provider-specific display label.
83
+ - For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint.
84
+ - Usage is cached briefly in memory and refreshed after turns.
@@ -1,14 +1,109 @@
1
1
  import { basename } from "node:path";
2
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ AuthStorage,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@mariozechner/pi-coding-agent";
3
7
  import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
8
+ import {
9
+ fetchOpenAICodexUsage,
10
+ formatUsageSummary,
11
+ isOpenAICodexProvider,
12
+ type UsageSnapshot,
13
+ } from "./openai-usage";
14
+
15
+ const USAGE_CACHE_TTL_MS = 5 * 60 * 1000;
16
+ const USAGE_REQUEST_TIMEOUT_MS = 10 * 1000;
17
+
18
+ type UsageSessionState = {
19
+ authStorage: AuthStorage;
20
+ snapshot?: UsageSnapshot;
21
+ lastFetchedAt?: number;
22
+ loading: boolean;
23
+ error?: string;
24
+ inflight?: Promise<void>;
25
+ requestRender?: () => void;
26
+ };
27
+
28
+ function clearUsageState(state: UsageSessionState): void {
29
+ state.snapshot = undefined;
30
+ state.lastFetchedAt = undefined;
31
+ state.loading = false;
32
+ state.error = undefined;
33
+ }
34
+
35
+ async function refreshUsageIfNeeded(
36
+ ctx: ExtensionContext,
37
+ state: UsageSessionState,
38
+ force = false,
39
+ ): Promise<void> {
40
+ if (!isOpenAICodexProvider(ctx.model?.provider)) {
41
+ clearUsageState(state);
42
+ state.requestRender?.();
43
+ return;
44
+ }
45
+
46
+ const now = Date.now();
47
+ if (
48
+ !force &&
49
+ state.lastFetchedAt &&
50
+ now - state.lastFetchedAt < USAGE_CACHE_TTL_MS
51
+ ) {
52
+ return;
53
+ }
54
+
55
+ if (state.inflight) {
56
+ return state.inflight;
57
+ }
58
+
59
+ state.loading = true;
60
+ state.requestRender?.();
61
+ state.inflight = (async () => {
62
+ try {
63
+ const snapshot = await fetchOpenAICodexUsage(state.authStorage, {
64
+ timeoutMs: USAGE_REQUEST_TIMEOUT_MS,
65
+ });
66
+ if (snapshot) {
67
+ state.snapshot = snapshot;
68
+ state.lastFetchedAt = snapshot.fetchedAt;
69
+ state.error = undefined;
70
+ } else {
71
+ state.snapshot = undefined;
72
+ state.lastFetchedAt = Date.now();
73
+ state.error = undefined;
74
+ }
75
+ } catch (error) {
76
+ state.error = error instanceof Error ? error.message : String(error);
77
+ } finally {
78
+ state.loading = false;
79
+ state.inflight = undefined;
80
+ state.requestRender?.();
81
+ }
82
+ })();
83
+
84
+ return state.inflight;
85
+ }
4
86
 
5
87
  export default function (pi: ExtensionAPI) {
6
- pi.on("session_start", async (_event, ctx) => {
88
+ const states = new WeakMap<object, UsageSessionState>();
89
+
90
+ pi.on("session_start", (_event, ctx) => {
91
+ const state: UsageSessionState = {
92
+ authStorage: AuthStorage.create(),
93
+ loading: false,
94
+ };
95
+ states.set(ctx.sessionManager, state);
96
+
7
97
  ctx.ui.setFooter((tui, theme, footerData) => {
8
98
  const unsub = footerData.onBranchChange(() => tui.requestRender());
99
+ state.requestRender = () => tui.requestRender();
100
+ void refreshUsageIfNeeded(ctx, state);
9
101
 
10
102
  return {
11
- dispose: unsub,
103
+ dispose() {
104
+ if (state.requestRender) state.requestRender = undefined;
105
+ unsub();
106
+ },
12
107
  invalidate() {},
13
108
  render(width: number): string[] {
14
109
  const repo = basename(ctx.cwd);
@@ -16,6 +111,10 @@ export default function (pi: ExtensionAPI) {
16
111
 
17
112
  const usage = ctx.getContextUsage();
18
113
  const context = usage?.percent == null ? "?" : `${usage.percent.toFixed(1)}%`;
114
+ const usageSummary = isOpenAICodexProvider(ctx.model?.provider)
115
+ ? formatUsageSummary(state.snapshot)
116
+ : undefined;
117
+ const contextText = usageSummary ? `${context} · ${usageSummary}` : context;
19
118
 
20
119
  const model = ctx.model?.id ?? "no-model";
21
120
  const thinking = pi.getThinkingLevel();
@@ -23,7 +122,7 @@ export default function (pi: ExtensionAPI) {
23
122
 
24
123
  const branchStyled = theme.fg("dim", branch);
25
124
  const repoStyled = theme.fg("dim", repo);
26
- const contextStyled = theme.fg("dim", context);
125
+ const contextStyled = theme.fg("dim", contextText);
27
126
  const modelStyled = theme.fg("dim", modelText);
28
127
 
29
128
  const renderSplitLine = (left: string, right: string): string => {
@@ -51,4 +150,16 @@ export default function (pi: ExtensionAPI) {
51
150
  };
52
151
  });
53
152
  });
153
+
154
+ pi.on("model_select", (_event, ctx) => {
155
+ const state = states.get(ctx.sessionManager);
156
+ if (!state) return;
157
+ void refreshUsageIfNeeded(ctx, state, true);
158
+ });
159
+
160
+ pi.on("turn_end", (_event, ctx) => {
161
+ const state = states.get(ctx.sessionManager);
162
+ if (!state) return;
163
+ void refreshUsageIfNeeded(ctx, state);
164
+ });
54
165
  }
@@ -0,0 +1,121 @@
1
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
2
+
3
+ const PROVIDER_ID = "openai-codex";
4
+
5
+ interface WhamUsageWindow {
6
+ reset_at?: number;
7
+ used_percent?: number;
8
+ }
9
+
10
+ interface WhamUsageResponse {
11
+ rate_limit?: {
12
+ primary_window?: WhamUsageWindow;
13
+ secondary_window?: WhamUsageWindow;
14
+ };
15
+ }
16
+
17
+ export interface UsageWindow {
18
+ usedPercent?: number;
19
+ resetAt?: number;
20
+ }
21
+
22
+ export interface UsageSnapshot {
23
+ primary?: UsageWindow;
24
+ secondary?: UsageWindow;
25
+ fetchedAt: number;
26
+ }
27
+
28
+ function normalizeUsedPercent(value?: number): number | undefined {
29
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
30
+ return Math.min(100, Math.max(0, value));
31
+ }
32
+
33
+ function normalizeResetAt(value?: number): number | undefined {
34
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
35
+ return value * 1000;
36
+ }
37
+
38
+ function parseUsageWindow(window?: WhamUsageWindow): UsageWindow | undefined {
39
+ if (!window) return undefined;
40
+ const usedPercent = normalizeUsedPercent(window.used_percent);
41
+ const resetAt = normalizeResetAt(window.reset_at);
42
+ if (usedPercent === undefined && resetAt === undefined) return undefined;
43
+ return { usedPercent, resetAt };
44
+ }
45
+
46
+ function parseUsageSnapshot(data: WhamUsageResponse): Omit<UsageSnapshot, "fetchedAt"> {
47
+ return {
48
+ primary: parseUsageWindow(data.rate_limit?.primary_window),
49
+ secondary: parseUsageWindow(data.rate_limit?.secondary_window),
50
+ };
51
+ }
52
+
53
+ function getOAuthAccountId(authStorage: AuthStorage): string | undefined {
54
+ const credential = authStorage.get(PROVIDER_ID);
55
+ if (!credential || credential.type !== "oauth") return undefined;
56
+ const accountId = (credential as { accountId?: unknown }).accountId;
57
+ return typeof accountId === "string" && accountId.trim()
58
+ ? accountId.trim()
59
+ : undefined;
60
+ }
61
+
62
+ function formatUsagePercent(value?: number): string | undefined {
63
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
64
+ return `${Math.round(value)}%`;
65
+ }
66
+
67
+ export function isOpenAICodexProvider(provider?: string): boolean {
68
+ return provider === PROVIDER_ID;
69
+ }
70
+
71
+ export function formatUsageSummary(snapshot?: UsageSnapshot): string | undefined {
72
+ if (!snapshot) return undefined;
73
+
74
+ const primary = formatUsagePercent(snapshot.primary?.usedPercent);
75
+ const secondary = formatUsagePercent(snapshot.secondary?.usedPercent);
76
+ const parts: string[] = [];
77
+
78
+ if (primary) parts.push(`5h ${primary}`);
79
+ if (secondary) parts.push(`7d ${secondary}`);
80
+
81
+ return parts.length > 0 ? parts.join(" · ") : undefined;
82
+ }
83
+
84
+ export async function fetchOpenAICodexUsage(
85
+ authStorage: AuthStorage,
86
+ options?: { timeoutMs?: number },
87
+ ): Promise<UsageSnapshot | undefined> {
88
+ const accessToken = await authStorage.getApiKey(PROVIDER_ID, {
89
+ includeFallback: false,
90
+ });
91
+ if (!accessToken) return undefined;
92
+
93
+ authStorage.reload();
94
+ const accountId = getOAuthAccountId(authStorage);
95
+ const controller = new AbortController();
96
+ const timeoutMs = options?.timeoutMs ?? 10_000;
97
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
98
+
99
+ try {
100
+ const headers: Record<string, string> = {
101
+ Authorization: `Bearer ${accessToken}`,
102
+ Accept: "application/json",
103
+ };
104
+ if (accountId) {
105
+ headers["ChatGPT-Account-Id"] = accountId;
106
+ }
107
+
108
+ const response = await fetch("https://chatgpt.com/backend-api/wham/usage", {
109
+ headers,
110
+ signal: controller.signal,
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`Usage request failed: ${response.status}`);
114
+ }
115
+
116
+ const data = (await response.json()) as WhamUsageResponse;
117
+ return { ...parseUsageSnapshot(data), fetchedAt: Date.now() };
118
+ } finally {
119
+ clearTimeout(timeout);
120
+ }
121
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": ["pi-package", "pi", "terminal", "footer"],
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "index.ts",
14
+ "openai-usage.ts",
14
15
  "README.md"
15
16
  ],
16
17
  "publishConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "A collection of pi extensions, including a minimal custom footer, an Amp-style oracle, a permission gate for dangerous bash commands, confirm-before-destructive session actions, and terminal notifications when pi is ready for input.",
5
5
  "keywords": ["pi-package", "pi", "terminal", "agent"],
6
6
  "license": "MIT",