import Foundation public protocol ClaudeUsageFetching: Sendable { func loadLatestUsage(model: String) async throws -> ClaudeUsageSnapshot func debugRawProbe(model: String) async -> String func detectVersion() -> String?} public struct ClaudeUsageSnapshot: Sendable { public enum PrimaryWindowKind: Equatable, Sendable { case usage case spendLimit } public let primary: RateWindow public let primaryWindowKind: PrimaryWindowKind public let secondary: RateWindow? public let opus: RateWindow? public let extraRateWindows: [NamedRateWindow] public let providerCost: ProviderCostSnapshot? public let updatedAt: Date public let accountEmail: String? public let accountOrganization: String? public let loginMethod: String? public let rawText: String? public init( primary: RateWindow, primaryWindowKind: PrimaryWindowKind = .usage, secondary: RateWindow?, opus: RateWindow?, extraRateWindows: [NamedRateWindow] = [], providerCost: ProviderCostSnapshot? = nil, updatedAt: Date, accountEmail: String?, accountOrganization: String?, loginMethod: String?, rawText: String?) { self.primary = primary self.primaryWindowKind = primaryWindowKind self.secondary = secondary self.opus = opus self.extraRateWindows = extraRateWindows self.providerCost = providerCost self.updatedAt = updatedAt self.accountEmail = accountEmail self.accountOrganization = accountOrganization self.loginMethod = loginMethod self.rawText = rawText }} public enum ClaudeUsageError: LocalizedError, Sendable { case claudeNotInstalled case parseFailed(String) case oauthFailed(String) public var errorDescription: String? { switch self { case .claudeNotInstalled: "Claude CLI is not installed. Install it from https://code.claude.com/docs/en/overview." case let .parseFailed(details): "Could not parse Claude usage: \(details)" case let .oauthFailed(details): details } }} public struct ClaudeUsageFetcher: ClaudeUsageFetching, Sendable { private static let sessionWindowMinutes = 5 * 60 private static let weeklyWindowMinutes = 7 * 24 * 60 private static let cliAutoProbeTimeout: TimeInterval = 12 private static let cliProbeTimeout: TimeInterval = 24 private static let cliRetryProbeTimeout: TimeInterval = 60 private struct Configuration { let environment: [String: String] let runtime: ProviderRuntime let dataSource: ClaudeUsageDataSource let oauthKeychainPromptCooldownEnabled: Bool let allowBackgroundDelegatedRefresh: Bool let allowStartupBootstrapPrompt: Bool let useWebExtras: Bool let manualCookieHeader: String? let webOrganizationID: String? let keepCLISessionsAlive: Bool let browserDetection: BrowserDetection } private let configuration: Configuration private static let log = CodexBarLog.logger(LogCategories.claudeUsage) private static var isClaudeOAuthFlowDebugEnabled: Bool { ProcessInfo.processInfo.environment["CODEXBAR_DEBUG_CLAUDE_OAUTH_FLOW"] == "1" } private var environment: [String: String] { self.configuration.environment } private var runtime: ProviderRuntime { self.configuration.runtime } private var dataSource: ClaudeUsageDataSource { self.configuration.dataSource } private var oauthKeychainPromptCooldownEnabled: Bool { self.configuration.oauthKeychainPromptCooldownEnabled } private var allowBackgroundDelegatedRefresh: Bool { self.configuration.allowBackgroundDelegatedRefresh } private var allowStartupBootstrapPrompt: Bool { self.configuration.allowStartupBootstrapPrompt } private var useWebExtras: Bool { self.configuration.useWebExtras } private var manualCookieHeader: String? { self.configuration.manualCookieHeader } private var webOrganizationID: String? { self.configuration.webOrganizationID } private var keepCLISessionsAlive: Bool { self.configuration.keepCLISessionsAlive } private var browserDetection: BrowserDetection { self.configuration.browserDetection } private struct ClaudeOAuthKeychainPromptPolicy { let mode: ClaudeOAuthKeychainPromptMode let isApplicable: Bool let interaction: ProviderInteraction var canPromptNow: Bool { switch self.mode { case .never: false case .onlyOnUserAction: self.interaction == .userInitiated case .always: true } } /// Respect the Keychain prompt cooldown for background operations to avoid spamming system dialogs. /// User actions (menu open / refresh / settings) are allowed to bypass the cooldown. var shouldRespectKeychainPromptCooldown: Bool { self.interaction != .userInitiated } var interactionLabel: String { self.interaction == .userInitiated ? "user" : "background" } } private static func currentClaudeOAuthInteractivePromptPolicy() -> ClaudeOAuthKeychainPromptPolicy { let policy = ClaudeOAuthKeychainPromptPolicy( mode: ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode(), isApplicable: true, interaction: ProviderInteractionContext.current) // User actions should be able to immediately retry a Security.framework fallback repair after a background // cooldown was recorded, even when /usr/bin/security is the primary reader. if policy.interaction == .userInitiated { if ClaudeOAuthKeychainAccessGate.clearDenied() { Self.log.info("Claude OAuth keychain cooldown cleared by user action") } } return policy } private static func currentClaudeOAuthDelegatedRefreshPolicy() -> ClaudeOAuthKeychainPromptPolicy { ClaudeOAuthKeychainPromptPolicy( mode: ClaudeOAuthKeychainPromptPreference.current(), isApplicable: ClaudeOAuthKeychainPromptPreference.isApplicable(), interaction: ProviderInteractionContext.current) } private static func assertDelegatedRefreshAllowedInCurrentInteraction( policy: ClaudeOAuthKeychainPromptPolicy, allowBackgroundDelegatedRefresh: Bool) throws { guard policy.isApplicable else { return } if policy.mode == .never { throw ClaudeUsageError.oauthFailed("Delegated refresh is disabled by 'never' keychain policy.") } if policy.mode == .onlyOnUserAction, policy.interaction != .userInitiated, !allowBackgroundDelegatedRefresh { throw ClaudeUsageError.oauthFailed( "Claude OAuth token expired, but background repair is suppressed when Keychain prompt policy " + "is set to only prompt on user action. Open the CodexBar menu or click Refresh to retry.") } } #if DEBUG @TaskLocal static var loadOAuthCredentialsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? @TaskLocal static var fetchOAuthUsageOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? @TaskLocal static var delegatedRefreshAttemptOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? @TaskLocal static var hasCachedCredentialsOverride: Bool? #endif /// Creates a new ClaudeUsageFetcher. /// - Parameters: /// - environment: Process environment (default: current process environment) /// - dataSource: Usage data source (default: OAuth API). /// - useWebExtras: If true, attempts to enrich usage with Claude web data (cookies). public init( browserDetection: BrowserDetection, environment: [String: String] = ProcessInfo.processInfo.environment, runtime: ProviderRuntime = .app, dataSource: ClaudeUsageDataSource = .oauth, oauthKeychainPromptCooldownEnabled: Bool = false, allowBackgroundDelegatedRefresh: Bool = false, allowStartupBootstrapPrompt: Bool = false, useWebExtras: Bool = false, manualCookieHeader: String? = nil, webOrganizationID: String? = nil, keepCLISessionsAlive: Bool = false) { self.configuration = Configuration( environment: environment, runtime: runtime, dataSource: dataSource, oauthKeychainPromptCooldownEnabled: oauthKeychainPromptCooldownEnabled, allowBackgroundDelegatedRefresh: allowBackgroundDelegatedRefresh, allowStartupBootstrapPrompt: allowStartupBootstrapPrompt, useWebExtras: useWebExtras, manualCookieHeader: manualCookieHeader, webOrganizationID: webOrganizationID, keepCLISessionsAlive: keepCLISessionsAlive, browserDetection: browserDetection) } private struct OAuthExecutor { let fetcher: ClaudeUsageFetcher func load(allowDelegatedRetry: Bool) async throws -> ClaudeUsageSnapshot { do { let promptPolicy = ClaudeUsageFetcher.currentClaudeOAuthInteractivePromptPolicy() #if DEBUG let hasCache = if let hasCachedCredentialsOverride = ClaudeUsageFetcher.hasCachedCredentialsOverride { hasCachedCredentialsOverride } else if ClaudeUsageFetcher.loadOAuthCredentialsOverride != nil { false } else { ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: self.fetcher.environment) } #else let hasCache = ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: self.fetcher.environment) #endif let startupBootstrapOverride = self.shouldAllowStartupBootstrapPrompt( policy: promptPolicy, hasCache: hasCache) let allowKeychainPrompt = (promptPolicy.canPromptNow || startupBootstrapOverride) && !hasCache ClaudeUsageFetcher.logOAuthBootstrapPromptDecision( allowKeychainPrompt: allowKeychainPrompt, policy: promptPolicy, hasCache: hasCache, startupBootstrapOverride: startupBootstrapOverride) let credentials = try await ClaudeOAuthCredentialsStore.$allowBackgroundPromptBootstrap .withValue(startupBootstrapOverride) { try await ClaudeUsageFetcher.loadOAuthCredentials( environment: self.fetcher.environment, allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: promptPolicy.shouldRespectKeychainPromptCooldown) } try self.validateRequiredOAuthScope(credentials) let usage = try await ClaudeUsageFetcher.fetchOAuthUsage(accessToken: credentials.accessToken) return try ClaudeUsageFetcher.mapOAuthUsage(usage, credentials: credentials) } catch let error as CancellationError { throw error } catch let error as ClaudeUsageError { throw error } catch let error as ClaudeOAuthCredentialsError { if case .refreshDelegatedToClaudeCLI = error { return try await self.loadAfterDelegatedRefresh(allowDelegatedRetry: allowDelegatedRetry) } throw ClaudeUsageError.oauthFailed(error.localizedDescription) } catch let error as ClaudeOAuthFetchError { if case .rateLimited = error { throw ClaudeUsageError.oauthFailed(error.localizedDescription) } ClaudeOAuthCredentialsStore.invalidateCache() if case let .serverError(statusCode, body) = error, statusCode == 403, body?.contains("user:profile") ?? false { throw ClaudeUsageError.oauthFailed( "Claude OAuth token does not meet scope requirement 'user:profile'. " + "Run `claude setup-token` to re-generate credentials, or switch Claude Source to " + "Web/CLI.") } throw ClaudeUsageError.oauthFailed(error.localizedDescription) } catch { throw ClaudeUsageError.oauthFailed(error.localizedDescription) } } private func shouldAllowStartupBootstrapPrompt( policy: ClaudeOAuthKeychainPromptPolicy, hasCache: Bool) -> Bool { guard self.fetcher.allowStartupBootstrapPrompt else { return false } guard !hasCache else { return false } guard ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode() == .onlyOnUserAction else { return false } guard policy.interaction == .background else { return false } return ProviderRefreshContext.current == .startup } private func loadAfterDelegatedRefresh(allowDelegatedRetry: Bool) async throws -> ClaudeUsageSnapshot { guard allowDelegatedRetry else { throw ClaudeUsageError.oauthFailed( "Claude OAuth token expired and delegated Claude CLI refresh did not recover. " + "Run `claude login`, then retry.") } try Task.checkCancellation() let delegatedPromptPolicy = ClaudeUsageFetcher.currentClaudeOAuthDelegatedRefreshPolicy() try ClaudeUsageFetcher.assertDelegatedRefreshAllowedInCurrentInteraction( policy: delegatedPromptPolicy, allowBackgroundDelegatedRefresh: self.fetcher.allowBackgroundDelegatedRefresh) let delegatedOutcome = await ClaudeUsageFetcher.attemptDelegatedRefresh( environment: self.fetcher.environment) ClaudeUsageFetcher.log.info( "Claude OAuth delegated refresh attempted", metadata: [ "outcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), ]) do { if self.fetcher.oauthKeychainPromptCooldownEnabled { switch delegatedOutcome { case .skippedByCooldown, .cliUnavailable: throw ClaudeUsageError.oauthFailed( "Claude OAuth token expired; delegated refresh is unavailable (outcome=" + "\(ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome))).") case .attemptedSucceeded, .attemptedFailed: break } } try Task.checkCancellation() _ = ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged() let didSyncSilently = delegatedOutcome == .attemptedSucceeded && ClaudeOAuthCredentialsStore.syncFromClaudeKeychainWithoutPrompt(now: Date()) let promptPolicy = ClaudeUsageFetcher.currentClaudeOAuthInteractivePromptPolicy() ClaudeUsageFetcher.logDeferredBackgroundDelegatedRecoveryIfNeeded( delegatedOutcome: delegatedOutcome, didSyncSilently: didSyncSilently, policy: promptPolicy) let retryAllowKeychainPrompt = promptPolicy.canPromptNow && !didSyncSilently if retryAllowKeychainPrompt { ClaudeUsageFetcher.log.info( "Claude OAuth keychain prompt allowed (post-delegation retry)", metadata: [ "interaction": promptPolicy.interactionLabel, "promptMode": promptPolicy.mode.rawValue, "promptPolicyApplicable": "\(promptPolicy.isApplicable)", "delegatedOutcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), "didSyncSilently": "\(didSyncSilently)", ]) } if ClaudeUsageFetcher.isClaudeOAuthFlowDebugEnabled { ClaudeUsageFetcher.log.debug( "Claude OAuth credential load (post-delegation retry start)", metadata: [ "cooldownEnabled": "\(self.fetcher.oauthKeychainPromptCooldownEnabled)", "didSyncSilently": "\(didSyncSilently)", "allowKeychainPrompt": "\(retryAllowKeychainPrompt)", "delegatedOutcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), "interaction": promptPolicy.interactionLabel, "promptMode": promptPolicy.mode.rawValue, "promptPolicyApplicable": "\(promptPolicy.isApplicable)", ]) } let refreshedCredentials = try await ClaudeUsageFetcher.loadOAuthCredentials( environment: self.fetcher.environment, allowKeychainPrompt: retryAllowKeychainPrompt, respectKeychainPromptCooldown: promptPolicy.shouldRespectKeychainPromptCooldown) if ClaudeUsageFetcher.isClaudeOAuthFlowDebugEnabled { ClaudeUsageFetcher.log.debug( "Claude OAuth credential load (post-delegation retry)", metadata: [ "cooldownEnabled": "\(self.fetcher.oauthKeychainPromptCooldownEnabled)", "didSyncSilently": "\(didSyncSilently)", "allowKeychainPrompt": "\(retryAllowKeychainPrompt)", "delegatedOutcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), "interaction": promptPolicy.interactionLabel, "promptMode": promptPolicy.mode.rawValue, "promptPolicyApplicable": "\(promptPolicy.isApplicable)", ]) } try self.validateRequiredOAuthScope(refreshedCredentials) let usage = try await ClaudeUsageFetcher.fetchOAuthUsage( accessToken: refreshedCredentials.accessToken) return try ClaudeUsageFetcher.mapOAuthUsage(usage, credentials: refreshedCredentials) } catch { ClaudeUsageFetcher.log.debug( "Claude OAuth post-delegation retry failed", metadata: ClaudeUsageFetcher.delegatedRetryFailureMetadata( error: error, oauthKeychainPromptCooldownEnabled: self.fetcher.oauthKeychainPromptCooldownEnabled, delegatedOutcome: delegatedOutcome)) throw ClaudeUsageError.oauthFailed( ClaudeUsageFetcher.delegatedRefreshFailureMessage( for: delegatedOutcome, retryError: error)) } } private func validateRequiredOAuthScope(_ credentials: ClaudeOAuthCredentials) throws { guard credentials.scopes.contains("user:profile") else { let scopes = credentials.scopes.joined(separator: ", ") let detail = scopes.isEmpty ? "Claude OAuth token missing 'user:profile' scope." : "Claude OAuth token missing 'user:profile' scope (has: \(scopes))." throw ClaudeUsageError.oauthFailed( detail + " Run `claude setup-token` to re-generate credentials, or switch Claude Source to " + "Web/CLI.") } } } private struct StepExecutor { let fetcher: ClaudeUsageFetcher func loadLatestUsage(model: String) async throws -> ClaudeUsageSnapshot { switch self.fetcher.dataSource { case .auto: return try await self.executeAuto(model: model) case .api: throw ClaudeUsageError.parseFailed("Claude Admin API usage is handled by the provider descriptor.") case .oauth: var snapshot = try await self.fetcher.loadViaOAuth(allowDelegatedRetry: true) snapshot = await self.fetcher.applyWebExtrasIfNeeded(to: snapshot) return snapshot case .web: return try await self.fetcher.loadViaWebAPI() case .cli: return try await self.loadViaCLIWithRetry(model: model) } } private func executeAuto(model: String) async throws -> ClaudeUsageSnapshot { let plan = await self.makeAutoFetchPlan() self.logAutoPlan(plan) let executionSteps = plan.executionSteps for (index, step) in executionSteps.enumerated() { do { return try await self.execute(step: step, model: model) } catch { if index < executionSteps.count - 1 { ClaudeUsageFetcher.log.debug( "Claude planner step failed; falling back to next step", metadata: [ "step": step.dataSource.rawValue, "reason": step.inclusionReason.rawValue, "errorType": String(describing: type(of: error)), ]) continue } throw error } } throw ClaudeUsageError.parseFailed("Claude planner produced no executable steps.") }Lines 1–500 of 1352. Load more
import Foundation public protocol ClaudeUsageFetching: Sendable { func loadLatestUsage(model: String) async throws -> ClaudeUsageSnapshot func debugRawProbe(model: String) async -> String func detectVersion() -> String? } public struct ClaudeUsageSnapshot: Sendable { public enum PrimaryWindowKind: Equatable, Sendable { case usage case spendLimit } public let primary: RateWindow public let primaryWindowKind: PrimaryWindowKind public let secondary: RateWindow? public let opus: RateWindow? public let extraRateWindows: [NamedRateWindow] public let providerCost: ProviderCostSnapshot? public let updatedAt: Date public let accountEmail: String? public let accountOrganization: String? public let loginMethod: String? public let rawText: String? public init( primary: RateWindow, primaryWindowKind: PrimaryWindowKind = .usage, secondary: RateWindow?, opus: RateWindow?, extraRateWindows: [NamedRateWindow] = [], providerCost: ProviderCostSnapshot? = nil, updatedAt: Date, accountEmail: String?, accountOrganization: String?, loginMethod: String?, rawText: String?) { self.primary = primary self.primaryWindowKind = primaryWindowKind self.secondary = secondary self.opus = opus self.extraRateWindows = extraRateWindows self.providerCost = providerCost self.updatedAt = updatedAt self.accountEmail = accountEmail self.accountOrganization = accountOrganization self.loginMethod = loginMethod self.rawText = rawText } } public enum ClaudeUsageError: LocalizedError, Sendable { case claudeNotInstalled case parseFailed(String) case oauthFailed(String) public var errorDescription: String? { switch self { case .claudeNotInstalled: "Claude CLI is not installed. Install it from https://code.claude.com/docs/en/overview." case let .parseFailed(details): "Could not parse Claude usage: \(details)" case let .oauthFailed(details): details } } } public struct ClaudeUsageFetcher: ClaudeUsageFetching, Sendable { private static let sessionWindowMinutes = 5 * 60 private static let weeklyWindowMinutes = 7 * 24 * 60 private static let cliAutoProbeTimeout: TimeInterval = 12 private static let cliProbeTimeout: TimeInterval = 24 private static let cliRetryProbeTimeout: TimeInterval = 60 private struct Configuration { let environment: [String: String] let runtime: ProviderRuntime let dataSource: ClaudeUsageDataSource let oauthKeychainPromptCooldownEnabled: Bool let allowBackgroundDelegatedRefresh: Bool let allowStartupBootstrapPrompt: Bool let useWebExtras: Bool let manualCookieHeader: String? let webOrganizationID: String? let keepCLISessionsAlive: Bool let browserDetection: BrowserDetection } private let configuration: Configuration private static let log = CodexBarLog.logger(LogCategories.claudeUsage) private static var isClaudeOAuthFlowDebugEnabled: Bool { ProcessInfo.processInfo.environment["CODEXBAR_DEBUG_CLAUDE_OAUTH_FLOW"] == "1" } private var environment: [String: String] { self.configuration.environment } private var runtime: ProviderRuntime { self.configuration.runtime } private var dataSource: ClaudeUsageDataSource { self.configuration.dataSource } private var oauthKeychainPromptCooldownEnabled: Bool { self.configuration.oauthKeychainPromptCooldownEnabled } private var allowBackgroundDelegatedRefresh: Bool { self.configuration.allowBackgroundDelegatedRefresh } private var allowStartupBootstrapPrompt: Bool { self.configuration.allowStartupBootstrapPrompt } private var useWebExtras: Bool { self.configuration.useWebExtras } private var manualCookieHeader: String? { self.configuration.manualCookieHeader } private var webOrganizationID: String? { self.configuration.webOrganizationID } private var keepCLISessionsAlive: Bool { self.configuration.keepCLISessionsAlive } private var browserDetection: BrowserDetection { self.configuration.browserDetection } private struct ClaudeOAuthKeychainPromptPolicy { let mode: ClaudeOAuthKeychainPromptMode let isApplicable: Bool let interaction: ProviderInteraction var canPromptNow: Bool { switch self.mode { case .never: false case .onlyOnUserAction: self.interaction == .userInitiated case .always: true } } /// Respect the Keychain prompt cooldown for background operations to avoid spamming system dialogs. /// User actions (menu open / refresh / settings) are allowed to bypass the cooldown. var shouldRespectKeychainPromptCooldown: Bool { self.interaction != .userInitiated } var interactionLabel: String { self.interaction == .userInitiated ? "user" : "background" } } private static func currentClaudeOAuthInteractivePromptPolicy() -> ClaudeOAuthKeychainPromptPolicy { let policy = ClaudeOAuthKeychainPromptPolicy( mode: ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode(), isApplicable: true, interaction: ProviderInteractionContext.current) // User actions should be able to immediately retry a Security.framework fallback repair after a background // cooldown was recorded, even when /usr/bin/security is the primary reader. if policy.interaction == .userInitiated { if ClaudeOAuthKeychainAccessGate.clearDenied() { Self.log.info("Claude OAuth keychain cooldown cleared by user action") } } return policy } private static func currentClaudeOAuthDelegatedRefreshPolicy() -> ClaudeOAuthKeychainPromptPolicy { ClaudeOAuthKeychainPromptPolicy( mode: ClaudeOAuthKeychainPromptPreference.current(), isApplicable: ClaudeOAuthKeychainPromptPreference.isApplicable(), interaction: ProviderInteractionContext.current) } private static func assertDelegatedRefreshAllowedInCurrentInteraction( policy: ClaudeOAuthKeychainPromptPolicy, allowBackgroundDelegatedRefresh: Bool) throws { guard policy.isApplicable else { return } if policy.mode == .never { throw ClaudeUsageError.oauthFailed("Delegated refresh is disabled by 'never' keychain policy.") } if policy.mode == .onlyOnUserAction, policy.interaction != .userInitiated, !allowBackgroundDelegatedRefresh { throw ClaudeUsageError.oauthFailed( "Claude OAuth token expired, but background repair is suppressed when Keychain prompt policy " + "is set to only prompt on user action. Open the CodexBar menu or click Refresh to retry.") } } #if DEBUG @TaskLocal static var loadOAuthCredentialsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? @TaskLocal static var fetchOAuthUsageOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? @TaskLocal static var delegatedRefreshAttemptOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? @TaskLocal static var hasCachedCredentialsOverride: Bool? #endif /// Creates a new ClaudeUsageFetcher. /// - Parameters: /// - environment: Process environment (default: current process environment) /// - dataSource: Usage data source (default: OAuth API). /// - useWebExtras: If true, attempts to enrich usage with Claude web data (cookies). public init( browserDetection: BrowserDetection, environment: [String: String] = ProcessInfo.processInfo.environment, runtime: ProviderRuntime = .app, dataSource: ClaudeUsageDataSource = .oauth, oauthKeychainPromptCooldownEnabled: Bool = false, allowBackgroundDelegatedRefresh: Bool = false, allowStartupBootstrapPrompt: Bool = false, useWebExtras: Bool = false, manualCookieHeader: String? = nil, webOrganizationID: String? = nil, keepCLISessionsAlive: Bool = false) { self.configuration = Configuration( environment: environment, runtime: runtime, dataSource: dataSource, oauthKeychainPromptCooldownEnabled: oauthKeychainPromptCooldownEnabled, allowBackgroundDelegatedRefresh: allowBackgroundDelegatedRefresh, allowStartupBootstrapPrompt: allowStartupBootstrapPrompt, useWebExtras: useWebExtras, manualCookieHeader: manualCookieHeader, webOrganizationID: webOrganizationID, keepCLISessionsAlive: keepCLISessionsAlive, browserDetection: browserDetection) } private struct OAuthExecutor { let fetcher: ClaudeUsageFetcher func load(allowDelegatedRetry: Bool) async throws -> ClaudeUsageSnapshot { do { let promptPolicy = ClaudeUsageFetcher.currentClaudeOAuthInteractivePromptPolicy() #if DEBUG let hasCache = if let hasCachedCredentialsOverride = ClaudeUsageFetcher.hasCachedCredentialsOverride { hasCachedCredentialsOverride } else if ClaudeUsageFetcher.loadOAuthCredentialsOverride != nil { false } else { ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: self.fetcher.environment) } #else let hasCache = ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: self.fetcher.environment) #endif let startupBootstrapOverride = self.shouldAllowStartupBootstrapPrompt( policy: promptPolicy, hasCache: hasCache) let allowKeychainPrompt = (promptPolicy.canPromptNow || startupBootstrapOverride) && !hasCache ClaudeUsageFetcher.logOAuthBootstrapPromptDecision( allowKeychainPrompt: allowKeychainPrompt, policy: promptPolicy, hasCache: hasCache, startupBootstrapOverride: startupBootstrapOverride) let credentials = try await ClaudeOAuthCredentialsStore.$allowBackgroundPromptBootstrap .withValue(startupBootstrapOverride) { try await ClaudeUsageFetcher.loadOAuthCredentials( environment: self.fetcher.environment, allowKeychainPrompt: allowKeychainPrompt, respectKeychainPromptCooldown: promptPolicy.shouldRespectKeychainPromptCooldown) } try self.validateRequiredOAuthScope(credentials) let usage = try await ClaudeUsageFetcher.fetchOAuthUsage(accessToken: credentials.accessToken) return try ClaudeUsageFetcher.mapOAuthUsage(usage, credentials: credentials) } catch let error as CancellationError { throw error } catch let error as ClaudeUsageError { throw error } catch let error as ClaudeOAuthCredentialsError { if case .refreshDelegatedToClaudeCLI = error { return try await self.loadAfterDelegatedRefresh(allowDelegatedRetry: allowDelegatedRetry) } throw ClaudeUsageError.oauthFailed(error.localizedDescription) } catch let error as ClaudeOAuthFetchError { if case .rateLimited = error { throw ClaudeUsageError.oauthFailed(error.localizedDescription) } ClaudeOAuthCredentialsStore.invalidateCache() if case let .serverError(statusCode, body) = error, statusCode == 403, body?.contains("user:profile") ?? false { throw ClaudeUsageError.oauthFailed( "Claude OAuth token does not meet scope requirement 'user:profile'. " + "Run `claude setup-token` to re-generate credentials, or switch Claude Source to " + "Web/CLI.") } throw ClaudeUsageError.oauthFailed(error.localizedDescription) } catch { throw ClaudeUsageError.oauthFailed(error.localizedDescription) } } private func shouldAllowStartupBootstrapPrompt( policy: ClaudeOAuthKeychainPromptPolicy, hasCache: Bool) -> Bool { guard self.fetcher.allowStartupBootstrapPrompt else { return false } guard !hasCache else { return false } guard ClaudeOAuthKeychainPromptPreference.securityFrameworkFallbackMode() == .onlyOnUserAction else { return false } guard policy.interaction == .background else { return false } return ProviderRefreshContext.current == .startup } private func loadAfterDelegatedRefresh(allowDelegatedRetry: Bool) async throws -> ClaudeUsageSnapshot { guard allowDelegatedRetry else { throw ClaudeUsageError.oauthFailed( "Claude OAuth token expired and delegated Claude CLI refresh did not recover. " + "Run `claude login`, then retry.") } try Task.checkCancellation() let delegatedPromptPolicy = ClaudeUsageFetcher.currentClaudeOAuthDelegatedRefreshPolicy() try ClaudeUsageFetcher.assertDelegatedRefreshAllowedInCurrentInteraction( policy: delegatedPromptPolicy, allowBackgroundDelegatedRefresh: self.fetcher.allowBackgroundDelegatedRefresh) let delegatedOutcome = await ClaudeUsageFetcher.attemptDelegatedRefresh( environment: self.fetcher.environment) ClaudeUsageFetcher.log.info( "Claude OAuth delegated refresh attempted", metadata: [ "outcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), ]) do { if self.fetcher.oauthKeychainPromptCooldownEnabled { switch delegatedOutcome { case .skippedByCooldown, .cliUnavailable: throw ClaudeUsageError.oauthFailed( "Claude OAuth token expired; delegated refresh is unavailable (outcome=" + "\(ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome))).") case .attemptedSucceeded, .attemptedFailed: break } } try Task.checkCancellation() _ = ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged() let didSyncSilently = delegatedOutcome == .attemptedSucceeded && ClaudeOAuthCredentialsStore.syncFromClaudeKeychainWithoutPrompt(now: Date()) let promptPolicy = ClaudeUsageFetcher.currentClaudeOAuthInteractivePromptPolicy() ClaudeUsageFetcher.logDeferredBackgroundDelegatedRecoveryIfNeeded( delegatedOutcome: delegatedOutcome, didSyncSilently: didSyncSilently, policy: promptPolicy) let retryAllowKeychainPrompt = promptPolicy.canPromptNow && !didSyncSilently if retryAllowKeychainPrompt { ClaudeUsageFetcher.log.info( "Claude OAuth keychain prompt allowed (post-delegation retry)", metadata: [ "interaction": promptPolicy.interactionLabel, "promptMode": promptPolicy.mode.rawValue, "promptPolicyApplicable": "\(promptPolicy.isApplicable)", "delegatedOutcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), "didSyncSilently": "\(didSyncSilently)", ]) } if ClaudeUsageFetcher.isClaudeOAuthFlowDebugEnabled { ClaudeUsageFetcher.log.debug( "Claude OAuth credential load (post-delegation retry start)", metadata: [ "cooldownEnabled": "\(self.fetcher.oauthKeychainPromptCooldownEnabled)", "didSyncSilently": "\(didSyncSilently)", "allowKeychainPrompt": "\(retryAllowKeychainPrompt)", "delegatedOutcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), "interaction": promptPolicy.interactionLabel, "promptMode": promptPolicy.mode.rawValue, "promptPolicyApplicable": "\(promptPolicy.isApplicable)", ]) } let refreshedCredentials = try await ClaudeUsageFetcher.loadOAuthCredentials( environment: self.fetcher.environment, allowKeychainPrompt: retryAllowKeychainPrompt, respectKeychainPromptCooldown: promptPolicy.shouldRespectKeychainPromptCooldown) if ClaudeUsageFetcher.isClaudeOAuthFlowDebugEnabled { ClaudeUsageFetcher.log.debug( "Claude OAuth credential load (post-delegation retry)", metadata: [ "cooldownEnabled": "\(self.fetcher.oauthKeychainPromptCooldownEnabled)", "didSyncSilently": "\(didSyncSilently)", "allowKeychainPrompt": "\(retryAllowKeychainPrompt)", "delegatedOutcome": ClaudeUsageFetcher.delegatedRefreshOutcomeLabel(delegatedOutcome), "interaction": promptPolicy.interactionLabel, "promptMode": promptPolicy.mode.rawValue, "promptPolicyApplicable": "\(promptPolicy.isApplicable)", ]) } try self.validateRequiredOAuthScope(refreshedCredentials) let usage = try await ClaudeUsageFetcher.fetchOAuthUsage( accessToken: refreshedCredentials.accessToken) return try ClaudeUsageFetcher.mapOAuthUsage(usage, credentials: refreshedCredentials) } catch { ClaudeUsageFetcher.log.debug( "Claude OAuth post-delegation retry failed", metadata: ClaudeUsageFetcher.delegatedRetryFailureMetadata( error: error, oauthKeychainPromptCooldownEnabled: self.fetcher.oauthKeychainPromptCooldownEnabled, delegatedOutcome: delegatedOutcome)) throw ClaudeUsageError.oauthFailed( ClaudeUsageFetcher.delegatedRefreshFailureMessage( for: delegatedOutcome, retryError: error)) } } private func validateRequiredOAuthScope(_ credentials: ClaudeOAuthCredentials) throws { guard credentials.scopes.contains("user:profile") else { let scopes = credentials.scopes.joined(separator: ", ") let detail = scopes.isEmpty ? "Claude OAuth token missing 'user:profile' scope." : "Claude OAuth token missing 'user:profile' scope (has: \(scopes))." throw ClaudeUsageError.oauthFailed( detail + " Run `claude setup-token` to re-generate credentials, or switch Claude Source to " + "Web/CLI.") } } } private struct StepExecutor { let fetcher: ClaudeUsageFetcher func loadLatestUsage(model: String) async throws -> ClaudeUsageSnapshot { switch self.fetcher.dataSource { case .auto: return try await self.executeAuto(model: model) case .api: throw ClaudeUsageError.parseFailed("Claude Admin API usage is handled by the provider descriptor.") case .oauth: var snapshot = try await self.fetcher.loadViaOAuth(allowDelegatedRetry: true) snapshot = await self.fetcher.applyWebExtrasIfNeeded(to: snapshot) return snapshot case .web: return try await self.fetcher.loadViaWebAPI() case .cli: return try await self.loadViaCLIWithRetry(model: model) } } private func executeAuto(model: String) async throws -> ClaudeUsageSnapshot { let plan = await self.makeAutoFetchPlan() self.logAutoPlan(plan) let executionSteps = plan.executionSteps for (index, step) in executionSteps.enumerated() { do { return try await self.execute(step: step, model: model) } catch { if index < executionSteps.count - 1 { ClaudeUsageFetcher.log.debug( "Claude planner step failed; falling back to next step", metadata: [ "step": step.dataSource.rawValue, "reason": step.inclusionReason.rawValue, "errorType": String(describing: type(of: error)), ]) continue } throw error } } throw ClaudeUsageError.parseFailed("Claude planner produced no executable steps.") }