From 95a61ecfc54af8c8871d5e078dbd8af7e1e67c6b Mon Sep 17 00:00:00 2001 From: red Date: Sat, 4 Jul 2026 22:46:11 -0300 Subject: [PATCH 1/4] feat(linux): support Cursor provider via app auth token Enable the Cursor usage provider on Linux by reading the signed-in Cursor app's access token from the XDG config state database and reusing the existing cursor.com usage endpoints. - Resolve Linux state.vscdb path via XDG_CONFIG_HOME - Skip macOS-only browser cookie import on Linux - Allow Linux CLI web-source routing for Cursor - Document Linux auth path and manual cookie support --- Sources/CodexBarCLI/CLIUsageCommand.swift | 8 +++ .../ProviderStorageFootprint.swift | 1 + .../Providers/Cursor/CursorStatusProbe.swift | 57 ++++++++++++++++--- Tests/CodexBarTests/CLIEntryTests.swift | 17 ++++++ .../CursorStatusProbeTests.swift | 18 ++++++ docs/cursor.md | 12 +++- 6 files changed, 104 insertions(+), 9 deletions(-) diff --git a/Sources/CodexBarCLI/CLIUsageCommand.swift b/Sources/CodexBarCLI/CLIUsageCommand.swift index 7f822a3b1b..e92adb45fc 100644 --- a/Sources/CodexBarCLI/CLIUsageCommand.swift +++ b/Sources/CodexBarCLI/CLIUsageCommand.swift @@ -606,6 +606,14 @@ extension CodexBarCLI { { return false } + #if os(Linux) + if provider == .cursor, + settings?.cursor?.cookieSource != .off + { + // Linux uses Cursor app auth and manual cookies; browser import remains macOS-only. + return false + } + #endif if provider == .sakana, sourceMode == .auto || sourceMode == .web, environment.map({ SakanaSettingsReader.cookieHeader(environment: $0) != nil }) == true diff --git a/Sources/CodexBarCore/ProviderStorageFootprint.swift b/Sources/CodexBarCore/ProviderStorageFootprint.swift index 75feb7a366..f47d09fd36 100644 --- a/Sources/CodexBarCore/ProviderStorageFootprint.swift +++ b/Sources/CodexBarCore/ProviderStorageFootprint.swift @@ -332,6 +332,7 @@ public enum ProviderStoragePathCatalog { case .cursor: [ homePath("Library/Application Support/Cursor"), + homePath(".config/Cursor"), homePath("Library/Application Support/Caches/cursor-updater"), homePath(".cursor"), homePath("Library/Caches/Cursor"), diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift index 6bb82fa037..c293a829c1 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift @@ -7,11 +7,14 @@ import SweetCookieKit import SQLite3 #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) // MARK: - Cursor Cookie Importer /// Imports Cursor session cookies from browser cookies. @@ -186,6 +189,7 @@ public enum CursorCookieImporter { } } } +#endif // MARK: - Cursor API Models @@ -408,10 +412,7 @@ 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 @@ -419,6 +420,32 @@ struct CursorAppAuthStore: CursorAppAuthSessionProviding { 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 base: String = if let configHome, !configHome.isEmpty { + (configHome as NSString).expandingTildeInPath + } 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 +732,23 @@ 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 session cookie in CodexBar settings." + #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, paste a Cookie header from cursor.com in settings, or log in to cursor.com in your browser." + #endif } } } @@ -860,7 +895,7 @@ public struct CursorStatusProbe: Sendable { baseURL: baseURL, timeout: timeout, browserDetection: browserDetection, - browserCookieImportOrder: cursorCookieImportOrder, + browserCookieImportOrder: Self.defaultBrowserCookieImportOrder, urlSession: urlSession, appAuthStore: CursorAppAuthStore()) } @@ -869,7 +904,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 +962,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 +1001,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 { @@ -1363,6 +1400,12 @@ public struct CursorStatusProbe: Sendable { requestsUsed: requestsUsed, requestsLimit: requestsLimit) } + + #if os(macOS) + private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = cursorCookieImportOrder + #else + private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = Browser.defaultImportOrder + #endif } #else diff --git a/Tests/CodexBarTests/CLIEntryTests.swift b/Tests/CodexBarTests/CLIEntryTests.swift index 5e51be2166..0d22c410ae 100644 --- a/Tests/CodexBarTests/CLIEntryTests.swift +++ b/Tests/CodexBarTests/CLIEntryTests.swift @@ -454,5 +454,22 @@ final class CLIEntryTests: XCTestCase { .auto, provider: .mimo, environment: ["MIMO_LOCAL_USAGE_PATH": directory.appendingPathComponent("missing.json").path])) + #if os(Linux) + XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( + .auto, + provider: .cursor, + settings: ProviderSettingsSnapshot.make( + cursor: .init(cookieSource: .auto, manualCookieHeader: nil)))) + XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( + .web, + provider: .cursor, + settings: ProviderSettingsSnapshot.make( + cursor: .init(cookieSource: .manual, manualCookieHeader: "WorkosCursorSessionToken=test")))) + XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( + .auto, + provider: .cursor, + settings: ProviderSettingsSnapshot.make( + cursor: .init(cookieSource: .off, manualCookieHeader: nil)))) + #endif } } diff --git a/Tests/CodexBarTests/CursorStatusProbeTests.swift b/Tests/CodexBarTests/CursorStatusProbeTests.swift index 921a4ed8b9..819c90ac06 100644 --- a/Tests/CodexBarTests/CursorStatusProbeTests.swift +++ b/Tests/CodexBarTests/CursorStatusProbeTests.swift @@ -976,6 +976,24 @@ extension CursorStatusProbeTests { #expect(session == CursorAppAuthSession(accessToken: "app-token")) } + #if os(Linux) + @Test + func `resolveDefaultDBPath honors XDG config home on Linux`() { + let path = CursorAppAuthStore.resolveDefaultDBPath( + home: "/home/test", + environment: ["XDG_CONFIG_HOME": "/custom/config"]) + #expect(path == "/custom/config/Cursor/User/globalStorage/state.vscdb") + } + + @Test + func `resolveDefaultDBPath falls back to dot config on Linux`() { + let path = CursorAppAuthStore.resolveDefaultDBPath( + home: "/home/test", + environment: [:]) + #expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb") + } + #endif + @Test func `fetch ignores user info failure when usage summary succeeds`() async throws { let testSession = CursorStatusProbeTestSession { request in diff --git a/docs/cursor.md b/docs/cursor.md index d303fdfe96..2200298ab9 100644 --- a/docs/cursor.md +++ b/docs/cursor.md @@ -31,8 +31,11 @@ Cursor is primarily web-backed. Usage is fetched via browser cookies or a stored 4) **Cursor.app local auth** (last fallback) - Reads Cursor.app's VS Code-style global state DB for the local app bearer token. - - File: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`. + - File: + - macOS: `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` + - Linux: `$XDG_CONFIG_HOME/Cursor/User/globalStorage/state.vscdb` (default `~/.config/Cursor/...`) - Used only after cookie/session sources fail so existing account-selection precedence stays stable. + - On Linux, this is the primary automatic source because browser import and the WebKit login flow are macOS-only. - Derives Cursor's first-party web-session cookie, then uses the same usage and account endpoints as browser sessions. - Account identity comes from that authenticated session; cached app profile fields are not mixed across accounts. @@ -53,9 +56,14 @@ Manual option: - Chrome/Chromium forks: `~/Library/Application Support/Google/Chrome/*/Cookies` - Firefox: `~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite` +## Linux CLI +- `codexbar usage --provider cursor` reads the signed-in Cursor app's access token from the Linux global state DB and reuses the same `cursor.com` usage endpoints as macOS. +- Automatic browser cookie import and the in-app WebKit login flow remain macOS-only. +- Manual cookie headers from `~/.codexbar/config.json` work on Linux. + ## Local storage footprint When **Settings → Advanced → Track provider local storage** is enabled, CodexBar measures: -- `~/Library/Application Support/Cursor` +- `~/Library/Application Support/Cursor` (macOS) or `~/.config/Cursor` (Linux) - `~/Library/Application Support/Caches/cursor-updater` - `~/.cursor` - `~/Library/Caches/Cursor` From 2e9bd2fe8d97f1503e020f4a821c6128a8cb73c6 Mon Sep 17 00:00:00 2001 From: red Date: Sat, 4 Jul 2026 22:58:39 -0300 Subject: [PATCH 2/4] fix(linux): address review feedback for Cursor provider - Gate macOS-only browser session helpers behind os(macOS) - Use an empty browser import order on non-macOS Linux builds - Move Linux Cursor coverage into TestsLinux --- .../Providers/Cursor/CursorStatusProbe.swift | 4 +- Tests/CodexBarTests/CLIEntryTests.swift | 17 ------ .../CursorStatusProbeTests.swift | 18 ------- TestsLinux/CursorLinuxTests.swift | 52 +++++++++++++++++++ 4 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 TestsLinux/CursorLinuxTests.swift diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift index c293a829c1..6a952cdf98 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift @@ -1059,6 +1059,7 @@ public struct CursorStatusProbe: Sendable { throw CursorStatusProbeError.noSessionCookie } + #if os(macOS) enum ImportedSessionFetchOutcome { case succeeded(CursorStatusSnapshot) case tryNextBrowser @@ -1141,6 +1142,7 @@ public struct CursorStatusProbe: Sendable { return .failed(.networkError(error.localizedDescription)) } } + #endif private func fetchWithCookieHeader( _ cookieHeader: String, @@ -1404,7 +1406,7 @@ public struct CursorStatusProbe: Sendable { #if os(macOS) private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = cursorCookieImportOrder #else - private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = Browser.defaultImportOrder + private static let defaultBrowserCookieImportOrder: BrowserCookieImportOrder = [] #endif } diff --git a/Tests/CodexBarTests/CLIEntryTests.swift b/Tests/CodexBarTests/CLIEntryTests.swift index 0d22c410ae..5e51be2166 100644 --- a/Tests/CodexBarTests/CLIEntryTests.swift +++ b/Tests/CodexBarTests/CLIEntryTests.swift @@ -454,22 +454,5 @@ final class CLIEntryTests: XCTestCase { .auto, provider: .mimo, environment: ["MIMO_LOCAL_USAGE_PATH": directory.appendingPathComponent("missing.json").path])) - #if os(Linux) - XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( - .auto, - provider: .cursor, - settings: ProviderSettingsSnapshot.make( - cursor: .init(cookieSource: .auto, manualCookieHeader: nil)))) - XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( - .web, - provider: .cursor, - settings: ProviderSettingsSnapshot.make( - cursor: .init(cookieSource: .manual, manualCookieHeader: "WorkosCursorSessionToken=test")))) - XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( - .auto, - provider: .cursor, - settings: ProviderSettingsSnapshot.make( - cursor: .init(cookieSource: .off, manualCookieHeader: nil)))) - #endif } } diff --git a/Tests/CodexBarTests/CursorStatusProbeTests.swift b/Tests/CodexBarTests/CursorStatusProbeTests.swift index 819c90ac06..921a4ed8b9 100644 --- a/Tests/CodexBarTests/CursorStatusProbeTests.swift +++ b/Tests/CodexBarTests/CursorStatusProbeTests.swift @@ -976,24 +976,6 @@ extension CursorStatusProbeTests { #expect(session == CursorAppAuthSession(accessToken: "app-token")) } - #if os(Linux) - @Test - func `resolveDefaultDBPath honors XDG config home on Linux`() { - let path = CursorAppAuthStore.resolveDefaultDBPath( - home: "/home/test", - environment: ["XDG_CONFIG_HOME": "/custom/config"]) - #expect(path == "/custom/config/Cursor/User/globalStorage/state.vscdb") - } - - @Test - func `resolveDefaultDBPath falls back to dot config on Linux`() { - let path = CursorAppAuthStore.resolveDefaultDBPath( - home: "/home/test", - environment: [:]) - #expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb") - } - #endif - @Test func `fetch ignores user info failure when usage summary succeeds`() async throws { let testSession = CursorStatusProbeTestSession { request in diff --git a/TestsLinux/CursorLinuxTests.swift b/TestsLinux/CursorLinuxTests.swift new file mode 100644 index 0000000000..03f2a3066a --- /dev/null +++ b/TestsLinux/CursorLinuxTests.swift @@ -0,0 +1,52 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBarCLI + +@Suite +struct CursorLinuxTests { + @Test + func resolveDefaultDBPathHonorsXDGConfigHome() { + let path = CursorAppAuthStore.resolveDefaultDBPath( + home: "/home/test", + environment: ["XDG_CONFIG_HOME": "/custom/config"]) + #expect(path == "/custom/config/Cursor/User/globalStorage/state.vscdb") + } + + @Test + func resolveDefaultDBPathFallsBackToDotConfig() { + let path = CursorAppAuthStore.resolveDefaultDBPath( + home: "/home/test", + environment: [:]) + #expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb") + } + + @Test + func cursorAutoSourceDoesNotRequireMacOSWebSupport() { + #expect(!CodexBarCLI.sourceModeRequiresWebSupport( + .auto, + provider: .cursor, + settings: ProviderSettingsSnapshot.make( + cursor: .init(cookieSource: .auto, manualCookieHeader: nil)))) + } + + @Test + func cursorManualCookieDoesNotRequireMacOSWebSupport() { + #expect(!CodexBarCLI.sourceModeRequiresWebSupport( + .web, + provider: .cursor, + settings: ProviderSettingsSnapshot.make( + cursor: .init( + cookieSource: .manual, + manualCookieHeader: "WorkosCursorSessionToken=test")))) + } + + @Test + func cursorOffStillRequiresMacOSWebSupport() { + #expect(CodexBarCLI.sourceModeRequiresWebSupport( + .auto, + provider: .cursor, + settings: ProviderSettingsSnapshot.make( + cursor: .init(cookieSource: .off, manualCookieHeader: nil)))) + } +} \ No newline at end of file From 9d1f8482e5a73fd1af51ea5c04d9982770cd29b0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Jul 2026 22:20:08 -0700 Subject: [PATCH 3/4] fix: harden Linux Cursor auth discovery --- .../ProviderStorageFootprint.swift | 1 - .../Providers/Cursor/CursorStatusProbe.swift | 17 +++++++++--- TestsLinux/CursorLinuxTests.swift | 27 ++++++++++++------- docs/cursor.md | 6 ++--- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/Sources/CodexBarCore/ProviderStorageFootprint.swift b/Sources/CodexBarCore/ProviderStorageFootprint.swift index f47d09fd36..75feb7a366 100644 --- a/Sources/CodexBarCore/ProviderStorageFootprint.swift +++ b/Sources/CodexBarCore/ProviderStorageFootprint.swift @@ -332,7 +332,6 @@ public enum ProviderStoragePathCatalog { case .cursor: [ homePath("Library/Application Support/Cursor"), - homePath(".config/Cursor"), homePath("Library/Application Support/Caches/cursor-updater"), homePath(".cursor"), homePath("Library/Caches/Cursor"), diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift index 6a952cdf98..bff2b00cb8 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift @@ -5,6 +5,8 @@ import FoundationNetworking import SweetCookieKit #if canImport(SQLite3) import SQLite3 +#elseif canImport(CSQLite3) +import CSQLite3 #endif #if os(macOS) || os(Linux) @@ -15,6 +17,7 @@ private let cursorCookieImportOrder: BrowserCookieImportOrder = #endif #if os(macOS) + // MARK: - Cursor Cookie Importer /// Imports Cursor session cookies from browser cookies. @@ -432,8 +435,12 @@ struct CursorAppAuthStore: CursorAppAuthSessionProviding { #elseif os(Linux) let configHome = environment[CodexBarConfigStore.xdgConfigHomeEnvironmentKey]? .trimmingCharacters(in: .whitespacesAndNewlines) - let base: String = if let configHome, !configHome.isEmpty { - (configHome as NSString).expandingTildeInPath + let expandedConfigHome = configHome.map { ($0 as NSString).expandingTildeInPath } + let base: String = if let expandedConfigHome, + !expandedConfigHome.isEmpty, + (expandedConfigHome as NSString).isAbsolutePath + { + expandedConfigHome } else { "\(home)/.config" } @@ -735,7 +742,8 @@ public enum CursorStatusProbeError: LocalizedError, Sendable { #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 session cookie in CodexBar settings." + "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)" @@ -747,7 +755,8 @@ public enum CursorStatusProbeError: LocalizedError, Sendable { + "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, paste a Cookie header from cursor.com in settings, or log in to cursor.com in your browser." + "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 } } diff --git a/TestsLinux/CursorLinuxTests.swift b/TestsLinux/CursorLinuxTests.swift index 03f2a3066a..493121a8ad 100644 --- a/TestsLinux/CursorLinuxTests.swift +++ b/TestsLinux/CursorLinuxTests.swift @@ -1,12 +1,12 @@ -import CodexBarCore +#if os(Linux) import Foundation import Testing @testable import CodexBarCLI +@testable import CodexBarCore -@Suite struct CursorLinuxTests { @Test - func resolveDefaultDBPathHonorsXDGConfigHome() { + func `Cursor database path honors absolute XDG config home`() { let path = CursorAppAuthStore.resolveDefaultDBPath( home: "/home/test", environment: ["XDG_CONFIG_HOME": "/custom/config"]) @@ -14,7 +14,7 @@ struct CursorLinuxTests { } @Test - func resolveDefaultDBPathFallsBackToDotConfig() { + func `Cursor database path falls back to dot config`() { let path = CursorAppAuthStore.resolveDefaultDBPath( home: "/home/test", environment: [:]) @@ -22,7 +22,15 @@ struct CursorLinuxTests { } @Test - func cursorAutoSourceDoesNotRequireMacOSWebSupport() { + 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, @@ -31,7 +39,7 @@ struct CursorLinuxTests { } @Test - func cursorManualCookieDoesNotRequireMacOSWebSupport() { + func `Cursor manual cookie does not require macOS web support`() { #expect(!CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .cursor, @@ -42,11 +50,12 @@ struct CursorLinuxTests { } @Test - func cursorOffStillRequiresMacOSWebSupport() { + func `disabled Cursor web source still requires macOS web support`() { #expect(CodexBarCLI.sourceModeRequiresWebSupport( - .auto, + .web, provider: .cursor, settings: ProviderSettingsSnapshot.make( cursor: .init(cookieSource: .off, manualCookieHeader: nil)))) } -} \ No newline at end of file +} +#endif diff --git a/docs/cursor.md b/docs/cursor.md index 2200298ab9..24be1e7b59 100644 --- a/docs/cursor.md +++ b/docs/cursor.md @@ -59,11 +59,11 @@ Manual option: ## Linux CLI - `codexbar usage --provider cursor` reads the signed-in Cursor app's access token from the Linux global state DB and reuses the same `cursor.com` usage endpoints as macOS. - Automatic browser cookie import and the in-app WebKit login flow remain macOS-only. -- Manual cookie headers from `~/.codexbar/config.json` work on Linux. +- Manual cookie headers from `~/.config/codexbar/config.json` (or legacy `~/.codexbar/config.json`) work on Linux. ## Local storage footprint -When **Settings → Advanced → Track provider local storage** is enabled, CodexBar measures: -- `~/Library/Application Support/Cursor` (macOS) or `~/.config/Cursor` (Linux) +When **Settings → Advanced → Track provider local storage** is enabled on macOS, CodexBar measures: +- `~/Library/Application Support/Cursor` - `~/Library/Application Support/Caches/cursor-updater` - `~/.cursor` - `~/Library/Caches/Cursor` From bdc9ae94a68dd35df7f2302ea5f65b70c93fc9d6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 5 Jul 2026 10:27:05 +0100 Subject: [PATCH 4/4] fix: align Linux Cursor web source --- CHANGELOG.md | 1 + .../Providers/Cursor/CursorProviderDescriptor.swift | 2 +- TestsLinux/CursorLinuxTests.swift | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9063f9e7da..280ee12b94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Codex dashboard: show calendar-correct raw Today and 30-day credit totals without converting credits to billed dollars. Thanks @avenoxai! +- Cursor: read the signed-in app token on Linux, with explicit manual-cookie web-source support and XDG config paths. Thanks @DonnieFi! - Devin: show remaining extra-usage balance in menus, CLI, and widgets while respecting optional-usage visibility. Thanks @FNDEVVE! - Widgets: make Mistral available in provider selection and switching. Thanks @joeVenner! diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift index c6f1ff35fa..983cae2dc9 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift @@ -33,7 +33,7 @@ public enum CursorProviderDescriptor { supportsTokenCost: false, noDataMessage: { "Cursor cost summary is not supported." }), fetchPlan: ProviderFetchPlan( - sourceModes: [.auto, .cli], + sourceModes: [.auto, .cli, .web], pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [CursorStatusFetchStrategy()] })), cli: ProviderCLIConfig( name: "cursor", diff --git a/TestsLinux/CursorLinuxTests.swift b/TestsLinux/CursorLinuxTests.swift index 493121a8ad..f78acb1ba5 100644 --- a/TestsLinux/CursorLinuxTests.swift +++ b/TestsLinux/CursorLinuxTests.swift @@ -38,6 +38,11 @@ struct CursorLinuxTests { 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(