import CodexBarCore import Foundation import SweetCookieKit @MainActor extension UsageStore { func debugClaudeDump() async -> String { await ClaudeStatusProbe.latestDumps() } } extension UsageStore { struct ClaudeDebugLogConfiguration { let runtime: CodexBarCore.ProviderRuntime let sourceMode: ProviderSourceMode let environment: [String: String] let webExtrasEnabled: Bool let usageDataSource: ClaudeUsageDataSource let cookieSource: ProviderCookieSource let cookieHeader: String let keepCLISessionsAlive: Bool } static func debugClaudeLog( browserDetection: BrowserDetection, configuration: ClaudeDebugLogConfiguration) async -> String { struct OAuthDebugProbe: Sendable { let hasCredentials: Bool let ownerRawValue: String let sourceRawValue: String let isExpired: Bool } return await runWithTimeout(seconds: 15) { var lines: [String] = [] let manualHeader = configuration.cookieSource == .manual ? CookieHeaderNormalizer.normalize(configuration.cookieHeader) : nil let hasKey = if configuration.cookieSource == .off { false } else if let manualHeader { ClaudeWebAPIFetcher.hasSessionKey(cookieHeader: manualHeader) } else { ClaudeWebAPIFetcher.hasSessionKey(browserDetection: browserDetection) { msg in lines.append(msg) } } let oauthProbe = await withTaskGroup(of: OAuthDebugProbe.self) { group in // Preserve task-local test overrides while keeping the keychain read off the calling task. group.addTask(priority: .utility) { let oauthRecord = try? ClaudeOAuthCredentialsStore.loadRecord( environment: configuration.environment, allowKeychainPrompt: false, respectKeychainPromptCooldown: true, allowClaudeKeychainRepairWithoutPrompt: false) return OAuthDebugProbe( hasCredentials: oauthRecord?.credentials.scopes.contains("user:profile") == true, ownerRawValue: oauthRecord?.owner.rawValue ?? "none", sourceRawValue: oauthRecord?.source.rawValue ?? "none", isExpired: oauthRecord?.credentials.isExpired ?? false) } return await group.next() ?? OAuthDebugProbe( hasCredentials: false, ownerRawValue: "none", sourceRawValue: "none", isExpired: false) } let hasOAuthCredentials = ClaudeOAuthPlanningAvailability.isAvailable( runtime: configuration.runtime, sourceMode: configuration.sourceMode, environment: configuration.environment) let hasClaudeBinary = ClaudeCLIResolver.isAvailable(environment: configuration.environment) let delegatedCooldownSeconds = ClaudeOAuthDelegatedRefreshCoordinator.cooldownRemainingSeconds() let planningInput = ClaudeSourcePlanningInput( runtime: configuration.runtime, selectedDataSource: configuration.usageDataSource, webExtrasEnabled: configuration.webExtrasEnabled, hasWebSession: hasKey, hasCLI: hasClaudeBinary, hasOAuthCredentials: hasOAuthCredentials) let plan = ClaudeSourcePlanner.resolve(input: planningInput) let strategy = plan.compatibilityStrategy lines.append(contentsOf: plan.debugLines()) lines.append("hasSessionKey=\(hasKey)") lines.append("hasOAuthCredentials=\(hasOAuthCredentials)") lines.append("oauthCredentialOwner=\(oauthProbe.ownerRawValue)") lines.append("oauthCredentialSource=\(oauthProbe.sourceRawValue)") lines.append("oauthCredentialExpired=\(oauthProbe.isExpired)") lines.append("delegatedRefreshCLIAvailable=\(hasClaudeBinary)") lines.append("delegatedRefreshCooldownActive=\(delegatedCooldownSeconds != nil)") if let delegatedCooldownSeconds { lines.append("delegatedRefreshCooldownSeconds=\(delegatedCooldownSeconds)") } lines.append("hasClaudeBinary=\(hasClaudeBinary)") if strategy?.useWebExtras == true { lines.append("web_extras=enabled") } lines.append("") guard let strategy else { lines.append("No planner-selected Claude source.") return lines.joined(separator: "\n") } switch strategy.dataSource { case .auto: lines.append("Auto source selected.") return lines.joined(separator: "\n") case .api: let hasAdminKey = ProviderTokenResolver.claudeAdminAPIToken( environment: configuration.environment) != nil lines.append("Admin API source selected.") lines.append("hasAdminAPIKey=\(hasAdminKey)") return lines.joined(separator: "\n") case .web: do { let web: ClaudeWebAPIFetcher.WebUsageData = if let manualHeader { try await ClaudeWebAPIFetcher.fetchUsage(cookieHeader: manualHeader) { msg in lines.append(msg) } } else { try await ClaudeWebAPIFetcher.fetchUsage(browserDetection: browserDetection) { msg in lines.append(msg) } } lines.append("") lines.append("Web API summary:") let sessionReset = web.sessionResetsAt?.description ?? "nil" lines.append("session_used=\(web.sessionPercentUsed)% resetsAt=\(sessionReset)") if let weekly = web.weeklyPercentUsed { let weeklyReset = web.weeklyResetsAt?.description ?? "nil" lines.append("weekly_used=\(weekly)% resetsAt=\(weeklyReset)") } else { lines.append("weekly_used=nil") } lines.append("opus_used=\(web.opusPercentUsed?.description ?? "nil")") if let extra = web.extraUsageCost { let resetsAt = extra.resetsAt?.description ?? "nil" let period = extra.period ?? "nil" let line = "extra_usage used=\(extra.used) limit=\(extra.limit) " + "currency=\(extra.currencyCode) period=\(period) resetsAt=\(resetsAt)" lines.append(line) } else { lines.append("extra_usage=nil") } return lines.joined(separator: "\n") } catch { lines.append("Web API failed: \(error.localizedDescription)") return lines.joined(separator: "\n") } case .cli: let fetcher = ClaudeUsageFetcher( browserDetection: browserDetection, environment: configuration.environment, runtime: configuration.runtime, dataSource: configuration.usageDataSource, keepCLISessionsAlive: configuration.keepCLISessionsAlive) let cli = await fetcher.debugRawProbe(model: "sonnet") lines.append(cli) return lines.joined(separator: "\n") case .oauth: lines.append("OAuth source selected.") return lines.joined(separator: "\n") } } } }