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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.29.2 — Unreleased

### Added
- MiniMax: add a redacted diagnostic CLI export for safe issue reports (#1128). Thanks @Yuxin-Qiao!
- Antigravity: show the complete per-model quota breakdown alongside the existing summary lanes (#1139). Thanks @guhyun9454!
- Widget: show tertiary usage rows for providers that expose a third quota lane (#1160). Thanks @LeoLin990405!
- DeepSeek: show optional web-session usage and cost summaries alongside the balance card (#1166). Thanks @Yuxin-Qiao!
Expand Down
129 changes: 129 additions & 0 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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 providerRaw = values.options["provider"]?.last ?? "minimax"
guard providerRaw.lowercased() == "minimax" else {
Self.exit(
code: .failure,
message: "Error: only 'minimax' provider is supported for diagnose",
output: output,
kind: .args)
}

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 pretty = values.flags.contains("pretty")
let verbose = values.flags.contains("verbose")
let browserDetection = BrowserDetection()
let fetcher = 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)
}

let activeMiniMaxAccount: ProviderTokenAccount? = {
let accounts = (try? tokenContext.resolvedAccounts(for: .minimax)) ?? []
return accounts.first
}()
let env = tokenContext.environment(
base: ProcessInfo.processInfo.environment,
provider: .minimax,
account: activeMiniMaxAccount,
codexActiveSourceOverride: nil)
let settings = tokenContext.settingsSnapshot(
for: .minimax,
account: activeMiniMaxAccount,
codexActiveSourceOverride: nil)
let sourceMode = tokenContext.preferredSourceMode(for: .minimax)

let authMode = Self.resolveMiniMaxAuthMode(environment: env, settings: settings)

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)

let outcome = await Self.fetchProviderUsage(provider: .minimax, context: fetchContext)

let diagnostic = MiniMaxDiagnosticExportBuilder.build(
outcome: outcome,
settings: settings,
authMode: authMode)

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
if pretty {
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
} else {
encoder.outputFormatting = .sortedKeys
}

do {
let data = try encoder.encode(diagnostic)
var jsonString = String(data: data, encoding: .utf8) ?? "{}"
jsonString = LogRedactor.redact(jsonString)
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)
}
}

