202 lines
9.2 KiB
Markdown
202 lines
9.2 KiB
Markdown
---
|
|
summary: "Provider authoring guide: shared host APIs, provider boundaries, and how to add a new provider."
|
|
read_when:
|
|
- Adding a new provider (usage + status + identity)
|
|
- Refactoring provider architecture or shared host APIs
|
|
- Reviewing provider boundaries (no identity leakage)
|
|
---
|
|
|
|
# Provider authoring guide
|
|
|
|
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** and the exact steps to add a new provider.
|
|
|
|
## Terms
|
|
- **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**.
|
|
|
|
## Architecture overview (now)
|
|
- `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: `UsageProvider` enum (used for persistence + widgets).
|
|
- Provider wiring is descriptor-driven:
|
|
- `ProviderDescriptor` owns labels, URLs, default enablement, and fetch pipeline.
|
|
- `ProviderFetchStrategy` objects 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)
|
|
|
|
Provider behavior is descriptor-driven. Two explicit, exhaustive registries form the bootstrap boundary:
|
|
`ProviderDescriptorRegistry` owns core descriptors and `ProviderImplementationRegistry` owns app implementations.
|
|
|
|
## Provider descriptor (source of truth)
|
|
|
|
Introduce a single descriptor per provider:
|
|
- `id` (stable `UsageProvider`)
|
|
- display/labels/URLs (menu title, dashboard URL, status URL)
|
|
- UI branding (icon name, primary color)
|
|
- capabilities (supportsCredits, supportsTokenCost, supportsStatusPolling, supportsLogin)
|
|
- fetch plan (allowed `--source` modes + ordered strategy pipeline)
|
|
- CLI metadata (cliName, aliases, version provider)
|
|
- account behavior (e.g., `usesAccountFallback` for 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)
|
|
|
|
## Fetch strategies
|
|
|
|
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 `--source` or 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`.
|
|
|
|
## Host APIs are explicit, small, testable
|
|
Expose a narrow set of protocols/structs that provider implementations can use:
|
|
- `KeychainAPI`: read-only, allowlisted service/account pairs
|
|
- `BrowserCookieAPI`: import cookies by domain list; returns cookie header + diagnostics
|
|
- `PTYAPI`: run CLI interactions with timeouts + “send on substring” + stop rules
|
|
- `HTTPAPI`: URLSession wrapper with domain allowlist + standard headers + tracing
|
|
- `WebViewScrapeAPI`: WKWebView lease + `evaluateJavaScript` + snapshot dumping
|
|
- `TokenCostAPI`: 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.
|
|
|
|
## Provider-specific code layout
|
|
- `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)
|
|
|
|
## Minimal provider example (copy-paste)
|
|
|
|
```swift
|
|
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: .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 }
|
|
}
|
|
```
|
|
|
|
## Guardrails (non-negotiable)
|
|
- 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.
|
|
|
|
## Adding a new provider (current flow)
|
|
|
|
Checklist:
|
|
- Add `UsageProvider` case in `Sources/CodexBarCore/Providers/Providers.swift`.
|
|
- Create `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).
|
|
- Define a cached `public static let descriptor` and `static func makeDescriptor() -> ProviderDescriptor`.
|
|
- Add the descriptor to `ProviderDescriptorRegistry.descriptorsByID`.
|
|
- Add `Sources/CodexBar/Providers/<ProviderID>/<ProviderID>ProviderImplementation.swift`:
|
|
- `ProviderImplementation` only for settings/login UI hooks.
|
|
- Add an exhaustive case to `ProviderImplementationRegistry.makeImplementation(for:)`.
|
|
- Add icons + color in descriptor:
|
|
- `iconName` must match `ProviderIcon-<id>` asset.
|
|
- Color used in menu cards + switcher.
|
|
- If CLI-specific behavior is needed:
|
|
- add `cliName`, `cliAliases`, `sourceModes`, `versionProvider` in descriptor.
|
|
- strategies decide which `--source` modes apply.
|
|
- Tests:
|
|
- `UsageSnapshot` mapping unit tests
|
|
- strategy availability + fallback tests
|
|
- CLI provider parsing (aliases + --source validation)
|
|
- Docs:
|
|
- add provider section in `docs/providers.md` with data source + auth notes
|
|
- update `docs/provider.md` if the pipeline model changes
|
|
|
|
## UI notes (Providers settings)
|
|
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.
|