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