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
40 changes: 38 additions & 2 deletions Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,14 +359,31 @@ public struct KiroStatusProbe: Sendable {
// Track which key patterns matched to detect format changes
var matchedPercent = false
var matchedCredits = false
var matchedNewFormat = false

// Parse plan name from "| KIRO FREE" or similar
// Parse plan name from "| KIRO FREE" or similar (legacy format)
var planName = "Kiro"
if let planMatch = stripped.range(of: #"\|\s*(KIRO\s+\w+)"#, options: .regularExpression) {
let raw = String(stripped[planMatch]).replacingOccurrences(of: "|", with: "")
planName = raw.trimmingCharacters(in: .whitespaces)
}

// Parse plan name from "Plan: Q Developer Pro" (new format, kiro-cli 1.24+)
if let newPlanMatch = stripped.range(of: #"Plan:\s*(.+)"#, options: .regularExpression) {
let line = String(stripped[newPlanMatch])
// Extract just the plan name, stopping at newline
let planLine = line.replacingOccurrences(of: "Plan:", with: "").trimmingCharacters(in: .whitespaces)
if let firstLine = planLine.split(separator: "\n").first {
planName = String(firstLine).trimmingCharacters(in: .whitespaces)
matchedNewFormat = true
}
}

// Check if this is a managed/enterprise plan with no usage data
let isManagedPlan = lowered.contains("managed by admin")
|| lowered.contains("managed by organization")
|| lowered.contains("enterprise")

// Parse reset date from "resets on 01/01"
var resetsAt: Date?
if let resetMatch = stripped.range(of: #"resets on (\d{2}/\d{2})"#, options: .regularExpression) {
Expand Down Expand Up @@ -423,8 +440,25 @@ public struct KiroStatusProbe: Sendable {
}
}

// For managed/enterprise plans in new format, we may not have usage data
// but we should still show the plan name without error
if matchedNewFormat, isManagedPlan {
// Managed plans don't expose credits; return snapshot with plan name only
return KiroUsageSnapshot(
planName: planName,
creditsUsed: 0,
creditsTotal: 0,
creditsPercent: 0,
bonusCreditsUsed: nil,
bonusCreditsTotal: nil,
bonusExpiryDays: nil,
resetsAt: nil,
updatedAt: Date())
}

// Require at least one key pattern to match to avoid silent failures
if !matchedPercent, !matchedCredits {
// Only bypass error for managed plans in new format (they don't expose usage data)
if !matchedPercent, !matchedCredits, !(matchedNewFormat && isManagedPlan) {
throw KiroStatusProbeError.parseError(
"No recognizable usage patterns found. Kiro CLI output format may have changed.")
}
Expand Down Expand Up @@ -482,5 +516,7 @@ public struct KiroStatusProbe: Sendable {
return stripped.contains("covered in plan")
|| stripped.contains("resets on")
|| stripped.contains("bonus credits")
|| stripped.contains("plan:")
|| stripped.contains("managed by admin")
}
}
49 changes: 49 additions & 0 deletions Tests/CodexBarTests/KiroStatusProbeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,55 @@ struct KiroStatusProbeTests {
}
}

// MARK: - New Format (kiro-cli 1.24+, Q Developer)

@Test
func parsesQDeveloperManagedPlan() throws {
let output = """
Plan: Q Developer Pro
Your plan is managed by admin

Tip: to see context window usage, run /context
"""

let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)

#expect(snapshot.planName == "Q Developer Pro")
#expect(snapshot.creditsPercent == 0)
#expect(snapshot.creditsUsed == 0)
#expect(snapshot.creditsTotal == 0)
#expect(snapshot.bonusCreditsUsed == nil)
#expect(snapshot.resetsAt == nil)
}

@Test
func parsesQDeveloperFreePlan() throws {
let output = """
Plan: Q Developer Free
Your plan is managed by admin
"""

let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)

#expect(snapshot.planName == "Q Developer Free")
#expect(snapshot.creditsPercent == 0)
}

@Test
func parsesNewFormatWithANSICodes() throws {
let output = """
\u{001B}[38;5;141mPlan: Q Developer Pro\u{001B}[0m
Your plan is managed by admin
"""

let probe = KiroStatusProbe()
let snapshot = try probe.parse(output: output)

#expect(snapshot.planName == "Q Developer Pro")
}

// MARK: - Snapshot Conversion

@Test
Expand Down