281 lines · 9.7 KB · Swift
RawDownload
import Foundation // MARK: - Cached Plan Info (Codable) public struct WindsurfCachedPlanInfo: Codable, Sendable {    public let planName: String?    public let startTimestamp: Int64?    public let endTimestamp: Int64?    public let usage: Usage?    public let quotaUsage: QuotaUsage?     public struct Usage: Codable, Sendable {        public let messages: Int?        public let usedMessages: Int?        public let remainingMessages: Int?        public let flowActions: Int?        public let usedFlowActions: Int?        public let remainingFlowActions: Int?        public let flexCredits: Int?        public let usedFlexCredits: Int?        public let remainingFlexCredits: Int?    }     public struct QuotaUsage: Codable, Sendable {        public let dailyRemainingPercent: Double?        public let weeklyRemainingPercent: Double?        public let dailyResetAtUnix: Int64?        public let weeklyResetAtUnix: Int64?    }} // MARK: - Errors & Probe #if os(macOS) import SQLite3 public enum WindsurfStatusProbeError: LocalizedError, Sendable, Equatable {    case dbNotFound(String)    case sqliteFailed(String)    case noData    case parseFailed(String)     public var errorDescription: String? {        switch self {        case let .dbNotFound(path):            "Windsurf database not found at \(path). Ensure Windsurf is installed and has been launched at least once."        case let .sqliteFailed(message):            "SQLite error reading Windsurf data: \(message)"        case .noData:            "No plan data found in Windsurf database. Sign in to Windsurf first."        case let .parseFailed(message):            "Could not parse Windsurf plan data: \(message)"        }    }} // MARK: - Probe public struct WindsurfStatusProbe: Sendable {    private static let defaultDBPath: String = {        let home = NSHomeDirectory()        return "\(home)/Library/Application Support/Windsurf/User/globalStorage/state.vscdb"    }()     private static let query = "SELECT value FROM ItemTable WHERE key = 'windsurf.settings.cachedPlanInfo' LIMIT 1;"     private let dbPath: String     public init(dbPath: String? = nil) {        self.dbPath = dbPath ?? Self.defaultDBPath    }     public func fetch() throws -> WindsurfCachedPlanInfo {        guard FileManager.default.fileExists(atPath: self.dbPath) else {            throw WindsurfStatusProbeError.dbNotFound(self.dbPath)        }         var db: OpaquePointer?        guard sqlite3_open_v2(self.dbPath, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {            let message = db.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "unknown error"            sqlite3_close(db)            throw WindsurfStatusProbeError.sqliteFailed(message)        }        defer { sqlite3_close(db) }         sqlite3_busy_timeout(db, 250)         var stmt: OpaquePointer?        guard sqlite3_prepare_v2(db, Self.query, -1, &stmt, nil) == SQLITE_OK else {            let message = db.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "unknown error"            throw WindsurfStatusProbeError.sqliteFailed(message)        }        defer { sqlite3_finalize(stmt) }         let stepResult = sqlite3_step(stmt)        guard stepResult == SQLITE_ROW else {            if stepResult == SQLITE_DONE {                throw WindsurfStatusProbeError.noData            }            let message = db.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "unknown error"            throw WindsurfStatusProbeError.sqliteFailed(message)        }         guard let jsonString = Self.decodeSQLiteValue(stmt: stmt, index: 0) else {            throw WindsurfStatusProbeError.noData        }        guard let jsonData = jsonString.data(using: .utf8) else {            throw WindsurfStatusProbeError.parseFailed("Invalid UTF-8 encoding")        }         do {            return try JSONDecoder().decode(WindsurfCachedPlanInfo.self, from: jsonData)        } catch {            throw WindsurfStatusProbeError.parseFailed(error.localizedDescription)        }    }     private static func decodeSQLiteValue(stmt: OpaquePointer?, index: Int32) -> String? {        switch sqlite3_column_type(stmt, index) {        case SQLITE_TEXT:            guard let c = sqlite3_column_text(stmt, index) else { return nil }            return String(cString: c)        case SQLITE_BLOB:            guard let bytes = sqlite3_column_blob(stmt, index) else { return nil }            let data = Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))            // VSCode/Windsurf state.vscdb schema declares value as BLOB;            // only accept decodes that still parse as JSON to avoid UTF-16 mojibake.            return self.decodeJSONBlob(data)        default:            return nil        }    }     private static func decodeJSONBlob(_ data: Data) -> String? {        for encoding in [String.Encoding.utf8, .utf16LittleEndian] {            guard let decoded = String(data: data, encoding: encoding) else { continue }            let trimmed = decoded.trimmingCharacters(in: .controlCharacters)            guard let jsonData = trimmed.data(using: .utf8),                  (try? JSONSerialization.jsonObject(with: jsonData)) != nil            else {                continue            }            return trimmed        }        return nil    }} #else // MARK: - Windsurf (Unsupported) public enum WindsurfStatusProbeError: LocalizedError, Sendable, Equatable {    case notSupported     public var errorDescription: String? {        "Windsurf is only supported on macOS."    }} public struct WindsurfStatusProbe: Sendable {    public init(dbPath _: String? = nil) {}     public func fetch() throws -> WindsurfCachedPlanInfo {        throw WindsurfStatusProbeError.notSupported    }} #endif // MARK: - Conversion to UsageSnapshot extension WindsurfCachedPlanInfo {    public func toUsageSnapshot() -> UsageSnapshot {        var primary: RateWindow?        var secondary: RateWindow?         if let quota = self.quotaUsage {            // Primary: daily usage (usedPercent = 100 - dailyRemainingPercent)            if let daily = quota.dailyRemainingPercent {                let resetDate = quota.dailyResetAtUnix.map {                    Date(timeIntervalSince1970: TimeInterval($0))                }                primary = RateWindow(                    usedPercent: max(0, min(100, 100 - daily)),                    windowMinutes: nil,                    resetsAt: resetDate,                    resetDescription: Self.formatResetDescription(resetDate))            }             // Secondary: weekly usage            if let weekly = quota.weeklyRemainingPercent {                let resetDate = quota.weeklyResetAtUnix.map {                    Date(timeIntervalSince1970: TimeInterval($0))                }                secondary = RateWindow(                    usedPercent: max(0, min(100, 100 - weekly)),                    windowMinutes: nil,                    resetsAt: resetDate,                    resetDescription: Self.formatResetDescription(resetDate))            }        }         if primary == nil, let usage = self.usage {            primary = Self.makeUsageWindow(                used: usage.usedMessages,                remaining: usage.remainingMessages,                total: usage.messages,                unit: "messages")        }         if secondary == nil, let usage = self.usage {            secondary = Self.makeUsageWindow(                used: usage.usedFlowActions,                remaining: usage.remainingFlowActions,                total: usage.flowActions,                unit: "flow actions")        }         // Identity        var orgDescription: String?        if let endTimestamp = self.endTimestamp {            let endDate = Date(timeIntervalSince1970: TimeInterval(endTimestamp) / 1000)            let formatter = DateFormatter()            formatter.dateStyle = .medium            formatter.timeStyle = .none            orgDescription = "Expires \(formatter.string(from: endDate))"        }         let identity = ProviderIdentitySnapshot(            providerID: .windsurf,            accountEmail: nil,            accountOrganization: orgDescription,            loginMethod: self.planName)         return UsageSnapshot(            primary: primary,            secondary: secondary,            updatedAt: Date(),            identity: identity)    }     private static func makeUsageWindow(        used rawUsed: Int?,        remaining rawRemaining: Int?,        total rawTotal: Int?,        unit: String) -> RateWindow?    {        guard let total = rawTotal, total > 0 else { return nil }        let inferredUsed = rawUsed ?? rawRemaining.map { max(0, total - $0) }        guard let used = inferredUsed else { return nil }        let clampedUsed = max(0, min(total, used))        let usedPercent = max(0, min(100, Double(clampedUsed) / Double(total) * 100))        return RateWindow(            usedPercent: usedPercent,            windowMinutes: nil,            resetsAt: nil,            resetDescription: "\(clampedUsed) / \(total) \(unit)")    }     private static func formatResetDescription(_ date: Date?) -> String? {        guard let date else { return nil }        let now = Date()        let interval = date.timeIntervalSince(now)        guard interval > 0 else { return "Expired" }         let hours = Int(interval / 3600)        let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60)         if hours > 24 {            let days = hours / 24            let remainingHours = hours % 24            return "Resets in \(days)d \(remainingHours)h"        } else if hours > 0 {            return "Resets in \(hours)h \(minutes)m"        } else {            return "Resets in \(minutes)m"        }    }}