From 299e9cd3de91d4feed532f738e6d491c2c2129fd Mon Sep 17 00:00:00 2001 From: Ruben Beuker Date: Mon, 18 May 2026 13:11:57 +0200 Subject: [PATCH 1/2] Add Command Code provider --- CopilotMonitor/CLI/CLIProviderManager.swift | 4 +- .../CopilotMonitor.xcodeproj/project.pbxproj | 10 + .../App/StatusBarController.swift | 5 + .../Helpers/ProviderMenuBuilder.swift | 46 ++ .../Models/ProviderProtocol.swift | 7 + .../Models/SubscriptionSettings.swift | 9 + .../Providers/CommandCodeProvider.swift | 490 ++++++++++++++++++ .../Services/BrowserCookieService.swift | 129 ++++- .../Services/ProviderManager.swift | 1 + .../MultiProviderStatusBarIconView.swift | 2 + .../SwiftUI/ModernStatusBarIconView.swift | 1 + .../CommandCodeProviderTests.swift | 117 +++++ 12 files changed, 819 insertions(+), 2 deletions(-) create mode 100644 CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift create mode 100644 CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift diff --git a/CopilotMonitor/CLI/CLIProviderManager.swift b/CopilotMonitor/CLI/CLIProviderManager.swift index f463174..7cc4ab0 100644 --- a/CopilotMonitor/CLI/CLIProviderManager.swift +++ b/CopilotMonitor/CLI/CLIProviderManager.swift @@ -13,7 +13,7 @@ actor CLIProviderManager { private let fetchTimeout: TimeInterval = 10.0 static let registeredProviders: [ProviderIdentifier] = [ - .claude, .codex, .cursor, .geminiCLI, .openRouter, + .claude, .codex, .commandCode, .cursor, .geminiCLI, .openRouter, .antigravity, .openCodeZen, .kimi, .minimaxCodingPlan, .zaiCodingPlan, .nanoGpt, .chutes, .copilot, @@ -27,6 +27,7 @@ actor CLIProviderManager { // Shared providers (no UI dependencies) let claudeProvider = ClaudeProvider() let codexProvider = CodexProvider() + let commandCodeProvider = CommandCodeProvider() let cursorProvider = CursorProvider() let geminiCLIProvider = GeminiCLIProvider() let openRouterProvider = OpenRouterProvider() @@ -45,6 +46,7 @@ actor CLIProviderManager { self.providers = [ claudeProvider, codexProvider, + commandCodeProvider, cursorProvider, geminiCLIProvider, openRouterProvider, diff --git a/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj b/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj index b86b134..5f31191 100644 --- a/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj +++ b/CopilotMonitor/CopilotMonitor.xcodeproj/project.pbxproj @@ -48,6 +48,9 @@ CLIANTIGRAVITY11111111 /* AntigravityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1DA8E47F4CFEFC3571708AD /* AntigravityProvider.swift */; }; CLICLAUDE11111111111111 /* ClaudeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DDC1B4DE6B5118CE4AE8F82 /* ClaudeProvider.swift */; }; CLICODEX111111111111111 /* CodexProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76783C44AA2329AE3FA7E981 /* CodexProvider.swift */; }; + CMDCODEAPP111111111111 /* CommandCodeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CMDCODEFILE11111111111 /* CommandCodeProvider.swift */; }; + CMDCODECLI111111111111 /* CommandCodeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CMDCODEFILE11111111111 /* CommandCodeProvider.swift */; }; + CMDCODETESTBF111111111 /* CommandCodeProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CMDCODETESTFR111111111 /* CommandCodeProviderTests.swift */; }; CLICURSOR1111111111111 /* CursorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CURSORFILE222222222222 /* CursorProvider.swift */; }; CLICOPILOT11111111111111 /* CopilotCLIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CLICOPILOT22222222222222 /* CopilotCLIProvider.swift */; }; CLICOPILOTUSAGE111111111 /* CopilotUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22222222222222222222222 /* CopilotUsage.swift */; }; @@ -177,6 +180,8 @@ CLI5555555555555555555555 /* opencodebar-cli */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "opencodebar-cli"; sourceTree = BUILT_PRODUCTS_DIR; }; CLICOPILOT22222222222222 /* CopilotCLIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotCLIProvider.swift; sourceTree = ""; }; CLIPROVMGR22222222222222 /* CLIProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIProviderManager.swift; sourceTree = ""; }; + CMDCODEFILE11111111111 /* CommandCodeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandCodeProvider.swift; sourceTree = ""; }; + CMDCODETESTFR111111111 /* CommandCodeProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandCodeProviderTests.swift; sourceTree = ""; }; CURSORFILE222222222222 /* CursorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CursorProvider.swift; sourceTree = ""; }; CURSORTSTFR11111111111 /* CursorProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CursorProviderTests.swift; sourceTree = ""; }; D22222222222222222222222 /* StatusBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarController.swift; sourceTree = ""; }; @@ -269,6 +274,7 @@ 06EC3B683EB6892E4F9C8316 /* GeminiCLIProvider.swift */, 4DDC1B4DE6B5118CE4AE8F82 /* ClaudeProvider.swift */, 76783C44AA2329AE3FA7E981 /* CodexProvider.swift */, + CMDCODEFILE11111111111 /* CommandCodeProvider.swift */, CURSORFILE222222222222 /* CursorProvider.swift */, F4AE672516E564CC52739BD9 /* CopilotProvider.swift */, OC2222222222222222222222 /* OpenCodeProvider.swift */, @@ -430,6 +436,7 @@ CURSORTSTFR11111111111 /* CursorProviderTests.swift */, TOKENTESTFR1111111111111 /* TokenManagerTests.swift */, CODEXTESTFR111111111111 /* CodexProviderTests.swift */, + CMDCODETESTFR111111111 /* CommandCodeProviderTests.swift */, OCAUTHTESTFR11111111111 /* OpenCodeAuthDecodingTests.swift */, OCZENTESTFR111111111111 /* OpenCodeZenProviderTests.swift */, ); @@ -592,6 +599,7 @@ 68CE796F262067298FE88469 /* BrowserCookieService.swift in Sources */, CLICLAUDE11111111111111 /* ClaudeProvider.swift in Sources */, CLICODEX111111111111111 /* CodexProvider.swift in Sources */, + CMDCODECLI111111111111 /* CommandCodeProvider.swift in Sources */, CLICURSOR1111111111111 /* CursorProvider.swift in Sources */, F35960B456C28D71BE7575A5 /* GeminiCLIProvider.swift in Sources */, CLIOPENROUTER111111111 /* OpenRouterProvider.swift in Sources */, @@ -635,6 +643,7 @@ 87297CDD8B65F7EF0BC5D650 /* ProviderManager.swift in Sources */, AD95EBD6AE3134DF4C797577 /* ClaudeProvider.swift in Sources */, 0B6CF8BEB936A2055F19AA8C /* CodexProvider.swift in Sources */, + CMDCODEAPP111111111111 /* CommandCodeProvider.swift in Sources */, CURSORAPP1111111111111 /* CursorProvider.swift in Sources */, 27A0B56ADAC5B75A3270F90D /* CopilotProvider.swift in Sources */, OC1111111111111111111111 /* OpenCodeProvider.swift in Sources */, @@ -676,6 +685,7 @@ CURSORTSTBF11111111111 /* CursorProviderTests.swift in Sources */, TOKENTESTBF1111111111111 /* TokenManagerTests.swift in Sources */, CODEXTESTBF111111111111 /* CodexProviderTests.swift in Sources */, + CMDCODETESTBF111111111 /* CommandCodeProviderTests.swift in Sources */, OCAUTHTESTBF11111111111 /* OpenCodeAuthDecodingTests.swift in Sources */, OCZENTESTBF111111111111 /* OpenCodeZenProviderTests.swift in Sources */, ); diff --git a/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift b/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift index 0cea599..ffdd60f 100644 --- a/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift +++ b/CopilotMonitor/CopilotMonitor/App/StatusBarController.swift @@ -985,6 +985,8 @@ final class StatusBarController: NSObject { details?.sparkUsage, priority: priorityForWindowHours(details?.sparkPrimaryWindowHours, fallback: .hourly) ) + case .commandCode: + add(usage.usagePercentage, priority: .monthly) case .cursor: add(details?.cursorAutoUsage, priority: .monthly) add(details?.cursorApiUsage, priority: .monthly) @@ -1876,6 +1878,7 @@ final class StatusBarController: NSObject { .kimi, .minimaxCodingPlan, .codex, + .commandCode, .cursor, .zaiCodingPlan, .nanoGpt, @@ -2929,6 +2932,8 @@ final class StatusBarController: NSObject { image = NSImage(named: "ClaudeIcon") case .codex: image = NSImage(named: "CodexIcon") + case .commandCode: + image = NSImage(systemSymbolName: identifier.iconName, accessibilityDescription: identifier.displayName) case .cursor: image = NSImage(named: "CursorIcon") case .geminiCLI: diff --git a/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift b/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift index 6b199dc..7fb76ab 100644 --- a/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift +++ b/CopilotMonitor/CopilotMonitor/Helpers/ProviderMenuBuilder.swift @@ -389,6 +389,52 @@ extension StatusBarController { // === Subscription === addSubscriptionItems(to: submenu, provider: .codex, accountId: subscriptionAccountId) + case .commandCode: + if let total = details.creditsTotal, + total > 0, + let remaining = details.creditsRemaining { + let usagePercent = max(0, min(((total - remaining) / total) * 100.0, 999.0)) + createUsageWindowRow( + label: "Monthly Credits", + usagePercent: usagePercent, + resetDate: details.primaryReset, + isMonthly: true + ).forEach { submenu.addItem($0) } + } + + if details.planType != nil || details.creditsTotal != nil || details.creditsRemaining != nil { + submenu.addItem(NSMenuItem.separator()) + } + + if let plan = details.planType { + let item = NSMenuItem() + item.view = createDisabledLabelView( + text: "Plan: \(plan)", + icon: NSImage(systemSymbolName: "crown", accessibilityDescription: "Plan") + ) + submenu.addItem(item) + } + + if let used = details.monthlyCost, let total = details.creditsTotal { + let item = NSMenuItem() + item.view = createDisabledLabelView(text: String(format: "Monthly Used: $%.2f / $%.2f", used, total)) + submenu.addItem(item) + } + + if let remaining = details.creditsRemaining { + let item = NSMenuItem() + item.view = createDisabledLabelView(text: String(format: "Credits Left: $%.2f", remaining)) + submenu.addItem(item) + } + + if let purchasedCredits = details.creditsBalance, purchasedCredits > 0 { + let item = NSMenuItem() + item.view = createDisabledLabelView(text: String(format: "Purchased Credits: $%.2f", purchasedCredits)) + submenu.addItem(item) + } + + addSubscriptionItems(to: submenu, provider: .commandCode, accountId: subscriptionAccountId) + case .cursor: var hasUsageWindow = false func addCursorUsageWindow(label: String, usagePercent: Double?, resetDate: Date?) { diff --git a/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift b/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift index ec3a190..41ec86d 100644 --- a/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift +++ b/CopilotMonitor/CopilotMonitor/Models/ProviderProtocol.swift @@ -13,6 +13,7 @@ enum ProviderIdentifier: String, CaseIterable { case copilot case claude case codex + case commandCode = "command_code" case cursor case geminiCLI = "gemini_cli" case openRouter = "openrouter" @@ -36,6 +37,8 @@ enum ProviderIdentifier: String, CaseIterable { return "Claude" case .codex: return "ChatGPT" + case .commandCode: + return "Command Code" case .cursor: return "Cursor" case .geminiCLI: @@ -75,6 +78,8 @@ enum ProviderIdentifier: String, CaseIterable { return "Claude" case .codex: return "Codex" + case .commandCode: + return "Command" case .cursor: return "Cursor" case .geminiCLI: @@ -114,6 +119,8 @@ enum ProviderIdentifier: String, CaseIterable { return "brain.head.profile" case .codex: return "sparkles" + case .commandCode: + return "command" case .cursor: return "CursorIcon" case .geminiCLI: diff --git a/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift b/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift index 8879428..88fc095 100644 --- a/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift +++ b/CopilotMonitor/CopilotMonitor/Models/SubscriptionSettings.swift @@ -109,6 +109,13 @@ struct ProviderSubscriptionPresets { SubscriptionPreset(name: "Pro", cost: 200) ] + static let commandCode: [SubscriptionPreset] = [ + SubscriptionPreset(name: "Go", cost: 10), + SubscriptionPreset(name: "Pro", cost: 30), + SubscriptionPreset(name: "Max", cost: 150), + SubscriptionPreset(name: "Ultra", cost: 300) + ] + static let cursor: [SubscriptionPreset] = [ SubscriptionPreset(name: "Pro", cost: 20), SubscriptionPreset(name: "Teams", cost: 40) @@ -181,6 +188,8 @@ struct ProviderSubscriptionPresets { return claude case .codex: return codex + case .commandCode: + return commandCode case .cursor: return cursor case .geminiCLI: diff --git a/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift b/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift new file mode 100644 index 0000000..313bf3f --- /dev/null +++ b/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift @@ -0,0 +1,490 @@ +import Foundation +import Foundation +import os.log + +private let commandCodeLogger = Logger(subsystem: "com.opencodeproviders", category: "CommandCodeProvider") + +struct CommandCodePlan: Equatable { + let id: String + let displayName: String + let monthlyCreditsUSD: Double +} + +enum CommandCodePlanCatalog { + static let plans: [String: CommandCodePlan] = [ + "individual-go": CommandCodePlan(id: "individual-go", displayName: "Go", monthlyCreditsUSD: 10), + "individual-pro": CommandCodePlan(id: "individual-pro", displayName: "Pro", monthlyCreditsUSD: 30), + "individual-max": CommandCodePlan(id: "individual-max", displayName: "Max", monthlyCreditsUSD: 150), + "individual-ultra": CommandCodePlan(id: "individual-ultra", displayName: "Ultra", monthlyCreditsUSD: 300) + ] + + static func plan(for id: String?) -> CommandCodePlan? { + guard let id else { return nil } + return plans[id.lowercased()] + } +} + +struct CommandCodeCookieHeader: Equatable { + static let supportedCookieNames = [ + "__Host-better-auth.session_token", + "__Secure-better-auth.session_token", + "better-auth.session_token" + ] + static let productionCookieName = "__Secure-better-auth.session_token" + + let name: String + let token: String + + var headerValue: String { "\(name)=\(token)" } + + static func override(from rawValue: String?) -> CommandCodeCookieHeader? { + guard let rawValue = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !rawValue.isEmpty else { + return nil + } + + let parts = rawValue.split(separator: ";", omittingEmptySubsequences: true) + for part in parts { + let pair = part.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + guard pair.count == 2 else { continue } + let name = pair[0].trimmingCharacters(in: .whitespacesAndNewlines) + guard supportedCookieNames.contains(name) else { continue } + let token = pair[1].trimmingCharacters(in: .whitespacesAndNewlines) + guard !token.isEmpty else { continue } + return CommandCodeCookieHeader(name: name, token: token) + } + + guard !rawValue.contains(";") && !rawValue.contains("=") else { return nil } + return CommandCodeCookieHeader(name: productionCookieName, token: rawValue) + } +} + +enum CommandCodeProviderError: LocalizedError { + case missingCredentials + case invalidCredentials + case apiError(Int) + case parseFailed(String) + case unknownPlan(String) + + var errorDescription: String? { + switch self { + case .missingCredentials: + return "Command Code session cookie not found. Sign in to commandcode.ai or start OpenCommand." + case .invalidCredentials: + return "Command Code session cookie is invalid or expired." + case .apiError(let status): + return "Command Code API returned HTTP \(status)." + case .parseFailed(let message): + return "Command Code response could not be parsed: \(message)" + case .unknownPlan(let planID): + return "Command Code returned an unknown active plan: \(planID)" + } + } +} + +struct CommandCodeUsageSnapshot: Equatable { + let monthlyCreditsRemaining: Double + let purchasedCredits: Double + let premiumMonthlyCredits: Double + let opensourceMonthlyCredits: Double + let plan: CommandCodePlan? + let billingPeriodEnd: Date? + let subscriptionStatus: String? + let authSource: String + + var monthlyCreditsTotal: Double? { plan?.monthlyCreditsUSD } + + var monthlyCreditsUsed: Double? { + guard let monthlyCreditsTotal else { return nil } + return max(0, monthlyCreditsTotal - monthlyCreditsRemaining) + } + + var usagePercent: Double { + guard let monthlyCreditsTotal, monthlyCreditsTotal > 0, let monthlyCreditsUsed else { return 0 } + return min(max((monthlyCreditsUsed / monthlyCreditsTotal) * 100.0, 0), 999) + } + + var usageSummary: String? { + guard let monthlyCreditsTotal, let monthlyCreditsUsed else { return nil } + let planDisplay = plan?.displayName ?? "Free" + return String(format: "%@ · $%.2f of $%.2f", planDisplay, monthlyCreditsUsed, monthlyCreditsTotal) + } +} + +final class CommandCodeProvider: ProviderProtocol { + let identifier: ProviderIdentifier = .commandCode + let type: ProviderType = .quotaBased + let fetchTimeout: TimeInterval = 15.0 + let minimumFetchInterval: TimeInterval = 60.0 + + private let session: URLSession + private let fileManager: FileManager + private let environment: [String: String] + + init( + session: URLSession = .shared, + fileManager: FileManager = .default, + environment: [String: String] = ProcessInfo.processInfo.environment + ) { + self.session = session + self.fileManager = fileManager + self.environment = environment + } + + func fetch() async throws -> ProviderResult { + debugLog("fetch started") + + if let proxyURL = loadOpenCommandProxyURL() { + do { + try await validateOpenCommandProxy(proxyURL: proxyURL) + let snapshot = try await fetchOpenCommandUsage(proxyURL: proxyURL) + commandCodeLogger.info("Command Code usage fetched through OpenCommand local proxy") + debugLog("fetch completed through OpenCommand local proxy") + return Self.makeResult(from: snapshot) + } catch { + commandCodeLogger.warning("OpenCommand proxy fetch failed: \(error.localizedDescription, privacy: .public)") + debugLog("OpenCommand proxy fetch failed: \(error.localizedDescription); falling back to direct Command Code API") + } + } + + guard let cookieHeader = loadCookieHeader() else { + debugLog("fetch failed: no Command Code credentials found") + throw ProviderError.authenticationFailed(CommandCodeProviderError.missingCredentials.localizedDescription) + } + + do { + let snapshot = try await fetchDirectUsage(cookieHeader: cookieHeader.headerValue) + commandCodeLogger.info("Command Code usage fetched through direct billing API") + debugLog("fetch completed through direct Command Code API") + return Self.makeResult(from: snapshot) + } catch let error as CommandCodeProviderError { + if case .invalidCredentials = error { + throw ProviderError.authenticationFailed(error.localizedDescription) + } + throw ProviderError.providerError(error.localizedDescription) + } catch { + throw ProviderError.networkError(error.localizedDescription) + } + } + + // MARK: - Mapping + + static func makeResult(from snapshot: CommandCodeUsageSnapshot) -> ProviderResult { + let totalUSD = snapshot.monthlyCreditsTotal ?? max(snapshot.monthlyCreditsRemaining, 0) + let remainingUSD = snapshot.monthlyCreditsRemaining + let entitlementCents = max(Int((totalUSD * 100.0).rounded()), 1) + let remainingCents = Int((remainingUSD * 100.0).rounded()) + + let details = DetailedUsage( + primaryReset: snapshot.billingPeriodEnd, + creditsBalance: snapshot.purchasedCredits, + planType: snapshot.plan?.displayName ?? snapshot.subscriptionStatus, + monthlyCost: snapshot.monthlyCreditsUsed, + creditsRemaining: snapshot.monthlyCreditsRemaining, + creditsTotal: snapshot.monthlyCreditsTotal, + authSource: snapshot.authSource, + authUsageSummary: snapshot.usageSummary + ) + + return ProviderResult( + usage: .quotaBased(remaining: remainingCents, entitlement: entitlementCents, overagePermitted: false), + details: details + ) + } + + static func snapshotFromOpenCommandUsage(_ data: Data, authSource: String) throws -> CommandCodeUsageSnapshot { + guard let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw CommandCodeProviderError.parseFailed("Expected JSON object") + } + + let remaining = parseDouble(from: object, keys: ["credits_remaining", "creditsRemaining"]) + let monthlySpend = parseDouble(from: object, keys: ["monthly_spend", "monthlySpend"]) + var monthlyLimit = parseDouble(from: object, keys: ["monthly_limit", "monthlyLimit"]) + if monthlyLimit <= 0 { + monthlyLimit = monthlySpend + remaining + } + guard monthlyLimit > 0 else { + throw CommandCodeProviderError.parseFailed("monthly_limit is missing") + } + + let resetDate = parseDate(from: object["reset_date"] as? String ?? object["resetDate"] as? String) + let plan = CommandCodePlan(id: "opencommand", displayName: "OpenCommand", monthlyCreditsUSD: monthlyLimit) + + return CommandCodeUsageSnapshot( + monthlyCreditsRemaining: remaining, + purchasedCredits: 0, + premiumMonthlyCredits: 0, + opensourceMonthlyCredits: remaining, + plan: plan, + billingPeriodEnd: resetDate, + subscriptionStatus: "OpenCommand", + authSource: authSource + ) + } + + static func snapshotFromDirectAPI(creditsData: Data, subscriptionData: Data, authSource: String) throws -> CommandCodeUsageSnapshot { + let credits = try parseCreditsPayload(creditsData) + let subscription = try parseSubscriptionPayload(subscriptionData) + let plan = CommandCodePlanCatalog.plan(for: subscription.planID) + + if let planID = subscription.planID, + subscription.status?.lowercased() == "active", + plan == nil { + throw CommandCodeProviderError.unknownPlan(planID) + } + + return CommandCodeUsageSnapshot( + monthlyCreditsRemaining: credits.monthlyCredits, + purchasedCredits: credits.purchasedCredits, + premiumMonthlyCredits: credits.premiumMonthlyCredits, + opensourceMonthlyCredits: credits.opensourceMonthlyCredits, + plan: plan, + billingPeriodEnd: subscription.currentPeriodEnd, + subscriptionStatus: subscription.status, + authSource: authSource + ) + } + + // MARK: - OpenCommand proxy + + private func loadOpenCommandProxyURL() -> URL? { + let configURL = openCommandDirectory().appendingPathComponent("proxy-config.json") + guard fileManager.fileExists(atPath: configURL.path), fileManager.isReadableFile(atPath: configURL.path) else { + return nil + } + + do { + let data = try Data(contentsOf: configURL) + guard let object = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let rawURL = object["url"] as? String, + let proxyURL = URL(string: rawURL) else { + debugLog("OpenCommand proxy config found but could not be parsed") + return nil + } + debugLog("OpenCommand proxy config found") + return proxyURL + } catch { + debugLog("OpenCommand proxy config read failed: \(error.localizedDescription)") + return nil + } + } + + private func validateOpenCommandProxy(proxyURL: URL) async throws { + let healthURL = proxyURL.appendingPathComponent("healthz") + var request = URLRequest(url: healthURL) + request.timeoutInterval = 3 + let (_, response) = try await session.data(for: request) + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + throw ProviderError.networkError("OpenCommand health check failed") + } + } + + private func fetchOpenCommandUsage(proxyURL: URL) async throws -> CommandCodeUsageSnapshot { + let usageURL = proxyURL.appendingPathComponent("v1/account/usage") + var request = URLRequest(url: usageURL) + request.timeoutInterval = fetchTimeout + request.setValue("application/json", forHTTPHeaderField: "Accept") + + let (data, response) = try await session.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw ProviderError.networkError("Invalid OpenCommand response") + } + guard (200...299).contains(httpResponse.statusCode) else { + throw ProviderError.networkError("OpenCommand usage returned HTTP \(httpResponse.statusCode)") + } + + return try Self.snapshotFromOpenCommandUsage(data, authSource: "OpenCommand local proxy") + } + + // MARK: - Direct Command Code API + + private func fetchDirectUsage(cookieHeader: String) async throws -> CommandCodeUsageSnapshot { + async let creditsTask = sendDirectRequest(path: "/internal/billing/credits", cookieHeader: cookieHeader) + async let subscriptionTask = sendDirectRequest(path: "/internal/billing/subscriptions", cookieHeader: cookieHeader) + let (creditsData, subscriptionData) = try await (creditsTask, subscriptionTask) + return try Self.snapshotFromDirectAPI( + creditsData: creditsData, + subscriptionData: subscriptionData, + authSource: "Command Code browser session cookie" + ) + } + + private func sendDirectRequest(path: String, cookieHeader: String) async throws -> Data { + guard let url = URL(string: "https://api.commandcode.ai\(path)") else { + throw CommandCodeProviderError.parseFailed("Invalid Command Code URL") + } + + var request = URLRequest(url: url) + request.timeoutInterval = fetchTimeout + request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language") + request.setValue("https://commandcode.ai", forHTTPHeaderField: "Origin") + request.setValue("https://commandcode.ai/studio", forHTTPHeaderField: "Referer") + request.setValue( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", + forHTTPHeaderField: "User-Agent" + ) + + let (data, response) = try await session.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { + throw CommandCodeProviderError.parseFailed("Invalid Command Code response") + } + if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { + throw CommandCodeProviderError.invalidCredentials + } + guard (200...299).contains(httpResponse.statusCode) else { + throw CommandCodeProviderError.apiError(httpResponse.statusCode) + } + return data + } + + private func loadCookieHeader() -> CommandCodeCookieHeader? { + for key in ["CC_SESSION_COOKIE", "COMMANDCODE_SESSION_COOKIE", "COMMAND_CODE_SESSION_COOKIE"] { + if let header = CommandCodeCookieHeader.override(from: environment[key]) { + debugLog("Command Code cookie loaded from environment key \(key)") + return header + } + } + + if let header = loadCookieHeaderFromOpenCommandSecrets() { + return header + } + + do { + let headerValue = try BrowserCookieService.shared.getCommandCodeCookieHeader() + if let header = CommandCodeCookieHeader.override(from: headerValue) { + debugLog("Command Code cookie loaded from browser cookie store") + return header + } + } catch { + debugLog("Browser cookie lookup failed: \(error.localizedDescription)") + } + + return nil + } + + private func loadCookieHeaderFromOpenCommandSecrets() -> CommandCodeCookieHeader? { + let secretsURL = openCommandDirectory().appendingPathComponent("opencommand-secrets.json") + guard fileManager.fileExists(atPath: secretsURL.path), fileManager.isReadableFile(atPath: secretsURL.path) else { + return nil + } + + do { + let data = try Data(contentsOf: secretsURL) + guard let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + for key in ["opencommand.cc_session_cookie", "opencommand.cc_session_token"] { + if let header = CommandCodeCookieHeader.override(from: object[key] as? String) { + debugLog("Command Code cookie loaded from OpenCommand secrets") + return header + } + } + } catch { + debugLog("OpenCommand secrets read failed: \(error.localizedDescription)") + } + + return nil + } + + private func openCommandDirectory() -> URL { + fileManager.homeDirectoryForCurrentUser.appendingPathComponent(".opencommand", isDirectory: true) + } + + // MARK: - Parsing helpers + + private struct CreditsPayload { + let monthlyCredits: Double + let purchasedCredits: Double + let premiumMonthlyCredits: Double + let opensourceMonthlyCredits: Double + } + + private struct SubscriptionPayload { + let planID: String? + let status: String? + let currentPeriodEnd: Date? + } + + private static func parseCreditsPayload(_ data: Data) throws -> CreditsPayload { + guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let credits = root["credits"] as? [String: Any] else { + throw CommandCodeProviderError.parseFailed("Missing credits object") + } + + return CreditsPayload( + monthlyCredits: parseDouble(from: credits, keys: ["monthlyCredits"]), + purchasedCredits: parseDouble(from: credits, keys: ["purchasedCredits"]), + premiumMonthlyCredits: parseDouble(from: credits, keys: ["premiumMonthlyCredits"]), + opensourceMonthlyCredits: parseDouble(from: credits, keys: ["opensourceMonthlyCredits"]) + ) + } + + private static func parseSubscriptionPayload(_ data: Data) throws -> SubscriptionPayload { + guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw CommandCodeProviderError.parseFailed("Missing subscription object") + } + + if let success = root["success"] as? Bool, !success { + return SubscriptionPayload(planID: nil, status: nil, currentPeriodEnd: nil) + } + + guard let dataObject = root["data"] as? [String: Any] else { + return SubscriptionPayload(planID: nil, status: nil, currentPeriodEnd: nil) + } + + let planID = dataObject["planId"] as? String ?? dataObject["planID"] as? String + let status = dataObject["status"] as? String ?? "unknown" + let currentPeriodEnd = parseDate(from: dataObject["currentPeriodEnd"] as? String) + + return SubscriptionPayload(planID: planID, status: status, currentPeriodEnd: currentPeriodEnd) + } + + static func parseDouble(from dict: [String: Any], keys: [String]) -> Double { + for key in keys { + if let value = dict[key] as? Double { return value } + if let value = dict[key] as? Int { return Double(value) } + if let value = dict[key] as? NSNumber { return value.doubleValue } + if let value = dict[key] as? String, let parsed = Double(value) { return parsed } + } + return 0 + } + + static func parseDate(from rawValue: String?) -> Date? { + guard let rawValue = rawValue?.trimmingCharacters(in: .whitespacesAndNewlines), !rawValue.isEmpty else { + return nil + } + + let fractionalFormatter = ISO8601DateFormatter() + fractionalFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = fractionalFormatter.date(from: rawValue) { return date } + + let plainFormatter = ISO8601DateFormatter() + plainFormatter.formatOptions = [.withInternetDateTime] + if let date = plainFormatter.date(from: rawValue) { return date } + + let dateOnlyFormatter = DateFormatter() + dateOnlyFormatter.locale = Locale(identifier: "en_US_POSIX") + dateOnlyFormatter.dateFormat = "yyyy-MM-dd" + if let utc = TimeZone(identifier: "UTC") { + dateOnlyFormatter.timeZone = utc + } + return dateOnlyFormatter.date(from: rawValue) + } + + private func debugLog(_ message: String) { + #if DEBUG + let msg = "[\(Date())] CommandCodeProvider: \(message)\n" + if let data = msg.data(using: .utf8) { + let path = "/tmp/provider_debug.log" + if fileManager.fileExists(atPath: path), let handle = FileHandle(forWritingAtPath: path) { + handle.seekToEndOfFile() + handle.write(data) + handle.closeFile() + } else { + try? data.write(to: URL(fileURLWithPath: path)) + } + } + #endif + } +} diff --git a/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift b/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift index ae25318..881a259 100644 --- a/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift +++ b/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift @@ -58,6 +58,34 @@ class BrowserCookieService { throw BrowserCookieError.noBrowserFound } + func getCommandCodeCookieHeader() throws -> String { + debugLog("Starting Command Code cookie extraction from browsers") + + for browser in SupportedBrowser.allCases { + debugLog("Trying browser for Command Code cookies: \(browser.displayName)") + do { + let cookies = try extractCookieValues( + from: browser, + hostSuffix: "commandcode.ai", + cookieNames: CommandCodeCookieHeader.supportedCookieNames + ) + + for cookieName in CommandCodeCookieHeader.supportedCookieNames { + if let token = cookies[cookieName], !token.isEmpty { + debugLog("Successfully extracted Command Code cookie named \(cookieName) from \(browser.displayName)") + return "\(cookieName)=\(token)" + } + } + } catch { + debugLog("Failed to extract Command Code cookie from \(browser.displayName): \(error.localizedDescription)") + continue + } + } + + debugLog("No browser found with valid Command Code cookies") + throw BrowserCookieError.noBrowserFound + } + // MARK: - Browser Detection & Cookie Extraction private func extractCookies(from browser: SupportedBrowser) throws -> GitHubCookies { @@ -89,6 +117,40 @@ class BrowserCookieService { throw BrowserCookieError.noBrowserFound } + private func extractCookieValues(from browser: SupportedBrowser, hostSuffix: String, cookieNames: [String]) throws -> [String: String] { + let cookieDBPaths = browser.cookieDBPaths + debugLog("Found \(cookieDBPaths.count) cookie paths for \(browser.displayName)") + + guard !cookieDBPaths.isEmpty else { + throw BrowserCookieError.cookieDBNotFound + } + + let encryptionKey = try getEncryptionKey(for: browser) + let aesKey = try deriveAESKey(from: encryptionKey) + + for path in cookieDBPaths { + debugLog("Trying cookie path: \(path)") + do { + let cookies = try readCookieValues( + from: path, + aesKey: aesKey, + hostSuffix: hostSuffix, + cookieNames: cookieNames + ) + if !cookies.isEmpty { + debugLog("Found matching cookies at: \(path)") + return cookies + } + debugLog("No matching cookies at \(path)") + } catch { + debugLog("Failed to read cookies from \(path): \(error.localizedDescription)") + continue + } + } + + throw BrowserCookieError.noBrowserFound + } + // MARK: - Keychain Access // Uses /usr/bin/security instead of SecItemCopyMatching to read @@ -231,6 +293,71 @@ class BrowserCookieService { ) } + private func readCookieValues( + from dbPath: String, + aesKey: Data, + hostSuffix: String, + cookieNames: [String] + ) throws -> [String: String] { + let tempPath = NSTemporaryDirectory() + "browser_cookies_temp_\(UUID().uuidString).db" + try? FileManager.default.removeItem(atPath: tempPath) + try FileManager.default.copyItem(atPath: dbPath, toPath: tempPath) + defer { try? FileManager.default.removeItem(atPath: tempPath) } + + var db: OpaquePointer? + guard sqlite3_open_v2(tempPath, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else { + logger.error("Failed to open SQLite database at \(tempPath)") + throw BrowserCookieError.invalidCookieFormat + } + defer { sqlite3_close(db) } + + let placeholders = cookieNames.map { _ in "?" }.joined(separator: ",") + let query = "SELECT name, encrypted_value, value FROM cookies WHERE host_key LIKE ? AND name IN (\(placeholders))" + var statement: OpaquePointer? + guard sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK else { + logger.error("Failed to prepare SQLite statement") + throw BrowserCookieError.invalidCookieFormat + } + defer { sqlite3_finalize(statement) } + + let transient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + sqlite3_bind_text(statement, 1, "%\(hostSuffix)", -1, transient) + for (index, cookieName) in cookieNames.enumerated() { + sqlite3_bind_text(statement, Int32(index + 2), cookieName, -1, transient) + } + + var cookies: [String: String] = [:] + + while sqlite3_step(statement) == SQLITE_ROW { + guard let namePtr = sqlite3_column_text(statement, 0) else { continue } + let name = String(cString: namePtr) + + if let encryptedBlob = sqlite3_column_blob(statement, 1) { + let encryptedLength = Int(sqlite3_column_bytes(statement, 1)) + if encryptedLength > 0 { + let encryptedData = Data(bytes: encryptedBlob, count: encryptedLength) + + if let decrypted = try? decryptCookie(encryptedData, aesKey: aesKey), !decrypted.isEmpty { + cookies[name] = decrypted + logger.debug("Decrypted browser cookie named \(name)") + continue + } + } + } + + if let valuePtr = sqlite3_column_text(statement, 2) { + let value = String(cString: valuePtr) + if !value.isEmpty { + cookies[name] = value + logger.debug("Found plaintext browser cookie named \(name)") + } + } + } + + logger.info("Found \(cookies.count) matching browser cookies for \(hostSuffix)") + return cookies + } + // MARK: - AES-CBC Decryption /// Chromium cookies: v10/v11 prefix, IV=16 spaces (0x20), skip first 32 garbage bytes @@ -425,7 +552,7 @@ enum BrowserCookieError: LocalizedError { var errorDescription: String? { switch self { case .noBrowserFound: - return "No supported browser found with GitHub cookies" + return "No supported browser found with matching cookies" case .cookieDBNotFound: return "Cookie database not found" case .keychainAccessFailed: diff --git a/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift b/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift index 9e8fa45..c959463 100644 --- a/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift +++ b/CopilotMonitor/CopilotMonitor/Services/ProviderManager.swift @@ -30,6 +30,7 @@ actor ProviderManager { CopilotProvider(), ClaudeProvider(), CodexProvider(), + CommandCodeProvider(), CursorProvider(), GeminiCLIProvider(), MiniMaxProvider(), diff --git a/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift b/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift index f78420e..86db1de 100644 --- a/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift +++ b/CopilotMonitor/CopilotMonitor/Views/MultiProviderStatusBarIconView.swift @@ -132,6 +132,8 @@ final class MultiProviderStatusBarIconView: NSView { iconName = "ClaudeIcon" case .codex: iconName = "CodexIcon" + case .commandCode: + iconName = "command" case .cursor: iconName = "CursorIcon" case .geminiCLI: diff --git a/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift b/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift index a45bafe..26d7bb3 100644 --- a/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift +++ b/CopilotMonitor/CopilotMonitor/Views/SwiftUI/ModernStatusBarIconView.swift @@ -124,6 +124,7 @@ struct SwiftUIProviderAlertView: View { switch identifier { case .claude: return "brain" case .codex: return "terminal" + case .commandCode: return "command" case .cursor: return nil case .geminiCLI: return "sparkles" case .copilot: return "airplane" diff --git a/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift b/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift new file mode 100644 index 0000000..2a0749f --- /dev/null +++ b/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift @@ -0,0 +1,117 @@ +import XCTest +@testable import OpenCode_Bar + +final class CommandCodeProviderTests: XCTestCase { + func testProviderIdentifier() { + let provider = CommandCodeProvider() + XCTAssertEqual(provider.identifier, .commandCode) + } + + func testProviderType() { + let provider = CommandCodeProvider() + XCTAssertEqual(provider.type, .quotaBased) + } + + func testCookieHeaderParsesSecureCookie() { + let header = CommandCodeCookieHeader.override( + from: "foo=bar; __Secure-better-auth.session_token=test-token; other=value" + ) + + XCTAssertEqual(header?.name, "__Secure-better-auth.session_token") + XCTAssertEqual(header?.token, "test-token") + XCTAssertEqual(header?.headerValue, "__Secure-better-auth.session_token=test-token") + } + + func testCookieHeaderParsesBareToken() { + let header = CommandCodeCookieHeader.override(from: "test-token") + + XCTAssertEqual(header?.name, "__Secure-better-auth.session_token") + XCTAssertEqual(header?.token, "test-token") + } + + func testCookieHeaderRejectsUnsupportedCookieHeader() { + let header = CommandCodeCookieHeader.override(from: "unrelated=value") + + XCTAssertNil(header) + } + + func testDirectAPISnapshotParsesCodexBarCompatiblePayloads() throws { + let creditsJSON = """ + { + "credits": { + "belowThreshold": false, + "creditThreshold": 0, + "monthlyCredits": 8.7784, + "purchasedCredits": 0, + "premiumMonthlyCredits": 0, + "opensourceMonthlyCredits": 8.7784 + } + } + """.data(using: .utf8)! + let subscriptionJSON = """ + { + "success": true, + "data": { + "planId": "individual-go", + "status": "active", + "currentPeriodEnd": "2026-06-06T07:28:50.000Z" + } + } + """.data(using: .utf8)! + + let snapshot = try CommandCodeProvider.snapshotFromDirectAPI( + creditsData: creditsJSON, + subscriptionData: subscriptionJSON, + authSource: "test" + ) + + XCTAssertEqual(snapshot.plan?.displayName, "Go") + XCTAssertEqual(snapshot.monthlyCreditsTotal, 10) + XCTAssertEqual(snapshot.monthlyCreditsUsed ?? 0, 1.2216, accuracy: 0.0001) + XCTAssertEqual(snapshot.usagePercent, 12.216, accuracy: 0.001) + XCTAssertEqual(snapshot.usageSummary, "Go · $1.22 of $10.00") + XCTAssertNotNil(snapshot.billingPeriodEnd) + } + + func testOpenCommandUsageSnapshotParsesProxyPayload() throws { + let data = """ + { + "credits_remaining": 21.5, + "monthly_spend": 8.5, + "monthly_limit": 30, + "remaining_days": 12, + "reset_date": "2026-06-06" + } + """.data(using: .utf8)! + + let snapshot = try CommandCodeProvider.snapshotFromOpenCommandUsage(data, authSource: "OpenCommand local proxy") + + XCTAssertEqual(snapshot.monthlyCreditsRemaining, 21.5) + XCTAssertEqual(snapshot.monthlyCreditsTotal, 30) + XCTAssertEqual(snapshot.monthlyCreditsUsed ?? 0, 8.5, accuracy: 0.0001) + XCTAssertEqual(snapshot.usagePercent, 28.333, accuracy: 0.001) + XCTAssertEqual(snapshot.authSource, "OpenCommand local proxy") + XCTAssertNotNil(snapshot.billingPeriodEnd) + } + + func testProviderResultUsesCentPrecisionForDollarCredits() { + let snapshot = CommandCodeUsageSnapshot( + monthlyCreditsRemaining: 8.7784, + purchasedCredits: 0, + premiumMonthlyCredits: 0, + opensourceMonthlyCredits: 8.7784, + plan: CommandCodePlan(id: "individual-go", displayName: "Go", monthlyCreditsUSD: 10), + billingPeriodEnd: nil, + subscriptionStatus: "active", + authSource: "test" + ) + + let result = CommandCodeProvider.makeResult(from: snapshot) + + XCTAssertEqual(result.usage.totalEntitlement, 1000) + XCTAssertEqual(result.usage.remainingQuota, 878) + XCTAssertEqual(result.usage.usagePercentage, 12.2, accuracy: 0.0001) + XCTAssertEqual(result.details?.creditsRemaining, 8.7784) + XCTAssertEqual(result.details?.creditsTotal, 10) + } +} From 3bb1ba4b6559fd356c67f176a228789993be45e8 Mon Sep 17 00:00:00 2001 From: Ruben Beuker Date: Mon, 18 May 2026 14:05:55 +0200 Subject: [PATCH 2/2] Add Comet cookie support for Command Code --- .../CopilotMonitor/Providers/CommandCodeProvider.swift | 3 ++- .../CopilotMonitor/Services/BrowserCookieService.swift | 8 ++++++++ .../CopilotMonitorTests/CommandCodeProviderTests.swift | 10 ++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift b/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift index 313bf3f..661e886 100644 --- a/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift +++ b/CopilotMonitor/CopilotMonitor/Providers/CommandCodeProvider.swift @@ -28,7 +28,8 @@ struct CommandCodeCookieHeader: Equatable { static let supportedCookieNames = [ "__Host-better-auth.session_token", "__Secure-better-auth.session_token", - "better-auth.session_token" + "better-auth.session_token", + "__Secure-commandcode_prod_.session_token" ] static let productionCookieName = "__Secure-better-auth.session_token" diff --git a/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift b/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift index 881a259..7e39129 100644 --- a/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift +++ b/CopilotMonitor/CopilotMonitor/Services/BrowserCookieService.swift @@ -445,6 +445,7 @@ enum SupportedBrowser: CaseIterable { case brave case arc case edge + case comet var displayName: String { switch self { @@ -452,6 +453,7 @@ enum SupportedBrowser: CaseIterable { case .brave: return "Brave" case .arc: return "Arc" case .edge: return "Edge" + case .comet: return "Comet" } } @@ -480,6 +482,10 @@ enum SupportedBrowser: CaseIterable { let baseDir = "\(home)/Library/Application Support/Microsoft Edge" paths.append("\(baseDir)/Default/Cookies") paths.append(contentsOf: findProfilePaths(in: baseDir)) + case .comet: + let baseDir = "\(home)/Library/Application Support/Comet" + paths.append("\(baseDir)/Default/Cookies") + paths.append(contentsOf: findProfilePaths(in: baseDir)) } return paths.filter { FileManager.default.fileExists(atPath: $0) } @@ -501,6 +507,7 @@ enum SupportedBrowser: CaseIterable { case .brave: return "Brave Safe Storage" case .arc: return "Arc Safe Storage" case .edge: return "Microsoft Edge Safe Storage" + case .comet: return "Comet Safe Storage" } } @@ -510,6 +517,7 @@ enum SupportedBrowser: CaseIterable { case .brave: return "Brave" case .arc: return "Arc" case .edge: return "Microsoft Edge" + case .comet: return "Comet" } } } diff --git a/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift b/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift index 2a0749f..96d1aca 100644 --- a/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift +++ b/CopilotMonitor/CopilotMonitorTests/CommandCodeProviderTests.swift @@ -22,6 +22,16 @@ final class CommandCodeProviderTests: XCTestCase { XCTAssertEqual(header?.headerValue, "__Secure-better-auth.session_token=test-token") } + func testCookieHeaderParsesCommandCodeProductionCookie() { + let header = CommandCodeCookieHeader.override( + from: "foo=bar; __Secure-commandcode_prod_.session_token=test-token; other=value" + ) + + XCTAssertEqual(header?.name, "__Secure-commandcode_prod_.session_token") + XCTAssertEqual(header?.token, "test-token") + XCTAssertEqual(header?.headerValue, "__Secure-commandcode_prod_.session_token=test-token") + } + func testCookieHeaderParsesBareToken() { let header = CommandCodeCookieHeader.override(from: "test-token")