import CodexBarCoreimport Commanderimport Foundation extension CodexBarCLI { private static let costSupportedProviders: Set<UsageProvider> = { #if os(macOS) [.claude, .codex, .cursor] #else // Cursor cost relies on the macOS-only dashboard fetch path; `supportsTokenSnapshot(.cursor)` // is false elsewhere, so don't advertise Cursor cost where it can only fail. [.claude, .codex] #endif }() static func runCost(_ values: ParsedValues) async { let output = CLIOutputPreferences.from(values: values) let config = CodexBarCLI.loadConfig(output: output) let selection = CodexBarCLI.decodeProvider(from: values, config: config) let providers = Self.costProviders(from: selection) let unsupported = selection.asList.filter { !Self.costSupportedProviders.contains($0) } if !unsupported.isEmpty { let names = unsupported .map { ProviderDescriptorRegistry.descriptor(for: $0).metadata.displayName } .sorted() .joined(separator: ", ") if !output.jsonOnly { Self.writeStderr("Skipping providers without local cost usage: \(names)\n") } } guard !providers.isEmpty else { Self.exit( code: .failure, message: "Error: cost is only supported for \(Self.costSupportedProviderNames()).", output: output, kind: .args) } let format = output.format let forceRefresh = values.flags.contains("refresh") let useColor = Self.shouldUseColor(noColor: values.flags.contains("noColor"), format: format) let historyDays = Self.decodeCostHistoryDays(from: values) // Cursor cost reuses the same cookie-source policy as usage fetches: reject the fetch when the // user set Cursor cookies to Off, and forward the Manual header so the dashboard request uses // the configured session instead of auto-resolving a different one. let cursorCookieSettings: ProviderSettingsSnapshot.CursorProviderSettings? let cursorCookieSettingsError: Error? do { cursorCookieSettings = try Self.cursorCookieSettings(config: config, providers: providers) cursorCookieSettingsError = nil } catch { cursorCookieSettings = nil cursorCookieSettingsError = error } let groupBy = Self.decodeCostGroupBy(from: values) if groupBy == .project { let unsupportedProjectProviders = providers.filter { $0 != .codex } if !unsupportedProjectProviders.isEmpty, !output.jsonOnly { let names = unsupportedProjectProviders .map { ProviderDescriptorRegistry.descriptor(for: $0).metadata.displayName } .sorted() .joined(separator: ", ") Self.writeStderr("Skipping project grouping for providers without Codex project data: \(names)\n") } } let fetcher = CostUsageFetcher() var sections: [String] = [] var payload: [CostPayload] = [] var exitCode: ExitCode = .success for provider in providers where groupBy != .project || provider == .codex || format == .json { if let error = Self.cursorCostAvailabilityError( provider, settings: cursorCookieSettings, resolutionError: cursorCookieSettingsError) { exitCode = Self.mapError(error) if format == .json { payload.append(Self.makeCostPayload(provider: provider, snapshot: nil, error: error)) } else if !output.jsonOnly { Self.writeStderr("Error: \(error.localizedDescription)\n") } continue } do { // Claude/Codex cost comes from local logs; Cursor cost is fetched from its // cookie-authenticated dashboard API via the shared session resolution. let snapshot = try await fetcher.loadTokenSnapshot( provider: provider, forceRefresh: forceRefresh, historyDays: historyDays, cursorCookieHeaderOverride: Self.cursorCostHeaderOverride(provider, settings: cursorCookieSettings), refreshPricingInBackground: false) switch format { case .text: sections.append(Self.renderCostText( provider: provider, snapshot: snapshot, groupBy: groupBy, useColor: useColor)) case .json: payload.append(Self.makeCostPayload(provider: provider, snapshot: snapshot, error: nil)) } } catch { exitCode = Self.mapError(error) if format == .json { payload.append(Self.makeCostPayload(provider: provider, snapshot: nil, error: error)) } else if !output.jsonOnly { Self.writeStderr("Error: \(error.localizedDescription)\n") } } } switch format { case .text: if !sections.isEmpty { print(sections.joined(separator: "\n\n")) } case .json: if !payload.isEmpty { Self.printJSON(payload, pretty: output.pretty) } } Self.exit(code: exitCode, output: output, kind: exitCode == .success ? .runtime : .provider) } enum CostGroupBy: String { case none case project } static func renderCostText( provider: UsageProvider, snapshot: CostUsageTokenSnapshot, groupBy: CostGroupBy = .none, useColor: Bool) -> String { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName let title = provider == .codex ? "\(name) API-equivalent estimate (not billed)" : "\(name) Cost (API-rate estimate)" let header = Self.costHeaderLine(title, useColor: useColor) if groupBy == .project, provider == .codex { return Self.renderProjectCostText(header: header, snapshot: snapshot) } let todayCost = snapshot.sessionCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let todayTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) } let todayLine = todayTokens.map { "Today: \(todayCost) · \($0) tokens" } ?? "Today: \(todayCost)" let monthCost = snapshot.last30DaysCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let monthTokens = snapshot.last30DaysTokens.map { UsageFormatter.tokenCountString($0) } let historyLabel = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "Today" : "Last \(snapshot.historyDays) days") let monthLine = monthTokens.map { "\(historyLabel): \(monthCost) · \($0) tokens" } ?? "\(historyLabel): \(monthCost)" // Plan-metered spend over the same window (what Cursor actually deducts), shown // alongside the API-rate estimate. Only providers like Cursor report it. let meteredLine: String? = snapshot.meteredCostUSD.map { let amount = UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) return "Cursor-metered: \(amount) (\(historyLabel.lowercased()))" } let hintLine = Self.costEstimateHint(provider: provider) return [header, todayLine, monthLine, meteredLine, hintLine] .compactMap(\.self) .joined(separator: "\n") } private static func renderProjectCostText(header: String, snapshot: CostUsageTokenSnapshot) -> String { let historyLabel = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "Today" : "Last \(snapshot.historyDays) days") var lines = [header, "Projects (\(historyLabel)):"] guard !snapshot.projects.isEmpty else { lines.append("—") lines.append(Self.costEstimateHint(provider: .codex)) return lines.joined(separator: "\n") } for project in snapshot.projects { let cost = project.totalCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let tokens = project.totalTokens.map { UsageFormatter.tokenCountString($0) } let summary = tokens.map { "\(cost) · \($0) tokens" } ?? cost lines.append("\(project.name): \(summary)") if let path = project.path { lines.append(" \(path)") } for source in project.sources { let sourceCost = source.totalCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let sourceTokens = source.totalTokens.map { UsageFormatter.tokenCountString($0) } let sourceSummary = sourceTokens.map { "\(sourceCost) · \($0) tokens" } ?? sourceCost lines.append(" - \(source.name): \(sourceSummary)") if let path = source.path { lines.append(" \(path)") } } } lines.append(Self.costEstimateHint(provider: .codex)) return lines.joined(separator: "\n") } private static func costEstimateHint(provider: UsageProvider) -> String { provider == .codex ? "Not a subscription bill or plan value · local usage × public API prices" : UsageFormatter.costEstimateHint(provider: provider) } private static func costHeaderLine(_ header: String, useColor: Bool) -> String { guard useColor else { return header } return "\u{001B}[1;36m\(header)\u{001B}[0m" } static func costProviders(from selection: ProviderSelection) -> [UsageProvider] { selection.asList.filter { Self.costSupportedProviders.contains($0) } } static func makeCostPayload( provider: UsageProvider, snapshot: CostUsageTokenSnapshot?, error: Error?) -> CostPayload { let daily = snapshot?.daily.map(Self.costDailyPayload(from:)) ?? [] let projects = provider == .codex ? snapshot?.projects.map { project in CostProjectPayload( name: project.name, path: project.path, totalTokens: project.totalTokens, totalCostUSD: project.totalCostUSD, daily: project.daily.map(Self.costDailyPayload(from:)), modelBreakdowns: project.modelBreakdowns?.map(Self.costModelBreakdownPayload(from:)), sources: project.sources.map { source in CostProjectSourcePayload( name: source.name, path: source.path, totalTokens: source.totalTokens, totalCostUSD: source.totalCostUSD, daily: source.daily.map(Self.costDailyPayload(from:)), modelBreakdowns: source.modelBreakdowns?.map(Self.costModelBreakdownPayload(from:))) }) } ?? [] : [] return CostPayload( provider: provider.rawValue, source: provider == .cursor ? "web" : "local", updatedAt: snapshot?.updatedAt ?? (error == nil ? nil : Date()), currencyCode: snapshot?.currencyCode, sessionTokens: snapshot?.sessionTokens, sessionCostUSD: snapshot?.sessionCostUSD, historyDays: snapshot?.historyDays, last30DaysTokens: snapshot?.last30DaysTokens, last30DaysCostUSD: snapshot?.last30DaysCostUSD, meteredCostUSD: snapshot?.meteredCostUSD, daily: daily, projects: projects, totals: snapshot.flatMap(Self.costTotals(from:)), error: error.map { Self.makeErrorPayload($0) }) } private static func costDailyPayload(from entry: CostUsageDailyReport.Entry) -> CostDailyEntryPayload { CostDailyEntryPayload( date: entry.date, inputTokens: entry.inputTokens, outputTokens: entry.outputTokens, cacheReadTokens: entry.cacheReadTokens, cacheCreationTokens: entry.cacheCreationTokens, totalTokens: entry.totalTokens, costUSD: entry.costUSD, modelsUsed: entry.modelsUsed, modelBreakdowns: entry.modelBreakdowns?.map(self.costModelBreakdownPayload(from:))) } private static func costModelBreakdownPayload( from breakdown: CostUsageDailyReport.ModelBreakdown) -> CostModelBreakdownPayload { CostModelBreakdownPayload( modelName: breakdown.modelName, costUSD: breakdown.costUSD, totalTokens: breakdown.totalTokens) } private static func costTotals(from snapshot: CostUsageTokenSnapshot) -> CostTotalsPayload? { let entries = snapshot.daily guard !entries.isEmpty else { guard snapshot.last30DaysTokens != nil || snapshot.last30DaysCostUSD != nil else { return nil } return CostTotalsPayload( totalInputTokens: nil, totalOutputTokens: nil, cacheReadTokens: nil, cacheCreationTokens: nil, totalTokens: snapshot.last30DaysTokens, totalCostUSD: snapshot.last30DaysCostUSD) } var totalInput = 0 var totalOutput = 0 var totalCacheRead = 0 var totalCacheCreation = 0 var totalTokens = 0 var totalCost = 0.0 var sawInput = false var sawOutput = false var sawCacheRead = false var sawCacheCreation = false var sawTokens = false var sawCost = false for entry in entries { if let input = entry.inputTokens { totalInput += input sawInput = true } if let output = entry.outputTokens { totalOutput += output sawOutput = true } if let cacheRead = entry.cacheReadTokens { totalCacheRead += cacheRead sawCacheRead = true } if let cacheCreation = entry.cacheCreationTokens { totalCacheCreation += cacheCreation sawCacheCreation = true } if let tokens = entry.totalTokens { totalTokens += tokens sawTokens = true } if let cost = entry.costUSD { totalCost += cost sawCost = true } } // Prefer totals derived from daily rows; fall back to snapshot aggregates when rows omit fields. return CostTotalsPayload( totalInputTokens: sawInput ? totalInput : nil, totalOutputTokens: sawOutput ? totalOutput : nil, cacheReadTokens: sawCacheRead ? totalCacheRead : nil, cacheCreationTokens: sawCacheCreation ? totalCacheCreation : nil, totalTokens: sawTokens ? totalTokens : snapshot.last30DaysTokens, totalCostUSD: sawCost ? totalCost : snapshot.last30DaysCostUSD) } private static func decodeCostHistoryDays(from values: ParsedValues) -> Int { guard let raw = values.options["days"]?.last, let parsed = Int(raw) else { return 30 } return max(1, min(365, parsed)) } private static func decodeCostGroupBy(from values: ParsedValues) -> CostGroupBy { guard let raw = values.options["groupBy"]?.last?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return .none } return CostGroupBy(rawValue: raw.lowercased()) ?? .none } /// Human-readable list of providers that support a cost report, used by both `cost` and serve. static func costSupportedProviderNames() -> String { self.costSupportedProviders .map { ProviderDescriptorRegistry.descriptor(for: $0).metadata.displayName } .sorted() .joined(separator: ", ") } /// Resolve the configured Cursor cookie settings (source + manual header) the same way the CLI /// usage path does, so Cursor cost honors Off/Manual instead of always auto-resolving a session. /// Shared by `cost` and the serve `/cost` route. static func cursorCookieSettings( config: CodexBarConfig, providers: [UsageProvider]) throws -> ProviderSettingsSnapshot.CursorProviderSettings? { guard providers.contains(.cursor) else { return nil } let selection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false) let context = try TokenAccountCLIContext(selection: selection, config: config, verbose: false) let account = try context.resolvedAccounts(for: .cursor).first return context.settingsSnapshot(for: .cursor, account: account)?.cursor } /// Return the actionable error for a Cursor cost fetch disabled by cookie-source policy. static func cursorCostAvailabilityError( _ provider: UsageProvider, settings: ProviderSettingsSnapshot.CursorProviderSettings?, resolutionError: Error? = nil) -> Error? { guard provider == .cursor else { return nil } if let resolutionError { return resolutionError } guard let settings else { return nil } switch settings.cookieSource { case .off: return CursorCostAvailabilityError.cookieSourceOff case .manual where CookieHeaderNormalizer.normalize(settings.manualCookieHeader) == nil: return CursorCostAvailabilityError.manualCookieMissing default: return nil } } /// Manual cookie header to forward for a Cursor cost fetch, or nil for auto/non-cursor sources. static func cursorCostHeaderOverride( _ provider: UsageProvider, settings: ProviderSettingsSnapshot.CursorProviderSettings?) -> String? { guard provider == .cursor, settings?.cookieSource == .manual else { return nil } return CookieHeaderNormalizer.normalize(settings?.manualCookieHeader) }} enum CursorCostAvailabilityError: LocalizedError { case cookieSourceOff case manualCookieMissing var errorDescription: String? { switch self { case .cookieSourceOff: "Cursor cost is unavailable because the Cursor cookie source is set to Off." case .manualCookieMissing: "Cursor cost requires a non-empty Manual cookie header." } }} struct CostOptions: CommanderParsable { @Flag(names: [.short("v"), .long("verbose")], help: "Enable verbose logging") var verbose: Bool = false @Flag(name: .long("json-output"), help: "Emit machine-readable logs") var jsonOutput: Bool = false @Option(name: .long("log-level"), help: "Set log level (trace|verbose|debug|info|warning|error|critical)") var logLevel: String? @Option( name: .long("provider"), help: ProviderHelp.optionHelp) var provider: ProviderSelection? @Option(name: .long("format"), help: "Output format: text | json") var format: OutputFormat? @Flag(name: .long("json"), help: "") var jsonShortcut: Bool = false @Flag(name: .long("json-only"), help: "Emit JSON only (suppress non-JSON output)") var jsonOnly: Bool = false @Flag(name: .long("pretty"), help: "Pretty-print JSON output") var pretty: Bool = false @Flag(name: .long("no-color"), help: "Disable ANSI colors in text output") var noColor: Bool = false @Flag(name: .long("refresh"), help: "Force refresh by ignoring cached scans") var refresh: Bool = false @Option(name: .long("days"), help: "Cost history window in days (1...365)") var days: Int? @Option(name: .long("group-by"), help: "Group text output by: project") var groupBy: String?} struct CostPayload: Encodable, Sendable { let provider: String let source: String let updatedAt: Date? let currencyCode: String? let sessionTokens: Int? let sessionCostUSD: Double? let historyDays: Int? let last30DaysTokens: Int? let last30DaysCostUSD: Double? let meteredCostUSD: Double? let daily: [CostDailyEntryPayload] let projects: [CostProjectPayload] let totals: CostTotalsPayload? let error: ProviderErrorPayload? init( provider: String, source: String, updatedAt: Date?, currencyCode: String? = nil, sessionTokens: Int?, sessionCostUSD: Double?, historyDays: Int?, last30DaysTokens: Int?, last30DaysCostUSD: Double?, meteredCostUSD: Double? = nil,Lines 1–500 of 633. Load more
import CodexBarCore import Commander import Foundation extension CodexBarCLI { private static let costSupportedProviders: Set<UsageProvider> = { #if os(macOS) [.claude, .codex, .cursor] #else // Cursor cost relies on the macOS-only dashboard fetch path; `supportsTokenSnapshot(.cursor)` // is false elsewhere, so don't advertise Cursor cost where it can only fail. [.claude, .codex] #endif }() static func runCost(_ values: ParsedValues) async { let output = CLIOutputPreferences.from(values: values) let config = CodexBarCLI.loadConfig(output: output) let selection = CodexBarCLI.decodeProvider(from: values, config: config) let providers = Self.costProviders(from: selection) let unsupported = selection.asList.filter { !Self.costSupportedProviders.contains($0) } if !unsupported.isEmpty { let names = unsupported .map { ProviderDescriptorRegistry.descriptor(for: $0).metadata.displayName } .sorted() .joined(separator: ", ") if !output.jsonOnly { Self.writeStderr("Skipping providers without local cost usage: \(names)\n") } } guard !providers.isEmpty else { Self.exit( code: .failure, message: "Error: cost is only supported for \(Self.costSupportedProviderNames()).", output: output, kind: .args) } let format = output.format let forceRefresh = values.flags.contains("refresh") let useColor = Self.shouldUseColor(noColor: values.flags.contains("noColor"), format: format) let historyDays = Self.decodeCostHistoryDays(from: values) // Cursor cost reuses the same cookie-source policy as usage fetches: reject the fetch when the // user set Cursor cookies to Off, and forward the Manual header so the dashboard request uses // the configured session instead of auto-resolving a different one. let cursorCookieSettings: ProviderSettingsSnapshot.CursorProviderSettings? let cursorCookieSettingsError: Error? do { cursorCookieSettings = try Self.cursorCookieSettings(config: config, providers: providers) cursorCookieSettingsError = nil } catch { cursorCookieSettings = nil cursorCookieSettingsError = error } let groupBy = Self.decodeCostGroupBy(from: values) if groupBy == .project { let unsupportedProjectProviders = providers.filter { $0 != .codex } if !unsupportedProjectProviders.isEmpty, !output.jsonOnly { let names = unsupportedProjectProviders .map { ProviderDescriptorRegistry.descriptor(for: $0).metadata.displayName } .sorted() .joined(separator: ", ") Self.writeStderr("Skipping project grouping for providers without Codex project data: \(names)\n") } } let fetcher = CostUsageFetcher() var sections: [String] = [] var payload: [CostPayload] = [] var exitCode: ExitCode = .success for provider in providers where groupBy != .project || provider == .codex || format == .json { if let error = Self.cursorCostAvailabilityError( provider, settings: cursorCookieSettings, resolutionError: cursorCookieSettingsError) { exitCode = Self.mapError(error) if format == .json { payload.append(Self.makeCostPayload(provider: provider, snapshot: nil, error: error)) } else if !output.jsonOnly { Self.writeStderr("Error: \(error.localizedDescription)\n") } continue } do { // Claude/Codex cost comes from local logs; Cursor cost is fetched from its // cookie-authenticated dashboard API via the shared session resolution. let snapshot = try await fetcher.loadTokenSnapshot( provider: provider, forceRefresh: forceRefresh, historyDays: historyDays, cursorCookieHeaderOverride: Self.cursorCostHeaderOverride(provider, settings: cursorCookieSettings), refreshPricingInBackground: false) switch format { case .text: sections.append(Self.renderCostText( provider: provider, snapshot: snapshot, groupBy: groupBy, useColor: useColor)) case .json: payload.append(Self.makeCostPayload(provider: provider, snapshot: snapshot, error: nil)) } } catch { exitCode = Self.mapError(error) if format == .json { payload.append(Self.makeCostPayload(provider: provider, snapshot: nil, error: error)) } else if !output.jsonOnly { Self.writeStderr("Error: \(error.localizedDescription)\n") } } } switch format { case .text: if !sections.isEmpty { print(sections.joined(separator: "\n\n")) } case .json: if !payload.isEmpty { Self.printJSON(payload, pretty: output.pretty) } } Self.exit(code: exitCode, output: output, kind: exitCode == .success ? .runtime : .provider) } enum CostGroupBy: String { case none case project } static func renderCostText( provider: UsageProvider, snapshot: CostUsageTokenSnapshot, groupBy: CostGroupBy = .none, useColor: Bool) -> String { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName let title = provider == .codex ? "\(name) API-equivalent estimate (not billed)" : "\(name) Cost (API-rate estimate)" let header = Self.costHeaderLine(title, useColor: useColor) if groupBy == .project, provider == .codex { return Self.renderProjectCostText(header: header, snapshot: snapshot) } let todayCost = snapshot.sessionCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let todayTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) } let todayLine = todayTokens.map { "Today: \(todayCost) · \($0) tokens" } ?? "Today: \(todayCost)" let monthCost = snapshot.last30DaysCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let monthTokens = snapshot.last30DaysTokens.map { UsageFormatter.tokenCountString($0) } let historyLabel = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "Today" : "Last \(snapshot.historyDays) days") let monthLine = monthTokens.map { "\(historyLabel): \(monthCost) · \($0) tokens" } ?? "\(historyLabel): \(monthCost)" // Plan-metered spend over the same window (what Cursor actually deducts), shown // alongside the API-rate estimate. Only providers like Cursor report it. let meteredLine: String? = snapshot.meteredCostUSD.map { let amount = UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) return "Cursor-metered: \(amount) (\(historyLabel.lowercased()))" } let hintLine = Self.costEstimateHint(provider: provider) return [header, todayLine, monthLine, meteredLine, hintLine] .compactMap(\.self) .joined(separator: "\n") } private static func renderProjectCostText(header: String, snapshot: CostUsageTokenSnapshot) -> String { let historyLabel = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "Today" : "Last \(snapshot.historyDays) days") var lines = [header, "Projects (\(historyLabel)):"] guard !snapshot.projects.isEmpty else { lines.append("—") lines.append(Self.costEstimateHint(provider: .codex)) return lines.joined(separator: "\n") } for project in snapshot.projects { let cost = project.totalCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let tokens = project.totalTokens.map { UsageFormatter.tokenCountString($0) } let summary = tokens.map { "\(cost) · \($0) tokens" } ?? cost lines.append("\(project.name): \(summary)") if let path = project.path { lines.append(" \(path)") } for source in project.sources { let sourceCost = source.totalCostUSD .map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? "—" let sourceTokens = source.totalTokens.map { UsageFormatter.tokenCountString($0) } let sourceSummary = sourceTokens.map { "\(sourceCost) · \($0) tokens" } ?? sourceCost lines.append(" - \(source.name): \(sourceSummary)") if let path = source.path { lines.append(" \(path)") } } } lines.append(Self.costEstimateHint(provider: .codex)) return lines.joined(separator: "\n") } private static func costEstimateHint(provider: UsageProvider) -> String { provider == .codex ? "Not a subscription bill or plan value · local usage × public API prices" : UsageFormatter.costEstimateHint(provider: provider) } private static func costHeaderLine(_ header: String, useColor: Bool) -> String { guard useColor else { return header } return "\u{001B}[1;36m\(header)\u{001B}[0m" } static func costProviders(from selection: ProviderSelection) -> [UsageProvider] { selection.asList.filter { Self.costSupportedProviders.contains($0) } } static func makeCostPayload( provider: UsageProvider, snapshot: CostUsageTokenSnapshot?, error: Error?) -> CostPayload { let daily = snapshot?.daily.map(Self.costDailyPayload(from:)) ?? [] let projects = provider == .codex ? snapshot?.projects.map { project in CostProjectPayload( name: project.name, path: project.path, totalTokens: project.totalTokens, totalCostUSD: project.totalCostUSD, daily: project.daily.map(Self.costDailyPayload(from:)), modelBreakdowns: project.modelBreakdowns?.map(Self.costModelBreakdownPayload(from:)), sources: project.sources.map { source in CostProjectSourcePayload( name: source.name, path: source.path, totalTokens: source.totalTokens, totalCostUSD: source.totalCostUSD, daily: source.daily.map(Self.costDailyPayload(from:)), modelBreakdowns: source.modelBreakdowns?.map(Self.costModelBreakdownPayload(from:))) }) } ?? [] : [] return CostPayload( provider: provider.rawValue, source: provider == .cursor ? "web" : "local", updatedAt: snapshot?.updatedAt ?? (error == nil ? nil : Date()), currencyCode: snapshot?.currencyCode, sessionTokens: snapshot?.sessionTokens, sessionCostUSD: snapshot?.sessionCostUSD, historyDays: snapshot?.historyDays, last30DaysTokens: snapshot?.last30DaysTokens, last30DaysCostUSD: snapshot?.last30DaysCostUSD, meteredCostUSD: snapshot?.meteredCostUSD, daily: daily, projects: projects, totals: snapshot.flatMap(Self.costTotals(from:)), error: error.map { Self.makeErrorPayload($0) }) } private static func costDailyPayload(from entry: CostUsageDailyReport.Entry) -> CostDailyEntryPayload { CostDailyEntryPayload( date: entry.date, inputTokens: entry.inputTokens, outputTokens: entry.outputTokens, cacheReadTokens: entry.cacheReadTokens, cacheCreationTokens: entry.cacheCreationTokens, totalTokens: entry.totalTokens, costUSD: entry.costUSD, modelsUsed: entry.modelsUsed, modelBreakdowns: entry.modelBreakdowns?.map(self.costModelBreakdownPayload(from:))) } private static func costModelBreakdownPayload( from breakdown: CostUsageDailyReport.ModelBreakdown) -> CostModelBreakdownPayload { CostModelBreakdownPayload( modelName: breakdown.modelName, costUSD: breakdown.costUSD, totalTokens: breakdown.totalTokens) } private static func costTotals(from snapshot: CostUsageTokenSnapshot) -> CostTotalsPayload? { let entries = snapshot.daily guard !entries.isEmpty else { guard snapshot.last30DaysTokens != nil || snapshot.last30DaysCostUSD != nil else { return nil } return CostTotalsPayload( totalInputTokens: nil, totalOutputTokens: nil, cacheReadTokens: nil, cacheCreationTokens: nil, totalTokens: snapshot.last30DaysTokens, totalCostUSD: snapshot.last30DaysCostUSD) } var totalInput = 0 var totalOutput = 0 var totalCacheRead = 0 var totalCacheCreation = 0 var totalTokens = 0 var totalCost = 0.0 var sawInput = false var sawOutput = false var sawCacheRead = false var sawCacheCreation = false var sawTokens = false var sawCost = false for entry in entries { if let input = entry.inputTokens { totalInput += input sawInput = true } if let output = entry.outputTokens { totalOutput += output sawOutput = true } if let cacheRead = entry.cacheReadTokens { totalCacheRead += cacheRead sawCacheRead = true } if let cacheCreation = entry.cacheCreationTokens { totalCacheCreation += cacheCreation sawCacheCreation = true } if let tokens = entry.totalTokens { totalTokens += tokens sawTokens = true } if let cost = entry.costUSD { totalCost += cost sawCost = true } } // Prefer totals derived from daily rows; fall back to snapshot aggregates when rows omit fields. return CostTotalsPayload( totalInputTokens: sawInput ? totalInput : nil, totalOutputTokens: sawOutput ? totalOutput : nil, cacheReadTokens: sawCacheRead ? totalCacheRead : nil, cacheCreationTokens: sawCacheCreation ? totalCacheCreation : nil, totalTokens: sawTokens ? totalTokens : snapshot.last30DaysTokens, totalCostUSD: sawCost ? totalCost : snapshot.last30DaysCostUSD) } private static func decodeCostHistoryDays(from values: ParsedValues) -> Int { guard let raw = values.options["days"]?.last, let parsed = Int(raw) else { return 30 } return max(1, min(365, parsed)) } private static func decodeCostGroupBy(from values: ParsedValues) -> CostGroupBy { guard let raw = values.options["groupBy"]?.last?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return .none } return CostGroupBy(rawValue: raw.lowercased()) ?? .none } /// Human-readable list of providers that support a cost report, used by both `cost` and serve. static func costSupportedProviderNames() -> String { self.costSupportedProviders .map { ProviderDescriptorRegistry.descriptor(for: $0).metadata.displayName } .sorted() .joined(separator: ", ") } /// Resolve the configured Cursor cookie settings (source + manual header) the same way the CLI /// usage path does, so Cursor cost honors Off/Manual instead of always auto-resolving a session. /// Shared by `cost` and the serve `/cost` route. static func cursorCookieSettings( config: CodexBarConfig, providers: [UsageProvider]) throws -> ProviderSettingsSnapshot.CursorProviderSettings? { guard providers.contains(.cursor) else { return nil } let selection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false) let context = try TokenAccountCLIContext(selection: selection, config: config, verbose: false) let account = try context.resolvedAccounts(for: .cursor).first return context.settingsSnapshot(for: .cursor, account: account)?.cursor } /// Return the actionable error for a Cursor cost fetch disabled by cookie-source policy. static func cursorCostAvailabilityError( _ provider: UsageProvider, settings: ProviderSettingsSnapshot.CursorProviderSettings?, resolutionError: Error? = nil) -> Error? { guard provider == .cursor else { return nil } if let resolutionError { return resolutionError } guard let settings else { return nil } switch settings.cookieSource { case .off: return CursorCostAvailabilityError.cookieSourceOff case .manual where CookieHeaderNormalizer.normalize(settings.manualCookieHeader) == nil: return CursorCostAvailabilityError.manualCookieMissing default: return nil } } /// Manual cookie header to forward for a Cursor cost fetch, or nil for auto/non-cursor sources. static func cursorCostHeaderOverride( _ provider: UsageProvider, settings: ProviderSettingsSnapshot.CursorProviderSettings?) -> String? { guard provider == .cursor, settings?.cookieSource == .manual else { return nil } return CookieHeaderNormalizer.normalize(settings?.manualCookieHeader) } } enum CursorCostAvailabilityError: LocalizedError { case cookieSourceOff case manualCookieMissing var errorDescription: String? { switch self { case .cookieSourceOff: "Cursor cost is unavailable because the Cursor cookie source is set to Off." case .manualCookieMissing: "Cursor cost requires a non-empty Manual cookie header." } } } struct CostOptions: CommanderParsable { @Flag(names: [.short("v"), .long("verbose")], help: "Enable verbose logging") var verbose: Bool = false @Flag(name: .long("json-output"), help: "Emit machine-readable logs") var jsonOutput: Bool = false @Option(name: .long("log-level"), help: "Set log level (trace|verbose|debug|info|warning|error|critical)") var logLevel: String? @Option( name: .long("provider"), help: ProviderHelp.optionHelp) var provider: ProviderSelection? @Option(name: .long("format"), help: "Output format: text | json") var format: OutputFormat? @Flag(name: .long("json"), help: "") var jsonShortcut: Bool = false @Flag(name: .long("json-only"), help: "Emit JSON only (suppress non-JSON output)") var jsonOnly: Bool = false @Flag(name: .long("pretty"), help: "Pretty-print JSON output") var pretty: Bool = false @Flag(name: .long("no-color"), help: "Disable ANSI colors in text output") var noColor: Bool = false @Flag(name: .long("refresh"), help: "Force refresh by ignoring cached scans") var refresh: Bool = false @Option(name: .long("days"), help: "Cost history window in days (1...365)") var days: Int? @Option(name: .long("group-by"), help: "Group text output by: project") var groupBy: String? } struct CostPayload: Encodable, Sendable { let provider: String let source: String let updatedAt: Date? let currencyCode: String? let sessionTokens: Int? let sessionCostUSD: Double? let historyDays: Int? let last30DaysTokens: Int? let last30DaysCostUSD: Double? let meteredCostUSD: Double? let daily: [CostDailyEntryPayload] let projects: [CostProjectPayload] let totals: CostTotalsPayload? let error: ProviderErrorPayload? init( provider: String, source: String, updatedAt: Date?, currencyCode: String? = nil, sessionTokens: Int?, sessionCostUSD: Double?, historyDays: Int?, last30DaysTokens: Int?, last30DaysCostUSD: Double?, meteredCostUSD: Double? = nil,