import Foundation public struct ClaudeSourcePlanningInput: Equatable, Sendable { public let runtime: ProviderRuntime public let selectedDataSource: ClaudeUsageDataSource public let webExtrasEnabled: Bool public let hasWebSession: Bool public let hasCLI: Bool public let hasOAuthCredentials: Bool public init( runtime: ProviderRuntime, selectedDataSource: ClaudeUsageDataSource, webExtrasEnabled: Bool, hasWebSession: Bool, hasCLI: Bool, hasOAuthCredentials: Bool) { self.runtime = runtime self.selectedDataSource = selectedDataSource self.webExtrasEnabled = webExtrasEnabled self.hasWebSession = hasWebSession self.hasCLI = hasCLI self.hasOAuthCredentials = hasOAuthCredentials } } public enum ClaudeSourcePlanReason: String, Equatable, Sendable { case explicitSourceSelection = "explicit-source-selection" case appAutoPreferredOAuth = "app-auto-preferred-oauth" case appAutoFallbackCLI = "app-auto-fallback-cli" case appAutoFallbackWeb = "app-auto-fallback-web" case cliAutoPreferredWeb = "cli-auto-preferred-web" case cliAutoFallbackCLI = "cli-auto-fallback-cli" } public struct ClaudeFetchPlanStep: Equatable, Sendable { public let dataSource: ClaudeUsageDataSource public let inclusionReason: ClaudeSourcePlanReason public let isPlausiblyAvailable: Bool public init( dataSource: ClaudeUsageDataSource, inclusionReason: ClaudeSourcePlanReason, isPlausiblyAvailable: Bool) { self.dataSource = dataSource self.inclusionReason = inclusionReason self.isPlausiblyAvailable = isPlausiblyAvailable } } public struct ClaudeFetchPlan: Equatable, Sendable { public let input: ClaudeSourcePlanningInput public let orderedSteps: [ClaudeFetchPlanStep] public init(input: ClaudeSourcePlanningInput, orderedSteps: [ClaudeFetchPlanStep]) { self.input = input self.orderedSteps = orderedSteps } public var availableSteps: [ClaudeFetchPlanStep] { self.orderedSteps.filter(\.isPlausiblyAvailable) } public var isNoSourceAvailable: Bool { self.availableSteps.isEmpty } public var preferredStep: ClaudeFetchPlanStep? { switch self.input.selectedDataSource { case .auto: self.availableSteps.first case .api, .oauth, .web, .cli: self.orderedSteps.first } } public var executionSteps: [ClaudeFetchPlanStep] { switch self.input.selectedDataSource { case .auto: self.availableSteps case .api, .oauth, .web, .cli: self.orderedSteps } } public var compatibilityStrategy: ClaudeUsageStrategy? { guard let preferredStep else { return nil } let useWebExtras = self.input.runtime == .app && preferredStep.dataSource == .cli && self.input.webExtrasEnabled return ClaudeUsageStrategy( dataSource: preferredStep.dataSource, useWebExtras: useWebExtras) } public var orderLabel: String { self.orderedSteps.map(\.dataSource.sourceLabel).joined(separator: "→") } public func debugLines() -> [String] { var lines = ["planner_order=\(self.orderLabel)"] lines.append("planner_selected=\(self.preferredStep?.dataSource.rawValue ?? "none")") lines.append("planner_no_source=\(self.isNoSourceAvailable)") for step in self.orderedSteps { let availability = step.isPlausiblyAvailable ? "available" : "unavailable" lines.append( "planner_step.\(step.dataSource.rawValue)=\(availability) reason=\(step.inclusionReason.rawValue)") } return lines } } public enum ClaudeCLIResolver { #if DEBUG @TaskLocal static var resolvedBinaryPathOverrideForTesting: String? public static var currentResolvedBinaryPathOverrideForTesting: String? { self.resolvedBinaryPathOverrideForTesting } public static func withResolvedBinaryPathOverrideForTesting( _ path: String?, operation: () async throws -> T) async rethrows -> T { try await self.$resolvedBinaryPathOverrideForTesting.withValue(path) { try await operation() } } #endif public static func resolvedBinaryPath( environment: [String: String] = ProcessInfo.processInfo.environment, loginPATH: [String]? = LoginShellPathCache.shared.current) -> String? { #if DEBUG if let override = self.resolvedBinaryPathOverrideForTesting { return FileManager.default.isExecutableFile(atPath: override) ? override : nil } #endif var normalizedEnvironment = environment if let override = environment["CLAUDE_CLI_PATH"]?.trimmingCharacters(in: .whitespacesAndNewlines) { if override.isEmpty { normalizedEnvironment.removeValue(forKey: "CLAUDE_CLI_PATH") } else { normalizedEnvironment["CLAUDE_CLI_PATH"] = override if FileManager.default.isExecutableFile(atPath: override) { return override } } } return BinaryLocator.resolveClaudeBinary( env: normalizedEnvironment, loginPATH: loginPATH) } public static func isAvailable(environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool { self.resolvedBinaryPath(environment: environment) != nil } } public enum ClaudeSourcePlanner { public static func resolve(input: ClaudeSourcePlanningInput) -> ClaudeFetchPlan { ClaudeFetchPlan(input: input, orderedSteps: self.makeSteps(input: input)) } private static func makeSteps(input: ClaudeSourcePlanningInput) -> [ClaudeFetchPlanStep] { switch input.selectedDataSource { case .auto: switch input.runtime { case .app: [ self.step(.oauth, reason: .appAutoPreferredOAuth, input: input), self.step(.cli, reason: .appAutoFallbackCLI, input: input), self.step(.web, reason: .appAutoFallbackWeb, input: input), ] case .cli: [ self.step(.web, reason: .cliAutoPreferredWeb, input: input), self.step(.cli, reason: .cliAutoFallbackCLI, input: input), ] } case .api: [self.step(.api, reason: .explicitSourceSelection, input: input)] case .oauth: [self.step(.oauth, reason: .explicitSourceSelection, input: input)] case .web: [self.step(.web, reason: .explicitSourceSelection, input: input)] case .cli: [self.step(.cli, reason: .explicitSourceSelection, input: input)] } } private static func step( _ dataSource: ClaudeUsageDataSource, reason: ClaudeSourcePlanReason, input: ClaudeSourcePlanningInput) -> ClaudeFetchPlanStep { ClaudeFetchPlanStep( dataSource: dataSource, inclusionReason: reason, isPlausiblyAvailable: self.isPlausiblyAvailable(dataSource, input: input)) } private static func isPlausiblyAvailable( _ dataSource: ClaudeUsageDataSource, input: ClaudeSourcePlanningInput) -> Bool { switch dataSource { case .auto, .api: false case .oauth: input.hasOAuthCredentials case .web: input.hasWebSession case .cli: input.hasCLI } } }