Initial release: Anthropic subscription usage footer extension
This commit is contained in:
commit
4d57e293e6
|
|
@ -0,0 +1,32 @@
|
|||
# pi-anthropic-usage
|
||||
|
||||
Pi agent extension that shows your **Anthropic claude.ai Pro/Max subscription usage** live in the TUI footer.
|
||||
|
||||
## What it shows
|
||||
|
||||
```
|
||||
☁ 5h █████░░░ 62% ↺1h23m 7d ██░░░░░░ 18% ↺4d2h
|
||||
```
|
||||
|
||||
- **5h** — 5-hour rolling rate limit window
|
||||
- **7d** — 7-day rolling rate limit window
|
||||
- Color-coded: green → yellow (≥70%) → red (≥90%)
|
||||
- Reset timer shows when each window clears
|
||||
|
||||
Only active when using an Anthropic claude.ai subscription (OAuth token). Regular API keys → no display.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pi install git:git.vpsj.de/jay/pi-anthropic-usage
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|----------|--------------------------------------|
|
||||
| `/usage` | Manual refresh with detailed output |
|
||||
|
||||
## Auto-updates
|
||||
|
||||
Usage refreshes automatically after every model response and on model switch.
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Anthropic Subscription Usage Extension
|
||||
*
|
||||
* Zeigt die aktuellen Rate-Limit-Auslastungen für Anthropic claude.ai
|
||||
* Pro/Max Subscriptions live im TUI Footer an.
|
||||
*
|
||||
* Endpunkt: https://api.anthropic.com/api/oauth/usage
|
||||
* Nur aktiv wenn OAuth-Token vorhanden (sk-ant-oat... = Subscription).
|
||||
*
|
||||
* Anzeige: Footer-Status via ctx.ui.setStatus()
|
||||
* Update: nach agent_end + model_select + /usage Command
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
||||
const STATUS_KEY = "anthropic-usage";
|
||||
const FETCH_TIMEOUT_MS = 8000;
|
||||
|
||||
// ─── Typen ───────────────────────────────────────────────────────────────────
|
||||
|
||||
interface RateWindow {
|
||||
utilization: number; // 0–100
|
||||
resets_at: string | null; // ISO-8601 oder null
|
||||
}
|
||||
|
||||
interface UsageResponse {
|
||||
five_hour?: RateWindow | null;
|
||||
seven_day?: RateWindow | null;
|
||||
seven_day_opus?: RateWindow | null;
|
||||
seven_day_sonnet?: RateWindow | null;
|
||||
[key: string]: RateWindow | null | undefined;
|
||||
}
|
||||
|
||||
// ─── Hilfsfunktionen ─────────────────────────────────────────────────────────
|
||||
|
||||
function formatTimeUntil(isoString: string | null): string {
|
||||
if (!isoString) return "—";
|
||||
const diffMs = new Date(isoString).getTime() - Date.now();
|
||||
if (diffMs <= 0) return "jetzt";
|
||||
const totalMin = Math.floor(diffMs / 60_000);
|
||||
const h = Math.floor(totalMin / 60);
|
||||
const m = totalMin % 60;
|
||||
const d = Math.floor(h / 24);
|
||||
if (d > 0) return `${d}d${h % 24}h`;
|
||||
if (h === 0) return `${m}m`;
|
||||
if (m === 0) return `${h}h`;
|
||||
return `${h}h${m}m`;
|
||||
}
|
||||
|
||||
function bar(pct: number, width = 8): string {
|
||||
const filled = Math.round((pct / 100) * width);
|
||||
return "█".repeat(filled) + "░".repeat(width - filled);
|
||||
}
|
||||
|
||||
function colorForPct(pct: number, text: string, theme: any): string {
|
||||
if (pct >= 90) return theme.fg("error", text);
|
||||
if (pct >= 70) return theme.fg("warning", text);
|
||||
return theme.fg("success", text);
|
||||
}
|
||||
|
||||
function fmtWindowShort(label: string, w: RateWindow, theme: any): string {
|
||||
const pct = Math.round(w.utilization);
|
||||
const reset = formatTimeUntil(w.resets_at);
|
||||
const indicator = colorForPct(pct, `${bar(pct)} ${pct}%`, theme);
|
||||
return `${theme.fg("dim", label)} ${indicator} ${theme.fg("dim", `↺${reset}`)}`;
|
||||
}
|
||||
|
||||
function formatStatus(data: UsageResponse, theme: any): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (data.five_hour?.utilization != null)
|
||||
parts.push(fmtWindowShort("5h", data.five_hour, theme));
|
||||
if (data.seven_day?.utilization != null)
|
||||
parts.push(fmtWindowShort("7d", data.seven_day, theme));
|
||||
|
||||
// Optionale Modell-spezifische Fenster (falls belegt)
|
||||
if (data.seven_day_opus?.utilization)
|
||||
parts.push(fmtWindowShort("opus", data.seven_day_opus, theme));
|
||||
if (data.seven_day_sonnet?.utilization)
|
||||
parts.push(fmtWindowShort("sonnet", data.seven_day_sonnet, theme));
|
||||
|
||||
if (parts.length === 0) return theme.fg("dim", "☁ keine Usage-Daten");
|
||||
return `☁ ${parts.join(" ")}`;
|
||||
}
|
||||
|
||||
// ─── Fetch ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function fetchUsage(token: string): Promise<UsageResponse | null> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
const res = await fetch(USAGE_URL, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as UsageResponse;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Extension ───────────────────────────────────────────────────────────────
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
|
||||
async function getOAuthToken(ctx: any): Promise<string | null> {
|
||||
const model = ctx.model;
|
||||
if (!model || model.provider !== "anthropic") return null;
|
||||
if (!ctx.modelRegistry.isUsingOAuth(model)) return null;
|
||||
return (await ctx.modelRegistry.getApiKeyForProvider("anthropic")) ?? null;
|
||||
}
|
||||
|
||||
async function refresh(ctx: any): Promise<void> {
|
||||
if (!ctx.hasUI) return;
|
||||
|
||||
const token = await getOAuthToken(ctx);
|
||||
if (!token) {
|
||||
ctx.ui.setStatus(STATUS_KEY, undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await fetchUsage(token);
|
||||
if (!data) {
|
||||
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg("dim", "☁ usage: Fehler"));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ui.setStatus(STATUS_KEY, formatStatus(data, ctx.ui.theme));
|
||||
}
|
||||
|
||||
pi.on("session_start", async (_e, ctx) => { await refresh(ctx); });
|
||||
pi.on("agent_end", async (_e, ctx) => { await refresh(ctx); });
|
||||
pi.on("model_select", async (_e, ctx) => { await refresh(ctx); });
|
||||
|
||||
// ── /usage Command ────────────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("usage", {
|
||||
description: "Anthropic Subscription Usage anzeigen (Pro/Max)",
|
||||
handler: async (_args, ctx) => {
|
||||
const token = await getOAuthToken(ctx);
|
||||
if (!token) {
|
||||
ctx.ui.notify(
|
||||
"Kein OAuth-Token — nur für claude.ai Pro/Max Subscriptions",
|
||||
"warning"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ui.notify("Lade Usage-Daten…", "info");
|
||||
const data = await fetchUsage(token);
|
||||
if (!data) {
|
||||
ctx.ui.notify("Fetch fehlgeschlagen", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const lines: string[] = ["Anthropic Subscription Usage\n"];
|
||||
|
||||
const fmtDetail = (label: string, w: RateWindow) => {
|
||||
const pct = w.utilization.toFixed(1);
|
||||
const reset = formatTimeUntil(w.resets_at);
|
||||
lines.push(`${label.padEnd(12)} ${bar(w.utilization, 14)} ${pct}% ↺ ${reset}`);
|
||||
};
|
||||
|
||||
if (data.five_hour?.utilization != null) fmtDetail("5h-Fenster", data.five_hour);
|
||||
if (data.seven_day?.utilization != null) fmtDetail("7d-Fenster", data.seven_day);
|
||||
if (data.seven_day_opus?.utilization) fmtDetail("7d Opus", data.seven_day_opus);
|
||||
if (data.seven_day_sonnet?.utilization) fmtDetail("7d Sonnet", data.seven_day_sonnet);
|
||||
|
||||
if (lines.length === 1) lines.push("Keine Daten verfügbar");
|
||||
|
||||
ctx.ui.notify(lines.join("\n"), "info");
|
||||
ctx.ui.setStatus(STATUS_KEY, formatStatus(data, ctx.ui.theme));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "pi-anthropic-usage",
|
||||
"version": "1.0.0",
|
||||
"description": "Pi agent extension: shows Anthropic claude.ai Pro/Max subscription usage (5h + 7d rate limits) in the TUI footer",
|
||||
"keywords": ["pi-package", "pi", "pi-coding-agent", "anthropic", "usage", "subscription"],
|
||||
"author": "jay",
|
||||
"license": "MIT",
|
||||
"pi": {
|
||||
"extensions": ["./index.ts"]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue