codeburn/mac/Sources/CodeBurnMenubar/Data/ProviderConnectionCatalog.swift
Resham Joshi 39b9b3adcd
feat(quota): Command Code quota and ClinePass from the Cline sign-in (#1682)
* feat(quota): Command Code quota and ClinePass from the Cline sign-in

Command Code: reads the CLI's ~/.commandcode/auth.json key and the billing
credits and subscription endpoints. 5-hour and weekly windows as used of
cap, remaining credits as a note, plan from planId. Live in the Mac dock
from the local CLI, in codeburn quota and the Windows dock.

ClinePass: without a saved or env key, falls back to the Cline CLI's
providers.json session (accessToken, then apiKey, then auth.apiKey, with
Cline's path overrides). Read-only; an expired session stops before the
network and says to run cline. The Mac catalog adds the local CLI source
so Automatic connects without a key.

* fix(quota): keep retrying after an expired Cline sign-in

Running cline refreshes the session file CodeBurn only reads, so an
expired or rejected Cline session is now a retryable failure that keeps
the last quota and shows the guidance, on the Mac and in the CLI. A
rejected API key and a missing credential stay terminal.

* test(quota): build expected Cline paths with path.join for Windows
2026-10-07 23:34:31 +02:00

232 lines
12 KiB
Swift

import Foundation
/// Fetch-source vocabulary used by Capacity Dock's provider catalog.
enum ProviderReferenceSourceMode: String, Codable, CaseIterable, Hashable, Sendable {
case automatic = "auto"
case web
case cli
case oauth
case api
}
/// User-facing connection semantics behind a provider's fetch sources.
///
/// These are broader than raw fetch modes. Gemini's local OAuth probe is stored
/// as an `api` source, while the credential the user experiences is still a
/// local CLI OAuth session.
enum ProviderAuthMethod: String, Codable, CaseIterable, Hashable, Sendable {
case localAppOrCLI
case oauth
case apiTokenOrCloudCredentials
case cookieOrWebSession
case localhost
case none
var title: String {
switch self {
case .localAppOrCLI: L("Installed app or CLI")
case .oauth: L("OAuth")
case .apiTokenOrCloudCredentials: L("API or cloud credentials")
case .cookieOrWebSession: L("Browser session")
case .localhost: L("Localhost service")
case .none: L("No sign-in required")
}
}
}
struct ProviderConnectionCatalogEntry: Equatable, Sendable {
let id: String
let displayName: String
let sourceModes: Set<ProviderReferenceSourceMode>
let authMethods: Set<ProviderAuthMethod>
let hasLiveCodeBurnQuotaAdapter: Bool
}
/// Declarative connection inventory owned by CodeBurn. The live flag identifies
/// every provider with a native CodeBurn quota adapter.
enum ProviderConnectionCatalog {
static let inventoryRevision = "5b09e44d0f9e60107d3fbed232e5f6e17d486df3"
static let providers: [ProviderConnectionCatalogEntry] = [
entry("codex", "Codex", [.automatic, .web, .cli, .oauth, .api],
[.localAppOrCLI, .oauth, .apiTokenOrCloudCredentials, .cookieOrWebSession], live: true),
entry("openai", "OpenAI", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("azureopenai", "Azure OpenAI", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("claude", "Claude", [.automatic, .api, .web, .cli, .oauth],
[.localAppOrCLI, .oauth, .apiTokenOrCloudCredentials, .cookieOrWebSession], live: true),
entry("clinepass", "ClinePass", [.automatic, .api, .cli],
[.localAppOrCLI, .apiTokenOrCloudCredentials], live: true),
entry("cursor", "Cursor", [.automatic, .cli, .web], [.localAppOrCLI, .cookieOrWebSession], live: true),
entry("opencode", "OpenCode", [.automatic, .web], [.cookieOrWebSession]),
entry("opencodego", "OpenCode Go", [.automatic, .api, .web],
[.localAppOrCLI, .apiTokenOrCloudCredentials, .cookieOrWebSession, .none]),
entry("alibaba", "Alibaba Coding Plan", [.automatic, .web, .api],
[.cookieOrWebSession, .apiTokenOrCloudCredentials]),
entry("alibabatokenplan", "Alibaba Token Plan", [.automatic, .cli, .web],
[.localAppOrCLI, .cookieOrWebSession]),
entry("qwencloud", "Qwen Cloud", [.automatic, .web], [.cookieOrWebSession]),
entry("factory", "Droid", [.automatic, .api, .web, .cli],
[.localAppOrCLI, .apiTokenOrCloudCredentials, .cookieOrWebSession]),
entry("fireworks", "Fireworks", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("gemini", "Gemini", [.automatic, .api], [.localAppOrCLI, .oauth], live: true),
entry("antigravity", "Antigravity", [.automatic, .cli, .oauth],
[.localAppOrCLI, .oauth, .localhost, .none], live: true),
entry("copilot", "Copilot", [.automatic, .api],
[.oauth, .apiTokenOrCloudCredentials, .cookieOrWebSession], live: true),
entry("devin", "Devin", [.automatic, .cli], [.localAppOrCLI], live: true),
entry("zai", "Z.ai", [.automatic, .api],
[.localAppOrCLI, .apiTokenOrCloudCredentials], live: true),
entry("zcode", "ZCode", [.automatic, .cli],
[.localAppOrCLI], live: true),
entry("minimax", "MiniMax", [.automatic, .web, .api],
[.cookieOrWebSession, .apiTokenOrCloudCredentials]),
entry("manus", "Manus", [.automatic, .web], [.cookieOrWebSession]),
entry("kimi", "Kimi Code", [.automatic, .api, .web],
[.apiTokenOrCloudCredentials, .cookieOrWebSession], live: true),
entry("kilo", "Kilo", [.automatic, .api, .cli],
[.apiTokenOrCloudCredentials, .localAppOrCLI]),
entry("kiro", "Kiro", [.automatic, .cli], [.localAppOrCLI]),
entry("vertexai", "Vertex AI", [.automatic, .oauth], [.oauth]),
entry("augment", "Augment", [.automatic, .cli], [.localAppOrCLI, .cookieOrWebSession]),
entry("jetbrains", "JetBrains AI", [.automatic, .cli], [.localAppOrCLI]),
entry("moonshot", "Moonshot", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("amp", "Amp", [.automatic, .api, .web, .cli],
[.localAppOrCLI, .apiTokenOrCloudCredentials, .cookieOrWebSession]),
entry("t3chat", "T3 Chat", [.automatic, .web], [.cookieOrWebSession]),
entry("ollama", "Ollama", [.automatic, .web, .api],
[.cookieOrWebSession, .apiTokenOrCloudCredentials, .localhost]),
entry("synthetic", "Synthetic", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("openrouter", "OpenRouter", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("elevenlabs", "ElevenLabs", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("warp", "Warp", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("windsurf", "Windsurf", [.automatic, .web, .cli],
[.cookieOrWebSession, .localAppOrCLI]),
entry("zed", "Zed", [.automatic, .api], [.localAppOrCLI, .none]),
entry("perplexity", "Perplexity", [.automatic, .web], [.cookieOrWebSession]),
entry("mimo", "Xiaomi MiMo", [.automatic, .web], [.localAppOrCLI, .cookieOrWebSession]),
entry("doubao", "Doubao", [.automatic, .cli, .api],
[.localAppOrCLI, .apiTokenOrCloudCredentials]),
entry("sakana", "Sakana AI", [.automatic, .web], [.cookieOrWebSession]),
entry("abacus", "Abacus AI", [.automatic, .web], [.cookieOrWebSession]),
entry("mistral", "Mistral", [.automatic, .web], [.cookieOrWebSession]),
entry("deepseek", "DeepSeek", [.automatic, .api, .web],
[.apiTokenOrCloudCredentials, .cookieOrWebSession]),
entry("deepinfra", "DeepInfra", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("codebuff", "Codebuff", [.automatic, .api],
[.localAppOrCLI, .apiTokenOrCloudCredentials]),
entry("crof", "Crof", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("venice", "Venice", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("commandcode", "Command Code", [.automatic, .cli], [.localAppOrCLI], live: true),
entry("qoder", "Qoder", [.automatic, .web], [.cookieOrWebSession]),
entry("stepfun", "StepFun", [.automatic, .web], [.cookieOrWebSession]),
entry("bedrock", "AWS Bedrock", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("grok", "Grok", [.automatic, .cli, .oauth],
[.localAppOrCLI, .oauth], live: true),
entry("grokbot", "Grok Bot", [.automatic, .cli], [.localAppOrCLI], live: true),
entry("groq", "Groq", [.automatic, .web, .api],
[.cookieOrWebSession, .apiTokenOrCloudCredentials]),
entry("llmproxy", "LLM Proxy", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("litellm", "LiteLLM", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("deepgram", "Deepgram", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("poe", "Poe", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("chutes", "Chutes", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("neuralwatt", "Neuralwatt", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("clawrouter", "ClawRouter", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("longcat", "LongCat", [.automatic, .web], [.cookieOrWebSession]),
entry("sub2api", "sub2api", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("wayfinder", "Wayfinder", [.automatic, .api], [.localhost, .none]),
entry("zenmux", "ZenMux", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("aiand", "ai&", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("zoommate", "ZoomMate", [.automatic, .web], [.cookieOrWebSession]),
entry("xai", "xAI", [.automatic, .api], [.apiTokenOrCloudCredentials]),
entry("notion", "Notion AI", [.automatic, .web], [.cookieOrWebSession]),
entry("ibmbob", "IBM Bob", [.automatic, .api], [.apiTokenOrCloudCredentials]),
]
private static func entry(
_ id: String,
_ displayName: String,
_ sourceModes: Set<ProviderReferenceSourceMode>,
_ authMethods: Set<ProviderAuthMethod>,
live: Bool = false
) -> ProviderConnectionCatalogEntry {
ProviderConnectionCatalogEntry(
id: id,
displayName: displayName,
sourceModes: sourceModes,
authMethods: authMethods,
hasLiveCodeBurnQuotaAdapter: live
)
}
static func entry(id: String) -> ProviderConnectionCatalogEntry? {
providers.first { $0.id == id }
}
}
enum ProviderConnectionGuidance {
static func instruction(for provider: CapacityDockProvider) -> String {
let methods = provider.catalogEntry.authMethods
// ClinePass rides on the Cline sign-in; there is no ClinePass app.
if provider.id == "clinepass" {
return L("Sign in with Cline, or enter a ClinePass API key below, then click Retry.")
}
if methods == [.apiTokenOrCloudCredentials] {
return L("Enter an API key or token below, then press Save & Connect.")
}
if methods == [.cookieOrWebSession] {
return L("Sign in to %@ in a supported browser, then click Retry.", provider.displayName)
}
if methods.contains(.localAppOrCLI) {
return L("Sign in with the %@ app or CLI, then click Retry.", provider.displayName)
}
if methods.contains(.oauth) {
return L("Complete %@ OAuth, then click Retry.", provider.displayName)
}
if methods.contains(.localhost) {
return L("Start the local %@ service, then click Retry.", provider.displayName)
}
if methods.contains(.apiTokenOrCloudCredentials) {
return L("Enter the required API or cloud credentials below, then press Save & Connect.")
}
if methods.contains(.cookieOrWebSession) {
return L("Sign in to %@ in a supported browser, then click Retry.", provider.displayName)
}
return L("No sign-in is required. Click Retry to refresh quota.")
}
static func dockInstruction(for provider: CapacityDockProvider) -> String {
let methods = provider.catalogEntry.authMethods
if methods == [.apiTokenOrCloudCredentials] {
return L("Add an API key or token in Provider Settings.")
}
if methods.contains(.apiTokenOrCloudCredentials),
!methods.contains(.localAppOrCLI),
!methods.contains(.cookieOrWebSession),
!methods.contains(.oauth) {
return L("Add the required API or cloud credentials in Provider Settings.")
}
return instruction(for: provider)
}
}
enum ProviderConnectionSubmissionPolicy {
enum Action: Equatable {
case connect
case saveAndConnect
case requiresCredential
}
static func resolve(
credential: CapacityDockProviderCredential,
savedCredential: CapacityDockProviderCredential,
requiresExplicitCredential: Bool
) -> Action {
if requiresExplicitCredential,
credential.apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return .requiresCredential
}
if credential != savedCredential { return .saveAndConnect }
return .connect
}
}