@latentminds/pi-quotas 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Latent Minds Pty Ltd
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,94 @@
1
+ <p align="center">
2
+ <img src="docs/latent-minds@2x.png" alt="Latent Minds" width="320" />
3
+ </p>
4
+
5
+ # @latentminds/pi-quotas
6
+
7
+ Quota monitoring for the [Pi coding agent](https://github.com/mariozechner/pi). Shows remaining usage and rate limits for Anthropic, OpenAI Codex, and GitHub Copilot — directly in your Pi session.
8
+
9
+ ## Screenshots
10
+
11
+ | `/quotas` dashboard | Footer status |
12
+ |---|---|
13
+ | ![Quotas dashboard](docs/quotas-dashboard.png) | ![Footer status](docs/footer-status.png) |
14
+
15
+ ## Install
16
+
17
+ **From npm** (recommended):
18
+
19
+ ```bash
20
+ pi install npm:@latentminds/pi-quotas
21
+ ```
22
+
23
+ **From source:**
24
+
25
+ ```bash
26
+ git clone https://github.com/latentminds-ai/pi-quotas.git
27
+ pi install ./pi-quotas
28
+ ```
29
+
30
+ **Try without installing:**
31
+
32
+ ```bash
33
+ pi -e npm:@latentminds/pi-quotas
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ | Command | Description |
39
+ |---------|-------------|
40
+ | `/quotas` | Combined quota dashboard for all providers |
41
+ | `/anthropic:quotas` | Anthropic quotas only |
42
+ | `/codex:quotas` | OpenAI Codex quotas only |
43
+ | `/github:quotas` | GitHub Copilot quotas only |
44
+ | `/quotas:settings` | Toggle individual features on or off |
45
+
46
+ ## Features
47
+
48
+ ### Quota dashboard
49
+
50
+ Run `/quotas` to open a bordered TUI view showing all providers side by side, with progress bars, used/remaining counts, and reset times. Press `r` to refresh, `q` or `Esc` to close.
51
+
52
+ ### Footer status widget
53
+
54
+ When your active model is from a supported provider, the Pi footer shows real-time quota headroom — updated every 60 seconds and on each turn. Colours shift from green → amber → red as usage climbs.
55
+
56
+ ### Quota warnings
57
+
58
+ Automatic notifications when projected usage is on track to exceed limits before the window resets. Warnings escalate from `warning` → `high` → `critical` based on your consumption pace.
59
+
60
+ ### Per-feature toggles
61
+
62
+ Use `/quotas:settings` to enable or disable:
63
+ - Combined `/quotas` command
64
+ - Per-provider commands (`/anthropic:quotas`, `/codex:quotas`, `/github:quotas`)
65
+ - Footer status widget
66
+ - Quota warning notifications
67
+
68
+ Settings can be saved globally (`~/.pi/agent/extensions/quotas.json`) or per-project (`.pi/quotas.json`). Run `/reload` after changing command visibility.
69
+
70
+ ## Supported providers
71
+
72
+ | Provider | Windows | Details |
73
+ |----------|---------|---------|
74
+ | Anthropic | 5h, 7d, per-model 7d, extra usage | Utilization percentages; optional overage budget in local currency |
75
+ | OpenAI Codex | 5h, 7d, credits, spend cap | Rate-limit percentages; credit balance; spend-cap reached/OK |
76
+ | GitHub Copilot | Premium/chat/completions per month | Remaining/entitlement counts with overage indicators |
77
+
78
+ ## Credentials
79
+
80
+ pi-quotas reads existing Pi auth entries from `~/.pi/agent/auth.json`:
81
+
82
+ - `anthropic` — Anthropic OAuth token
83
+ - `openai-codex` — Codex access token (also reads `~/.codex/auth.json` for the account ID)
84
+ - `github-copilot` — GitHub Copilot OAuth token (falls back to `gh auth token` if needed)
85
+
86
+ No additional setup is required — if Pi can use the provider, pi-quotas can check its quotas.
87
+
88
+ ## Requirements
89
+
90
+ - [Pi](https://github.com/mariozechner/pi) >= 0.61.0
91
+
92
+ ## License
93
+
94
+ [MIT](LICENSE) © Latent Minds Pty Ltd
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@latentminds/pi-quotas",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "private": false,
7
+ "author": "Markus Strazds <markus@latentminds.ai>",
8
+ "keywords": ["pi-package", "pi-extension", "pi"],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/latentminds-ai/pi-quotas"
12
+ },
13
+ "pi": {
14
+ "extensions": [
15
+ "./src/extensions/core/index.ts",
16
+ "./src/extensions/command-quotas/index.ts",
17
+ "./src/extensions/usage-status/index.ts",
18
+ "./src/extensions/quota-warnings/index.ts"
19
+ ],
20
+ "image": "https://raw.githubusercontent.com/latentminds-ai/pi-quotas/main/docs/quotas-dashboard.png"
21
+ },
22
+ "files": ["src", "README.md", "LICENSE"],
23
+ "peerDependencies": {
24
+ "@mariozechner/pi-coding-agent": "*",
25
+ "@mariozechner/pi-tui": "*"
26
+ },
27
+ "peerDependenciesMeta": {
28
+ "@mariozechner/pi-coding-agent": { "optional": true },
29
+ "@mariozechner/pi-tui": { "optional": true }
30
+ },
31
+ "devDependencies": {
32
+ "@mariozechner/pi-coding-agent": "0.61.0",
33
+ "@mariozechner/pi-tui": "0.61.0",
34
+ "@sinclair/typebox": "^0.34.48",
35
+ "@types/node": "^25.0.10",
36
+ "typescript": "^5.9.3",
37
+ "vitest": "^4.0.18"
38
+ },
39
+ "scripts": {
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest"
43
+ }
44
+ }
package/src/config.ts ADDED
@@ -0,0 +1,216 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import pkg from "../package.json" with { type: "json" };
7
+
8
+ export type QuotasFeatureId =
9
+ | "quotasCommand"
10
+ | "providerCommands"
11
+ | "usageStatus"
12
+ | "quotaWarnings";
13
+
14
+ export const QUOTAS_EXTENSIONS_REQUEST_EVENT =
15
+ "quotas:extensions:request" as const;
16
+ export const QUOTAS_EXTENSIONS_REGISTER_EVENT =
17
+ "quotas:extensions:register" as const;
18
+ export const QUOTAS_CONFIG_UPDATED_EVENT =
19
+ "quotas:config:updated" as const;
20
+
21
+ export interface QuotasExtensionsRegisterPayload {
22
+ feature: QuotasFeatureId;
23
+ }
24
+
25
+ export interface QuotasConfig {
26
+ configVersion?: string;
27
+ quotasCommand?: boolean;
28
+ providerCommands?: boolean;
29
+ usageStatus?: boolean;
30
+ quotaWarnings?: boolean;
31
+ }
32
+
33
+ export interface ResolvedQuotasConfig {
34
+ configVersion: string;
35
+ quotasCommand: boolean;
36
+ providerCommands: boolean;
37
+ usageStatus: boolean;
38
+ quotaWarnings: boolean;
39
+ }
40
+
41
+ const DEFAULT_CONFIG: ResolvedQuotasConfig = {
42
+ configVersion: pkg.version,
43
+ quotasCommand: true,
44
+ providerCommands: true,
45
+ usageStatus: true,
46
+ quotaWarnings: true,
47
+ };
48
+
49
+ let pendingMigrationNotice = false;
50
+
51
+ function markMigrationNoticePending(): void {
52
+ pendingMigrationNotice = true;
53
+ }
54
+
55
+ export function hasPendingMigrationNotice(): boolean {
56
+ return pendingMigrationNotice;
57
+ }
58
+
59
+ export function clearPendingMigrationNotice(): void {
60
+ pendingMigrationNotice = false;
61
+ }
62
+
63
+ class QuotasConfigStore {
64
+ private config: ResolvedQuotasConfig = DEFAULT_CONFIG;
65
+ private cwd = process.cwd();
66
+
67
+ private globalPath(): string {
68
+ return join(homedir(), ".pi", "agent", "extensions", "quotas.json");
69
+ }
70
+
71
+ private localPath(): string {
72
+ return join(this.cwd, ".pi", "quotas.json");
73
+ }
74
+
75
+ private resolve(input?: QuotasConfig): ResolvedQuotasConfig {
76
+ return {
77
+ configVersion: input?.configVersion ?? DEFAULT_CONFIG.configVersion,
78
+ quotasCommand: input?.quotasCommand ?? DEFAULT_CONFIG.quotasCommand,
79
+ providerCommands: input?.providerCommands ?? DEFAULT_CONFIG.providerCommands,
80
+ usageStatus: input?.usageStatus ?? DEFAULT_CONFIG.usageStatus,
81
+ quotaWarnings: input?.quotaWarnings ?? DEFAULT_CONFIG.quotaWarnings,
82
+ };
83
+ }
84
+
85
+ private async readConfig(path: string): Promise<QuotasConfig | undefined> {
86
+ try {
87
+ const data = JSON.parse(await readFile(path, "utf8")) as QuotasConfig;
88
+ return data;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ async load(cwd = process.cwd()): Promise<void> {
95
+ this.cwd = cwd;
96
+ const global = await this.readConfig(this.globalPath());
97
+ const local = await this.readConfig(this.localPath());
98
+ const merged = { ...global, ...local };
99
+ if (!global && !local) markMigrationNoticePending();
100
+ this.config = this.resolve(merged);
101
+ }
102
+
103
+ getConfig(): ResolvedQuotasConfig {
104
+ return this.config;
105
+ }
106
+
107
+ hasConfig(scope: "global" | "local"): boolean {
108
+ const path = scope === "global" ? this.globalPath() : this.localPath();
109
+ return existsSync(path);
110
+ }
111
+
112
+ async save(scope: "global" | "local", config: QuotasConfig): Promise<void> {
113
+ const path = scope === "global" ? this.globalPath() : this.localPath();
114
+ await mkdir(dirname(path), { recursive: true });
115
+ await writeFile(path, JSON.stringify(this.resolve(config), null, 2) + "\n", "utf8");
116
+ await this.load(this.cwd);
117
+ }
118
+ }
119
+
120
+ export const configLoader = new QuotasConfigStore();
121
+
122
+ export async function seedQuotasConfigIfMissing(): Promise<void> {
123
+ if (configLoader.hasConfig("global") || configLoader.hasConfig("local")) return;
124
+ markMigrationNoticePending();
125
+ try {
126
+ await configLoader.save("global", DEFAULT_CONFIG);
127
+ } catch {
128
+ // ignore
129
+ }
130
+ }
131
+
132
+ export interface QuotasConfigUpdatedPayload {
133
+ config: ResolvedQuotasConfig;
134
+ }
135
+
136
+ export function emitQuotasConfigUpdated(pi: ExtensionAPI): void {
137
+ pi.events.emit(QUOTAS_CONFIG_UPDATED_EVENT, {
138
+ config: configLoader.getConfig(),
139
+ });
140
+ }
141
+
142
+ const FEATURE_META: Array<{
143
+ id: QuotasFeatureId;
144
+ label: string;
145
+ description: string;
146
+ }> = [
147
+ {
148
+ id: "quotasCommand",
149
+ label: "Combined quotas command",
150
+ description: "Toggle the `/quotas` command",
151
+ },
152
+ {
153
+ id: "providerCommands",
154
+ label: "Provider quota commands",
155
+ description: "Toggle `/anthropic:quotas`, `/codex:quotas`, and `/github:quotas`",
156
+ },
157
+ {
158
+ id: "usageStatus",
159
+ label: "Usage status",
160
+ description: "Toggle footer quota status for the active provider",
161
+ },
162
+ {
163
+ id: "quotaWarnings",
164
+ label: "Quota warnings",
165
+ description: "Toggle projected-usage warning notifications",
166
+ },
167
+ ];
168
+
169
+ export function registerQuotasSettings(
170
+ pi: ExtensionAPI,
171
+ getLoadedFeatures: () => Set<QuotasFeatureId>,
172
+ ): void {
173
+ pi.registerCommand("quotas:settings", {
174
+ description: "Configure quota extension settings",
175
+ handler: async (_args, ctx) => {
176
+ await configLoader.load(ctx.cwd);
177
+ const scopeChoice = await ctx.ui.select("Save settings to", [
178
+ "global",
179
+ "local",
180
+ "cancel",
181
+ ]);
182
+ if (!scopeChoice || scopeChoice === "cancel") return;
183
+ const scope = scopeChoice as "global" | "local";
184
+ const draft: ResolvedQuotasConfig = { ...configLoader.getConfig() };
185
+
186
+ while (true) {
187
+ const choices = FEATURE_META.map((feature) => {
188
+ const loaded = getLoadedFeatures().has(feature.id) ? "" : " (not loaded)";
189
+ const enabled = draft[feature.id] ? "enabled" : "disabled";
190
+ return `${feature.label}: ${enabled}${loaded}`;
191
+ });
192
+ choices.push("Save and exit", "Cancel");
193
+
194
+ const selected = await ctx.ui.select("Quotas Settings", choices);
195
+ if (!selected || selected === "Cancel") return;
196
+ if (selected === "Save and exit") {
197
+ await configLoader.save(scope, draft);
198
+ emitQuotasConfigUpdated(pi);
199
+ ctx.ui.notify(
200
+ "Quota settings saved. Run /reload to fully apply command visibility changes.",
201
+ "info",
202
+ );
203
+ return;
204
+ }
205
+
206
+ const feature = FEATURE_META.find((item) => selected.startsWith(item.label));
207
+ if (!feature) continue;
208
+ if (!getLoadedFeatures().has(feature.id)) {
209
+ ctx.ui.notify(`${feature.label} is not loaded by Pi in this session.`, "warning");
210
+ continue;
211
+ }
212
+ draft[feature.id] = !draft[feature.id];
213
+ }
214
+ },
215
+ });
216
+ }
@@ -0,0 +1,122 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ QUOTAS_EXTENSIONS_REGISTER_EVENT,
4
+ QUOTAS_EXTENSIONS_REQUEST_EVENT,
5
+ configLoader,
6
+ } from "../../config.js";
7
+ import {
8
+ fetchAllProviderQuotas,
9
+ fetchProviderQuotas,
10
+ SUPPORTED_PROVIDERS,
11
+ } from "../../lib/quotas.js";
12
+ import type { QuotasResult, SupportedQuotaProvider } from "../../types/quotas.js";
13
+ import { QuotasComponent } from "./components/quotas-display.js";
14
+ import { getProviderCommandInfo } from "./provider-commands.js";
15
+
16
+ type Snapshot = { provider: SupportedQuotaProvider; result: QuotasResult };
17
+
18
+ async function openQuotaView(
19
+ title: string,
20
+ loadSnapshots: (force: boolean, signal?: AbortSignal) => Promise<Snapshot[]>,
21
+ ctx: ExtensionCommandContext,
22
+ ): Promise<void> {
23
+ const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
24
+ const controller = new AbortController();
25
+ const component = new QuotasComponent(
26
+ theme,
27
+ tui,
28
+ title,
29
+ () => {
30
+ controller.abort();
31
+ done(null);
32
+ },
33
+ () => {
34
+ component.setState({ type: "loading" });
35
+ tui.requestRender();
36
+ void load(true);
37
+ },
38
+ );
39
+
40
+ async function load(force = false): Promise<void> {
41
+ const snapshots = await loadSnapshots(force, controller.signal);
42
+ if (controller.signal.aborted) return;
43
+ component.setState({ type: "loaded", snapshots });
44
+ tui.requestRender();
45
+ }
46
+
47
+ void load();
48
+
49
+ return {
50
+ render: (width: number) => component.render(width),
51
+ invalidate: () => component.invalidate(),
52
+ handleInput: (data: string) => component.handleInput(data),
53
+ dispose: () => {
54
+ controller.abort();
55
+ component.destroy();
56
+ },
57
+ };
58
+ });
59
+
60
+ if (result === undefined) {
61
+ const snapshots = await loadSnapshots(true);
62
+ ctx.ui.notify(JSON.stringify(snapshots, null, 2), "info");
63
+ }
64
+ }
65
+
66
+ export function registerQuotasCommands(pi: ExtensionAPI): void {
67
+ pi.registerCommand("quotas", {
68
+ description: "Display remaining quotas for Anthropic, Codex, and GitHub Copilot",
69
+ handler: async (_args, ctx) => {
70
+ if (!configLoader.getConfig().quotasCommand) {
71
+ ctx.ui.notify("/quotas is disabled. Re-enable it in /quotas:settings.", "warning");
72
+ return;
73
+ }
74
+ await openQuotaView(
75
+ "Provider Quotas",
76
+ (force, signal) => fetchAllProviderQuotas(ctx.modelRegistry.authStorage, { force, signal }),
77
+ ctx,
78
+ );
79
+ },
80
+ });
81
+
82
+ for (const provider of SUPPORTED_PROVIDERS) {
83
+ const info = getProviderCommandInfo(provider);
84
+ pi.registerCommand(info.commandName, {
85
+ description: `Display remaining ${info.title.toLowerCase()}`,
86
+ handler: async (_args, ctx) => {
87
+ if (!configLoader.getConfig().providerCommands) {
88
+ ctx.ui.notify(`${info.commandName} is disabled. Re-enable it in /quotas:settings.`, "warning");
89
+ return;
90
+ }
91
+ await openQuotaView(
92
+ info.title,
93
+ async (force, signal) => [
94
+ {
95
+ provider,
96
+ result: await fetchProviderQuotas(ctx.modelRegistry.authStorage, provider, { force, signal }),
97
+ },
98
+ ],
99
+ ctx,
100
+ );
101
+ },
102
+ });
103
+ }
104
+ }
105
+
106
+ export default async function (pi: ExtensionAPI) {
107
+ await configLoader.load();
108
+
109
+ const config = configLoader.getConfig();
110
+ if (config.quotasCommand || config.providerCommands) {
111
+ registerQuotasCommands(pi);
112
+ }
113
+
114
+ pi.events.on(QUOTAS_EXTENSIONS_REQUEST_EVENT, () => {
115
+ if (configLoader.getConfig().quotasCommand) {
116
+ pi.events.emit(QUOTAS_EXTENSIONS_REGISTER_EVENT, { feature: "quotasCommand" });
117
+ }
118
+ if (configLoader.getConfig().providerCommands) {
119
+ pi.events.emit(QUOTAS_EXTENSIONS_REGISTER_EVENT, { feature: "providerCommands" });
120
+ }
121
+ });
122
+ }