import Foundation #if os(macOS) /// Fetches Augment usage via `auggie account status` CLI command public struct AuggieCLIProbe: Sendable { private static let log = CodexBarLog.logger(LogCategories.auggieCLI) public init() {} public func fetch() async throws -> AugmentStatusSnapshot { let output = try await self.runAuggieAccountStatus() return try self.parse(output) } /// Timeout for the `auggie account status` command. private static let commandTimeout: TimeInterval = 15 private func runAuggieAccountStatus() async throws -> String { let env = ProcessInfo.processInfo.environment let loginPATH = LoginShellPathCache.shared.current let executable = BinaryLocator.resolveAuggieBinary(env: env, loginPATH: loginPATH) ?? "auggie" var pathEnv = env pathEnv["PATH"] = PathBuilder.effectivePATH( purposes: [.tty, .nodeTooling], env: env, loginPATH: loginPATH) let result = try await SubprocessRunner.run( binary: executable, arguments: ["account", "status"], environment: pathEnv, timeout: Self.commandTimeout, label: "auggie-account-status") let output = result.stdout let errorOutput = result.stderr guard !output.isEmpty else { if !errorOutput.isEmpty { Self.log.error("Auggie stderr: \(errorOutput)") } throw AuggieCLIError.noOutput } // Check for auth errors if output.contains("Authentication failed") || output.contains("auggie login") { throw AuggieCLIError.notAuthenticated } return output } func parse(_ output: String) throws -> AugmentStatusSnapshot { // Legacy output: // Max Plan 450,000 credits / month // 11,657 remaining · 953,170 / 964,827 credits used // 2 days remaining in this billing cycle (ends 1/8/2026) // // Current output (2026+): // 319,054 credits remaining Max Plan // 450,000 credits / month // 9 days remaining in this billing cycle (ends 6/9/2026) var maxCredits: Int? var remaining: Int? var used: Int? var total: Int? var billingCycleEnd: Date? for line in output.split(separator: "\n") { let trimmed = line.trimmingCharacters(in: .whitespaces) if trimmed.contains("credits / month") { if let match = trimmed.range(of: #"([\d,]+)\s+credits\s*/\s*month"#, options: .regularExpression) { let numberStr = String(trimmed[match]).replacingOccurrences(of: ",", with: "") .replacingOccurrences(of: " credits", with: "") .replacingOccurrences(of: " / month", with: "") maxCredits = Int(numberStr) total = total ?? Int(numberStr) } } else if trimmed.contains("Max Plan"), trimmed.contains("credits"), !trimmed.contains("remaining") { if let match = trimmed.range(of: #"([\d,]+)\s+credits"#, options: .regularExpression) { let numberStr = String(trimmed[match]).replacingOccurrences(of: ",", with: "") .replacingOccurrences(of: " credits", with: "") maxCredits = Int(numberStr) } } if trimmed.contains("credits remaining"), !trimmed.contains("billing cycle") { if let match = trimmed.range(of: #"([\d,]+)\s+credits\s+remaining"#, options: .regularExpression) { let numberStr = String(trimmed[match]).replacingOccurrences(of: ",", with: "") .replacingOccurrences(of: " credits", with: "") .replacingOccurrences(of: " remaining", with: "") remaining = Int(numberStr) } } // Parse "11,657 remaining · 953,170 / 964,827 credits used" if trimmed.contains("remaining"), trimmed.contains("credits used") { if let remMatch = trimmed.range(of: #"([\d,]+)\s+remaining"#, options: .regularExpression) { let numStr = String(trimmed[remMatch]) .replacingOccurrences(of: ",", with: "") .replacingOccurrences(of: " remaining", with: "") remaining = Int(numStr) } if let usedMatch = trimmed.range( of: #"([\d,]+)\s*/\s*([\d,]+)\s+credits used"#, options: .regularExpression) { let parts = String(trimmed[usedMatch]) .replacingOccurrences(of: " credits used", with: "") .split(separator: "/") if parts.count == 2 { used = Int(parts[0].replacingOccurrences(of: ",", with: "") .trimmingCharacters(in: .whitespaces)) total = Int(parts[1].replacingOccurrences(of: ",", with: "") .trimmingCharacters(in: .whitespaces)) } } } if trimmed.contains("billing cycle"), trimmed.contains("ends") { if let dateMatch = trimmed.range(of: #"ends\s+([\d/]+)"#, options: .regularExpression) { let dateStr = String(trimmed[dateMatch]) .replacingOccurrences(of: "ends", with: "") .trimmingCharacters(in: .whitespaces) let formatter = DateFormatter() formatter.dateFormat = "M/d/yyyy" formatter.locale = Locale(identifier: "en_US_POSIX") formatter.timeZone = TimeZone.current billingCycleEnd = formatter.date(from: dateStr) } } } guard let finalRemaining = remaining else { Self.log.error("Failed to parse auggie output: \(output)") throw AuggieCLIError.parseError("Could not extract credits from output") } let finalTotal = total ?? maxCredits guard let finalTotal else { Self.log.error("Failed to parse auggie output: \(output)") throw AuggieCLIError.parseError("Could not extract credits from output") } let finalUsed = used ?? max(0, finalTotal - finalRemaining) return AugmentStatusSnapshot( creditsRemaining: Double(finalRemaining), creditsUsed: Double(finalUsed), creditsLimit: Double(finalTotal), billingCycleEnd: billingCycleEnd, accountEmail: nil, accountPlan: maxCredits.map { "\($0.formatted()) credits/month" }, rawJSON: nil) } } #else public struct AuggieCLIProbe: Sendable { public init() {} public func fetch() async throws -> AugmentStatusSnapshot { throw AugmentStatusProbeError.notSupported } } #endif public enum AuggieCLIError: LocalizedError { case noOutput case notAuthenticated case parseError(String) public var errorDescription: String? { switch self { case .noOutput: "Auggie CLI returned no output" case .notAuthenticated: "Not authenticated. Run 'auggie login' to authenticate." case let .parseError(msg): "Failed to parse auggie output: \(msg)" } } }