From 9c229d803b32a2ae94dcab2df49fa18c8d4ee7b1 Mon Sep 17 00:00:00 2001 From: kilhyeonjun Date: Mon, 2 Feb 2026 09:42:28 +0900 Subject: [PATCH 1/2] fix(kiro): support kiro-cli 1.24+ Q Developer format - Add parsing for new 'Plan: X' format from kiro-cli 1.24+ - Handle 'managed by admin' cases for enterprise plans - Add isUsageOutputComplete checks for new format - Add 3 test cases for Q Developer plan formats Fixes #287 --- .../Providers/Kiro/KiroStatusProbe.swift | 39 ++++++++++++++- .../CodexBarTests/KiroStatusProbeTests.swift | 49 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift b/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift index fd408da8..c0fdc7b0 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,24 @@ 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 { + if !matchedPercent, !matchedCredits, !matchedNewFormat { throw KiroStatusProbeError.parseError( "No recognizable usage patterns found. Kiro CLI output format may have changed.") } @@ -482,5 +515,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 From 7559522d79e48821cba47b490ffe8ed14cb0eceb Mon Sep 17 00:00:00 2001 From: kilhyeonjun Date: Mon, 2 Feb 2026 10:25:09 +0900 Subject: [PATCH 2/2] fix: require usage data for non-managed new format plans Address review feedback: only bypass parse error for managed plans that don't expose usage data. Non-managed plans with Plan: header but no usage data will now correctly throw parse error instead of falling back to default 50 credits. --- Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift b/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift index c0fdc7b0..e6cc967d 100644 --- a/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift +++ b/Sources/CodexBarCore/Providers/Kiro/KiroStatusProbe.swift @@ -457,7 +457,8 @@ public struct KiroStatusProbe: Sendable { } // Require at least one key pattern to match to avoid silent failures - if !matchedPercent, !matchedCredits, !matchedNewFormat { + // 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.") }