diff --git a/ClaudeChecker/ClaudeCheckerApp.swift b/ClaudeChecker/ClaudeCheckerApp.swift
index 41702f1..45ae5d9 100644
--- a/ClaudeChecker/ClaudeCheckerApp.swift
+++ b/ClaudeChecker/ClaudeCheckerApp.swift
@@ -1,6 +1,7 @@
import SwiftUI
import WebKit
import Combine
+import UserNotifications
@main
struct ClaudeCheckerApp: App {
@@ -11,7 +12,7 @@ struct ClaudeCheckerApp: App {
}
@MainActor
-class AppDelegate: NSObject, NSApplicationDelegate {
+class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
var statusItem: NSStatusItem?
var popover: NSPopover?
var usageViewModel = UsageViewModel()
@@ -23,6 +24,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
+ let center = UNUserNotificationCenter.current()
+ center.delegate = self
+ center.requestAuthorization(options: [.alert, .sound]) { _, _ in }
+
// Hidden WKWebView to prime the shared cookie store
let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default()
@@ -205,4 +210,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillTerminate(_ notification: Notification) {
refreshTimer?.invalidate()
}
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler handler: @escaping (UNNotificationPresentationOptions) -> Void) {
+ handler([.banner, .sound])
+ }
}
diff --git a/ClaudeChecker/Info.plist b/ClaudeChecker/Info.plist
index 47315f2..84b7dd2 100644
--- a/ClaudeChecker/Info.plist
+++ b/ClaudeChecker/Info.plist
@@ -15,9 +15,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.0.8
+ 2.0.9
CFBundleVersion
- 15
+ 16
LSMinimumSystemVersion
13.0
LSUIElement
diff --git a/ClaudeChecker/Models.swift b/ClaudeChecker/Models.swift
index cd016f1..465fdc9 100644
--- a/ClaudeChecker/Models.swift
+++ b/ClaudeChecker/Models.swift
@@ -31,6 +31,12 @@ enum WindowType: String {
case fiveHour = "5h"
case sevenDay = "7d"
var label: String { rawValue }
+ var displayName: String {
+ switch self {
+ case .fiveHour: return "5-hour"
+ case .sevenDay: return "7-day"
+ }
+ }
}
enum ProjectedHit {
diff --git a/ClaudeChecker/UsageViewModel.swift b/ClaudeChecker/UsageViewModel.swift
index e20ce9b..db93e80 100644
--- a/ClaudeChecker/UsageViewModel.swift
+++ b/ClaudeChecker/UsageViewModel.swift
@@ -1,5 +1,6 @@
import SwiftUI
import WebKit
+import UserNotifications
@MainActor
class UsageViewModel: ObservableObject {
@@ -29,6 +30,8 @@ class UsageViewModel: ObservableObject {
private var burnHistoryStore: [String: [Double]] = [:]
private let maxHistorySamples = 24
private var cachedOrgId: String?
+ private var previousPercents: [String: Double] = [:]
+ private var firedThresholds: [String: Set] = [:]
init() {
cachedOrgId = UserDefaults.standard.string(forKey: "claude_org_id") ?? Self.orgId
@@ -84,6 +87,7 @@ class UsageViewModel: ObservableObject {
burnHistoryStore[key] = history
if history.count > 1 { limits[i].burnHistory = history }
}
+ checkLimitNotifications(for: limits)
} catch AppError.notAuthenticated {
isNotAuthenticated = true
errorMessage = "Not signed in"
@@ -234,6 +238,50 @@ class UsageViewModel: ObservableObject {
return result
}
+ // MARK: - Limit notifications
+
+ private func checkLimitNotifications(for limits: [AgentLimit]) {
+ let center = UNUserNotificationCenter.current()
+ let thresholds = [80, 95]
+
+ for limit in limits {
+ let key = limit.window.rawValue
+ let curr = limit.usedPercent
+ let prev = previousPercents[key]
+ var fired = firedThresholds[key] ?? []
+
+ // Threshold warnings (80% and 95%)
+ for t in thresholds {
+ let threshold = Double(t)
+ guard curr >= threshold, prev ?? threshold < threshold, !fired.contains(t) else { continue }
+ fired.insert(t)
+ let content = UNMutableNotificationContent()
+ content.title = "Claude \(limit.window.displayName) limit"
+ content.body = t == 95
+ ? "\(Int(curr.rounded()))% used — almost at your limit"
+ : "\(Int(curr.rounded()))% used — approaching your limit"
+ content.sound = .default
+ center.add(UNNotificationRequest(identifier: "cc_limit_\(key)_\(t)", content: content, trigger: nil))
+ }
+
+ // Reset detection: was high, now low
+ if let prev, prev > 50, curr < 20 {
+ fired.removeAll()
+ let content = UNMutableNotificationContent()
+ content.title = "Claude \(limit.window.displayName) limit reset"
+ content.body = "Your \(limit.window.displayName) quota has been reset"
+ content.sound = .default
+ center.add(UNNotificationRequest(
+ identifier: "cc_reset_\(key)_\(Int(Date().timeIntervalSince1970))",
+ content: content, trigger: nil
+ ))
+ }
+
+ firedThresholds[key] = fired
+ previousPercents[key] = curr
+ }
+ }
+
private func loadPlaceholderData() {
let now = Date()
limits = [
diff --git a/README.md b/README.md
index 04ce071..fe3130e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
[](https://www.apple.com/macos/)
[](https://swift.org)
-[](https://github.com/superdooper86/claudechecker/releases)
+[](https://github.com/superdooper86/claudechecker/releases)
[](LICENSE)
diff --git a/version.json b/version.json
index 34a17ff..f4dc77d 100644
--- a/version.json
+++ b/version.json
@@ -1,5 +1,5 @@
{
- "version": "2.0.8",
- "url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.8/ClaudeChecker.zip",
- "notes": "Updated data sources list in settings."
+ "version": "2.0.9",
+ "url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.9/ClaudeChecker.zip",
+ "notes": "Added limit notifications: warns at 80% and 95% usage, and notifies when a limit resets."
}