chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
@@ -0,0 +1,190 @@
#if os(macOS)
import Foundation
extension OpenAIDashboardBrowserCookieImporter {
private struct PendingCookieStoreMutation {
let token: UUID
let task: Task<Void, Never>
}
@MainActor private static var pendingCookieStoreMutations: [ObjectIdentifier: PendingCookieStoreMutation] = [:]
private nonisolated static let cookieCacheQueue = DispatchQueue(
label: "com.steipete.codexbar.openai-cookie-cache")
private nonisolated static let deadlineQueue = DispatchQueue(
label: "com.steipete.codexbar.openai-cookie-deadline",
qos: .userInitiated)
private final class CookieLoadCompletion: @unchecked Sendable {
private let lock = NSLock()
private var didFinish = false
func finish(_ action: () -> Void) {
let shouldFinish = self.lock.withLock {
guard !self.didFinish else { return false }
self.didFinish = true
return true
}
if shouldFinish { action() }
}
}
nonisolated static func remainingTimeout(
until deadline: Date?,
cappedAt localLimit: TimeInterval? = nil,
now: Date = Date()) throws -> TimeInterval
{
guard let deadline else {
return localLimit.map(OpenAIDashboardFetcher.sanitizedTimeout) ?? .greatestFiniteMagnitude
}
let remaining = deadline.timeIntervalSince(now)
guard remaining > 0 else { throw URLError(.timedOut) }
guard let localLimit else { return remaining }
return min(OpenAIDashboardFetcher.sanitizedTimeout(localLimit), remaining)
}
nonisolated static func runBoundedCookieLoad<T: Sendable>(
deadline: Date?,
timeoutObserver: (@Sendable () -> Void)? = nil,
operation: @escaping @Sendable () throws -> T) async throws -> T
{
// The detached/GCD loader does not inherit TaskLocal prompt policy or retry selection.
let contextualOperation = BrowserCookieAccessGate.operationPreservingAccessContext(operation)
guard let deadline else {
return try await Task.detached(priority: .userInitiated, operation: contextualOperation).value
}
let timeout = try self.remainingTimeout(until: deadline)
let completion = CookieLoadCompletion()
return try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let result = Result(catching: contextualOperation)
completion.finish { continuation.resume(with: result) }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish {
timeoutObserver?()
continuation.resume(throwing: URLError(.timedOut))
}
}
}
}
nonisolated static func runBoundedCookieCacheOperation<T: Sendable>(
deadline: Date?,
operation: @escaping @Sendable () throws -> T) async throws -> T
{
guard let deadline else {
return try await withCheckedThrowingContinuation { continuation in
self.cookieCacheQueue.async {
continuation.resume(with: Result(catching: operation))
}
}
}
let timeout = try self.remainingTimeout(until: deadline)
let completion = CookieLoadCompletion()
return try await withCheckedThrowingContinuation { continuation in
self.cookieCacheQueue.async {
let result = Result(catching: operation)
completion.finish { continuation.resume(with: result) }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish { continuation.resume(throwing: URLError(.timedOut)) }
}
}
}
static func runBoundedCallback(
deadline: Date?,
timeoutObserver: (@Sendable () -> Void)? = nil,
start: (@escaping @Sendable () -> Void) -> Void) async throws
{
let completion = CookieLoadCompletion()
guard let deadline else {
await withCheckedContinuation { continuation in
start {
completion.finish { continuation.resume() }
}
}
return
}
let timeout = try self.remainingTimeout(until: deadline)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
start {
completion.finish { continuation.resume() }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish {
timeoutObserver?()
continuation.resume(throwing: URLError(.timedOut))
}
}
}
}
static func runBoundedValueCallback<T: Sendable>(
deadline: Date?,
timeoutObserver: (@Sendable () -> Void)? = nil,
start: (@escaping @Sendable (T) -> Void) -> Void) async throws -> T
{
let completion = CookieLoadCompletion()
guard let deadline else {
return await withCheckedContinuation { continuation in
start { value in
completion.finish { continuation.resume(returning: value) }
}
}
}
let timeout = try self.remainingTimeout(until: deadline)
return try await withCheckedThrowingContinuation { continuation in
start { value in
completion.finish { continuation.resume(returning: value) }
}
self.deadlineQueue.asyncAfter(deadline: .now() + timeout) {
completion.finish {
timeoutObserver?()
continuation.resume(throwing: URLError(.timedOut))
}
}
}
}
static func runSerializedCallback(
key: ObjectIdentifier,
deadline: Date?,
start: @escaping (@escaping @Sendable () -> Void) -> Void) async throws
{
while let pending = self.pendingCookieStoreMutations[key] {
try await self.waitForMutation(pending.task, deadline: deadline)
if self.pendingCookieStoreMutations[key]?.token == pending.token {
self.pendingCookieStoreMutations[key] = nil
}
}
let token = UUID()
let task = Task { @MainActor in
await withCheckedContinuation { continuation in
start { continuation.resume() }
}
}
self.pendingCookieStoreMutations[key] = PendingCookieStoreMutation(token: token, task: task)
Task { @MainActor in
await task.value
if self.pendingCookieStoreMutations[key]?.token == token {
self.pendingCookieStoreMutations[key] = nil
}
}
try await self.waitForMutation(task, deadline: deadline)
}
private static func waitForMutation(_ task: Task<Void, Never>, deadline: Date?) async throws {
try await self.runBoundedCallback(deadline: deadline) { completion in
Task { @MainActor in
await task.value
completion()
}
}
}
}
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
#if os(macOS)
import Foundation
extension OpenAIDashboardFetcher {
struct ReturnableDashboardDataInput {
let codeReview: Double?
let events: [CreditEvent]
let usageBreakdown: [OpenAIDashboardDailyBreakdown]
let hasUsageLimits: Bool
let creditsRemaining: Double?
let codexCreditLimit: CodexCreditLimitSnapshot?
}
nonisolated static func hasReturnableDashboardData(_ input: ReturnableDashboardDataInput) -> Bool {
input.codeReview != nil
|| !input.events.isEmpty
|| !input.usageBreakdown.isEmpty
|| input.hasUsageLimits
|| input.creditsRemaining != nil
|| input.codexCreditLimit != nil
}
nonisolated static func hasAnyDashboardSignal(
hasReturnableData: Bool,
creditsHeaderPresent: Bool) -> Bool
{
hasReturnableData || creditsHeaderPresent
}
}
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,101 @@
#if os(macOS)
import Foundation
import WebKit
// MARK: - Navigation helper (revived from the old credits scraper)
@MainActor
final class NavigationDelegate: NSObject, WKNavigationDelegate {
private let completion: (Result<Void, Error>) -> Void
private var hasCompleted: Bool = false
private var timeoutWorkItem: DispatchWorkItem?
private var postCommitWorkItem: DispatchWorkItem?
static var associationKey: UInt8 = 0
nonisolated static let postCommitSuccessDelay: TimeInterval = 0.75
init(completion: @escaping (Result<Void, Error>) -> Void) {
self.completion = completion
}
func armTimeout(seconds: TimeInterval) {
self.timeoutWorkItem?.cancel()
let delay = max(seconds, 0)
let workItem = DispatchWorkItem { [weak self] in
MainActor.assumeIsolated {
self?.completeOnce(.failure(URLError(.timedOut)))
}
}
self.timeoutWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
func cancel() {
self.completeOnce(.failure(CancellationError()))
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.completeOnce(.success(()))
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
guard !self.hasCompleted else { return }
self.postCommitWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
MainActor.assumeIsolated {
self?.completeOnce(.success(()))
}
}
self.postCommitWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + Self.postCommitSuccessDelay, execute: workItem)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
if Self.shouldIgnoreNavigationError(error) { return }
self.completeOnce(.failure(error))
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if Self.shouldIgnoreNavigationError(error) { return }
self.completeOnce(.failure(error))
}
nonisolated static func shouldIgnoreNavigationError(_ error: Error) -> Bool {
let nsError = error as NSError
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorCancelled {
return true
}
if nsError.domain == "WebKitErrorDomain", nsError.code == 102 {
return true
}
return false
}
private func completeOnce(_ result: Result<Void, Error>) {
guard !self.hasCompleted else { return }
self.hasCompleted = true
self.timeoutWorkItem?.cancel()
self.timeoutWorkItem = nil
self.postCommitWorkItem?.cancel()
self.postCommitWorkItem = nil
self.completion(result)
}
}
extension WKWebView {
var codexNavigationDelegate: NavigationDelegate? {
get {
objc_getAssociatedObject(self, &NavigationDelegate.associationKey) as? NavigationDelegate
}
set {
objc_setAssociatedObject(
self,
&NavigationDelegate.associationKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
#endif
@@ -0,0 +1,551 @@
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:
/// `<script type="application/json" id="client-bootstrap"></script>`.
/// 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")
for regex in self.codeReviewRegexes {
let range = NSRange(cleaned.startIndex..<cleaned.endIndex, in: cleaned)
guard let match = regex.firstMatch(in: cleaned, options: [], range: range),
match.numberOfRanges >= 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 parseCreditsRemaining(bodyText: String) -> Double? {
let cleaned = bodyText.replacingOccurrences(of: "\r", with: "\n")
let patterns = [
#"credits\s*remaining[^0-9]*([0-9][0-9.,]*)"#,
#"remaining\s*credits[^0-9]*([0-9][0-9.,]*)"#,
#"credit\s*balance[^0-9]*([0-9][0-9.,]*)"#,
]
for pattern in patterns {
if let val = TextParsing.firstNumber(pattern: pattern, text: cleaned) { return val }
}
return nil
}
public static func parseRateLimits(
bodyText: String,
now: Date = .init()) -> (primary: RateWindow?, secondary: RateWindow?)
{
let cleaned = bodyText.replacingOccurrences(of: "\r", with: "\n")
let lines = cleaned
.split(whereSeparator: \.isNewline)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
let primary = self.parseRateWindow(
lines: lines,
match: self.isFiveHourLimitLine,
windowMinutes: 5 * 60,
now: now)
let secondary = self.parseRateWindow(
lines: lines,
match: self.isWeeklyLimitLine,
windowMinutes: 7 * 24 * 60,
now: now)
return (primary, secondary)
}
public static func parseCodeReviewLimit(bodyText: String, now: Date = .init()) -> RateWindow? {
let cleaned = bodyText.replacingOccurrences(of: "\r", with: "\n")
let lines = cleaned
.split(whereSeparator: \.isNewline)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
return self.parseRateWindow(
lines: lines,
match: self.isCodeReviewLimitLine,
windowMinutes: nil,
now: now)
}
public static func parsePlanFromHTML(html: String) -> String? {
if let data = self.clientBootstrapJSONData(fromHTML: html),
let plan = self.findPlan(in: data)
{
return plan
}
if let data = self.nextDataJSONData(fromHTML: html),
let plan = self.findPlan(in: data)
{
return plan
}
return nil
}
public static func parseCreditEvents(rows: [[String]]) -> [CreditEvent] {
let formatter = self.creditDateFormatter()
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 {
guard let raw = self.firstNumberToken(in: text) else { return 0 }
let token = raw
.replacingOccurrences(of: "\u{00A0}", with: "")
.replacingOccurrences(of: "\u{202F}", with: "")
.replacingOccurrences(of: " ", with: "")
let hasComma = token.contains(",")
let hasDot = token.contains(".")
if hasComma, hasDot {
return TextParsing.firstNumber(pattern: #"([0-9][0-9.,\s\p{Zs}]*)"#, text: token) ?? 0
}
if hasComma {
if self.usesLocalizedDecimalCommaCreditLabel(text) {
return Double(token.replacingOccurrences(of: ",", with: ".")) ?? 0
}
if token.range(of: #"^\d{1,3}(,\d{3})+$"#, options: .regularExpression) != nil {
return Double(token.replacingOccurrences(of: ",", with: "")) ?? 0
}
return Double(token.replacingOccurrences(of: ",", with: ".")) ?? 0
}
return Double(token) ?? 0
}
private static func usesLocalizedDecimalCommaCreditLabel(_ text: String) -> Bool {
text
.lowercased()
.contains("crédit")
}
private static func firstNumberToken(in text: String) -> String? {
guard let regex = try? NSRegularExpression(
pattern: #"([0-9][0-9.,\s\p{Zs}]*)"#,
options: [])
else {
return nil
}
let range = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, options: [], range: range),
match.numberOfRanges >= 2,
let tokenRange = Range(match.range(at: 1), in: text)
else {
return nil
}
return String(text[tokenRange])
}
// MARK: - Private
private static let codeReviewRegexes: [NSRegularExpression] = {
let patterns = [
#"Code\s*review[^0-9%]*([0-9]{1,3})%\s*remaining"#,
#"Core\s*review[^0-9%]*([0-9]{1,3})%\s*remaining"#,
]
return patterns.compactMap { pattern in
try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
}
}()
private static let creditDateFormatterKey = "OpenAIDashboardParser.creditDateFormatter"
private static let clientBootstrapNeedle = Data("id=\"client-bootstrap\"".utf8)
private static let nextDataNeedle = Data("id=\"__NEXT_DATA__\"".utf8)
private static let scriptCloseNeedle = Data("</script>".utf8)
private static func creditDateFormatter() -> DateFormatter {
let threadDict = Thread.current.threadDictionary
if let cached = threadDict[self.creditDateFormatterKey] as? DateFormatter {
return cached
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "MMM d, yyyy"
threadDict[self.creditDateFormatterKey] = formatter
return formatter
}
private static func clientBootstrapJSONData(fromHTML html: String) -> Data? {
let data = Data(html.utf8)
guard let idRange = data.range(of: self.clientBootstrapNeedle) else { return nil }
guard let openTagEnd = data[idRange.upperBound...].firstIndex(of: UInt8(ascii: ">")) else { return nil }
let contentStart = data.index(after: openTagEnd)
guard let closeRange = data.range(
of: self.scriptCloseNeedle,
options: [],
in: contentStart..<data.endIndex)
else {
return nil
}
let rawData = data[contentStart..<closeRange.lowerBound]
let trimmed = self.trimASCIIWhitespace(Data(rawData))
return trimmed.isEmpty ? nil : trimmed
}
private static func nextDataJSONData(fromHTML html: String) -> Data? {
let data = Data(html.utf8)
guard let idRange = data.range(of: self.nextDataNeedle) else { return nil }
guard let openTagEnd = data[idRange.upperBound...].firstIndex(of: UInt8(ascii: ">")) else { return nil }
let contentStart = data.index(after: openTagEnd)
guard let closeRange = data.range(
of: self.scriptCloseNeedle,
options: [],
in: contentStart..<data.endIndex)
else {
return nil
}
let rawData = data[contentStart..<closeRange.lowerBound]
let trimmed = self.trimASCIIWhitespace(Data(rawData))
return trimmed.isEmpty ? nil : trimmed
}
private static func trimASCIIWhitespace(_ data: Data) -> Data {
guard !data.isEmpty else { return data }
var start = data.startIndex
var end = data.endIndex
while start < end, data[start].isASCIIWhitespace {
start = data.index(after: start)
}
while end > start {
let prev = data.index(before: end)
if data[prev].isASCIIWhitespace {
end = prev
} else {
break
}
}
return data.subdata(in: start..<end)
}
private static func parseRateWindow(
lines: [String],
match: (String) -> Bool,
windowMinutes: Int?,
now: Date) -> RateWindow?
{
for idx in lines.indices where match(lines[idx]) {
let end = min(lines.count - 1, idx + 5)
let windowLines = Array(lines[idx...end])
var percentValue: Double?
var isRemaining = true
for line in windowLines {
if let percent = self.parsePercent(from: line) {
percentValue = percent.value
isRemaining = percent.isRemaining
break
}
}
guard let percentValue else { continue }
let usedPercent = isRemaining ? max(0, min(100, 100 - percentValue)) : max(0, min(100, percentValue))
let resetLine = windowLines.first { $0.localizedCaseInsensitiveContains("reset") }
let resetDescription = resetLine?.trimmingCharacters(in: .whitespacesAndNewlines)
let resetsAt = resetLine.flatMap { self.parseResetDate(from: $0, now: now) }
let fallbackDescription = resetsAt.map { UsageFormatter.resetDescription(from: $0) }
return RateWindow(
usedPercent: usedPercent,
windowMinutes: windowMinutes,
resetsAt: resetsAt,
resetDescription: resetDescription ?? fallbackDescription)
}
return nil
}
private static func parsePercent(from line: String) -> (value: Double, isRemaining: Bool)? {
guard let percent = TextParsing.firstNumber(pattern: #"([0-9]{1,3})\s*%"#, text: line) else { return nil }
let lower = line.lowercased()
let isRemaining = lower.contains("remaining") || lower.contains("left")
let isUsed = lower.contains("used") || lower.contains("spent") || lower.contains("consumed")
if isUsed { return (percent, false) }
if isRemaining { return (percent, true) }
return (percent, true)
}
private static func isFiveHourLimitLine(_ line: String) -> Bool {
let lower = line.lowercased()
if lower.contains("5h") { return true }
if lower.range(of: #"\b5\s*h\b"#, options: .regularExpression) != nil { return true }
if lower.contains("5-hour") { return true }
if lower.contains("5 hour") { return true }
return false
}
private static func isWeeklyLimitLine(_ line: String) -> Bool {
let lower = line.lowercased()
if lower.contains("weekly") { return true }
if lower.contains("7-day") { return true }
if lower.contains("7 day") { return true }
if lower.contains("7d") { return true }
if lower.range(of: #"\b7\s*d\b"#, options: .regularExpression) != nil { return true }
return false
}
private static func isCodeReviewLimitLine(_ line: String) -> Bool {
let lower = line.lowercased()
guard lower.contains("code review") || lower.contains("core review") else { return false }
if lower.contains("github code review") { return false }
return true
}
private static func parseResetDate(from line: String, now: Date) -> Date? {
var raw = line.trimmingCharacters(in: .whitespacesAndNewlines)
raw = raw.replacingOccurrences(of: #"(?i)^resets?:?\s*"#, with: "", options: .regularExpression)
raw = raw.replacingOccurrences(of: " at ", with: " ", options: .caseInsensitive)
raw = raw.replacingOccurrences(of: " on ", with: " ", options: .caseInsensitive)
raw = raw.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
let calendar = Calendar(identifier: .gregorian)
let monthDayFormatter = DateFormatter()
monthDayFormatter.locale = Locale(identifier: "en_US_POSIX")
monthDayFormatter.timeZone = TimeZone.current
monthDayFormatter.dateFormat = "MMM d"
var candidate = raw
let lower = candidate.lowercased()
var usedRelativeDay = false
if lower.contains("today") {
usedRelativeDay = true
let dateText = monthDayFormatter.string(from: now)
candidate = candidate.replacingOccurrences(of: "today", with: dateText, options: .caseInsensitive)
} else if lower.contains("tomorrow") {
usedRelativeDay = true
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now) {
let dateText = monthDayFormatter.string(from: tomorrow)
candidate = candidate.replacingOccurrences(of: "tomorrow", with: dateText, options: .caseInsensitive)
}
}
if let weekdayMatch = self.weekdayMatch(in: candidate) {
usedRelativeDay = true
let target = self.nextWeekdayDate(weekday: weekdayMatch.weekday, now: now, calendar: calendar)
let dateText = monthDayFormatter.string(from: target)
candidate = candidate.replacingOccurrences(
of: weekdayMatch.matched,
with: dateText,
options: .caseInsensitive)
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone.current
formatter.defaultDate = now
let formats = [
"MMM d h:mma",
"MMM d, h:mma",
"MMM d h:mm a",
"MMM d, h:mm a",
"MMM d HH:mm",
"MMM d, HH:mm",
"MMM d",
"M/d h:mma",
"M/d h:mm a",
"M/d/yyyy h:mm a",
"M/d/yy h:mm a",
"M/d",
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd h:mm a",
"yyyy-MM-dd",
]
for format in formats {
formatter.dateFormat = format
if let date = formatter.date(from: candidate) {
if usedRelativeDay, date < now {
if lower.contains("today"),
let bumped = calendar.date(byAdding: .day, value: 1, to: date)
{
return bumped
}
if let bumped = calendar.date(byAdding: .day, value: 7, to: date) {
return bumped
}
}
return date
}
}
return nil
}
private struct WeekdayMatch {
let matched: String
let weekday: Int
}
private static func weekdayMatch(in text: String) -> WeekdayMatch? {
// The optional "(day)?" suffix requires each alternative to be long enough that
// adding "day" reproduces the full weekday name. "wed"+"day"="wedday" and
// "sat"+"day"="satday", so the longer prefixes "wednes" and "satur" are needed
// to cover the full "Wednesday" and "Saturday" forms mirroring how the existing
// "tue|tues" and "thu|thur|thurs" alternatives layer up to "tuesday"/"thursday".
let pattern = #"\b(mon|tue|tues|wed|wednes|thu|thur|thurs|fri|sat|satur|sun)(day)?\b"#
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else { return nil }
let range = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, options: [], range: range),
let r = Range(match.range(at: 0), in: text)
else { return nil }
let matched = String(text[r])
let lower = matched.lowercased()
let weekday = switch lower.prefix(3) {
case "mon": 2
case "tue": 3
case "wed": 4
case "thu": 5
case "fri": 6
case "sat": 7
default: 1
}
return WeekdayMatch(matched: matched, weekday: weekday)
}
private static func nextWeekdayDate(weekday: Int, now: Date, calendar: Calendar) -> Date {
let currentWeekday = calendar.component(.weekday, from: now)
var delta = weekday - currentWeekday
if delta < 0 { delta += 7 }
guard let next = calendar.date(byAdding: .day, value: delta, to: calendar.startOfDay(for: now)) else {
return now
}
return next
}
private static func findPlan(in data: Data) -> String? {
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil }
return self.findPlan(in: json)
}
private static func findPlan(in json: Any) -> String? {
var queue: [Any] = [json]
var seen = 0
while !queue.isEmpty, seen < 6000 {
let cur = queue.removeFirst()
seen += 1
if let dict = cur as? [String: Any] {
for (k, v) in dict {
if let plan = self.planCandidate(forKey: k, value: v) { return plan }
queue.append(v)
}
} else if let arr = cur as? [Any] {
queue.append(contentsOf: arr)
}
}
return nil
}
private static func planCandidate(forKey key: String, value: Any) -> String? {
guard self.isPlanKey(key) else { return nil }
if let str = value as? String {
return self.normalizePlanValue(str)
}
if let dict = value as? [String: Any] {
if let name = dict["name"] as? String, let plan = self.normalizePlanValue(name) { return plan }
if let display = dict["displayName"] as? String, let plan = self.normalizePlanValue(display) { return plan }
if let tier = dict["tier"] as? String, let plan = self.normalizePlanValue(tier) { return plan }
}
return nil
}
private static func isPlanKey(_ key: String) -> Bool {
let lower = key.lowercased()
return lower.contains("plan") || lower.contains("tier") || lower.contains("subscription")
}
private static func normalizePlanValue(_ value: String) -> String? {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
let lower = trimmed.lowercased()
let allowed = [
"free",
"plus",
"pro",
"team",
"enterprise",
"business",
"edu",
"education",
"gov",
"premium",
"essential",
]
guard allowed.contains(where: { lower.contains($0) }) else { return nil }
return CodexPlanFormatting.displayName(trimmed) ?? UsageFormatter.cleanPlanName(trimmed)
}
}
extension UInt8 {
fileprivate var isASCIIWhitespace: Bool {
switch self {
case 9, 10, 13, 32: true
default: false
}
}
}
@@ -0,0 +1,907 @@
#if os(macOS)
let openAIDashboardScrapeScript = """
(() => {
const textOf = el => {
const raw = el && (el.innerText || el.textContent) ? String(el.innerText || el.textContent) : '';
return raw.trim();
};
const parseHexColor = (color) => {
if (!color) return null;
const c = String(color).trim().toLowerCase();
if (c.startsWith('#')) {
if (c.length === 4) {
return '#' + c[1] + c[1] + c[2] + c[2] + c[3] + c[3];
}
if (c.length === 7) return c;
return c;
}
const m = c.match(/^rgba?\\(([^)]+)\\)$/);
if (m) {
const parts = m[1].split(',').map(x => parseFloat(x.trim())).filter(x => Number.isFinite(x));
if (parts.length >= 3) {
const r = Math.max(0, Math.min(255, Math.round(parts[0])));
const g = Math.max(0, Math.min(255, Math.round(parts[1])));
const b = Math.max(0, Math.min(255, Math.round(parts[2])));
const toHex = n => n.toString(16).padStart(2, '0');
return '#' + toHex(r) + toHex(g) + toHex(b);
}
}
return c;
};
const reactPropsOf = (el) => {
if (!el) return null;
try {
const keys = Object.keys(el);
const propsKey = keys.find(k => k.startsWith('__reactProps$'));
if (propsKey) return el[propsKey] || null;
const fiberKey = keys.find(k => k.startsWith('__reactFiber$'));
if (fiberKey) {
const fiber = el[fiberKey];
return (fiber && (fiber.memoizedProps || fiber.pendingProps)) || null;
}
} catch {}
return null;
};
const reactFiberOf = (el) => {
if (!el) return null;
try {
const keys = Object.keys(el);
const fiberKey = keys.find(k => k.startsWith('__reactFiber$'));
return fiberKey ? (el[fiberKey] || null) : null;
} catch {
return null;
}
};
const nestedBarMetaOf = (root) => {
if (!root || typeof root !== 'object') return null;
const queue = [root];
const seen = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
let steps = 0;
while (queue.length && steps < 250) {
const cur = queue.shift();
steps++;
if (!cur || typeof cur !== 'object') continue;
if (seen) {
if (seen.has(cur)) continue;
seen.add(cur);
}
if (cur.payload && (cur.dataKey || cur.name || cur.value !== undefined)) return cur;
const values = Array.isArray(cur) ? cur : Object.values(cur);
for (const v of values) {
if (v && typeof v === 'object') queue.push(v);
}
}
return null;
};
const barMetaFromElement = (el) => {
const direct = reactPropsOf(el);
if (direct && direct.payload && (direct.dataKey || direct.name || direct.value !== undefined)) return direct;
const fiber = reactFiberOf(el);
if (fiber) {
let cur = fiber;
for (let i = 0; i < 10 && cur; i++) {
const props = (cur.memoizedProps || cur.pendingProps) || null;
if (props && props.payload && (props.dataKey || props.name || props.value !== undefined)) return props;
const nested = props ? nestedBarMetaOf(props) : null;
if (nested) return nested;
cur = cur.return || null;
}
}
if (direct) {
const nested = nestedBarMetaOf(direct);
if (nested) return nested;
}
return null;
};
const normalizeHref = (raw) => {
if (!raw) return null;
const href = String(raw).trim();
if (!href) return null;
if (href.startsWith('http://') || href.startsWith('https://')) return href;
if (href.startsWith('//')) return window.location.protocol + href;
if (href.startsWith('/')) return window.location.origin + href;
return window.location.origin + '/' + href;
};
const isLikelyCreditsURL = (raw) => {
if (!raw) return false;
try {
const url = new URL(raw, window.location.origin);
if (!url.host || !url.host.includes('chatgpt.com')) return false;
const path = url.pathname.toLowerCase();
return (
path.includes('settings') ||
path.includes('usage') ||
path.includes('billing') ||
path.includes('credits')
);
} catch {
return false;
}
};
const purchaseTextMatches = (text) => {
const lower = String(text || '').trim().toLowerCase();
if (!lower) return false;
if (lower.includes('add more')) return true;
if (!lower.includes('credit')) return false;
return (
lower.includes('buy') ||
lower.includes('add') ||
lower.includes('purchase') ||
lower.includes('top up') ||
lower.includes('top-up')
);
};
const elementLabel = (el) => {
if (!el) return '';
return (
textOf(el) ||
el.getAttribute('aria-label') ||
el.getAttribute('title') ||
''
);
};
const urlFromProps = (props) => {
if (!props || typeof props !== 'object') return null;
const candidates = [
props.href,
props.to,
props.url,
props.link,
props.destination,
props.navigateTo
];
for (const candidate of candidates) {
if (typeof candidate === 'string' && candidate.trim()) {
return normalizeHref(candidate);
}
}
return null;
};
const purchaseURLFromElement = (el) => {
if (!el) return null;
const isAnchor = el.tagName && el.tagName.toLowerCase() === 'a';
const anchor = isAnchor ? el : (el.closest ? el.closest('a') : null);
const anchorHref = anchor ? anchor.getAttribute('href') : null;
const dataHref = el.getAttribute
? (el.getAttribute('data-href') ||
el.getAttribute('data-url') ||
el.getAttribute('data-link') ||
el.getAttribute('data-destination'))
: null;
const propHref = urlFromProps(reactPropsOf(el)) || urlFromProps(reactPropsOf(anchor));
const normalized = normalizeHref(anchorHref || dataHref || propHref);
return normalized && isLikelyCreditsURL(normalized) ? normalized : null;
};
const cleanPlanName = (raw) => String(raw || '')
.replace(/\\b(claude|codex|account|plan)\\b/gi, ' ')
.replace(/_/g, ' ')
.replace(/-/g, ' ')
.replace(/\\s+/g, ' ')
.trim();
const codexPlanDisplayName = (raw) => {
const trimmed = String(raw || '').trim();
if (!trimmed) return null;
const lower = trimmed.toLowerCase();
const exact = {
pro: 'Pro 20x',
prolite: 'Pro 5x',
'pro_lite': 'Pro 5x',
'pro-lite': 'Pro 5x',
'pro lite': 'Pro 5x'
};
if (exact[lower]) return exact[lower];
const cleaned = cleanPlanName(trimmed);
if (!cleaned) return trimmed;
if (exact[cleaned.toLowerCase()]) return exact[cleaned.toLowerCase()];
return cleaned.split(' ')
.filter(Boolean)
.map(word => {
const wordLower = word.toLowerCase();
if (wordLower === 'cbp' || wordLower === 'k12') return wordLower.toUpperCase();
if (word === word.toUpperCase() && /[a-z]/i.test(word)) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ') || cleaned;
};
const normalizePlanValue = (value) => {
const trimmed = String(value || '').trim();
if (!trimmed) return null;
const lower = trimmed.toLowerCase();
const allowed = [
'free',
'plus',
'pro',
'team',
'enterprise',
'business',
'edu',
'education',
'gov',
'premium',
'essential'
];
if (!allowed.some(token => lower.includes(token))) return null;
return codexPlanDisplayName(trimmed) || cleanPlanName(trimmed);
};
const planCandidate = (key, value) => {
const lower = String(key || '').toLowerCase();
if (!lower.includes('plan') && !lower.includes('tier') && !lower.includes('subscription')) return null;
if (typeof value === 'string') return normalizePlanValue(value);
if (value && typeof value === 'object' && !Array.isArray(value)) {
return normalizePlanValue(value.name) ||
normalizePlanValue(value.displayName) ||
normalizePlanValue(value.tier);
}
return null;
};
const findPlan = (root) => {
if (!root || typeof root !== 'object') return null;
const queue = [root];
const seenObjects = typeof WeakSet !== 'undefined' ? new WeakSet() : null;
let index = 0;
let seen = 0;
while (index < queue.length && seen < 6000) {
const cur = queue[index++];
seen++;
if (!cur || typeof cur !== 'object') continue;
if (seenObjects) {
if (seenObjects.has(cur)) continue;
seenObjects.add(cur);
}
if (Array.isArray(cur)) {
for (const v of cur) {
if (v && typeof v === 'object') queue.push(v);
}
continue;
}
for (const [k, v] of Object.entries(cur)) {
const plan = planCandidate(k, v);
if (plan) return plan;
if (v && typeof v === 'object') queue.push(v);
}
}
return null;
};
const parseJSONScript = (id) => {
try {
const node = document.getElementById(id);
const raw = node && node.textContent ? String(node.textContent) : '';
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
};
const pickLikelyPurchaseButton = (buttons) => {
if (!buttons || buttons.length === 0) return null;
const labeled = buttons.find(btn => {
const label = elementLabel(btn);
if (purchaseTextMatches(label)) return true;
const aria = String(btn.getAttribute('aria-label') || '').toLowerCase();
return aria.includes('credit') || aria.includes('buy') || aria.includes('add');
});
return labeled || buttons[0];
};
const findCreditsPurchaseButton = () => {
const nodes = Array.from(document.querySelectorAll('h1,h2,h3,div,span,p'));
const labelMatch = nodes.find(node => {
const lower = textOf(node).toLowerCase();
return lower === 'credits remaining' || (lower.includes('credits') && lower.includes('remaining'));
});
if (!labelMatch) return null;
let cur = labelMatch;
for (let i = 0; i < 6 && cur; i++) {
const buttons = Array.from(cur.querySelectorAll('button, a'));
const picked = pickLikelyPurchaseButton(buttons);
if (picked) return picked;
cur = cur.parentElement;
}
return null;
};
const dayKeyFromPayload = (payload) => {
if (!payload || typeof payload !== 'object') return null;
const localDayKeyForDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const keys = ['day', 'date', 'name', 'label', 'x', 'time', 'timestamp'];
for (const k of keys) {
const v = payload[k];
if (typeof v === 'string') {
const s = v.trim();
if (/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return s;
const iso = s.match(/^(\\d{4}-\\d{2}-\\d{2})/);
if (iso) return iso[1];
}
if (typeof v === 'number' && Number.isFinite(v) && (k === 'timestamp' || k === 'time' || k === 'x')) {
try {
const d = new Date(v);
if (!isNaN(d.getTime())) return localDayKeyForDate(d);
} catch {}
}
}
return null;
};
const isSkillUsageServiceKey = (raw) => {
const key = raw === null || raw === undefined ? '' : String(raw).trim().toLowerCase();
return key.startsWith('skillusage:');
};
const displayNameForUsageServiceKey = (raw) => {
const key = raw === null || raw === undefined ? '' : String(raw).trim();
if (!key) return key;
if (isSkillUsageServiceKey(key)) return null;
if (key.toUpperCase() === key && key.length <= 6) return key;
const lower = key.toLowerCase();
if (lower === 'cli') return 'CLI';
if (lower.includes('github') && lower.includes('review')) return 'GitHub Code Review';
const words = lower.replace(/[_-]+/g, ' ').split(' ').filter(Boolean);
return words.map(w => w.length <= 2 ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
};
const isLikelyCodexUsageService = (raw) => {
const service = raw === null || raw === undefined ? '' : String(raw).trim().toLowerCase();
return (
service === 'cli' ||
service === 'desktop' ||
service === 'desktop app' ||
service === 'vscode' ||
service === 'vs code' ||
service === 'unknown' ||
(service.includes('github') && service.includes('review'))
);
};
const usageChartRootForPath = (path) => {
if (!path || !path.closest) return null;
return (
path.closest('.recharts-wrapper') ||
path.closest('svg.recharts-surface') ||
path.closest('section') ||
path.parentElement ||
null
);
};
const uniqueUsageChartRoots = (paths) => {
const roots = [];
for (const path of paths) {
const root = usageChartRootForPath(path);
if (root && !roots.includes(root)) roots.push(root);
}
return roots;
};
const usageBreakdownTitleScore = (title) => {
const lower = String(title || '').trim().toLowerCase().replace(/\\s+/g, ' ');
if (!lower) return 0;
if (lower === 'usage breakdown') return 1000000;
if (lower.includes('usage breakdown')) return 900000;
if (lower === 'personal usage') return 800000;
if (lower.includes('threads') ||
lower.includes('turns') ||
lower.includes('client') ||
lower.includes('skill') ||
lower.includes('invocation')) return -1000000;
return 0;
};
const titleLikeElements = (scope) => {
try {
return Array.from(scope.querySelectorAll('h1,h2,h3,[role=\"heading\"],div,span,p'))
.filter(el => {
const title = textOf(el);
const lower = title.toLowerCase();
const tag = el.tagName ? el.tagName.toLowerCase() : '';
const isHeading = tag === 'h1' ||
tag === 'h2' ||
tag === 'h3' ||
String(el.getAttribute('role') || '').toLowerCase() === 'heading';
return title.length > 0 &&
title.length <= 80 &&
(
isHeading ||
usageBreakdownTitleScore(title) !== 0 ||
lower.includes('usage breakdown') ||
lower.includes('threads') ||
lower.includes('turns') ||
lower.includes('client') ||
lower.includes('skill') ||
lower.includes('invocation')
);
});
} catch {
return [];
}
};
const titleNodePrecedesRoot = (titleNode, root) => {
if (!titleNode || titleNode === root || root.contains(titleNode) || titleNode.contains(root)) return false;
const relation = titleNode.compareDocumentPosition(root);
return Boolean(relation & Node.DOCUMENT_POSITION_FOLLOWING);
};
const nearestScoredChartTitleInScope = (scope, root) => {
let best = null;
for (const titleNode of titleLikeElements(scope)) {
if (!titleNodePrecedesRoot(titleNode, root)) continue;
const title = textOf(titleNode);
const score = usageBreakdownTitleScore(title);
if (score === 0) continue;
if (!best || score >= best.score) best = { title, score };
}
return best ? best.title : '';
};
const chartTitleBoundaryForRoot = (root) => {
if (!root) return null;
try {
return root.closest('section,[role=\"region\"],article') || root.parentElement || null;
} catch {
return root.parentElement || null;
}
};
const nearestTitleTextInScope = (scope, root) => {
if (!scope) return '';
let nearest = null;
for (const titleNode of titleLikeElements(scope)) {
if (titleNodePrecedesRoot(titleNode, root)) nearest = titleNode;
}
return textOf(nearest);
};
const nearestChartTitleTextForRoot = (root) => {
if (!root) return '';
try {
const boundary = chartTitleBoundaryForRoot(root) || root.parentElement || null;
let ancestor = root.parentElement || null;
for (let i = 0; i < 8 && ancestor; i++) {
const scoredTitle = nearestScoredChartTitleInScope(ancestor, root);
if (scoredTitle) return scoredTitle;
if (ancestor === boundary) break;
ancestor = ancestor.parentElement || null;
}
return nearestTitleTextInScope(boundary, root);
} catch {
return '';
}
};
const legendMapForUsageChartRoot = (root) => {
const legendMap = {};
const scopes = [
root,
root && root.parentElement,
root && root.closest ? root.closest('section') : null
].filter(Boolean);
for (const scope of scopes) {
try {
const legendItems = Array.from(scope.querySelectorAll('div[title]'));
for (const item of legendItems) {
const title = item.getAttribute('title') ? String(item.getAttribute('title')).trim() : '';
const square = item.querySelector('div[style*=\"background-color\"]');
const color = (square && square.style && square.style.backgroundColor)
? square.style.backgroundColor
: null;
const hex = parseHexColor(color);
if (title && hex) legendMap[hex] = title;
}
} catch {}
if (Object.keys(legendMap).length > 0) break;
}
return legendMap;
};
const parseUsageBreakdownFromChartPaths = (paths, legendMap) => {
const totalsByDay = {}; // day -> service -> value
const addValue = (day, service, value) => {
if (!day || !service || isSkillUsageServiceKey(service)) return false;
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return false;
if (!totalsByDay[day]) totalsByDay[day] = {};
totalsByDay[day][service] = (totalsByDay[day][service] || 0) + value;
return true;
};
let pointCount = 0;
for (const path of paths) {
const meta = barMetaFromElement(path) || barMetaFromElement(path.parentElement) || null;
if (!meta) continue;
const payload = meta.payload || null;
const day = dayKeyFromPayload(payload);
if (!day) continue;
const valuesObj = (payload && payload.values && typeof payload.values === 'object') ? payload.values : null;
if (valuesObj) {
for (const [k, v] of Object.entries(valuesObj)) {
const service = displayNameForUsageServiceKey(k);
if (addValue(day, service, v)) pointCount++;
}
continue;
}
let value = null;
if (typeof meta.value === 'number' && Number.isFinite(meta.value)) value = meta.value;
if (value === null && typeof meta.value === 'string') {
const v = parseFloat(meta.value.replace(/,/g, ''));
if (Number.isFinite(v)) value = v;
}
if (value === null) continue;
const fill = parseHexColor(meta.fill || path.getAttribute('fill'));
const service =
(fill && legendMap[fill]) ||
(typeof meta.name === 'string' && meta.name) ||
null;
if (addValue(day, service, value)) pointCount++;
}
const dayKeys = Object.keys(totalsByDay)
.filter(day => Object.keys(totalsByDay[day] || {}).length > 0)
.sort((a, b) => b.localeCompare(a))
.slice(0, 30);
const breakdown = dayKeys.map(day => {
const servicesMap = totalsByDay[day] || {};
const services = Object.keys(servicesMap).map(service => ({
service,
creditsUsed: servicesMap[service]
})).sort((a, b) => {
if (a.creditsUsed === b.creditsUsed) return a.service.localeCompare(b.service);
return b.creditsUsed - a.creditsUsed;
});
const totalCreditsUsed = services.reduce((sum, s) => sum + (Number(s.creditsUsed) || 0), 0);
return { day, services, totalCreditsUsed };
});
const services = Array.from(new Set(breakdown.flatMap(day => day.services.map(service => service.service))));
const totalCreditsUsed = breakdown.reduce((sum, day) => sum + (Number(day.totalCreditsUsed) || 0), 0);
const likelyCodexServiceCount = services.filter(isLikelyCodexUsageService).length;
return {
breakdown,
pointCount,
services,
totalCreditsUsed,
likelyCodexServiceCount,
score: likelyCodexServiceCount * 1000 + services.length * 100 + pointCount + totalCreditsUsed / 1000
};
};
const usageBreakdownJSON = (() => {
try {
if (window.__codexbarUsageBreakdownJSON) return window.__codexbarUsageBreakdownJSON;
const paths = Array.from(document.querySelectorAll('g.recharts-bar-rectangle path.recharts-rectangle'));
let debug = {
pathCount: paths.length,
chartCount: 0,
eligibleCandidateCount: 0,
selectedCandidateTitle: null,
candidateSummaries: [],
sampleReactKeys: null,
sampleMetaKeys: null,
samplePayloadKeys: null,
sampleValuesKeys: null,
sampleDayKey: null
};
try {
const sample = paths[0] || null;
if (sample) {
const names = Object.getOwnPropertyNames(sample);
debug.sampleReactKeys = names.filter(k => k.includes('react')).slice(0, 10);
const metaSample = barMetaFromElement(sample) || barMetaFromElement(sample.parentElement) || null;
if (metaSample) {
debug.sampleMetaKeys = Object.keys(metaSample).slice(0, 12);
const payload = metaSample.payload || null;
if (payload && typeof payload === 'object') {
debug.samplePayloadKeys = Object.keys(payload).slice(0, 12);
debug.sampleDayKey = dayKeyFromPayload(payload);
const values = payload.values || null;
if (values && typeof values === 'object') {
debug.sampleValuesKeys = Object.keys(values).slice(0, 12);
}
}
}
}
} catch {}
const roots = uniqueUsageChartRoots(paths);
debug.chartCount = roots.length;
const candidates = roots.map(root => {
const chartPaths = paths.filter(path => usageChartRootForPath(path) === root);
const title = nearestChartTitleTextForRoot(root);
const titleScore = usageBreakdownTitleScore(title);
const parsed = parseUsageBreakdownFromChartPaths(chartPaths, legendMapForUsageChartRoot(root));
return {
root,
title,
titleScore,
pathCount: chartPaths.length,
...parsed,
score: titleScore + parsed.score
};
}).filter(candidate => candidate.breakdown.length > 0);
const rejectedTitleCandidates = candidates.filter(candidate => candidate.titleScore < 0);
const titledCandidates = candidates.filter(candidate => candidate.titleScore > 0);
const unknownTitleCandidates = candidates.filter(candidate => candidate.titleScore === 0);
const eligibleCandidates = titledCandidates;
eligibleCandidates.sort((a, b) => b.score - a.score);
debug.eligibleCandidateCount = eligibleCandidates.length;
debug.selectedCandidateTitle = eligibleCandidates[0] ? eligibleCandidates[0].title : null;
if (eligibleCandidates.length === 0 && candidates.length > 0) {
if (unknownTitleCandidates.length > 0) {
debug.error = 'No English usage breakdown chart title found. Candidate titles: ' +
candidates.map(candidate => candidate.title || 'Untitled chart').join(', ');
} else if (rejectedTitleCandidates.length > 0) {
debug.error = 'Only non-usage chart candidates found: ' +
rejectedTitleCandidates.map(candidate => candidate.title || 'Untitled chart').join(', ');
}
}
debug.candidateSummaries = candidates.slice(0, 6).map(candidate => ({
title: candidate.title,
titleScore: candidate.titleScore,
pathCount: candidate.pathCount,
dayCount: candidate.breakdown.length,
pointCount: candidate.pointCount,
serviceCount: candidate.services.length,
likelyCodexServiceCount: candidate.likelyCodexServiceCount,
services: candidate.services.slice(0, 8)
}));
const breakdown = eligibleCandidates[0] ? eligibleCandidates[0].breakdown : [];
const json = (breakdown.length > 0) ? JSON.stringify(breakdown) : null;
window.__codexbarUsageBreakdownJSON = json;
window.__codexbarUsageBreakdownDebug = json ? null : JSON.stringify(debug);
return json;
} catch {
return null;
}
})();
const usageBreakdownDebug = (() => {
try {
return window.__codexbarUsageBreakdownDebug || null;
} catch {
return null;
}
})();
const usageBreakdownError = (() => {
try {
if (!usageBreakdownDebug) return null;
const parsed = JSON.parse(usageBreakdownDebug);
return parsed && parsed.error ? String(parsed.error) : null;
} catch {
return null;
}
})();
const bodyText = document.body ? String(document.body.innerText || '').trim() : '';
const href = window.location ? String(window.location.href || '') : '';
const workspacePicker = bodyText.includes('Select a workspace');
const title = document.title ? String(document.title || '') : '';
const cloudflareInterstitial =
title.toLowerCase().includes('just a moment') ||
bodyText.toLowerCase().includes('checking your browser') ||
bodyText.toLowerCase().includes('cloudflare');
const authSelector = [
'input[type="email"]',
'input[type="password"]',
'input[name="username"]'
].join(', ');
const hasAuthInputs = !!document.querySelector(authSelector);
const lower = bodyText.toLowerCase();
const loginCTA =
lower.includes('sign in') ||
lower.includes('log in') ||
lower.includes('continue with google') ||
lower.includes('continue with apple') ||
lower.includes('continue with microsoft');
const loginRequired =
href.includes('/auth/') ||
href.includes('/login') ||
(hasAuthInputs && loginCTA) ||
(!hasAuthInputs && loginCTA && href.includes('chatgpt.com'));
const scrollY = (typeof window.scrollY === 'number') ? window.scrollY : 0;
const scrollHeight = document.documentElement ? (document.documentElement.scrollHeight || 0) : 0;
const viewportHeight = (typeof window.innerHeight === 'number') ? window.innerHeight : 0;
let creditsHeaderPresent = false;
let creditsHeaderInViewport = false;
let didScrollToCredits = false;
let rows = [];
try {
const looksLikeCreditsEventRow = (cells) => {
if (!cells || cells.length < 3) return false;
const first = String(cells[0] || '');
const amount = String(cells[2] || '');
return /\\d{4}|\\d{1,2}[\\/.\\-]\\d{1,2}/.test(first) && /\\d/.test(amount);
};
const allTableRows = () => Array.from(document.querySelectorAll('tbody tr')).map(tr => {
const cells = Array.from(tr.querySelectorAll('td')).map(td => textOf(td));
return cells;
}).filter(looksLikeCreditsEventRow);
const headings = Array.from(document.querySelectorAll('h1,h2,h3'));
const header = headings.find(h => textOf(h).toLowerCase() === 'credits usage history');
if (header) {
creditsHeaderPresent = true;
const rect = header.getBoundingClientRect();
creditsHeaderInViewport = rect.top >= 0 && rect.top <= viewportHeight;
// Only scrape rows from the *credits usage history* table. The page can contain other tables,
// and treating any <table> as credits history can prevent our scroll-to-load logic from running.
const container = header.closest('section') || header.parentElement || document;
const table = container.querySelector('table') || null;
const scope = table || container;
rows = Array.from(scope.querySelectorAll('tbody tr')).map(tr => {
const cells = Array.from(tr.querySelectorAll('td')).map(td => textOf(td));
return cells;
}).filter(r => r.length >= 3);
if (rows.length === 0) {
rows = allTableRows();
}
if (rows.length === 0 && !window.__codexbarDidScrollToCredits) {
window.__codexbarDidScrollToCredits = true;
// If the table is virtualized/lazy-loaded, we need to scroll to trigger rendering even if the
// header is already in view.
header.scrollIntoView({ block: 'start', inline: 'nearest' });
if (creditsHeaderInViewport) {
window.scrollBy(0, Math.max(220, viewportHeight * 0.6));
}
didScrollToCredits = true;
}
} else if (rows.length === 0 && !window.__codexbarDidScrollToCredits && scrollHeight > viewportHeight * 1.5) {
rows = allTableRows();
if (rows.length > 0) {
creditsHeaderPresent = true;
creditsHeaderInViewport = true;
}
}
if (rows.length === 0 && !window.__codexbarDidScrollToCredits && scrollHeight > viewportHeight * 1.5) {
// The credits history section often isn't part of the DOM until you scroll down. Nudge the page
// once so subsequent scrapes can find the header and rows.
window.__codexbarDidScrollToCredits = true;
window.scrollTo(0, Math.max(0, scrollHeight - viewportHeight - 40));
didScrollToCredits = true;
}
} catch {}
let creditsPurchaseURL = null;
try {
const creditsButton = findCreditsPurchaseButton();
if (creditsButton) {
const url = purchaseURLFromElement(creditsButton);
if (url) creditsPurchaseURL = url;
}
const candidates = Array.from(document.querySelectorAll('a, button'));
for (const node of candidates) {
const label = elementLabel(node);
if (!purchaseTextMatches(label)) continue;
const url = purchaseURLFromElement(node);
if (url) {
creditsPurchaseURL = url;
break;
}
}
if (!creditsPurchaseURL) {
const anchors = Array.from(document.querySelectorAll('a[href]'));
for (const anchor of anchors) {
const label = elementLabel(anchor);
const href = anchor.getAttribute('href') || '';
const hrefLooksRelevant = /credits|billing/i.test(href);
if (!hrefLooksRelevant && !purchaseTextMatches(label)) continue;
const url = normalizeHref(href);
if (url) {
creditsPurchaseURL = url;
break;
}
}
}
} catch {}
let signedInEmail = null;
let authStatus = null;
let accountPlan = null;
try {
const next = window.__NEXT_DATA__ || null;
const props = (next && next.props && next.props.pageProps) ? next.props.pageProps : null;
const userEmail = (props && props.user) ? props.user.email : null;
const sessionEmail = (props && props.session && props.session.user) ? props.session.user.email : null;
signedInEmail = userEmail || sessionEmail || null;
} catch {}
const clientBootstrap = parseJSONScript('client-bootstrap');
if (clientBootstrap) {
try {
authStatus = typeof clientBootstrap.authStatus === 'string' ? clientBootstrap.authStatus : null;
if (!signedInEmail) {
const session = clientBootstrap.session || null;
const user = (session && session.user) || clientBootstrap.user || null;
const email = user && typeof user.email === 'string' ? user.email : null;
if (email && email.includes('@')) signedInEmail = email;
}
if (!accountPlan) accountPlan = findPlan(clientBootstrap);
} catch {}
}
if (!accountPlan) {
try {
accountPlan = findPlan(window.__NEXT_DATA__ || parseJSONScript('__NEXT_DATA__'));
} catch {}
}
if (!signedInEmail) {
try {
const obj = parseJSONScript('__NEXT_DATA__');
if (obj) {
const queue = [obj];
let seen = 0;
while (queue.length && seen < 2000 && !signedInEmail) {
const cur = queue.shift();
seen++;
if (!cur) continue;
if (typeof cur === 'string') {
if (cur.includes('@')) signedInEmail = cur;
continue;
}
if (typeof cur !== 'object') continue;
for (const [k, v] of Object.entries(cur)) {
if (signedInEmail) break;
if (k === 'email' && typeof v === 'string' && v.includes('@')) {
signedInEmail = v;
break;
}
if (typeof v === 'object' && v) queue.push(v);
}
}
}
} catch {}
}
if (!signedInEmail) {
try {
const emailRe = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/ig;
const found = (bodyText.match(emailRe) || []).map(x => String(x).trim().toLowerCase());
const unique = Array.from(new Set(found));
if (unique.length === 1) {
signedInEmail = unique[0];
} else if (unique.length > 1) {
signedInEmail = unique[0];
}
} catch {}
}
if (!signedInEmail) {
// Last resort: open the account menu so the email becomes part of the DOM text.
try {
const emailRe = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/ig;
const hasMenu = Boolean(document.querySelector('[role="menu"]'));
if (!hasMenu) {
const button =
document.querySelector('button[aria-haspopup="menu"]') ||
document.querySelector('button[aria-expanded]');
if (button && !button.disabled) {
button.click();
}
}
const afterText = document.body ? String(document.body.innerText || '').trim() : '';
const found = (afterText.match(emailRe) || []).map(x => String(x).trim().toLowerCase());
const unique = Array.from(new Set(found));
if (unique.length === 1) {
signedInEmail = unique[0];
} else if (unique.length > 1) {
signedInEmail = unique[0];
}
} catch {}
}
return {
loginRequired,
workspacePicker,
cloudflareInterstitial,
href,
bodyText,
signedInEmail,
authStatus,
accountPlan,
creditsPurchaseURL,
rows,
usageBreakdownJSON,
usageBreakdownDebug,
usageBreakdownError,
scrollY,
scrollHeight,
viewportHeight,
creditsHeaderPresent,
creditsHeaderInViewport,
didScrollToCredits
};
})();
"""
#endif
@@ -0,0 +1,782 @@
#if os(macOS)
import AppKit
import Foundation
import WebKit
struct OpenAIDashboardWebViewLease {
let webView: WKWebView
let log: (String) -> Void
let setPreserveLoadedPageOnRelease: (Bool) -> Void
let release: () -> Void
}
@MainActor
final class OpenAIDashboardWebViewCache {
static let shared = OpenAIDashboardWebViewCache()
fileprivate static let log = CodexBarLog.logger(LogCategories.openAIWebview)
private final class ReleaseState {
var preserveLoadedPageOnRelease: Bool
init(preserveLoadedPageOnRelease: Bool) {
self.preserveLoadedPageOnRelease = preserveLoadedPageOnRelease
}
}
private struct AcquireOptions {
let allowTimeoutRetry: Bool
let preserveLoadedPageOnRelease: Bool
}
@MainActor
private final class NavigationCancellationState {
private weak var webView: WKWebView?
private var delegate: NavigationDelegate?
private var isCancelled = false
func install(webView: WKWebView, delegate: NavigationDelegate) {
self.webView = webView
self.delegate = delegate
if self.isCancelled {
self.cancel()
}
}
func cancel() {
self.isCancelled = true
guard let webView, let delegate else { return }
delegate.cancel()
if webView.codexNavigationDelegate === delegate {
webView.stopLoading()
webView.navigationDelegate = nil
webView.codexNavigationDelegate = nil
}
self.delegate = nil
self.webView = nil
}
}
private final class Entry {
let webView: WKWebView
let host: OffscreenWebViewHost
var lastUsedAt: Date
var isBusy: Bool
var preservedPageExpiresAt: Date?
var preservedPageExpiryWorkItem: DispatchWorkItem?
init(
webView: WKWebView,
host: OffscreenWebViewHost,
lastUsedAt: Date,
isBusy: Bool,
preservedPageExpiresAt: Date? = nil)
{
self.webView = webView
self.host = host
self.lastUsedAt = lastUsedAt
self.isBusy = isBusy
self.preservedPageExpiresAt = preservedPageExpiresAt
}
func armPreservedPage(until expiry: Date) {
self.preservedPageExpiresAt = expiry
}
func setPreservedPageExpiryWorkItem(_ workItem: DispatchWorkItem?) {
self.preservedPageExpiryWorkItem?.cancel()
self.preservedPageExpiryWorkItem = workItem
}
func clearPreservedPage() {
self.preservedPageExpiresAt = nil
self.preservedPageExpiryWorkItem?.cancel()
self.preservedPageExpiryWorkItem = nil
}
func consumePreservedPageReuseIfAvailable(now: Date) -> Bool {
guard let preservedPageExpiresAt else { return false }
self.preservedPageExpiresAt = nil
self.preservedPageExpiryWorkItem?.cancel()
self.preservedPageExpiryWorkItem = nil
return preservedPageExpiresAt > now
}
func hasExpiredPreservedPage(now: Date) -> Bool {
guard let preservedPageExpiresAt else { return false }
return preservedPageExpiresAt <= now
}
}
private var entries: [ObjectIdentifier: Entry] = [:]
/// Keep the WebView alive only long enough for immediate retries/menu reopens.
/// Long-lived hidden ChatGPT tabs still consume noticeable energy on some setups.
private let idleTimeout: TimeInterval
private var idlePruneWorkItem: DispatchWorkItem?
private var idlePruneGeneration = 0
#if DEBUG
private(set) var idlePruneDeadlineForTesting: Date?
#endif
/// Reuse the validated analytics page only for the immediate next handoff.
private let preservedPageHandoffTimeout: TimeInterval = 5
private let blankURL = URL(string: "about:blank")!
private let idlePageClearScript = """
(() => {
try {
document.documentElement.innerHTML = '';
return true;
} catch {
return false;
}
})();
"""
private let reusablePageResetScript = """
(() => {
try {
delete window.__codexbarDidScrollToCredits;
delete window.__codexbarUsageBreakdownJSON;
delete window.__codexbarUsageBreakdownDebug;
return true;
} catch {
return false;
}
})();
"""
private let preferredLanguageScript = """
(() => {
const define = (target, name, value) => {
try {
Object.defineProperty(target, name, {
get: () => value,
configurable: true
});
} catch {}
};
define(Navigator.prototype, 'language', 'en-US');
define(Navigator.prototype, 'languages', ['en-US', 'en']);
define(navigator, 'language', 'en-US');
define(navigator, 'languages', ['en-US', 'en']);
})();
"""
init(idleTimeout: TimeInterval = 60) {
self.idleTimeout = idleTimeout
}
nonisolated static func remainingNavigationTimeout(
until deadline: Date,
now: Date = Date()) throws -> TimeInterval
{
let remaining = deadline.timeIntervalSince(now)
guard remaining > 0 else { throw URLError(.timedOut) }
return remaining
}
private func releaseCachedEntry(_ entry: Entry, preserveLoadedPage: Bool) {
entry.isBusy = false
let now = Date()
entry.lastUsedAt = now
self.updatePreservedPageState(for: entry, preserveLoadedPage: preserveLoadedPage)
self.prepareCachedWebViewForIdle(
entry.webView,
host: entry.host,
preserveLoadedPage: preserveLoadedPage)
self.prune(now: now)
self.scheduleNextIdlePrune(now: now)
}
private func releaseNewEntry(_ entry: Entry, webView: WKWebView, preserveLoadedPage: Bool) {
entry.isBusy = false
let now = Date()
entry.lastUsedAt = now
self.updatePreservedPageState(for: entry, preserveLoadedPage: preserveLoadedPage)
self.prepareCachedWebViewForIdle(
webView,
host: entry.host,
preserveLoadedPage: preserveLoadedPage)
self.prune(now: now)
self.scheduleNextIdlePrune(now: now)
}
// MARK: - Testing support
#if DEBUG
/// Number of cached WebView entries (for testing).
var entryCount: Int {
self.entries.count
}
/// Check if a WebView is cached for the given data store (for testing).
func hasCachedEntry(for websiteDataStore: WKWebsiteDataStore) -> Bool {
let key = ObjectIdentifier(websiteDataStore)
return self.entries[key] != nil
}
/// Force prune with a custom "now" timestamp (for testing idle timeout).
func pruneForTesting(now: Date) {
self.prune(now: now)
}
var idleTimeoutForTesting: TimeInterval {
self.idleTimeout
}
var preservedPageHandoffTimeoutForTesting: TimeInterval {
self.preservedPageHandoffTimeout
}
func hasPreservedPageForTesting(for websiteDataStore: WKWebsiteDataStore) -> Bool {
let key = ObjectIdentifier(websiteDataStore)
return self.entries[key]?.preservedPageExpiresAt != nil
}
func markPreservedPageForTesting(
websiteDataStore: WKWebsiteDataStore,
expiresAt: Date = .init().addingTimeInterval(5))
{
let key = ObjectIdentifier(websiteDataStore)
guard let entry = self.entries[key] else { return }
entry.armPreservedPage(until: expiresAt)
self.schedulePreservedPageExpiry(for: key, entry: entry, expiresAt: expiresAt)
}
func consumePreservedPageForTesting(websiteDataStore: WKWebsiteDataStore, now: Date = Date()) -> Bool {
let key = ObjectIdentifier(websiteDataStore)
guard let entry = self.entries[key] else { return false }
return entry.consumePreservedPageReuseIfAvailable(now: now)
}
/// Seed a cached entry without navigating a real page (for test stability).
@discardableResult
func cacheEntryForTesting(
websiteDataStore: WKWebsiteDataStore,
lastUsedAt: Date = Date(),
isBusy: Bool = false) -> WKWebView
{
let key = ObjectIdentifier(websiteDataStore)
if let existing = self.entries.removeValue(forKey: key) {
existing.host.close()
}
let (webView, host) = self.makeWebView(websiteDataStore: websiteDataStore)
let entry = Entry(webView: webView, host: host, lastUsedAt: lastUsedAt, isBusy: isBusy)
self.entries[key] = entry
if isBusy {
host.show()
} else {
host.hide()
}
return webView
}
/// Clear all cached entries (for test isolation).
func clearAllForTesting() {
self.cancelIdlePrune()
for (_, entry) in self.entries {
entry.clearPreservedPage()
entry.host.close()
}
self.entries.removeAll()
}
func resetReusablePageStateForTesting(_ webView: WKWebView) async -> Bool {
await self.resetReusablePageState(webView)
}
#endif
func acquire(
websiteDataStore: WKWebsiteDataStore,
usageURL: URL,
logger: ((String) -> Void)?,
navigationTimeout: TimeInterval = 15,
allowTimeoutRetry: Bool = true,
preserveLoadedPageOnRelease: Bool = false) async throws -> OpenAIDashboardWebViewLease
{
let deadline = Date().addingTimeInterval(max(navigationTimeout, 0.01))
return try await self.acquire(
websiteDataStore: websiteDataStore,
usageURL: usageURL,
logger: logger,
deadline: deadline,
options: .init(
allowTimeoutRetry: allowTimeoutRetry,
preserveLoadedPageOnRelease: preserveLoadedPageOnRelease))
}
private func acquire(
websiteDataStore: WKWebsiteDataStore,
usageURL: URL,
logger: ((String) -> Void)?,
deadline: Date,
options: AcquireOptions) async throws -> OpenAIDashboardWebViewLease
{
let now = Date()
self.prune(now: now)
let log: (String) -> Void = { message in
logger?("[webview] \(message)")
}
let key = ObjectIdentifier(websiteDataStore)
let remainingTimeout = try Self.remainingNavigationTimeout(until: deadline, now: now)
if let entry = self.entries[key] {
if entry.isBusy {
log("Cached WebView busy; using a temporary WebView.")
let (webView, host) = self.makeWebView(websiteDataStore: websiteDataStore)
host.show()
do {
try await self.prepareWebView(
webView,
usageURL: usageURL,
timeout: remainingTimeout,
canReuseLoadedPage: false)
} catch {
if options.allowTimeoutRetry, Self.isPrepareTimeout(error) {
host.close()
log("Temporary OpenAI WebView timed out; retrying with a fresh WebView.")
return try await self.acquireTemporaryWebView(
websiteDataStore: websiteDataStore,
usageURL: usageURL,
log: log,
deadline: deadline)
}
host.close()
throw error
}
return OpenAIDashboardWebViewLease(
webView: webView,
log: log,
setPreserveLoadedPageOnRelease: { _ in },
release: { host.close() })
}
entry.isBusy = true
entry.lastUsedAt = now
let canReuseLoadedPage = entry.consumePreservedPageReuseIfAvailable(now: now)
let releaseState = ReleaseState(preserveLoadedPageOnRelease: options.preserveLoadedPageOnRelease)
entry.host.show()
do {
try await self.prepareWebView(
entry.webView,
usageURL: usageURL,
timeout: remainingTimeout,
canReuseLoadedPage: canReuseLoadedPage)
} catch {
if options.allowTimeoutRetry, Self.isPrepareTimeout(error) {
entry.isBusy = false
entry.lastUsedAt = Date()
entry.clearPreservedPage()
entry.host.close()
self.entries.removeValue(forKey: key)
log("Cached OpenAI WebView timed out; recreating it.")
return try await self.acquire(
websiteDataStore: websiteDataStore,
usageURL: usageURL,
logger: logger,
deadline: deadline,
options: .init(
allowTimeoutRetry: false,
preserveLoadedPageOnRelease: options.preserveLoadedPageOnRelease))
}
entry.isBusy = false
entry.lastUsedAt = Date()
entry.clearPreservedPage()
entry.host.close()
self.entries.removeValue(forKey: key)
Self.log.warning("OpenAI webview prepare failed")
throw error
}
return OpenAIDashboardWebViewLease(
webView: entry.webView,
log: log,
setPreserveLoadedPageOnRelease: { preserveLoadedPageOnRelease in
releaseState.preserveLoadedPageOnRelease = preserveLoadedPageOnRelease
},
release: { [weak self, weak entry] in
guard let self, let entry else { return }
self.releaseCachedEntry(
entry,
preserveLoadedPage: releaseState.preserveLoadedPageOnRelease)
})
}
let (webView, host) = self.makeWebView(websiteDataStore: websiteDataStore)
let entry = Entry(webView: webView, host: host, lastUsedAt: now, isBusy: true)
self.entries[key] = entry
host.show()
let releaseState = ReleaseState(preserveLoadedPageOnRelease: options.preserveLoadedPageOnRelease)
do {
try await self.prepareWebView(
webView,
usageURL: usageURL,
timeout: remainingTimeout,
canReuseLoadedPage: false)
} catch {
if options.allowTimeoutRetry, Self.isPrepareTimeout(error) {
self.entries.removeValue(forKey: key)
host.close()
log("OpenAI WebView timed out during prepare; retrying once.")
return try await self.acquire(
websiteDataStore: websiteDataStore,
usageURL: usageURL,
logger: logger,
deadline: deadline,
options: .init(
allowTimeoutRetry: false,
preserveLoadedPageOnRelease: options.preserveLoadedPageOnRelease))
}
self.entries.removeValue(forKey: key)
host.close()
Self.log.warning("OpenAI webview prepare failed")
throw error
}
return OpenAIDashboardWebViewLease(
webView: webView,
log: log,
setPreserveLoadedPageOnRelease: { preserveLoadedPageOnRelease in
releaseState.preserveLoadedPageOnRelease = preserveLoadedPageOnRelease
},
release: { [weak self, weak entry] in
guard let self, let entry else { return }
self.releaseNewEntry(
entry,
webView: webView,
preserveLoadedPage: releaseState.preserveLoadedPageOnRelease)
})
}
func evict(websiteDataStore: WKWebsiteDataStore) {
let key = ObjectIdentifier(websiteDataStore)
guard let entry = self.entries.removeValue(forKey: key) else { return }
entry.clearPreservedPage()
Self.log.debug("OpenAI webview evicted")
entry.host.close()
self.scheduleNextIdlePrune()
}
func evictAll() {
self.cancelIdlePrune()
let existing = self.entries
self.entries.removeAll()
for (_, entry) in existing {
entry.clearPreservedPage()
entry.host.close()
}
if !existing.isEmpty {
Self.log.debug("OpenAI webview evicted all")
}
}
func evictIdle() {
let idleEntries = self.entries.filter { _, entry in
!entry.isBusy
}
guard !idleEntries.isEmpty else { return }
for (key, entry) in idleEntries {
entry.clearPreservedPage()
entry.host.close()
self.entries.removeValue(forKey: key)
}
Self.log.debug("OpenAI idle webviews evicted", metadata: ["count": "\(idleEntries.count)"])
self.scheduleNextIdlePrune()
}
private func prepareCachedWebViewForIdle(
_ webView: WKWebView,
host: OffscreenWebViewHost,
preserveLoadedPage: Bool)
{
webView.navigationDelegate = nil
webView.codexNavigationDelegate = nil
if preserveLoadedPage {
host.hide()
return
}
// Detach the heavyweight ChatGPT SPA as soon as a scrape completes. Keeping the WebView object around
// still helps with immediate reuse, but letting chatgpt.com remain the active document is too expensive.
webView.stopLoading()
webView.evaluateJavaScript(self.idlePageClearScript, completionHandler: nil)
_ = webView.load(URLRequest(url: self.blankURL))
host.hide()
}
/// Schedule against the oldest idle entry so later releases cannot postpone its eviction.
private func scheduleNextIdlePrune(now: Date = Date()) {
self.cancelIdlePrune()
guard let nextExpiry = self.entries.values
.filter({ !$0.isBusy })
.map({ $0.lastUsedAt.addingTimeInterval(self.idleTimeout) })
.min()
else { return }
let generation = self.idlePruneGeneration
let workItem = DispatchWorkItem { [weak self] in
MainActor.assumeIsolated {
guard let self, self.idlePruneGeneration == generation else { return }
self.idlePruneWorkItem = nil
#if DEBUG
self.idlePruneDeadlineForTesting = nil
#endif
let pruneTime = Date()
self.prune(now: pruneTime)
self.scheduleNextIdlePrune(now: pruneTime)
}
}
self.idlePruneWorkItem = workItem
#if DEBUG
self.idlePruneDeadlineForTesting = nextExpiry
#endif
let delay = max(0, nextExpiry.timeIntervalSince(now)) + 0.01
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
private func cancelIdlePrune() {
self.idlePruneGeneration &+= 1
self.idlePruneWorkItem?.cancel()
self.idlePruneWorkItem = nil
#if DEBUG
self.idlePruneDeadlineForTesting = nil
#endif
}
private func prune(now: Date) {
for entry in self.entries.values where !entry.isBusy && entry.hasExpiredPreservedPage(now: now) {
entry.clearPreservedPage()
self.prepareCachedWebViewForIdle(
entry.webView,
host: entry.host,
preserveLoadedPage: false)
Self.log.debug("OpenAI webview preserved page expired")
}
let expired = self.entries.filter { _, entry in
!entry.isBusy && now.timeIntervalSince(entry.lastUsedAt) >= self.idleTimeout
}
for (key, entry) in expired {
entry.host.close()
self.entries.removeValue(forKey: key)
Self.log.debug("OpenAI webview pruned")
}
}
private func makeWebView(websiteDataStore: WKWebsiteDataStore) -> (WKWebView, OffscreenWebViewHost) {
let config = WKWebViewConfiguration()
config.websiteDataStore = websiteDataStore
let userContentController = WKUserContentController()
userContentController.addUserScript(WKUserScript(
source: self.preferredLanguageScript,
injectionTime: .atDocumentStart,
forMainFrameOnly: false))
config.userContentController = userContentController
if #available(macOS 14.0, *) {
config.preferences.inactiveSchedulingPolicy = .suspend
}
let webView = WKWebView(frame: .zero, configuration: config)
let host = OffscreenWebViewHost(webView: webView)
return (webView, host)
}
private func prepareWebView(
_ webView: WKWebView,
usageURL: URL,
timeout: TimeInterval,
canReuseLoadedPage: Bool) async throws
{
#if DEBUG
if usageURL.absoluteString == "about:blank" {
_ = webView.loadHTMLString("", baseURL: nil)
return
}
#endif
if canReuseLoadedPage,
let currentURL = webView.url?.absoluteString,
OpenAIDashboardFetcher.isUsageRoute(currentURL)
{
if await self.resetReusablePageState(webView) {
return
}
Self.log.debug("OpenAI preserved page reset failed; reloading usage URL")
}
try Task.checkCancellation()
let cancellationState = NavigationCancellationState()
try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let delegate = NavigationDelegate { result in
cont.resume(with: result)
}
webView.navigationDelegate = delegate
webView.codexNavigationDelegate = delegate
cancellationState.install(webView: webView, delegate: delegate)
delegate.armTimeout(seconds: timeout)
_ = webView.load(OpenAIDashboardFetcher.usageURLRequest(url: usageURL))
if Task.isCancelled {
cancellationState.cancel()
}
}
} onCancel: {
Task { @MainActor in
cancellationState.cancel()
}
}
}
private func acquireTemporaryWebView(
websiteDataStore: WKWebsiteDataStore,
usageURL: URL,
log: @escaping (String) -> Void,
deadline: Date) async throws -> OpenAIDashboardWebViewLease
{
let remainingTimeout = try Self.remainingNavigationTimeout(until: deadline)
let (webView, host) = self.makeWebView(websiteDataStore: websiteDataStore)
host.show()
do {
try await self.prepareWebView(
webView,
usageURL: usageURL,
timeout: remainingTimeout,
canReuseLoadedPage: false)
} catch {
host.close()
throw error
}
return OpenAIDashboardWebViewLease(
webView: webView,
log: log,
setPreserveLoadedPageOnRelease: { _ in },
release: { host.close() })
}
private static func isPrepareTimeout(_ error: Error) -> Bool {
let nsError = error as NSError
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
}
private func updatePreservedPageState(for entry: Entry, preserveLoadedPage: Bool) {
if preserveLoadedPage {
let expiresAt = Date().addingTimeInterval(self.preservedPageHandoffTimeout)
entry.armPreservedPage(until: expiresAt)
if let key = self.entries.first(where: { $0.value === entry })?.key {
self.schedulePreservedPageExpiry(for: key, entry: entry, expiresAt: expiresAt)
}
} else {
entry.clearPreservedPage()
}
}
private func schedulePreservedPageExpiry(
for key: ObjectIdentifier,
entry: Entry,
expiresAt: Date)
{
let delay = max(0, expiresAt.timeIntervalSinceNow)
let workItem = DispatchWorkItem { [weak self] in
MainActor.assumeIsolated {
self?.expirePreservedPageIfNeeded(for: key, expectedExpiry: expiresAt)
}
}
entry.setPreservedPageExpiryWorkItem(workItem)
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
private func expirePreservedPageIfNeeded(for key: ObjectIdentifier, expectedExpiry: Date) {
guard let entry = self.entries[key],
!entry.isBusy,
let preservedPageExpiresAt = entry.preservedPageExpiresAt,
preservedPageExpiresAt == expectedExpiry,
preservedPageExpiresAt <= Date()
else {
return
}
entry.clearPreservedPage()
self.prepareCachedWebViewForIdle(
entry.webView,
host: entry.host,
preserveLoadedPage: false)
Self.log.debug("OpenAI webview preserved page expired")
self.prune(now: Date())
}
private func resetReusablePageState(_ webView: WKWebView) async -> Bool {
do {
let any = try await webView.evaluateJavaScript(self.reusablePageResetScript)
return (any as? Bool) ?? true
} catch {
return false
}
}
}
@MainActor
private final class OffscreenWebViewHost {
private let window: NSWindow
private weak var webView: WKWebView?
init(webView: WKWebView) {
// WebKit throttles timers/RAF aggressively when a WKWebView is not considered "visible".
// The Codex usage page uses streaming SSR + client hydration; if RAF is throttled, the
// dashboard never becomes part of the visible DOM and `document.body.innerText` stays tiny.
//
// Keep a transparent (mouse-ignoring) window technically "on-screen" while scraping, but
// place it almost entirely off-screen so we never ghost-render dashboard UI over the desktop.
let visibleFrame = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 900, height: 700)
let frame = OpenAIDashboardFetcher.offscreenHostWindowFrame(for: visibleFrame)
let window = NSWindow(
contentRect: frame,
styleMask: [.borderless],
backing: .buffered,
defer: false)
window.isReleasedWhenClosed = false
window.backgroundColor = .clear
window.isOpaque = false
// Keep it effectively invisible, but non-zero alpha so WebKit treats it as "visible" and doesn't
// stall hydration (we've observed a head-only HTML shell for minutes at alpha=0).
window.alphaValue = OpenAIDashboardFetcher.offscreenHostAlphaValue()
window.hasShadow = false
window.ignoresMouseEvents = true
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.isExcludedFromWindowsMenu = true
window.contentView = webView
self.window = window
self.webView = webView
}
func show() {
OpenAIDashboardWebViewCache.log.debug("OpenAI webview show")
self.window.alphaValue = OpenAIDashboardFetcher.offscreenHostAlphaValue()
self.window.orderFrontRegardless()
}
func hide() {
// Set alpha to 0 so WebKit recognizes the page as inactive and applies
// its scheduling policy (throttle/suspend), reducing CPU when idle.
OpenAIDashboardWebViewCache.log.debug("OpenAI webview hide")
self.window.alphaValue = 0.0
self.window.orderOut(nil)
}
func close() {
OpenAIDashboardWebViewCache.log.debug("OpenAI webview close")
WebKitTeardown.scheduleCleanup(
owner: self,
window: self.window,
webView: self.webView,
closeWindow: { [window] in
window.orderOut(nil)
window.close()
})
}
}
#endif
@@ -0,0 +1,118 @@
#if os(macOS)
import CryptoKit
import Foundation
import WebKit
/// Per-account persistent `WKWebsiteDataStore` for the OpenAI dashboard scrape.
///
/// Why: `WKWebsiteDataStore.default()` is a single shared cookie jar. If the user switches Codex accounts,
/// we want to keep multiple signed-in dashboard sessions around (one per email) without clearing cookies.
///
/// Implementation detail: macOS 14+ supports `WKWebsiteDataStore.dataStore(forIdentifier:)`, which creates
/// persistent isolated stores keyed by an identifier. We derive a stable UUID from the email and optional
/// Codex source scope so distinct profiles with the same email never share a cookie store.
///
/// Important: We cache the `WKWebsiteDataStore` instances so the same object is returned for the same
/// account email. This ensures `OpenAIDashboardWebViewCache` can use object identity for cache lookups.
@MainActor
public enum OpenAIDashboardWebsiteDataStore {
/// Cached data store instances keyed by normalized email and optional Codex source scope.
/// Using the same instance ensures stable object identity for WebView cache lookups.
private static var cachedStores: [String: WKWebsiteDataStore] = [:]
public static func store(
forAccountEmail email: String?,
scope: CookieHeaderCache.Scope? = nil) -> WKWebsiteDataStore
{
guard let normalized = normalizeEmail(email) else { return .default() }
let storageKey = self.storageKey(normalizedEmail: normalized, scope: scope)
// Return cached instance if available to maintain stable object identity
if let cached = cachedStores[storageKey] {
return cached
}
let id = Self.identifier(forStorageKey: storageKey)
let store = WKWebsiteDataStore(forIdentifier: id)
self.cachedStores[storageKey] = store
return store
}
/// Clears the persistent cookie store for a single account email.
///
/// Note: this does *not* impact other accounts, and is safe to use when the stored session is "stuck"
/// or signed in to a different account than expected.
public static func clearStore(
forAccountEmail email: String?,
scope: CookieHeaderCache.Scope? = nil) async
{
// Clear only ChatGPT/OpenAI domain data for the per-account store.
// Avoid deleting the entire persistent store (WebKit requires all WKWebViews using it to be released).
let store = self.store(forAccountEmail: email, scope: scope)
await withCheckedContinuation { cont in
store.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
let filtered = records.filter { record in
let name = record.displayName.lowercased()
return name.contains("chatgpt.com") || name.contains("openai.com")
}
store.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: filtered) {
cont.resume()
}
}
}
// Remove from cache so a fresh instance is created on next access
if let normalized = normalizeEmail(email) {
self.cachedStores.removeValue(forKey: self.storageKey(normalizedEmail: normalized, scope: scope))
}
}
#if DEBUG
/// Clear all cached store instances (for test isolation).
public static func clearCacheForTesting() {
self.cachedStores.removeAll()
}
#endif
// MARK: - Private
private static func normalizeEmail(_ email: String?) -> String? {
guard let raw = email?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil }
return raw.lowercased()
}
private static func storageKey(normalizedEmail: String, scope: CookieHeaderCache.Scope?) -> String {
guard let scope else { return normalizedEmail }
return "\(normalizedEmail)|\(scope.isolationIdentifier)"
}
private static func identifier(forStorageKey storageKey: String) -> UUID {
let digest = SHA256.hash(data: Data(storageKey.utf8))
var bytes = Array(digest.prefix(16))
// Make it a well-formed UUID (v4 + RFC4122 variant) while staying deterministic.
bytes[6] = (bytes[6] & 0x0F) | 0x40
bytes[8] = (bytes[8] & 0x3F) | 0x80
let uuidBytes: uuid_t = (
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes[4],
bytes[5],
bytes[6],
bytes[7],
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15])
return UUID(uuid: uuidBytes)
}
}
#endif