diff --git a/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift b/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift index fd408da8..e6cc967d 100644 --- a/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift @@ -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) { @@ -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.") } @@ -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") } } diff --git a/Tests/CodexBarTests/KiroStatusProbeTests.swift b/Tests/CodexBarTests/KiroStatusProbeTests.swift index 578c06f4..57c57d91 100644 --- a/Tests/CodexBarTests/KiroStatusProbeTests.swift +++ b/Tests/CodexBarTests/KiroStatusProbeTests.swift @@ -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