Skip to content

feat: add Cursor provider for tracking Cursor AI usage quotas#119

Closed
alxDisplayr wants to merge 2 commits intotddworks:mainfrom
alxDisplayr:feature/add-cursor-provider
Closed

feat: add Cursor provider for tracking Cursor AI usage quotas#119
alxDisplayr wants to merge 2 commits intotddworks:mainfrom
alxDisplayr:feature/add-cursor-provider

Conversation

@alxDisplayr
Copy link
Copy Markdown

@alxDisplayr alxDisplayr commented Feb 15, 2026

Summary

  • Adds a new Cursor provider that monitors Cursor IDE subscription usage (included requests and on-demand spending) directly from the ClaudeBar menu bar
  • Follows the established provider pattern (Domain model + Infrastructure probe + Visual identity)
  • The probe auto-detects Cursor's auth token from its local SQLite database — no manual configuration needed

How It Works

  1. Reads the access token from Cursor's local SQLite database (~/Library/Application Support/Cursor/User/globalStorage/state.vscdb)
  2. Decodes the JWT payload to extract the user ID (sub claim)
  3. Calls https://cursor.com/api/usage-summary with cookie authentication (WorkosCursorSessionToken={userId}::{accessToken})
  4. Parses plan usage (monthly included requests) and on-demand usage into UsageQuota items

Features

  • Auto-detection: No manual token setup — reads directly from Cursor's local database
  • Plan support: Pro, Business, and Free plans with automatic tier badge detection
  • Dual quota tracking: Both included requests (e.g., 123/500) and on-demand spending
  • Billing cycle awareness: Parses billingCycleEnd for reset time display
  • Unlimited plan support: Properly handles isUnlimited: true responses

Files Changed

Layer File Purpose
Domain Sources/Domain/Provider/Cursor/CursorProvider.swift Provider class (follows Kiro pattern)
Infrastructure Sources/Infrastructure/Cursor/CursorUsageProbe.swift HTTP API probe with SQLite token extraction
App Sources/App/ClaudeBarApp.swift Provider registration
App Sources/App/Views/ProviderVisualIdentity.swift Visual identity + all 5 lookup switches
App Sources/App/Resources/Assets.xcassets/CursorIcon.imageset/ Icon assets (SVG)
Tests Tests/InfrastructureTests/Cursor/CursorUsageProbeParsingTests.swift 15 parsing tests

Test Plan

  • All 15 parsing tests pass (usage summary parsing + JWT extraction)
  • Project builds successfully with xcodebuild build
  • Manual testing with active Cursor installation to verify end-to-end flow
  • Verify menu bar icon renders correctly in light and dark mode

Notes

  • The Cursor usage API (cursor.com/api/usage-summary) is the same endpoint used by community extensions like cursor-stats and cursor-usage-stats
  • Token extraction uses the sqlite3 CLI (available on all macOS systems) rather than a Swift SQLite library, keeping dependencies unchanged
  • The probe is designed to gracefully degrade: if Cursor is not installed or the user isn't logged in, isAvailable() returns false

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Cursor as a supported AI provider with usage monitoring, enable/disable settings, and quick access to its dashboard
    • Added themed visual identity and app icon for the Cursor provider
  • Tests

    • Added comprehensive tests for Cursor usage data parsing and JWT/user-id extraction logic

Adds a new Cursor provider that monitors Cursor IDE subscription usage
(included requests and on-demand spending) directly from the menu bar.

The probe works by:
1. Reading the auth token from Cursor's local SQLite database
   (~/.../Cursor/User/globalStorage/state.vscdb)
2. Decoding the JWT to extract the user ID
3. Calling https://cursor.com/api/usage-summary with cookie auth
4. Parsing plan usage (monthly included requests) and on-demand usage

Supports Pro, Business, and Free plans with automatic tier detection.

Files added:
- Domain: CursorProvider (follows Kiro/AmpCode pattern)
- Infrastructure: CursorUsageProbe (HTTP API + SQLite token extraction)
- Tests: 15 parsing tests covering all response formats
- App: Visual identity, icon assets, provider registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

Adds support for the Cursor AI provider: a new observable CursorProvider, a CursorUsageProbe that reads local DB and calls Cursor's usage API, visual identity and asset, registration in app providers list, and comprehensive parsing/unit tests.

Changes

