docs/provider.md
Goal: adding a provider should feel like:
This doc describes the current provider architecture 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)Provider behavior is descriptor-driven. Two flat first-party manifests form the closed bootstrap boundary:
ProviderManifest lists core descriptors and ProviderImplementationManifest lists app implementations. The registries
retain thread-safe register(_:) methods for future dynamic providers.
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 + diagnosticsBrowserLocalStorageAPI: read origin-scoped key/value snapshots across browser profilesPTYAPI: 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 Foundation
public enum ExampleProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()
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: .init(provider: .example),
iconResourceName: "ProviderIcon-example",
color: ProviderColor(red: 0.2, green: 0.6, blue: 0.8),
confettiPalette: [
ProviderColor(hex: 0x3399CC),
ProviderColor(hex: 0x66C2FF),
]),
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 }
}
Hosted relays and upstream aggregators need enough public evidence for maintainers and users to evaluate the trust boundary:
An integration can be restored when missing operator or authorization evidence becomes available.
The mandatory registration checklist is intentionally short:
Sources/CodexBarCore/Providers/<Name>/ with the descriptor and fetch strategies.Sources/CodexBar/Providers/<Name>/ with the app implementation and optional settings extension.UsageProvider in Sources/CodexBarCore/Providers/Providers.swift.ProviderIcon-<id>.svg resource under Sources/CodexBar/Resources and reference it from branding.ProviderManifest.allDescriptors.ProviderImplementationManifest.makeImplementations.ProviderChoice AppEnum. New descriptors are widget-selectable by default; set
widgetSelectable: false in the provider's descriptor only when the provider genuinely cannot appear in widgets.Everything else is derived from the descriptor: icon-style identity, log-category construction, display and compact labels, default enablement, fetch/CLI metadata, icon validation, and widget display representations. The provider architecture gatekeeper test reports missing descriptor, implementation, icon, or widget registrations by provider ID.
Provider-specific behavior still deserves focused tests—for example snapshot mapping, strategy availability/fallback,
CLI aliases/source validation, and parser fixtures. Add a section to docs/providers.md when users need data-source or
authentication guidance.
Current: checkboxes per provider.
Preferred direction: table/list rows (like a “sessions” table):
This keeps the pane scannable once we have >5 providers.