pi update

This commit is contained in:
Jonas H
2026-04-10 09:01:25 +02:00
parent 7106db51b5
commit 4666776bda
16 changed files with 3042 additions and 35 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
{
"name": "claude-agent-sdk",
"type": "module",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.92",
"change-case": "^5.4.4"
},
"pi": {
"extensions": ["./index.ts"]
}
}

View File

@@ -0,0 +1,12 @@
{
"url": "https://mcp.exa.ai/mcp",
"apiKey": null,
"tools": [
"web_search_exa",
"get_code_context_exa"
],
"timeoutMs": 30000,
"protocolVersion": "2025-06-18",
"maxBytes": 51200,
"maxLines": 2000
}

View File

@@ -36,8 +36,8 @@ export function writeUsageCache(cache: UsageCache): void {
} catch {}
}
export type ProviderKey = "codex" | "claude" | "zai" | "gemini" | "antigravity";
export type OAuthProviderId = "openai-codex" | "anthropic" | "google-gemini-cli" | "google-antigravity";
export type ProviderKey = "codex" | "claude" | "zai" | "gemini" | "antigravity" | "opencode-go";
export type OAuthProviderId = "openai-codex" | "anthropic" | "google-gemini-cli" | "google-antigravity" | "opencode-go";
export interface AuthData {
"openai-codex"?: { access?: string; refresh?: string; expires?: number };
@@ -45,6 +45,7 @@ export interface AuthData {
zai?: { key?: string; access?: string; refresh?: string; expires?: number };
"google-gemini-cli"?: { access?: string; refresh?: string; projectId?: string; expires?: number };
"google-antigravity"?: { access?: string; refresh?: string; projectId?: string; expires?: number };
"opencode-go"?: { key?: string; access?: string };
}
export interface UsageData {
@@ -61,6 +62,127 @@ export interface UsageData {
export type UsageByProvider = Record<ProviderKey, UsageData | null>;
// OpenCode Go usage tracking (local, since no API exists yet)
export interface OpenCodeGoLocalUsage {
/** Dollar value used in the 5-hour window */
fiveHourUsed: number;
/** Dollar value used in the weekly window */
weeklyUsed: number;
/** Dollar value used in the monthly window */
monthlyUsed: number;
/** Timestamp of the last update */
lastUpdated: number;
/** When the current 5-hour window started */
fiveHourWindowStart: number;
/** When the current week started (Unix ms) */
weekStart: number;
/** When the current month started (Unix ms) */
monthStart: number;
}
const OPENCODE_GO_USAGE_FILE = path.join(os.homedir(), ".pi", "agent", "opencode-go-usage.json");
const OPENCODE_GO_FIVE_HOUR_LIMIT = 12;
const OPENCODE_GO_WEEKLY_LIMIT = 30;
const OPENCODE_GO_MONTHLY_LIMIT = 60;
const OPENCODE_GO_FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
export function readOpenCodeGoUsage(): OpenCodeGoLocalUsage | null {
try {
const raw = fs.readFileSync(OPENCODE_GO_USAGE_FILE, "utf-8");
const parsed = JSON.parse(raw);
if (typeof parsed?.lastUpdated === "number") return parsed as OpenCodeGoLocalUsage;
} catch {}
return null;
}
export function writeOpenCodeGoUsage(usage: OpenCodeGoLocalUsage): void {
try {
const dir = path.dirname(OPENCODE_GO_USAGE_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const tmp = `${OPENCODE_GO_USAGE_FILE}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
fs.renameSync(tmp, OPENCODE_GO_USAGE_FILE);
} catch {}
}
export function resetOpenCodeGoUsageIfNeeded(existing: OpenCodeGoLocalUsage | null): OpenCodeGoLocalUsage {
const now = Date.now();
const nowDate = new Date(now);
// Start with defaults
let usage: OpenCodeGoLocalUsage = existing ?? {
fiveHourUsed: 0,
weeklyUsed: 0,
monthlyUsed: 0,
lastUpdated: now,
fiveHourWindowStart: now,
weekStart: now,
monthStart: now,
};
// Reset 5-hour window if expired
if (now - usage.fiveHourWindowStart >= OPENCODE_GO_FIVE_HOUR_MS) {
usage.fiveHourUsed = 0;
usage.fiveHourWindowStart = now;
}
// Reset weekly window (Monday-based)
const dayOfWeek = nowDate.getDay();
const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const thisMonday = new Date(nowDate);
thisMonday.setDate(nowDate.getDate() - daysSinceMonday);
thisMonday.setHours(0, 0, 0, 0);
if (usage.weekStart < thisMonday.getTime()) {
usage.weeklyUsed = 0;
usage.weekStart = thisMonday.getTime();
}
// Reset monthly window (1st of month)
const thisMonthStart = new Date(nowDate.getFullYear(), nowDate.getMonth(), 1);
if (usage.monthStart < thisMonthStart.getTime()) {
usage.monthlyUsed = 0;
usage.monthStart = thisMonthStart.getTime();
}
usage.lastUpdated = now;
return usage;
}
export function addOpenCodeGoSpend(dollars: number): void {
let usage = resetOpenCodeGoUsageIfNeeded(readOpenCodeGoUsage());
usage.fiveHourUsed += dollars;
usage.weeklyUsed += dollars;
usage.monthlyUsed += dollars;
usage.lastUpdated = Date.now();
writeOpenCodeGoUsage(usage);
}
export function getOpenCodeGoUsageData(): UsageData {
const usage = resetOpenCodeGoUsageIfNeeded(readOpenCodeGoUsage());
if (!usage) {
return { session: 0, weekly: 0, error: "no local usage data" };
}
const sessionPct = Math.min(100, (usage.fiveHourUsed / OPENCODE_GO_FIVE_HOUR_LIMIT) * 100);
const weeklyPct = Math.min(100, (usage.weeklyUsed / OPENCODE_GO_WEEKLY_LIMIT) * 100);
// Calculate resets
const fiveHourEnd = usage.fiveHourWindowStart + OPENCODE_GO_FIVE_HOUR_MS;
const fiveHourRemaining = Math.max(0, fiveHourEnd - Date.now());
const weekEnd = usage.weekStart + 7 * 24 * 60 * 60 * 1000;
const weekRemaining = Math.max(0, weekEnd - Date.now());
return {
session: sessionPct,
weekly: weeklyPct,
sessionResetsIn: formatDuration(Math.round(fiveHourRemaining / 1000)),
weeklyResetsIn: formatDuration(Math.round(weekRemaining / 1000)),
extraSpend: usage.monthlyUsed,
extraLimit: OPENCODE_GO_MONTHLY_LIMIT,
};
}
export interface UsageEndpoints {
zai: string;
gemini: string;
@@ -600,12 +722,14 @@ export function detectProvider(
if (typeof model === "string") return null;
const provider = (model.provider || "").toLowerCase();
const id = (model.id || "").toLowerCase();
if (provider === "openai-codex") return "codex";
if (provider === "anthropic") return "claude";
if (provider === "zai") return "zai";
if (provider === "google-gemini-cli") return "gemini";
if (provider === "google-antigravity") return "antigravity";
if (provider === "opencode-go" || id.startsWith("opencode-go/")) return "opencode-go";
return null;
}
@@ -615,6 +739,7 @@ export function providerToOAuthProviderId(active: ProviderKey | null): OAuthProv
if (active === "claude") return "anthropic";
if (active === "gemini") return "google-gemini-cli";
if (active === "antigravity") return "google-antigravity";
if (active === "opencode-go") return "opencode-go";
return null;
}
@@ -629,6 +754,9 @@ export function canShowForProvider(active: ProviderKey | null, auth: AuthData |
if (active === "antigravity") {
return !!(auth["google-antigravity"]?.access || auth["google-antigravity"]?.refresh) && !!endpoints.antigravity;
}
if (active === "opencode-go") {
return !!(auth["opencode-go"]?.key || auth["opencode-go"]?.access);
}
return false;
}
@@ -654,6 +782,7 @@ export async function fetchAllUsages(config: FetchAllUsagesConfig = {}): Promise
zai: null,
gemini: null,
antigravity: null,
"opencode-go": null,
};
if (!auth) return results;
@@ -733,6 +862,11 @@ export async function fetchAllUsages(config: FetchAllUsagesConfig = {}): Promise
}
}
// OpenCode Go uses local tracking (no public API yet)
if (authData["opencode-go"]?.key || authData["opencode-go"]?.access) {
results["opencode-go"] = getOpenCodeGoUsageData();
}
await Promise.all(tasks);
return results;
}

View File

@@ -79,6 +79,7 @@ const PROVIDER_LABELS: Record<ProviderKey, string> = {
zai: "Z.AI",
gemini: "Gemini",
antigravity: "Antigravity",
"opencode-go": "Go",
};
// ---------------------------------------------------------------------------
@@ -163,6 +164,7 @@ class UsageSelectorComponent extends Container implements Focusable {
{ key: "zai", name: "Z.AI" },
{ key: "gemini", name: "Gemini" },
{ key: "antigravity", name: "Antigravity" },
{ key: "opencode-go", name: "Go" },
];
this.allItems = [];
for (const p of providers) {
@@ -287,7 +289,7 @@ interface PollOptions {
export default function (pi: ExtensionAPI) {
const endpoints = resolveUsageEndpoints();
const state: UsageState = {
codex: null, claude: null, zai: null, gemini: null, antigravity: null,
codex: null, claude: null, zai: null, gemini: null, antigravity: null, "opencode-go": null,
lastPoll: 0, activeProvider: null,
};
@@ -433,6 +435,10 @@ export default function (pi: ExtensionAPI) {
result = creds?.access
? await fetchGoogleUsage(creds.access, endpoints.gemini, creds.projectId, "gemini", { endpoints })
: { session: 0, weekly: 0, error: "missing access token (try /login again)" };
} else if (active === "opencode-go") {
// OpenCode Go uses local tracking (no public usage API yet)
const { getOpenCodeGoUsageData } = await import("./core");
result = getOpenCodeGoUsageData();
} else {
const creds = effectiveAuth["google-antigravity"];
result = creds?.access
@@ -561,6 +567,22 @@ export default function (pi: ExtensionAPI) {
void poll({ forceFresh: true });
});
// Listen for OpenCode Go spend events from other extensions
pi.events.on("opencode-go:spend", async (amount: number) => {
if (typeof amount === "number" && amount > 0) {
const { addOpenCodeGoSpend } = await import("./core");
addOpenCodeGoSpend(amount);
// Invalidate cache and re-poll
const cache = readUsageCache();
if (cache?.data?.["opencode-go"]) {
const nextCache: import("./core").UsageCache = { ...cache, data: { ...cache.data } };
delete nextCache.data["opencode-go"];
writeUsageCache(nextCache);
}
void poll({ forceFresh: true });
}
});
// ── /usage command ───────────────────────────────────────────────────────
pi.registerCommand("usage", {