BIG pi update with claude chat

This commit is contained in:
Jonas H
2026-04-24 14:22:59 +02:00
parent fbb00a49ba
commit 248667468c
24 changed files with 4225 additions and 1112 deletions

View File

@@ -55,6 +55,8 @@ export interface UsageData {
/** Unix ms timestamp of when the session window resets (from the raw API response). */
sessionResetsAt?: number;
weeklyResetsIn?: string;
/** Unix ms timestamp of when the weekly window resets. */
weeklyResetsAt?: number;
extraSpend?: number;
extraLimit?: number;
error?: string;
@@ -321,13 +323,34 @@ export function formatResetsAt(isoDate: string, nowMs = Date.now()): string {
return formatDuration(diffSeconds);
}
const CLAUDE_CREDENTIALS_FILE = path.join(os.homedir(), ".claude", ".credentials.json");
export function readAuth(authFile = DEFAULT_AUTH_FILE): AuthData | null {
let result: AuthData | null = null;
// Read pi auth.json for non-Claude providers
try {
const parsed = JSON.parse(fs.readFileSync(authFile, "utf-8"));
return asObject(parsed) as AuthData;
result = asObject(parsed) as AuthData;
} catch {
return null;
result = {} as AuthData;
}
// Read Claude credentials from ~/.claude/.credentials.json
try {
const claudeRaw = fs.readFileSync(CLAUDE_CREDENTIALS_FILE, "utf-8");
const claudeCreds = JSON.parse(claudeRaw);
const oauth = claudeCreds?.claudeAiOauth;
if (oauth?.accessToken) {
result!.anthropic = {
access: oauth.accessToken,
refresh: oauth.refreshToken,
expires: typeof oauth.expiresAt === "number" ? oauth.expiresAt : undefined,
};
}
} catch {}
return result;
}
export function writeAuth(auth: AuthData, authFile = DEFAULT_AUTH_FILE): boolean {
@@ -647,6 +670,9 @@ export async function fetchClaudeUsage(token: string, config: RequestConfig = {}
const sessionResetsAt = data?.five_hour?.resets_at
? new Date(data.five_hour.resets_at).getTime()
: undefined;
const weeklyResetsAt = data?.seven_day?.resets_at
? new Date(data.seven_day.resets_at).getTime()
: undefined;
const usage: UsageData = {
session: readPercentCandidate(data?.five_hour?.utilization) ?? 0,
@@ -654,6 +680,7 @@ export async function fetchClaudeUsage(token: string, config: RequestConfig = {}
sessionResetsIn: data?.five_hour?.resets_at ? formatResetsAt(data.five_hour.resets_at) : undefined,
sessionResetsAt: Number.isFinite(sessionResetsAt) ? sessionResetsAt : undefined,
weeklyResetsIn: data?.seven_day?.resets_at ? formatResetsAt(data.seven_day.resets_at) : undefined,
weeklyResetsAt: Number.isFinite(weeklyResetsAt) ? weeklyResetsAt : undefined,
};
if (data?.extra_usage?.is_enabled) {

View File

@@ -306,14 +306,27 @@ export default function (pi: ExtensionAPI) {
const active = state.activeProvider;
const data = active ? state[active] : null;
// Always emit event for other extensions (e.g. footer-display)
if (data && !data.error) {
// Always emit Claude usage for other extensions (e.g. footer-display)
// so S/W bars are visible regardless of active model.
const claudeData = state.claude;
if (claudeData && !claudeData.error) {
pi.events.emit("usage:update", {
session: claudeData.session,
weekly: claudeData.weekly,
sessionResetsIn: claudeData.sessionResetsIn,
sessionResetsAt: claudeData.sessionResetsAt,
weeklyResetsIn: claudeData.weeklyResetsIn,
weeklyResetsAt: claudeData.weeklyResetsAt,
});
} else if (data && !data.error) {
// Fallback to active provider data if Claude data unavailable
pi.events.emit("usage:update", {
session: data.session,
weekly: data.weekly,
sessionResetsIn: data.sessionResetsIn,
sessionResetsAt: data.sessionResetsAt,
weeklyResetsIn: data.weeklyResetsIn,
weeklyResetsAt: data.weeklyResetsAt,
});
}
@@ -372,6 +385,48 @@ export default function (pi: ExtensionAPI) {
const auth = readAuth();
const active = state.activeProvider;
// Always try to fetch Claude data so S/W bars show regardless of active provider
if (auth && canShowForProvider("claude", auth, endpoints)) {
try {
const cache = readUsageCache();
const now = Date.now();
const cacheTtl = options.cacheTtl ?? CACHE_TTL_MS;
const claudeBlockedUntil = cache?.rateLimitedUntil?.claude ?? 0;
if (now < claudeBlockedUntil) {
if (cache?.data?.claude) state.claude = cache.data.claude;
} else {
const claudeCacheFresh = cache && now - cache.timestamp < cacheTtl && cache.data?.claude;
if (claudeCacheFresh && !options.forceFresh) {
state.claude = cache.data.claude;
} else {
const claudeAccess = auth.anthropic?.access;
if (claudeAccess) {
const claudeResult = await fetchClaudeUsage(claudeAccess);
state.claude = claudeResult;
if (!claudeResult.error) {
const nextCache: import("./core").UsageCache = {
timestamp: now,
data: { ...(cache?.data ?? {}), claude: claudeResult },
rateLimitedUntil: { ...(cache?.rateLimitedUntil ?? {}) },
};
delete nextCache.rateLimitedUntil!.claude;
writeUsageCache(nextCache);
} else if (claudeResult.error === "HTTP 429") {
// Record backoff even when Claude is not the active provider —
// without this the prefetch would hammer the API on every poll.
const nextCache: import("./core").UsageCache = {
timestamp: cache?.timestamp ?? now,
data: { ...(cache?.data ?? {}) },
rateLimitedUntil: { ...(cache?.rateLimitedUntil ?? {}), claude: now + RATE_LIMITED_BACKOFF_MS },
};
writeUsageCache(nextCache);
}
}
}
}
} catch {}
}
if (!canShowForProvider(active, auth, endpoints) || !auth || !active) {
state.lastPoll = Date.now(); updateStatus(); return;
}
@@ -478,7 +533,9 @@ export default function (pi: ExtensionAPI) {
await Promise.race([runPollInner(options), timeout]);
}
const POLL_TIMEOUT_MS = 30_000;
// Must be less than the 25 000 ms timeout inside runPoll so the guard fires
// before runPoll's finally-block clears pollInFlight.
const POLL_TIMEOUT_MS = 20_000;
async function poll(options: PollOptions = {}) {
// If a previous poll has been running longer than POLL_TIMEOUT_MS, abandon it
@@ -557,15 +614,6 @@ export default function (pi: ExtensionAPI) {
await poll({ cacheTtl: ACTIVE_CACHE_TTL_MS });
});
pi.events.on("claude-account:switched", () => {
const cache = readUsageCache();
if (cache?.data?.claude) {
const nextCache: import("./core").UsageCache = { ...cache, data: { ...cache.data } };
delete nextCache.data.claude;
writeUsageCache(nextCache);
}
void poll({ forceFresh: true });
});
// Listen for OpenCode Go spend events from other extensions
pi.events.on("opencode-go:spend", async (amount: number) => {