Memoji character (11 expressions) on the engine's motion; Claude limit shows time to reset; ChatGPT weekly limit from Codex logs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 22:16:53 +03:00
parent 8a8dd98e06
commit fbf90fd4fb
23 changed files with 358 additions and 89 deletions
+109
View File
@@ -0,0 +1,109 @@
import Foundation
// MARK: - ChatGPT (Codex) plan limits
//
// The ChatGPT/Codex app writes its plan limits into every session log
// (~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl) as
// "rate_limits":{"primary":{"used_percent":23.0,"window_minutes":10080,"resets_at":1791634947},"secondary":…}
// We read the newest logs once a minute and keep the weekly window (10080 min).
// Nothing is sent anywhere; the numbers are only as fresh as the last Codex turn.
enum CodexUsage {
static let weekMinutes = 10080
/// Weekly window from the most recent `rate_limits` found in the newest session logs.
static func readWeekly(home: URL = FileManager.default.homeDirectoryForCurrentUser) -> PlanWindow? {
let root = home.appendingPathComponent(".codex/sessions")
for file in newestLogs(in: root, limit: 4) {
if let w = lastWeekly(in: file) { return w }
}
return nil
}
/// Newest *.jsonl files under the date folders, newest first.
static func newestLogs(in root: URL, limit: Int) -> [URL] {
let fm = FileManager.default
guard let e = fm.enumerator(at: root, includingPropertiesForKeys: [.contentModificationDateKey],
options: [.skipsHiddenFiles]) else { return [] }
var files: [(URL, Date)] = []
for case let url as URL in e where url.pathExtension == "jsonl" {
let d = (try? url.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
files.append((url, d))
}
return files.sorted { $0.1 > $1.1 }.prefix(limit).map(\.0)
}
/// Reads the tail of a log and returns the weekly window of its last `rate_limits`.
static func lastWeekly(in file: URL) -> PlanWindow? {
guard let h = try? FileHandle(forReadingFrom: file) else { return nil }
defer { try? h.close() }
let size = (try? h.seekToEnd()) ?? 0
let chunk: UInt64 = 512 * 1024
try? h.seek(toOffset: size > chunk ? size - chunk : 0)
guard let data = try? h.readToEnd(), let text = String(data: data, encoding: .utf8) else { return nil }
for line in text.split(separator: "\n").reversed() where line.contains("\"rate_limits\"") {
if let w = parseLine(String(line)) { return w }
}
return nil
}
static func parseLine(_ line: String) -> PlanWindow? {
guard let obj = try? JSONSerialization.jsonObject(with: Data(line.utf8)),
let limits = findRateLimits(obj) else { return nil }
let windows = [limits["primary"], limits["secondary"]].compactMap { $0 as? [String: Any] }
let weekly = windows.first { ($0["window_minutes"] as? NSNumber)?.intValue == weekMinutes }
guard let w = weekly,
let pct = (w["used_percent"] as? NSNumber)?.doubleValue,
let resets = (w["resets_at"] as? NSNumber)?.doubleValue, resets > 0 else { return nil }
return PlanWindow(usedPct: min(100, max(0, pct)), resetsAt: Date(timeIntervalSince1970: resets))
}
/// `rate_limits` can sit at any depth of the event (payload.info…), so search for it.
private static func findRateLimits(_ obj: Any) -> [String: Any]? {
if let d = obj as? [String: Any] {
if let r = d["rate_limits"] as? [String: Any] { return r }
for v in d.values { if let r = findRateLimits(v) { return r } }
} else if let a = obj as? [Any] {
for v in a { if let r = findRateLimits(v) { return r } }
}
return nil
}
}
@MainActor
final class CodexUsageMonitor {
static let shared = CodexUsageMonitor()
private var task: Task<Void, Never>?
func start() {
guard task == nil else { return }
task = Task {
while !Task.isCancelled {
let w = await Task.detached(priority: .utility) { CodexUsage.readWeekly() }.value
AppState.shared.codexWeekly = w
try? await Task.sleep(for: .seconds(60))
}
}
}
}
// MARK: - Reset time wording
enum PlanTime {
/// "3 ч 20 мин", "2 д 5 ч", "12 мин", "<1 мин".
static func untilReset(_ date: Date, now: Date = Date()) -> String {
let s = max(0, Int(date.timeIntervalSince(now)))
let d = s / 86400, h = (s % 86400) / 3600, m = (s % 3600) / 60
if d > 0 { return h > 0 ? "\(d) д \(h) ч" : "\(d) д" }
if h > 0 { return m > 0 ? "\(h) ч \(m) мин" : "\(h) ч" }
return m > 0 ? "\(m) мин" : "<1 мин"
}
/// "пт 14:00" within a week, "12 окт 14:00" beyond.
static func resetMoment(_ date: Date, now: Date = Date()) -> String {
let f = DateFormatter()
f.locale = Locale(identifier: "ru_RU")
f.dateFormat = date.timeIntervalSince(now) < 6 * 86400 ? "EE HH:mm" : "d MMM HH:mm"
return f.string(from: date).replacingOccurrences(of: ".", with: "")
}
}