Skip to content
Open
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
18 changes: 14 additions & 4 deletions Sources/CodexBarCore/Providers/Cursor/CursorStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public struct CursorUsageSummary: Codable, Sendable {

public struct CursorIndividualUsage: Codable, Sendable {
public let plan: CursorPlanUsage?
public let overall: CursorPlanUsage?
public let onDemand: CursorOnDemandUsage?
}

Expand Down Expand Up @@ -128,6 +129,7 @@ public struct CursorOnDemandUsage: Codable, Sendable {

public struct CursorTeamUsage: Codable, Sendable {
public let onDemand: CursorOnDemandUsage?
public let pooled: CursorPlanUsage?
}

// MARK: - Cursor Usage API Models (Legacy Request-Based Plans)
Expand Down Expand Up @@ -689,15 +691,23 @@ public struct CursorStatusProbe: Sendable {
return formatter.date(from: dateString) ?? ISO8601DateFormatter().date(from: dateString)
}

// Cursor uses different keys across account types:
// - individualUsage.plan: classic personal plans
// - individualUsage.overall: enterprise/team accounts
// - teamUsage.pooled: fallback pooled quota for some org plans
let primaryUsage = summary.individualUsage?.plan
?? summary.individualUsage?.overall
?? summary.teamUsage?.pooled

// Convert cents to USD (plan percent derives from raw values to avoid percent unit mismatches).
// Use plan.limit directly - breakdown.total represents total *used* credits, not the limit.
let planUsedRaw = Double(summary.individualUsage?.plan?.used ?? 0)
let planLimitRaw = Double(summary.individualUsage?.plan?.limit ?? 0)
// Use limit directly - breakdown.total represents usage, not the plan cap.
let planUsedRaw = Double(primaryUsage?.used ?? 0)
let planLimitRaw = Double(primaryUsage?.limit ?? 0)
let planUsed = planUsedRaw / 100.0
let planLimit = planLimitRaw / 100.0
let planPercentUsed: Double = if planLimitRaw > 0 {
(planUsedRaw / planLimitRaw) * 100
} else if let totalPercentUsed = summary.individualUsage?.plan?.totalPercentUsed {
} else if let totalPercentUsed = primaryUsage?.totalPercentUsed {
totalPercentUsed <= 1 ? totalPercentUsed * 100 : totalPercentUsed
} else {
0
Expand Down
82 changes: 82 additions & 0 deletions Tests/CodexBarTests/CursorStatusProbeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,43 @@ struct CursorStatusProbeTests {
#expect(summary.teamUsage?.onDemand?.limit == 50000)
}

@Test
func `parses team overall usage summary`() throws {
let json = """
{
"billingCycleStart": "2026-03-01T00:00:00.000Z",
"billingCycleEnd": "2026-04-01T00:00:00.000Z",
"membershipType": "enterprise",
"limitType": "team",
"individualUsage": {
"overall": {
"enabled": true,
"used": 10985,
"limit": 15000,
"remaining": 4015
}
},
"teamUsage": {
"pooled": {
"enabled": true,
"used": 4749435,
"limit": 10693200,
"remaining": 5943765
}
}
}
"""
let data = try #require(json.data(using: .utf8))
let summary = try JSONDecoder().decode(CursorUsageSummary.self, from: data)

#expect(summary.membershipType == "enterprise")
#expect(summary.individualUsage?.plan == nil)
#expect(summary.individualUsage?.overall?.used == 10985)
#expect(summary.individualUsage?.overall?.limit == 15000)
#expect(summary.teamUsage?.pooled?.used == 4749435)
#expect(summary.teamUsage?.pooled?.limit == 10693200)
}

@Test
func `parses minimal usage summary`() throws {
let json = """
Expand Down Expand Up @@ -140,6 +177,7 @@ struct CursorStatusProbeTests {
autoPercentUsed: nil,
apiPercentUsed: nil,
totalPercentUsed: 0.40625),
overall: nil,
onDemand: nil),
teamUsage: nil),
userInfo: nil,
Expand Down Expand Up @@ -170,6 +208,7 @@ struct CursorStatusProbeTests {
autoPercentUsed: nil,
apiPercentUsed: nil,
totalPercentUsed: 0.5),
overall: nil,
onDemand: nil),
teamUsage: nil),
userInfo: nil,
Expand All @@ -178,6 +217,49 @@ struct CursorStatusProbeTests {
#expect(snapshot.planPercentUsed == 50.0)
}

@Test
func `uses overall usage when plan bucket is absent`() {
let snapshot = CursorStatusProbe(browserDetection: BrowserDetection(cacheTTL: 0))
.parseUsageSummary(
CursorUsageSummary(
billingCycleStart: "2026-03-01T00:00:00.000Z",
billingCycleEnd: "2026-04-01T00:00:00.000Z",
membershipType: "enterprise",
limitType: "team",
isUnlimited: false,
autoModelSelectedDisplayMessage: nil,
namedModelSelectedDisplayMessage: nil,
individualUsage: CursorIndividualUsage(
plan: nil,
overall: CursorPlanUsage(
enabled: true,
used: 10985,
limit: 15000,
remaining: 4015,
breakdown: nil,
autoPercentUsed: nil,
apiPercentUsed: nil,
totalPercentUsed: nil),
onDemand: nil),
teamUsage: CursorTeamUsage(
onDemand: nil,
pooled: CursorPlanUsage(
enabled: true,
used: 4749435,
limit: 10693200,
remaining: 5943765,
breakdown: nil,
autoPercentUsed: nil,
apiPercentUsed: nil,
totalPercentUsed: nil))),
userInfo: nil,
rawJSON: nil)

#expect(snapshot.planUsedUSD == 109.85)
#expect(snapshot.planLimitUSD == 150.0)
#expect(abs(snapshot.planPercentUsed - 73.23333333333333) < 0.0001)
}

@Test
func `converts snapshot to usage snapshot`() {
let snapshot = CursorStatusSnapshot(
Expand Down