Cohort / File(s) Summary
Provider Implementation
Sources/Domain/Provider/Cursor/CursorProvider.swift
New observable AI provider managing Cursor state, persisted enabled flag, syncing, snapshot, lastError, availability check, and refresh workflow.
Usage Probe
Sources/Infrastructure/Cursor/CursorUsageProbe.swift
New probe that reads Cursor auth token from local SQLite, extracts user ID from JWT, calls Cursor usage-summary API, parses JSON into UsageSnapshot/quotas, and exposes isAvailable/probe methods.
Parsing Tests
Tests/InfrastructureTests/Cursor/CursorUsageProbeParsingTests.swift
Extensive unit tests covering parseUsageSummary and extractUserIdFromJWT across many scenarios (plans, on-demand, unlimited, errors, JWT edge cases, resets parsing).
Visual Identity & Assets
Sources/App/Views/ProviderVisualIdentity.swift, Sources/App/Resources/Assets.xcassets/CursorIcon.imageset/Contents.json
Adds Cursor visual identity (symbol, asset name, theme color/gradient) and CursorIcon asset catalog entry.
App Integration
Sources/App/ClaudeBarApp.swift
Registers CursorProvider in the app's AI providers initialization list.

Sequence Diagram

sequenceDiagram
    participant User
    participant CursorProvider
    participant CursorUsageProbe
    participant SQLiteDB as Cursor<br/>SQLite DB
    participant CursorAPI as Cursor<br/>Usage API

    User->>CursorProvider: refresh()
    activate CursorProvider
    CursorProvider->>CursorProvider: isSyncing = true
    CursorProvider->>CursorUsageProbe: probe()
    activate CursorUsageProbe
    
    CursorUsageProbe->>SQLiteDB: Read auth token
    activate SQLiteDB
    SQLiteDB-->>CursorUsageProbe: JWT token
    deactivate SQLiteDB
    
    CursorUsageProbe->>CursorUsageProbe: extractUserIdFromJWT(token)
    CursorUsageProbe->>CursorAPI: GET /v1/usage/summary (Cookie header)
    activate CursorAPI
    CursorAPI-->>CursorUsageProbe: Usage JSON response
    deactivate CursorAPI
    
    CursorUsageProbe->>CursorUsageProbe: parseUsageSummary(response)
    CursorUsageProbe-->>CursorProvider: UsageSnapshot
    deactivate CursorUsageProbe
    
    CursorProvider->>CursorProvider: snapshot = UsageSnapshot\nisSyncing = false
    CursorProvider-->>User: Updated snapshot
    deactivate CursorProvider
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰 I nibble tokens from SQLite roots,
I pull the JWT, trace its routes,
I hop to the API, fetch what's due,
Pack snapshots and quotas — fresh and true,
A tiny cursor rabbit, syncing for you. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Cursor provider for tracking Cursor AI usage quotas' accurately describes the main change: introducing a new Cursor provider to monitor Cursor IDE subscription usage, which aligns with all the file additions (CursorProvider, CursorUsageProbe, visual identity, tests, and app integration).
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@Sources/App/Resources/Assets.xcassets/CursorIcon.imageset/Contents.json`:
- Around line 1-15: The CursorIcon imageset currently points to CursorIcon.svg
instead of the project's PNG runtime convention; replace the vector reference in
CursorIcon.imageset/Contents.json by generating PNG variants from the SVG source
(e.g., CursorIcon_1x.png, CursorIcon_2x.png, CursorIcon_3x.png or sizes like
CursorIcon_64.png, CursorIcon_128.png, CursorIcon_192.png) and update the
imageset entries to reference those PNG filenames, ensuring
"preserves-vector-representation" is removed or set appropriately so the asset
catalog uses the PNGs at runtime.

In `@Sources/Infrastructure/Cursor/CursorUsageProbe.swift`:
- Around line 222-240: The planUsage block can append a Monthly quota even when
isUnlimited is true, causing duplicate Monthly quotas; update the logic in the
CursorUsageProbe parsing so you check the isUnlimited flag before processing
json["planUsage"] and skip the planUsage quota when isUnlimited is true (i.e.,
only append the UsageQuota with quotaType .timeLimit("Monthly") from the
planUsage branch when isUnlimited is false), ensuring unique quotas for
UsageQuota creation (percentRemaining, providerId "cursor", resetsAt,
resetText).
- Around line 191-289: In parseUsageSummary, percentRemaining is computed in the
planUsage and onDemand blocks (currently Double(limit - used) / Double(limit) *
100) and can exceed 100 if used is negative; update both calculations to clamp
the value between 0 and 100 (e.g., percentRemaining = min(100, max(0,
percentRemaining))) before creating the UsageQuota instances so percentRemaining
is always within [0,100]; ensure you apply this change in the planUsage block
and the onDemand block where percentRemaining is set.
🧹 Nitpick comments (4)
Sources/Infrastructure/Cursor/CursorUsageProbe.swift (3)

89-120: waitUntilExit() blocks the cooperative thread pool.

Process.waitUntilExit() is a synchronous blocking call. When called from an async function, it holds a cooperative thread pool thread hostage, potentially starving other async tasks. For a single short-lived sqlite3 query with a local DB this is unlikely to cause visible issues, but it's still an anti-pattern in structured concurrency.

Consider wrapping the blocking work in a detached task or dispatching to a non-cooperative queue:

♻️ Suggested approach
-    private func readAccessToken(from dbPath: String) throws -> String {
+    private func readAccessToken(from dbPath: String) async throws -> String {
+      try await withCheckedThrowingContinuation { continuation in
+        DispatchQueue.global().async {
+          do {
             let process = Process()
             process.executableURL = URL(fileURLWithPath: "/usr/bin/sqlite3")
             process.arguments = [dbPath, "SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'"]

             let pipe = Pipe()
             process.standardOutput = pipe
             process.standardError = Pipe()

-        do {
             try process.run()
             process.waitUntilExit()
-        } catch {
-            AppLog.probes.error("Cursor: Failed to run sqlite3 - \(error.localizedDescription)")
-            throw ProbeError.executionFailed("Failed to read Cursor database: \(error.localizedDescription)")
-        }

             guard process.terminationStatus == 0 else {
-            AppLog.probes.error("Cursor: sqlite3 exited with status \(process.terminationStatus)")
-            throw ProbeError.executionFailed("sqlite3 exited with status \(process.terminationStatus)")
+              continuation.resume(throwing: ProbeError.executionFailed("sqlite3 exited with status \(process.terminationStatus)"))
+              return
             }

             let data = pipe.fileHandleForReading.readDataToEndOfFile()
             let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""

             guard !token.isEmpty else {
-            AppLog.probes.error("Cursor: No access token found in database (not logged in?)")
-            throw ProbeError.authenticationRequired
+              continuation.resume(throwing: ProbeError.authenticationRequired)
+              return
             }

-        return token
+            continuation.resume(returning: token)
+          } catch {
+            continuation.resume(throwing: ProbeError.executionFailed("Failed to read Cursor database: \(error.localizedDescription)"))
+          }
+        }
+      }
     }

159-163: Content-Type header is inappropriate for a GET request.

Line 162 sets Content-Type: application/json, but this is a GET request with no body. Use Accept instead to indicate the desired response format.

♻️ Proposed fix
         request.httpMethod = "GET"
         request.setValue(cookie, forHTTPHeaderField: "Cookie")
-        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+        request.setValue("application/json", forHTTPHeaderField: "Accept")

66-68: File path containing the user's home directory is persisted to file log at error level.

dbPath includes the home directory path (e.g., /Users/janedoe/...). At .error level this is written to the file log. While likely acceptable for a local desktop app, be aware this could surface the macOS username in exported diagnostic logs.

As per coding guidelines: "Do not expose secrets or sensitive data in file logs; manually redact sensitive information before logging."

Tests/InfrastructureTests/Cursor/CursorUsageProbeParsingTests.swift (1)

199-216: Consider adding a test for planUsage.enabled: true combined with isUnlimited: true.

This would validate behavior when both flags are set simultaneously—currently the parser would emit two "Monthly" quotas, which relates to the potential duplicate quota issue flagged in CursorUsageProbe.swift.

Comment on lines +1 to +15
{
"images" : [
{
"filename" : "CursorIcon.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all Contents.json files in Assets.xcassets and check what image formats they reference
find . -path '*/Assets.xcassets/*.imageset/Contents.json' -type f 2>/dev/null | head -20 | while read file; do
  echo "=== $file ==="
  grep '"filename"' "$file" | head -3
done

echo -e "\n--- Summary of image formats used in imagesets ---"
find . -path '*/Assets.xcassets/*.imageset/Contents.json' -type f 2>/dev/null | xargs grep -h '"filename"' | sed 's/.*"\([^"]*\)".*/\1/' | sed 's/.*\.//' | sort | uniq -c | sort -rn

Repository: tddworks/ClaudeBar

Length of output: 2222


🏁 Script executed:

#!/bin/bash
# Search for CursorIcon and AmpCodeIcon files in various formats
echo "=== CursorIcon files ==="
find . -name '*CursorIcon*' -type f 2>/dev/null | sort

echo -e "\n=== AmpCodeIcon files ==="
find . -name '*AmpCodeIcon*' -type f 2>/dev/null | sort

echo -e "\n=== Check if CursorIcon.svg has preserves-vector-representation ==="
grep -r 'preserves-vector-representation' Sources/App/Resources/Assets.xcassets/*.imageset/Contents.json 2>/dev/null | grep -i cursor

echo -e "\n=== Check AmpCodeIcon Contents.json ==="
cat ./Sources/App/Resources/Assets.xcassets/AmpCodeIcon.imageset/Contents.json

Repository: tddworks/ClaudeBar

Length of output: 761


CursorIcon asset deviates from project PNG convention.

The imageset references CursorIcon.svg directly instead of PNG variants, inconsistent with the project pattern where 28 of 30 assets use PNG at runtime (at 1×/2×/3× scales). While the SVG source file is properly maintained at Sources/App/Resources/CursorIcon.svg for regeneration, the runtime asset should follow the established convention by generating and using PNG variants like other icons (e.g., CursorIcon_64.png, CursorIcon_128.png, CursorIcon_192.png or similar). Consider converting the SVG source to PNG variants for consistency.

🤖 Prompt for AI Agents
In `@Sources/App/Resources/Assets.xcassets/CursorIcon.imageset/Contents.json`
around lines 1 - 15, The CursorIcon imageset currently points to CursorIcon.svg
instead of the project's PNG runtime convention; replace the vector reference in
CursorIcon.imageset/Contents.json by generating PNG variants from the SVG source
(e.g., CursorIcon_1x.png, CursorIcon_2x.png, CursorIcon_3x.png or sizes like
CursorIcon_64.png, CursorIcon_128.png, CursorIcon_192.png) and update the
imageset entries to reference those PNG filenames, ensuring
"preserves-vector-representation" is removed or set appropriately so the asset
catalog uses the PNGs at runtime.

Comment on lines +191 to +289
public static func parseUsageSummary(_ data: Data) throws -> UsageSnapshot {
let json: [String: Any]
do {
guard let parsed = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw ProbeError.parseFailed("Response is not a JSON object")
}
json = parsed
} catch let error as ProbeError {
throw error
} catch {
throw ProbeError.parseFailed("Invalid JSON: \(error.localizedDescription)")
}

var quotas: [UsageQuota] = []

let membershipType = json["membershipType"] as? String ?? "unknown"

// Parse billing cycle dates for reset time
var resetsAt: Date?
if let cycleEnd = json["billingCycleEnd"] as? String {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
if let date = formatter.date(from: cycleEnd) {
resetsAt = date
} else {
// Try without fractional seconds
formatter.formatOptions = [.withInternetDateTime]
resetsAt = formatter.date(from: cycleEnd)
}
}

// Parse plan usage (included requests)
if let planUsage = json["planUsage"] as? [String: Any],
let enabled = planUsage["enabled"] as? Bool, enabled {
let used = (planUsage["used"] as? Int) ?? (planUsage["used"] as? Double).map { Int($0) } ?? 0
let limit = (planUsage["limit"] as? Int) ?? (planUsage["limit"] as? Double).map { Int($0) } ?? 0

if limit > 0 {
let percentRemaining = Double(limit - used) / Double(limit) * 100
let requestsText = "\(used)/\(limit) requests"

quotas.append(UsageQuota(
percentRemaining: max(0, percentRemaining),
quotaType: .timeLimit("Monthly"),
providerId: "cursor",
resetsAt: resetsAt,
resetText: requestsText
))
}
}

// Parse on-demand usage (usage-based pricing)
if let onDemand = json["onDemandUsage"] as? [String: Any],
let enabled = onDemand["enabled"] as? Bool, enabled {
let used = (onDemand["used"] as? Int) ?? (onDemand["used"] as? Double).map { Int($0) } ?? 0
let limit = (onDemand["limit"] as? Int) ?? (onDemand["limit"] as? Double).map { Int($0) } ?? 0

if limit > 0 {
let percentRemaining = Double(limit - used) / Double(limit) * 100
quotas.append(UsageQuota(
percentRemaining: max(0, percentRemaining),
quotaType: .timeLimit("On-Demand"),
providerId: "cursor",
resetsAt: resetsAt,
resetText: "\(used)/\(limit) on-demand"
))
}
}

// Check for unlimited plans
if let isUnlimited = json["isUnlimited"] as? Bool, isUnlimited {
quotas.append(UsageQuota(
percentRemaining: 100,
quotaType: .timeLimit("Monthly"),
providerId: "cursor",
resetText: "Unlimited"
))
}

// If no quotas found, the user might be on a free plan with no data
guard !quotas.isEmpty else {
throw ProbeError.parseFailed("No usage data found in Cursor response")
}

// Determine account tier from membership type
let tier: AccountTier? = switch membershipType.lowercased() {
case "pro": .custom("PRO")
case "business": .custom("BUSINESS")
case "free": .custom("FREE")
default: membershipType.isEmpty ? nil : .custom(membershipType.uppercased())
}

return UsageSnapshot(
providerId: "cursor",
quotas: quotas,
capturedAt: Date(),
accountTier: tier
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

percentRemaining can exceed 100 when used is negative.

Line 229: Double(limit - used) / Double(limit) * 100 — you clamp the lower bound to 0 via max(0, ...) on line 233, but there's no upper-bound clamp. If the API ever returns a negative used value (e.g., credits/adjustments), percentRemaining could exceed 100. Consider min(100, max(0, percentRemaining)).

🤖 Prompt for AI Agents
In `@Sources/Infrastructure/Cursor/CursorUsageProbe.swift` around lines 191 - 289,
In parseUsageSummary, percentRemaining is computed in the planUsage and onDemand
blocks (currently Double(limit - used) / Double(limit) * 100) and can exceed 100
if used is negative; update both calculations to clamp the value between 0 and
100 (e.g., percentRemaining = min(100, max(0, percentRemaining))) before
creating the UsageQuota instances so percentRemaining is always within [0,100];
ensure you apply this change in the planUsage block and the onDemand block where
percentRemaining is set.

Comment on lines +222 to +240
// Parse plan usage (included requests)
if let planUsage = json["planUsage"] as? [String: Any],
let enabled = planUsage["enabled"] as? Bool, enabled {
let used = (planUsage["used"] as? Int) ?? (planUsage["used"] as? Double).map { Int($0) } ?? 0
let limit = (planUsage["limit"] as? Int) ?? (planUsage["limit"] as? Double).map { Int($0) } ?? 0

if limit > 0 {
let percentRemaining = Double(limit - used) / Double(limit) * 100
let requestsText = "\(used)/\(limit) requests"

quotas.append(UsageQuota(
percentRemaining: max(0, percentRemaining),
quotaType: .timeLimit("Monthly"),
providerId: "cursor",
resetsAt: resetsAt,
resetText: requestsText
))
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Possible duplicate "Monthly" quota when planUsage.enabled and isUnlimited are both true.

If the API returns planUsage.enabled: true with a valid limit and isUnlimited: true, both blocks will append a quota with .timeLimit("Monthly"). This would produce two Monthly quotas in the snapshot. Consider making these mutually exclusive—check isUnlimited first and skip planUsage when unlimited.

♻️ Suggested guard
+        let isUnlimited = (json["isUnlimited"] as? Bool) ?? false
+
         // Parse plan usage (included requests)
-        if let planUsage = json["planUsage"] as? [String: Any],
+        if !isUnlimited,
+           let planUsage = json["planUsage"] as? [String: Any],
            let enabled = planUsage["enabled"] as? Bool, enabled {
             ...
         }

         // Check for unlimited plans
-        if let isUnlimited = json["isUnlimited"] as? Bool, isUnlimited {
+        if isUnlimited {

Also applies to: 260-268

🤖 Prompt for AI Agents
In `@Sources/Infrastructure/Cursor/CursorUsageProbe.swift` around lines 222 - 240,
The planUsage block can append a Monthly quota even when isUnlimited is true,
causing duplicate Monthly quotas; update the logic in the CursorUsageProbe
parsing so you check the isUnlimited flag before processing json["planUsage"]
and skip the planUsage quota when isUnlimited is true (i.e., only append the
UsageQuota with quotaType .timeLimit("Monthly") from the planUsage branch when
isUnlimited is false), ensuring unique quotas for UsageQuota creation
(percentRemaining, providerId "cursor", resetsAt, resetText).

The actual cursor.com/api/usage-summary response nests usage data under
`individualUsage.plan` and `individualUsage.onDemand`, not top-level
`planUsage` and `onDemandUsage` as initially assumed.

Also adds "ultra" tier detection and a test using the real API response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Sources/Infrastructure/Cursor/CursorUsageProbe.swift`:
- Line 216: The code sets let membershipType = json["membershipType"] as? String
?? "unknown", which makes the later membershipType.isEmpty branch unreachable
and always falls back to .custom("UNKNOWN"); fix by either defaulting the parsed
value to an empty string (let membershipType = json["membershipType"] as? String
?? "") so the .isEmpty check can work as intended, or keep the "unknown" default
but update the mapping logic that produces .custom("UNKNOWN") to explicitly
treat "unknown" (case-insensitively) as the empty/missing case; update the code
that maps membershipType -> membershipTier (the block producing
.custom("UNKNOWN")) accordingly.
🧹 Nitpick comments (2)
Sources/Infrastructure/Cursor/CursorUsageProbe.swift (1)

