docs/provider.md
Goal: adding a provider should feel like:
This doc describes the current provider architecture (post-macro registry) and the exact steps to add a new provider.
Sources/CodexBarCore: provider descriptors + fetch strategies + probes + parsing + shared utilities.Sources/CodexBar: UI/state + provider implementations (settings/login/menu hooks only).UsageProvider enum (used for persistence + widgets).ProviderDescriptor owns labels, URLs, default enablement, and fetch pipeline.ProviderFetchStrategy objects implement concrete fetch paths.Common building blocks already exist:
TTYCommandRunnerSubprocessRunnerBrowserCookieImporter (Safari/Chrome/Firefox adapters)OpenAIDashboardFetcher (WKWebView + JS)The old “switch provider” wiring is gone. Everything should be driven by the descriptor and its strategies.
Introduce a single descriptor per provider:
id (stable UsageProvider)--source modes + ordered strategy pipeline)usesAccountFallback for Codex auth.json)UI and settings should become descriptor-driven:
A provider declares a pipeline of strategies, in priority order. Each strategy:
kind (cli, web cookies, oauth, api token, local probe, web dashboard)UsageSnapshot (and optional credits/dashboard)--source or app settingsThe pipeline resolves to the best available strategy, and falls back on failure when allowed.
Each run returns a ProviderFetchOutcome with attempts + errors for debug UI and CLI --verbose.
Expose a narrow set of protocols/structs that provider implementations can use:
KeychainAPI: read-only, allowlisted service/account pairsBrowserCookieAPI: import cookies by domain list; returns cookie header + diagnosticsPTYAPI: run CLI interactions with timeouts + “send on substring” + stop rulesHTTPAPI: URLSession wrapper with domain allowlist + standard headers + tracingWebViewScrapeAPI: WKWebView lease + evaluateJavaScript + snapshot dumpingTokenCostAPI: Cost Usage local-log integration (Codex/Claude today; extend later)StatusAPI: status polling helpers (Statuspage + Workspace incidents)LoggerAPI: scoped logger + redaction helpersRule: providers do not talk to FileManager, Security, or “browser internals” directly unless they are the host API implementation.
Sources/CodexBarCore/Providers/<ProviderID>/
<ProviderID>Descriptor.swift (descriptor + strategy pipeline)<ProviderID>Strategies.swift (strategy implementations)<ProviderID>Probe.swift / <ProviderID>Fetcher.swift<ProviderID>Models.swift<ProviderID>Parser.swift (if text/HTML parsing)Sources/CodexBar/Providers/<ProviderID>/
<ProviderID>ProviderImplementation.swift (settings/login UI hooks only)import CodexBarMacroSupport
import Foundation
@ProviderDescriptorRegistration
@ProviderDescriptorDefinition
public enum ExampleProviderDescriptor {
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .example,
metadata: ProviderMetadata(
id: .example,
displayName: "Example",
sessionLabel: "Session",
weeklyLabel: "Weekly",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Example usage",
cliName: "example",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: nil,
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .codex,
iconResourceName: "ProviderIcon-example",
color: ProviderColor(red: 0.2, green: 0.6, blue: 0.8)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Example cost summary is not supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .cli],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [ExampleFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "example",
versionDetector: nil))
}
}
struct ExampleFetchStrategy: ProviderFetchStrategy {
let id: String = "example.cli"
let kind: ProviderFetchKind = .cli
func isAvailable(_: ProviderFetchContext) async -> Bool { true }
func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult {
let usage = UsageSnapshot(
primary: .init(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
secondary: nil,
updatedAt: Date(),
identity: nil)
return self.makeResult(usage: usage, sourceLabel: "cli")
}
func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { false }
}
Checklist:
UsageProvider case in Sources/CodexBarCore/Providers/Providers.swift.Sources/CodexBarCore/Providers/<ProviderID>/:
<ProviderID>Descriptor.swift: define ProviderDescriptor + fetch pipeline.<ProviderID>Strategies.swift: implement one or more ProviderFetchStrategy.<ProviderID>Probe.swift / <ProviderID>Fetcher.swift: concrete fetcher logic.<ProviderID>Models.swift: snapshot structs.<ProviderID>Parser.swift (if needed).@ProviderDescriptorRegistration + @ProviderDescriptorDefinition to the descriptor type.
Implement static func makeDescriptor() -> ProviderDescriptor.@ProviderImplementationRegistration to the implementation type (macros auto-register).
Sources/CodexBar/Providers/<ProviderID>/<ProviderID>ProviderImplementation.swift:
ProviderImplementation only for settings/login UI hooks.iconName must match ProviderIcon-<id> asset.cliName, cliAliases, sourceModes, versionProvider in descriptor.--source modes apply.UsageSnapshot mapping unit testsdocs/providers.md with data source + auth notesdocs/provider.md if the pipeline model changesCurrent: checkboxes per provider.
Preferred direction: table/list rows (like a “sessions” table):
This keeps the pane scannable once we have >5 providers.