Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBarCLI/CLIUsageCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +610 to +614

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add Cursor web source before unblocking Linux web mode

For Linux Cursor manual-cookie runs with --source web or source: "web", this exception now skips the macOS-only web-support error, but CursorProviderDescriptor still declares only .auto and .cli (Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift:36) and CLIHelpers.fetchProviderUsage rejects unsupported source modes before the strategy runs (Sources/CodexBarCLI/CLIHelpers.swift:191). The result is still Source 'web' is not supported for cursor instead of using the configured manual cookie; either add .web to Cursor's source modes or limit this Linux exception to modes the descriptor actually accepts.

Useful? React with 👍 / 👎.

}
#endif
if provider == .sakana,
sourceMode == .auto || sourceMode == .web,
environment.map({ SakanaSettingsReader.cookieHeader(environment: $0) != nil }) == true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
68 changes: 61 additions & 7 deletions Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep CursorCookieImporter types available on Linux

When this file is compiled on Linux, the new outer #if os(macOS) || os(Linux) includes CursorStatusProbe, but this added macOS-only guard removes CursorCookieImporter; the unguarded helper signatures below still reference CursorCookieImporter.SessionInfo (scanBrowsers, scanImportedSessions, and fetchIfSessionAccepted), so the Linux CLI target cannot compile. Either provide a Linux-compatible SessionInfo/stub or wrap those helpers in the same macOS-only condition.

Useful? React with 👍 / 👎.


// MARK: - Cursor Cookie Importer

Expand Down Expand Up @@ -186,6 +192,7 @@ public enum CursorCookieImporter {
}
}
}
#endif

// MARK: - Cursor API Models

Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -860,7 +904,7 @@ public struct CursorStatusProbe: Sendable {
baseURL: baseURL,
timeout: timeout,
browserDetection: browserDetection,
browserCookieImportOrder: cursorCookieImportOrder,
browserCookieImportOrder: Self.defaultBrowserCookieImportOrder,
urlSession: urlSession,
appAuthStore: CursorAppAuthStore())
}
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1022,6 +1068,7 @@ public struct CursorStatusProbe: Sendable {
throw CursorStatusProbeError.noSessionCookie
}

#if os(macOS)
enum ImportedSessionFetchOutcome {
case succeeded(CursorStatusSnapshot)
case tryNextBrowser
Expand Down Expand Up @@ -1104,6 +1151,7 @@ public struct CursorStatusProbe: Sendable {
return .failed(.networkError(error.localizedDescription))
}
}
#endif

private func fetchWithCookieHeader(
_ cookieHeader: String,
Expand Down Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions TestsLinux/CursorLinuxTests.swift
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
12 changes: 10 additions & 2 deletions docs/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -53,8 +56,13 @@ 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 `~/.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:
When **Settings → Advanced → Track provider local storage** is enabled on macOS, CodexBar measures:
- `~/Library/Application Support/Cursor`
- `~/Library/Application Support/Caches/cursor-updater`
- `~/.cursor`
Expand Down