mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-25 08:09:35 +00:00
* 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
246 lines
8.8 KiB
Swift
246 lines
8.8 KiB
Swift
import Foundation
|
|
|
|
/// Marks the refresh as an explicit user action without granting hidden access
|
|
/// to browser cookies, another app's Keychain items, or copied provider code.
|
|
@MainActor
|
|
enum CapacityDockProviderRefreshInteraction {
|
|
static func userInitiated<T>(
|
|
operation: () async throws -> T
|
|
) async rethrows -> T {
|
|
try await operation()
|
|
}
|
|
}
|
|
|
|
/// CodeBurn-owned registry for Capacity Dock providers that are not served by
|
|
/// AppStore's six native quota paths. A provider is connectable only when an
|
|
/// adapter is explicitly registered here.
|
|
@MainActor
|
|
final class CapacityDockProviderQuotaService {
|
|
struct Dependencies: Sendable {
|
|
// No defaults here: only `.live` may reach the real adapters, so a
|
|
// test can never silently read the local Cursor session or hit the
|
|
// network by omitting a field.
|
|
var refreshClinePass: @Sendable (String?) async throws -> QuotaSummary
|
|
var refreshCommandCode: @Sendable () async throws -> QuotaSummary
|
|
var refreshCursor: @Sendable () async throws -> QuotaSummary
|
|
var refreshDevin: @Sendable () async throws -> QuotaSummary
|
|
var refreshGrok: @Sendable () async throws -> QuotaSummary
|
|
var refreshGrokBot: @Sendable () async throws -> QuotaSummary
|
|
var refreshZai: @Sendable (String?) async throws -> QuotaSummary
|
|
var refreshZcode: @Sendable () async throws -> QuotaSummary
|
|
|
|
static let live = Dependencies(
|
|
refreshClinePass: { apiKey in
|
|
try await ClinePassSubscriptionService.refresh(apiKey: apiKey)
|
|
},
|
|
refreshCommandCode: {
|
|
try await CommandCodeSubscriptionService.refresh()
|
|
},
|
|
refreshCursor: {
|
|
try await CursorSubscriptionService.refresh()
|
|
},
|
|
refreshDevin: {
|
|
try DevinSubscriptionService.refresh()
|
|
},
|
|
refreshGrok: {
|
|
try await GrokBuildSubscriptionService.refresh()
|
|
},
|
|
refreshGrokBot: {
|
|
try await GrokBotSubscriptionService.refresh()
|
|
},
|
|
refreshZai: { apiKey in
|
|
try await ZaiSubscriptionService.refresh(apiKey: apiKey)
|
|
},
|
|
refreshZcode: {
|
|
try await ZcodeSubscriptionService.refresh()
|
|
}
|
|
)
|
|
}
|
|
|
|
static let shared = CapacityDockProviderQuotaService(dependencies: .live)
|
|
|
|
private let dependencies: Dependencies
|
|
|
|
init(dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
}
|
|
|
|
func fetch(
|
|
provider: CapacityDockProvider,
|
|
credential: CapacityDockProviderCredential
|
|
) async throws -> QuotaSummary {
|
|
switch provider.id {
|
|
case "cursor":
|
|
do {
|
|
return try await dependencies.refreshCursor()
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "devin":
|
|
do {
|
|
return try await dependencies.refreshDevin()
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "grok":
|
|
do {
|
|
return try await dependencies.refreshGrok()
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "grokbot":
|
|
do {
|
|
return try await dependencies.refreshGrokBot()
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "clinepass":
|
|
do {
|
|
return try await dependencies.refreshClinePass(credential.sanitizedOverride.apiKey)
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "commandcode":
|
|
do {
|
|
return try await dependencies.refreshCommandCode()
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "zai":
|
|
do {
|
|
return try await dependencies.refreshZai(credential.sanitizedOverride.apiKey)
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
case "zcode":
|
|
do {
|
|
return try await dependencies.refreshZcode()
|
|
} catch is CancellationError {
|
|
throw CancellationError()
|
|
} catch {
|
|
throw CapacityDockProviderFetchFailure(error: error)
|
|
}
|
|
default:
|
|
throw CapacityDockProviderFetchFailure(
|
|
message: "\(provider.displayName) does not have a CodeBurn live quota adapter yet. "
|
|
+ "Remove it from the dock or choose a supported provider.",
|
|
disposition: .terminal
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CapacityDockProviderFetchFailureDisposition: Equatable, Sendable {
|
|
case terminal
|
|
case transient
|
|
}
|
|
|
|
/// The only failure distinction the dock needs: user-actionable connection
|
|
/// failures versus failures that should preserve last-known quota and retry.
|
|
struct CapacityDockProviderFetchFailure: LocalizedError, Equatable, Sendable {
|
|
let message: String
|
|
let disposition: CapacityDockProviderFetchFailureDisposition
|
|
|
|
init(message: String, disposition: CapacityDockProviderFetchFailureDisposition) {
|
|
self.message = message
|
|
self.disposition = disposition
|
|
}
|
|
|
|
init(error: Error) {
|
|
message = error.localizedDescription
|
|
disposition = Self.disposition(for: error)
|
|
}
|
|
|
|
var errorDescription: String? { message }
|
|
|
|
static func disposition(for error: Error) -> CapacityDockProviderFetchFailureDisposition {
|
|
if let failure = error as? CapacityDockProviderFetchFailure {
|
|
return failure.disposition
|
|
}
|
|
if let error = error as? ClinePassSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? CommandCodeSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? CursorSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? DevinSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? GrokBuildSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? GrokBotSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? ZaiSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if let error = error as? ZcodeSubscriptionService.FetchError {
|
|
switch error.classification {
|
|
case .terminalAuth:
|
|
return .terminal
|
|
case .transient, .parseFailure:
|
|
return .transient
|
|
}
|
|
}
|
|
if error is URLError {
|
|
return .transient
|
|
}
|
|
// Unknown adapter failures are treated as temporary. They must never
|
|
// erase a valid last-known connection merely because classification is
|
|
// incomplete.
|
|
return .transient
|
|
}
|
|
}
|