| summary | read_when | |||
|---|---|---|---|---|
Provider authoring guide: shared host APIs, provider boundaries, and how to add a new provider. |
|
Goal: adding a provider should feel like:
- add one folder
- define one descriptor + strategies
- add one implementation (UI hooks only)
- done (tests + docs)
This doc describes the current provider architecture (post-macro registry) and the exact steps to add a new provider.
- Provider: a source of usage/quota/status data (Codex, Claude, Gemini, Antigravity, Cursor, …).
- Descriptor: the single source of truth for labels, URLs, defaults, and fetch strategies.
- Fetch strategy: one concrete way to obtain usage (CLI, web cookies, OAuth API, local probe, etc.).
- Host APIs: shared capabilities we provide to providers (Keychain, browser cookies, PTY, HTTP, WebView scrape, token-cost).
- Identity fields: email/org/plan/loginMethod. Must stay siloed per provider.
Sources/CodexBarCore: provider descriptors + fetch strategies + probes + parsing + shared utilities.Sources/CodexBar: UI/state + provider implementations (settings/login/menu hooks only).- Provider IDs are compile-time:
UsageProviderenum (used for persistence + widgets). - Provider wiring is descriptor-driven:
ProviderDescriptorowns labels, URLs, default enablement, and fetch pipeline.ProviderFetchStrategyobjects implement concrete fetch paths.- CLI + app both call the same descriptor/fetch pipeline.
Common building blocks already exist:
- PTY:
TTYCommandRunner - subprocess:
SubprocessRunner - cookie import:
BrowserCookieImporter(Safari/Chrome/Firefox adapters) - OpenAI dashboard web scrape:
OpenAIDashboardFetcher(WKWebView + JS) - cost usage: local log scanner (Codex + Claude)
The old “switch provider” wiring is gone. Everything should be driven by the descriptor and its strategies.
Introduce a single descriptor per provider:
id(stableUsageProvider)- display/labels/URLs (menu title, dashboard URL, status URL)
- UI branding (icon name, primary color)
- capabilities (supportsCredits, supportsTokenCost, supportsStatusPolling, supportsLogin)
- fetch plan (allowed
--sourcemodes + ordered strategy pipeline) - CLI metadata (cliName, aliases, version provider)
- account behavior (e.g.,
usesAccountFallbackfor Codex auth.json)
UI and settings should become descriptor-driven:
- no provider-specific branching for labels/links/toggle titles
- minimal provider-specific UI (only when a provider truly needs bespoke UX)
A provider declares a pipeline of strategies, in priority order. Each strategy:
- advertises a
kind(cli, web cookies, oauth, api token, local probe, web dashboard) - declares availability (checks settings, cookies, env vars, installed CLI)
- fetches
UsageSnapshot(and optional credits/dashboard) - can be filtered by CLI
--sourceor app settings
The 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 helpers
Rule: 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 }
}- Identity silo: never display identity/plan fields from provider A inside provider B UI.
- Privacy: default to on-device parsing; browser cookies are opt-in and never persisted by us beyond WebKit stores.
- Reliability: providers must be timeout-bounded; no unbounded waits on network/PTY/UI.
- Degradation: prefer cached data over flapping; show clear errors when stale.
Checklist:
- Add
UsageProvidercase inSources/CodexBarCore/Providers/Providers.swift. - Create
Sources/CodexBarCore/Providers/<ProviderID>/:<ProviderID>Descriptor.swift: defineProviderDescriptor+ fetch pipeline.<ProviderID>Strategies.swift: implement one or moreProviderFetchStrategy.<ProviderID>Probe.swift/<ProviderID>Fetcher.swift: concrete fetcher logic.<ProviderID>Models.swift: snapshot structs.<ProviderID>Parser.swift(if needed).
- Attach
@ProviderDescriptorRegistration+@ProviderDescriptorDefinitionto the descriptor type. Implementstatic func makeDescriptor() -> ProviderDescriptor. - Attach
@ProviderImplementationRegistrationto the implementation type (macros auto-register).- No manual list edits.
- Add
Sources/CodexBar/Providers/<ProviderID>/<ProviderID>ProviderImplementation.swift:ProviderImplementationonly for settings/login UI hooks.
- Add icons + color in descriptor:
iconNamemust matchProviderIcon-<id>asset.- Color used in menu cards + switcher.
- If CLI-specific behavior is needed:
- add
cliName,cliAliases,sourceModes,versionProviderin descriptor. - strategies decide which
--sourcemodes apply.
- add
- Tests:
UsageSnapshotmapping unit tests- strategy availability + fallback tests
- CLI provider parsing (aliases + --source validation)
- Docs:
- add provider section in
docs/providers.mdwith data source + auth notes - update
docs/provider.mdif the pipeline model changes
- add provider section in
Current: checkboxes per provider.
Preferred direction: table/list rows (like a “sessions” table):
- Provider (name + short auth hint)
- Enabled toggle
- Status (ok/stale/error + last updated)
- Auth source (CLI / cookies / web / oauth) when applicable
- Actions (Login / Diagnose / Copy debug log)
This keeps the pane scannable once we have >5 providers.