import Foundation
public enum OpenAIDashboardParser {
/// Extracts the signed-in email from the embedded `client-bootstrap` JSON payload, if present.
///
/// The Codex usage dashboard currently ships a JSON blob in:
/// ``.
/// WebKit `document.body.innerText` often does not include the email, so we parse it from HTML.
public static func parseSignedInEmailFromClientBootstrap(html: String) -> String? {
guard let data = self.clientBootstrapJSONData(fromHTML: html) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil }
// Fast path: common structure.
if let dict = json as? [String: Any] {
if let session = dict["session"] as? [String: Any],
let user = session["user"] as? [String: Any],
let email = user["email"] as? String,
email.contains("@")
{
return email.trimmingCharacters(in: .whitespacesAndNewlines)
}
if let user = dict["user"] as? [String: Any],
let email = user["email"] as? String,
email.contains("@")
{
return email.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
// Fallback: BFS scan for an email key/value.
var queue: [Any] = [json]
var seen = 0
while !queue.isEmpty, seen < 4000 {
let cur = queue.removeFirst()
seen += 1
if let dict = cur as? [String: Any] {
for (k, v) in dict {
if k.lowercased() == "email", let email = v as? String, email.contains("@") {
return email.trimmingCharacters(in: .whitespacesAndNewlines)
}
queue.append(v)
}
} else if let arr = cur as? [Any] {
queue.append(contentsOf: arr)
}
}
return nil
}
/// Extracts the auth status from `client-bootstrap`, if present.
/// Expected values include `logged_in` and `logged_out`.
public static func parseAuthStatusFromClientBootstrap(html: String) -> String? {
guard let data = self.clientBootstrapJSONData(fromHTML: html) else { return nil }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil }
guard let dict = json as? [String: Any] else { return nil }
if let authStatus = dict["authStatus"] as? String, !authStatus.isEmpty {
return authStatus.trimmingCharacters(in: .whitespacesAndNewlines)
}
return nil
}
public static func parseCodeReviewRemainingPercent(bodyText: String) -> Double? {
let cleaned = bodyText.replacingOccurrences(of: "\r", with: "\n")
let patterns = [
#"Code\s*review[^0-9%]*([0-9]{1,3})%\s*remaining"#,
#"Core\s*review[^0-9%]*([0-9]{1,3})%\s*remaining"#,
]
for pattern in patterns {
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { continue }
let range = NSRange(cleaned.startIndex..= 2,
let r = Range(match.range(at: 1), in: cleaned)
else { continue }
if let val = Double(cleaned[r]) { return min(100, max(0, val)) }
}
return nil
}
public static func parseCreditEvents(rows: [[String]]) -> [CreditEvent] {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "MMM d, yyyy"
return rows.compactMap { row in
guard row.count >= 3 else { return nil }
let dateString = row[0]
let service = row[1].trimmingCharacters(in: .whitespacesAndNewlines)
let amountString = row[2]
guard let date = formatter.date(from: dateString) else { return nil }
let creditsUsed = Self.parseCreditsUsed(amountString)
return CreditEvent(date: date, service: service, creditsUsed: creditsUsed)
}
.sorted { $0.date > $1.date }
}
private static func parseCreditsUsed(_ text: String) -> Double {
let cleaned = text
.replacingOccurrences(of: ",", with: "")
.replacingOccurrences(of: "credits", with: "", options: .caseInsensitive)
.trimmingCharacters(in: .whitespacesAndNewlines)
return Double(cleaned) ?? 0
}
// MARK: - Private
private static func clientBootstrapJSONData(fromHTML html: String) -> Data? {
let needle = "id=\"client-bootstrap\""
guard let idRange = html.range(of: needle) else { return nil }
// Find the end of the opening ", range: contentStart..