From 731431af448b4305ef33fb2f7dd8979d32b5d2b5 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Thu, 21 May 2026 20:19:45 +0200 Subject: [PATCH 1/2] feat(core): add QuotaManager with exponential backoff and persistence Unified quota cache and API gateway for main + fallback quota state. All consumers share one QuotaManager instance for consistent caching. Features: - Inflight deduplication prevents concurrent API calls - Exponential backoff (60s-15min) for 429/5xx errors - Persists main quota and backoff state to disk via callbacks - Cross-process file lock guard for quota API dedup - Seeds from persisted storage on construction - Integrates with FallbackAccountManager for shared staleness - Captures storage path at init to prevent test config corruption - Request-count-based refresh trigger (refreshEveryNRequests) --- packages/core/src/accounts.ts | 152 ++++++- packages/core/src/index.ts | 1 + packages/core/src/quota-manager.ts | 390 ++++++++++++++++++ packages/opencode/src/index.ts | 239 ++++++----- packages/opencode/src/tests/index.test.ts | 112 ++++- .../opencode/src/tests/quota-manager.test.ts | 307 ++++++++++++++ 6 files changed, 1091 insertions(+), 110 deletions(-) create mode 100644 packages/core/src/quota-manager.ts create mode 100644 packages/opencode/src/tests/quota-manager.test.ts diff --git a/packages/core/src/accounts.ts b/packages/core/src/accounts.ts index c5599df..e6cbf82 100644 --- a/packages/core/src/accounts.ts +++ b/packages/core/src/accounts.ts @@ -67,8 +67,12 @@ export type AccountStorage = { quota?: { enabled?: boolean checkIntervalMinutes?: number + refreshEveryNRequests?: number minimumRemaining?: Partial> failClosedOnUnknownQuota?: boolean + mainQuota?: OAuthQuotaSnapshot + mainQuotaCheckedAt?: number + mainLastQuotaApiError?: AccountOperationError } claudeCache?: { enabled?: boolean @@ -113,6 +117,7 @@ export type AccountManagerOptions = { now?: () => number fetchImpl?: typeof fetch configPath?: string + quotaManager?: import('./quota-manager.ts').QuotaManager } export type AccountRefreshError = { @@ -127,6 +132,9 @@ const DEFAULT_REFRESH_INTERVAL_MINUTES = 10 const MIN_REFRESH_RETRY_DELAY_MS = 5 * 60_000 const MAX_REFRESH_RETRY_DELAY_MS = 60 * 60_000 const NON_TRANSIENT_REFRESH_RETRY_DELAY_MS = 24 * 60 * 60_000 +const MIN_QUOTA_RETRY_DELAY_MS = 60_000 +const MAX_QUOTA_RETRY_DELAY_MS = 15 * 60_000 +const NON_TRANSIENT_QUOTA_RETRY_DELAY_MS = 5 * 60_000 const DEFAULT_QUOTA_CHECK_INTERVAL_MINUTES = 5 const DEFAULT_MINIMUM_REMAINING: Record = { five_hour: 0, @@ -608,12 +616,95 @@ export function formatRefreshBackoffMessage( return `Claude OAuth refresh is backed off for ${seconds}s after: ${error.message}` } +function isTransientQuotaError(error: unknown): boolean { + if (!(error instanceof Error)) return false + // fetchOAuthQuotaSnapshot throws: "Claude quota check failed: {status} — {body}" + const statusMatch = error.message.match(/Claude quota check failed: (\d+)/) + if (statusMatch) { + const status = Number(statusMatch[1]) + return status === 429 || status >= 500 + } + // Network errors + return ( + error.message.includes('fetch failed') || + ('code' in error && + (error.code === 'ECONNRESET' || + error.code === 'ECONNREFUSED' || + error.code === 'ETIMEDOUT' || + error.code === 'UND_ERR_CONNECT_TIMEOUT')) + ) +} + +export function buildQuotaOperationError(input: { + error: unknown + now: number + previous?: AccountOperationError +}): AccountOperationError { + const previousRetryCount = input.previous?.retryCount ?? 0 + const retryCount = previousRetryCount + 1 + const delay = isTransientQuotaError(input.error) + ? Math.min( + MAX_QUOTA_RETRY_DELAY_MS, + MIN_QUOTA_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6), + ) + : NON_TRANSIENT_QUOTA_RETRY_DELAY_MS + return { + message: formatErrorMessage(input.error), + checkedAt: input.now, + nextRetryAt: input.now + delay, + retryCount, + } +} + +export function quotaBackoffActive( + error: AccountOperationError | undefined, + now: number, +): boolean { + if (!error?.nextRetryAt || error.nextRetryAt <= now) return false + return true +} + +export function formatQuotaBackoffMessage( + error: AccountOperationError, + now: number, +): string { + const seconds = Math.max( + 1, + Math.ceil(((error.nextRetryAt ?? now) - now) / 1000), + ) + return `Quota API backed off for ${seconds}s after: ${error.message}` +} + export function getQuotaCheckIntervalMs(storage: AccountStorage | null) { const minutes = storage?.quota?.checkIntervalMinutes ?? DEFAULT_QUOTA_CHECK_INTERVAL_MINUTES return Math.max(1, minutes) * 60_000 } +export function getPersistedMainQuota( + storage: AccountStorage | null, +): { quota: OAuthQuotaSnapshot; checkedAt: number } | null { + if (!storage?.quota?.mainQuota || !storage.quota.mainQuotaCheckedAt) + return null + return { + quota: storage.quota.mainQuota, + checkedAt: storage.quota.mainQuotaCheckedAt, + } +} + +/** + * How often (in requests) to force a quota refresh, independent of the timer. + * Returns 0 when disabled (default). + */ +export function getQuotaRefreshEveryNRequests( + storage: AccountStorage | null, +): number { + const n = storage?.quota?.refreshEveryNRequests + return typeof n === 'number' && Number.isFinite(n) && n > 0 + ? Math.floor(n) + : 0 +} + function failClosedOnUnknownQuota(storage: AccountStorage | null) { return ( storage?.quota?.failClosedOnUnknownQuota ?? @@ -764,10 +855,11 @@ function recordQuotaRefreshError( error: unknown, now: number, ) { - account.lastQuotaRefreshError = { - message: formatErrorMessage(error), - checkedAt: now, - } + account.lastQuotaRefreshError = buildQuotaOperationError({ + error, + now, + previous: account.lastQuotaRefreshError, + }) if (error instanceof ClaudeOAuthRefreshError) { recordRefreshError(account, error, now) } @@ -780,11 +872,37 @@ export class FallbackAccountManager { private readonly refreshPromises = new Map>() private refreshTimer: ReturnType | null = null private quotaTimer: ReturnType | null = null + readonly quotaManager: import('./quota-manager.ts').QuotaManager | null constructor(options: AccountManagerOptions = {}) { this.now = options.now ?? Date.now this.fetchImpl = options.fetchImpl ?? fetch this.configPath = options.configPath ?? getAccountStoragePath() + this.quotaManager = options.quotaManager ?? null + } + + /** + * Seed QuotaManager from persisted account.quota if no cache entry exists + * yet. Prevents unnecessary API calls when the on-disk snapshot is fresh. + */ + private seedFallbackQuota( + account: OAuthAccount, + storage: AccountStorage, + ): void { + if (!this.quotaManager) return + if (this.quotaManager.getFallback(account.id)) return + if (!account.quota) return + const checkedAt = Math.max( + account.quota.five_hour?.checkedAt ?? 0, + account.quota.seven_day?.checkedAt ?? 0, + ) + if (checkedAt <= 0) return + const checkInterval = getQuotaCheckIntervalMs(storage) + this.quotaManager.setFallback(account.id, { + quota: account.quota, + refreshAfter: checkedAt + checkInterval, + checkedAt, + }) } async load() { @@ -840,7 +958,11 @@ export class FallbackAccountManager { next = await this.refreshAccount(next, storage) changed = true } - if (quotaIsStale(next, storage, this.now())) { + this.seedFallbackQuota(next, storage) + const stale = this.quotaManager + ? this.quotaManager.isFallbackStale(next.id) + : quotaIsStale(next, storage, this.now()) + if (stale) { next = await this.refreshAccountQuota(next, storage) changed = true } @@ -937,7 +1059,16 @@ export class FallbackAccountManager { next = await this.refreshAccount(next, storage) changed = true } - if (!quotaIsStale(next, storage, this.now())) continue + if (quotaBackoffActive(next.lastQuotaRefreshError, this.now())) { + continue + } + this.seedFallbackQuota(next, storage) + // Use QuotaManager staleness when available (shared cache); + // fall back to per-account on-disk staleness otherwise. + const stale = this.quotaManager + ? this.quotaManager.isFallbackStale(next.id) + : quotaIsStale(next, storage, this.now()) + if (!stale) continue await this.refreshAccountQuota(next, storage) changed = true } catch (error) { @@ -1085,6 +1216,15 @@ export class FallbackAccountManager { } target.lastQuotaRefreshError = undefined updateStoredAccount(storage, target) + // Sync to shared QuotaManager so all consumers see the same cache + if (this.quotaManager && target.quota) { + const now = this.now() + this.quotaManager.setFallback(target.id, { + quota: target.quota, + refreshAfter: now + getQuotaCheckIntervalMs(storage), + checkedAt: now, + }) + } return target } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f9ae5fe..ce93e01 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,5 +9,6 @@ export * from './dump.ts' export * from './fast.ts' export * from './logger.ts' export * from './pkce.ts' +export * from './quota-manager.ts' export * from './quotas.ts' export * from './relay.ts' diff --git a/packages/core/src/quota-manager.ts b/packages/core/src/quota-manager.ts new file mode 100644 index 0000000..e6b72f4 --- /dev/null +++ b/packages/core/src/quota-manager.ts @@ -0,0 +1,390 @@ +/** + * Unified quota cache and API gateway. + * + * Single source of truth for main + fallback quota state. All consumers + * share one QuotaManager instance so they see the same in-memory cache. + * Handles deduplication, rate-limiting (429 backoff), and staleness. + */ + +import type { + AccountOperationError, + AccountStorage, + OAuthAccount, + OAuthQuotaSnapshot, +} from './accounts.ts' +import { + acquireRefreshFileLock, + buildQuotaOperationError, + fetchOAuthQuotaSnapshot, + getPersistedMainQuota, + getQuotaCheckIntervalMs, + getQuotaNextRefreshAt, + getQuotaRefreshEveryNRequests, + quotaBackoffActive, +} from './accounts.ts' + +// Capture real setTimeout before tests can mock globalThis.setTimeout +const nativeSetTimeout = globalThis.setTimeout + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type QuotaEntry = { + quota: OAuthQuotaSnapshot + refreshAfter: number // Unix ms — earliest next refresh + checkedAt: number // when snapshot was fetched +} + +export type QuotaManagerOptions = { + storage: AccountStorage | null + fetchImpl?: typeof fetch + now?: () => number + onMainQuotaFetched?: (quota: OAuthQuotaSnapshot, checkedAt: number) => void + onApiError?: (error: AccountOperationError) => void +} + +// --------------------------------------------------------------------------- +// Class +// --------------------------------------------------------------------------- + +export class QuotaManager { + // --- State --- + private main: QuotaEntry | null = null + private mainAccessToken: string | null = null + private fallbacks = new Map() + + // --- Inflight deduplication --- + private inflightMain: Promise | null = null + private inflightFallbacks = new Map>() + + // --- Rate-limiting --- + private lastApiError: AccountOperationError | undefined = undefined + + // --- Serial API gate (prevents concurrent quota API calls) --- + private apiGate: Promise = Promise.resolve() + private lastApiCallAt = 0 + + // --- Config --- + private storage: AccountStorage | null + private readonly fetchImpl: typeof fetch + private readonly now: () => number + private readonly onMainQuotaFetched: QuotaManagerOptions['onMainQuotaFetched'] + private readonly onApiError: QuotaManagerOptions['onApiError'] + + constructor(opts: QuotaManagerOptions) { + this.storage = opts.storage + this.fetchImpl = opts.fetchImpl ?? fetch + this.now = opts.now ?? Date.now + this.onMainQuotaFetched = opts.onMainQuotaFetched + this.onApiError = opts.onApiError + + // Seed main quota from persisted storage + const persisted = getPersistedMainQuota(opts.storage) + if (persisted) { + this.main = { + quota: persisted.quota, + refreshAfter: + persisted.checkedAt + getQuotaCheckIntervalMs(opts.storage), + checkedAt: persisted.checkedAt, + } + } + + // Seed backoff state from persisted storage + const persistedError = opts.storage?.quota?.mainLastQuotaApiError + if (persistedError && quotaBackoffActive(persistedError, this.now())) { + this.lastApiError = persistedError + } + } + + // ========================================================================= + // Get (synchronous, from cache) + // ========================================================================= + + getMain(): QuotaEntry | null { + return this.main + } + + getFallback(accountId: string): QuotaEntry | null { + return this.fallbacks.get(accountId) ?? null + } + + getAllFallbacks(): Map { + return this.fallbacks + } + + // ========================================================================= + // Set (manual inject — seeding from persisted account.quota on boot) + // ========================================================================= + + setMain(accessToken: string, entry: QuotaEntry): void { + this.mainAccessToken = accessToken + this.main = entry + } + + setFallback(accountId: string, entry: QuotaEntry): void { + this.fallbacks.set(accountId, entry) + } + + // ========================================================================= + // Refresh (async, deduplicated, rate-limited) + // ========================================================================= + + async refreshMain(accessToken: string): Promise { + // If token changed, invalidate cache + if (this.mainAccessToken && this.mainAccessToken !== accessToken) { + this.main = null + this.mainAccessToken = null + } + + // Deduplicate — return in-flight promise if already fetching + if (this.inflightMain) return this.inflightMain + + // Rate-limit — if API recently 429'd, return stale or throw + if (this.isBackedOff()) { + if (this.main) return this.main.quota + throw new Error('Quota API rate-limited — try again later') + } + + this.inflightMain = this._fetchMain(accessToken) + return this.inflightMain + } + + async refreshFallback( + accountId: string, + accessToken: string, + ): Promise { + // Deduplicate + const inflight = this.inflightFallbacks.get(accountId) + if (inflight) return inflight + + // Rate-limit + if (this.isBackedOff()) { + const cached = this.fallbacks.get(accountId) + if (cached) return cached.quota + throw new Error('Quota API rate-limited — try again later') + } + + const promise = this._fetchFallback(accountId, accessToken) + this.inflightFallbacks.set(accountId, promise) + return promise + } + + async refreshAllFallbacks(accounts: OAuthAccount[]): Promise { + const now = this.now() + + for (const account of accounts) { + if (account.enabled === false) continue + if (!account.access) continue + + const cached = this.fallbacks.get(account.id) + if (cached && now < cached.refreshAfter) continue + + try { + await this.refreshFallback(account.id, account.access) + } catch { + // Best-effort — keep stale cache entry if fetch fails + } + } + } + + /** + * Fire-and-forget refresh. Does not await, swallows errors. + */ + refreshMainInBackground(accessToken: string): void { + if (this.inflightMain) return + if (this.isBackedOff()) return + void this.refreshMain(accessToken).catch(() => {}) + } + + // ========================================================================= + // Staleness queries + // ========================================================================= + + isMainStale(): boolean { + if (!this.main) return true + return this.now() >= this.main.refreshAfter + } + + isFallbackStale(accountId: string): boolean { + const entry = this.fallbacks.get(accountId) + if (!entry) return true + return this.now() >= entry.refreshAfter + } + + shouldRefreshOnRequestCount(requestCount: number): boolean { + const everyN = getQuotaRefreshEveryNRequests(this.storage) + if (everyN <= 0) return false + return requestCount > 0 && requestCount % everyN === 0 + } + + /** + * Combined check: should a refresh happen right now? + * True if main is stale by time OR triggered by request count. + */ + needsRefresh(requestCount: number): boolean { + return this.isMainStale() || this.shouldRefreshOnRequestCount(requestCount) + } + + // ========================================================================= + // Config + // ========================================================================= + + updateStorage(storage: AccountStorage | null): void { + this.storage = storage + } + + /** + * Seed fallback cache entries from persisted account.quota data. + * Only seeds accounts that don't already have a cache entry. + * Prevents unnecessary API calls when persisted quota is still fresh. + */ + seedFallbacksFromAccounts(accounts: OAuthAccount[]): void { + const checkInterval = getQuotaCheckIntervalMs(this.storage) + for (const account of accounts) { + if (account.enabled === false) continue + if (this.fallbacks.has(account.id)) continue + if (!account.quota) continue + const checkedAt = Math.max( + account.quota.five_hour?.checkedAt ?? 0, + account.quota.seven_day?.checkedAt ?? 0, + ) + if (checkedAt <= 0) continue + this.fallbacks.set(account.id, { + quota: account.quota, + refreshAfter: checkedAt + checkInterval, + checkedAt, + }) + } + } + + /** + * Whether the API is currently in backoff due to a recent error. + */ + isBackedOff(): boolean { + return quotaBackoffActive(this.lastApiError, this.now()) + } + + getLastApiError(): AccountOperationError | undefined { + return this.lastApiError + } + + // ========================================================================= + // Private + // ========================================================================= + + /** Minimum gap between consecutive quota API calls (ms). */ + private static readonly API_CALL_GAP_MS = 1_000 + + /** + * Serialize API calls through a shared gate so only one + * quota API request runs at a time, with a minimum gap + * between calls. Prevents concurrent and rapid-fire calls + * from triggering Anthropic's rate limits. + */ + private _enqueueApiFetch(fn: () => Promise): Promise { + const gatedFn = async (): Promise => { + // Wait until minimum gap since last API call + const elapsed = this.now() - this.lastApiCallAt + if (elapsed < QuotaManager.API_CALL_GAP_MS) { + await new Promise((r) => { + const id = nativeSetTimeout(r, QuotaManager.API_CALL_GAP_MS - elapsed) + if (typeof id === 'object' && 'unref' in id) id.unref() + }) + } + this.lastApiCallAt = this.now() + return fn() + } + const queued = this.apiGate.then(gatedFn, gatedFn) + this.apiGate = queued.catch(() => {}) + return queued + } + + private async _fetchMain(accessToken: string): Promise { + return this._enqueueApiFetch(async () => { + try { + // Re-check backoff inside gate — may have been set by + // a preceding queued call while we waited + if (this.isBackedOff()) { + if (this.main) return this.main.quota + throw new Error('Quota API rate-limited — try again later') + } + const fileLock = await acquireRefreshFileLock({ + name: 'opencode-main-quota-refresh', + ttlMs: 30_000, + }) + if (!fileLock) { + if (this.main) return this.main.quota + throw new Error('Quota refresh is already in progress') + } + try { + const quota = await fetchOAuthQuotaSnapshot({ + accessToken, + fetchImpl: this.fetchImpl, + now: this.now, + }) + const now = this.now() + this.mainAccessToken = accessToken + this.main = { + quota, + refreshAfter: getQuotaNextRefreshAt(quota, this.storage, now), + checkedAt: now, + } + this.lastApiError = undefined + this.onMainQuotaFetched?.(quota, now) + return quota + } catch (error) { + this._handleFetchError(error) + throw error + } finally { + await fileLock.release() + } + } finally { + this.inflightMain = null + } + }) + } + + private async _fetchFallback( + accountId: string, + accessToken: string, + ): Promise { + return this._enqueueApiFetch(async () => { + try { + // Re-check backoff inside gate + if (this.isBackedOff()) { + const cached = this.fallbacks.get(accountId) + if (cached) return cached.quota + throw new Error('Quota API rate-limited — try again later') + } + const quota = await fetchOAuthQuotaSnapshot({ + accessToken, + fetchImpl: this.fetchImpl, + now: this.now, + }) + const now = this.now() + this.fallbacks.set(accountId, { + quota, + refreshAfter: now + getQuotaCheckIntervalMs(this.storage), + checkedAt: now, + }) + this.lastApiError = undefined + return quota + } catch (error) { + this._handleFetchError(error) + throw error + } finally { + this.inflightFallbacks.delete(accountId) + } + }) + } + + private _handleFetchError(error: unknown): void { + this.lastApiError = buildQuotaOperationError({ + error, + now: this.now(), + previous: this.lastApiError, + }) + this.onApiError?.(this.lastApiError) + } +} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 1c8561e..3bc332a 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -21,13 +21,12 @@ import { executeDumpCommand, executeFastModeCommand, FallbackAccountManager, - fetchOAuthQuotaSnapshot, + formatQuotaBackoffMessage, formatRefreshBackoffMessage, + getAccountStoragePath, getCache1hMode, getCache1hPersistentMode, getCacheKeepWindow, - getQuotaCheckIntervalMs, - getQuotaNextRefreshAt, getRelayConfig, hashRefreshToken, isCache1hEnabled, @@ -41,12 +40,12 @@ import { loadAccounts, log, mergeAnthropicBetas, - type OAuthQuotaSnapshot, parseCache1hCommandAction, parseCacheKeepCommandAction, parseDumpCommandAction, parseFastModeCommandAction, type QuotaAccountSummary, + QuotaManager, quotaSnapshotPassesPolicy, type RelayConfig, refreshBackoffActive, @@ -86,12 +85,6 @@ const MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240 const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES -type MainQuotaCache = { - accessToken: string - refreshAfter: number - quota: OAuthQuotaSnapshot -} - type NotificationRequest = { path: { id: string } body: { @@ -247,11 +240,50 @@ function throwHandledSentinel(): never { export const AnthropicAuthPlugin: Plugin = async (ctx) => { startEventLoopLagMonitor() const { client } = ctx - const fallbackManager = new FallbackAccountManager() + const accountStoragePath = getAccountStoragePath() + const initialStorage = await loadAccounts(accountStoragePath) + const quotaManager = new QuotaManager({ + storage: initialStorage, + onMainQuotaFetched: async (quota, checkedAt) => { + try { + const storage = (await loadAccounts(accountStoragePath)) ?? { + version: 1 as const, + accounts: [], + } + storage.quota = storage.quota ?? {} + storage.quota.mainQuota = quota + storage.quota.mainQuotaCheckedAt = checkedAt + storage.quota.mainLastQuotaApiError = undefined + await saveAccounts(storage, accountStoragePath) + } catch (error) { + log('[quota] failed to persist main quota', { + error: error instanceof Error ? error.message : String(error), + }) + } + }, + onApiError: async (error) => { + try { + const storage = (await loadAccounts(accountStoragePath)) ?? { + version: 1 as const, + accounts: [], + } + storage.quota = storage.quota ?? {} + storage.quota.mainLastQuotaApiError = error + await saveAccounts(storage, accountStoragePath) + } catch (e) { + log('[quota] failed to persist backoff state', { + error: e instanceof Error ? e.message : String(e), + }) + } + }, + }) + const fallbackManager = new FallbackAccountManager({ + quotaManager, + }) fallbackManager.startBackgroundRefresh() let latestRefreshMainAccessToken: (() => Promise) | null = null const cacheKeepManager = new CacheKeepManager({ - loadStorage: () => loadAccounts(), + loadStorage: () => loadAccounts(accountStoragePath), prepareHeaders: async (headers, target) => { if (!latestGetAuth) return headers const auth = await latestGetAuth() @@ -289,14 +321,13 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { }, log, }) - const initialCache1hStorage = await loadAccounts() - const relayConfig: RelayConfig | null = getRelayConfig(initialCache1hStorage) + const relayConfig: RelayConfig | null = getRelayConfig(initialStorage) setCache1hState({ - enabled: isCache1hPersistentlyEnabled(initialCache1hStorage), - mode: getCache1hPersistentMode(initialCache1hStorage), + enabled: isCache1hPersistentlyEnabled(initialStorage), + mode: getCache1hPersistentMode(initialStorage), }) - setDumpEnabled(isDumpPersistentlyEnabled(initialCache1hStorage)) - setFastModeEnabled(isFastModePersistentlyEnabled(initialCache1hStorage)) + setDumpEnabled(isDumpPersistentlyEnabled(initialStorage)) + setFastModeEnabled(isFastModePersistentlyEnabled(initialStorage)) let latestGetAuth: | (() => Promise<{ type: string @@ -324,7 +355,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { async function clearStaleMainRefreshError(refreshToken?: string) { if (!refreshToken) return - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) const error = storage?.refresh?.mainLastRefreshError if (!storage?.refresh || !error?.tokenHash) return const tokenHash = hashRefreshToken(refreshToken) @@ -347,10 +378,15 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { try { const auth = await latestGetAuth() if (auth.type === 'oauth' && auth.access) { + // Use QuotaManager cache; eager fetch on first request means + // this is always populated after the first API call. + const cached = quotaManager.getMain() + const quota = + cached?.quota ?? (await quotaManager.refreshMain(auth.access)) accounts.push({ name: 'OpenCode anthropic', role: 'main', - quota: await fetchOAuthQuotaSnapshot({ accessToken: auth.access }), + quota, }) } else if (auth.type === 'oauth') { accounts.push({ @@ -361,22 +397,34 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { }) } } catch (error) { + const msg = error instanceof Error ? error.message : String(error) accounts.push({ name: 'OpenCode anthropic', role: 'main', - error: error instanceof Error ? error.message : String(error), + error: msg.includes('429') + ? 'Usage API rate limited — try again in a moment' + : msg, }) } } - const { storage, errors } = - await fallbackManager.refreshQuotaForAllAccounts() - accounts.push( - ...buildFallbackQuotaSummaries( - storage, - new Map(errors.map((error) => [error.accountId, error.message])), - ), + // Use QuotaManager for fallbacks — goes through serial API gate + const storage = await loadAccounts(accountStoragePath) + const fallbackAccts = (storage?.accounts ?? []).filter( + (a) => a.enabled !== false && a.access, ) + try { + await quotaManager.refreshAllFallbacks(fallbackAccts) + } catch { + // Best-effort — stale cache is fine for display + } + // Overlay QuotaManager cache onto storage accounts for display + const fallbackEntries = quotaManager.getAllFallbacks() + for (const account of storage?.accounts ?? []) { + const cached = fallbackEntries.get(account.id) + if (cached) account.quota = cached.quota + } + accounts.push(...buildFallbackQuotaSummaries(storage, new Map())) if (!latestGetAuth) { accounts.unshift({ @@ -410,7 +458,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { }) } - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) const enabled = isCache1hPersistentlyEnabled(storage) const mode = getCache1hPersistentMode(storage) setCache1hState({ enabled, mode }) @@ -419,7 +467,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { async function executePersistentCacheKeepCommand(argumentsText: string) { const action = parseCacheKeepCommandAction(argumentsText) - let storage = await loadAccounts() + let storage = await loadAccounts(accountStoragePath) if (action.type === 'window') { storage = await setCacheKeepPersistentWindow( action.startHour, @@ -450,7 +498,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { return executeDumpCommand({ argumentsText, enabled }) } - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) const enabled = isDumpPersistentlyEnabled(storage) setDumpEnabled(enabled) return executeDumpCommand({ argumentsText, enabled }) @@ -465,7 +513,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { return executeFastModeCommand({ argumentsText, enabled }) } - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) const enabled = isFastModePersistentlyEnabled(storage) setFastModeEnabled(enabled) return executeFastModeCommand({ argumentsText, enabled }) @@ -581,9 +629,6 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { // Shared inflight refresh promise — prevents concurrent token refreshes // from racing against each other (and causing 401 cascades with token rotation) let refreshPromise: Promise | null = null - let mainQuotaCache: MainQuotaCache | null = null - let mainQuotaRefreshPromise: Promise | null = null - let mainQuotaRetryAfter = 0 async function refreshMainAccessToken() { if (!refreshPromise) { @@ -597,7 +642,9 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { async function updateMainRefreshState( update: (storage: AccountStorage) => void, ) { - const storage: AccountStorage = (await loadAccounts()) ?? { + const storage: AccountStorage = (await loadAccounts( + accountStoragePath, + )) ?? { version: 1, main: { type: 'opencode', provider: 'anthropic' }, accounts: [], @@ -665,7 +712,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { ) } - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) const refreshTokenHash = hashRefreshToken(freshAuth.refresh) const mainError = storage?.refresh?.mainLastRefreshError log('[refresh] opencode main oauth refresh check', { @@ -751,7 +798,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { nextStorage.refresh.mainRefreshLeaseTokenHash = refreshTokenHash }) - const latestLease = await loadAccounts() + const latestLease = await loadAccounts(accountStoragePath) log( '[refresh] opencode main oauth refresh lease acquired', { @@ -897,7 +944,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { const run = async () => { try { - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) if (!mainRefreshEnabled(storage)) return const latestAuth = await getAuth() if (latestAuth.type !== 'oauth') return @@ -1143,7 +1190,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { isCache1hEnabled() && getCache1hMode() === 'hybrid' ) { - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) const tracked = await cacheKeepManager.track({ sessionId: relayAffinity, url: rewritten.url?.toString() ?? rewritten.input.toString(), @@ -1187,54 +1234,6 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { return response } - async function refreshMainQuotaCache( - accessToken: string, - storage: Awaited>, - ) { - const now = Date.now() - const quota = await fetchOAuthQuotaSnapshot({ accessToken }) - mainQuotaCache = { - accessToken, - refreshAfter: getQuotaNextRefreshAt(quota, storage, now), - quota, - } - return quota - } - - function refreshMainQuotaCacheInBackground( - accessToken: string, - storage: Awaited>, - ) { - const now = Date.now() - if (mainQuotaRefreshPromise || now < mainQuotaRetryAfter) return - mainQuotaRefreshPromise = refreshMainQuotaCache( - accessToken, - storage, - ) - .catch((error) => { - mainQuotaRetryAfter = now + getQuotaCheckIntervalMs(storage) - throw error - }) - .finally(() => { - mainQuotaRefreshPromise = null - }) - void mainQuotaRefreshPromise.catch(() => {}) - } - - async function getMainQuotaForRouting( - accessToken: string, - storage: Awaited>, - ) { - const now = Date.now() - if (mainQuotaCache?.accessToken !== accessToken) { - return await refreshMainQuotaCache(accessToken, storage) - } - if (now >= mainQuotaCache.refreshAfter) { - refreshMainQuotaCacheInBackground(accessToken, storage) - } - return mainQuotaCache.quota - } - async function tryUsableFallbackAccounts( input: string | URL | Request, init: RequestInit | undefined, @@ -1296,7 +1295,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { if (!isReplayableRequest(input, init?.body)) return mainResponse const loadStart = nowMs() - const storage = existingStorage ?? (await loadAccounts()) + const storage = existingStorage ?? (await loadAccounts(accountStoragePath)) trace?.mark('fallback_load_storage', { ms: roundMs(nowMs() - loadStart), cached: !!existingStorage, @@ -1385,7 +1384,9 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { throw new Error('OAuth access token is missing after refresh') } const loadStart = nowMs() - const storage = await loadAccounts() + const storage = await loadAccounts(accountStoragePath) + quotaManager.updateStorage(storage) + quotaManager.seedFallbacksFromAccounts(storage?.accounts ?? []) trace.mark('load_storage', { ms: roundMs(nowMs() - loadStart) }) let preselectedFallbackAccounts: | Awaited< @@ -1400,18 +1401,22 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { ) { try { const quotaStart = nowMs() - const mainQuota = await getMainQuotaForRouting( - auth.access, - storage, - ) + // Use QuotaManager: get cached or eagerly refresh if first time + let routingQuota = quotaManager.getMain()?.quota + if (!routingQuota) { + routingQuota = await quotaManager.refreshMain(auth.access) + } else if (quotaManager.isMainStale()) { + // Background refresh — return stale to avoid blocking + void quotaManager.refreshMain(auth.access).catch(() => {}) + } trace.mark('main_quota_for_routing', { ms: roundMs(nowMs() - quotaStart), - passes: quotaSnapshotPassesPolicy(mainQuota, storage), + passes: quotaSnapshotPassesPolicy(routingQuota, storage), }) - if (!quotaSnapshotPassesPolicy(mainQuota, storage)) { + if (!quotaSnapshotPassesPolicy(routingQuota, storage)) { const fallbackStart = nowMs() preselectedFallbackAccounts = - await fallbackManager.getUsableFallbackAccounts() + await fallbackManager.getUsableFallbackAccounts(storage) trace.mark('preselect_fallback_accounts', { ms: roundMs(nowMs() - fallbackStart), accounts: preselectedFallbackAccounts.length, @@ -1439,6 +1444,46 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { // Main quota checks should optimize routing, not break requests. } } + + // Fail-closed: if failClosedOnUnknownQuota is set, quota API is backed off, + // and we have no cached quota, block the request. + const mainQuota = quotaManager.getMain()?.quota + if ( + storage?.quota?.failClosedOnUnknownQuota && + !mainQuota && + quotaManager.isBackedOff() + ) { + const lastError = quotaManager.getLastApiError() + const msg = lastError + ? formatQuotaBackoffMessage(lastError, Date.now()) + : 'Quota API unavailable' + log('[killswitch] blocked: quota API backed off', { + nextRetryAt: lastError?.nextRetryAt, + retryCount: lastError?.retryCount, + }) + return new Response( + JSON.stringify({ + type: 'error', + error: { type: 'rate_limit_error', message: msg }, + }), + { + status: 429, + headers: { + 'content-type': 'application/json', + 'retry-after': String( + lastError?.nextRetryAt + ? Math.max( + 1, + Math.ceil( + (lastError.nextRetryAt - Date.now()) / 1000, + ), + ) + : 60, + ), + }, + }, + ) + } const mainResponse = await sendWithAccessToken( input, init, diff --git a/packages/opencode/src/tests/index.test.ts b/packages/opencode/src/tests/index.test.ts index be2a3de..920bf13 100644 --- a/packages/opencode/src/tests/index.test.ts +++ b/packages/opencode/src/tests/index.test.ts @@ -263,6 +263,18 @@ describe('auth.loader', () => { let capturedBody: string | undefined globalThis.fetch = mock((input: any, init: any) => { + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } capturedHeaders = init?.headers capturedBody = init?.body return Promise.resolve(new Response(null, { status: 200 })) @@ -331,7 +343,19 @@ describe('auth.loader', () => { let capturedBody: string | undefined let capturedHeaders: Headers | undefined globalThis.fetch = mock((input: any, init: any) => { - capturedUrl = extractUrl(input) + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } + capturedUrl = url capturedBody = init?.body capturedHeaders = new Headers(init?.headers) return Promise.resolve( @@ -790,7 +814,19 @@ describe('auth.loader', () => { let capturedHeaders: Headers | undefined let capturedBody: string | undefined - globalThis.fetch = mock((_input: any, init: any) => { + globalThis.fetch = mock((input: any, init: any) => { + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } capturedHeaders = init?.headers capturedBody = init?.body return Promise.resolve(new Response(null, { status: 200 })) @@ -832,7 +868,19 @@ describe('auth.loader', () => { let capturedHeaders: Headers | undefined let capturedBody: string | undefined - globalThis.fetch = mock((_input: any, init: any) => { + globalThis.fetch = mock((input: any, init: any) => { + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } capturedHeaders = init?.headers capturedBody = init?.body return Promise.resolve(new Response(null, { status: 200 })) @@ -869,7 +917,19 @@ describe('auth.loader', () => { let capturedBody: string | undefined const mockClient = createMockClient() - globalThis.fetch = mock((_input: any, init: any) => { + globalThis.fetch = mock((input: any, init: any) => { + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } capturedBody = init?.body return Promise.resolve(new Response(null, { status: 200 })) }) as unknown as typeof fetch @@ -924,7 +984,19 @@ describe('auth.loader', () => { const mockClient = createMockClient() globalThis.fetch = mock( - (_input: string | URL | Request, init?: RequestInit) => { + (input: string | URL | Request, init?: RequestInit) => { + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } capturedBody = String(init?.body) capturedHeaders = new Headers(init?.headers) return Promise.resolve(new Response(null, { status: 200 })) @@ -974,7 +1046,19 @@ describe('auth.loader', () => { const mockClient = createMockClient() globalThis.fetch = mock( - (_input: string | URL | Request, init?: RequestInit) => { + (input: string | URL | Request, init?: RequestInit) => { + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } capturedBody = String(init?.body) return Promise.resolve(new Response(null, { status: 200 })) }, @@ -1653,7 +1737,19 @@ describe('auth.loader', () => { let capturedUrl: string | undefined globalThis.fetch = mock((input: any) => { - capturedUrl = extractUrl(input) + const url = extractUrl(input) + if (url.includes('/api/oauth/usage')) { + return Promise.resolve( + new Response( + JSON.stringify({ + five_hour: { utilization: 0 }, + seven_day: { utilization: 0 }, + }), + { status: 200 }, + ), + ) + } + capturedUrl = url return Promise.resolve(new Response(null, { status: 200 })) }) as unknown as typeof fetch @@ -1875,6 +1971,8 @@ describe('auth.loader', () => { ]) expect(second).toBe('message-2') + // Background quota refresh involves file-lock I/O; wait for it to fire. + await new Promise((r) => setTimeout(r, 50)) expect(quotaCalls).toBe(2) expect(messageCalls).toBe(2) } finally { diff --git a/packages/opencode/src/tests/quota-manager.test.ts b/packages/opencode/src/tests/quota-manager.test.ts new file mode 100644 index 0000000..94f7cdb --- /dev/null +++ b/packages/opencode/src/tests/quota-manager.test.ts @@ -0,0 +1,307 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { QuotaManager } from '@cortexkit/anthropic-auth-core' + +function makeQuotaResponse(now: number) { + return new Response( + JSON.stringify({ + five_hour: { + utilization: 25, + resets_at: new Date(now + 3600_000).toISOString(), + }, + seven_day: { + utilization: 50, + }, + }), + { status: 200 }, + ) +} + +describe('QuotaManager', () => { + let now: number + let tempDir: string + + beforeEach(async () => { + now = 1_000_000 + tempDir = await mkdtemp(join(tmpdir(), 'qm-test-')) + process.env.OPENCODE_ANTHROPIC_AUTH_FILE = join( + tempDir, + 'anthropic-auth.json', + ) + }) + + afterEach(async () => { + delete process.env.OPENCODE_ANTHROPIC_AUTH_FILE + await rm(tempDir, { recursive: true, force: true }) + }) + + function createQM(fetchImpl?: typeof fetch) { + return new QuotaManager({ + storage: null, + fetchImpl, + now: () => now, + }) + } + + describe('backoff', () => { + test('first 429 backs off for 60s', async () => { + const fetchMock = mock(() => + Promise.resolve(new Response('rate limited', { status: 429 })), + ) as unknown as typeof fetch + const qm = createQM(fetchMock) + + try { + await qm.refreshMain('token') + } catch {} + + expect(qm.isBackedOff()).toBe(true) + now += 59_000 + expect(qm.isBackedOff()).toBe(true) + now += 2_000 + expect(qm.isBackedOff()).toBe(false) + }) + + test('repeated 429s escalate backoff exponentially', async () => { + const fetchMock = mock(() => + Promise.resolve(new Response('rate limited', { status: 429 })), + ) as unknown as typeof fetch + const qm = createQM(fetchMock) + + // First failure: 60s + try { + await qm.refreshMain('token') + } catch {} + expect(qm.isBackedOff()).toBe(true) + + now += 61_000 + expect(qm.isBackedOff()).toBe(false) + + // Second failure: 120s + try { + await qm.refreshMain('token') + } catch {} + now += 119_000 + expect(qm.isBackedOff()).toBe(true) + now += 2_000 + expect(qm.isBackedOff()).toBe(false) + }) + + test('backoff caps at 15 minutes', async () => { + const fetchMock = mock(() => + Promise.resolve(new Response('rate limited', { status: 429 })), + ) as unknown as typeof fetch + const qm = createQM(fetchMock) + + // Trigger 8 failures to exceed cap + for (let i = 0; i < 8; i++) { + try { + await qm.refreshMain('token') + } catch {} + now += 16 * 60_000 + } + + try { + await qm.refreshMain('token') + } catch {} + now += 14 * 60_000 + expect(qm.isBackedOff()).toBe(true) + now += 2 * 60_000 + expect(qm.isBackedOff()).toBe(false) + }) + + test('successful fetch resets backoff', async () => { + let failNext = true + const fetchMock = mock(() => { + if (failNext) { + return Promise.resolve(new Response('rate limited', { status: 429 })) + } + return Promise.resolve(makeQuotaResponse(now)) + }) as unknown as typeof fetch + const qm = createQM(fetchMock) + + try { + await qm.refreshMain('token') + } catch {} + expect(qm.isBackedOff()).toBe(true) + + now += 61_000 + failNext = false + await qm.refreshMain('token') + expect(qm.isBackedOff()).toBe(false) + + // Next failure starts from 60s again (not escalated) + failNext = true + now += 1_100 + try { + await qm.refreshMain('token') + } catch {} + now += 59_000 + expect(qm.isBackedOff()).toBe(true) + now += 2_000 + expect(qm.isBackedOff()).toBe(false) + }) + + test('getLastApiError exposes backoff state', async () => { + const fetchMock = mock(() => + Promise.resolve(new Response('rate limited', { status: 429 })), + ) as unknown as typeof fetch + const qm = createQM(fetchMock) + + expect(qm.getLastApiError()).toBeUndefined() + + try { + await qm.refreshMain('token') + } catch {} + + const err = qm.getLastApiError() + expect(err).toBeDefined() + expect(err!.retryCount).toBe(1) + expect(err!.nextRetryAt).toBeGreaterThan(now) + }) + + test('500 errors also trigger backoff', async () => { + const fetchMock = mock(() => + Promise.resolve(new Response('internal error', { status: 500 })), + ) as unknown as typeof fetch + const qm = createQM(fetchMock) + + try { + await qm.refreshMain('token') + } catch {} + expect(qm.isBackedOff()).toBe(true) + }) + + test('returns cached quota during backoff', async () => { + let failNext = false + const fetchMock = mock(() => { + if (failNext) { + return Promise.resolve(new Response('rate limited', { status: 429 })) + } + return Promise.resolve(makeQuotaResponse(now)) + }) as unknown as typeof fetch + const qm = createQM(fetchMock) + + const first = await qm.refreshMain('token') + expect(first).toBeDefined() + + failNext = true + now += 1_100 + try { + await qm.refreshMain('token') + } catch {} + + const cached = qm.getMain() + expect(cached).not.toBeNull() + }) + }) + + describe('persistence', () => { + test('seeds main quota from persisted storage', () => { + const quota = { + quotas: [], + expires: new Date(2_000_000).toISOString(), + } + const qm = new QuotaManager({ + storage: { + version: 1, + accounts: [], + quota: { + mainQuota: quota as any, + mainQuotaCheckedAt: 900_000, + }, + }, + now: () => 1_000_000, + }) + + const main = qm.getMain() + expect(main).not.toBeNull() + expect(main!.checkedAt).toBe(900_000) + }) + + test('calls onMainQuotaFetched after successful fetch', async () => { + let callbackQuota: any = null + const fetchMock = mock(() => + Promise.resolve(makeQuotaResponse(now)), + ) as unknown as typeof fetch + + const qm = new QuotaManager({ + storage: null, + fetchImpl: fetchMock, + now: () => now, + onMainQuotaFetched: (quota, checkedAt) => { + callbackQuota = { quota, checkedAt } + }, + }) + + await qm.refreshMain('token') + expect(callbackQuota).not.toBeNull() + expect(callbackQuota.checkedAt).toBe(now) + }) + + test('seeds backoff state from persisted storage', () => { + const qm = new QuotaManager({ + storage: { + version: 1, + accounts: [], + quota: { + mainLastQuotaApiError: { + message: 'Claude quota check failed: 429 — rate limited', + checkedAt: now - 30_000, + nextRetryAt: now + 30_000, + retryCount: 1, + }, + }, + }, + now: () => now, + }) + + expect(qm.isBackedOff()).toBe(true) + }) + + test('ignores expired persisted backoff', () => { + const qm = new QuotaManager({ + storage: { + version: 1, + accounts: [], + quota: { + mainLastQuotaApiError: { + message: 'old error', + checkedAt: now - 120_000, + nextRetryAt: now - 60_000, + retryCount: 1, + }, + }, + }, + now: () => now, + }) + + expect(qm.isBackedOff()).toBe(false) + }) + + test('calls onApiError callback on failure', async () => { + let errorCallback: any = null + const fetchMock = mock(() => + Promise.resolve(new Response('rate limited', { status: 429 })), + ) as unknown as typeof fetch + + const qm = new QuotaManager({ + storage: null, + fetchImpl: fetchMock, + now: () => now, + onApiError: (error) => { + errorCallback = error + }, + }) + + try { + await qm.refreshMain('token') + } catch {} + + expect(errorCallback).not.toBeNull() + expect(errorCallback.retryCount).toBe(1) + expect(errorCallback.nextRetryAt).toBeGreaterThan(now) + }) + }) +}) From 8b20be731ccf67fd4da893f7b82738587b7e05a9 Mon Sep 17 00:00:00 2001 From: iceteaSA <171169159+iceteaSA@users.noreply.github.com> Date: Thu, 21 May 2026 20:43:35 +0200 Subject: [PATCH 2/2] feat(opencode): TUI sidebar quota widget Adds a TUI sidebar widget showing real-time quota usage for main and fallback accounts. Displays usage bars, reset times, relay status, fast mode, and cache-keepalive state. New files: sidebar-state.ts, tui.tsx, scripts/copy-tui.mjs Modified: package.json (TUI deps, exports), index.ts (writeSidebarState) --- bun.lock | 225 +++++++++++++++++++++- packages/core/src/cachekeep.ts | 4 + packages/opencode/package.json | 15 +- packages/opencode/scripts/copy-tui.mjs | 9 + packages/opencode/src/index.ts | 47 +++++ packages/opencode/src/sidebar-state.ts | 69 +++++++ packages/opencode/src/tui.tsx | 251 +++++++++++++++++++++++++ packages/opencode/tsconfig.build.json | 2 +- 8 files changed, 614 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/scripts/copy-tui.mjs create mode 100644 packages/opencode/src/sidebar-state.ts create mode 100644 packages/opencode/src/tui.tsx diff --git a/bun.lock b/bun.lock index e2dcfb4..2cbd0b7 100644 --- a/bun.lock +++ b/bun.lock @@ -20,7 +20,7 @@ }, "packages/core": { "name": "@cortexkit/anthropic-auth-core", - "version": "1.2.0", + "version": "1.2.2", "dependencies": { "xxhash-wasm": "^1.1.0", }, @@ -33,12 +33,15 @@ }, "packages/opencode": { "name": "@cortexkit/opencode-anthropic-auth", - "version": "1.2.0", + "version": "1.2.2", "bin": { - "opencode-anthropic-auth": "./dist/cli.js", + "opencode-anthropic-auth": "dist/cli.js", }, "dependencies": { - "@cortexkit/anthropic-auth-core": "1.0.0", + "@cortexkit/anthropic-auth-core": "1.2.2", + "@opentui/core": ">=0.1.92", + "@opentui/solid": ">=0.1.92", + "solid-js": "^1.9.10", }, "peerDependencies": { "@opencode-ai/plugin": "*", @@ -46,7 +49,7 @@ }, "packages/pi": { "name": "@cortexkit/pi-anthropic-auth", - "version": "1.2.0", + "version": "1.2.2", "dependencies": { "@cortexkit/anthropic-auth-core": "1.1.3", }, @@ -58,6 +61,8 @@ }, }, "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -132,8 +137,64 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + + "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.3", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.29.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@biomejs/biome": ["@biomejs/biome@2.4.15", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.15", "@biomejs/cli-darwin-x64": "2.4.15", "@biomejs/cli-linux-arm64": "2.4.15", "@biomejs/cli-linux-arm64-musl": "2.4.15", "@biomejs/cli-linux-x64": "2.4.15", "@biomejs/cli-linux-x64-musl": "2.4.15", "@biomejs/cli-win32-arm64": "2.4.15", "@biomejs/cli-win32-x64": "2.4.15" }, "bin": { "biome": "bin/biome" } }, "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg=="], @@ -234,6 +295,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], @@ -282,6 +345,22 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.15.5", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-ozJuEmXzrOvia5n0L1KAuvpyf9ESGmTk1FiPhn0RK5X1whbzjlTXL0NAxqNCEkqETxL35jS1KHArEiTpvtJ6FQ=="], + "@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="], + + "@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="], + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], @@ -414,26 +493,46 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], + + "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -448,8 +547,18 @@ "effect": ["effect@4.0.0-beta.65", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.360", "", {}, "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], @@ -460,14 +569,24 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], @@ -478,10 +597,14 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], @@ -490,14 +613,22 @@ "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], @@ -530,6 +661,8 @@ "lefthook-windows-x64": ["lefthook-windows-x64@2.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-q4z2n3xucLscoWiyMwFViEj3N8MDSkPulMwcJYuCYFHoPhP1h+icqNu7QRLGYj6AnVrCQweiUJY3Tb2X+GbD/A=="], + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], @@ -556,30 +689,58 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "node-releases": ["node-releases@2.0.45", "", {}, "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg=="], + "openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], "protobufjs": ["protobufjs@7.5.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.1", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg=="], "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="], + + "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -588,10 +749,18 @@ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], @@ -606,10 +775,14 @@ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "workerd": ["workerd@1.20260515.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260515.1", "@cloudflare/workerd-darwin-arm64": "1.20260515.1", "@cloudflare/workerd-linux-64": "1.20260515.1", "@cloudflare/workerd-linux-arm64": "1.20260515.1", "@cloudflare/workerd-windows-64": "1.20260515.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-MjKOJLcvU45xXedQowvuiHtJTxu4WTHYQeIlF7YmjuqhiI6dImTFxWCEoRQHiskztxuVSNEmdO7/0UfDu6OMnQ=="], @@ -618,8 +791,12 @@ "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], @@ -628,24 +805,62 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@google/genai/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mistralai/mistralai/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + "@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + + "@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + "miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + + "babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/packages/core/src/cachekeep.ts b/packages/core/src/cachekeep.ts index f129fee..61caf01 100644 --- a/packages/core/src/cachekeep.ts +++ b/packages/core/src/cachekeep.ts @@ -303,6 +303,10 @@ export class CacheKeepManager { return { trackedSessions: targets.length, nextPrewarmAt } } + trackedCount(): number { + return this.targets.size + } + track(input: { sessionId?: string | null url: string diff --git a/packages/opencode/package.json b/packages/opencode/package.json index c9df889..3742d70 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -13,8 +13,16 @@ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" + }, + "./tui": { + "types": "./dist/tui.d.ts", + "default": "./dist/tui.tsx" } }, + "oc-plugin": [ + ".", + "./tui" + ], "bin": { "opencode-anthropic-auth": "dist/cli.js" }, @@ -27,7 +35,7 @@ "LICENSE" ], "scripts": { - "build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly", + "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/sidebar-state.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly && node scripts/copy-tui.mjs", "build:dev": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "bun ../../scripts/dev.ts", "dev:clean": "bun ../../scripts/dev-clean.ts", @@ -41,6 +49,9 @@ "@opencode-ai/plugin": "*" }, "dependencies": { - "@cortexkit/anthropic-auth-core": "1.2.2" + "@cortexkit/anthropic-auth-core": "1.2.2", + "@opentui/core": ">=0.1.92", + "@opentui/solid": ">=0.1.92", + "solid-js": "^1.9.10" } } diff --git a/packages/opencode/scripts/copy-tui.mjs b/packages/opencode/scripts/copy-tui.mjs new file mode 100644 index 0000000..037c216 --- /dev/null +++ b/packages/opencode/scripts/copy-tui.mjs @@ -0,0 +1,9 @@ +import { copyFileSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const src = resolve(__dirname, '..', 'src', 'tui.tsx') +const dest = resolve(__dirname, '..', 'dist', 'tui.tsx') +copyFileSync(src, dest) +console.log('copied tui.tsx to dist/') \ No newline at end of file diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 3bc332a..8bda141 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -66,6 +66,7 @@ import { } from '@cortexkit/anthropic-auth-core' import type { Plugin } from '@opencode-ai/plugin' import { resolvePromptContext } from './prompt-context.ts' +import { type SidebarState, setSidebarState } from './sidebar-state.ts' import { addFastModeBetaHeader, createStrippedStream, @@ -328,6 +329,49 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { }) setDumpEnabled(isDumpPersistentlyEnabled(initialStorage)) setFastModeEnabled(isFastModePersistentlyEnabled(initialStorage)) + + function writeSidebarState( + storage: Awaited>, + activeId: string | undefined, + route: string, + ) { + const mainEntry = quotaManager.getMain() + const state: SidebarState = { + main: { + quota: mainEntry?.quota ?? null, + }, + fallbacks: (storage?.accounts ?? []) + .filter((a) => a.enabled !== false) + .map((a) => ({ + id: a.id, + label: a.label, + quota: a.quota ?? null, + enabled: a.enabled !== false, + })), + activeId, + route, + relay: relayConfig + ? { enabled: true, transport: relayConfig.transport ?? 'http' } + : null, + fastMode: isFastModeEnabled(), + cacheKeep: { + enabled: isCacheKeepHybridActive(storage), + window: + storage?.cacheKeep?.startHour != null && + storage?.cacheKeep?.endHour != null + ? `${storage.cacheKeep.startHour}-${storage.cacheKeep.endHour}` + : undefined, + trackedSessions: cacheKeepManager.trackedCount(), + }, + lastUpdated: Date.now(), + } + setSidebarState(state).catch((error) => + log('[sidebar] state write failed', { + error: error instanceof Error ? error.message : String(error), + }), + ) + } + let latestGetAuth: | (() => Promise<{ type: string @@ -579,6 +623,8 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { input.sessionID, await buildQuotaCommandSummary(), ) + const cmdStorage = await loadAccounts() + writeSidebarState(cmdStorage, 'main', 'main') throwHandledSentinel() } @@ -1010,6 +1056,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => { } startMainBackgroundRefresh() + writeSidebarState(initialStorage, 'main', 'main') function isReplayableRequest( input: string | URL | Request, diff --git a/packages/opencode/src/sidebar-state.ts b/packages/opencode/src/sidebar-state.ts new file mode 100644 index 0000000..7e2cb4d --- /dev/null +++ b/packages/opencode/src/sidebar-state.ts @@ -0,0 +1,69 @@ +export interface QuotaWindow { + usedPercent: number + remainingPercent: number + resetsAt?: string +} + +export interface AccountQuota { + five_hour?: QuotaWindow + seven_day?: QuotaWindow +} + +export interface SidebarAccountState { + id: string + label: string | undefined + quota: AccountQuota | null + enabled: boolean +} + +export interface SidebarState { + main: { + quota: AccountQuota | null + } + fallbacks: SidebarAccountState[] + activeId: string | undefined + route: string + relay: { enabled: boolean; transport: string } | null + fastMode: boolean + cacheKeep?: { + enabled: boolean + window?: string + trackedSessions?: number + } + lastUpdated: number +} + +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth') +const STATE_FILE = join(STATE_DIR, 'sidebar-state.json') + +export const DEFAULT_SIDEBAR_STATE: SidebarState = { + main: { quota: null }, + fallbacks: [], + activeId: undefined, + route: 'main', + relay: null, + fastMode: false, + lastUpdated: 0, +} + +export async function getSidebarState(): Promise { + try { + const raw = await readFile(STATE_FILE, 'utf8') + return JSON.parse(raw) as SidebarState + } catch { + return DEFAULT_SIDEBAR_STATE + } +} + +export async function setSidebarState(state: SidebarState): Promise { + try { + await mkdir(STATE_DIR, { recursive: true }) + await writeFile(STATE_FILE, JSON.stringify(state), 'utf8') + } catch { + // Best-effort — sidebar is non-critical + } +} diff --git a/packages/opencode/src/tui.tsx b/packages/opencode/src/tui.tsx new file mode 100644 index 0000000..ceef011 --- /dev/null +++ b/packages/opencode/src/tui.tsx @@ -0,0 +1,251 @@ +/** @jsxImportSource @opentui/solid */ + +import { readFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { + TuiPlugin, + TuiPluginApi, + TuiPluginModule, +} from '@opencode-ai/plugin/tui' +import { createSignal, For, onCleanup, Show } from 'solid-js' +import type { AccountQuota, SidebarState } from './sidebar-state.js' + +const STATE_FILE = join( + tmpdir(), + 'opencode-anthropic-auth', + 'sidebar-state.json', +) +const POLL_MS = 2000 + +const DEFAULT_STATE: SidebarState = { + main: { quota: null }, + fallbacks: [], + activeId: undefined, + route: 'main', + relay: null, + fastMode: false, + lastUpdated: 0, +} + +async function readStateFromFile(): Promise { + try { + const raw = await readFile(STATE_FILE, 'utf8') + return JSON.parse(raw) as SidebarState + } catch { + return DEFAULT_STATE + } +} + +const ID = 'cortexkit.anthropic-auth' +const BAR_WIDTH = 12 +const BAR_FILLED = '\u2588' +const BAR_EMPTY = '\u2591' + +function quotaBar(usedPct: number, width = BAR_WIDTH): string { + const filled = Math.round((usedPct / 100) * width) + return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(width - filled) +} + +function barColor(usedPct: number, api: TuiPluginApi): string { + if (usedPct < 50) + return api.theme.current.success ?? api.theme.current.accent ?? 'green' + if (usedPct < 80) return api.theme.current.warning ?? 'yellow' + return api.theme.current.error ?? 'red' +} + +function formatResetIn(resetsAt: string | undefined): string { + if (!resetsAt) return '' + const ms = new Date(resetsAt).getTime() - Date.now() + if (ms <= 0) return 'now' + const mins = Math.floor(ms / 60_000) + if (mins < 60) return `${mins}m` + const hrs = Math.floor(mins / 60) + const rm = mins % 60 + return rm > 0 ? `${hrs}h${rm}m` : `${hrs}h` +} + +function QuotaBar(props: { + label: string + usedPct: number + api: TuiPluginApi +}) { + const color = () => barColor(props.usedPct, props.api) + const muted = () => props.api.theme.current.textMuted + return ( + + {` ${props.label} `} + {quotaBar(props.usedPct)} + {` ${String(Math.round(props.usedPct)).padStart(3)}%`} + + ) +} + +function AccountSection(props: { + name: string + quota: AccountQuota | null + active: boolean + api: TuiPluginApi +}) { + const dotColor = () => + props.active + ? (props.api.theme.current.success ?? + props.api.theme.current.accent ?? + 'green') + : (props.api.theme.current.textMuted ?? 'gray') + const muted = () => props.api.theme.current.textMuted + const resetStr = () => formatResetIn(props.quota?.five_hour?.resetsAt) + return ( + + + + {'* '} + + {props.name} + + {` ${resetStr()}`} + + + {' checking...'}} + > + + + + + ) +} + +function QuotaSidebar(props: { api: TuiPluginApi }) { + const [state, setState] = createSignal(DEFAULT_STATE) + let lastUpdated = 0 + + async function refresh() { + const next = await readStateFromFile() + if (next.lastUpdated !== lastUpdated) { + lastUpdated = next.lastUpdated + setState(next) + + } + } + + // Poll globalThis since server and TUI load separate module instances + const timer = setInterval(refresh, POLL_MS) + onCleanup(() => clearInterval(timer)) + + // Also refresh on OpenCode events for faster updates + const unsubs = [ + props.api.event.on('session.updated', refresh), + props.api.event.on('message.updated', refresh), + ] + onCleanup(() => { + for (const u of unsubs) u() + }) + + // Initial refresh after short delay (server plugin may not have written yet) + setTimeout(refresh, 500) + setTimeout(refresh, 2000) + + + + const hasData = () => + state().main.quota != null || state().fallbacks.length > 0 + const muted = () => props.api.theme.current.textMuted ?? '#71717a' + + return ( + + + {'\u2500 Claude Quota \u2500\u2500\u2500\u2500\u2500'} + + {' Waiting...'}} + > + + + f.enabled)}> + {(fb) => ( + + + + + )} + + + + + { + '\u2500\u2500 Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500' + } + + + {' Route: '} + + {state().route} + + + + {' Mode: '} + {state().fastMode ? 'fast \u26a1' : 'std'} + + + {' Relay: '} + {'\u2014'}} + > + {`${state().relay?.transport} `} + + {'*'} + + + + + ) +} + +const tui: TuiPlugin = async (api) => { + api.slots.register({ + order: 150, + slots: { + sidebar_content(_ctx: unknown, _props: { session_id: string }) { + return + }, + }, + }) +} + +const plugin: TuiPluginModule & { id: string } = { + id: ID, + tui, +} + +export default plugin diff --git a/packages/opencode/tsconfig.build.json b/packages/opencode/tsconfig.build.json index 0f2a29d..0bd81dd 100644 --- a/packages/opencode/tsconfig.build.json +++ b/packages/opencode/tsconfig.build.json @@ -10,5 +10,5 @@ "rewriteRelativeImportExtensions": true }, "include": ["src/**/*.ts"], - "exclude": ["src/tests/**"] + "exclude": ["src/tests/**", "src/tui.tsx"] }