Skip to content
Draft
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 Sources/CodexBarCLI/CLICardsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ extension CodexBarCLI {
resetStyle: resetStyle,
weeklyWorkDays: weeklyWorkDays,
jsonOnly: output.jsonOnly,
includeAllCodexAccounts: tokenSelection.allAccounts && providerList == [.codex],
includeAllAccounts: tokenSelection.allAccounts,
fetcher: fetcher,
claudeFetcher: claudeFetcher,
browserDetection: browserDetection,
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBarCLI/CLIDashboardCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ extension CodexBarCLI {
startedAt: startedAt,
requestTimeout: timeout),
providerOperations: providerOperations,
includeAllCodexAccounts: false,
selectedAccountOnlyProviders: Self.dashboardClaudeSwapIsEligible(config: configSnapshot.config)
? [.claude]
: [],
persistCLISessions: false),
costCollection: ServeCostCollectionContext(
configFingerprint: configSnapshot.cacheToken,
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBarCLI/CLIErrorReporting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ extension CodexBarCLI {
provider: UsageProvider,
account: String?,
cacheAccountKey: String? = nil,
accountIsActive: Bool? = nil,
accountCollectionError: String? = nil,
source: String,
status: ProviderStatusPayload?,
error: Error,
Expand All @@ -59,6 +61,8 @@ extension CodexBarCLI {
provider: provider,
account: account,
cacheAccountKey: cacheAccountKey,
accountIsActive: accountIsActive,
accountCollectionError: accountCollectionError,
version: nil,
source: source,
status: status,
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIHelp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ extension CodexBarCLI {
beyond a trusted network segment.
Snapshot identity defaults to full account emails. --identity redacted hides
email local parts and is recommended whenever responses cross a network.
/usage fetches every configured account; dashboard snapshots nest providers
with multiple accounts under providers[].accounts.

Endpoints:
GET / Built-in web dashboard
Expand Down
12 changes: 12 additions & 0 deletions Sources/CodexBarCLI/CLIPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ struct ProviderPayload: Encodable {
let provider: String
let account: String?
let cacheAccountKey: String?
/// Internal account-selection metadata for dashboard projection. This is deliberately
/// excluded from the public `/usage` payload, whose schema remains unchanged.
let accountIsActive: Bool?
let accountCollectionError: String?
let version: String?
let source: String
let status: ProviderStatusPayload?
Expand Down Expand Up @@ -38,6 +42,8 @@ struct ProviderPayload: Encodable {
provider: UsageProvider,
account: String?,
cacheAccountKey: String? = nil,
accountIsActive: Bool? = nil,
accountCollectionError: String? = nil,
version: String?,
source: String,
status: ProviderStatusPayload?,
Expand All @@ -52,6 +58,8 @@ struct ProviderPayload: Encodable {
self.provider = provider.rawValue
self.account = account
self.cacheAccountKey = cacheAccountKey
self.accountIsActive = accountIsActive
self.accountCollectionError = accountCollectionError
self.version = version
self.source = source
self.status = status
Expand All @@ -68,6 +76,8 @@ struct ProviderPayload: Encodable {
providerID: String,
account: String?,
cacheAccountKey: String? = nil,
accountIsActive: Bool? = nil,
accountCollectionError: String? = nil,
version: String?,
source: String,
status: ProviderStatusPayload?,
Expand All @@ -82,6 +92,8 @@ struct ProviderPayload: Encodable {
self.provider = providerID
self.account = account
self.cacheAccountKey = cacheAccountKey
self.accountIsActive = accountIsActive
self.accountCollectionError = accountCollectionError
self.version = version
self.source = source
self.status = status
Expand Down
33 changes: 23 additions & 10 deletions Sources/CodexBarCLI/CLIServeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ struct ServeUsageContext: Sendable {
let providerTimeout: TimeInterval?
let providerDeadline: ContinuousClock.Instant?
let providerOperations: CLIServeOperationCoordinator<UsageCommandOutput>
let includeAllCodexAccounts: Bool
let includeAllAccounts: Bool
let selectedAccountOnlyProviders: Set<UsageProvider>
let persistCLISessions: Bool

init(
Expand All @@ -189,7 +190,8 @@ struct ServeUsageContext: Sendable {
providerTimeout: TimeInterval?,
providerDeadline: ContinuousClock.Instant?,
providerOperations: CLIServeOperationCoordinator<UsageCommandOutput>,
includeAllCodexAccounts: Bool = true,
includeAllAccounts: Bool = true,
selectedAccountOnlyProviders: Set<UsageProvider> = [],
persistCLISessions: Bool = true)
{
self.config = config
Expand All @@ -198,7 +200,8 @@ struct ServeUsageContext: Sendable {
self.providerTimeout = providerTimeout
self.providerDeadline = providerDeadline
self.providerOperations = providerOperations
self.includeAllCodexAccounts = includeAllCodexAccounts
self.includeAllAccounts = includeAllAccounts
self.selectedAccountOnlyProviders = selectedAccountOnlyProviders
self.persistCLISessions = persistCLISessions
}
}
Expand Down Expand Up @@ -997,7 +1000,10 @@ extension CodexBarCLI {
providerTimeout: providerTimeout,
providerDeadline: providerDeadline,
providerOperations: runtime.providerOperations,
includeAllCodexAccounts: false),
selectedAccountOnlyProviders: Self.dashboardClaudeSwapIsEligible(
config: snapshot.config)
? [.claude]
: []),
costCollection: ServeCostCollectionContext(
configFingerprint: snapshot.cacheToken,
providerTimeout: providerTimeout,
Expand Down Expand Up @@ -1267,7 +1273,7 @@ extension CodexBarCLI {
resetStyle: Self.resetTimeDisplayStyleFromDefaults(),
weeklyWorkDays: Self.weeklyProgressWorkDaysFromDefaults(),
jsonOnly: true,
includeAllCodexAccounts: context.includeAllCodexAccounts,
includeAllAccounts: context.includeAllAccounts,
fetcher: UsageFetcher(),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection,
Expand All @@ -1279,25 +1285,32 @@ extension CodexBarCLI {
providers: selection.asList,
configFingerprint: Self.serveUsageOperationFingerprint(
configFingerprint: context.configFingerprint,
includeAllCodexAccounts: context.includeAllCodexAccounts),
includeAllAccounts: context.includeAllAccounts,
selectedAccountOnlyProviders: context.selectedAccountOnlyProviders),
deadline: context.providerDeadline,
operations: context.providerOperations)
{ provider in
await ProviderInteractionContext.$current.withValue(.background) {
var scopedCommand = command
scopedCommand.includeAllAccounts = context.includeAllAccounts
&& !context.selectedAccountOnlyProviders.contains(provider)
let providerCommand = scopedCommand
return await ProviderInteractionContext.$current.withValue(.background) {
await Self.fetchUsageOutputs(
provider: provider,
status: nil,
tokenContext: tokenContext,
command: command)
command: providerCommand)
}
}
}

static func serveUsageOperationFingerprint(
configFingerprint: String,
includeAllCodexAccounts: Bool) -> String
includeAllAccounts: Bool,
selectedAccountOnlyProviders: Set<UsageProvider> = []) -> String
{
"\(configFingerprint):codex-accounts=\(includeAllCodexAccounts ? "all" : "selected")"
let selectedOnly = selectedAccountOnlyProviders.map(\.rawValue).sorted().joined(separator: ",")
return "\(configFingerprint):accounts=\(includeAllAccounts ? "all" : "selected"):selected-only=\(selectedOnly)"
}

/// Adapts the shared dashboard snapshot producer to the authenticated HTTP
Expand Down
44 changes: 36 additions & 8 deletions Sources/CodexBarCLI/CLIUsageCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct UsageCommandContext {
let resetStyle: ResetTimeDisplayStyle
let weeklyWorkDays: Int?
let jsonOnly: Bool
let includeAllCodexAccounts: Bool
var includeAllAccounts: Bool
let fetcher: UsageFetcher
let claudeFetcher: ClaudeUsageFetcher
let browserDetection: BrowserDetection
Expand All @@ -41,6 +41,8 @@ private struct UsageSuccessRenderInput {
let provider: UsageProvider
let accountLabel: String?
let cacheAccountKey: String?
let accountIsActive: Bool?
let accountCollectionError: String?
let version: String?
let source: String
let status: ProviderStatusPayload?
Expand Down Expand Up @@ -179,7 +181,7 @@ extension CodexBarCLI {
resetStyle: resetStyle,
weeklyWorkDays: weeklyWorkDays,
jsonOnly: output.jsonOnly,
includeAllCodexAccounts: tokenSelection.allAccounts && providerList == [.codex],
includeAllAccounts: tokenSelection.allAccounts,
fetcher: fetcher,
claudeFetcher: claudeFetcher,
browserDetection: browserDetection,
Expand Down Expand Up @@ -257,16 +259,22 @@ extension CodexBarCLI {
tokenContext: TokenAccountCLIContext,
command: UsageCommandContext) async -> UsageCommandOutput
{
// Provider-specific by design: Codex can enumerate reconciled live, managed, and profile-home accounts.
if provider == .codex, command.includeAllCodexAccounts {
// Provider-specific by design: Codex reconciles accounts beyond token-account config.
if provider == .codex, command.includeAllAccounts {
var output = UsageCommandOutput()
let accounts = tokenContext.visibleCodexAccounts().visibleAccounts
let projection = tokenContext.visibleCodexAccounts()
let accounts = projection.visibleAccounts
let accountCollectionError = projection.hasUnreadableAddedAccountStore
? "Managed Codex account storage is unreadable."
: nil
let selections: [CodexVisibleAccount?] = accounts.isEmpty ? [nil] : accounts.map { Optional($0) }
for visibleAccount in selections {
let result = await Self.fetchUsageOutput(
provider: provider,
account: nil,
codexVisibleAccount: visibleAccount,
accountIsActive: visibleAccount.map { projection.activeVisibleAccountID == $0.id },
accountCollectionError: accountCollectionError,
status: status,
tokenContext: tokenContext,
command: command)
Expand All @@ -277,7 +285,9 @@ extension CodexBarCLI {

let accounts: [ProviderTokenAccount]
do {
accounts = try tokenContext.resolvedAccounts(for: provider)
accounts = try tokenContext.resolvedAccounts(
for: provider,
includeAllAccounts: command.includeAllAccounts)
} catch {
return Self.usageOutputForAccountResolutionError(
provider: provider,
Expand All @@ -288,6 +298,7 @@ extension CodexBarCLI {

let selections = Self.accountSelections(from: accounts)
var output = UsageCommandOutput()
let activeAccountID = tokenContext.activeConfiguredAccountID(for: provider)
let accountRefreshDelay = TokenAccountSupportCatalog
.support(for: provider)?.minimumDelayBetweenAccountRefreshes
for (index, account) in selections.enumerated() {
Expand All @@ -301,6 +312,7 @@ extension CodexBarCLI {
let result = await Self.fetchUsageOutput(
provider: provider,
account: account,
accountIsActive: account.map { activeAccountID == $0.id },
status: status,
tokenContext: tokenContext,
command: command)
Expand Down Expand Up @@ -348,6 +360,8 @@ extension CodexBarCLI {
provider: UsageProvider,
accountLabel: String?,
cacheAccountKey: String?,
accountIsActive: Bool?,
accountCollectionError: String?,
version: String?,
source: String,
status: ProviderStatusPayload?,
Expand All @@ -362,6 +376,8 @@ extension CodexBarCLI {
provider: provider,
account: accountLabel,
cacheAccountKey: cacheAccountKey,
accountIsActive: accountIsActive,
accountCollectionError: accountCollectionError,
version: version,
source: source,
status: status,
Expand Down Expand Up @@ -418,6 +434,8 @@ extension CodexBarCLI {
provider: input.provider,
accountLabel: input.accountLabel,
cacheAccountKey: input.cacheAccountKey,
accountIsActive: input.accountIsActive,
accountCollectionError: input.accountCollectionError,
version: input.version,
source: input.source,
status: input.status,
Expand All @@ -434,6 +452,8 @@ extension CodexBarCLI {
provider: UsageProvider,
account: ProviderTokenAccount?,
codexVisibleAccount: CodexVisibleAccount? = nil,
accountIsActive: Bool? = nil,
accountCollectionError: String? = nil,
status: ProviderStatusPayload?,
tokenContext: TokenAccountCLIContext,
command: UsageCommandContext) async -> UsageCommandOutput
Expand Down Expand Up @@ -470,7 +490,9 @@ extension CodexBarCLI {
provider: provider,
account: (
label: account?.label ?? codexVisibleAccount?.menuDisplayName,
cacheKey: cacheAccountKey),
cacheKey: cacheAccountKey,
isActive: accountIsActive,
collectionError: accountCollectionError),
source: effectiveSourceMode.rawValue,
status: status,
command: command)
Expand Down Expand Up @@ -539,6 +561,8 @@ extension CodexBarCLI {
provider: provider,
accountLabel: account?.label ?? codexVisibleAccount?.menuDisplayName,
cacheAccountKey: cacheAccountKey,
accountIsActive: accountIsActive,
accountCollectionError: accountCollectionError,
version: version,
source: source,
status: status,
Expand All @@ -558,6 +582,8 @@ extension CodexBarCLI {
provider: provider,
account: account?.label ?? codexVisibleAccount?.menuDisplayName,
cacheAccountKey: cacheAccountKey,
accountIsActive: accountIsActive,
accountCollectionError: accountCollectionError,
source: effectiveSourceMode.rawValue,
status: status,
error: error,
Expand Down Expand Up @@ -671,7 +697,7 @@ extension CodexBarCLI {

private static func webSourceUnsupportedOutput(
provider: UsageProvider,
account: (label: String?, cacheKey: String?),
account: (label: String?, cacheKey: String?, isActive: Bool?, collectionError: String?),
source: String,
status: ProviderStatusPayload?,
command: UsageCommandContext) -> UsageCommandOutput
Expand All @@ -688,6 +714,8 @@ extension CodexBarCLI {
provider: provider,
account: account.label,
cacheAccountKey: account.cacheKey,
accountIsActive: account.isActive,
accountCollectionError: account.collectionError,
source: source,
status: status,
error: error,
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBarCLI/DashboardPayloads.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ struct DashboardProviderPayload: Encodable {
let display: DashboardDisplayPayload
let error: ProviderErrorPayload?
let updatedAt: Date?
/// Per-account entries from a local multi-account source (today: claude-swap).
/// Additive schema-v1 data; absent for providers without such a source.
/// Per-account entries from the provider's reconciled multi-account source.
/// Additive schema-v1 data; absent for single-account providers.
let accounts: [DashboardAccountPayload]?
/// Row-local failure of the multi-account source; the ambient provider row stays intact.
let accountsError: String?
Expand Down
Loading
Loading