97-112: Blocking Process call on the cooperative thread pool.

readAccessToken runs a synchronous Process (with waitUntilExit()) and is called from probe(), which is async. This blocks one of Swift's limited cooperative thread-pool threads. If several providers probe concurrently, this could starve the pool.

Wrap the blocking work so it doesn't occupy a cooperative thread:

♻️ Suggested fix
-    private func readAccessToken(from dbPath: String) throws -> String {
+    private func readAccessToken(from dbPath: String) async throws -> String {
+      try await withCheckedThrowingContinuation { continuation in
+        DispatchQueue.global(qos: .userInitiated).async {
+            do {
+                let token = try self._readAccessTokenSync(from: dbPath)
+                continuation.resume(returning: token)
+            } catch {
+                continuation.resume(throwing: error)
+            }
+        }
+      }
+    }
+
+    private func _readAccessTokenSync(from dbPath: String) throws -> String {
         let process = Process()
         ...
     }
Tests/InfrastructureTests/Cursor/CursorUsageProbeParsingTests.swift (1)

180-199: Missing test for isUnlimited: true with planUsage.enabled: true (duplicate quota edge case).

The unlimited test uses plan.enabled: false, which sidesteps the duplicate "Monthly" quota bug flagged in the probe. Add a test where both isUnlimited: true and plan.enabled: true with a valid limit to document the expected behavior and guard against regressions.

🧪 Suggested test
`@Test`
func `parse unlimited with enabled plan does not duplicate monthly quota`() throws {
    let json = """
    {
        "membershipType": "business",
        "isUnlimited": true,
        "individualUsage": {
            "plan": { "enabled": true, "used": 100, "limit": 500, "remaining": 400 },
            "onDemand": { "enabled": false, "used": 0, "limit": null, "remaining": null }
        }
    }
    """.data(using: .utf8)!

    let snapshot = try CursorUsageProbe.parseUsageSummary(json)
    // Should only have ONE Monthly quota, not two
    let monthlyQuotas = snapshot.quotas.filter { $0.quotaType == .timeLimit("Monthly") }
    `#expect`(monthlyQuotas.count == 1)
    `#expect`(monthlyQuotas[0].resetText == "Unlimited")
}


var quotas: [UsageQuota] = []

let membershipType = json["membershipType"] as? String ?? "unknown"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Dead branch: membershipType.isEmpty is unreachable.

Line 216 defaults membershipType to "unknown" when the key is missing, so the .isEmpty check on Line 294 can never be true — it will always produce .custom("UNKNOWN") for unrecognized tiers. If returning nil for missing membership is intended, default to "" instead of "unknown".

♻️ Suggested fix
-        let membershipType = json["membershipType"] as? String ?? "unknown"
+        let membershipType = json["membershipType"] as? String ?? ""

Also applies to: 289-295

🤖 Prompt for AI Agents
In `@Sources/Infrastructure/Cursor/CursorUsageProbe.swift` at line 216, The code
sets let membershipType = json["membershipType"] as? String ?? "unknown", which
makes the later membershipType.isEmpty branch unreachable and always falls back
to .custom("UNKNOWN"); fix by either defaulting the parsed value to an empty
string (let membershipType = json["membershipType"] as? String ?? "") so the
.isEmpty check can work as intended, or keep the "unknown" default but update
the mapping logic that produces .custom("UNKNOWN") to explicitly treat "unknown"
(case-insensitively) as the empty/missing case; update the code that maps
membershipType -> membershipTier (the block producing .custom("UNKNOWN"))
accordingly.

@alxDisplayr
Copy link
Copy Markdown
Author

Superseded by #120 — recreated from the correct GitHub account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant