Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion Sources/CodexBarCLI/CLICostCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?

Expand All @@ -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?
Expand All @@ -495,6 +508,7 @@ struct CostPayload: Encodable, Sendable {
sessionTokens: Int?,
sessionCostUSD: Double?,
historyDays: Int?,
historyCoverageIsEstablished: Bool? = nil,
last30DaysTokens: Int?,
last30DaysCostUSD: Double?,
meteredCostUSD: Double? = nil,
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBarCLI/CLIHelp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@ extension CodexBarCLI {
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>] [-v|--verbose]
[--provider \(ProviderHelp.list)]
[--no-color] [--pretty] [--refresh] [--days <days>] [--group-by project]
[--no-color] [--pretty] [--refresh] [--provider-native-only]
[--days <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
Expand Down Expand Up @@ -386,6 +388,7 @@ extension CodexBarCLI {
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>] [-v|--verbose]
[--provider \(ProviderHelp.list)] [--no-color] [--pretty] [--refresh]
[--provider-native-only]
[--days <days>] [--group-by project]
codexbar sessions [--json] [--pretty]
codexbar sessions focus <id>
Expand Down
29 changes: 21 additions & 8 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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()
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -391,6 +398,7 @@ public struct CostUsageFetcher: Sendable {
now: now,
historyDays: clampedHistoryDays,
calendar: scanOptions.calendar,
historyCoverageIsEstablished: scanResult.historyCoverageIsEstablished,
projects: scanResult.projects,
sessions: scanResult.sessions)
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -789,6 +801,7 @@ public struct CostUsageFetcher: Sendable {
last30DaysTokens: last30DaysTokens,
last30DaysCostUSD: last30DaysCostUSD,
historyDays: historyDays,
historyCoverageIsEstablished: historyCoverageIsEstablished,
historyLabel: historyLabel,
meteredCostUSD: meteredCostUSD,
credentialScopeFingerprint: credentialScopeFingerprint,
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [:]
Expand Down
88 changes: 65 additions & 23 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -1022,31 +1044,35 @@ 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,
.ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana,
.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)
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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<String> = []
var files: [URL] = []
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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(
Expand Down
Loading