diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 75a020a0c1..f78c7afacb 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -420,7 +420,7 @@ jobs: update-homebrew-tap: runs-on: ubuntu-24.04 needs: build-cli - if: github.event_name == 'release' + if: github.event_name == 'release' && github.repository == 'steipete/CodexBar' steps: - name: Resolve release tag id: release diff --git a/Sources/CodexBarCLI/CLICostCommand.swift b/Sources/CodexBarCLI/CLICostCommand.swift index 0497db6ea7..fa62966bba 100644 --- a/Sources/CodexBarCLI/CLICostCommand.swift +++ b/Sources/CodexBarCLI/CLICostCommand.swift @@ -38,6 +38,7 @@ extension CodexBarCLI { let format = output.format let forceRefresh = values.flags.contains("refresh") + let includePiSessions = Self.decodeCostIncludePiSessions(from: values) 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 @@ -91,7 +92,8 @@ extension CodexBarCLI { forceRefresh: forceRefresh, historyDays: historyDays, cursorCookieHeaderOverride: Self.cursorCostHeaderOverride(provider, settings: cursorCookieSettings), - refreshPricingInBackground: false) + refreshPricingInBackground: false, + includePiSessions: includePiSessions) switch format { case .text: sections.append(Self.renderCostText( @@ -256,6 +258,7 @@ extension CodexBarCLI { sessionTokens: snapshot?.sessionTokens, sessionCostUSD: snapshot?.sessionCostUSD, historyDays: snapshot?.historyDays, + historyCoverageIsEstablished: snapshot?.historyCoverageIsEstablished, last30DaysTokens: snapshot?.last30DaysTokens, last30DaysCostUSD: snapshot?.last30DaysCostUSD, meteredCostUSD: snapshot?.meteredCostUSD, @@ -357,6 +360,10 @@ extension CodexBarCLI { return max(1, min(365, parsed)) } + static func decodeCostIncludePiSessions(from values: ParsedValues) -> Bool { + !values.flags.contains("providerNativeOnly") + } + private static func decodeCostGroupBy(from values: ParsedValues) -> CostGroupBy { guard let raw = values.options["groupBy"]?.last?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty @@ -464,6 +471,11 @@ struct CostOptions: CommanderParsable { @Flag(name: .long("refresh"), help: "Force refresh by ignoring cached scans") var refresh: Bool = false + @Flag( + name: .long("provider-native-only"), + help: "Exclude pi and OMP session mirrors from Claude/Codex cost history") + var providerNativeOnly: Bool = false + @Option(name: .long("days"), help: "Cost history window in days (1...365)") var days: Int? @@ -479,6 +491,7 @@ struct CostPayload: Encodable, Sendable { let sessionTokens: Int? let sessionCostUSD: Double? let historyDays: Int? + let historyCoverageIsEstablished: Bool? let last30DaysTokens: Int? let last30DaysCostUSD: Double? let meteredCostUSD: Double? @@ -495,6 +508,7 @@ struct CostPayload: Encodable, Sendable { sessionTokens: Int?, sessionCostUSD: Double?, historyDays: Int?, + historyCoverageIsEstablished: Bool? = nil, last30DaysTokens: Int?, last30DaysCostUSD: Double?, meteredCostUSD: Double? = nil, @@ -510,6 +524,7 @@ struct CostPayload: Encodable, Sendable { self.sessionTokens = sessionTokens self.sessionCostUSD = sessionCostUSD self.historyDays = historyDays + self.historyCoverageIsEstablished = historyCoverageIsEstablished self.last30DaysTokens = last30DaysTokens self.last30DaysCostUSD = last30DaysCostUSD self.meteredCostUSD = meteredCostUSD diff --git a/Sources/CodexBarCLI/CLIHelp.swift b/Sources/CodexBarCLI/CLIHelp.swift index 3eb27e8bca..18c14fa488 100644 --- a/Sources/CodexBarCLI/CLIHelp.swift +++ b/Sources/CodexBarCLI/CLIHelp.swift @@ -99,11 +99,13 @@ extension CodexBarCLI { [--json-only] [--json-output] [--log-level ] [-v|--verbose] [--provider \(ProviderHelp.list)] - [--no-color] [--pretty] [--refresh] [--days ] [--group-by project] + [--no-color] [--pretty] [--refresh] [--provider-native-only] + [--days ] [--group-by project] Description: Print local token cost usage from Claude/Codex native logs plus supported pi and OMP sessions. This does not require web or CLI access and uses cached scan results unless --refresh is provided. + Use --provider-native-only to exclude pi and OMP session mirrors. Examples: codexbar cost @@ -386,6 +388,7 @@ extension CodexBarCLI { [--json-only] [--json-output] [--log-level ] [-v|--verbose] [--provider \(ProviderHelp.list)] [--no-color] [--pretty] [--refresh] + [--provider-native-only] [--days ] [--group-by project] codexbar sessions [--json] [--pretty] codexbar sessions focus diff --git a/Sources/CodexBarCore/CostUsageFetcher.swift b/Sources/CodexBarCore/CostUsageFetcher.swift index 226d061364..bacdb79a95 100644 --- a/Sources/CodexBarCore/CostUsageFetcher.swift +++ b/Sources/CodexBarCore/CostUsageFetcher.swift @@ -210,6 +210,7 @@ public struct CostUsageFetcher: Sendable { return options } + // swiftlint:disable:next function_body_length static func loadTokenSnapshot( provider: UsageProvider, environment: [String: String] = ProcessInfo.processInfo.environment, @@ -288,13 +289,14 @@ public struct CostUsageFetcher: Sendable { // scanner-level checks. let scanOptions = options let scanResult = try await CostUsageScanExecutor.run { checkCancellation in - var daily = try CostUsageScanner.loadDailyReportCancellable( + var dailyResult = try CostUsageScanner.loadDailyReportResultCancellable( provider: provider, since: since, until: now, now: now, options: scanOptions, checkCancellation: checkCancellation) + var daily = dailyResult.report try checkCancellation() if provider == .vertexai, @@ -304,13 +306,14 @@ public struct CostUsageFetcher: Sendable { { var fallback = scanOptions fallback.claudeLogProviderFilter = .all - daily = try CostUsageScanner.loadDailyReportCancellable( + dailyResult = try CostUsageScanner.loadDailyReportResultCancellable( provider: provider, since: since, until: now, now: now, options: fallback, checkCancellation: checkCancellation) + daily = dailyResult.report try checkCancellation() } @@ -355,7 +358,11 @@ public struct CostUsageFetcher: Sendable { sessions = [] } } - return (daily: daily, projects: projects, sessions: sessions) + return ( + daily: daily, + projects: projects, + sessions: sessions, + historyCoverageIsEstablished: dailyResult.historyCoverageIsEstablished) } if allowPricingRefresh, @@ -391,6 +398,7 @@ public struct CostUsageFetcher: Sendable { now: now, historyDays: clampedHistoryDays, calendar: scanOptions.calendar, + historyCoverageIsEstablished: scanResult.historyCoverageIsEstablished, projects: scanResult.projects, sessions: scanResult.sessions) } @@ -535,11 +543,12 @@ public struct CostUsageFetcher: Sendable { var scanTimes: [Date] = [] var piMerged = false - if cache.timeZoneIdentifier == range.calendar.timeZone.identifier, - !cache.days.isEmpty, - cache.roots == CostUsageScanner.codexRootsFingerprint(options: options), - !CostUsageScanner.requestedWindowExpandsCache(range: range, cache: cache) - { + let nativeCacheIsValid = + cache.timeZoneIdentifier == range.calendar.timeZone.identifier + && !cache.days.isEmpty + && cache.roots == CostUsageScanner.codexRootsFingerprint(options: options) + && !CostUsageScanner.requestedWindowExpandsCache(range: range, cache: cache) + if nativeCacheIsValid { let daily = CostUsageScanner.buildCodexReportFromCache( cache: cache, range: range, @@ -597,6 +606,8 @@ public struct CostUsageFetcher: Sendable { now: now, historyDays: clampedHistoryDays, calendar: options.calendar, + historyCoverageIsEstablished: nativeCacheIsValid + && cache.codexHistoryCoverageIsEstablished == true, projects: Self.mergedProjectBreakdowns(projects), sessions: sessions, updatedAt: scanTimes.min()), @@ -750,6 +761,7 @@ public struct CostUsageFetcher: Sendable { historyDays: Int = 30, useCurrentLocalDayForSession: Bool = true, calendar: Calendar = .current, + historyCoverageIsEstablished: Bool = true, meteredCostUSD: Double? = nil, credentialScopeFingerprint: String? = nil, historyLabel: String? = nil, @@ -789,6 +801,7 @@ public struct CostUsageFetcher: Sendable { last30DaysTokens: last30DaysTokens, last30DaysCostUSD: last30DaysCostUSD, historyDays: historyDays, + historyCoverageIsEstablished: historyCoverageIsEstablished, historyLabel: historyLabel, meteredCostUSD: meteredCostUSD, credentialScopeFingerprint: credentialScopeFingerprint, diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index ca353e24bb..c037666fdd 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "3aa49b47f4b78e13" + static let value = "2172c319c3d32054" } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift index e13e0bcd3b..e4e7f36be0 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift @@ -123,6 +123,7 @@ struct CostUsageCache: Codable { var codexProjectMetadataVersion: Int? var codexPriorityTurnKeys: [String: String]? var codexPriorityTurnIDsByDay: [String: [String]]? + var codexHistoryCoverageIsEstablished: Bool? /// filePath -> file usage var files: [String: CostUsageFileUsage] = [:] diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index fe2677734a..2301a1f4cc 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -962,6 +962,11 @@ enum CostUsageScanner { let parsedBytes: Int64 } + struct DailyReportLoadResult { + let report: CostUsageDailyReport + let historyCoverageIsEstablished: Bool + } + enum ClaudePathRole: String, Codable { case parent case subagent @@ -1009,6 +1014,23 @@ enum CostUsageScanner { now: Date = Date(), options: Options = Options(), checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport + { + try self.loadDailyReportResultCancellable( + provider: provider, + since: since, + until: until, + now: now, + options: options, + checkCancellation: checkCancellation).report + } + + static func loadDailyReportResultCancellable( + provider: UsageProvider, + since: Date, + until: Date, + now: Date = Date(), + options: Options = Options(), + checkCancellation: CancellationCheck?) throws -> DailyReportLoadResult { let range = CostUsageDayRange(since: since, until: until, calendar: options.calendar) let emptyReport = CostUsageDailyReport(data: [], summary: nil) @@ -1022,23 +1044,27 @@ enum CostUsageScanner { options: options, checkCancellation: checkCancellation) case .claude: - return try self.loadClaudeDaily( - provider: .claude, - range: range, - now: now, - options: options, - checkCancellation: checkCancellation) + return try DailyReportLoadResult( + report: self.loadClaudeDaily( + provider: .claude, + range: range, + now: now, + options: options, + checkCancellation: checkCancellation), + historyCoverageIsEstablished: true) case .vertexai: var filtered = options if filtered.claudeLogProviderFilter == .all { filtered.claudeLogProviderFilter = .vertexAIOnly } - return try self.loadClaudeDaily( - provider: .vertexai, - range: range, - now: now, - options: filtered, - checkCancellation: checkCancellation) + return try DailyReportLoadResult( + report: self.loadClaudeDaily( + provider: .vertexai, + range: range, + now: now, + options: filtered, + checkCancellation: checkCancellation), + historyCoverageIsEstablished: true) case .openai, .azureopenai, .clinepass, .zai, .gemini, .antigravity, .cursor, .opencode, .opencodego, .alibaba, .alibabatokenplan, .qwencloud, .factory, .copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .moonshot, .augment, .jetbrains, .amp, @@ -1046,7 +1072,7 @@ enum CostUsageScanner { .abacus, .mistral, .deepseek, .deepinfra, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .neuralwatt, .clawrouter, .longcat, .sub2api, .wayfinder, .zenmux, .aiand, .zoommate, .xai: - return emptyReport + return DailyReportLoadResult(report: emptyReport, historyCoverageIsEstablished: true) } } @@ -3390,7 +3416,7 @@ enum CostUsageScanner { range: CostUsageDayRange, now: Date, options: Options, - checkCancellation: CancellationCheck?) throws -> CostUsageDailyReport + checkCancellation: CancellationCheck?) throws -> DailyReportLoadResult { var cache = Self.loadCodexCache(options: options, range: range) let nowMs = Int64(now.timeIntervalSince1970 * 1000) @@ -3404,7 +3430,9 @@ enum CostUsageScanner { let cachedSinceKey = cache.scanSinceKey let cachedUntilKey = cache.scanUntilKey - let shouldRunColdCacheLookback = cache.files.isEmpty || plan.rootsChanged + let shouldRunColdCacheLookback = cache.files.isEmpty + || plan.rootsChanged + || cache.codexHistoryCoverageIsEstablished != true let coldCacheLookbackStart = Self.localStartOfDay(range.scanSinceKey, calendar: options.calendar) var seenPaths: Set = [] var files: [URL] = [] @@ -3531,8 +3559,19 @@ enum CostUsageScanner { } } - let shouldRetainWiderWindow = !options.forceRescan && !plan.pricingChanged && !plan - .priorityMetadataChanged && !plan.needsTurnIDCacheMigration && !plan.needsProjectMetadataMigration + let historyCoverageIsEstablished = + scanBudget.resumedPartialFileCount == 0 + && scanBudget.deferredByBudgetFileCount == 0 + && !filePathsInScan.contains { + cache.files[$0]?.codexScanComplete == false + } + let shouldRetainWiderWindow = !options.forceRescan + && !plan.pricingChanged + && !plan.priorityMetadataChanged + && !plan.needsTurnIDCacheMigration + && !plan.needsProjectMetadataMigration + && cache.codexHistoryCoverageIsEstablished == true + && historyCoverageIsEstablished let retainedSinceKey = shouldRetainWiderWindow ? [cachedSinceKey, range.scanSinceKey].compactMap(\.self).min() ?? range.scanSinceKey : range.scanSinceKey @@ -3546,6 +3585,7 @@ enum CostUsageScanner { cache.codexPricingKey = plan.codexPricingKey cache.codexPriorityMetadataKey = plan.codexPriorityMetadataKey cache.codexProjectMetadataVersion = Self.codexProjectMetadataVersion + cache.codexHistoryCoverageIsEstablished = historyCoverageIsEstablished if plan.hasPriorityMetadata { cache.codexPriorityTurnKeys = Self.mergePriorityTurnKeys( existing: shouldRetainWiderWindow ? cache.codexPriorityTurnKeys : nil, @@ -3565,12 +3605,14 @@ enum CostUsageScanner { Self.saveCodexCache(cache, options: options, range: range) } - return Self.buildCodexReportFromCache( - cache: cache, - range: range, - modelsDevCatalog: plan.modelsDevCatalog, - modelsDevCacheRoot: options.cacheRoot, - priorityTurns: plan.priorityTurns) + return DailyReportLoadResult( + report: Self.buildCodexReportFromCache( + cache: cache, + range: range, + modelsDevCatalog: plan.modelsDevCatalog, + modelsDevCacheRoot: options.cacheRoot, + priorityTurns: plan.priorityTurns), + historyCoverageIsEstablished: cache.codexHistoryCoverageIsEstablished == true) } private static func codexFileScanContext( diff --git a/Tests/CodexBarTests/CLICostTests.swift b/Tests/CodexBarTests/CLICostTests.swift index 6e0bfc7e87..ac5461405e 100644 --- a/Tests/CodexBarTests/CLICostTests.swift +++ b/Tests/CodexBarTests/CLICostTests.swift @@ -16,6 +16,17 @@ struct CLICostTests { #expect(CodexBarCLI._decodeFormatForTesting(from: parsed) == .json) } + @Test + func `provider native only excludes pi and OMP session mirrors`() throws { + let parser = CommandParser(signature: CodexBarCLI._costSignatureForTesting()) + + let defaultValues = try parser.parse(arguments: []) + #expect(CodexBarCLI.decodeCostIncludePiSessions(from: defaultValues)) + + let nativeOnlyValues = try parser.parse(arguments: ["--provider-native-only"]) + #expect(!CodexBarCLI.decodeCostIncludePiSessions(from: nativeOnlyValues)) + } + @Test func `renders cost text snapshot`() { let snap = CostUsageTokenSnapshot( @@ -164,6 +175,7 @@ struct CLICostTests { sessionCostUSD: 0.01, last30DaysTokens: 40, last30DaysCostUSD: 0.04, + historyCoverageIsEstablished: false, daily: [ CostUsageDailyReport.Entry( date: "2026-04-02", @@ -232,6 +244,7 @@ struct CLICostTests { } #expect(json.contains("\"projects\"")) + #expect(json.contains("\"historyCoverageIsEstablished\":false")) #expect(json.contains("\"sources\"")) #expect(json.contains("\"name\":\"client-a\"")) #expect(json.contains("/work/client-a") || json.contains("\\/work\\/client-a")) diff --git a/Tests/CodexBarTests/CostUsageFetcherCacheSnapshotTests.swift b/Tests/CodexBarTests/CostUsageFetcherCacheSnapshotTests.swift index bcf9d25777..c18e26b4ed 100644 --- a/Tests/CodexBarTests/CostUsageFetcherCacheSnapshotTests.swift +++ b/Tests/CodexBarTests/CostUsageFetcherCacheSnapshotTests.swift @@ -384,6 +384,7 @@ struct CostUsageFetcherCacheSnapshotTests { #expect(cached?.sessionTokens == 165) #expect(cached?.last30DaysTokens == 165) + #expect(cached?.historyCoverageIsEstablished == false) } @Test @@ -426,6 +427,7 @@ struct CostUsageFetcherCacheSnapshotTests { #expect(cached?.sessionTokens == 165) #expect(cached?.last30DaysTokens == 165) + #expect(cached?.historyCoverageIsEstablished == false) } private static func writeCodexSessionFile( diff --git a/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift b/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift index c202d20157..0ba1cfdb39 100644 --- a/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift +++ b/Tests/CodexBarTests/CostUsagePerformanceGateTests.swift @@ -319,14 +319,18 @@ struct CostUsagePerformanceGateTests { options.refreshMinIntervalSeconds = 0 var offsets: [Int64] = [] + var coverage: [Bool] = [] var report: CostUsageDailyReport? for _ in 0..<12 { - report = CostUsageScanner.loadDailyReport( + let result = try CostUsageScanner.loadDailyReportResultCancellable( provider: .codex, since: day, until: day, now: day, - options: options) + options: options, + checkCancellation: nil) + report = result.report + coverage.append(result.historyCoverageIsEstablished) let cached = try #require(CostUsageCacheIO.load( provider: .codex, cacheRoot: env.cacheRoot).files.values.first) @@ -337,12 +341,126 @@ struct CostUsagePerformanceGateTests { } #expect(offsets.count > 1) + #expect(coverage.first == false) + #expect(coverage.last == true) #expect(zip(offsets, offsets.dropFirst()).allSatisfy { $0 <= $1 }) #expect(offsets.last == metadata.size) #expect(report?.summary?.totalTokens == baseline.summary?.totalTokens) #expect(report?.data.map(\.totalTokens) == baseline.data.map(\.totalTokens)) } + @Test + func `unfinished recent lookback remains incomplete until resumed`() throws { + let env = try CostUsageTestEnvironment() + defer { env.cleanup() } + let requestedDay = try env.makeLocalNoon(year: 2026, month: 5, day: 10) + let partitionDay = try env.makeLocalNoon(year: 2026, month: 4, day: 1) + let model = "openai/gpt-5.2-codex" + let fileURL = try env.writeCodexSessionFile( + day: partitionDay, + filename: "recent-lookback.jsonl", + contents: env.jsonl([ + [ + "type": "session_meta", + "timestamp": env.isoString(for: partitionDay), + "payload": ["session_id": "recent-lookback"], + ], + [ + "type": "turn_context", + "timestamp": env.isoString(for: partitionDay), + "payload": ["model": model], + ], + [ + "type": "event_msg", + "timestamp": env.isoString(for: partitionDay), + "payload": [ + "type": "token_count", + "padding": String(repeating: "x", count: 4096), + "info": [ + "last_token_usage": [ + "input_tokens": 10, + "cached_input_tokens": 0, + "output_tokens": 0, + ], + "model": model, + ], + ], + ], + [ + "type": "event_msg", + "timestamp": env.isoString(for: requestedDay), + "payload": [ + "type": "token_count", + "info": [ + "last_token_usage": [ + "input_tokens": 42, + "cached_input_tokens": 0, + "output_tokens": 0, + ], + "model": model, + ], + ], + ], + ])) + try FileManager.default.setAttributes( + [.modificationDate: requestedDay], + ofItemAtPath: fileURL.path) + let metadata = CostUsageScanner.codexFileMetadata(fileURL: fileURL) + let slice = max(1, metadata.size / 4) + var options = CostUsageScanner.Options( + codexSessionsRoot: env.codexSessionsRoot, + claudeProjectsRoots: nil, + cacheRoot: env.cacheRoot, + codexTraceDatabaseURL: env.root.appendingPathComponent("missing.sqlite"), + maxCodexSessionFileBytes: slice, + maxCodexScanBytesPerRefresh: slice) + options.refreshMinIntervalSeconds = 0 + + let first = try CostUsageScanner.loadDailyReportResultCancellable( + provider: .codex, + since: requestedDay, + until: requestedDay, + now: requestedDay, + options: options, + checkCancellation: nil) + let firstCached = try #require(CostUsageCacheIO.load( + provider: .codex, + cacheRoot: env.cacheRoot).files[fileURL.path]) + let firstOffset = try #require(firstCached.parsedBytes) + + #expect(!first.historyCoverageIsEstablished) + #expect(firstCached.codexScanComplete == false) + #expect(firstCached.days["2026-05-10"] == nil) + + let second = try CostUsageScanner.loadDailyReportResultCancellable( + provider: .codex, + since: requestedDay, + until: requestedDay, + now: requestedDay, + options: options, + checkCancellation: nil) + let secondCached = try #require(CostUsageCacheIO.load( + provider: .codex, + cacheRoot: env.cacheRoot).files[fileURL.path]) + + #expect(!second.historyCoverageIsEstablished) + #expect((secondCached.parsedBytes ?? 0) > firstOffset) + + var final = second + for _ in 0..<8 where !final.historyCoverageIsEstablished { + final = try CostUsageScanner.loadDailyReportResultCancellable( + provider: .codex, + since: requestedDay, + until: requestedDay, + now: requestedDay, + options: options, + checkCancellation: nil) + } + + #expect(final.historyCoverageIsEstablished) + #expect(final.report.summary?.totalTokens == 42) + } + @Test func `oversized codex progress survives cache round trip`() throws { let env = try CostUsageTestEnvironment() @@ -561,15 +679,18 @@ struct CostUsagePerformanceGateTests { preferNewestCodexSessionsFirst: true) options.refreshMinIntervalSeconds = 0 - _ = CostUsageScanner.loadDailyReport( + let result = try CostUsageScanner.loadDailyReportResultCancellable( provider: .codex, since: day, until: day, now: day, - options: options) + options: options, + checkCancellation: nil) let cache = CostUsageCacheIO.load(provider: .codex, cacheRoot: env.cacheRoot) let cachedNames = Set(cache.files.keys.map { URL(fileURLWithPath: $0).lastPathComponent }) + #expect(!result.historyCoverageIsEstablished) + #expect(cache.codexHistoryCoverageIsEstablished == false) #expect(cachedNames.contains(newer.lastPathComponent)) #expect(!cachedNames.contains(older.lastPathComponent)) } diff --git a/docs/cli.md b/docs/cli.md index ad834cb7b2..04127c65e3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -49,6 +49,7 @@ See `docs/configuration.md` for the schema. - Cursor is fetched from the cookie-authenticated cursor.com dashboard API (macOS only; see `docs/cursor.md`) and honors the configured cookie source: a non-empty Manual header is required and forwarded, while Off fails explicitly instead of silently omitting Cursor. - `--format text|json` (default: text). - `--refresh` ignores cached scans. + - `--provider-native-only` excludes pi and OMP session mirrors from Claude and Codex history. - `codexbar cards` prints a one-shot usage snapshot as a responsive terminal card grid. - Reuses the same provider, source, account, credits, and status flags as `codexbar usage`. - Account lines and plan badges are included in the card grid by default. @@ -161,6 +162,7 @@ payloads include the visible account label in `account`. - `provider`, `source` (`local` for Claude/Codex log scans, `web` for Cursor dashboard data), `updatedAt` - `sessionTokens`, `sessionCostUSD` - `last30DaysTokens`, `last30DaysCostUSD` +- `historyCoverageIsEstablished`: `false` while a bounded Codex scan still has deferred history - Cursor only: `meteredCostUSD` — what Cursor's plan actually deducts over the window, alongside the API-rate estimate in `last30DaysCostUSD`. - `daily[]`: `date`, `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheCreationTokens`, `totalTokens`, `totalCost`, `modelsUsed`, `modelBreakdowns[]` (`modelName`, `cost`) - Codex only: `projects[]`: `name`, `path`, `totalTokens`, `totalCost`, `daily[]`, `modelBreakdowns[]`, `sources[]`