import Foundationimport SweetCookieKit #if os(macOS) private let cursorCookieImportOrder: BrowserCookieImportOrder = ProviderDefaults.metadata[.cursor]?.browserCookieOrder ?? Browser.defaultImportOrder // MARK: - Cursor Cookie Importer /// Imports Cursor session cookies from browser cookies.public enum CursorCookieImporter { private static let cookieClient = BrowserCookieClient() private static let sessionCookieNames: Set<String> = [ "WorkosCursorSessionToken", "__Secure-next-auth.session-token", "next-auth.session-token", ] public struct SessionInfo: Sendable { public let cookies: [HTTPCookie] public let sourceLabel: String public init(cookies: [HTTPCookie], sourceLabel: String) { self.cookies = cookies self.sourceLabel = sourceLabel } public var cookieHeader: String { self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") } } /// Attempts to import Cursor cookies using the standard browser import order. public static func importSession( browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) throws -> SessionInfo { let log: (String) -> Void = { msg in logger?("[cursor-cookie] \(msg)") } // Filter to cookie-eligible browsers to avoid unnecessary keychain prompts let installedBrowsers = cursorCookieImportOrder.cookieImportCandidates(using: browserDetection) let cookieDomains = ["cursor.com", "cursor.sh"] for browserSource in installedBrowsers { do { let query = BrowserCookieQuery(domains: cookieDomains) let sources = try Self.cookieClient.records( matching: query, in: browserSource, logger: log) for source in sources where !source.records.isEmpty { let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin) if httpCookies.contains(where: { Self.sessionCookieNames.contains($0.name) }) { log("Found \(httpCookies.count) Cursor cookies in \(source.label)") return SessionInfo(cookies: httpCookies, sourceLabel: source.label) } else { log("\(source.label) cookies found, but no Cursor session cookie present") } } } catch { log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } } throw CursorStatusProbeError.noSessionCookie } /// Check if Cursor session cookies are available public static func hasSession(browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) -> Bool { do { let session = try self.importSession(browserDetection: browserDetection, logger: logger) return !session.cookies.isEmpty } catch { return false } }} // MARK: - Cursor API Models public struct CursorUsageSummary: Codable, Sendable { public let billingCycleStart: String? public let billingCycleEnd: String? public let membershipType: String? public let limitType: String? public let isUnlimited: Bool? public let autoModelSelectedDisplayMessage: String? public let namedModelSelectedDisplayMessage: String? public let individualUsage: CursorIndividualUsage? public let teamUsage: CursorTeamUsage?} public struct CursorIndividualUsage: Codable, Sendable { public let plan: CursorPlanUsage? public let onDemand: CursorOnDemandUsage?} public struct CursorPlanUsage: Codable, Sendable { public let enabled: Bool? /// Usage in cents (e.g., 2000 = $20.00) public let used: Int? /// Limit in cents (e.g., 2000 = $20.00) public let limit: Int? /// Remaining in cents public let remaining: Int? public let breakdown: CursorPlanBreakdown? public let autoPercentUsed: Double? public let apiPercentUsed: Double? public let totalPercentUsed: Double?} public struct CursorPlanBreakdown: Codable, Sendable { public let included: Int? public let bonus: Int? public let total: Int?} public struct CursorOnDemandUsage: Codable, Sendable { public let enabled: Bool? /// Usage in cents public let used: Int? /// Limit in cents (nil if unlimited) public let limit: Int? /// Remaining in cents (nil if unlimited) public let remaining: Int?} public struct CursorTeamUsage: Codable, Sendable { public let onDemand: CursorOnDemandUsage?} // MARK: - Cursor Usage API Models (Legacy Request-Based Plans) /// Response from `/api/usage?user=ID` endpoint for legacy request-based plans.public struct CursorUsageResponse: Codable, Sendable { public let gpt4: CursorModelUsage? public let startOfMonth: String? enum CodingKeys: String, CodingKey { case gpt4 = "gpt-4" case startOfMonth }} public struct CursorModelUsage: Codable, Sendable { public let numRequests: Int? public let numRequestsTotal: Int? public let numTokens: Int? public let maxRequestUsage: Int? public let maxTokenUsage: Int?} public struct CursorUserInfo: Codable, Sendable { public let email: String? public let emailVerified: Bool? public let name: String? public let sub: String? public let createdAt: String? public let updatedAt: String? public let picture: String? enum CodingKeys: String, CodingKey { case email case emailVerified = "email_verified" case name case sub case createdAt = "created_at" case updatedAt = "updated_at" case picture }} // MARK: - Cursor Status Snapshot public struct CursorStatusSnapshot: Sendable { /// Percentage of included plan usage (0-100) public let planPercentUsed: Double /// Included plan usage in USD public let planUsedUSD: Double /// Included plan limit in USD public let planLimitUSD: Double /// On-demand usage in USD public let onDemandUsedUSD: Double /// On-demand limit in USD (nil if unlimited) public let onDemandLimitUSD: Double? /// Team on-demand usage in USD (for team plans) public let teamOnDemandUsedUSD: Double? /// Team on-demand limit in USD public let teamOnDemandLimitUSD: Double? /// Billing cycle reset date public let billingCycleEnd: Date? /// Membership type (e.g., "enterprise", "pro", "hobby") public let membershipType: String? /// User email public let accountEmail: String? /// User name public let accountName: String? /// Raw API response for debugging public let rawJSON: String? // MARK: - Legacy Plan (Request-Based) Fields /// Requests used this billing cycle (legacy plans only) public let requestsUsed: Int? /// Request limit (non-nil indicates legacy request-based plan) public let requestsLimit: Int? /// Whether this is a legacy request-based plan (vs token-based) public var isLegacyRequestPlan: Bool { self.requestsLimit != nil } public init( planPercentUsed: Double, planUsedUSD: Double, planLimitUSD: Double, onDemandUsedUSD: Double, onDemandLimitUSD: Double?, teamOnDemandUsedUSD: Double?, teamOnDemandLimitUSD: Double?, billingCycleEnd: Date?, membershipType: String?, accountEmail: String?, accountName: String?, rawJSON: String?, requestsUsed: Int? = nil, requestsLimit: Int? = nil) { self.planPercentUsed = planPercentUsed self.planUsedUSD = planUsedUSD self.planLimitUSD = planLimitUSD self.onDemandUsedUSD = onDemandUsedUSD self.onDemandLimitUSD = onDemandLimitUSD self.teamOnDemandUsedUSD = teamOnDemandUsedUSD self.teamOnDemandLimitUSD = teamOnDemandLimitUSD self.billingCycleEnd = billingCycleEnd self.membershipType = membershipType self.accountEmail = accountEmail self.accountName = accountName self.rawJSON = rawJSON self.requestsUsed = requestsUsed self.requestsLimit = requestsLimit } /// Convert to UsageSnapshot for the common provider interface public func toUsageSnapshot() -> UsageSnapshot { // Primary: For legacy request-based plans, use request usage; otherwise use plan percentage let primaryUsedPercent: Double = if self.isLegacyRequestPlan, let used = self.requestsUsed, let limit = self.requestsLimit, limit > 0 { (Double(used) / Double(limit)) * 100 } else { self.planPercentUsed } let primary = RateWindow( usedPercent: primaryUsedPercent, windowMinutes: nil, resetsAt: self.billingCycleEnd, resetDescription: self.billingCycleEnd.map { Self.formatResetDate($0) }) // Always use individual on-demand values (what users see in their Cursor dashboard). // Team values are aggregates across all members, not useful for individual tracking. let resolvedOnDemandUsed = self.onDemandUsedUSD let resolvedOnDemandLimit = self.onDemandLimitUSD // Secondary: On-demand usage as percentage of individual limit let secondary: RateWindow? = if let limit = resolvedOnDemandLimit, limit > 0 { RateWindow( usedPercent: (resolvedOnDemandUsed / limit) * 100, windowMinutes: nil, resetsAt: self.billingCycleEnd, resetDescription: self.billingCycleEnd.map { Self.formatResetDate($0) }) } else { nil } // Provider cost snapshot for on-demand usage let providerCost: ProviderCostSnapshot? = if resolvedOnDemandUsed > 0 { ProviderCostSnapshot( used: resolvedOnDemandUsed, limit: resolvedOnDemandLimit ?? 0, currencyCode: "USD", period: "monthly", resetsAt: self.billingCycleEnd, updatedAt: Date()) } else { nil } // Legacy plan request usage (when maxRequestUsage is set) let cursorRequests: CursorRequestUsage? = if let used = self.requestsUsed, let limit = self.requestsLimit { CursorRequestUsage(used: used, limit: limit) } else { nil } let identity = ProviderIdentitySnapshot( providerID: .cursor, accountEmail: self.accountEmail, accountOrganization: nil, loginMethod: self.membershipType.map { Self.formatMembershipType($0) }) return UsageSnapshot( primary: primary, secondary: secondary, tertiary: nil, providerCost: providerCost, cursorRequests: cursorRequests, updatedAt: Date(), identity: identity) } private static func formatResetDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "MMM d 'at' h:mma" formatter.locale = Locale(identifier: "en_US_POSIX") return "Resets " + formatter.string(from: date) } private static func formatMembershipType(_ type: String) -> String { switch type.lowercased() { case "enterprise": "Cursor Enterprise" case "pro": "Cursor Pro" case "hobby": "Cursor Hobby" case "team": "Cursor Team" default: "Cursor \(type.capitalized)" } }} // MARK: - Cursor Status Probe Error public enum CursorStatusProbeError: LocalizedError, Sendable { case notLoggedIn case networkError(String) case parseFailed(String) case noSessionCookie public var errorDescription: String? { switch self { case .notLoggedIn: "Not logged in to Cursor. Please log in via the CodexBar menu." case let .networkError(msg): "Cursor API error: \(msg)" case let .parseFailed(msg): "Could not parse Cursor usage: \(msg)" case .noSessionCookie: "No Cursor session found. Please log in to cursor.com in \(cursorCookieImportOrder.loginHint)." } }} // MARK: - Cursor Session Store public actor CursorSessionStore { public static let shared = CursorSessionStore() private var sessionCookies: [HTTPCookie] = [] private var hasLoadedFromDisk = false private let fileURL: URL private init() { let fm = FileManager.default let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? fm.temporaryDirectory let dir = appSupport.appendingPathComponent("CodexBar", isDirectory: true) try? fm.createDirectory(at: dir, withIntermediateDirectories: true) self.fileURL = dir.appendingPathComponent("cursor-session.json") // Load saved cookies on init Task { await self.loadFromDiskIfNeeded() } } public func setCookies(_ cookies: [HTTPCookie]) { self.hasLoadedFromDisk = true self.sessionCookies = cookies self.saveToDisk() } public func getCookies() -> [HTTPCookie] { self.loadFromDiskIfNeeded() return self.sessionCookies } public func clearCookies() { self.hasLoadedFromDisk = true self.sessionCookies = [] try? FileManager.default.removeItem(at: self.fileURL) } public func hasValidSession() -> Bool { self.loadFromDiskIfNeeded() return !self.sessionCookies.isEmpty } #if DEBUG func resetForTesting(clearDisk: Bool = true) { self.hasLoadedFromDisk = false self.sessionCookies = [] if clearDisk { try? FileManager.default.removeItem(at: self.fileURL) } } #endif private func loadFromDiskIfNeeded() { guard !self.hasLoadedFromDisk else { return } self.hasLoadedFromDisk = true self.loadFromDisk() } private func saveToDisk() { // Convert cookie properties to JSON-serializable format // Date values must be converted to TimeInterval (Double) let cookieData = self.sessionCookies.compactMap { cookie -> [String: Any]? in guard let props = cookie.properties else { return nil } var serializable: [String: Any] = [:] for (key, value) in props { let keyString = key.rawValue if let date = value as? Date { // Convert Date to TimeInterval for JSON compatibility serializable[keyString] = date.timeIntervalSince1970 serializable[keyString + "_isDate"] = true } else if let url = value as? URL { serializable[keyString] = url.absoluteString serializable[keyString + "_isURL"] = true } else if JSONSerialization.isValidJSONObject([value]) || value is String || value is Bool || value is NSNumber { serializable[keyString] = value } } return serializable } guard !cookieData.isEmpty, let data = try? JSONSerialization.data(withJSONObject: cookieData, options: [.prettyPrinted]) else { return } try? data.write(to: self.fileURL) } private func loadFromDisk() { guard let data = try? Data(contentsOf: self.fileURL), let cookieArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return } self.sessionCookies = cookieArray.compactMap { props in // Convert back to HTTPCookiePropertyKey dictionary var cookieProps: [HTTPCookiePropertyKey: Any] = [:] for (key, value) in props { // Skip marker keys if key.hasSuffix("_isDate") || key.hasSuffix("_isURL") { continue } let propKey = HTTPCookiePropertyKey(key) // Check if this was a Date if props[key + "_isDate"] as? Bool == true, let interval = value as? TimeInterval { cookieProps[propKey] = Date(timeIntervalSince1970: interval) } // Check if this was a URL else if props[key + "_isURL"] as? Bool == true, let urlString = value as? String { cookieProps[propKey] = URL(string: urlString) } else { cookieProps[propKey] = value } } return HTTPCookie(properties: cookieProps) } }} // MARK: - Cursor Status Probe public struct CursorStatusProbe: Sendable { public let baseURL: URL public var timeout: TimeInterval = 15.0 private let browserDetection: BrowserDetection public init( baseURL: URL = URL(string: "https://cursor.com")!, timeout: TimeInterval = 15.0, browserDetection: BrowserDetection) { self.baseURL = baseURL self.timeout = timeout self.browserDetection = browserDetectionLines 1–500 of 767. Load more
import Foundation import SweetCookieKit #if os(macOS) private let cursorCookieImportOrder: BrowserCookieImportOrder = ProviderDefaults.metadata[.cursor]?.browserCookieOrder ?? Browser.defaultImportOrder // MARK: - Cursor Cookie Importer /// Imports Cursor session cookies from browser cookies. public enum CursorCookieImporter { private static let cookieClient = BrowserCookieClient() private static let sessionCookieNames: Set<String> = [ "WorkosCursorSessionToken", "__Secure-next-auth.session-token", "next-auth.session-token", ] public struct SessionInfo: Sendable { public let cookies: [HTTPCookie] public let sourceLabel: String public init(cookies: [HTTPCookie], sourceLabel: String) { self.cookies = cookies self.sourceLabel = sourceLabel } public var cookieHeader: String { self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") } } /// Attempts to import Cursor cookies using the standard browser import order. public static func importSession( browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) throws -> SessionInfo { let log: (String) -> Void = { msg in logger?("[cursor-cookie] \(msg)") } // Filter to cookie-eligible browsers to avoid unnecessary keychain prompts let installedBrowsers = cursorCookieImportOrder.cookieImportCandidates(using: browserDetection) let cookieDomains = ["cursor.com", "cursor.sh"] for browserSource in installedBrowsers { do { let query = BrowserCookieQuery(domains: cookieDomains) let sources = try Self.cookieClient.records( matching: query, in: browserSource, logger: log) for source in sources where !source.records.isEmpty { let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin) if httpCookies.contains(where: { Self.sessionCookieNames.contains($0.name) }) { log("Found \(httpCookies.count) Cursor cookies in \(source.label)") return SessionInfo(cookies: httpCookies, sourceLabel: source.label) } else { log("\(source.label) cookies found, but no Cursor session cookie present") } } } catch { log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } } throw CursorStatusProbeError.noSessionCookie } /// Check if Cursor session cookies are available public static func hasSession(browserDetection: BrowserDetection, logger: ((String) -> Void)? = nil) -> Bool { do { let session = try self.importSession(browserDetection: browserDetection, logger: logger) return !session.cookies.isEmpty } catch { return false } } } // MARK: - Cursor API Models public struct CursorUsageSummary: Codable, Sendable { public let billingCycleStart: String? public let billingCycleEnd: String? public let membershipType: String? public let limitType: String? public let isUnlimited: Bool? public let autoModelSelectedDisplayMessage: String? public let namedModelSelectedDisplayMessage: String? public let individualUsage: CursorIndividualUsage? public let teamUsage: CursorTeamUsage? } public struct CursorIndividualUsage: Codable, Sendable { public let plan: CursorPlanUsage? public let onDemand: CursorOnDemandUsage? } public struct CursorPlanUsage: Codable, Sendable { public let enabled: Bool? /// Usage in cents (e.g., 2000 = $20.00) public let used: Int? /// Limit in cents (e.g., 2000 = $20.00) public let limit: Int? /// Remaining in cents public let remaining: Int? public let breakdown: CursorPlanBreakdown? public let autoPercentUsed: Double? public let apiPercentUsed: Double? public let totalPercentUsed: Double? } public struct CursorPlanBreakdown: Codable, Sendable { public let included: Int? public let bonus: Int? public let total: Int? } public struct CursorOnDemandUsage: Codable, Sendable { public let enabled: Bool? /// Usage in cents public let used: Int? /// Limit in cents (nil if unlimited) public let limit: Int? /// Remaining in cents (nil if unlimited) public let remaining: Int? } public struct CursorTeamUsage: Codable, Sendable { public let onDemand: CursorOnDemandUsage? } // MARK: - Cursor Usage API Models (Legacy Request-Based Plans) /// Response from `/api/usage?user=ID` endpoint for legacy request-based plans. public struct CursorUsageResponse: Codable, Sendable { public let gpt4: CursorModelUsage? public let startOfMonth: String? enum CodingKeys: String, CodingKey { case gpt4 = "gpt-4" case startOfMonth } } public struct CursorModelUsage: Codable, Sendable { public let numRequests: Int? public let numRequestsTotal: Int? public let numTokens: Int? public let maxRequestUsage: Int? public let maxTokenUsage: Int? } public struct CursorUserInfo: Codable, Sendable { public let email: String? public let emailVerified: Bool? public let name: String? public let sub: String? public let createdAt: String? public let updatedAt: String? public let picture: String? enum CodingKeys: String, CodingKey { case email case emailVerified = "email_verified" case name case sub case createdAt = "created_at" case updatedAt = "updated_at" case picture } } // MARK: - Cursor Status Snapshot public struct CursorStatusSnapshot: Sendable { /// Percentage of included plan usage (0-100) public let planPercentUsed: Double /// Included plan usage in USD public let planUsedUSD: Double /// Included plan limit in USD public let planLimitUSD: Double /// On-demand usage in USD public let onDemandUsedUSD: Double /// On-demand limit in USD (nil if unlimited) public let onDemandLimitUSD: Double? /// Team on-demand usage in USD (for team plans) public let teamOnDemandUsedUSD: Double? /// Team on-demand limit in USD public let teamOnDemandLimitUSD: Double? /// Billing cycle reset date public let billingCycleEnd: Date? /// Membership type (e.g., "enterprise", "pro", "hobby") public let membershipType: String? /// User email public let accountEmail: String? /// User name public let accountName: String? /// Raw API response for debugging public let rawJSON: String? // MARK: - Legacy Plan (Request-Based) Fields /// Requests used this billing cycle (legacy plans only) public let requestsUsed: Int? /// Request limit (non-nil indicates legacy request-based plan) public let requestsLimit: Int? /// Whether this is a legacy request-based plan (vs token-based) public var isLegacyRequestPlan: Bool { self.requestsLimit != nil } public init( planPercentUsed: Double, planUsedUSD: Double, planLimitUSD: Double, onDemandUsedUSD: Double, onDemandLimitUSD: Double?, teamOnDemandUsedUSD: Double?, teamOnDemandLimitUSD: Double?, billingCycleEnd: Date?, membershipType: String?, accountEmail: String?, accountName: String?, rawJSON: String?, requestsUsed: Int? = nil, requestsLimit: Int? = nil) { self.planPercentUsed = planPercentUsed self.planUsedUSD = planUsedUSD self.planLimitUSD = planLimitUSD self.onDemandUsedUSD = onDemandUsedUSD self.onDemandLimitUSD = onDemandLimitUSD self.teamOnDemandUsedUSD = teamOnDemandUsedUSD self.teamOnDemandLimitUSD = teamOnDemandLimitUSD self.billingCycleEnd = billingCycleEnd self.membershipType = membershipType self.accountEmail = accountEmail self.accountName = accountName self.rawJSON = rawJSON self.requestsUsed = requestsUsed self.requestsLimit = requestsLimit } /// Convert to UsageSnapshot for the common provider interface public func toUsageSnapshot() -> UsageSnapshot { // Primary: For legacy request-based plans, use request usage; otherwise use plan percentage let primaryUsedPercent: Double = if self.isLegacyRequestPlan, let used = self.requestsUsed, let limit = self.requestsLimit, limit > 0 { (Double(used) / Double(limit)) * 100 } else { self.planPercentUsed } let primary = RateWindow( usedPercent: primaryUsedPercent, windowMinutes: nil, resetsAt: self.billingCycleEnd, resetDescription: self.billingCycleEnd.map { Self.formatResetDate($0) }) // Always use individual on-demand values (what users see in their Cursor dashboard). // Team values are aggregates across all members, not useful for individual tracking. let resolvedOnDemandUsed = self.onDemandUsedUSD let resolvedOnDemandLimit = self.onDemandLimitUSD // Secondary: On-demand usage as percentage of individual limit let secondary: RateWindow? = if let limit = resolvedOnDemandLimit, limit > 0 { RateWindow( usedPercent: (resolvedOnDemandUsed / limit) * 100, windowMinutes: nil, resetsAt: self.billingCycleEnd, resetDescription: self.billingCycleEnd.map { Self.formatResetDate($0) }) } else { nil } // Provider cost snapshot for on-demand usage let providerCost: ProviderCostSnapshot? = if resolvedOnDemandUsed > 0 { ProviderCostSnapshot( used: resolvedOnDemandUsed, limit: resolvedOnDemandLimit ?? 0, currencyCode: "USD", period: "monthly", resetsAt: self.billingCycleEnd, updatedAt: Date()) } else { nil } // Legacy plan request usage (when maxRequestUsage is set) let cursorRequests: CursorRequestUsage? = if let used = self.requestsUsed, let limit = self.requestsLimit { CursorRequestUsage(used: used, limit: limit) } else { nil } let identity = ProviderIdentitySnapshot( providerID: .cursor, accountEmail: self.accountEmail, accountOrganization: nil, loginMethod: self.membershipType.map { Self.formatMembershipType($0) }) return UsageSnapshot( primary: primary, secondary: secondary, tertiary: nil, providerCost: providerCost, cursorRequests: cursorRequests, updatedAt: Date(), identity: identity) } private static func formatResetDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "MMM d 'at' h:mma" formatter.locale = Locale(identifier: "en_US_POSIX") return "Resets " + formatter.string(from: date) } private static func formatMembershipType(_ type: String) -> String { switch type.lowercased() { case "enterprise": "Cursor Enterprise" case "pro": "Cursor Pro" case "hobby": "Cursor Hobby" case "team": "Cursor Team" default: "Cursor \(type.capitalized)" } } } // MARK: - Cursor Status Probe Error public enum CursorStatusProbeError: LocalizedError, Sendable { case notLoggedIn case networkError(String) case parseFailed(String) case noSessionCookie public var errorDescription: String? { switch self { case .notLoggedIn: "Not logged in to Cursor. Please log in via the CodexBar menu." case let .networkError(msg): "Cursor API error: \(msg)" case let .parseFailed(msg): "Could not parse Cursor usage: \(msg)" case .noSessionCookie: "No Cursor session found. Please log in to cursor.com in \(cursorCookieImportOrder.loginHint)." } } } // MARK: - Cursor Session Store public actor CursorSessionStore { public static let shared = CursorSessionStore() private var sessionCookies: [HTTPCookie] = [] private var hasLoadedFromDisk = false private let fileURL: URL private init() { let fm = FileManager.default let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? fm.temporaryDirectory let dir = appSupport.appendingPathComponent("CodexBar", isDirectory: true) try? fm.createDirectory(at: dir, withIntermediateDirectories: true) self.fileURL = dir.appendingPathComponent("cursor-session.json") // Load saved cookies on init Task { await self.loadFromDiskIfNeeded() } } public func setCookies(_ cookies: [HTTPCookie]) { self.hasLoadedFromDisk = true self.sessionCookies = cookies self.saveToDisk() } public func getCookies() -> [HTTPCookie] { self.loadFromDiskIfNeeded() return self.sessionCookies } public func clearCookies() { self.hasLoadedFromDisk = true self.sessionCookies = [] try? FileManager.default.removeItem(at: self.fileURL) } public func hasValidSession() -> Bool { self.loadFromDiskIfNeeded() return !self.sessionCookies.isEmpty } #if DEBUG func resetForTesting(clearDisk: Bool = true) { self.hasLoadedFromDisk = false self.sessionCookies = [] if clearDisk { try? FileManager.default.removeItem(at: self.fileURL) } } #endif private func loadFromDiskIfNeeded() { guard !self.hasLoadedFromDisk else { return } self.hasLoadedFromDisk = true self.loadFromDisk() } private func saveToDisk() { // Convert cookie properties to JSON-serializable format // Date values must be converted to TimeInterval (Double) let cookieData = self.sessionCookies.compactMap { cookie -> [String: Any]? in guard let props = cookie.properties else { return nil } var serializable: [String: Any] = [:] for (key, value) in props { let keyString = key.rawValue if let date = value as? Date { // Convert Date to TimeInterval for JSON compatibility serializable[keyString] = date.timeIntervalSince1970 serializable[keyString + "_isDate"] = true } else if let url = value as? URL { serializable[keyString] = url.absoluteString serializable[keyString + "_isURL"] = true } else if JSONSerialization.isValidJSONObject([value]) || value is String || value is Bool || value is NSNumber { serializable[keyString] = value } } return serializable } guard !cookieData.isEmpty, let data = try? JSONSerialization.data(withJSONObject: cookieData, options: [.prettyPrinted]) else { return } try? data.write(to: self.fileURL) } private func loadFromDisk() { guard let data = try? Data(contentsOf: self.fileURL), let cookieArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return } self.sessionCookies = cookieArray.compactMap { props in // Convert back to HTTPCookiePropertyKey dictionary var cookieProps: [HTTPCookiePropertyKey: Any] = [:] for (key, value) in props { // Skip marker keys if key.hasSuffix("_isDate") || key.hasSuffix("_isURL") { continue } let propKey = HTTPCookiePropertyKey(key) // Check if this was a Date if props[key + "_isDate"] as? Bool == true, let interval = value as? TimeInterval { cookieProps[propKey] = Date(timeIntervalSince1970: interval) } // Check if this was a URL else if props[key + "_isURL"] as? Bool == true, let urlString = value as? String { cookieProps[propKey] = URL(string: urlString) } else { cookieProps[propKey] = value } } return HTTPCookie(properties: cookieProps) } } } // MARK: - Cursor Status Probe public struct CursorStatusProbe: Sendable { public let baseURL: URL public var timeout: TimeInterval = 15.0 private let browserDetection: BrowserDetection public init( baseURL: URL = URL(string: "https://cursor.com")!, timeout: TimeInterval = 15.0, browserDetection: BrowserDetection) { self.baseURL = baseURL self.timeout = timeout self.browserDetection = browserDetection