chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
static func runCacheClear(_ values: ParsedValues) {
let output = CLIOutputPreferences.from(values: values)
let cookies = values.flags.contains("cookies")
let cost = values.flags.contains("cost")
let all = values.flags.contains("all")
let rawProvider = values.options["provider"]?.last
let clearCookies = cookies || all
let clearCost = cost || all
if !clearCookies, !clearCost {
Self.exit(
code: .failure,
message: "Specify --cookies, --cost, or --all.",
output: output,
kind: .args)
}
if let error = Self.cacheClearProviderScopeError(rawProvider: rawProvider, clearCost: clearCost) {
Self.exit(code: .failure, message: error, output: output, kind: .args)
}
var results: [CacheClearResult] = []
if clearCookies {
if let rawProvider {
if let provider = ProviderDescriptorRegistry.cliNameMap[rawProvider.lowercased()] {
let summary = CookieHeaderCache.clearAllScopesDetailed(provider: provider)
results.append(CacheClearResult(
cache: "cookies",
provider: provider.rawValue,
cleared: summary.clearedCount,
error: Self.cookieClearError(failedCount: summary.failedCount)))
} else {
Self.exit(
code: .failure,
message: "Unknown provider: \(rawProvider)",
output: output,
kind: .args)
}
} else {
let summary = CookieHeaderCache.clearAllDetailed()
results.append(CacheClearResult(
cache: "cookies",
provider: nil,
cleared: summary.clearedCount,
error: Self.cookieClearError(failedCount: summary.failedCount)))
}
}
if clearCost {
let fm = FileManager.default
let cacheDir = Self.costUsageCacheDirectory(fileManager: fm)
var cleared = 0
var costError: String?
if fm.fileExists(atPath: cacheDir.path) {
do {
try fm.removeItem(at: cacheDir)
cleared = 1
} catch {
costError = error.localizedDescription
}
}
results.append(CacheClearResult(cache: "cost", provider: nil, cleared: cleared, error: costError))
}
switch output.format {
case .text:
for result in results {
let scope = result.provider ?? "all providers"
if let error = result.error {
print("\(result.cache): failed to clear (\(scope)) - \(error)")
} else if result.cleared > 0 {
print("\(result.cache): cleared (\(scope))")
} else {
print("\(result.cache): nothing to clear (\(scope))")
}
}
case .json:
Self.printJSON(results, pretty: output.pretty)
}
let hasErrors = results.contains(where: { $0.error != nil })
Self.exit(code: hasErrors ? .failure : .success, output: output, kind: .runtime)
}
static func cacheClearProviderScopeError(rawProvider: String?, clearCost: Bool) -> String? {
guard rawProvider != nil, clearCost else { return nil }
return "--provider only scopes cookie caches. Use --cookies --provider <name>, or omit --provider."
}
private static func cookieClearError(failedCount: Int) -> String? {
guard failedCount > 0 else { return nil }
return "Cookie cache cleanup failed for \(failedCount) operation\(failedCount == 1 ? "" : "s")"
}
}
struct CacheOptions: 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("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("cookies"), help: "Clear browser cookie caches")
var cookies: Bool = false
@Flag(name: .long("cost"), help: "Clear cost usage caches")
var cost: Bool = false
@Flag(name: .long("all"), help: "Clear all caches")
var all: Bool = false
@Option(name: .long("provider"), help: "Clear cache for a specific provider only")
var provider: String?
}
private struct CacheClearResult: Encodable {
let cache: String
let provider: String?
let cleared: Int
var error: String?
}
extension CodexBarCLI {
/// Mirrors the cost usage cache directory used by the app (UsageStore.costUsageCacheDirectory).
static func costUsageCacheDirectory(fileManager: FileManager = .default) -> URL {
let root = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return root
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("cost-usage", isDirectory: true)
}
}
@@ -0,0 +1,590 @@
import CodexBarCore
import Foundation
struct CLICardsBriefRow: Sendable, Equatable {
let provider: UsageProvider
let providerName: String
let sourceLabel: String
let planBadge: String?
let accountLabel: String?
let metricLabel: String?
let usedPercent: Double?
let resetLabel: String?
let resetAt: Date?
}
private struct CLICardsBriefColumns {
let provider: Int
let usage: Int
let reset: Int
}
enum CLICardsBriefRenderer {
private static let warningUsedThreshold = 85.0
private static let tableBorderOverhead = 10
private static let providerColumnMin = 20
private static let providerColumnFloor = 8
private static let providerColumnMax = 34
private static let usageColumnMin = 22
private static let usageColumnFloor = 9
private static let usageColumnWidth = 28
private static let usageBarMaxWidth = 22
private static let resetColumnMin = 8
private static let resetColumnFloor = 5
private static let resetColumnMax = 10
static func makeRows(cards: [CLICardModel]) -> [CLICardsBriefRow] {
cards.map { card in
let metric = card.metrics.first
let usedPercent = metric.map { max(0, min(100, 100 - $0.remainingPercent)) }
let resetLabel = Self.briefResetLabel(metric?.resetText)
return CLICardsBriefRow(
provider: card.provider,
providerName: card.title,
sourceLabel: card.sourceLabel,
planBadge: card.planBadge,
accountLabel: card.accountLine,
metricLabel: metric?.label,
usedPercent: usedPercent,
resetLabel: resetLabel,
resetAt: metric?.resetAt)
}
}
static func render(
rows: [CLICardsBriefRow],
failures: [CLICardFailure],
terminalWidth: Int,
useColor: Bool,
enhanced: Bool = false,
now: Date = Date()) -> String
{
guard !rows.isEmpty else {
return CLICardsRenderer.renderFailuresOnly(failures, useColor: useColor)
}
var lines: [String] = []
lines.append(Self.titleLine(now: now, terminalWidth: terminalWidth, useColor: useColor, enhanced: enhanced))
lines.append(contentsOf: Self.summaryLines(
rows: rows,
now: now,
terminalWidth: terminalWidth,
useColor: useColor,
enhanced: enhanced))
lines.append("")
lines.append(contentsOf: Self.tableLines(
rows: rows,
terminalWidth: terminalWidth,
useColor: useColor,
enhanced: enhanced))
let warningLines = Self.warningLines(rows: rows, terminalWidth: terminalWidth, useColor: useColor)
if !warningLines.isEmpty {
lines.append("")
lines.append(contentsOf: warningLines)
}
if !failures.isEmpty {
lines.append("")
lines.append(CLICardsRenderer.renderFailureFooter(failures: failures, useColor: useColor))
}
return lines.joined(separator: "\n")
}
private static func titleLine(now: Date, terminalWidth: Int, useColor: Bool, enhanced: Bool) -> String {
let left: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedAccentBold("codexbar • AI Usage & Limits")
} else if useColor {
CLIRenderer.colorizeAccentBold("codexbar • AI Usage & Limits")
} else {
"codexbar • AI Usage & Limits"
}
let timestamp = Self.timestampString(now: now)
guard Self.visibleLength(left) + timestamp.count + 1 <= terminalWidth else {
if Self.visibleLength(left) <= terminalWidth { return left }
return Self.truncatePlain(TextParsing.stripANSICodes(left), width: terminalWidth)
}
let gap = max(1, terminalWidth - Self.visibleLength(left) - timestamp.count)
let right: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedReadableMuted(timestamp)
} else if useColor {
CLIRenderer.colorizeSubtle(timestamp)
} else {
timestamp
}
return left + String(repeating: " ", count: gap) + right
}
private static func summaryLines(
rows: [CLICardsBriefRow],
now: Date,
terminalWidth: Int,
useColor: Bool,
enhanced: Bool) -> [String]
{
var parts: [String] = []
if let nextReset = Self.nextResetSummary(rows: rows, now: now) {
parts.append("Next reset: \(nextReset)")
}
let text = parts.joined(separator: "")
guard !text.isEmpty else { return [] }
let lines = Self.wrapText(
text,
firstPrefix: "",
continuationPrefix: " ",
width: terminalWidth)
if useColor, enhanced {
return lines.map(CLIRenderer.colorizeEnhancedReadable)
}
if useColor {
return lines.map(CLIRenderer.colorizeReadable)
}
return lines
}
private static func providerPlainLabel(_ row: CLICardsBriefRow) -> String {
if let account = row.accountLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !account.isEmpty {
return "\(row.providerName) · \(account) · \(row.sourceLabel)"
}
if let plan = row.planBadge, !plan.isEmpty {
return "\(row.providerName) · \(row.sourceLabel) · \(plan)"
}
return "\(row.providerName) · \(row.sourceLabel)"
}
private static func tableLines(
rows: [CLICardsBriefRow],
terminalWidth: Int,
useColor: Bool,
enhanced: Bool) -> [String]
{
let columns = Self.tableColumnWidths(
rows: rows,
terminalWidth: terminalWidth)
let top = Self.tableTop(
providerWidth: columns.provider,
usageWidth: columns.usage,
resetWidth: columns.reset,
useColor: useColor,
enhanced: enhanced)
let header = Self.tableHeaderRow(
providerWidth: columns.provider,
usageWidth: columns.usage,
resetWidth: columns.reset,
useColor: useColor,
enhanced: enhanced)
let divider = Self.tableDivider(
providerWidth: columns.provider,
usageWidth: columns.usage,
resetWidth: columns.reset,
useColor: useColor,
enhanced: enhanced)
var lines = [top, header, divider]
for row in rows {
lines.append(Self.dataRow(
row: row,
columns: columns,
useColor: useColor,
enhanced: enhanced))
}
lines.append(Self.tableBottom(
providerWidth: columns.provider,
usageWidth: columns.usage,
resetWidth: columns.reset,
useColor: useColor,
enhanced: enhanced))
return lines
}
private static func tableBorderLine(_ line: String, useColor: Bool, enhanced: Bool) -> String {
guard useColor else { return line }
if enhanced {
return CLIRenderer.colorizeEnhancedBorder(line)
}
return CLIRenderer.colorizeCardBorder(line)
}
private static func tableHeaderRow(
providerWidth: Int,
usageWidth: Int,
resetWidth: Int,
useColor: Bool,
enhanced: Bool) -> String
{
let provider = Self.styledHeaderLabel("Provider", width: providerWidth, useColor: useColor, enhanced: enhanced)
let usage = Self.styledHeaderLabel("Usage", width: usageWidth, useColor: useColor, enhanced: enhanced)
let reset = Self.styledHeaderLabel(
"Reset",
width: resetWidth,
alignRight: true,
useColor: useColor,
enhanced: enhanced)
return "\(provider)\(usage)\(reset)"
}
private static func styledHeaderLabel(
_ text: String,
width: Int,
alignRight: Bool = false,
useColor: Bool,
enhanced: Bool) -> String
{
let styled: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedReadable(text)
} else if useColor {
CLIRenderer.colorizeReadable(text)
} else {
text
}
return Self.pad(styled, width: width, alignRight: alignRight)
}
private static func tableTop(
providerWidth: Int,
usageWidth: Int,
resetWidth: Int,
useColor: Bool,
enhanced: Bool) -> String
{
let line = "" + String(repeating: "", count: providerWidth + 2)
+ "" + String(repeating: "", count: usageWidth + 2)
+ "" + String(repeating: "", count: resetWidth + 2) + ""
return Self.tableBorderLine(line, useColor: useColor, enhanced: enhanced)
}
private static func tableBottom(
providerWidth: Int,
usageWidth: Int,
resetWidth: Int,
useColor: Bool,
enhanced: Bool) -> String
{
let line = "" + String(repeating: "", count: providerWidth + 2)
+ "" + String(repeating: "", count: usageWidth + 2)
+ "" + String(repeating: "", count: resetWidth + 2) + ""
return Self.tableBorderLine(line, useColor: useColor, enhanced: enhanced)
}
private static func tableDivider(
providerWidth: Int,
usageWidth: Int,
resetWidth: Int,
useColor: Bool,
enhanced: Bool) -> String
{
let line = "" + String(repeating: "", count: providerWidth + 2)
+ "" + String(repeating: "", count: usageWidth + 2)
+ "" + String(repeating: "", count: resetWidth + 2) + ""
return Self.tableBorderLine(line, useColor: useColor, enhanced: enhanced)
}
private static func styledProviderCell(
row: CLICardsBriefRow,
width: Int,
useColor: Bool,
enhanced: Bool) -> String
{
if row.accountLabel?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
let fitted = Self.fitCell(Self.providerPlainLabel(row), width: width)
if useColor, enhanced {
return CLIRenderer.colorizeEnhancedReadable(fitted)
}
return useColor ? CLIRenderer.colorizeReadable(fitted) : fitted
}
guard useColor else {
return self.plainProviderCell(row: row, width: width)
}
let sourceVisibleWidth = row.sourceLabel.count + 2
let prefixVisibleWidth = row.providerName.count + 1 + sourceVisibleWidth
let plan: String? = row.planBadge.flatMap { $0.isEmpty ? nil : $0 }
let planPrefix = " · "
let planWidth = plan.map { _ in max(0, width - prefixVisibleWidth - planPrefix.count) } ?? 0
let styled: String = if enhanced {
CLIRenderer.colorizeEnhancedAccentBold(row.providerName)
+ " "
+ CLIRenderer.colorizeEnhancedBadge(row.sourceLabel)
+ Self.styledProviderPlan(
plan,
prefix: planPrefix,
width: planWidth,
useColor: useColor,
enhanced: enhanced)
} else {
CLIRenderer.colorizeReadable(row.providerName)
+ " "
+ CLIRenderer.colorizeCardBadge(row.sourceLabel)
+ Self.styledProviderPlan(
plan,
prefix: planPrefix,
width: planWidth,
useColor: useColor,
enhanced: enhanced)
}
return Self.fitCell(styled, width: width)
}
private static func styledProviderPlan(
_ plan: String?,
prefix: String,
width: Int,
useColor: Bool,
enhanced: Bool) -> String
{
guard let plan, width > 0 else { return "" }
let text = prefix + Self.truncatePlain(plan, width: width)
if useColor, enhanced {
return CLIRenderer.colorizeEnhancedReadableMuted(text)
}
if useColor {
return CLIRenderer.colorizeReadableMuted(text)
}
return text
}
private static func plainProviderCell(row: CLICardsBriefRow, width: Int) -> String {
if row.accountLabel?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
return self.fitCell(self.providerPlainLabel(row), width: width)
}
guard
let plan = row.planBadge,
!plan.isEmpty
else {
return self.fitCell(self.providerPlainLabel(row), width: width)
}
let prefix = "\(row.providerName) · \(row.sourceLabel) · "
let planWidth = max(0, width - prefix.count)
if planWidth > 0 {
return Self.pad(prefix + Self.truncatePlain(plan, width: planWidth), width: width)
}
return Self.fitCell("\(row.providerName) · \(row.sourceLabel)", width: width)
}
private static func styledResetCell(_ text: String, width: Int, useColor: Bool, enhanced: Bool) -> String {
let styled: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedReadableMuted(text)
} else if useColor {
CLIRenderer.colorizeReadableMuted(text)
} else {
text
}
return Self.fitCell(styled, width: width, alignRight: true)
}
private static func dataRow(
row: CLICardsBriefRow,
columns: CLICardsBriefColumns,
useColor: Bool,
enhanced: Bool) -> String
{
let provider = Self.styledProviderCell(
row: row,
width: columns.provider,
useColor: useColor,
enhanced: enhanced)
let usage: String
if let used = row.usedPercent {
let percent = String(format: "%.0f%%", used.rounded())
let barWidth = max(4, min(Self.usageBarMaxWidth, columns.usage - Self.visibleLength(percent) - 1))
let bar: String = if useColor, enhanced {
CLIRenderer.gradientUsedBar(usedPercent: used, width: barWidth)
} else {
CLIRenderer.cardUsedBar(usedPercent: used, width: barWidth, useColor: useColor)
}
let coloredPercent: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedUsedPercent(percent, usedPercent: used)
} else {
CLIRenderer.colorizeCardUsedPercent(percent, usedPercent: used, useColor: useColor)
}
usage = Self.pad("\(coloredPercent) \(bar)", width: columns.usage)
} else {
usage = Self.pad("", width: columns.usage)
}
let reset = Self.styledResetCell(
row.resetLabel ?? "",
width: columns.reset,
useColor: useColor,
enhanced: enhanced)
return "\(provider)\(usage)\(reset)"
}
private static func warningLines(
rows: [CLICardsBriefRow],
terminalWidth: Int,
useColor: Bool) -> [String]
{
let warnings = rows.compactMap { row -> String? in
guard let used = row.usedPercent, used >= Self.warningUsedThreshold else { return nil }
let label = row.metricLabel ?? "Usage"
return "\(row.providerName) \(label): \(Int(used.rounded()))% used"
}
guard !warnings.isEmpty else { return [] }
let lines = Self.wrapText(
warnings.joined(separator: "; "),
firstPrefix: "⚠ Warnings: ",
continuationPrefix: " ",
width: terminalWidth)
return useColor ? lines.map(CLIRenderer.colorizeWarning) : lines
}
private static func wrapText(
_ text: String,
firstPrefix: String,
continuationPrefix: String,
width: Int) -> [String]
{
let lineWidth = max(16, width)
var lines: [String] = []
var line = firstPrefix
var hasContent = false
for word in text.split(separator: " ").map(String.init) {
let separator = hasContent ? " " : ""
if line.count + separator.count + word.count <= lineWidth {
line += separator + word
hasContent = true
continue
}
if hasContent {
lines.append(line)
} else if !line.isEmpty {
lines.append(Self.truncatePlain(line, width: lineWidth))
}
let available = max(1, lineWidth - continuationPrefix.count)
line = continuationPrefix + Self.truncatePlain(word, width: available)
hasContent = true
}
if hasContent {
lines.append(line)
}
return lines
}
private static func nextResetSummary(rows: [CLICardsBriefRow], now: Date) -> String? {
guard let (row, label, _) = rows.compactMap({ row -> (CLICardsBriefRow, String, Date)? in
guard let reset = row.resetLabel, !reset.isEmpty, reset != "" else { return nil }
let sortDate = row.resetAt ?? Self.resetSortDate(reset, now: now)
guard let sortDate else { return nil }
return (row, reset, sortDate)
}).min(by: { $0.2 < $1.2 })
else { return nil }
let separator = Self.resetDurationMinutes(label) == nil ? " · " : " in "
return "\(row.providerName)\(separator)\(label)"
}
private static func resetSortDate(_ label: String, now: Date) -> Date? {
guard let minutes = resetDurationMinutes(label) else { return nil }
return now.addingTimeInterval(TimeInterval(minutes * 60))
}
private static func resetDurationMinutes(_ label: String) -> Int? {
var minutes = 0
var matched = false
if let match = label.range(of: #"(\d+)d"#, options: .regularExpression) {
matched = true
minutes += (Int(label[match].dropLast()) ?? 0) * 24 * 60
}
if let match = label.range(of: #"(\d+)h"#, options: .regularExpression) {
matched = true
minutes += (Int(label[match].dropLast()) ?? 0) * 60
}
if let match = label.range(of: #"(\d+)m"#, options: .regularExpression) {
matched = true
minutes += Int(label[match].dropLast()) ?? 0
}
return matched ? minutes : nil
}
private static func tableColumnWidths(
rows: [CLICardsBriefRow],
terminalWidth: Int) -> CLICardsBriefColumns
{
let providerContent = rows.map { Self.providerPlainLabel($0).count }.max() ?? Self.providerColumnMin
let resetContent = rows.compactMap(\.resetLabel).map(\.count).max() ?? 6
var providerWidth = min(Self.providerColumnMax, max(Self.providerColumnMin, providerContent))
var usageWidth = Self.usageColumnWidth
var resetWidth = min(Self.resetColumnMax, max(Self.resetColumnMin, resetContent))
while providerWidth + usageWidth + resetWidth + Self.tableBorderOverhead > terminalWidth {
if usageWidth > Self.usageColumnMin {
usageWidth -= 1
} else if providerWidth > Self.providerColumnMin {
providerWidth -= 1
} else if resetWidth > Self.resetColumnMin {
resetWidth -= 1
} else {
break
}
}
while providerWidth + usageWidth + resetWidth + Self.tableBorderOverhead > terminalWidth {
if providerWidth > Self.providerColumnFloor {
providerWidth -= 1
} else if usageWidth > Self.usageColumnFloor {
usageWidth -= 1
} else if resetWidth > Self.resetColumnFloor {
resetWidth -= 1
} else {
break
}
}
return CLICardsBriefColumns(provider: providerWidth, usage: usageWidth, reset: resetWidth)
}
private static func briefResetLabel(_ resetText: String?) -> String? {
guard var text = resetText?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else {
return nil
}
if text.hasPrefix("") {
text = String(text.dropFirst(2))
}
if text.hasPrefix("Resets in ") {
text = String(text.dropFirst("Resets in ".count))
} else if text.hasPrefix("Resets ") {
text = String(text.dropFirst("Resets ".count))
}
return text.isEmpty ? nil : text
}
private static func timestampString(now: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
formatter.dateFormat = "yyyy-MM-dd HH:mm zzz"
return formatter.string(from: now)
}
private static func pad(_ text: String, width: Int, alignRight: Bool = false) -> String {
let visible = Self.visibleLength(text)
if visible >= width { return text }
let padding = String(repeating: " ", count: width - visible)
return alignRight ? padding + text : text + padding
}
private static func fitCell(_ text: String, width: Int, alignRight: Bool = false) -> String {
let visible = Self.visibleLength(text)
if visible <= width {
return Self.pad(text, width: width, alignRight: alignRight)
}
let plain = TextParsing.stripANSICodes(text)
let clipped = Self.truncatePlain(plain, width: width)
return alignRight ? Self.pad(clipped, width: width, alignRight: true) : clipped
}
private static func truncatePlain(_ text: String, width: Int) -> String {
guard width > 0 else { return "" }
if text.count <= width { return text }
guard width > 1 else { return String(text.prefix(width)) }
return String(text.prefix(width - 1)) + ""
}
private static func visibleLength(_ text: String) -> Int {
TextParsing.stripANSICodes(text).count
}
}
+212
View File
@@ -0,0 +1,212 @@
import CodexBarCore
import Commander
import Foundation
struct CardsOptions: CommanderParsable {
private static let sourceHelp: String = {
#if os(macOS)
"Data source: auto | web | cli | oauth | api (auto behavior is provider-specific)"
#else
"Data source: auto | web | cli | oauth | api (web/auto are macOS only for web-capable providers)"
#endif
}()
@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("account"), help: "Token account label to use (from config.json)")
var account: String?
@Option(name: .long("account-index"), help: "Token account index (1-based)")
var accountIndex: Int?
@Flag(name: .long("all-accounts"), help: "Fetch all token accounts, or all visible Codex accounts")
var allAccounts: Bool = false
@Flag(name: .long("no-credits"), help: "Skip Codex credits line")
var noCredits: Bool = false
@Flag(name: .long("no-color"), help: "Disable ANSI colors in text output")
var noColor: Bool = false
@Flag(name: .long("status"), help: "Fetch and include provider status")
var status: Bool = false
@Flag(name: .long("web"), help: "Alias for --source web")
var web: Bool = false
@Option(name: .long("source"), help: Self.sourceHelp)
var source: String?
@Option(name: .long("web-timeout"), help: "Web fetch timeout (seconds; source=auto or web)")
var webTimeout: Double?
@Flag(name: .long("web-debug-dump-html"), help: "Dump HTML snapshots to /tmp when Codex dashboard data is missing")
var webDebugDumpHtml: Bool = false
@Flag(name: .long("antigravity-plan-debug"), help: "Emit Antigravity planInfo fields (debug)")
var antigravityPlanDebug: Bool = false
@Flag(name: .long("augment-debug"), help: "Emit Augment API responses (debug)")
var augmentDebug: Bool = false
@Flag(name: .long("brief"), help: "Compact table layout instead of the card grid")
var brief: Bool = false
}
extension CodexBarCLI {
static func runCards(_ values: ParsedValues) async {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let provider = Self.decodeProvider(from: values, config: config)
let includeCredits = !values.flags.contains("noCredits")
let includeStatus = values.flags.contains("status")
let sourceModeRaw = values.options["source"]?.last
let parsedSourceMode = Self.decodeSourceMode(from: values)
if sourceModeRaw != nil, parsedSourceMode == nil {
Self.exit(
code: .failure,
message: "Error: --source must be auto|web|cli|oauth|api.",
output: output,
kind: .args)
}
let antigravityPlanDebug = values.flags.contains("antigravityPlanDebug")
let augmentDebug = values.flags.contains("augmentDebug")
let webDebugDumpHTML = values.flags.contains("webDebugDumpHtml")
let webTimeout: TimeInterval
do {
webTimeout = try Self.decodeWebTimeout(from: values) ?? 60
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .args)
}
let verbose = values.flags.contains("verbose")
let noColor = values.flags.contains("noColor")
let useColor = Self.shouldUseColor(noColor: noColor, format: .text)
let brief = values.flags.contains("brief")
let resetStyle = Self.resetTimeDisplayStyleFromDefaults()
let weeklyWorkDays = Self.weeklyProgressWorkDaysFromDefaults()
let providerList = provider.asList
let tokenSelection: TokenAccountCLISelection
do {
tokenSelection = try Self.decodeTokenAccountSelection(from: values)
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .args)
}
if tokenSelection.allAccounts, tokenSelection.label != nil || tokenSelection.index != nil {
Self.exit(
code: .failure,
message: "Error: --all-accounts cannot be combined with --account or --account-index.",
output: output,
kind: .args)
}
if tokenSelection.usesOverride {
guard providerList.count == 1 else {
Self.exit(
code: .failure,
message: "Error: account selection requires a single provider.",
output: output,
kind: .args)
}
let supportsAllCodexAccounts = providerList[0] == .codex
&& tokenSelection.allAccounts
&& tokenSelection.label == nil
&& tokenSelection.index == nil
guard supportsAllCodexAccounts || TokenAccountSupportCatalog.support(for: providerList[0]) != nil else {
Self.exit(
code: .failure,
message: "Error: \(providerList[0].rawValue) does not support token accounts.",
output: output,
kind: .args)
}
}
let browserDetection = BrowserDetection()
let fetcher = UsageFetcher()
let claudeFetcher = ClaudeUsageFetcher(browserDetection: browserDetection)
let tokenContext: TokenAccountCLIContext
do {
tokenContext = try TokenAccountCLIContext(
selection: tokenSelection,
config: config,
verbose: verbose)
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config)
}
var cards: [CLICardModel] = []
var failures: [CLICardFailure] = []
var exitCode: ExitCode = .success
let command = UsageCommandContext(
format: .text,
includeCredits: includeCredits,
sourceModeOverride: parsedSourceMode,
antigravityPlanDebug: antigravityPlanDebug,
augmentDebug: augmentDebug,
webDebugDumpHTML: webDebugDumpHTML,
webTimeout: webTimeout,
verbose: verbose,
useColor: useColor,
resetStyle: resetStyle,
weeklyWorkDays: weeklyWorkDays,
jsonOnly: output.jsonOnly,
includeAllCodexAccounts: tokenSelection.allAccounts && providerList == [.codex],
fetcher: fetcher,
claudeFetcher: claudeFetcher,
browserDetection: browserDetection,
cardsLayout: true)
for provider in providerList {
let status = includeStatus ? await Self.fetchStatus(for: provider) : nil
let result = await ProviderInteractionContext.$current.withValue(.background) {
await Self.fetchUsageOutputs(
provider: provider,
status: status,
tokenContext: tokenContext,
command: command)
}
if result.exitCode != .success {
exitCode = result.exitCode
}
cards.append(contentsOf: result.cards)
failures.append(contentsOf: result.cardFailures)
}
let rendered: String
let enhanced = CLITerminalCapabilities.supportsEnhancedCards(useColor: useColor)
if brief {
let rows = CLICardsBriefRenderer.makeRows(cards: cards)
rendered = CLICardsBriefRenderer.render(
rows: rows,
failures: failures,
terminalWidth: CLICardsRenderer.terminalColumnCount(),
useColor: useColor,
enhanced: enhanced)
} else {
rendered = CLICardsRenderer.render(
cards: cards,
failures: failures,
terminalWidth: CLICardsRenderer.terminalColumnCount(),
useColor: useColor,
enhanced: enhanced)
}
if !rendered.isEmpty {
print(rendered)
}
Self.exit(code: exitCode, output: output, kind: exitCode == .success ? .runtime : .provider)
}
}
+529
View File
@@ -0,0 +1,529 @@
import CodexBarCore
import Foundation
#if canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#elseif canImport(Darwin)
import Darwin
#endif
struct CLICardMetric: Sendable, Equatable {
let label: String
let remainingPercent: Double
let resetText: String?
let resetAt: Date?
let detailText: String?
init(
label: String,
remainingPercent: Double,
resetText: String?,
resetAt: Date? = nil,
detailText: String? = nil)
{
self.label = label
self.remainingPercent = remainingPercent
self.resetText = resetText
self.resetAt = resetAt
self.detailText = detailText
}
}
struct CLICardModel: Sendable, Equatable {
let provider: UsageProvider
let title: String
let sourceLabel: String
let planBadge: String?
let accountLine: String?
let infoLines: [String]
let metrics: [CLICardMetric]
let extraLines: [String]
let statusLine: String?
}
struct CLICardFailure: Sendable, Equatable {
let provider: UsageProvider
let accountLabel: String?
let message: String
}
struct CLICardBuildInput: Sendable {
let provider: UsageProvider
let snapshot: UsageSnapshot
let credits: CreditsSnapshot?
let source: String
let status: ProviderStatusPayload?
let notes: [String]
let useColor: Bool
let resetStyle: ResetTimeDisplayStyle
let weeklyWorkDays: Int?
let now: Date
}
enum CLICardsRenderer {
static let minCardWidth = 38
static let maxCardWidth = 42
static let cardGap = 2
static func terminalColumnCount() -> Int {
if let value = terminalColumnCountFromTTY(), value > 0 {
return value
}
if let columns = ProcessInfo.processInfo.environment["COLUMNS"],
let value = Int(columns.trimmingCharacters(in: .whitespacesAndNewlines)),
value > 0
{
return value
}
return 80
}
private static func terminalColumnCountFromTTY(fileDescriptor: Int32 = STDOUT_FILENO) -> Int? {
guard isatty(fileDescriptor) == 1 else { return nil }
var windowSize = winsize(ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0)
guard ioctl(fileDescriptor, UInt(TIOCGWINSZ), &windowSize) == 0 else { return nil }
let columns = Int(windowSize.ws_col)
return columns > 0 ? columns : nil
}
static func columnCount(terminalWidth: Int, minCardWidth: Int = Self.minCardWidth) -> Int {
let usable = max(minCardWidth, terminalWidth)
return max(1, (usable + Self.cardGap) / (minCardWidth + Self.cardGap))
}
static func cardWidth(terminalWidth: Int, columns: Int) -> Int {
let totalGaps = (columns - 1) * Self.cardGap
let availableWidth = max(1, (terminalWidth - totalGaps) / columns)
return min(Self.maxCardWidth, availableWidth)
}
static func makeCard(_ input: CLICardBuildInput) -> CLICardModel {
let provider = input.provider
let snapshot = input.snapshot
let displayName = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
let context = RenderContext(
header: displayName,
status: input.status,
useColor: input.useColor,
resetStyle: input.resetStyle,
weeklyWorkDays: input.weeklyWorkDays,
notes: input.notes)
let infoLines = CLIRenderer.collectCardInfoLines(
provider: provider,
snapshot: snapshot,
credits: input.credits,
notes: input.notes,
useColor: input.useColor,
now: input.now)
let metrics = CLIRenderer.collectCardMetrics(
provider: provider,
snapshot: snapshot,
resetStyle: input.resetStyle,
now: input.now)
let extraLines = CLIRenderer.collectCardExtraLines(
provider: provider,
snapshot: snapshot,
credits: input.credits,
context: context,
now: input.now)
let statusLine: String?
if let status = input.status {
let line = "Status: \(status.indicator.label)\(status.descriptionSuffix)"
statusLine = CLIRenderer.colorizeStatusLine(line, indicator: status.indicator, useColor: input.useColor)
} else {
statusLine = nil
}
return CLICardModel(
provider: provider,
title: displayName,
sourceLabel: Self.normalizedSourceLabel(input.source),
planBadge: CLIRenderer.planBadgeText(provider: provider, snapshot: snapshot),
accountLine: snapshot.accountEmail(for: provider),
infoLines: infoLines,
metrics: metrics,
extraLines: extraLines,
statusLine: statusLine)
}
static func render(
cards: [CLICardModel],
failures: [CLICardFailure],
terminalWidth: Int,
useColor: Bool,
enhanced: Bool = false) -> String
{
guard !cards.isEmpty else {
return self.renderFailuresOnly(failures, useColor: useColor)
}
let columns = Self.columnCount(terminalWidth: terminalWidth)
let width = Self.cardWidth(terminalWidth: terminalWidth, columns: columns)
var chunks: [String] = []
for rowStart in stride(from: 0, to: cards.count, by: columns) {
let rowCards = Array(cards[rowStart..<min(rowStart + columns, cards.count)])
let rendered = rowCards.map { Self.renderCard($0, width: width, useColor: useColor, enhanced: enhanced) }
let rowHeight = rendered.map(\.count).max() ?? 0
for lineIndex in 0..<rowHeight {
let parts = rendered.map { lines -> String in
if lineIndex < lines.count - 1 {
return lines[lineIndex]
}
if lineIndex == rowHeight - 1, let bottom = lines.last {
return bottom
}
return Self.emptyCardLine(width: width, useColor: useColor, enhanced: enhanced)
}
chunks.append(parts.joined(separator: String(repeating: " ", count: Self.cardGap)))
}
if rowStart + columns < cards.count {
chunks.append("")
}
}
if !failures.isEmpty {
if !chunks.isEmpty {
chunks.append("")
}
chunks.append(Self.renderFailureFooter(failures: failures, useColor: useColor))
}
return chunks.joined(separator: "\n")
}
static func renderCard(_ card: CLICardModel, width: Int, useColor: Bool, enhanced: Bool = false) -> [String] {
let innerWidth = max(12, width - 4)
var lines: [String] = []
lines.append(Self.boxLine(kind: .top, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
lines.append(Self.headerLine(card: card, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
if let account = card.accountLine?.trimmingCharacters(in: .whitespacesAndNewlines), !account.isEmpty {
let accountText = "@ \(account)"
lines.append(Self.contentLine(
accountText,
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced,
style: .subtle))
}
lines.append(Self.separatorLine(innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
for infoLine in card.infoLines {
lines.append(Self.detailLine(
infoLine,
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced))
}
if !card.metrics.isEmpty, !card.infoLines.isEmpty {
lines.append(Self.contentLine("", innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
}
for (index, metric) in card.metrics.enumerated() {
if index > 0 {
lines.append(Self.contentLine("", innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
}
lines.append(Self.metricLabelLine(
metric: metric,
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced))
lines.append(Self.metricBarLine(
metric: metric,
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced))
if let resetText = metric.resetText {
lines.append(Self.contentLine(
resetText,
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced,
style: .subtle))
}
if let detailText = metric.detailText {
lines.append(Self.contentLine(
detailText,
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced,
style: .subtle))
}
}
for extraLine in card.extraLines {
lines.append(Self.detailLine(extraLine, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
}
if let statusLine = card.statusLine {
lines.append(Self.contentLine(statusLine, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
}
lines.append(Self.boxLine(kind: .bottom, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced))
return lines
}
private enum BoxLineKind {
case top
case bottom
}
private enum ContentStyle: Equatable {
case normal
case subtle
case border
}
private static func boxLine(kind: BoxLineKind, innerWidth: Int, useColor: Bool, enhanced: Bool) -> String {
let chars = switch kind {
case .top: ("", "")
case .bottom: ("", "")
}
let line = chars.0 + String(repeating: "", count: innerWidth + 2) + chars.1
return Self.styleBorder(line, useColor: useColor, enhanced: enhanced)
}
private static func headerLine(card: CLICardModel, innerWidth: Int, useColor: Bool, enhanced: Bool) -> String {
let title: String
let badge: String
if useColor, enhanced {
title = CLIRenderer.colorizeEnhancedAccentBold(card.title)
badge = CLIRenderer.colorizeEnhancedBadge(card.sourceLabel)
} else if useColor {
title = CLIRenderer.colorizeAccentBold(card.title)
badge = CLIRenderer.colorizeCardBadge(card.sourceLabel)
} else {
title = card.title
badge = "[\(card.sourceLabel)]"
}
let left = "\(title) \(badge)"
let leftVisible = Self.visibleLength(left)
let rawPlanText = card.planBadge.map { "PLAN \($0)" } ?? ""
let maxPlanWidth = max(0, innerWidth - leftVisible - 1)
let planText = maxPlanWidth >= 8 ? Self.truncatePlain(rawPlanText, width: maxPlanWidth) : ""
let planVisible = Self.visibleLength(planText)
let gap = max(1, innerWidth - leftVisible - planVisible)
let plan = Self.planPill(text: planText, useColor: useColor, enhanced: enhanced)
let content = left + String(repeating: " ", count: gap) + plan
return Self.sideBorder(content, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
private static func planPill(text: String, useColor: Bool, enhanced: Bool) -> String {
guard !text.isEmpty else { return "" }
let pieces = text.split(separator: " ", maxSplits: 1).map(String.init)
guard pieces.count == 2 else {
return useColor ? CLIRenderer.colorizeCardPlanBox(text) : text
}
if useColor, enhanced {
return CLIRenderer.colorizeEnhancedPlanLabel(pieces[0])
+ " "
+ CLIRenderer.colorizeEnhancedPlanValue(pieces[1])
}
if useColor {
return CLIRenderer.colorizeCardPlanBox(pieces[0])
+ " "
+ CLIRenderer.colorizeWarning(pieces[1])
}
return text
}
private static func separatorLine(innerWidth: Int, useColor: Bool, enhanced: Bool) -> String {
self.sideBorder(
String(repeating: "", count: innerWidth),
innerWidth: innerWidth,
useColor: useColor,
enhanced: enhanced,
contentStyle: .border)
}
private static func metricLabelLine(
metric: CLICardMetric,
innerWidth: Int,
useColor: Bool,
enhanced: Bool) -> String
{
let percentText = UsageFormatter.usageLine(
remaining: metric.remainingPercent,
used: 100 - metric.remainingPercent,
showUsed: false)
let coloredPercent: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedRemainingPercent(percentText, remainingPercent: metric.remainingPercent)
} else {
CLIRenderer.colorizeCardPercent(
percentText,
remainingPercent: metric.remainingPercent,
useColor: useColor)
}
let label: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedReadable(metric.label)
} else if useColor {
CLIRenderer.colorizeReadable(metric.label)
} else {
metric.label
}
let gap = max(1, innerWidth - Self.visibleLength(label) - Self.visibleLength(coloredPercent))
let content = label + String(repeating: " ", count: gap) + coloredPercent
return Self.sideBorder(content, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
private static func metricBarLine(
metric: CLICardMetric,
innerWidth: Int,
useColor: Bool,
enhanced: Bool) -> String
{
let barWidth = max(4, innerWidth - 4)
let bar: String = if useColor, enhanced {
CLIRenderer.gradientRemainingTrackBar(remainingPercent: metric.remainingPercent, width: barWidth)
} else {
CLIRenderer.cardBlockBar(
remainingPercent: metric.remainingPercent,
width: barWidth,
useColor: useColor)
}
return Self.sideBorder("[ \(bar) ]", innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
private static func detailLine(_ content: String, innerWidth: Int, useColor: Bool, enhanced: Bool) -> String {
let normalized = Self.normalizeGlyphs(content)
let plain = TextParsing.stripANSICodes(normalized)
let parts = plain.split(separator: ":", maxSplits: 1).map(String.init)
guard parts.count == 2 else {
return Self.contentLine(normalized, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
let rawLabel = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) + ":"
let rawValue = parts[1].trimmingCharacters(in: .whitespacesAndNewlines)
let label: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedReadable(rawLabel)
} else if useColor {
CLIRenderer.colorizeReadable(rawLabel)
} else {
rawLabel
}
let value: String = if useColor, enhanced {
CLIRenderer.colorizeEnhancedGood(rawValue)
} else if useColor {
CLIRenderer.colorizeAccent(rawValue)
} else {
rawValue
}
let gap = max(1, innerWidth - Self.visibleLength(label) - Self.visibleLength(value))
let line = label + String(repeating: " ", count: gap) + value
return Self.sideBorder(line, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
private static func contentLine(
_ content: String,
innerWidth: Int,
useColor: Bool,
enhanced: Bool,
style: ContentStyle = .normal) -> String
{
let normalized = Self.normalizeGlyphs(content)
let stripped = TextParsing.stripANSICodes(normalized)
let clipped = stripped.count <= innerWidth
? normalized
: (innerWidth <= 1 ? String(stripped.prefix(innerWidth)) : String(stripped.prefix(innerWidth - 1)) + "")
let display: String = if style == .subtle, useColor, enhanced {
CLIRenderer.colorizeEnhancedSubtle(TextParsing.stripANSICodes(clipped))
} else if style == .subtle, useColor {
CLIRenderer.colorizeSubtle(TextParsing.stripANSICodes(clipped))
} else {
clipped
}
return Self.sideBorder(display, innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
private static func sideBorder(
_ content: String,
innerWidth: Int,
useColor: Bool,
enhanced: Bool,
contentStyle: ContentStyle = .normal) -> String
{
let fitted = Self.fitContent(content, width: innerWidth)
let padding = max(0, innerWidth - Self.visibleLength(fitted))
let padded = fitted + String(repeating: " ", count: padding)
let visible = "\(padded)"
guard useColor else { return visible }
let left = Self.styleBorder("", useColor: useColor, enhanced: enhanced)
let right = Self.styleBorder("", useColor: useColor, enhanced: enhanced)
let styledContent: String = if contentStyle == .border {
Self.styleBorder(padded, useColor: useColor, enhanced: enhanced)
} else {
padded
}
return left + styledContent + right
}
private static func styleBorder(_ text: String, useColor: Bool, enhanced: Bool) -> String {
guard useColor else { return text }
if enhanced {
return CLIRenderer.colorizeEnhancedBorder(text)
}
return CLIRenderer.colorizeCardBorder(text)
}
private static func emptyCardLine(width: Int, useColor: Bool, enhanced: Bool) -> String {
let innerWidth = max(12, width - 4)
return Self.sideBorder("", innerWidth: innerWidth, useColor: useColor, enhanced: enhanced)
}
private static func visibleLength(_ text: String) -> Int {
TextParsing.stripANSICodes(self.normalizeGlyphs(text)).count
}
private static func truncatePlain(_ text: String, width: Int) -> String {
guard width > 0 else { return "" }
guard text.count > width else { return text }
if width <= 1 { return String(text.prefix(width)) }
return String(text.prefix(width - 1)) + ""
}
private static func fitContent(_ text: String, width: Int) -> String {
guard self.visibleLength(text) > width else { return text }
return self.truncatePlain(TextParsing.stripANSICodes(text), width: width)
}
private static func normalizeGlyphs(_ text: String) -> String {
text
.replacingOccurrences(of: "👤", with: "@")
.replacingOccurrences(of: "⏳ Resets in ", with: "Reset in ")
.replacingOccurrences(of: "⏳ Resets ", with: "Reset ")
.replacingOccurrences(of: "", with: "Reset ")
}
static func renderFailureFooter(failures: [CLICardFailure], useColor: Bool) -> String {
var lines = ["Failed providers:"]
for failure in failures {
let name = ProviderDescriptorRegistry.descriptor(for: failure.provider).metadata.displayName
if let account = failure.accountLabel, !account.isEmpty {
lines.append(" - \(name) (\(account)): \(failure.message)")
} else {
lines.append(" - \(name): \(failure.message)")
}
}
let text = lines.joined(separator: "\n")
guard useColor else { return text }
return CLIRenderer.colorizeError(text)
}
static func renderFailuresOnly(_ failures: [CLICardFailure], useColor: Bool) -> String {
guard !failures.isEmpty else { return "" }
return self.renderFailureFooter(failures: failures, useColor: useColor)
}
private static func normalizedSourceLabel(_ source: String) -> String {
let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return "auto" }
if trimmed.contains("oauth") { return "oauth" }
if trimmed.contains("web") || trimmed.contains("openai-web") { return "web" }
if trimmed.contains("api") { return "api" }
if trimmed.contains("cli") { return "cli" }
return trimmed
}
}
+444
View File
@@ -0,0 +1,444 @@
import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
static func runConfigValidate(_ values: ParsedValues) {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let issues = CodexBarConfigValidator.validate(config)
let hasErrors = issues.contains(where: { $0.severity == .error })
switch output.format {
case .text:
if issues.isEmpty {
print("Config: OK")
} else {
for issue in issues {
let provider = issue.provider?.rawValue ?? "config"
let field = issue.field ?? ""
let prefix = "[\(issue.severity.rawValue.uppercased())]"
let suffix = field.isEmpty ? "" : " (\(field))"
print("\(prefix) \(provider)\(suffix): \(issue.message)")
}
}
case .json:
Self.printJSON(issues, pretty: output.pretty)
}
Self.exit(code: hasErrors ? .failure : .success, output: output, kind: .config)
}
static func runConfigDump(_ values: ParsedValues) {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
Self.printJSON(config, pretty: output.pretty)
Self.exit(code: .success, output: output, kind: .config)
}
static func runConfigProviders(_ values: ParsedValues) {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let results = Self.configProviderStatuses(config)
switch output.format {
case .text:
for result in results {
let state = result.enabled ? "enabled" : "disabled"
let marker = result.defaultEnabled ? " default" : ""
print("\(result.provider): \(state)\(marker) (\(result.displayName))")
}
case .json:
Self.printJSON(results, pretty: output.pretty)
}
Self.exit(code: .success, output: output, kind: .config)
}
static func runConfigSetProviderEnabled(_ values: ParsedValues, enabled: Bool) {
let output = CLIOutputPreferences.from(values: values)
guard let rawProvider = values.options["provider"]?.last,
let provider = ProviderDescriptorRegistry.cliNameMap[rawProvider.lowercased()]
else {
Self.exit(
code: .failure,
message: "Unknown or missing provider. Use --provider <name>.",
output: output,
kind: .args)
}
let store = CodexBarConfigStore()
var config = Self.loadConfig(output: output)
config = Self.configSettingProviderEnabled(config, provider: provider, enabled: enabled)
do {
try store.save(config)
} catch {
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .config)
}
let metadata = ProviderDescriptorRegistry.descriptor(for: provider).metadata
let result = ConfigProviderToggleResult(
provider: provider.rawValue,
displayName: metadata.displayName,
enabled: enabled,
configPath: store.fileURL.path)
switch output.format {
case .text:
let state = enabled ? "enabled" : "disabled"
print("Config: \(state) \(metadata.displayName)")
case .json:
Self.printJSON(result, pretty: output.pretty)
}
Self.exit(code: .success, output: output, kind: .config)
}
static func runConfigSetAPIKey(_ values: ParsedValues) {
let output = CLIOutputPreferences.from(values: values)
guard let rawProvider = values.options["provider"]?.last,
let provider = ProviderDescriptorRegistry.cliNameMap[rawProvider.lowercased()]
else {
Self.exit(
code: .failure,
message: "Unknown or missing provider. Use --provider <name>.",
output: output,
kind: .args)
}
guard ProviderConfigEnvironment.supportsAPIKeyOverride(for: provider) else {
Self.exit(
code: .failure,
message: "\(rawProvider) does not support config API keys.",
output: output,
kind: .args)
}
let apiKey: String
do {
apiKey = try Self.resolveConfigAPIKeyInput(
apiKey: values.options["apiKey"]?.last,
readFromStdin: values.flags.contains("stdin"))
} catch {
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .args)
}
let enableProvider = !values.flags.contains("noEnable")
let store = CodexBarConfigStore()
var config = Self.loadConfig(output: output)
let accountOptions: ConfigAPIKeyAccountOptions?
do {
accountOptions = try Self.resolveConfigAPIKeyAccountOptions(
provider: provider,
label: values.options["label"]?.last,
usageScope: values.options["usageScope"]?.last,
organizationID: values.options["organizationId"]?.last,
workspaceID: values.options["workspaceId"]?.last)
} catch {
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .args)
}
config = Self.configSettingAPIKey(
config,
provider: provider,
apiKey: apiKey,
enableProvider: enableProvider,
accountOptions: accountOptions)
do {
try store.save(config)
} catch {
Self.exit(code: .failure, message: error.localizedDescription, output: output, kind: .config)
}
let result = ConfigSetAPIKeyResult(
provider: provider.rawValue,
enabled: config.providerConfig(for: provider)?.enabled ?? false,
configPath: store.fileURL.path)
switch output.format {
case .text:
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
let suffix = result.enabled ? " and enabled" : ""
let action = accountOptions == nil ? "stored API key" : "stored team token account"
print("Config: \(action) for \(name)\(suffix)")
case .json:
Self.printJSON(result, pretty: output.pretty)
}
Self.exit(code: .success, output: output, kind: .config)
}
static func resolveConfigAPIKeyInput(apiKey: String?, readFromStdin: Bool) throws -> String {
if apiKey != nil, readFromStdin {
throw CLIArgumentError("Use either --api-key or --stdin, not both.")
}
let raw: String? = if readFromStdin {
String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8)
} else {
apiKey
}
guard let value = Self.cleanConfigSecret(raw) else {
throw CLIArgumentError("Missing API key. Pass --api-key <key> or pipe it with --stdin.")
}
return value
}
static func configSettingAPIKey(
_ config: CodexBarConfig,
provider: UsageProvider,
apiKey: String,
enableProvider: Bool,
accountOptions: ConfigAPIKeyAccountOptions? = nil) -> CodexBarConfig
{
var updated = config.normalized()
var providerConfig = updated.providerConfig(for: provider) ?? ProviderConfig(id: provider)
if let accountOptions {
let existing = providerConfig.tokenAccounts
let accounts = existing?.accounts ?? []
let account = ProviderTokenAccount(
id: UUID(),
label: accountOptions.label,
token: apiKey,
addedAt: Date().timeIntervalSince1970,
lastUsed: nil,
usageScope: accountOptions.usageScope.rawValue,
organizationID: accountOptions.organizationID,
workspaceID: accountOptions.workspaceID)
providerConfig.tokenAccounts = ProviderTokenAccountData(
version: existing?.version ?? 1,
accounts: accounts + [account],
activeIndex: accounts.count)
providerConfig.apiKey = nil
if enableProvider {
providerConfig.enabled = true
}
updated.setProviderConfig(providerConfig)
return updated
}
providerConfig.apiKey = apiKey
if enableProvider {
providerConfig.enabled = true
}
updated.setProviderConfig(providerConfig)
return updated
}
static func resolveConfigAPIKeyAccountOptions(
provider: UsageProvider,
label: String?,
usageScope: String?,
organizationID: String?,
workspaceID: String?) throws -> ConfigAPIKeyAccountOptions?
{
let cleanedLabel = Self.cleanConfigValue(label)
let cleanedScope = Self.cleanConfigValue(usageScope)
let cleanedOrganizationID = try Self.cleanSingleLineConfigValue(
organizationID,
fieldName: "organization-id")
let cleanedWorkspaceID = try Self.cleanSingleLineConfigValue(
workspaceID,
fieldName: "workspace-id")
let hasAccountOptions = cleanedLabel != nil ||
cleanedScope != nil ||
cleanedOrganizationID != nil ||
cleanedWorkspaceID != nil
guard hasAccountOptions else { return nil }
guard provider == .zai else {
throw CLIArgumentError("Token-account options are only supported for --provider zai.")
}
guard cleanedScope?.lowercased() == ZaiUsageScope.team.rawValue else {
throw CLIArgumentError("Use --usage-scope team for z.ai team accounts, or omit account options.")
}
guard let organizationID = cleanedOrganizationID else {
throw CLIArgumentError("Missing --organization-id for z.ai team usage.")
}
guard let workspaceID = cleanedWorkspaceID else {
throw CLIArgumentError("Missing --workspace-id for z.ai team usage.")
}
return ConfigAPIKeyAccountOptions(
label: cleanedLabel ?? "Team",
usageScope: .team,
organizationID: organizationID,
workspaceID: workspaceID)
}
static func configSettingProviderEnabled(
_ config: CodexBarConfig,
provider: UsageProvider,
enabled: Bool) -> CodexBarConfig
{
var updated = config.normalized()
var providerConfig = updated.providerConfig(for: provider) ?? ProviderConfig(id: provider)
providerConfig.enabled = enabled
updated.setProviderConfig(providerConfig)
return updated
}
static func configProviderStatuses(_ config: CodexBarConfig) -> [ConfigProviderStatusResult] {
let metadata = ProviderDescriptorRegistry.metadata
return config.normalized().providers.map { providerConfig in
let meta = metadata[providerConfig.id]
let defaultEnabled = meta?.defaultEnabled ?? false
return ConfigProviderStatusResult(
provider: providerConfig.id.rawValue,
displayName: meta?.displayName ?? providerConfig.id.rawValue,
enabled: providerConfig.enabled ?? defaultEnabled,
defaultEnabled: defaultEnabled)
}
}
private static func cleanConfigSecret(_ raw: String?) -> String? {
guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
(value.hasPrefix("'") && value.hasSuffix("'"))
{
value = String(value.dropFirst().dropLast())
}
value = value.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
private static func cleanConfigValue(_ raw: String?) -> String? {
guard let value = self.cleanConfigSecret(raw) else { return nil }
return value
}
private static func cleanSingleLineConfigValue(_ raw: String?, fieldName: String) throws -> String? {
guard let value = self.cleanConfigValue(raw) else { return nil }
guard !value.contains(where: \.isNewline) else {
throw CLIArgumentError("--\(fieldName) must be a single line.")
}
return value
}
}
struct ConfigAPIKeyAccountOptions: Equatable {
let label: String
let usageScope: ZaiUsageScope
let organizationID: String
let workspaceID: String
}
struct ConfigOptions: 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("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
}
struct ConfigSetAPIKeyOptions: 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("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
@Option(name: .long("provider"), help: ProviderHelp.optionHelp)
var provider: String?
@Option(name: .long("api-key"), help: "API key to store")
var apiKey: String?
@Flag(name: .long("stdin"), help: "Read API key from stdin")
var stdin: Bool = false
@Flag(name: .long("no-enable"), help: "Store the key without enabling the provider")
var noEnable: Bool = false
@Option(name: .long("label"), help: "Token-account label (z.ai team mode)")
var label: String?
@Option(name: .long("usage-scope"), help: "Token-account usage scope (z.ai: team)")
var usageScope: String?
@Option(name: .long("organization-id"), help: "z.ai BigModel organization ID for team usage")
var organizationId: String?
@Option(name: .long("workspace-id"), help: "z.ai BigModel project ID for team usage")
var workspaceId: String?
}
struct ConfigProviderToggleOptions: 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("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
@Option(name: .long("provider"), help: ProviderHelp.optionHelp)
var provider: String?
}
private struct ConfigSetAPIKeyResult: Encodable {
let provider: String
let enabled: Bool
let configPath: String
}
struct ConfigProviderStatusResult: Encodable, Equatable {
let provider: String
let displayName: String
let enabled: Bool
let defaultEnabled: Bool
}
private struct ConfigProviderToggleResult: Encodable {
let provider: String
let displayName: String
let enabled: Bool
let configPath: String
}
+510
View File
@@ -0,0 +1,510 @@
import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
private static let costSupportedProviders: Set<UsageProvider> = [.claude, .codex]
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 Claude and Codex.",
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)
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 {
do {
// Cost usage is local-only; it does not require web/CLI provider fetches.
let snapshot = try await fetcher.loadTokenSnapshot(
provider: provider,
forceRefresh: forceRefresh,
historyDays: historyDays,
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 header = Self.costHeaderLine("\(name) Cost (API-rate estimate)", 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)"
let hintLine = UsageFormatter.costEstimateHint(provider: provider)
return [header, todayLine, monthLine, hintLine].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(UsageFormatter.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(UsageFormatter.costEstimateHint(provider: .codex))
return lines.joined(separator: "\n")
}
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: "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,
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
}
}
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 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?,
daily: [CostDailyEntryPayload],
projects: [CostProjectPayload] = [],
totals: CostTotalsPayload?,
error: ProviderErrorPayload?)
{
self.provider = provider
self.source = source
self.updatedAt = updatedAt
self.currencyCode = currencyCode
self.sessionTokens = sessionTokens
self.sessionCostUSD = sessionCostUSD
self.historyDays = historyDays
self.last30DaysTokens = last30DaysTokens
self.last30DaysCostUSD = last30DaysCostUSD
self.daily = daily
self.projects = projects
self.totals = totals
self.error = error
}
}
struct CostDailyEntryPayload: Encodable, Sendable {
let date: String
let inputTokens: Int?
let outputTokens: Int?
let cacheReadTokens: Int?
let cacheCreationTokens: Int?
let totalTokens: Int?
let costUSD: Double?
let modelsUsed: [String]?
let modelBreakdowns: [CostModelBreakdownPayload]?
private enum CodingKeys: String, CodingKey {
case date
case inputTokens
case outputTokens
case cacheReadTokens
case cacheCreationTokens
case totalTokens
case costUSD = "totalCost"
case modelsUsed
case modelBreakdowns
}
}
struct CostModelBreakdownPayload: Encodable, Sendable {
let modelName: String
let costUSD: Double?
let totalTokens: Int?
private enum CodingKeys: String, CodingKey {
case modelName
case costUSD = "cost"
case totalTokens
}
}
struct CostProjectPayload: Encodable, Sendable {
let name: String
let path: String?
let totalTokens: Int?
let totalCostUSD: Double?
let daily: [CostDailyEntryPayload]
let modelBreakdowns: [CostModelBreakdownPayload]?
let sources: [CostProjectSourcePayload]
private enum CodingKeys: String, CodingKey {
case name
case path
case totalTokens
case totalCostUSD = "totalCost"
case daily
case modelBreakdowns
case sources
}
init(
name: String,
path: String?,
totalTokens: Int?,
totalCostUSD: Double?,
daily: [CostDailyEntryPayload],
modelBreakdowns: [CostModelBreakdownPayload]?,
sources: [CostProjectSourcePayload] = [])
{
self.name = name
self.path = path
self.totalTokens = totalTokens
self.totalCostUSD = totalCostUSD
self.daily = daily
self.modelBreakdowns = modelBreakdowns
self.sources = sources
}
}
struct CostProjectSourcePayload: Encodable, Sendable {
let name: String
let path: String?
let totalTokens: Int?
let totalCostUSD: Double?
let daily: [CostDailyEntryPayload]
let modelBreakdowns: [CostModelBreakdownPayload]?
private enum CodingKeys: String, CodingKey {
case name
case path
case totalTokens
case totalCostUSD = "totalCost"
case daily
case modelBreakdowns
}
}
struct CostTotalsPayload: Encodable, Sendable {
let totalInputTokens: Int?
let totalOutputTokens: Int?
let cacheReadTokens: Int?
let cacheCreationTokens: Int?
let totalTokens: Int?
let totalCostUSD: Double?
private enum CodingKeys: String, CodingKey {
case totalInputTokens = "inputTokens"
case totalOutputTokens = "outputTokens"
case cacheReadTokens
case cacheCreationTokens
case totalTokens
case totalCostUSD = "totalCost"
}
}
// Intentionally empty.
@@ -0,0 +1,341 @@
import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
static func runDiagnose(_ values: ParsedValues) async {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let format = Self.decodeFormat(from: values)
guard format == .json else {
Self.exit(
code: .failure,
message: "Error: only JSON format is supported for diagnose",
output: output,
kind: .args)
}
let providerSelection: ProviderSelection
if let rawProvider = values.options["provider"]?.last {
guard let parsed = ProviderSelection(argument: rawProvider) else {
Self.exit(
code: .failure,
message: "Error: unknown provider '\(rawProvider)'",
output: output,
kind: .args)
}
providerSelection = parsed
} else {
providerSelection = Self.providerSelection(rawOverride: nil, enabled: config.enabledProviders())
}
let providers = providerSelection.asList
let pretty = values.flags.contains("pretty")
let verbose = values.flags.contains("verbose")
let outputPath = values.options["output"]?.last
let browserDetection = BrowserDetection()
let baseFetcher = UsageFetcher()
let tokenSelection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false)
let tokenContext: TokenAccountCLIContext
do {
tokenContext = try TokenAccountCLIContext(
selection: tokenSelection,
config: config,
verbose: verbose)
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config)
}
var diagnostics: [ProviderDiagnosticExport] = []
diagnostics.reserveCapacity(providers.count)
for provider in providers {
await diagnostics.append(Self.makeDiagnosticExport(
provider: provider,
tokenContext: tokenContext,
baseFetcher: baseFetcher,
browserDetection: browserDetection,
verbose: verbose))
}
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = pretty ? [.prettyPrinted, .sortedKeys] : .sortedKeys
do {
let data: Data = if diagnostics.count == 1, let diagnostic = diagnostics.first {
try encoder.encode(diagnostic)
} else {
try encoder.encode(ProviderDiagnosticBatchExport(
timestamp: Date(),
diagnostics: diagnostics))
}
var jsonString = String(data: data, encoding: .utf8) ?? "{}"
jsonString = LogRedactor.redact(jsonString)
if let outputPath, !outputPath.isEmpty {
try Self.writeDiagnosticExport(jsonString, to: outputPath)
} else {
print(jsonString)
}
} catch {
Self.exit(
code: .failure,
message: "Error encoding diagnostic: \(error.localizedDescription)",
output: output,
kind: .runtime)
}
Self.exit(code: .success, output: output, kind: .runtime)
}
static func writeDiagnosticExport(_ jsonString: String, to path: String) throws {
let url = URL(fileURLWithPath: path)
let parent = url.deletingLastPathComponent()
if !parent.path.isEmpty {
try FileManager.default.createDirectory(
at: parent,
withIntermediateDirectories: true)
}
try jsonString.write(to: url, atomically: true, encoding: .utf8)
}
}
extension CodexBarCLI {
private static func makeDiagnosticExport(
provider: UsageProvider,
tokenContext: TokenAccountCLIContext,
baseFetcher: UsageFetcher,
browserDetection: BrowserDetection,
verbose: Bool) async -> ProviderDiagnosticExport
{
let account = ((try? tokenContext.resolvedAccounts(for: provider)) ?? []).first
let env = tokenContext.environment(
base: ProcessInfo.processInfo.environment,
provider: provider,
account: account,
codexActiveSourceOverride: nil)
let settings = tokenContext.settingsSnapshot(
for: provider,
account: account,
codexActiveSourceOverride: nil)
let preferredSourceMode = tokenContext.preferredSourceMode(for: provider)
let sourceMode = tokenContext.effectiveSourceMode(
base: preferredSourceMode,
provider: provider,
account: account)
let fetcher = tokenContext.fetcher(base: baseFetcher, provider: provider, env: env)
let fetchContext = ProviderFetchContext(
runtime: .cli,
sourceMode: sourceMode,
includeCredits: true,
includeOptionalUsage: true,
webTimeout: 60,
webDebugDumpHTML: false,
verbose: verbose,
env: env,
settings: settings,
fetcher: fetcher,
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection,
selectedTokenAccountID: account?.id,
tokenAccountTokenUpdater: tokenContext.tokenUpdater(for: account),
providerManualTokenUpdater: tokenContext.manualTokenUpdater())
let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext)
return ProviderDiagnosticExportBuilder.build(.init(
provider: provider,
descriptor: descriptor,
outcome: outcome,
sourceMode: sourceMode,
settings: settings,
auth: Self.diagnosticAuthSummary(
provider: provider,
account: account,
config: tokenContext.config.providerConfig(for: provider),
environment: env,
settings: settings),
appVersion: Self.currentVersion()))
}
static func diagnosticAuthSummary(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?,
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> ProviderDiagnosticAuthSummary
{
if provider == .minimax {
let authMode = self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
return ProviderDiagnosticAuthSummary(
configured: authMode.usesAPIToken || authMode.usesCookie,
modes: authMode == .none ? [] : [authMode.description])
}
var modes: [String] = []
if account != nil {
modes.append("tokenAccount")
}
let hasConfigAPIAuth = if provider == .bedrock {
config?.sanitizedAPIKey != nil && config?.sanitizedSecretKey != nil
} else {
config?.sanitizedAPIKey != nil || config?.sanitizedSecretKey != nil
}
if hasConfigAPIAuth {
modes.append("api")
}
if Self.environmentAPIAuthConfigured(provider: provider, environment: environment), !modes.contains("api") {
modes.append("api")
}
if config?.sanitizedCookieHeader != nil {
modes.append("web")
}
if Self.environmentWebAuthConfigured(provider: provider, environment: environment), !modes.contains("web") {
modes.append("web")
}
return ProviderDiagnosticAuthSummary(
configured: !modes.isEmpty,
modes: modes)
}
private static func environmentAPIAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
self.environmentCoreAPIAuthConfigured(provider: provider, environment: environment) ||
self.environmentExtendedAPIAuthConfigured(provider: provider, environment: environment)
}
private static func environmentCoreAPIAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
switch provider {
case .alibaba:
AlibabaCodingPlanSettingsReader.apiToken(environment: environment) != nil
case .azureopenai:
AzureOpenAISettingsReader.apiKey(environment: environment) != nil
case .bedrock:
BedrockSettingsReader.hasCredentials(environment: environment)
case .claude:
ClaudeAdminAPISettingsReader.apiKey(environment: environment) != nil
case .codebuff:
CodebuffSettingsReader.apiKey(environment: environment) != nil
case .chutes:
ChutesSettingsReader.apiKey(environment: environment) != nil
case .crof:
CrofSettingsReader.apiKey(environment: environment) != nil
case .crossmodel:
CrossModelSettingsReader.apiToken(environment: environment) != nil
case .deepgram:
DeepgramSettingsReader.apiKey(environment: environment) != nil
case .deepseek:
DeepSeekSettingsReader.apiKey(environment: environment) != nil
case .doubao:
DoubaoSettingsReader.apiKey(environment: environment) != nil
case .elevenlabs:
ElevenLabsSettingsReader.apiKey(environment: environment) != nil
case .groq:
GroqSettingsReader.apiKey(environment: environment) != nil
case .kilo:
KiloSettingsReader.apiKey(environment: environment) != nil
case .factory:
FactorySettingsReader.apiKey(environment: environment) != nil
default:
false
}
}
private static func environmentExtendedAPIAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
switch provider {
case .kimi:
KimiSettingsReader.apiKey(environment: environment) != nil
case .kimik2:
KimiK2SettingsReader.apiKey(environment: environment) != nil
case .llmproxy:
LLMProxySettingsReader.apiKey(environment: environment) != nil
case .clawrouter:
ClawRouterSettingsReader.apiKey(environment: environment) != nil
case .sub2api:
Sub2APISettingsReader.apiKey(environment: environment) != nil
case .moonshot:
MoonshotSettingsReader.apiKey(environment: environment) != nil
case .ollama:
OllamaAPISettingsReader.apiKey(environment: environment) != nil
case .openai:
OpenAIAPISettingsReader.apiKey(environment: environment) != nil
case .openrouter:
OpenRouterSettingsReader.apiToken(environment: environment) != nil
case .stepfun:
StepFunSettingsReader.token(environment: environment) != nil
case .synthetic:
SyntheticSettingsReader.apiKey(environment: environment) != nil
case .venice:
VeniceSettingsReader.apiKey(environment: environment) != nil
case .warp:
WarpSettingsReader.apiKey(environment: environment) != nil
case .zai:
ZaiSettingsReader.apiToken(environment: environment) != nil
default:
false
}
}
private static func environmentWebAuthConfigured(
provider: UsageProvider,
environment: [String: String]) -> Bool
{
switch provider {
case .alibabatokenplan:
AlibabaTokenPlanSettingsReader.cookieHeader(environment: environment) != nil
case .kimi:
KimiSettingsReader.authToken(environment: environment) != nil
case .manus:
ManusSettingsReader.sessionToken(environment: environment) != nil
case .perplexity:
PerplexitySettingsReader.sessionToken(environment: environment) != nil
default:
false
}
}
static func resolveMiniMaxAuthMode(
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
{
let apiToken = ProviderTokenResolver.minimaxToken(environment: environment)
let envCookieHeader = ProviderTokenResolver.minimaxCookie(environment: environment)
let settingsCookieHeader = CookieHeaderNormalizer.normalize(settings?.minimax?.manualCookieHeader)
let cookieHeader = envCookieHeader ?? settingsCookieHeader
return MiniMaxAuthMode.resolve(apiToken: apiToken, cookieHeader: cookieHeader)
}
}
#if DEBUG
extension CodexBarCLI {
static func _diagnosticAuthSummaryForTesting(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?,
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> ProviderDiagnosticAuthSummary
{
self.diagnosticAuthSummary(
provider: provider,
account: account,
config: config,
environment: environment,
settings: settings)
}
static func _resolveMiniMaxAuthModeForTesting(
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
{
self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
}
}
#endif
+226
View File
@@ -0,0 +1,226 @@
import CodexBarCore
import Commander
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@main
enum CodexBarCLI {
static func main() async {
let rawArgv = Array(CommandLine.arguments.dropFirst())
let argv = Self.effectiveArgv(rawArgv)
let outputPreferences = CLIOutputPreferences.from(argv: argv)
// Fast path: global help/version before building descriptors.
if let helpIndex = argv.firstIndex(where: { $0 == "-h" || $0 == "--help" }) {
let command = helpIndex == 0 ? argv.dropFirst().first : argv.first
Self.printHelp(for: command)
}
if argv.contains("-V") || argv.contains("--version") {
Self.printVersion()
}
let program = Program(descriptors: Self.commandDescriptors())
do {
let invocation = try program.resolve(argv: argv)
Self.bootstrapLogging(path: invocation.path, values: invocation.parsedValues)
switch invocation.path {
case ["cards"]:
let signalMonitor = CLITerminationSignalMonitor { signalNumber in
CLITerminationSignalMonitor.terminateActiveHelpersAndReraise(signalNumber)
}
defer { signalMonitor.cancel() }
await self.runCards(invocation.parsedValues)
case ["usage"]:
let signalMonitor = CLITerminationSignalMonitor { signalNumber in
CLITerminationSignalMonitor.terminateActiveHelpersAndReraise(signalNumber)
}
defer { signalMonitor.cancel() }
await self.runUsage(invocation.parsedValues)
case ["cost"]:
await self.runCost(invocation.parsedValues)
case ["sessions", "list"]:
await self.runSessions(invocation.parsedValues)
case ["sessions", "focus"]:
await self.runSessionsFocus(invocation.parsedValues)
case ["serve"]:
await self.runServe(invocation.parsedValues)
case ["config", "validate"]:
self.runConfigValidate(invocation.parsedValues)
case ["config", "dump"]:
self.runConfigDump(invocation.parsedValues)
case ["config", "providers"]:
self.runConfigProviders(invocation.parsedValues)
case ["config", "enable"]:
self.runConfigSetProviderEnabled(invocation.parsedValues, enabled: true)
case ["config", "disable"]:
self.runConfigSetProviderEnabled(invocation.parsedValues, enabled: false)
case ["config", "set-api-key"]:
self.runConfigSetAPIKey(invocation.parsedValues)
case ["cache", "clear"]:
self.runCacheClear(invocation.parsedValues)
case ["diagnose"]:
let signalMonitor = CLITerminationSignalMonitor { signalNumber in
CLITerminationSignalMonitor.terminateActiveHelpersAndReraise(signalNumber)
}
defer { signalMonitor.cancel() }
await self.runDiagnose(invocation.parsedValues)
default:
Self.exit(
code: .failure,
message: "Unknown command",
output: outputPreferences,
kind: .args)
}
} catch let error as CommanderProgramError {
Self.exit(code: .failure, message: error.description, output: outputPreferences, kind: .args)
} catch {
Self.exit(code: .failure, message: error.localizedDescription, output: outputPreferences, kind: .runtime)
}
}
private static func commandDescriptors() -> [CommandDescriptor] {
let cardsSignature = CommandSignature.describe(CardsOptions())
let usageSignature = CommandSignature.describe(UsageOptions())
let costSignature = CommandSignature.describe(CostOptions())
let sessionsSignature = CommandSignature.describe(SessionsOptions())
let sessionsFocusSignature = CommandSignature.describe(SessionsFocusOptions())
let serveSignature = CommandSignature.describe(ServeOptions())
let configSignature = CommandSignature.describe(ConfigOptions())
let configProviderToggleSignature = CommandSignature.describe(ConfigProviderToggleOptions())
let configSetAPIKeySignature = CommandSignature.describe(ConfigSetAPIKeyOptions())
let cacheSignature = CommandSignature.describe(CacheOptions())
let diagnoseSignature = CommandSignature.describe(DiagnoseOptions())
return [
CommandDescriptor(
name: "cards",
abstract: "Print usage as a terminal card grid",
discussion: nil,
signature: cardsSignature),
CommandDescriptor(
name: "usage",
abstract: "Print usage as text or JSON",
discussion: nil,
signature: usageSignature),
CommandDescriptor(
name: "cost",
abstract: "Print local cost usage as text or JSON",
discussion: nil,
signature: costSignature),
CommandDescriptor(
name: "sessions",
abstract: "List live Codex and Claude Code sessions",
discussion: nil,
signature: CommandSignature(),
subcommands: [
CommandDescriptor(
name: "list",
abstract: "List live Codex and Claude Code sessions",
discussion: nil,
signature: sessionsSignature),
CommandDescriptor(
name: "focus",
abstract: "Focus the window for a session",
discussion: nil,
signature: sessionsFocusSignature),
],
defaultSubcommandName: "list"),
CommandDescriptor(
name: "serve",
abstract: "Serve usage and cost JSON over localhost HTTP",
discussion: nil,
signature: serveSignature),
CommandDescriptor(
name: "config",
abstract: "Config utilities",
discussion: nil,
signature: CommandSignature(),
subcommands: [
CommandDescriptor(
name: "validate",
abstract: "Validate config file",
discussion: nil,
signature: configSignature),
CommandDescriptor(
name: "dump",
abstract: "Print normalized config JSON",
discussion: nil,
signature: configSignature),
CommandDescriptor(
name: "providers",
abstract: "List provider enablement",
discussion: nil,
signature: configSignature),
CommandDescriptor(
name: "enable",
abstract: "Enable a provider",
discussion: nil,
signature: configProviderToggleSignature),
CommandDescriptor(
name: "disable",
abstract: "Disable a provider",
discussion: nil,
signature: configProviderToggleSignature),
CommandDescriptor(
name: "set-api-key",
abstract: "Store a provider API key",
discussion: nil,
signature: configSetAPIKeySignature),
],
defaultSubcommandName: "validate"),
CommandDescriptor(
name: "cache",
abstract: "Cache management",
discussion: nil,
signature: CommandSignature(),
subcommands: [
CommandDescriptor(
name: "clear",
abstract: "Clear cached data (cookies, cost, or all)",
discussion: nil,
signature: cacheSignature),
],
defaultSubcommandName: "clear"),
CommandDescriptor(
name: "diagnose",
abstract: "Run provider diagnostic and emit safe JSON export",
discussion: nil,
signature: diagnoseSignature),
]
}
// MARK: - Helpers
private static func bootstrapLogging(path: [String], values: ParsedValues) {
CodexBarLog.bootstrapIfNeeded(self.loggingConfiguration(path: path, values: values))
}
static func loggingConfiguration(path: [String], values: ParsedValues) -> CodexBarLog.Configuration {
let isJSON = values.flags.contains("jsonOutput") || values.flags.contains("jsonOnly")
let verbose = values.flags.contains("verbose")
let rawLevel = values.options["logLevel"]?.last
let level = Self.resolvedLogLevel(verbose: verbose, rawLevel: rawLevel)
let destination: CodexBarLog.Destination = path == ["diagnose"] ? .discard : .stderr
return .init(destination: destination, level: level, json: isJSON)
}
static func resolvedLogLevel(verbose: Bool, rawLevel: String?) -> CodexBarLog.Level {
CodexBarLog.parseLevel(rawLevel) ?? (verbose ? .debug : .error)
}
static func effectiveArgv(_ argv: [String]) -> [String] {
guard let first = argv.first else { return ["usage"] }
if first.hasPrefix("-") { return ["usage"] + argv }
return argv
}
}
+131
View File
@@ -0,0 +1,131 @@
import CodexBarCore
import Foundation
enum CLIErrorKind: String, Encodable, Sendable {
case args
case config
case provider
case runtime
}
struct ProviderErrorPayload: Encodable, Sendable {
let code: Int32
let message: String
let kind: CLIErrorKind?
}
extension CodexBarCLI {
static func makeErrorPayload(_ error: Error, kind: CLIErrorKind? = nil) -> ProviderErrorPayload {
ProviderErrorPayload(
code: self.mapError(error).rawValue,
message: error.localizedDescription,
kind: kind)
}
static func makeErrorPayload(code: ExitCode, message: String, kind: CLIErrorKind? = nil) -> ProviderErrorPayload {
ProviderErrorPayload(code: code.rawValue, message: message, kind: kind)
}
static func makeCLIErrorPayload(
message: String,
code: ExitCode,
kind: CLIErrorKind,
pretty: Bool) -> String?
{
let payload = ProviderPayload(
providerID: "cli",
account: nil,
version: nil,
source: "cli",
status: nil,
usage: nil,
credits: nil,
antigravityPlanInfo: nil,
openaiDashboard: nil,
error: ProviderErrorPayload(code: code.rawValue, message: message, kind: kind))
return self.encodeJSON([payload], pretty: pretty)
}
static func makeProviderErrorPayload(
provider: UsageProvider,
account: String?,
cacheAccountKey: String? = nil,
source: String,
status: ProviderStatusPayload?,
error: Error,
kind: CLIErrorKind = .provider) -> ProviderPayload
{
ProviderPayload(
provider: provider,
account: account,
cacheAccountKey: cacheAccountKey,
version: nil,
source: source,
status: status,
usage: nil,
credits: nil,
antigravityPlanInfo: nil,
openaiDashboard: nil,
error: self.makeErrorPayload(error, kind: kind))
}
static func encodeJSON(_ payload: some Encodable, pretty: Bool) -> String? {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = pretty ? [.prettyPrinted, .sortedKeys] : []
guard let data = try? encoder.encode(payload) else { return nil }
return String(data: data, encoding: .utf8)
}
static func printJSON(_ payload: some Encodable, pretty: Bool) {
if let output = self.encodeJSON(payload, pretty: pretty) {
print(output)
}
}
static func exit(
code: ExitCode,
message: String? = nil,
output: CLIOutputPreferences? = nil,
kind: CLIErrorKind = .runtime) -> Never
{
if self.shouldPrintExitError(code: code, message: message) {
if let output, output.usesJSONOutput {
let payload = self.makeCLIErrorPayload(
message: message ?? "",
code: code,
kind: kind,
pretty: output.pretty)
if let payload {
print(payload)
}
} else if let message {
self.writeStderr("\(message)\n")
}
}
platformExit(code.rawValue)
}
static func shouldPrintExitError(code: ExitCode, message: String?) -> Bool {
code != .success && message != nil
}
static func printError(_ error: Error, output: CLIOutputPreferences, kind: CLIErrorKind = .runtime) {
if output.usesJSONOutput {
let payload = ProviderPayload(
providerID: "cli",
account: nil,
version: nil,
source: "cli",
status: nil,
usage: nil,
credits: nil,
antigravityPlanInfo: nil,
openaiDashboard: nil,
error: self.makeErrorPayload(error, kind: kind))
self.printJSON([payload], pretty: output.pretty)
} else {
self.writeStderr("Error: \(error.localizedDescription)\n")
}
}
}
+11
View File
@@ -0,0 +1,11 @@
enum ExitCode: Int32 {
case success = 0
case failure = 1
case binaryNotFound = 2
case parseError = 3
case timeout = 4
init(_ rawValue: Int) {
self = ExitCode(rawValue: Int32(rawValue)) ?? .failure
}
}
+325
View File
@@ -0,0 +1,325 @@
import CodexBarCore
import Foundation
extension CodexBarCLI {
static func cardsHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar cards [--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>] [-v|--verbose]
[--provider \(ProviderHelp.list)]
[--account <label>] [--account-index <index>] [--all-accounts]
[--no-credits] [--no-color] [--status] [--source <auto|web|cli|oauth|api>]
[--web-timeout <seconds>] [--web-debug-dump-html] [--antigravity-plan-debug] [--augment-debug]
[--brief]
Description:
Print a one-shot usage snapshot as a responsive card grid in the terminal.
Honors enabled providers from config and reuses the same fetch flags as codexbar usage.
Failed providers are summarized in a footer instead of error cards.
Use --brief for a compact table layout (Provider / Usage / Reset).
Stdout is always the rendered card/table text; --json-output only affects stderr logs.
Global flags:
-h, --help Show help
-V, --version Show version
-v, --verbose Enable verbose logging
--no-color Disable ANSI colors in text output
--log-level <trace|verbose|debug|info|warning|error|critical>
--json-output Emit machine-readable logs (JSONL) to stderr
Examples:
codexbar cards
codexbar cards --provider codex
codexbar cards --provider all --status
codexbar cards --brief
codexbar cards --no-color
"""
}
static func usageHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar usage [--format text|json]
[--json]
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>] [-v|--verbose]
[--provider \(ProviderHelp.list)]
[--account <label>] [--account-index <index>] [--all-accounts]
[--no-credits] [--no-color] [--pretty] [--status] [--source <auto|web|cli|oauth|api>]
[--web-timeout <seconds>] [--web-debug-dump-html] [--antigravity-plan-debug] [--augment-debug]
Description:
Print usage from enabled providers as text (default) or JSON. Honors your in-app toggles.
Output format: use --json (or --format json) for JSON on stdout; use --json-output for JSON logs on stderr.
Source behavior is provider-specific:
- Codex: OpenAI web dashboard (usage limits, credits remaining, code review remaining, usage breakdown).
Auto falls back to Codex CLI only when cookies are missing.
- Claude: claude.ai API.
Auto falls back to Claude CLI only when cookies are missing.
- Kilo: app.kilo.ai API.
Auto falls back to Kilo CLI when API credentials are missing or unauthorized.
Token accounts are loaded from the resolved CodexBar config file.
Use --account or --account-index to select a specific token account.
Use --all-accounts to fetch every token account, or every visible Codex account for Codex.
Account selection requires a single provider.
Global flags:
-h, --help Show help
-V, --version Show version
-v, --verbose Enable verbose logging
--no-color Disable ANSI colors in text output
--log-level <trace|verbose|debug|info|warning|error|critical>
--json-output Emit machine-readable logs (JSONL) to stderr
Examples:
codexbar usage
codexbar usage --provider claude
codexbar usage --provider gemini
codexbar usage --format json --provider all --pretty
codexbar usage --provider all --json
codexbar usage --status
codexbar usage --provider codex --source web --format json --pretty
"""
}
static func costHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar cost [--format text|json]
[--json]
[--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]
Description:
Print local token cost usage from Claude/Codex native logs plus supported pi sessions.
This does not require web or CLI access and uses cached scan results unless --refresh is provided.
Examples:
codexbar cost
codexbar cost --provider codex --group-by project
codexbar cost --provider claude --format json --pretty
"""
}
static func sessionsHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar sessions [--json] [--pretty]
codexbar sessions focus <id>
Description:
List live local Codex and Claude Code agent sessions.
JSON uses stable AgentSession field names and ISO-8601 dates.
Focus activates the owning terminal or desktop app on macOS.
Examples:
codexbar sessions
codexbar sessions --json
codexbar sessions focus 019f3497-73bf-7df3-a173-4f67d968914a
"""
}
static func serveHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar serve [--port <port>] [--refresh-interval <seconds>]
[--request-timeout <seconds>]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
Description:
Start a foreground localhost-only HTTP server that exposes existing CLI JSON payloads.
The server binds to 127.0.0.1 only in this initial version.
Endpoints:
GET /health
GET /usage
GET /usage?provider=claude
GET /usage?provider=all
GET /cost
GET /cost?provider=codex
Examples:
codexbar serve
codexbar serve --port 8080 --refresh-interval 60 --request-timeout 30
curl http://127.0.0.1:8080/usage?provider=all
"""
}
static func configHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar config validate [--format text|json]
[--json]
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--pretty]
codexbar config dump [--format text|json]
[--json]
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--pretty]
codexbar config providers [--format text|json] [--json] [--json-only] [--pretty]
codexbar config enable --provider <name> [--format text|json] [--json] [--json-only] [--pretty]
codexbar config disable --provider <name> [--format text|json] [--json] [--json-only] [--pretty]
codexbar config set-api-key --provider <name> (--api-key <key>|--stdin)
[--label <label>] [--usage-scope team]
[--organization-id <org>] [--workspace-id <project>]
[--no-enable]
[--format text|json] [--json] [--json-only] [--pretty]
Description:
Validate or print the CodexBar config file (default: validate).
providers lists persistent provider enablement.
enable/disable updates the same provider toggle used by Settings.
set-api-key stores a provider API key in the resolved config file and enables that provider by default.
For z.ai team usage, add --usage-scope team with BigModel organization and project IDs; this stores
the key as a token account instead of a provider-level personal key.
Examples:
codexbar config validate --format json --pretty
codexbar config dump --pretty
codexbar config providers
codexbar config enable --provider grok
codexbar config disable --provider cursor
printf '%s' "$ELEVENLABS_API_KEY" | codexbar config set-api-key --provider elevenlabs --stdin
printf '%s' "$Z_AI_API_KEY" | codexbar config set-api-key --provider zai --stdin \\
--label Team --usage-scope team --organization-id org_... --workspace-id proj_...
"""
}
static func cacheHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar cache clear <--cookies|--cost|--all>
[--provider <name>]
[--format text|json]
[--json]
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--pretty]
Description:
Clear cached data. Use --cookies to clear browser cookie caches (stored in Keychain),
--cost to clear cost usage scan caches, or --all for both.
Optionally specify --provider with --cookies to clear cookies for a single provider only.
Examples:
codexbar cache clear --cookies
codexbar cache clear --cookies --provider claude
codexbar cache clear --cost
codexbar cache clear --all
codexbar cache clear --all --format json --pretty
"""
}
static func diagnoseHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar diagnose --provider <name|all> --format json
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--redact] [--output <path>]
[--pretty]
Description:
Run provider diagnostic fetches and print a safe JSON export for issue reporting.
The export is redacted and omits raw API tokens, cookies, auth headers, emails,
account IDs, org IDs, raw responses, and billing-history records.
Examples:
codexbar diagnose --provider minimax --format json --redact --output diagnostic.json
codexbar diagnose --provider minimax --format json --pretty
codexbar diagnose --provider claude --format json --pretty
codexbar diagnose --provider all --format json
"""
}
static func rootHelp(version: String) -> String {
"""
CodexBar \(version)
Usage:
codexbar [--format text|json]
[--json]
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>] [-v|--verbose]
[--provider \(ProviderHelp.list)]
[--account <label>] [--account-index <index>] [--all-accounts]
[--no-credits] [--no-color] [--pretty] [--status] [--source <auto|web|cli|oauth|api>]
[--web-timeout <seconds>] [--web-debug-dump-html] [--antigravity-plan-debug] [--augment-debug]
codexbar cards [--provider \(ProviderHelp.list)] [--brief] [--no-color] [--status]
codexbar cost [--format text|json]
[--json]
[--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]
codexbar sessions [--json] [--pretty]
codexbar sessions focus <id>
codexbar serve [--port <port>] [--refresh-interval <seconds>]
[--request-timeout <seconds>]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>] [-v|--verbose]
codexbar config <validate|dump|providers> [--format text|json]
[--json]
[--json-only]
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--pretty]
codexbar config enable --provider <name>
codexbar config disable --provider <name>
codexbar config set-api-key --provider <name> (--api-key <key>|--stdin)
codexbar config set-api-key --provider zai --stdin --usage-scope team
--organization-id <org> --workspace-id <project>
codexbar cache clear <--cookies|--cost|--all> [--provider <name>]
codexbar diagnose --provider <name|all> --format json [--redact] [--output <path>] [--pretty]
Global flags:
-h, --help Show help
-V, --version Show version
-v, --verbose Enable verbose logging
--no-color Disable ANSI colors in text output
--log-level <trace|verbose|debug|info|warning|error|critical>
--json-output Emit machine-readable logs (JSONL) to stderr
Examples:
codexbar
codexbar --format json --provider all --pretty
codexbar --provider all --json
codexbar --provider gemini
codexbar cards --provider all --status
codexbar cards --brief
codexbar cost --provider claude --format json --pretty
codexbar sessions --json
codexbar serve --port 8080
codexbar config validate --format json --pretty
codexbar config enable --provider grok
codexbar config set-api-key --provider elevenlabs --stdin
codexbar cache clear --cookies
codexbar diagnose --provider minimax --format json --redact --output diagnostic.json
codexbar diagnose --provider minimax --format json --pretty
codexbar diagnose --provider all --format json
"""
}
}
+415
View File
@@ -0,0 +1,415 @@
import CodexBarCore
import Commander
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
import Foundation
extension CodexBarCLI {
static func decodeProvider(from values: ParsedValues, config: CodexBarConfig) -> ProviderSelection {
let rawOverride = values.options["provider"]?.last
return Self.providerSelection(rawOverride: rawOverride, enabled: config.enabledProviders())
}
static func providerSelection(rawOverride: String?, enabled: [UsageProvider]) -> ProviderSelection {
if let rawOverride, let parsed = ProviderSelection(argument: rawOverride) {
return parsed
}
if enabled.count == 2 {
let enabledSet = Set(enabled)
let primary = Set(ProviderDescriptorRegistry.all.filter(\ .metadata.isPrimaryProvider).map(\ .id))
if !primary.isEmpty, enabledSet == primary {
return .both
}
return .custom(enabled)
}
if enabled.count >= 3 { return .custom(enabled) }
if let first = enabled.first { return ProviderSelection(provider: first) }
return .custom([])
}
static func decodeFormat(from values: ParsedValues) -> OutputFormat {
if let raw = values.options["format"]?.last, let parsed = OutputFormat(argument: raw) {
return parsed
}
if values.flags.contains("jsonShortcut") || values.flags.contains("json") || values.flags.contains("jsonOnly") {
return .json
}
return .text
}
static func decodeTokenAccountSelection(from values: ParsedValues) throws -> TokenAccountCLISelection {
let label = values.options["account"]?.last
let rawIndex = values.options["accountIndex"]?.last
var index: Int?
if let rawIndex {
guard let parsed = Int(rawIndex), parsed > 0 else {
throw CLIArgumentError("--account-index must be a positive integer.")
}
index = parsed - 1
}
let allAccounts = values.flags.contains("allAccounts")
return TokenAccountCLISelection(label: label, index: index, allAccounts: allAccounts)
}
static func shouldUseColor(noColor: Bool, format: OutputFormat) -> Bool {
guard format == .text else { return false }
if noColor { return false }
let env = ProcessInfo.processInfo.environment
if env["TERM"]?.lowercased() == "dumb" { return false }
return isatty(STDOUT_FILENO) == 1
}
static func detectVersion(for provider: UsageProvider, browserDetection: BrowserDetection) -> String? {
ProviderDescriptorRegistry.descriptor(for: provider).cli.versionDetector?(browserDetection)
}
static func normalizeVersion(raw: String?) -> String? {
guard let raw, !raw.isEmpty else { return nil }
if let match = raw.range(of: #"(\d+(?:\.\d+)+)"#, options: .regularExpression) {
return String(raw[match]).trimmingCharacters(in: .whitespacesAndNewlines)
}
return raw.trimmingCharacters(in: .whitespacesAndNewlines)
}
static func makeHeader(provider: UsageProvider, version: String?, source: String) -> String {
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
if let version, !version.isEmpty {
return "\(name) \(version) (\(source))"
}
return "\(name) (\(source))"
}
static func printFetchAttempts(provider: UsageProvider, attempts: [ProviderFetchAttempt]) {
guard !attempts.isEmpty else { return }
self.writeStderr("[\(provider.rawValue)] fetch strategies:\n")
for attempt in attempts {
let kindLabel = Self.fetchKindLabel(attempt.kind)
var line = " - \(attempt.strategyID) (\(kindLabel))"
line += attempt.wasAvailable ? " available" : " unavailable"
if let error = attempt.errorDescription, !error.isEmpty {
line += " error=\(error)"
}
self.writeStderr("\(line)\n")
}
}
static func usageTextNotes(
provider: UsageProvider,
sourceMode: ProviderSourceMode,
resolvedSourceLabel: String) -> [String]
{
guard provider == .kilo,
sourceMode == .auto,
resolvedSourceLabel.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "cli"
else {
return []
}
return ["Using CLI fallback"]
}
static func kiloAutoFallbackSummary(
provider: UsageProvider,
sourceMode: ProviderSourceMode,
attempts: [ProviderFetchAttempt]) -> String?
{
guard provider == .kilo, sourceMode == .auto, !attempts.isEmpty else { return nil }
let parts = attempts.map { attempt in
let label = Self.fetchKindLabel(attempt.kind)
let message = attempt.errorDescription?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !message.isEmpty {
return "\(label): \(message)"
}
return "\(label): \(attempt.wasAvailable ? "success" : "unavailable")"
}
guard !parts.isEmpty else { return nil }
return "Kilo auto fallback attempts: " + parts.joined(separator: " -> ")
}
private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String {
switch kind {
case .cli: "cli"
case .web: "web"
case .oauth: "oauth"
case .apiToken: "api"
case .localProbe: "local"
case .webDashboard: "web"
}
}
static func fetchStatus(for provider: UsageProvider) async -> ProviderStatusPayload? {
let urlString = ProviderDescriptorRegistry.descriptor(for: provider).metadata.statusPageURL
guard let urlString,
let baseURL = URL(string: urlString) else { return nil }
do {
return try await StatusFetcher.fetch(from: baseURL)
} catch {
return ProviderStatusPayload(
indicator: .unknown,
description: error.localizedDescription,
updatedAt: nil,
url: urlString)
}
}
static func resetTimeDisplayStyleFromDefaults() -> ResetTimeDisplayStyle {
let domains = [
"com.steipete.codexbar",
"com.steipete.codexbar.debug",
]
for domain in domains {
if let value = UserDefaults(suiteName: domain)?.object(forKey: "resetTimesShowAbsolute") as? Bool {
return value ? .absolute : .countdown
}
}
let fallback = UserDefaults.standard.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false
return fallback ? .absolute : .countdown
}
static func weeklyProgressWorkDaysFromDefaults() -> Int? {
let domains = [
"com.steipete.codexbar",
"com.steipete.codexbar.debug",
]
for domain in domains {
if let value = UserDefaults(suiteName: domain)?.object(forKey: "weeklyProgressWorkDays") as? Int {
return value
}
}
return UserDefaults.standard.object(forKey: "weeklyProgressWorkDays") as? Int
}
static func fetchProviderUsage(
provider: UsageProvider,
context: ProviderFetchContext) async -> ProviderFetchOutcome
{
let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
if !descriptor.fetchPlan.sourceModes.contains(context.sourceMode) {
let error = SourceSelectionError.unsupported(
provider: descriptor.cli.name,
source: context.sourceMode)
return ProviderFetchOutcome(result: .failure(error), attempts: [])
}
return await descriptor.fetchOutcome(context: context)
}
private enum SourceSelectionError: LocalizedError {
case unsupported(provider: String, source: ProviderSourceMode)
var errorDescription: String? {
switch self {
case let .unsupported(provider, source):
"Source '\(source.rawValue)' is not supported for \(provider)."
}
}
}
static func loadOpenAIDashboardIfAvailable(
usage: UsageSnapshot,
sourceLabel: String,
context: ProviderFetchContext) -> OpenAIDashboardSnapshot?
{
guard let cache = OpenAIDashboardCacheStore.load() else { return nil }
let snapshot: OpenAIDashboardSnapshot = if cache.snapshot.dailyBreakdown.isEmpty,
!cache.snapshot.creditEvents.isEmpty
{
OpenAIDashboardSnapshot(
signedInEmail: cache.snapshot.signedInEmail,
codeReviewRemainingPercent: cache.snapshot.codeReviewRemainingPercent,
codeReviewLimit: cache.snapshot.codeReviewLimit,
creditEvents: cache.snapshot.creditEvents,
dailyBreakdown: OpenAIDashboardSnapshot.makeDailyBreakdown(
from: cache.snapshot.creditEvents,
maxDays: 30),
usageBreakdown: cache.snapshot.usageBreakdown,
creditsPurchaseURL: cache.snapshot.creditsPurchaseURL,
updatedAt: cache.snapshot.updatedAt)
} else {
cache.snapshot
}
let input = CodexCLIDashboardAuthorityContext.makeCachedDashboardInput(
dashboard: snapshot,
cachedAccountEmail: cache.accountEmail,
usage: usage,
sourceLabel: sourceLabel,
context: context)
let decision = CodexDashboardAuthority.evaluate(input)
if decision.allowedEffects.contains(.cachedDashboardReuse) {
return snapshot
}
if decision.cleanup.contains(.dashboardCache) {
OpenAIDashboardCacheStore.clear()
}
return nil
}
static func decodeWebTimeout(from values: ParsedValues) throws -> TimeInterval? {
guard let raw = values.options["webTimeout"]?.last else { return nil }
guard let seconds = Double(raw),
seconds.isFinite,
seconds >= 0,
seconds <= TimeInterval(Int64.max)
else {
throw CLIArgumentError("--web-timeout must be a finite, nonnegative number within the supported range.")
}
return seconds
}
static func decodeSourceMode(from values: ParsedValues) -> ProviderSourceMode? {
if values.flags.contains("web") {
return .web
}
guard let raw = values.options["source"]?.last?.lowercased() else { return nil }
return ProviderSourceMode(rawValue: raw)
}
static func renderOpenAIWebDashboardText(_ dash: OpenAIDashboardSnapshot) -> String {
var lines: [String] = []
if let email = dash.signedInEmail, !email.isEmpty {
lines.append("Web session: \(email)")
}
if let remaining = dash.codeReviewRemainingPercent {
let percent = Int(remaining.rounded())
if let limit = dash.codeReviewLimit,
let reset = UsageFormatter.resetLine(for: limit, style: .countdown)
{
lines.append("Code review: \(percent)% remaining (\(reset))")
} else {
lines.append("Code review: \(percent)% remaining")
}
}
if let first = dash.creditEvents.first {
let day = first.date.formatted(date: .abbreviated, time: .omitted)
lines.append("Web history: \(dash.creditEvents.count) events (latest \(day))")
} else {
lines.append("Web history: none")
}
return lines.joined(separator: "\n")
}
static func mapError(_ error: Error) -> ExitCode {
switch error {
case TTYCommandRunner.Error.binaryNotFound,
CodexStatusProbeError.codexNotInstalled,
ClaudeUsageError.claudeNotInstalled,
GeminiStatusProbeError.geminiNotInstalled:
ExitCode(2)
case CodexStatusProbeError.timedOut,
TTYCommandRunner.Error.timedOut,
GeminiStatusProbeError.timedOut,
ClaudeWebFetchStrategyError.timedOut,
CostUsageError.timedOut:
ExitCode(4)
case ClaudeUsageError.parseFailed,
ClaudeUsageError.oauthFailed,
CostUsageError.unsupportedProvider,
UsageError.decodeFailed,
UsageError.noRateLimitsFound,
GeminiStatusProbeError.parseFailed,
GeminiStatusProbeError.consumerTierDeprecated:
ExitCode(3)
default:
.failure
}
}
static func printAntigravityPlanInfo(_ info: AntigravityPlanInfoSummary) {
let fields: [(String, String?)] = [
("planName", info.planName),
("planDisplayName", info.planDisplayName),
("displayName", info.displayName),
("productName", info.productName),
("planShortName", info.planShortName),
]
self.writeStderr("Antigravity plan info:\n")
for (label, value) in fields {
guard let value, !value.isEmpty else { continue }
self.writeStderr(" \(label): \(value)\n")
}
}
static func loadConfig(output: CLIOutputPreferences) -> CodexBarConfig {
let store = CodexBarConfigStore()
do {
if let existing = try store.load() {
return existing
}
return CodexBarConfig.makeDefault()
} catch {
if output.usesJSONOutput {
let payload = ProviderPayload(
providerID: "cli",
account: nil,
version: nil,
source: "cli",
status: nil,
usage: nil,
credits: nil,
antigravityPlanInfo: nil,
openaiDashboard: nil,
error: self.makeErrorPayload(code: .failure, message: error.localizedDescription, kind: .config))
self.printJSON([payload], pretty: output.pretty)
} else {
self.writeStderr("Error: \(error.localizedDescription)\n")
}
Self.platformExit(ExitCode.failure.rawValue)
}
}
}
struct CLIArgumentError: LocalizedError {
let message: String
init(_ message: String) {
self.message = message
}
var errorDescription: String? {
self.message
}
}
#if DEBUG
extension CodexBarCLI {
static func _usageSignatureForTesting() -> CommandSignature {
CommandSignature.describe(UsageOptions())
}
static func _costSignatureForTesting() -> CommandSignature {
CommandSignature.describe(CostOptions())
}
static func _cacheSignatureForTesting() -> CommandSignature {
CommandSignature.describe(CacheOptions())
}
static func _diagnoseSignatureForTesting() -> CommandSignature {
CommandSignature.describe(DiagnoseOptions())
}
static func _configSetAPIKeySignatureForTesting() -> CommandSignature {
CommandSignature.describe(ConfigSetAPIKeyOptions())
}
static func _configProviderToggleSignatureForTesting() -> CommandSignature {
CommandSignature.describe(ConfigProviderToggleOptions())
}
static func _decodeFormatForTesting(from values: ParsedValues) -> OutputFormat {
self.decodeFormat(from: values)
}
static func _decodeWebTimeoutForTesting(from values: ParsedValues) throws -> TimeInterval? {
try self.decodeWebTimeout(from: values)
}
static func _decodeSourceModeForTesting(from values: ParsedValues) -> ProviderSourceMode? {
self.decodeSourceMode(from: values)
}
}
#endif
+127
View File
@@ -0,0 +1,127 @@
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
import Foundation
extension CodexBarCLI {
static func writeStderr(_ string: String) {
guard let data = string.data(using: .utf8) else { return }
FileHandle.standardError.write(data)
}
static func printVersion() -> Never {
if let version = currentVersion() {
print("CodexBar \(version)")
} else {
print("CodexBar")
}
Self.platformExit(0)
}
static func printHelp(for command: String?) -> Never {
let version = self.currentVersion() ?? "unknown"
switch command {
case "cards":
print(Self.cardsHelp(version: version))
case "usage":
print(Self.usageHelp(version: version))
case "cost":
print(Self.costHelp(version: version))
case "sessions", "focus":
print(Self.sessionsHelp(version: version))
case "serve":
print(Self.serveHelp(version: version))
case "config", "validate", "dump":
print(Self.configHelp(version: version))
case "cache", "clear":
print(Self.cacheHelp(version: version))
case "diagnose":
print(Self.diagnoseHelp(version: version))
default:
print(Self.rootHelp(version: version))
}
Self.platformExit(0)
}
static func currentVersion(
bundle: Bundle = .main,
executablePath: String? = CommandLine.arguments.first) -> String?
{
if let version = self.currentVersion(bundleVersion: nil, executablePath: executablePath) {
return version
}
return self.currentVersion(
bundleVersion: bundle.infoDictionary?["CFBundleShortVersionString"] as? String,
executablePath: nil)
}
static func currentVersion(bundleVersion: String?, executablePath: String?) -> String? {
if let executablePath, !executablePath.isEmpty {
let executableURL = URL(fileURLWithPath: executablePath).resolvingSymlinksInPath()
if let version = Self.adjacentVersionFileVersion(for: executableURL) {
return version
}
if let version = Self.containingAppVersion(for: executableURL) {
return version
}
}
return Self.normalizedBundleVersion(bundleVersion)
}
static func containingAppVersion(for executableURL: URL) -> String? {
var currentURL = executableURL.deletingLastPathComponent()
let fileManager = FileManager.default
while currentURL.path != currentURL.deletingLastPathComponent().path {
if currentURL.pathExtension == "app" {
let infoURL = currentURL
.appendingPathComponent("Contents")
.appendingPathComponent("Info.plist")
guard let data = fileManager.contents(atPath: infoURL.path),
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]
else { return nil }
return plist["CFBundleShortVersionString"] as? String
}
currentURL.deleteLastPathComponent()
}
return nil
}
static func adjacentVersionFileVersion(for executableURL: URL) -> String? {
let versionURL = executableURL
.deletingLastPathComponent()
.appendingPathComponent("VERSION")
guard let raw = try? String(contentsOf: versionURL, encoding: .utf8) else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
if trimmed.hasPrefix("v"), trimmed.dropFirst().first?.isNumber == true {
return String(trimmed.dropFirst())
}
return trimmed
}
static func normalizedBundleVersion(_ raw: String?) -> String? {
guard let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty,
trimmed != "CodexBar"
else { return nil }
return trimmed
}
static func platformExit(_ code: Int32) -> Never {
#if canImport(Darwin)
Darwin.exit(code)
#elseif canImport(Glibc)
Glibc.exit(code)
#elseif canImport(Musl)
Musl.exit(code)
#endif
}
}
@@ -0,0 +1,433 @@
import Foundation
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
private let requestReadTimeoutMilliseconds: Int32 = 5000
struct CLILocalHTTPRequest {
let method: String
let target: String
let host: String
let path: String
let queryItems: [String: String]
static func parse(_ data: Data) -> Result<CLILocalHTTPRequest, CLILocalHTTPRequestParseError> {
guard let raw = String(data: data, encoding: .utf8),
let firstLine = raw.components(separatedBy: "\r\n").first
else {
return .failure(.invalidRequest)
}
let parts = firstLine.split(separator: " ")
guard parts.count >= 3 else { return .failure(.invalidRequest) }
let method = String(parts[0]).uppercased()
let target = String(parts[1])
guard target.hasPrefix("/") else { return .failure(.invalidRequest) }
let headerResult = Self.parseHeaders(raw)
let host: String
switch headerResult {
case let .success(headers):
let hosts = headers.compactMap { name, value in
name.lowercased() == "host" ? value : nil
}
guard let candidate = hosts.first else { return .failure(.missingHost) }
guard hosts.count == 1 else { return .failure(.duplicateHost) }
guard Self.isAllowedLoopbackHost(candidate) else { return .failure(.disallowedHost) }
host = candidate
case let .failure(error):
return .failure(error)
}
let components = URLComponents(string: "http://localhost\(target)")
let path = components?.path ?? target
var queryItems: [String: String] = [:]
for item in components?.queryItems ?? [] {
if let value = item.value {
queryItems[item.name] = value
}
}
return .success(CLILocalHTTPRequest(
method: method,
target: target,
host: host,
path: path,
queryItems: queryItems))
}
private static func parseHeaders(_ raw: String) -> Result<[(String, String)], CLILocalHTTPRequestParseError> {
let lines = raw.components(separatedBy: "\r\n")
var headers: [(String, String)] = []
for line in lines.dropFirst() {
if line.isEmpty { break }
guard let separator = line.firstIndex(of: ":") else {
return .failure(.invalidRequest)
}
let name = String(line[..<separator]).trimmingCharacters(in: .whitespacesAndNewlines)
let value = String(line[line.index(after: separator)...]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !name.isEmpty else { return .failure(.invalidRequest) }
headers.append((name, value))
}
return .success(headers)
}
private static func isAllowedLoopbackHost(_ host: String) -> Bool {
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, !trimmed.contains(",") else { return false }
let hostWithoutPort: String
if trimmed.hasPrefix("[") {
guard let closingBracket = trimmed.firstIndex(of: "]") else { return false }
hostWithoutPort = String(trimmed[...closingBracket])
let remainder = trimmed[trimmed.index(after: closingBracket)...]
guard remainder.isEmpty || Self.isValidPortSuffix(String(remainder)) else { return false }
} else {
let segments = trimmed.split(separator: ":", omittingEmptySubsequences: false)
switch segments.count {
case 1:
hostWithoutPort = String(segments[0])
case 2:
guard Self.isValidPort(String(segments[1])) else { return false }
hostWithoutPort = String(segments[0])
default:
return false
}
}
switch hostWithoutPort.lowercased() {
case "127.0.0.1", "localhost", "localhost.", "[::1]":
return true
default:
return false
}
}
private static func isValidPortSuffix(_ raw: String) -> Bool {
guard raw.hasPrefix(":") else { return false }
return self.isValidPort(String(raw.dropFirst()))
}
private static func isValidPort(_ raw: String) -> Bool {
guard let port = Int(raw), port > 0, port <= Int(UInt16.max) else { return false }
return true
}
}
enum CLILocalHTTPRequestParseError: Error, Equatable {
case invalidRequest
case missingHost
case duplicateHost
case disallowedHost
}
enum CLIHTTPStatus {
case ok
case badRequest
case forbidden
case notFound
case methodNotAllowed
case internalServerError
case gatewayTimeout
var code: Int {
switch self {
case .ok: 200
case .badRequest: 400
case .forbidden: 403
case .notFound: 404
case .methodNotAllowed: 405
case .internalServerError: 500
case .gatewayTimeout: 504
}
}
var reason: String {
switch self {
case .ok: "OK"
case .badRequest: "Bad Request"
case .forbidden: "Forbidden"
case .notFound: "Not Found"
case .methodNotAllowed: "Method Not Allowed"
case .internalServerError: "Internal Server Error"
case .gatewayTimeout: "Gateway Timeout"
}
}
}
struct CLILocalHTTPResponse {
let status: CLIHTTPStatus
let body: Data
let contentType: String
let usageCacheKeys: [String?]?
init(
status: CLIHTTPStatus,
body: Data,
contentType: String = "application/json; charset=utf-8",
usageCacheKeys: [String?]? = nil)
{
self.status = status
self.body = body
self.contentType = contentType
self.usageCacheKeys = usageCacheKeys
}
var serialized: Data {
var headers = "HTTP/1.1 \(self.status.code) \(self.status.reason)\r\n"
headers += "Content-Type: \(self.contentType)\r\n"
headers += "Content-Length: \(self.body.count)\r\n"
headers += "Connection: close\r\n"
headers += "\r\n"
var data = Data(headers.utf8)
data.append(self.body)
return data
}
}
final class CLILocalHTTPServer: @unchecked Sendable {
typealias Handler = @Sendable (CLILocalHTTPRequest) async -> CLILocalHTTPResponse
private let host: String
private let port: UInt16
private let handler: Handler
private let stateLock = NSLock()
private var listeningFD: Int32?
private var stopRequested = false
init(host: String, port: UInt16, handler: @escaping Handler) {
self.host = host
self.port = port
self.handler = handler
}
func stop() {
self.stateLock.lock()
self.stopRequested = true
self.stateLock.unlock()
}
func run(onListening: @Sendable () -> Void = {}) async throws {
ignoreSIGPIPE()
#if canImport(Darwin)
let streamType = SOCK_STREAM
#elseif canImport(Glibc)
let streamType = Int32(SOCK_STREAM.rawValue)
#elseif canImport(Musl)
let streamType = Int32(SOCK_STREAM)
#endif
let serverFD = socket(AF_INET, streamType, 0)
guard serverFD >= 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
var ownsServerFD = true
defer {
if ownsServerFD {
closeSocket(serverFD)
}
}
var reuse: Int32 = 1
setsockopt(
serverFD,
SOL_SOCKET,
SO_REUSEADDR,
&reuse,
socklen_t(MemoryLayout<Int32>.size))
var address = sockaddr_in()
#if canImport(Darwin)
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
#endif
address.sin_family = sa_family_t(AF_INET)
address.sin_port = self.port.bigEndian
guard inet_pton(AF_INET, self.host, &address.sin_addr) == 1 else {
throw POSIXError(.EADDRNOTAVAIL)
}
let bound = withUnsafePointer(to: &address) { pointer in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { socketAddress in
bind(serverFD, socketAddress, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}
guard bound == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
guard listen(serverFD, 16) == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
guard self.installListeningFD(serverFD) else {
return
}
ownsServerFD = false
defer {
if self.releaseListeningFD(serverFD) {
closeSocket(serverFD)
}
}
onListening()
while !self.isStopRequested {
guard waitForReadable(serverFD, timeoutMilliseconds: 250) else {
continue
}
var clientAddress = sockaddr()
var clientLength = socklen_t(MemoryLayout<sockaddr>.size)
let clientFD = accept(serverFD, &clientAddress, &clientLength)
guard clientFD >= 0 else {
if self.isStopRequested { return }
if errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED {
continue
}
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
let handler = self.handler
Task {
defer { closeSocket(clientFD) }
await handleClient(clientFD, handler: handler)
}
}
}
private var isStopRequested: Bool {
self.stateLock.lock()
let value = self.stopRequested
self.stateLock.unlock()
return value
}
private func installListeningFD(_ fd: Int32) -> Bool {
self.stateLock.lock()
defer { self.stateLock.unlock() }
guard !self.stopRequested else { return false }
self.listeningFD = fd
return true
}
private func releaseListeningFD(_ fd: Int32) -> Bool {
self.stateLock.lock()
defer { self.stateLock.unlock() }
guard self.listeningFD == fd else { return false }
self.listeningFD = nil
return true
}
}
private func handleClient(
_ clientFD: Int32,
handler: @Sendable (CLILocalHTTPRequest) async -> CLILocalHTTPResponse) async
{
let request: CLILocalHTTPRequest
switch readRequest(clientFD) {
case let .success(parsedRequest):
request = parsedRequest
case .failure(.disallowedHost):
sendResponse(
CLILocalHTTPResponse(
status: .forbidden,
body: Data(#"{"error":"forbidden host"}"#.utf8)),
to: clientFD)
return
case .failure:
sendResponse(
CLILocalHTTPResponse(
status: .badRequest,
body: Data(#"{"error":"invalid request"}"#.utf8)),
to: clientFD)
return
}
let response = await handler(request)
sendResponse(response, to: clientFD)
}
private func readRequest(_ fd: Int32) -> Result<CLILocalHTTPRequest, CLILocalHTTPRequestParseError> {
var data = Data()
var buffer = [UInt8](repeating: 0, count: 4096)
let bufferSize = buffer.count
var sawHeaderEnd = false
while data.count < 16384 {
guard waitForReadable(fd, timeoutMilliseconds: requestReadTimeoutMilliseconds) else {
return .failure(.invalidRequest)
}
let count = buffer.withUnsafeMutableBytes { rawBuffer in
recv(fd, rawBuffer.baseAddress, bufferSize, 0)
}
guard count > 0 else { break }
data.append(buffer, count: count)
if data.range(of: Data("\r\n\r\n".utf8)) != nil {
sawHeaderEnd = true
break
}
}
guard sawHeaderEnd else { return .failure(.invalidRequest) }
return CLILocalHTTPRequest.parse(data)
}
private func sendResponse(_ response: CLILocalHTTPResponse, to fd: Int32) {
let data = response.serialized
data.withUnsafeBytes { rawBuffer in
guard let base = rawBuffer.baseAddress else { return }
var sent = 0
while sent < data.count {
let count = send(fd, base.advanced(by: sent), data.count - sent, sendNoSignalFlags())
guard count > 0 else { break }
sent += count
}
}
}
private func waitForReadable(_ fd: Int32, timeoutMilliseconds: Int32) -> Bool {
var pollFD = pollfd(fd: fd, events: Int16(POLLIN), revents: 0)
while true {
let result = poll(&pollFD, 1, timeoutMilliseconds)
if result > 0 {
return (pollFD.revents & Int16(POLLIN)) != 0
}
if result == -1, errno == EINTR {
continue
}
return false
}
}
private func sendNoSignalFlags() -> Int32 {
#if canImport(Darwin)
0
#else
Int32(MSG_NOSIGNAL)
#endif
}
private func ignoreSIGPIPE() {
#if canImport(Darwin)
_ = Darwin.signal(SIGPIPE, SIG_IGN)
#elseif canImport(Glibc)
_ = Glibc.signal(SIGPIPE, SIG_IGN)
#elseif canImport(Musl)
_ = Musl.signal(SIGPIPE, SIG_IGN)
#endif
}
private func closeSocket(_ fd: Int32) {
#if canImport(Darwin)
Darwin.close(fd)
#elseif canImport(Glibc)
Glibc.close(fd)
#elseif canImport(Musl)
Musl.close(fd)
#endif
}
+145
View File
@@ -0,0 +1,145 @@
import CodexBarCore
import Commander
import Foundation
// MARK: - Options & parsing helpers
struct UsageOptions: CommanderParsable {
private static let sourceHelp: String = {
#if os(macOS)
"Data source: auto | web | cli | oauth | api (auto behavior is provider-specific)"
#else
"Data source: auto | web | cli | oauth | api (web/auto are macOS only for web-capable providers)"
#endif
}()
@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("account"), help: "Token account label to use (from config.json)")
var account: String?
@Option(name: .long("account-index"), help: "Token account index (1-based)")
var accountIndex: Int?
@Flag(name: .long("all-accounts"), help: "Fetch all token accounts, or all visible Codex accounts")
var allAccounts: Bool = false
@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("no-credits"), help: "Skip Codex credits line")
var noCredits: Bool = false
@Flag(name: .long("no-color"), help: "Disable ANSI colors in text output")
var noColor: Bool = false
@Flag(name: .long("pretty"), help: "Pretty-print JSON output")
var pretty: Bool = false
@Flag(name: .long("status"), help: "Fetch and include provider status")
var status: Bool = false
@Flag(name: .long("web"), help: "Alias for --source web")
var web: Bool = false
@Option(name: .long("source"), help: Self.sourceHelp)
var source: String?
@Option(name: .long("web-timeout"), help: "Web fetch timeout (seconds; source=auto or web)")
var webTimeout: Double?
@Flag(name: .long("web-debug-dump-html"), help: "Dump HTML snapshots to /tmp when Codex dashboard data is missing")
var webDebugDumpHtml: Bool = false
@Flag(name: .long("antigravity-plan-debug"), help: "Emit Antigravity planInfo fields (debug)")
var antigravityPlanDebug: Bool = false
@Flag(name: .long("augment-debug"), help: "Emit Augment API responses (debug)")
var augmentDebug: Bool = false
}
enum ProviderSelection: ExpressibleFromArgument {
case single(UsageProvider)
case both
case all
case custom([UsageProvider])
init?(argument: String) {
let normalized = argument.lowercased()
switch normalized {
case "both":
self = .both
case "all":
self = .all
default:
if let provider = ProviderDescriptorRegistry.cliNameMap[normalized] {
self = .single(provider)
} else {
return nil
}
}
}
init(provider: UsageProvider) {
self = .single(provider)
}
var asList: [UsageProvider] {
switch self {
case let .single(provider):
return [provider]
case .both:
let primary = ProviderDescriptorRegistry.all.filter(\ .metadata.isPrimaryProvider)
if !primary.isEmpty {
return primary.map(\ .id)
}
return ProviderDescriptorRegistry.all.prefix(2).map(\ .id)
case .all:
return ProviderDescriptorRegistry.all.map(\ .id)
case let .custom(providers):
return providers
}
}
}
enum OutputFormat: String, ExpressibleFromArgument {
case text
case json
init?(argument: String) {
switch argument.lowercased() {
case "text": self = .text
case "json": self = .json
default: return nil
}
}
}
enum ProviderHelp {
static var list: String {
let names = ProviderDescriptorRegistry.all.map(\ .cli.name)
return (names + ["both", "all"]).joined(separator: "|")
}
static var optionHelp: String {
"Provider to query: \(self.list)"
}
}
@@ -0,0 +1,50 @@
import Commander
import Foundation
struct CLIOutputPreferences {
let format: OutputFormat
let jsonOnly: Bool
let pretty: Bool
var usesJSONOutput: Bool {
self.jsonOnly || self.format == .json
}
static func from(values: ParsedValues) -> CLIOutputPreferences {
let jsonOnly = values.flags.contains("jsonOnly")
let format = CodexBarCLI.decodeFormat(from: values)
let pretty = values.flags.contains("pretty")
return CLIOutputPreferences(format: format, jsonOnly: jsonOnly, pretty: pretty)
}
static func from(argv: [String]) -> CLIOutputPreferences {
var jsonOnly = false
var pretty = false
var format: OutputFormat = .text
var index = 0
while index < argv.count {
let arg = argv[index]
switch arg {
case "--json-only":
jsonOnly = true
format = .json
case "--json":
format = .json
case "--pretty":
pretty = true
case "--format":
let next = index + 1
if next < argv.count, let parsed = OutputFormat(argument: argv[next]) {
format = parsed
index += 1
}
default:
break
}
index += 1
}
return CLIOutputPreferences(format: format, jsonOnly: jsonOnly, pretty: pretty)
}
}
+187
View File
@@ -0,0 +1,187 @@
import CodexBarCore
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct ProviderPayload: Encodable {
let provider: String
let account: String?
let cacheAccountKey: String?
let version: String?
let source: String
let status: ProviderStatusPayload?
let usage: UsageSnapshot?
let credits: CreditsSnapshot?
let antigravityPlanInfo: AntigravityPlanInfoSummary?
let openaiDashboard: OpenAIDashboardSnapshot?
let error: ProviderErrorPayload?
let pace: ProviderPacePayload?
private enum CodingKeys: String, CodingKey {
case provider
case account
case version
case source
case status
case usage
case credits
case antigravityPlanInfo
case openaiDashboard
case error
case pace
}
init(
provider: UsageProvider,
account: String?,
cacheAccountKey: String? = nil,
version: String?,
source: String,
status: ProviderStatusPayload?,
usage: UsageSnapshot?,
credits: CreditsSnapshot?,
antigravityPlanInfo: AntigravityPlanInfoSummary?,
openaiDashboard: OpenAIDashboardSnapshot?,
error: ProviderErrorPayload?,
pace: ProviderPacePayload? = nil)
{
self.provider = provider.rawValue
self.account = account
self.cacheAccountKey = cacheAccountKey
self.version = version
self.source = source
self.status = status
self.usage = usage
self.credits = credits
self.antigravityPlanInfo = antigravityPlanInfo
self.openaiDashboard = openaiDashboard
self.error = error
self.pace = pace
}
init(
providerID: String,
account: String?,
cacheAccountKey: String? = nil,
version: String?,
source: String,
status: ProviderStatusPayload?,
usage: UsageSnapshot?,
credits: CreditsSnapshot?,
antigravityPlanInfo: AntigravityPlanInfoSummary?,
openaiDashboard: OpenAIDashboardSnapshot?,
error: ProviderErrorPayload?,
pace: ProviderPacePayload? = nil)
{
self.provider = providerID
self.account = account
self.cacheAccountKey = cacheAccountKey
self.version = version
self.source = source
self.status = status
self.usage = usage
self.credits = credits
self.antigravityPlanInfo = antigravityPlanInfo
self.openaiDashboard = openaiDashboard
self.error = error
self.pace = pace
}
}
struct ProviderPacePayload: Encodable {
let primary: PacePayload?
let secondary: PacePayload?
}
struct PacePayload: Encodable {
let stage: String
/// Rounded (used expected); positive = deficit, negative = reserve.
let deltaPercent: Double
let expectedUsedPercent: Double
let willLastToReset: Bool
let etaSeconds: TimeInterval?
/// Always absent in CLI output; kept for schema parity.
let runOutProbability: Double?
let summary: String
}
struct ProviderStatusPayload: Encodable {
let indicator: ProviderStatusIndicator
let description: String?
let updatedAt: Date?
let url: String
enum ProviderStatusIndicator: String, Encodable {
case none
case minor
case major
case critical
case maintenance
case unknown
var label: String {
switch self {
case .none: "Operational"
case .minor: "Partial outage"
case .major: "Major outage"
case .critical: "Critical issue"
case .maintenance: "Maintenance"
case .unknown: "Status unknown"
}
}
}
var descriptionSuffix: String {
guard let description, !description.isEmpty else { return "" }
return " \(description)"
}
}
enum StatusFetcher {
static func fetch(from baseURL: URL) async throws -> ProviderStatusPayload {
let apiURL = baseURL.appendingPathComponent("api/v2/status.json")
var request = URLRequest(url: apiURL)
request.timeoutInterval = 10
let (data, _) = try await URLSession.shared.data(for: request)
struct Response: Decodable {
struct Status: Decodable {
let indicator: String
let description: String?
}
struct Page: Decodable {
let updatedAt: Date?
private enum CodingKeys: String, CodingKey {
case updatedAt = "updated_at"
}
}
let page: Page?
let status: Status
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let raw = try container.decode(String.self)
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: raw) { return date }
formatter.formatOptions = [.withInternetDateTime]
if let date = formatter.date(from: raw) { return date }
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid ISO8601 date")
}
let response = try decoder.decode(Response.self, from: data)
let indicator = ProviderStatusPayload.ProviderStatusIndicator(rawValue: response.status.indicator) ?? .unknown
return ProviderStatusPayload(
indicator: indicator,
description: response.status.description,
updatedAt: response.page?.updatedAt,
url: baseURL.absoluteString)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,551 @@
import Foundation
/// Owns at most one running source operation for each logical serve key.
///
/// Cancellation is advisory in Swift. Timed-out work therefore remains owned
/// until its source body actually exits. A config successor can wait behind it,
/// but can never overlap it.
actor CLIServeOperationCoordinator<Value: Sendable> {
typealias Instant = ContinuousClock.Instant
typealias Now = @Sendable () -> Instant
typealias SleepUntil = @Sendable (Instant) async throws -> Void
struct Snapshot: Equatable, Sendable {
let operationCount: Int
let waiterCount: Int
let timerCount: Int
let isShutDown: Bool
}
private struct Waiter {
let id: UUID
let continuation: CheckedContinuation<Value, Never>
let timeoutValue: Value
}
private enum OperationPhase {
case running
case accepting
case timedOut
}
private struct Operation {
let generation: UInt64
let fingerprint: String
var deadline: Instant?
let makeValue: @Sendable () async -> Value
let acceptValue: @Sendable (Value) async -> Value
var sourceTask: Task<Void, Never>?
var deadlineTask: Task<Void, Never>?
var waiters: [Waiter]
var phase: OperationPhase
}
private struct Slot {
var active: Operation?
var pending: Operation?
}
private struct OperationRequest: Sendable {
let key: String
let fingerprint: String
let deadline: Instant?
let timeoutValue: Value
let makeValue: @Sendable () async -> Value
let acceptValue: @Sendable (Value) async -> Value
}
private enum WaitTarget {
case active
case pending
}
private let now: Now
private let sleepUntil: SleepUntil
// Callers validate route keys and provider keys come from UsageProvider, so
// retained canceled work has a small, closed key space.
private var slots: [String: Slot] = [:]
private var nextGeneration: UInt64 = 0
private var isShutDown = false
init(
now: @escaping Now = { ContinuousClock().now },
sleepUntil: @escaping SleepUntil = { deadline in
try await ContinuousClock().sleep(until: deadline)
})
{
self.now = now
self.sleepUntil = sleepUntil
}
func value(
for key: String,
fingerprint: String,
deadline: Instant?,
timeoutValue: Value,
accept: @Sendable @escaping (Value) async -> Value = { $0 },
operation makeValue: @Sendable @escaping () async -> Value) async -> Value
{
let request = OperationRequest(
key: key,
fingerprint: fingerprint,
deadline: deadline,
timeoutValue: timeoutValue,
makeValue: makeValue,
acceptValue: accept)
guard !self.isShutDown, !self.isExpired(deadline) else { return timeoutValue }
self.expireOverdueOperations(for: key)
guard let slot = self.slots[key], let active = slot.active else {
return await self.installActive(request)
}
if active.fingerprint == fingerprint, active.phase != .timedOut {
guard self.deadlinesAreCompatible(active.deadline, deadline) else { return timeoutValue }
guard self.canJoinAccepting(active, deadline: deadline) else { return timeoutValue }
return await self.awaitValue(for: request, target: .active)
}
if let pending = slot.pending, pending.fingerprint == fingerprint {
guard self.deadlinesAreCompatible(pending.deadline, deadline) else { return timeoutValue }
return await self.awaitValue(for: request, target: .pending)
}
if let pending = self.slots[key]?.pending {
self.timeoutPending(for: key, generation: pending.generation)
}
return await self.installPending(request)
}
func shutdown() {
guard !self.isShutDown else { return }
self.isShutDown = true
let active = self.slots.compactMap { key, slot in
slot.active.map { (key, $0.generation) }
}
let pending = self.slots.compactMap { key, slot in
slot.pending.map { (key, $0.generation) }
}
for (key, generation) in pending {
self.timeoutPending(for: key, generation: generation)
}
for (key, generation) in active {
self.shutdownActive(for: key, generation: generation)
}
}
func snapshot() -> Snapshot {
let operations = self.slots.values.flatMap { slot in
[slot.active, slot.pending].compactMap(\.self)
}
return Snapshot(
operationCount: operations.count,
waiterCount: operations.reduce(0) { $0 + $1.waiters.count },
timerCount: operations.reduce(0) { $0 + ($1.deadlineTask == nil ? 0 : 1) },
isShutDown: self.isShutDown)
}
private func allocateGeneration() -> UInt64 {
self.nextGeneration &+= 1
return self.nextGeneration
}
private func installActive(_ request: OperationRequest) async -> Value {
let generation = self.allocateGeneration()
let waiterID = UUID()
return await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
guard !Task.isCancelled, !self.isShutDown, !self.isExpired(request.deadline) else {
continuation.resume(returning: request.timeoutValue)
return
}
var operation = self.makeOperation(
request,
generation: generation,
waiter: Waiter(
id: waiterID,
continuation: continuation,
timeoutValue: request.timeoutValue))
operation.sourceTask = self.makeSourceTask(
key: request.key,
generation: generation,
makeValue: request.makeValue)
operation.deadlineTask = self.makeDeadlineTask(
key: request.key,
generation: generation,
deadline: request.deadline)
self.slots[request.key] = Slot(active: operation, pending: nil)
}
} onCancel: {
Task { await self.cancelWaiter(id: waiterID, for: request.key) }
}
}
private func installPending(_ request: OperationRequest) async -> Value {
guard self.slots[request.key]?.active != nil else {
return await self.installActive(request)
}
let generation = self.allocateGeneration()
let waiterID = UUID()
return await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
guard !Task.isCancelled, !self.isShutDown, !self.isExpired(request.deadline) else {
continuation.resume(returning: request.timeoutValue)
return
}
guard var slot = self.slots[request.key], slot.active != nil else {
continuation.resume(returning: request.timeoutValue)
return
}
var operation = self.makeOperation(
request,
generation: generation,
waiter: Waiter(
id: waiterID,
continuation: continuation,
timeoutValue: request.timeoutValue))
operation.deadlineTask = self.makeDeadlineTask(
key: request.key,
generation: generation,
deadline: request.deadline)
slot.pending = operation
self.slots[request.key] = slot
}
} onCancel: {
Task { await self.cancelWaiter(id: waiterID, for: request.key) }
}
}
private func awaitValue(for request: OperationRequest, target: WaitTarget) async -> Value {
let waiterID = UUID()
return await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
guard !Task.isCancelled, !self.isShutDown, !self.isExpired(request.deadline),
var slot = self.slots[request.key]
else {
continuation.resume(returning: request.timeoutValue)
return
}
let waiter = Waiter(
id: waiterID,
continuation: continuation,
timeoutValue: request.timeoutValue)
switch target {
case .active:
guard var operation = slot.active,
operation.fingerprint == request.fingerprint,
operation.phase != .timedOut
else {
continuation.resume(returning: request.timeoutValue)
return
}
guard self.canJoinAccepting(operation, deadline: request.deadline) else {
continuation.resume(returning: request.timeoutValue)
return
}
self.tightenDeadline(&operation, to: request.deadline, for: request.key)
operation.waiters.append(waiter)
slot.active = operation
case .pending:
guard var operation = slot.pending,
operation.fingerprint == request.fingerprint
else {
continuation.resume(returning: request.timeoutValue)
return
}
self.tightenDeadline(&operation, to: request.deadline, for: request.key)
operation.waiters.append(waiter)
slot.pending = operation
}
self.slots[request.key] = slot
}
} onCancel: {
Task { await self.cancelWaiter(id: waiterID, for: request.key) }
}
}
private func makeOperation(
_ request: OperationRequest,
generation: UInt64,
waiter: Waiter) -> Operation
{
Operation(
generation: generation,
fingerprint: request.fingerprint,
deadline: request.deadline,
makeValue: request.makeValue,
acceptValue: request.acceptValue,
sourceTask: nil,
deadlineTask: nil,
waiters: [waiter],
phase: .running)
}
private func makeSourceTask(
key: String,
generation: UInt64,
makeValue: @Sendable @escaping () async -> Value) -> Task<Void, Never>
{
Task.detached { [self] in
let value = await makeValue()
await self.sourceProduced(value, for: key, generation: generation)
}
}
private func makeDeadlineTask(
key: String,
generation: UInt64,
deadline: Instant?) -> Task<Void, Never>?
{
guard let deadline else { return nil }
let sleepUntil = self.sleepUntil
return Task.detached { [self] in
do {
try await sleepUntil(deadline)
} catch {
return
}
await self.deadlineReached(for: key, generation: generation)
}
}
private func cancelWaiter(id: UUID, for key: String) {
guard var slot = self.slots[key] else { return }
if var active = slot.active,
let index = active.waiters.firstIndex(where: { $0.id == id })
{
let waiter = active.waiters.remove(at: index)
slot.active = active
self.slots[key] = slot
waiter.continuation.resume(returning: waiter.timeoutValue)
if active.waiters.isEmpty, active.phase == .running {
self.timeoutActive(for: key, generation: active.generation)
}
return
}
guard var pending = slot.pending,
let index = pending.waiters.firstIndex(where: { $0.id == id })
else {
return
}
let waiter = pending.waiters.remove(at: index)
waiter.continuation.resume(returning: waiter.timeoutValue)
if pending.waiters.isEmpty {
pending.deadlineTask?.cancel()
slot.pending = nil
} else {
slot.pending = pending
}
self.store(slot, for: key)
}
private func deadlineReached(for key: String, generation: UInt64) {
if self.slots[key]?.active?.generation == generation {
self.timeoutActive(for: key, generation: generation)
} else if self.slots[key]?.pending?.generation == generation {
self.timeoutPending(for: key, generation: generation)
}
}
private func expireOverdueOperations(for key: String) {
if let active = self.slots[key]?.active,
active.phase == .running,
self.isExpired(active.deadline)
{
self.timeoutActive(for: key, generation: active.generation)
}
if let pending = self.slots[key]?.pending,
self.isExpired(pending.deadline)
{
self.timeoutPending(for: key, generation: pending.generation)
}
}
private func timeoutActive(for key: String, generation: UInt64) {
guard var slot = self.slots[key],
var operation = slot.active,
operation.generation == generation,
operation.phase == .running
else {
return
}
operation.phase = .timedOut
operation.sourceTask?.cancel()
operation.deadlineTask?.cancel()
operation.deadlineTask = nil
let waiters = operation.waiters
operation.waiters.removeAll()
slot.active = operation
self.slots[key] = slot
for waiter in waiters {
waiter.continuation.resume(returning: waiter.timeoutValue)
}
}
private func shutdownActive(for key: String, generation: UInt64) {
guard var slot = self.slots[key],
var operation = slot.active,
operation.generation == generation
else {
return
}
if operation.phase == .running {
self.timeoutActive(for: key, generation: generation)
return
}
guard operation.phase == .accepting else { return }
operation.phase = .timedOut
operation.sourceTask?.cancel()
let waiters = operation.waiters
operation.waiters.removeAll()
slot.active = operation
self.slots[key] = slot
for waiter in waiters {
waiter.continuation.resume(returning: waiter.timeoutValue)
}
}
private func timeoutPending(for key: String, generation: UInt64) {
guard var slot = self.slots[key],
let operation = slot.pending,
operation.generation == generation
else {
return
}
operation.deadlineTask?.cancel()
slot.pending = nil
self.store(slot, for: key)
for waiter in operation.waiters {
waiter.continuation.resume(returning: waiter.timeoutValue)
}
}
private func sourceProduced(_ value: Value, for key: String, generation: UInt64) async {
guard var slot = self.slots[key],
let observed = slot.active,
observed.generation == generation
else {
return
}
if observed.phase == .running, self.isExpired(observed.deadline) {
self.timeoutActive(for: key, generation: generation)
guard let refreshed = self.slots[key] else { return }
slot = refreshed
}
guard var operation = slot.active, operation.generation == generation else { return }
if operation.phase == .timedOut {
slot.active = nil
self.store(slot, for: key)
self.promotePending(for: key)
return
}
// Source completion wins the deadline only on this actor turn. `accept`
// is restricted to bounded in-process projection/cache work: keep the
// slot owned through that commit so a newer generation cannot start and
// then be overwritten by this older result. Shutdown still releases the
// waiters even if a commit is briefly queued on another actor.
operation.phase = .accepting
operation.deadlineTask?.cancel()
operation.deadlineTask = nil
slot.active = operation
self.slots[key] = slot
let acceptedValue = await operation.acceptValue(value)
guard var refreshed = self.slots[key],
let completed = refreshed.active,
completed.generation == generation
else {
return
}
refreshed.active = nil
self.store(refreshed, for: key)
if completed.phase == .accepting {
for waiter in completed.waiters {
waiter.continuation.resume(returning: acceptedValue)
}
}
self.promotePending(for: key)
}
private func promotePending(for key: String) {
guard !self.isShutDown,
var slot = self.slots[key],
slot.active == nil,
var pending = slot.pending
else {
return
}
if self.isExpired(pending.deadline) {
self.timeoutPending(for: key, generation: pending.generation)
return
}
pending.sourceTask = self.makeSourceTask(
key: key,
generation: pending.generation,
makeValue: pending.makeValue)
slot.active = pending
slot.pending = nil
self.slots[key] = slot
}
private func tightenDeadline(_ operation: inout Operation, to requested: Instant?, for key: String) {
guard operation.phase == .running,
let current = operation.deadline,
let requested,
requested < current
else {
return
}
operation.deadlineTask?.cancel()
operation.deadline = requested
operation.deadlineTask = self.makeDeadlineTask(
key: key,
generation: operation.generation,
deadline: requested)
}
private func deadlinesAreCompatible(_ active: Instant?, _ requested: Instant?) -> Bool {
(active == nil) == (requested == nil)
}
private func canJoinAccepting(_ operation: Operation, deadline: Instant?) -> Bool {
guard operation.phase == .accepting,
let activeDeadline = operation.deadline,
let deadline
else {
return true
}
// Source acceptance already canceled the shared timer. An older-budget
// follower cannot safely shorten that completed arbitration while the
// bounded cache commit is in progress, so fail it closed.
return deadline >= activeDeadline
}
private func isExpired(_ deadline: Instant?) -> Bool {
guard let deadline else { return false }
return deadline <= self.now()
}
private func store(_ slot: Slot, for key: String) {
if slot.active == nil, slot.pending == nil {
self.slots[key] = nil
} else {
self.slots[key] = slot
}
}
}
@@ -0,0 +1,94 @@
import CodexBarCore
import Commander
import Foundation
extension CodexBarCLI {
static func runSessions(_ values: ParsedValues) async {
let sessions = await LocalAgentSessionScanner().scan()
if values.flags.contains("jsonShortcut") {
Self.printJSON(sessions, pretty: values.flags.contains("pretty"))
} else {
print(Self.renderSessionsTable(sessions))
}
}
static func runSessionsFocus(_ values: ParsedValues) async {
guard let sessionID = values.positional.first, !sessionID.isEmpty else {
writeStderr("Missing session id.\n")
platformExit(1)
}
let sessions = await LocalAgentSessionScanner().scan()
guard let session = sessions.first(where: { $0.id == sessionID }) else {
Self.writeStderr("Unknown session: \(sessionID)\n")
Self.platformExit(1)
}
#if os(macOS)
let result = await MainActor.run {
SessionWindowFocuser.focus(session)
}
switch result {
case .focused, .activatedApplicationOnly:
Self.platformExit(0)
case .failed:
Self.writeStderr("Could not focus session: \(sessionID)\n")
Self.platformExit(2)
}
#else
Self.writeStderr("Session focus is only available on macOS.\n")
Self.platformExit(2)
#endif
}
static func renderSessionsTable(_ sessions: [AgentSession], now: Date = Date()) -> String {
guard !sessions.isEmpty else { return "No agent sessions found." }
let rows = sessions.map { session in
[
session.state == .active ? "active" : "idle",
session.provider.rawValue,
session.source.rawValue,
session.projectName ?? "",
Self.sessionAge(session, now: now),
session.id,
]
}
let headers = ["STATE", "PROVIDER", "SOURCE", "PROJECT", "ACTIVITY", "ID"]
let widths = headers.indices.map { index in
([headers[index]] + rows.map { $0[index] }).map(\ .count).max() ?? headers[index].count
}
let render: ([String]) -> String = { columns in
columns.indices.map { index in
columns[index].padding(toLength: widths[index], withPad: " ", startingAt: 0)
}.joined(separator: " ")
}
return ([render(headers)] + rows.map(render)).joined(separator: "\n")
}
private static func sessionAge(_ session: AgentSession, now: Date) -> String {
guard let date = session.lastActivityAt ?? session.startedAt else { return "now" }
let seconds = max(0, Int(now.timeIntervalSince(date)))
if seconds < 60 {
return "\(seconds)s"
}
if seconds < 3600 {
return "\(seconds / 60)m"
}
if seconds < 86400 {
return "\(seconds / 3600)h"
}
return "\(seconds / 86400)d"
}
}
struct SessionsOptions: CommanderParsable {
@Flag(name: .long("json"), help: "Emit JSON")
var jsonShortcut: Bool = false
@Flag(name: .long("pretty"), help: "Pretty-print JSON output")
var pretty: Bool = false
}
struct SessionsFocusOptions: CommanderParsable {
@Argument(help: "Session identifier")
var id: String = ""
}
@@ -0,0 +1,47 @@
import Foundation
enum CLITerminalBackend: Equatable, Sendable {
case standard
case truecolor
case kittyGraphics
}
enum CLITerminalCapabilities {
static func detect(environment: [String: String] = ProcessInfo.processInfo.environment) -> CLITerminalBackend {
if self.supportsKittyGraphics(environment: environment) {
return .kittyGraphics
}
if self.supportsTruecolor(environment: environment) {
return .truecolor
}
return .standard
}
static func supportsEnhancedCards(
useColor: Bool,
environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool
{
guard useColor else { return false }
if environment["CODEXBAR_CARDS_ENHANCED"] == "1" { return true }
if environment["CODEXBAR_CARDS_ENHANCED"] == "0" { return false }
return self.supportsTruecolor(environment: environment)
}
static func supportsKittyGraphics(environment: [String: String]) -> Bool {
if environment["KITTY_WINDOW_ID"] != nil { return true }
if environment["GHOSTTY_RESOURCES_DIR"] != nil { return true }
let term = environment["TERM"]?.lowercased() ?? ""
if term.contains("kitty") || term.contains("ghostty") || term.contains("wezterm") {
return true
}
return false
}
static func supportsTruecolor(environment: [String: String]) -> Bool {
if self.supportsKittyGraphics(environment: environment) { return true }
let colorTerm = environment["COLORTERM"]?.lowercased() ?? ""
if colorTerm.contains("truecolor") || colorTerm.contains("24bit") { return true }
let term = environment["TERM"]?.lowercased() ?? ""
return term.contains("foot") || term.contains("alacritty")
}
}
@@ -0,0 +1,79 @@
import CodexBarCore
import Dispatch
import Foundation
#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#endif
private func handleCLITerminationSignal(_: Int32) {}
final class CLITerminationSignalMonitor: @unchecked Sendable {
static let signalNumbers = [SIGINT, SIGTERM, SIGHUP]
private let lock = NSLock()
private let sources: [DispatchSourceSignal]
private var isCancelled = false
init(onSignal: @escaping @Sendable (Int32) -> Void) {
self.sources = Self.signalNumbers.map { signalNumber in
Self.installCaptureHandler(for: signalNumber)
let source = DispatchSource.makeSignalSource(signal: signalNumber, queue: .global(qos: .utility))
source.setEventHandler {
onSignal(signalNumber)
}
source.resume()
return source
}
}
func cancel() {
self.lock.lock()
guard !self.isCancelled else {
self.lock.unlock()
return
}
self.isCancelled = true
self.lock.unlock()
for source in self.sources {
source.cancel()
}
for signalNumber in Self.signalNumbers {
Self.restoreDefaultHandler(for: signalNumber)
}
}
deinit {
self.cancel()
}
static func terminateActiveHelpersAndReraise(_ signalNumber: Int32) {
TTYCommandRunner.terminateActiveProcessesForAppShutdown()
self.restoreDefaultHandler(for: signalNumber)
_ = kill(getpid(), signalNumber)
}
private static func installCaptureHandler(for signalNumber: Int32) {
#if canImport(Darwin)
_ = Darwin.signal(signalNumber, handleCLITerminationSignal)
#elseif canImport(Glibc)
_ = Glibc.signal(signalNumber, handleCLITerminationSignal)
#elseif canImport(Musl)
_ = Musl.signal(signalNumber, handleCLITerminationSignal)
#endif
}
private static func restoreDefaultHandler(for signalNumber: Int32) {
#if canImport(Darwin)
_ = Darwin.signal(signalNumber, SIG_DFL)
#elseif canImport(Glibc)
_ = Glibc.signal(signalNumber, SIG_DFL)
#elseif canImport(Musl)
_ = Musl.signal(signalNumber, SIG_DFL)
#endif
}
}
+744
View File
@@ -0,0 +1,744 @@
import CodexBarCore
import Commander
import Foundation
struct UsageCommandContext {
let format: OutputFormat
let includeCredits: Bool
let sourceModeOverride: ProviderSourceMode?
let antigravityPlanDebug: Bool
let augmentDebug: Bool
let webDebugDumpHTML: Bool
let webTimeout: TimeInterval
let verbose: Bool
let useColor: Bool
let resetStyle: ResetTimeDisplayStyle
let weeklyWorkDays: Int?
let jsonOnly: Bool
let includeAllCodexAccounts: Bool
let fetcher: UsageFetcher
let claudeFetcher: ClaudeUsageFetcher
let browserDetection: BrowserDetection
/// True for long-lived hosts (`codexbar serve`) that keep warm provider
/// helper sessions (such as the managed Antigravity `agy` process) alive
/// between fetches instead of resetting after each one-shot fetch.
var persistCLISessions: Bool = false
var persistentCLISessionIdleWindow: TimeInterval?
var cardsLayout: Bool = false
}
struct UsageCommandOutput {
var sections: [String] = []
var payload: [ProviderPayload] = []
var cards: [CLICardModel] = []
var cardFailures: [CLICardFailure] = []
var exitCode: ExitCode = .success
}
private struct UsageSuccessRenderInput {
let provider: UsageProvider
let accountLabel: String?
let cacheAccountKey: String?
let version: String?
let source: String
let status: ProviderStatusPayload?
let usage: UsageSnapshot
let credits: CreditsSnapshot?
let antigravityPlanInfo: AntigravityPlanInfoSummary?
let dashboard: OpenAIDashboardSnapshot?
let effectiveSourceMode: ProviderSourceMode
let command: UsageCommandContext
let notes: [String]
}
extension UsageCommandOutput {
mutating func merge(_ other: UsageCommandOutput) {
self.sections.append(contentsOf: other.sections)
self.payload.append(contentsOf: other.payload)
self.cards.append(contentsOf: other.cards)
self.cardFailures.append(contentsOf: other.cardFailures)
if other.exitCode != .success {
self.exitCode = other.exitCode
}
}
}
extension CodexBarCLI {
static func runUsage(_ values: ParsedValues) async {
let output = CLIOutputPreferences.from(values: values)
let config = Self.loadConfig(output: output)
let provider = Self.decodeProvider(from: values, config: config)
let format = output.format
let includeCredits = format == .json ? true : !values.flags.contains("noCredits")
let includeStatus = values.flags.contains("status")
let sourceModeRaw = values.options["source"]?.last
let parsedSourceMode = Self.decodeSourceMode(from: values)
if sourceModeRaw != nil, parsedSourceMode == nil {
Self.exit(
code: .failure,
message: "Error: --source must be auto|web|cli|oauth|api.",
output: output,
kind: .args)
}
let antigravityPlanDebug = values.flags.contains("antigravityPlanDebug")
let augmentDebug = values.flags.contains("augmentDebug")
let webDebugDumpHTML = values.flags.contains("webDebugDumpHtml")
let webTimeout: TimeInterval
do {
webTimeout = try Self.decodeWebTimeout(from: values) ?? 60
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .args)
}
let verbose = values.flags.contains("verbose")
let noColor = values.flags.contains("noColor")
let useColor = Self.shouldUseColor(noColor: noColor, format: format)
let resetStyle = Self.resetTimeDisplayStyleFromDefaults()
let weeklyWorkDays = Self.weeklyProgressWorkDaysFromDefaults()
let providerList = provider.asList
let tokenSelection: TokenAccountCLISelection
do {
tokenSelection = try Self.decodeTokenAccountSelection(from: values)
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .args)
}
if tokenSelection.allAccounts, tokenSelection.label != nil || tokenSelection.index != nil {
Self.exit(
code: .failure,
message: "Error: --all-accounts cannot be combined with --account or --account-index.",
output: output,
kind: .args)
}
if tokenSelection.usesOverride {
guard providerList.count == 1 else {
Self.exit(
code: .failure,
message: "Error: account selection requires a single provider.",
output: output,
kind: .args)
}
let supportsAllCodexAccounts = providerList[0] == .codex
&& tokenSelection.allAccounts
&& tokenSelection.label == nil
&& tokenSelection.index == nil
guard supportsAllCodexAccounts || TokenAccountSupportCatalog.support(for: providerList[0]) != nil else {
Self.exit(
code: .failure,
message: "Error: \(providerList[0].rawValue) does not support token accounts.",
output: output,
kind: .args)
}
}
let browserDetection = BrowserDetection()
let fetcher = UsageFetcher()
let claudeFetcher = ClaudeUsageFetcher(browserDetection: browserDetection)
let tokenContext: TokenAccountCLIContext
do {
tokenContext = try TokenAccountCLIContext(
selection: tokenSelection,
config: config,
verbose: verbose)
} catch {
Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config)
}
var sections: [String] = []
var payload: [ProviderPayload] = []
var exitCode: ExitCode = .success
let command = UsageCommandContext(
format: format,
includeCredits: includeCredits,
sourceModeOverride: parsedSourceMode,
antigravityPlanDebug: antigravityPlanDebug,
augmentDebug: augmentDebug,
webDebugDumpHTML: webDebugDumpHTML,
webTimeout: webTimeout,
verbose: verbose,
useColor: useColor,
resetStyle: resetStyle,
weeklyWorkDays: weeklyWorkDays,
jsonOnly: output.jsonOnly,
includeAllCodexAccounts: tokenSelection.allAccounts && providerList == [.codex],
fetcher: fetcher,
claudeFetcher: claudeFetcher,
browserDetection: browserDetection)
for p in providerList {
let status = includeStatus ? await Self.fetchStatus(for: p) : nil
// CLI usage should not clear Keychain cooldowns or attempt interactive Keychain prompts.
let output = await ProviderInteractionContext.$current.withValue(.background) {
await Self.fetchUsageOutputs(
provider: p,
status: status,
tokenContext: tokenContext,
command: command)
}
if output.exitCode != .success {
exitCode = output.exitCode
}
sections.append(contentsOf: output.sections)
payload.append(contentsOf: output.payload)
}
switch format {
case .text:
if !sections.isEmpty {
print(sections.joined(separator: "\n\n"))
}
case .json:
Self.printJSON(payload, pretty: output.pretty)
}
Self.exit(code: exitCode, output: output, kind: exitCode == .success ? .runtime : .provider)
}
static func fetchUsageOutputs(
provider: UsageProvider,
status: ProviderStatusPayload?,
tokenContext: TokenAccountCLIContext,
command: UsageCommandContext) async -> UsageCommandOutput
{
if provider == .codex, command.includeAllCodexAccounts {
var output = UsageCommandOutput()
let accounts = tokenContext.visibleCodexAccounts().visibleAccounts
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,
status: status,
tokenContext: tokenContext,
command: command)
output.merge(result)
}
return output
}
let accounts: [ProviderTokenAccount]
do {
accounts = try tokenContext.resolvedAccounts(for: provider)
} catch {
return Self.usageOutputForAccountResolutionError(
provider: provider,
status: status,
command: command,
error: error)
}
let selections = Self.accountSelections(from: accounts)
var output = UsageCommandOutput()
for account in selections {
let result = await Self.fetchUsageOutput(
provider: provider,
account: account,
status: status,
tokenContext: tokenContext,
command: command)
output.merge(result)
}
return output
}
private static func accountSelections(from accounts: [ProviderTokenAccount]) -> [ProviderTokenAccount?] {
if accounts.isEmpty { return [nil] }
return accounts.map { Optional($0) }
}
private static func usageOutputForAccountResolutionError(
provider: UsageProvider,
status: ProviderStatusPayload?,
command: UsageCommandContext,
error: Error) -> UsageCommandOutput
{
var output = UsageCommandOutput()
output.exitCode = .failure
if command.format == .json {
output.payload.append(Self.makeProviderErrorPayload(
provider: provider,
account: nil,
source: command.sourceModeOverride?.rawValue ?? "auto",
status: status,
error: error,
kind: .provider))
} else if command.cardsLayout {
output.cardFailures.append(CLICardFailure(
provider: provider,
accountLabel: nil,
message: error.localizedDescription))
} else if !command.jsonOnly {
Self.writeStderr("Error: \(error.localizedDescription)\n")
}
return output
}
// swiftlint:disable:next function_parameter_count
private static func makeUsagePayload(
provider: UsageProvider,
accountLabel: String?,
cacheAccountKey: String?,
version: String?,
source: String,
status: ProviderStatusPayload?,
usage: UsageSnapshot,
credits: CreditsSnapshot?,
antigravityPlanInfo: AntigravityPlanInfoSummary?,
dashboard: OpenAIDashboardSnapshot?,
weeklyWorkDays: Int?) -> ProviderPayload
{
ProviderPayload(
provider: provider,
account: accountLabel,
cacheAccountKey: cacheAccountKey,
version: version,
source: source,
status: status,
usage: usage,
credits: credits,
antigravityPlanInfo: antigravityPlanInfo,
openaiDashboard: dashboard,
error: nil,
pace: CLIRenderer.providerPacePayload(provider: provider, snapshot: usage, weeklyWorkDays: weeklyWorkDays))
}
private static func appendSuccessRenderOutput(
_ input: UsageSuccessRenderInput,
output: inout UsageCommandOutput)
{
switch input.command.format {
case .text:
if input.command.cardsLayout {
output.cards.append(CLICardsRenderer.makeCard(CLICardBuildInput(
provider: input.provider,
snapshot: input.usage,
credits: input.credits,
source: input.source,
status: input.status,
notes: input.notes,
useColor: input.command.useColor,
resetStyle: input.command.resetStyle,
weeklyWorkDays: input.command.weeklyWorkDays,
now: Date())))
} else {
var text = CLIRenderer.renderText(
provider: input.provider,
snapshot: input.usage,
credits: input.credits,
context: RenderContext(
header: Self.makeHeader(
provider: input.provider,
version: input.version,
source: input.source),
status: input.status,
useColor: input.command.useColor,
resetStyle: input.command.resetStyle,
weeklyWorkDays: input.command.weeklyWorkDays,
notes: input.notes))
if let dashboard = input.dashboard, input.provider == .codex, input.effectiveSourceMode.usesWeb {
text += "\n" + Self.renderOpenAIWebDashboardText(dashboard)
}
output.sections.append(text)
}
case .json:
output.payload.append(self.makeUsagePayload(
provider: input.provider,
accountLabel: input.accountLabel,
cacheAccountKey: input.cacheAccountKey,
version: input.version,
source: input.source,
status: input.status,
usage: input.usage,
credits: input.credits,
antigravityPlanInfo: input.antigravityPlanInfo,
dashboard: input.dashboard,
weeklyWorkDays: input.command.weeklyWorkDays))
}
}
private static func fetchUsageOutput(
provider: UsageProvider,
account: ProviderTokenAccount?,
codexVisibleAccount: CodexVisibleAccount? = nil,
status: ProviderStatusPayload?,
tokenContext: TokenAccountCLIContext,
command: UsageCommandContext) async -> UsageCommandOutput
{
var output = UsageCommandOutput()
let env = tokenContext.environment(
base: ProcessInfo.processInfo.environment,
provider: provider,
account: account,
codexActiveSourceOverride: codexVisibleAccount?.selectionSource)
let settings = tokenContext.settingsSnapshot(
for: provider,
account: account,
codexActiveSourceOverride: codexVisibleAccount?.selectionSource)
let configSource = tokenContext.preferredSourceMode(for: provider)
let baseSource = command.sourceModeOverride ?? configSource
let effectiveSourceMode = tokenContext.effectiveSourceMode(
base: baseSource,
provider: provider,
account: account)
let cacheAccountKey = Self.usageCacheAccountKey(
provider: provider,
account: account,
codexVisibleAccount: codexVisibleAccount)
#if !os(macOS)
if Self.sourceModeRequiresWebSupport(
effectiveSourceMode,
provider: provider,
environment: env,
settings: settings)
{
return Self.webSourceUnsupportedOutput(
provider: provider,
account: (
label: account?.label ?? codexVisibleAccount?.menuDisplayName,
cacheKey: cacheAccountKey),
source: effectiveSourceMode.rawValue,
status: status,
command: command)
}
#endif
let fetchContext = ProviderFetchContext(
runtime: .cli,
sourceMode: effectiveSourceMode,
includeCredits: command.includeCredits,
webTimeout: command.webTimeout,
webDebugDumpHTML: command.webDebugDumpHTML,
verbose: command.verbose,
env: env,
settings: settings,
fetcher: tokenContext.fetcher(base: command.fetcher, provider: provider, env: env),
claudeFetcher: command.claudeFetcher,
browserDetection: command.browserDetection,
selectedTokenAccountID: account?.id,
tokenAccountTokenUpdater: tokenContext.tokenUpdater(for: account),
providerManualTokenUpdater: tokenContext.manualTokenUpdater(),
persistsCLISessions: Self.persistsCLISessions(provider: provider, command: command),
persistentCLISessionIdleWindow: command.persistentCLISessionIdleWindow)
let outcome = await Self.fetchProviderUsage(provider: provider, context: fetchContext)
if command.verbose, !command.jsonOnly {
Self.printFetchAttempts(provider: provider, attempts: outcome.attempts)
}
switch outcome.result {
case let .success(result):
let antigravityPlanInfo = await Self.fetchAntigravityPlanInfoIfNeeded(
provider: provider,
command: command)
await Self.emitAugmentDebugIfNeeded(provider: provider, command: command)
var usage = result.usage.scoped(to: provider)
if let account {
usage = tokenContext.applyAccountLabel(usage, provider: provider, account: account)
} else if let codexVisibleAccount {
usage = tokenContext.applyCodexVisibleAccountLabel(usage, account: codexVisibleAccount)
}
var dashboard = result.dashboard
if dashboard == nil, command.format == .json, provider == .codex {
dashboard = Self.loadOpenAIDashboardIfAvailable(
usage: usage,
sourceLabel: result.sourceLabel,
context: fetchContext)
}
let shouldDetectVersion = Self.shouldDetectVersion(provider: provider, result: result)
let version = Self.normalizeVersion(
raw: shouldDetectVersion
? Self.detectVersion(for: provider, browserDetection: command.browserDetection)
: nil)
let source = result.sourceLabel
let notes = Self.usageTextNotes(
provider: provider,
sourceMode: effectiveSourceMode,
resolvedSourceLabel: source)
Self.appendSuccessRenderOutput(
UsageSuccessRenderInput(
provider: provider,
accountLabel: account?.label ?? codexVisibleAccount?.menuDisplayName,
cacheAccountKey: cacheAccountKey,
version: version,
source: source,
status: status,
usage: usage,
credits: result.credits,
antigravityPlanInfo: antigravityPlanInfo,
dashboard: dashboard,
effectiveSourceMode: effectiveSourceMode,
command: command,
notes: notes),
output: &output)
case let .failure(error):
output.exitCode = Self.mapError(error)
if command.format == .json {
output.payload.append(Self.makeProviderErrorPayload(
provider: provider,
account: account?.label ?? codexVisibleAccount?.menuDisplayName,
cacheAccountKey: cacheAccountKey,
source: effectiveSourceMode.rawValue,
status: status,
error: error,
kind: .provider))
} else if command.cardsLayout {
output.cardFailures.append(CLICardFailure(
provider: provider,
accountLabel: account?.label ?? codexVisibleAccount?.menuDisplayName,
message: error.localizedDescription))
} else if !command.jsonOnly {
if let accountLabel = account?.label ?? codexVisibleAccount?.menuDisplayName {
Self.writeStderr(
"Error (\(provider.rawValue) - \(accountLabel)): \(error.localizedDescription)\n")
} else {
Self.writeStderr("Error: \(error.localizedDescription)\n")
}
if let summary = Self.kiloAutoFallbackSummary(
provider: provider,
sourceMode: effectiveSourceMode,
attempts: outcome.attempts)
{
Self.writeStderr("\(summary)\n")
}
}
}
return await Self.finishUsageOutput(output, provider: provider, command: command)
}
static func shouldDetectVersion(provider: UsageProvider, result: ProviderFetchResult) -> Bool {
let descriptor = ProviderDescriptorRegistry.descriptor(for: provider)
guard descriptor.cli.versionDetector != nil else { return false }
guard result.strategyKind != .webDashboard else { return false }
return !(provider == .claude && result.strategyKind == .oauth)
}
private static func holdsAntigravitySession(
provider: UsageProvider,
command: UsageCommandContext) -> Bool
{
self.holdsAntigravityCLISessionForPlanDebug(
provider: provider,
planDebugEnabled: command.antigravityPlanDebug,
jsonOnly: command.jsonOnly,
persistsCLISessions: command.persistCLISessions)
}
private static func persistsCLISessions(
provider: UsageProvider,
command: UsageCommandContext) -> Bool
{
command.persistCLISessions || self.holdsAntigravitySession(provider: provider, command: command)
}
static func holdsAntigravityCLISessionForPlanDebug(
provider: UsageProvider,
planDebugEnabled: Bool,
jsonOnly: Bool,
persistsCLISessions: Bool) -> Bool
{
provider == .antigravity
&& planDebugEnabled
&& !jsonOnly
&& !persistsCLISessions
}
private static func finishUsageOutput(
_ output: UsageCommandOutput,
provider: UsageProvider,
command: UsageCommandContext) async -> UsageCommandOutput
{
if self.holdsAntigravitySession(provider: provider, command: command) {
await ProviderCLISessionLifecycle.shutdownPersistentSessions()
}
return output
}
private static func fetchAntigravityPlanInfoIfNeeded(
provider: UsageProvider,
command: UsageCommandContext) async -> AntigravityPlanInfoSummary?
{
guard command.antigravityPlanDebug,
provider == .antigravity,
!command.jsonOnly
else {
return nil
}
let info = try? await AntigravityStatusProbe().fetchPlanInfoSummary()
if command.format == .text, let info {
Self.printAntigravityPlanInfo(info)
}
return info
}
private static func emitAugmentDebugIfNeeded(
provider: UsageProvider,
command: UsageCommandContext) async
{
guard command.augmentDebug, provider == .augment else { return }
#if os(macOS)
let dump = await AugmentStatusProbe.latestDumps()
if command.format == .text, !dump.isEmpty, !command.jsonOnly {
Self.writeStderr("Augment API responses:\n\(dump)\n")
}
#endif
}
private static func webSourceUnsupportedOutput(
provider: UsageProvider,
account: (label: String?, cacheKey: String?),
source: String,
status: ProviderStatusPayload?,
command: UsageCommandContext) -> UsageCommandOutput
{
var output = UsageCommandOutput()
let error = NSError(
domain: "CodexBarCLI",
code: 1,
userInfo: [NSLocalizedDescriptionKey:
"Error: selected source requires web support and is only supported on macOS."])
output.exitCode = .failure
if command.format == .json {
output.payload.append(Self.makeProviderErrorPayload(
provider: provider,
account: account.label,
cacheAccountKey: account.cacheKey,
source: source,
status: status,
error: error,
kind: .runtime))
} else if command.cardsLayout {
output.cardFailures.append(CLICardFailure(
provider: provider,
accountLabel: account.label,
message: error.localizedDescription))
} else if !command.jsonOnly {
Self.writeStderr("Error: \(error.localizedDescription)\n")
}
return output
}
static func usageCacheAccountKey(
provider _: UsageProvider,
account: ProviderTokenAccount?,
codexVisibleAccount: CodexVisibleAccount?) -> String?
{
if let account {
return "token:\(account.id.uuidString.lowercased())"
}
if let codexVisibleAccount {
if let workspaceAccountID = codexVisibleAccount.workspaceAccountID?
.trimmingCharacters(in: .whitespacesAndNewlines),
!workspaceAccountID.isEmpty,
!codexVisibleAccount.email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
let email = codexVisibleAccount.email
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return "codex:workspace:\(workspaceAccountID):email:\(email)"
}
if let storedAccountID = codexVisibleAccount.storedAccountID {
return "codex:stored:\(storedAccountID.uuidString.lowercased())"
}
if let authFingerprint = codexVisibleAccount.authFingerprint {
return "codex:auth:\(authFingerprint)"
}
return nil
}
return nil
}
static func sourceModeRequiresWebSupport(
_ sourceMode: ProviderSourceMode,
provider: UsageProvider,
environment: [String: String]? = nil,
settings: ProviderSettingsSnapshot? = nil) -> Bool
{
guard provider != .grok, provider != .amp else {
return false
}
if provider == .codex, sourceMode == .auto {
return false
}
if provider == .claude, sourceMode == .auto {
// Claude's cross-platform planner skips its unavailable web step and falls back to the CLI.
return false
}
if provider == .opencodego {
if sourceMode == .auto || settings?.opencodego?.cookieSource == .manual {
return false
}
}
if provider == .commandcode,
settings?.commandcode?.cookieSource == .manual
{
return false
}
#if os(Linux)
if provider == .cursor,
settings?.cursor?.cookieSource != .off
{
// Linux uses Cursor app auth and manual cookies; browser import remains macOS-only.
return false
}
#endif
if provider == .sakana,
sourceMode == .auto || sourceMode == .web,
environment.map({ SakanaSettingsReader.cookieHeader(environment: $0) != nil }) == true
{
return false
}
if provider == .qoder,
settings?.qoder?.cookieSource == .manual
{
return false
}
if provider == .ollama,
sourceMode == .auto
{
let hasEnvironmentToken = environment.map {
ProviderTokenResolver.ollamaToken(environment: $0) != nil
} == true
if settings?.ollama?.cookieSource == .off || hasEnvironmentToken {
return false
}
}
if provider == .kimi,
sourceMode == .auto,
environment.map({ environment in
ProviderTokenResolver.kimiAPIToken(environment: environment) != nil ||
KimiSettingsReader.hasKimiCodeCredential(environment: environment)
}) == true
{
return false
}
if provider == .factory,
sourceMode == .auto || sourceMode == .cli,
environment.map({ FactorySettingsReader.apiKey(environment: $0) != nil }) == true
{
// Linux Auto/legacy-cli can use FACTORY_API_KEY without browser cookies.
return false
}
if provider == .mimo,
sourceMode == .auto,
let environment,
MiMoLocalUsageFallback.cacheExists(environment: environment)
{
return false
}
return switch sourceMode {
case .web:
true
case .auto:
ProviderDescriptorRegistry.descriptor(for: provider).fetchPlan.sourceModes.contains(.web)
case .cli, .oauth, .api:
false
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import CodexBarCore
import Commander
import Foundation
struct DiagnoseOptions: 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: String?
@Option(name: .long("format"), help: "Output format: json")
var format: String?
@Flag(name: .long("redact"), help: "Explicitly redact sensitive values (always enabled for diagnose)")
var redact: Bool = false
@Option(name: .long("output"), help: "Write redacted JSON diagnostic export to a file")
var output: String?
@Flag(name: .long("pretty"), help: "Pretty-print JSON output")
var pretty: Bool = false
}
+648
View File
@@ -0,0 +1,648 @@
import CodexBarCore
import Commander
import Foundation
struct TokenAccountCLISelection {
let label: String?
let index: Int?
let allAccounts: Bool
var usesOverride: Bool {
self.label != nil || self.index != nil || self.allAccounts
}
}
enum TokenAccountCLIError: LocalizedError {
case noAccounts(UsageProvider)
case accountNotFound(UsageProvider, String)
case indexOutOfRange(UsageProvider, Int, Int)
var errorDescription: String? {
switch self {
case let .noAccounts(provider):
"No token accounts configured for \(provider.rawValue)."
case let .accountNotFound(provider, label):
"No token account labeled '\(label)' for \(provider.rawValue)."
case let .indexOutOfRange(provider, index, count):
"Token account index \(index) out of range for \(provider.rawValue) (1-\(count))."
}
}
}
struct TokenAccountCLIContext {
let selection: TokenAccountCLISelection
let config: CodexBarConfig
let accountsByProvider: [UsageProvider: ProviderTokenAccountData]
private let baseEnvironment: [String: String]
private let managedCodexAccountStoreURL: URL?
init(
selection: TokenAccountCLISelection,
config: CodexBarConfig,
verbose _: Bool,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
managedCodexAccountStoreURL: URL? = nil) throws
{
self.selection = selection
self.config = config
self.baseEnvironment = baseEnvironment
self.managedCodexAccountStoreURL = managedCodexAccountStoreURL
self.accountsByProvider = Dictionary(uniqueKeysWithValues: config.providers.compactMap { provider in
guard let accounts = provider.tokenAccounts else { return nil }
return (provider.id, accounts)
})
}
func resolvedAccounts(for provider: UsageProvider) throws -> [ProviderTokenAccount] {
guard TokenAccountSupportCatalog.support(for: provider) != nil else { return [] }
guard let data = self.accountsByProvider[provider], !data.accounts.isEmpty else {
if self.selection.usesOverride {
throw TokenAccountCLIError.noAccounts(provider)
}
return []
}
if self.selection.allAccounts {
return data.accounts
}
if let label = self.selection.label?.trimmingCharacters(in: .whitespacesAndNewlines), !label.isEmpty {
let normalized = label.lowercased()
if let match = data.accounts.first(where: { $0.label.lowercased() == normalized }) {
return [match]
}
throw TokenAccountCLIError.accountNotFound(provider, label)
}
if let index = self.selection.index {
guard index >= 0, index < data.accounts.count else {
throw TokenAccountCLIError.indexOutOfRange(provider, index + 1, data.accounts.count)
}
return [data.accounts[index]]
}
let clamped = data.clampedActiveIndex()
return [data.accounts[clamped]]
}
func settingsSnapshot(
for provider: UsageProvider,
account: ProviderTokenAccount?,
codexActiveSourceOverride: CodexActiveSource? = nil) -> ProviderSettingsSnapshot?
{
let config = self.providerConfig(for: provider)
if provider == .qoder {
let settings = self.cookieSettings(provider: provider, account: account, config: config)
return self.makeSnapshot(qoder: self.makeProviderCookieSettings(settings))
}
if let snapshot = self.makeCookieBackedSnapshot(provider: provider, account: account, config: config) {
return snapshot
}
switch provider {
case .codex:
return self.makeSnapshot(codex: self.makeCodexSettingsSnapshot(
account: account,
codexActiveSourceOverride: codexActiveSourceOverride))
case .claude:
let routing = self.claudeCredentialRouting(account: account, config: config)
let claudeSource: ClaudeUsageDataSource = if routing.adminAPIKey != nil {
.api
} else if routing.isOAuth {
.oauth
} else {
.auto
}
let cookieSource = routing.isOAuth || routing.adminAPIKey != nil
? ProviderCookieSource.off
: self.cookieSource(provider: provider, account: account, config: config)
return self.makeSnapshot(
claude: ProviderSettingsSnapshot.ClaudeProviderSettings(
usageDataSource: claudeSource,
webExtrasEnabled: false,
cookieSource: cookieSource,
manualCookieHeader: routing.manualCookieHeader,
organizationID: account?.sanitizedOrganizationID))
case .zai:
return self.makeSnapshot(
zai: ProviderSettingsSnapshot.ZaiProviderSettings(
apiRegion: self.resolveZaiRegion(config),
usageScope: Self.zaiUsageScope(for: account),
teamContext: Self.zaiTeamContext(for: account)))
case .moonshot:
return self.makeSnapshot(
moonshot: ProviderSettingsSnapshot.MoonshotProviderSettings(
region: self.resolveMoonshotRegion(config)))
case .kilo:
return self.makeSnapshot(
kilo: ProviderSettingsSnapshot.KiloProviderSettings(
usageDataSource: Self.kiloUsageDataSource(from: config?.source),
extrasEnabled: Self.kiloExtrasEnabled(from: config)))
case .jetbrains:
return self.makeSnapshot(
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(
ideBasePath: nil))
default:
return nil
}
}
private func makeCookieBackedSnapshot(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?) -> ProviderSettingsSnapshot?
{
let cookieSettings = self.cookieSettings(provider: provider, account: account, config: config)
switch provider {
case .cursor:
return self.makeSnapshot(cursor: self.makeProviderCookieSettings(cookieSettings))
case .opencode:
return self.makeSnapshot(
opencode: ProviderSettingsSnapshot.OpenCodeProviderSettings(
cookieSource: cookieSettings.cookieSource,
manualCookieHeader: cookieSettings.manualCookieHeader,
workspaceID: config?.workspaceID))
case .opencodego:
return self.makeSnapshot(
opencodego: ProviderSettingsSnapshot.OpenCodeProviderSettings(
cookieSource: cookieSettings.cookieSource,
manualCookieHeader: cookieSettings.manualCookieHeader,
workspaceID: config?.workspaceID))
case .commandcode:
return self.makeSnapshot(commandcode: self.makeProviderCookieSettings(cookieSettings))
case .alibaba:
return self.makeSnapshot(
alibaba: ProviderSettingsSnapshot.AlibabaCodingPlanProviderSettings(
cookieSource: cookieSettings.cookieSource,
manualCookieHeader: cookieSettings.manualCookieHeader,
apiRegion: self.resolveAlibabaCodingPlanRegion(config)))
case .alibabatokenplan:
return self.makeSnapshot(
alibabaTokenPlan: ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings(
cookieSource: cookieSettings.cookieSource,
manualCookieHeader: cookieSettings.manualCookieHeader,
apiRegion: self.resolveAlibabaTokenPlanRegion(config)))
case .factory:
return self.makeSnapshot(factory: self.makeProviderCookieSettings(cookieSettings))
case .minimax:
return self.makeSnapshot(
minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings(
cookieSource: cookieSettings.cookieSource,
manualCookieHeader: cookieSettings.manualCookieHeader,
apiRegion: self.resolveMiniMaxRegion(config)))
case .manus:
return self.makeSnapshot(manus: self.makeProviderCookieSettings(cookieSettings))
case .augment:
return self.makeSnapshot(augment: self.makeProviderCookieSettings(cookieSettings))
case .amp:
return self.makeSnapshot(amp: self.makeProviderCookieSettings(cookieSettings))
case .ollama:
return self.makeSnapshot(ollama: self.makeProviderCookieSettings(cookieSettings))
case .kimi:
return self.makeSnapshot(kimi: self.makeProviderCookieSettings(cookieSettings))
case .perplexity:
return self.makeSnapshot(perplexity: self.makeProviderCookieSettings(cookieSettings))
case .mimo:
return self.makeSnapshot(mimo: self.makeProviderCookieSettings(cookieSettings))
case .doubao:
return nil
case .abacus:
return self.makeSnapshot(abacus: self.makeProviderCookieSettings(cookieSettings))
case .mistral:
return self.makeSnapshot(mistral: self.makeProviderCookieSettings(cookieSettings))
case .stepfun:
let stepfunSettings = self.cookieSettings(
provider: provider,
account: account,
config: config,
configuredHeader: config?.sanitizedRegion ?? config?.sanitizedCookieHeader)
return self.makeSnapshot(
stepfun: ProviderSettingsSnapshot.StepFunProviderSettings(
cookieSource: stepfunSettings.cookieSource,
manualToken: stepfunSettings.manualCookieHeader ?? "",
username: config?.sanitizedAPIKey ?? "",
password: ""))
default:
return nil
}
}
private func makeSnapshot(
codex: ProviderSettingsSnapshot.CodexProviderSettings? = nil,
claude: ProviderSettingsSnapshot.ClaudeProviderSettings? = nil,
cursor: ProviderSettingsSnapshot.CursorProviderSettings? = nil,
opencode: ProviderSettingsSnapshot.OpenCodeProviderSettings? = nil,
opencodego: ProviderSettingsSnapshot.OpenCodeProviderSettings? = nil,
alibaba: ProviderSettingsSnapshot.AlibabaCodingPlanProviderSettings? = nil,
alibabaTokenPlan: ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings? = nil,
factory: ProviderSettingsSnapshot.FactoryProviderSettings? = nil,
minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings? = nil,
manus: ProviderSettingsSnapshot.ManusProviderSettings? = nil,
zai: ProviderSettingsSnapshot.ZaiProviderSettings? = nil,
moonshot: ProviderSettingsSnapshot.MoonshotProviderSettings? = nil,
kilo: ProviderSettingsSnapshot.KiloProviderSettings? = nil,
kimi: ProviderSettingsSnapshot.KimiProviderSettings? = nil,
augment: ProviderSettingsSnapshot.AugmentProviderSettings? = nil,
amp: ProviderSettingsSnapshot.AmpProviderSettings? = nil,
commandcode: ProviderSettingsSnapshot.CommandCodeProviderSettings? = nil,
ollama: ProviderSettingsSnapshot.OllamaProviderSettings? = nil,
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil,
perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings? = nil,
mimo: ProviderSettingsSnapshot.MiMoProviderSettings? = nil,
abacus: ProviderSettingsSnapshot.AbacusProviderSettings? = nil,
mistral: ProviderSettingsSnapshot.MistralProviderSettings? = nil,
qoder: ProviderSettingsSnapshot.QoderProviderSettings? = nil,
stepfun: ProviderSettingsSnapshot.StepFunProviderSettings? = nil) -> ProviderSettingsSnapshot
{
ProviderSettingsSnapshot.make(
codex: codex,
claude: claude,
cursor: cursor,
opencode: opencode,
opencodego: opencodego,
alibaba: alibaba,
alibabaTokenPlan: alibabaTokenPlan,
factory: factory,
minimax: minimax,
manus: manus,
zai: zai,
kilo: kilo,
kimi: kimi,
augment: augment,
moonshot: moonshot,
amp: amp,
commandcode: commandcode,
ollama: ollama,
jetbrains: jetbrains,
perplexity: perplexity,
mimo: mimo,
abacus: abacus,
mistral: mistral,
qoder: qoder,
stepfun: stepfun)
}
private func makeCodexSettingsSnapshot(
account: ProviderTokenAccount?,
codexActiveSourceOverride: CodexActiveSource? = nil) ->
ProviderSettingsSnapshot.CodexProviderSettings
{
let config = self.providerConfig(for: .codex)
let reconciliationSnapshot = self.codexAccountReconciler(
activeSource: codexActiveSourceOverride).loadSnapshot()
let resolvedActiveSource = CodexActiveSourceResolver.resolve(from: reconciliationSnapshot)
return CodexProviderSettingsBuilder.make(input: CodexProviderSettingsBuilderInput(
usageDataSource: .auto,
cookieSource: self.cookieSource(provider: .codex, account: account, config: config),
manualCookieHeader: self.manualCookieHeader(provider: .codex, account: account, config: config),
reconciliationSnapshot: reconciliationSnapshot,
resolvedActiveSource: resolvedActiveSource))
}
func environment(
base: [String: String],
provider: UsageProvider,
account: ProviderTokenAccount?,
codexActiveSourceOverride: CodexActiveSource? = nil) -> [String: String]
{
let providerConfig = self.providerConfig(for: provider)
var env = ProviderEnvironmentResolver.resolve(
base: base,
provider: provider,
config: providerConfig,
selectedAccount: account)
if provider == .codex,
let codexHomePath = self.codexHomePath(for: codexActiveSourceOverride)
{
env = CodexHomeScope.scopedEnvironment(base: env, codexHome: codexHomePath)
}
return env
}
func tokenUpdater(for account: ProviderTokenAccount?) -> ProviderFetchContext.TokenAccountTokenUpdater? {
guard let account else { return nil }
return { provider, accountID, token in
guard accountID == account.id else { return }
try? Self.updateStoredTokenAccount(provider: provider, accountID: accountID, token: token)
}
}
func manualTokenUpdater() -> ProviderFetchContext.ProviderManualTokenUpdater {
{ provider, token in
try? Self.updateStoredManualToken(provider: provider, token: token)
}
}
private static func updateStoredManualToken(provider: UsageProvider, token: String) throws {
guard provider == .stepfun else { return }
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let store = CodexBarConfigStore()
var config = try store.load() ?? .makeDefault()
var providerConfig = config.providerConfig(for: provider) ?? ProviderConfig(id: provider)
providerConfig.region = trimmed
config.setProviderConfig(providerConfig)
try store.save(config)
}
private static func updateStoredTokenAccount(
provider: UsageProvider,
accountID: UUID,
token: String) throws
{
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
let store = CodexBarConfigStore()
guard var config = try store.load() else { return }
guard var providerConfig = config.providerConfig(for: provider),
let data = providerConfig.tokenAccounts,
let index = data.accounts.firstIndex(where: { $0.id == accountID })
else {
return
}
let existing = data.accounts[index]
var accounts = data.accounts
accounts[index] = ProviderTokenAccount(
id: existing.id,
label: existing.label,
token: trimmed,
addedAt: existing.addedAt,
lastUsed: existing.lastUsed,
externalIdentifier: existing.externalIdentifier,
usageScope: existing.usageScope,
organizationID: existing.organizationID,
workspaceID: existing.workspaceID)
providerConfig.tokenAccounts = ProviderTokenAccountData(
version: data.version,
accounts: accounts,
activeIndex: data.clampedActiveIndex())
config.setProviderConfig(providerConfig)
try store.save(config)
}
func fetcher(base: UsageFetcher, provider: UsageProvider, env: [String: String]) -> UsageFetcher {
guard provider == .codex else { return base }
return UsageFetcher(environment: env)
}
func visibleCodexAccounts() -> CodexVisibleAccountProjection {
self.codexAccountReconciler().loadVisibleAccounts()
}
func applyAccountLabel(
_ snapshot: UsageSnapshot,
provider: UsageProvider,
account: ProviderTokenAccount) -> UsageSnapshot
{
let label = account.label.trimmingCharacters(in: .whitespacesAndNewlines)
guard !label.isEmpty else { return snapshot }
let existing = snapshot.identity(for: provider)
let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
let resolvedEmail = (email?.isEmpty ?? true) ? label : email
let identity = ProviderIdentitySnapshot(
providerID: provider,
accountEmail: resolvedEmail,
accountOrganization: existing?.accountOrganization,
loginMethod: existing?.loginMethod)
return snapshot.withIdentity(identity)
}
func applyCodexVisibleAccountLabel(_ snapshot: UsageSnapshot, account: CodexVisibleAccount) -> UsageSnapshot {
let existing = snapshot.identity(for: .codex)
let identity = ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: account.email,
accountOrganization: account.workspaceLabel ?? existing?.accountOrganization,
loginMethod: existing?.loginMethod)
return snapshot.withIdentity(identity)
}
func effectiveSourceMode(
base: ProviderSourceMode,
provider: UsageProvider,
account: ProviderTokenAccount?) -> ProviderSourceMode
{
guard provider == .claude else {
return base
}
let config = self.providerConfig(for: provider)
let routing = self.claudeCredentialRouting(account: account, config: config)
if base == .auto {
if routing.adminAPIKey != nil { return .api }
return routing.isOAuth ? .oauth : base
}
guard base == .cli, account != nil else {
return base
}
// Claude CLI usage is ambient to the active local CLI profile, so per-token-account
// CLI reads can be mislabeled as separate accounts. Use the selected account's
// routable credential instead.
switch routing {
case .adminAPIKey:
return .api
case .oauth:
return .oauth
case .webCookie:
return .web
case .none:
return base
}
}
func preferredSourceMode(for provider: UsageProvider) -> ProviderSourceMode {
let config = self.providerConfig(for: provider)
return config?.source ?? .auto
}
private func providerConfig(for provider: UsageProvider) -> ProviderConfig? {
self.config.providerConfig(for: provider)
}
private func codexAccountReconciler(activeSource: CodexActiveSource? = nil) -> DefaultCodexAccountReconciler {
let storeLoader: @Sendable () throws -> ManagedCodexAccountSet = if let managedCodexAccountStoreURL {
{
try FileManagedCodexAccountStore(fileURL: managedCodexAccountStoreURL).loadAccounts()
}
} else {
{
try FileManagedCodexAccountStore().loadAccounts()
}
}
return DefaultCodexAccountReconciler(
storeLoader: storeLoader,
activeSource: activeSource ?? self.providerConfig(for: .codex)?.codexActiveSource ?? .liveSystem,
baseEnvironment: self.baseEnvironment,
profileHomePaths: self.providerConfig(for: .codex)?.codexProfileHomePaths ?? [],
managedEnvironmentBuilder: { environment, account in
CodexHomeScope.scopedEnvironment(base: environment, codexHome: account.managedHomePath)
})
}
private func codexHomePath(for activeSourceOverride: CodexActiveSource?) -> String? {
let activeSource: CodexActiveSource = if let activeSourceOverride {
activeSourceOverride
} else {
CodexActiveSourceResolver.resolve(from: self.codexAccountReconciler().loadSnapshot())
.resolvedSource
}
switch activeSource {
case .liveSystem:
return nil
case let .managedAccount(id):
let accounts: ManagedCodexAccountSet? = if let managedCodexAccountStoreURL {
try? FileManagedCodexAccountStore(fileURL: managedCodexAccountStoreURL).loadAccounts()
} else {
try? FileManagedCodexAccountStore().loadAccounts()
}
return accounts?.account(id: id)?.managedHomePath
case let .profileHome(path):
guard let normalizedPath = CodexHomeScope.normalizedHomePath(path) else { return nil }
let configuredPaths = self.providerConfig(for: .codex)?.codexProfileHomePaths ?? []
return configuredPaths.contains {
CodexHomeScope.normalizedHomePath($0) == normalizedPath
} ? normalizedPath : nil
}
}
private func manualCookieHeader(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?) -> String?
{
self.cookieSettings(provider: provider, account: account, config: config).manualCookieHeader
}
private func cookieSource(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?) -> ProviderCookieSource
{
self.cookieSettings(provider: provider, account: account, config: config).cookieSource
}
private func cookieSettings(
provider: UsageProvider,
account: ProviderTokenAccount?,
config: ProviderConfig?,
configuredHeader: String? = nil) -> ProviderSettingsSnapshot.CookieProviderSettings
{
let configuredSource: ProviderCookieSource = if let override = config?.cookieSource {
override
} else if provider == .stepfun, config?.sanitizedRegion != nil {
.manual
} else if config?.sanitizedCookieHeader != nil {
.manual
} else {
.auto
}
return ProviderCookieSettingsResolver.resolve(
provider: provider,
configuredSource: configuredSource,
configuredHeader: configuredHeader ?? config?.sanitizedCookieHeader,
selectedAccount: account)
}
private func makeProviderCookieSettings<Settings: ProviderCookieSettings>(
_ resolved: ProviderSettingsSnapshot.CookieProviderSettings) -> Settings
{
Settings(
cookieSource: resolved.cookieSource,
manualCookieHeader: resolved.manualCookieHeader)
}
private func resolveZaiRegion(_ config: ProviderConfig?) -> ZaiAPIRegion {
guard let raw = config?.region?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
return .global
}
return ZaiAPIRegion(rawValue: raw) ?? .global
}
private static func zaiUsageScope(for account: ProviderTokenAccount?) -> ZaiUsageScope {
guard let raw = account?.sanitizedUsageScope?.lowercased(),
let scope = ZaiUsageScope(rawValue: raw)
else {
return .personal
}
return scope
}
private static func zaiTeamContext(for account: ProviderTokenAccount?) -> ZaiBigModelTeamContext? {
guard self.zaiUsageScope(for: account) == .team else { return nil }
return ZaiBigModelTeamContext(
organizationID: account?.sanitizedOrganizationID,
projectID: account?.sanitizedWorkspaceID)
}
private func resolveMiniMaxRegion(_ config: ProviderConfig?) -> MiniMaxAPIRegion {
guard let raw = config?.region?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
return .global
}
return MiniMaxAPIRegion(rawValue: raw) ?? .global
}
private func resolveMoonshotRegion(_ config: ProviderConfig?) -> MoonshotRegion? {
guard let raw = config?.region?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
return nil
}
return MoonshotRegion(rawValue: raw) ?? .international
}
private func resolveAlibabaCodingPlanRegion(_ config: ProviderConfig?) -> AlibabaCodingPlanAPIRegion {
guard let raw = config?.region?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
return .international
}
return AlibabaCodingPlanAPIRegion(rawValue: raw) ?? .international
}
private func resolveAlibabaTokenPlanRegion(_ config: ProviderConfig?) -> AlibabaTokenPlanAPIRegion {
guard let raw = config?.region?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
return .chinaMainland
}
return AlibabaTokenPlanAPIRegion(rawValue: raw) ?? .chinaMainland
}
private static func kiloUsageDataSource(from source: ProviderSourceMode?) -> KiloUsageDataSource {
guard let source else { return .auto }
switch source {
case .auto, .web, .oauth:
return .auto
case .api:
return .api
case .cli:
return .cli
}
}
private static func kiloExtrasEnabled(from config: ProviderConfig?) -> Bool {
guard self.kiloUsageDataSource(from: config?.source) == .auto else { return false }
return config?.extrasEnabled ?? false
}
private func claudeCredentialRouting(
account: ProviderTokenAccount?,
config: ProviderConfig?) -> ClaudeCredentialRouting
{
let manualCookieHeader = account == nil ? config?.sanitizedCookieHeader : nil
return ClaudeCredentialRouting.resolve(
tokenAccountToken: account?.token,
manualCookieHeader: manualCookieHeader)
}
}