-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(linux): support Cursor provider via app auth token #1908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,12 +5,18 @@ import FoundationNetworking | |
| import SweetCookieKit | ||
| #if canImport(SQLite3) | ||
| import SQLite3 | ||
| #elseif canImport(CSQLite3) | ||
| import CSQLite3 | ||
| #endif | ||
|
|
||
| #if os(macOS) | ||
| #if os(macOS) || os(Linux) | ||
|
|
||
| #if os(macOS) | ||
| private let cursorCookieImportOrder: BrowserCookieImportOrder = | ||
| ProviderDefaults.metadata[.cursor]?.browserCookieOrder ?? Browser.defaultImportOrder | ||
| #endif | ||
|
|
||
| #if os(macOS) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this file is compiled on Linux, the new outer Useful? React with 👍 / 👎. |
||
|
|
||
| // MARK: - Cursor Cookie Importer | ||
|
|
||
|
|
@@ -186,6 +192,7 @@ public enum CursorCookieImporter { | |
| } | ||
| } | ||
| } | ||
| #endif | ||
|
|
||
| // MARK: - Cursor API Models | ||
|
|
||
|
|
@@ -408,17 +415,44 @@ protocol CursorAppAuthSessionProviding: Sendable { | |
| } | ||
|
|
||
| struct CursorAppAuthStore: CursorAppAuthSessionProviding { | ||
| private static let defaultDBPath: String = { | ||
| let home = NSHomeDirectory() | ||
| return "\(home)/Library/Application Support/Cursor/User/globalStorage/state.vscdb" | ||
| }() | ||
| private static let defaultDBPath: String = Self.resolveDefaultDBPath() | ||
|
|
||
| private let dbPath: String | ||
|
|
||
| init(dbPath: String? = nil) { | ||
| self.dbPath = dbPath ?? Self.defaultDBPath | ||
| } | ||
|
|
||
| static func resolveDefaultDBPath( | ||
| home: String = NSHomeDirectory(), | ||
| environment: [String: String] = ProcessInfo.processInfo.environment, | ||
| fileManager: FileManager = .default) -> String | ||
| { | ||
| #if os(macOS) | ||
| _ = environment | ||
| _ = fileManager | ||
| return "\(home)/Library/Application Support/Cursor/User/globalStorage/state.vscdb" | ||
| #elseif os(Linux) | ||
| let configHome = environment[CodexBarConfigStore.xdgConfigHomeEnvironmentKey]? | ||
| .trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let expandedConfigHome = configHome.map { ($0 as NSString).expandingTildeInPath } | ||
| let base: String = if let expandedConfigHome, | ||
| !expandedConfigHome.isEmpty, | ||
| (expandedConfigHome as NSString).isAbsolutePath | ||
| { | ||
| expandedConfigHome | ||
| } else { | ||
| "\(home)/.config" | ||
| } | ||
| return "\(base)/Cursor/User/globalStorage/state.vscdb" | ||
| #else | ||
| _ = home | ||
| _ = environment | ||
| _ = fileManager | ||
| return "" | ||
| #endif | ||
| } | ||
|
|
||
| func loadSession() throws -> CursorAppAuthSession? { | ||
| guard FileManager.default.fileExists(atPath: self.dbPath) else { return nil } | ||
|
|
||
|
|
@@ -705,15 +739,25 @@ public enum CursorStatusProbeError: LocalizedError, Sendable { | |
| public var errorDescription: String? { | ||
| switch self { | ||
| case .notLoggedIn: | ||
| #if os(macOS) | ||
| "Not logged in to Cursor. Please log in via the CodexBar menu." | ||
| #else | ||
| "Not logged in to Cursor. Sign in to the Cursor app on this machine or paste a Cookie header copied " | ||
| + "from cursor.com into ~/.config/codexbar/config.json (legacy: ~/.codexbar/config.json)." | ||
| #endif | ||
| case let .networkError(msg): | ||
| "Cursor API error: \(msg)" | ||
| case let .parseFailed(msg): | ||
| "Could not parse Cursor usage: \(msg)" | ||
| case .noSessionCookie: | ||
| #if os(macOS) | ||
| "No Cursor session found. \(Self.safariFullDiskAccessHint) " | ||
| + "Please log in to cursor.com in \(cursorCookieImportOrder.loginHint). " | ||
| + "You can also sign in to Cursor from the CodexBar menu (Add / switch account)." | ||
| #else | ||
| "No Cursor session found. Sign in to the Cursor app on this machine or paste a Cookie header copied " | ||
| + "from cursor.com into ~/.config/codexbar/config.json (legacy: ~/.codexbar/config.json)." | ||
| #endif | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -860,7 +904,7 @@ public struct CursorStatusProbe: Sendable { | |
| baseURL: baseURL, | ||
| timeout: timeout, | ||
| browserDetection: browserDetection, | ||
| browserCookieImportOrder: cursorCookieImportOrder, | ||
| browserCookieImportOrder: Self.defaultBrowserCookieImportOrder, | ||
| urlSession: urlSession, | ||
| appAuthStore: CursorAppAuthStore()) | ||
| } | ||
|
|
@@ -869,7 +913,7 @@ public struct CursorStatusProbe: Sendable { | |
| baseURL: URL = URL(string: "https://cursor.com")!, | ||
| timeout: TimeInterval = 15.0, | ||
| browserDetection: BrowserDetection, | ||
| browserCookieImportOrder: BrowserCookieImportOrder = cursorCookieImportOrder, | ||
| browserCookieImportOrder: BrowserCookieImportOrder = Self.defaultBrowserCookieImportOrder, | ||
| urlSession: any ProviderHTTPTransport = ProviderHTTPClient.shared, | ||
| appAuthStore: any CursorAppAuthSessionProviding) | ||
| { | ||
|
|
@@ -927,6 +971,7 @@ public struct CursorStatusProbe: Sendable { | |
| } | ||
| } | ||
|
|
||
| #if os(macOS) | ||
| // Try each browser in order. The first browser that *has* session cookie names is not always valid | ||
| // (e.g. stale Chrome tokens); keep trying until the API accepts a session or we run out of browsers. | ||
| let browserCandidates = self.browserCookieImportOrder.cookieImportCandidates(using: self.browserDetection) | ||
|
|
@@ -965,6 +1010,7 @@ public struct CursorStatusProbe: Sendable { | |
| case let .exhausted(error): | ||
| firstRecoverableError = error ?? firstRecoverableError | ||
| } | ||
| #endif | ||
|
|
||
| // Fall back to stored session cookies (from "Add Account" login flow) | ||
| if allowCachedSessions { | ||
|
|
@@ -1022,6 +1068,7 @@ public struct CursorStatusProbe: Sendable { | |
| throw CursorStatusProbeError.noSessionCookie | ||
| } | ||
|
|
||
| #if os(macOS) | ||
| enum ImportedSessionFetchOutcome { | ||
| case succeeded(CursorStatusSnapshot) | ||
| case tryNextBrowser | ||
|
|
@@ -1104,6 +1151,7 @@ public struct CursorStatusProbe: Sendable { | |
| return .failed(.networkError(error.localizedDescription)) | ||
| } | ||
| } | ||
| #endif | ||
|
|
||
| private func fetchWithCookieHeader( | ||
| _ cookieHeader: String, | ||
|
|
@@ -1363,6 +1411,12 @@ public struct CursorStatusProbe: Sendable { | |
| requestsUsed: requestsUsed, | ||
| requestsLimit: requestsLimit) | ||
| } | ||
|
|
||
| #if os(macOS) | ||
| private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = cursorCookieImportOrder | ||
| #else | ||
| private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = [] | ||
| #endif | ||
| } | ||
|
|
||
| #else | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #if os(Linux) | ||
| import Foundation | ||
| import Testing | ||
| @testable import CodexBarCLI | ||
| @testable import CodexBarCore | ||
|
|
||
| struct CursorLinuxTests { | ||
| @Test | ||
| func `Cursor database path honors absolute XDG config home`() { | ||
| let path = CursorAppAuthStore.resolveDefaultDBPath( | ||
| home: "/home/test", | ||
| environment: ["XDG_CONFIG_HOME": "/custom/config"]) | ||
| #expect(path == "/custom/config/Cursor/User/globalStorage/state.vscdb") | ||
| } | ||
|
|
||
| @Test | ||
| func `Cursor database path falls back to dot config`() { | ||
| let path = CursorAppAuthStore.resolveDefaultDBPath( | ||
| home: "/home/test", | ||
| environment: [:]) | ||
| #expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb") | ||
| } | ||
|
|
||
| @Test | ||
| func `Cursor database path rejects relative XDG config home`() { | ||
| let path = CursorAppAuthStore.resolveDefaultDBPath( | ||
| home: "/home/test", | ||
| environment: ["XDG_CONFIG_HOME": "relative/config"]) | ||
| #expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb") | ||
| } | ||
|
|
||
| @Test | ||
| func `Cursor automatic source does not require macOS web support`() { | ||
| #expect(!CodexBarCLI.sourceModeRequiresWebSupport( | ||
| .auto, | ||
| provider: .cursor, | ||
| settings: ProviderSettingsSnapshot.make( | ||
| cursor: .init(cookieSource: .auto, manualCookieHeader: nil)))) | ||
| } | ||
|
|
||
| @Test | ||
| func `Cursor descriptor accepts explicit web source`() { | ||
| #expect(CursorProviderDescriptor.descriptor.fetchPlan.sourceModes.contains(.web)) | ||
| } | ||
|
|
||
| @Test | ||
| func `Cursor manual cookie does not require macOS web support`() { | ||
| #expect(!CodexBarCLI.sourceModeRequiresWebSupport( | ||
| .web, | ||
| provider: .cursor, | ||
| settings: ProviderSettingsSnapshot.make( | ||
| cursor: .init( | ||
| cookieSource: .manual, | ||
| manualCookieHeader: "WorkosCursorSessionToken=test")))) | ||
| } | ||
|
|
||
| @Test | ||
| func `disabled Cursor web source still requires macOS web support`() { | ||
| #expect(CodexBarCLI.sourceModeRequiresWebSupport( | ||
| .web, | ||
| provider: .cursor, | ||
| settings: ProviderSettingsSnapshot.make( | ||
| cursor: .init(cookieSource: .off, manualCookieHeader: nil)))) | ||
| } | ||
| } | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Linux Cursor manual-cookie runs with
--source weborsource: "web", this exception now skips the macOS-only web-support error, butCursorProviderDescriptorstill declares only.autoand.cli(Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift:36) andCLIHelpers.fetchProviderUsagerejects unsupported source modes before the strategy runs (Sources/CodexBarCLI/CLIHelpers.swift:191). The result is stillSource 'web' is not supported for cursorinstead of using the configured manual cookie; either add.webto Cursor's source modes or limit this Linux exception to modes the descriptor actually accepts.Useful? React with 👍 / 👎.