extension CodexBarCLI {
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 _resolveMiniMaxAuthModeForTesting(
environment: [String: String],
settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode
{
self.resolveMiniMaxAuthMode(environment: environment, settings: settings)
}
}
#endif
19 changes: 16 additions & 3 deletions Sources/CodexBarCLI/CLIEntry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ enum CodexBarCLI {

do {
let invocation = try program.resolve(argv: argv)
Self.bootstrapLogging(values: invocation.parsedValues)
Self.bootstrapLogging(path: invocation.path, values: invocation.parsedValues)
switch invocation.path {
case ["usage"]:
await self.runUsage(invocation.parsedValues)
Expand All @@ -52,6 +52,8 @@ enum CodexBarCLI {
self.runConfigSetAPIKey(invocation.parsedValues)
case ["cache", "clear"]:
self.runCacheClear(invocation.parsedValues)
case ["diagnose"]:
await self.runDiagnose(invocation.parsedValues)
default:
Self.exit(
code: .failure,
Expand All @@ -74,6 +76,7 @@ enum CodexBarCLI {
let configProviderToggleSignature = CommandSignature.describe(ConfigProviderToggleOptions())
let configSetAPIKeySignature = CommandSignature.describe(ConfigSetAPIKeyOptions())
let cacheSignature = CommandSignature.describe(CacheOptions())
let diagnoseSignature = CommandSignature.describe(DiagnoseOptions())

return [
CommandDescriptor(
Expand Down Expand Up @@ -142,17 +145,27 @@ enum CodexBarCLI {
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(values: ParsedValues) {
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)
CodexBarLog.bootstrapIfNeeded(.init(destination: .stderr, level: level, json: isJSON))
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 {
Expand Down
22 changes: 22 additions & 0 deletions Sources/CodexBarCLI/CLIHelp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,26 @@ extension CodexBarCLI {
"""
}

static func diagnoseHelp(version: String) -> String {
"""
CodexBar \(version)

Usage:
codexbar diagnose --provider minimax --format json
[--json-output] [--log-level <trace|verbose|debug|info|warning|error|critical>]
[-v|--verbose]
[--pretty]

Description:
Run a MiniMax diagnostic fetch 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 --pretty
"""
}

static func rootHelp(version: String) -> String {
"""
CodexBar \(version)
Expand Down Expand Up @@ -198,6 +218,7 @@ extension CodexBarCLI {
codexbar config disable --provider <name>
codexbar config set-api-key --provider <name> (--api-key <key>|--stdin)
codexbar cache clear <--cookies|--cost|--all> [--provider <name>]
codexbar diagnose --provider minimax --format json [--pretty]

Global flags:
-h, --help Show help
Expand All @@ -218,6 +239,7 @@ extension CodexBarCLI {
codexbar config enable --provider grok
codexbar config set-api-key --provider elevenlabs --stdin
codexbar cache clear --cookies
codexbar diagnose --provider minimax --format json --pretty
"""
}
}
4 changes: 4 additions & 0 deletions Sources/CodexBarCLI/CLIHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,10 @@ extension CodexBarCLI {
CommandSignature.describe(CacheOptions())
}

static func _diagnoseSignatureForTesting() -> CommandSignature {
CommandSignature.describe(DiagnoseOptions())
}

static func _configSetAPIKeySignatureForTesting() -> CommandSignature {
CommandSignature.describe(ConfigSetAPIKeyOptions())
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ extension CodexBarCLI {
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))
}
Expand Down
23 changes: 23 additions & 0 deletions Sources/CodexBarCLI/DiagnoseOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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: "Provider to diagnose: minimax")
var provider: String?

@Option(name: .long("format"), help: "Output format: json")
var format: String?

@Flag(name: .long("pretty"), help: "Pretty-print JSON output")
var pretty: Bool = false
}
15 changes: 15 additions & 0 deletions Sources/CodexBarCore/Logging/CodexBarLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Logging
public enum CodexBarLog {
public enum Destination: Sendable {
case stderr
case discard
case oslog(subsystem: String)
}

Expand Down Expand Up @@ -85,6 +86,8 @@ public enum CodexBarLog {
case .stderr:
if config.json { return JSONStderrLogHandler(label: label) }
return StreamLogHandler.standardError(label: label)
case .discard:
return DiscardLogHandler()
case let .oslog(subsystem):
#if canImport(os)
return OSLogLogHandler(label: label, subsystem: subsystem)
Expand Down Expand Up @@ -154,6 +157,18 @@ public enum CodexBarLog {
}
}

private struct DiscardLogHandler: LogHandler {
var metadata: Logger.Metadata = [:]
var logLevel: Logger.Level = .critical

subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
get { self.metadata[metadataKey] }
set { self.metadata[metadataKey] = newValue }
}

func log(event _: LogEvent) {}
}

public struct CodexBarLogger: Sendable {
private let logFn: @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void

Expand Down
12 changes: 11 additions & 1 deletion Sources/CodexBarCore/Logging/LogRedactor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ public enum LogRedactor {
pattern: #"(?i)(authorization\s*:\s*)([^\r\n]+)"#)
private static let bearerRegex = Self.makeRegex(
pattern: #"(?i)\bbearer\s+[a-z0-9._\-]+=*\b"#)
private static let minimaxCodingPlanTokenRegex = Self.makeRegex(
pattern: #"sk-cp-[^\s"'`;,)>\]]+"#)
private static let minimaxApiTokenRegex = Self.makeRegex(
pattern: #"sk-api-[^\s"'`;,)>\]]+"#)

public static func redact(_ text: String) -> String {
var output = text
// Email is broad and safe first
output = self.replace(self.emailRegex, in: output, with: "<redacted-email>")
// MiniMax tokens before broader rules catch them
output = self.replace(self.minimaxCodingPlanTokenRegex, in: output, with: "<redacted-minimax-token>")
output = self.replace(self.minimaxApiTokenRegex, in: output, with: "<redacted-minimax-token>")
// Bearer catches "bearer <token>" before authorization wraps it
output = self.replace(self.bearerRegex, in: output, with: "Bearer <redacted>")
// Authorization catches the rest (already-redacted content)
output = self.replace(self.cookieHeaderRegex, in: output, with: "$1<redacted>")
output = self.replace(self.authorizationRegex, in: output, with: "$1<redacted>")
output = self.replace(self.bearerRegex, in: output, with: "Bearer <redacted>")
return output
}

Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBarCore/Providers/MiniMax/MiniMaxAuthMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public enum MiniMaxAuthMode: Sendable {
self != .apiToken
}

public var description: String {
switch self {
case .apiToken: "apiToken"
case .cookie: "cookie"
case .none: "none"
}
}

private static func cleaned(_ raw: String?) -> String? {
guard let value = raw?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
Expand Down
Loading