diff --git a/Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json b/Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Claude Monitor Widget/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Claude Monitor Widget/Assets.xcassets/Contents.json b/Claude Monitor Widget/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Claude Monitor Widget/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Claude Monitor Widget/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Claude Monitor Widget/Claude_Monitor_Widget.entitlements b/Claude Monitor Widget/Claude_Monitor_Widget.entitlements
new file mode 100644
index 0000000..ee95ab7
--- /dev/null
+++ b/Claude Monitor Widget/Claude_Monitor_Widget.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+
+
diff --git a/Claude Monitor Widget/Claude_Monitor_Widget.swift b/Claude Monitor Widget/Claude_Monitor_Widget.swift
new file mode 100644
index 0000000..9430c07
--- /dev/null
+++ b/Claude Monitor Widget/Claude_Monitor_Widget.swift
@@ -0,0 +1,22 @@
+//
+// Claude_Monitor_Widget.swift
+// Claude Monitor Widget
+//
+
+import SwiftUI
+import WidgetKit
+
+/// The Claude Monitor widget displaying API usage.
+struct Claude_Monitor_Widget: Widget {
+ let kind: String = "Claude_Monitor_Widget"
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: kind, provider: UsageTimelineProvider()) { entry in
+ UsageWidgetView(entry: entry)
+ .containerBackground(.fill.tertiary, for: .widget)
+ }
+ .configurationDisplayName("Claude Usage")
+ .description("Monitor your Claude API usage limits.")
+ .supportedFamilies([.systemMedium])
+ }
+}
diff --git a/Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift b/Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift
new file mode 100644
index 0000000..0a520d6
--- /dev/null
+++ b/Claude Monitor Widget/Claude_Monitor_WidgetBundle.swift
@@ -0,0 +1,14 @@
+//
+// Claude_Monitor_WidgetBundle.swift
+// Claude Monitor Widget
+//
+
+import SwiftUI
+import WidgetKit
+
+@main
+struct Claude_Monitor_WidgetBundle: WidgetBundle {
+ var body: some Widget {
+ Claude_Monitor_Widget()
+ }
+}
diff --git a/Claude Monitor Widget/Info.plist b/Claude Monitor Widget/Info.plist
new file mode 100644
index 0000000..0f118fb
--- /dev/null
+++ b/Claude Monitor Widget/Info.plist
@@ -0,0 +1,11 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/Claude Monitor Widget/Models/UsageLimit.swift b/Claude Monitor Widget/Models/UsageLimit.swift
new file mode 100644
index 0000000..b354753
--- /dev/null
+++ b/Claude Monitor Widget/Models/UsageLimit.swift
@@ -0,0 +1,21 @@
+//
+// UsageLimit.swift
+// Claude Monitor Widget
+//
+
+import Foundation
+
+/// A usage limit representing a specific quota bucket from the Claude API.
+struct UsageLimit: Identifiable {
+ /// A unique identifier for this limit, typically matching the API field name.
+ let id: String
+
+ /// A localized display title for the limit (e.g., "Current session", "All models").
+ let title: String
+
+ /// The utilization percentage as a value between 0.0 and 1.0.
+ let utilization: Double
+
+ /// The date when this limit will reset, or `nil` if unknown.
+ let resetsAt: Date?
+}
diff --git a/Claude Monitor Widget/Models/UsageResponse.swift b/Claude Monitor Widget/Models/UsageResponse.swift
new file mode 100644
index 0000000..5cea5b5
--- /dev/null
+++ b/Claude Monitor Widget/Models/UsageResponse.swift
@@ -0,0 +1,46 @@
+//
+// UsageResponse.swift
+// Claude Monitor Widget
+//
+
+import Foundation
+
+/// The raw JSON response from the Anthropic OAuth usage API endpoint.
+struct UsageResponse: Codable {
+ /// The 5-hour rolling session limit.
+ let fiveHour: UsageBucket?
+
+ /// The 7-day combined usage limit across all models.
+ let sevenDay: UsageBucket?
+
+ /// The 7-day usage limit for OAuth applications.
+ let sevenDayOauthApps: UsageBucket?
+
+ /// The 7-day usage limit specifically for Claude Opus.
+ let sevenDayOpus: UsageBucket?
+
+ /// The 7-day usage limit specifically for Claude Sonnet.
+ let sevenDaySonnet: UsageBucket?
+
+ enum CodingKeys: String, CodingKey {
+ case fiveHour = "five_hour"
+ case sevenDay = "seven_day"
+ case sevenDayOauthApps = "seven_day_oauth_apps"
+ case sevenDayOpus = "seven_day_opus"
+ case sevenDaySonnet = "seven_day_sonnet"
+ }
+}
+
+/// A single usage bucket from the API response.
+struct UsageBucket: Codable {
+ /// The current utilization as a percentage (0-100).
+ let utilization: Double
+
+ /// The ISO 8601 timestamp when this limit will reset, or `nil` if not applicable.
+ let resetsAt: String?
+
+ enum CodingKeys: String, CodingKey {
+ case utilization
+ case resetsAt = "resets_at"
+ }
+}
diff --git a/Claude Monitor Widget/RefreshIntent.swift b/Claude Monitor Widget/RefreshIntent.swift
new file mode 100644
index 0000000..eb10d12
--- /dev/null
+++ b/Claude Monitor Widget/RefreshIntent.swift
@@ -0,0 +1,19 @@
+//
+// RefreshIntent.swift
+// Claude Monitor Widget
+//
+
+import AppIntents
+import WidgetKit
+
+/// An App Intent that refreshes the widget's data.
+struct RefreshIntent: AppIntent {
+ static let title: LocalizedStringResource = "Refresh Usage"
+ static let description: IntentDescription = "Refreshes the Claude usage data"
+
+ func perform() async throws -> some IntentResult {
+ // Trigger a timeline refresh
+ WidgetCenter.shared.reloadAllTimelines()
+ return .result()
+ }
+}
diff --git a/Claude Monitor Widget/Services/ClaudeAPIService.swift b/Claude Monitor Widget/Services/ClaudeAPIService.swift
new file mode 100644
index 0000000..b8be390
--- /dev/null
+++ b/Claude Monitor Widget/Services/ClaudeAPIService.swift
@@ -0,0 +1,66 @@
+//
+// ClaudeAPIService.swift
+// Claude Monitor Widget
+//
+
+import Foundation
+
+/// Errors that can occur when communicating with the Claude API.
+enum APIError: Error, LocalizedError {
+ case noToken
+ case invalidURL
+ case httpError(statusCode: Int, message: String?)
+ case decodingError(Error)
+ case networkError(Error)
+
+ var errorDescription: String? {
+ "Could not access the Claude API."
+ }
+}
+
+/// A service for communicating with the Anthropic Claude API.
+final class ClaudeAPIService: Sendable {
+ static let shared = ClaudeAPIService()
+
+ private let baseURL = "https://api.anthropic.com"
+ private let usagePath = "/api/oauth/usage"
+
+ private init() {}
+
+ /// Fetches the current usage data from the Claude API.
+ func fetchUsage(token: String) async throws -> UsageResponse {
+ guard let url = URL(string: baseURL + usagePath) else {
+ throw APIError.invalidURL
+ }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ request.setValue("oauth-2025-04-20", forHTTPHeaderField: "anthropic-beta")
+ request.setValue("claude-code/2.0.32", forHTTPHeaderField: "User-Agent")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+
+ do {
+ let (data, response) = try await URLSession.shared.data(for: request)
+
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw APIError.networkError(NSError(domain: "Invalid response", code: -1))
+ }
+
+ guard (200...299).contains(httpResponse.statusCode) else {
+ let message = String(data: data, encoding: .utf8)
+ throw APIError.httpError(statusCode: httpResponse.statusCode, message: message)
+ }
+
+ do {
+ return try JSONDecoder().decode(UsageResponse.self, from: data)
+ } catch {
+ throw APIError.decodingError(error)
+ }
+ } catch let error as APIError {
+ throw error
+ } catch {
+ throw APIError.networkError(error)
+ }
+ }
+}
diff --git a/Claude Monitor Widget/Services/KeychainService.swift b/Claude Monitor Widget/Services/KeychainService.swift
new file mode 100644
index 0000000..b4f9c42
--- /dev/null
+++ b/Claude Monitor Widget/Services/KeychainService.swift
@@ -0,0 +1,131 @@
+//
+// KeychainService.swift
+// Claude Monitor Widget
+//
+
+import Foundation
+import Security
+
+/// Errors that can occur when accessing the macOS Keychain.
+enum KeychainError: Error {
+ case itemNotFound
+ case unexpectedStatus(OSStatus)
+ case invalidData
+}
+
+/// The source of an OAuth token.
+enum TokenSource: String {
+ case claudeCode
+ case manual
+}
+
+/// A service for secure token retrieval using the macOS Keychain.
+/// This widget version only reads tokens; it cannot write them.
+final class KeychainService: Sendable {
+ static let shared = KeychainService()
+
+ private let appServiceName = "codes.tim.Claude-Monitor"
+ private let appAccountName = "api-token"
+ private let claudeCodeServiceName = "Claude Code-credentials"
+
+ private init() {}
+
+ /// Reads the OAuth token from Claude Code's Keychain entry.
+ func readClaudeCodeToken() throws -> String {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: claudeCodeServiceName,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne
+ ]
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ if status == errSecItemNotFound {
+ throw KeychainError.itemNotFound
+ }
+ throw KeychainError.unexpectedStatus(status)
+ }
+
+ guard let data = result as? Data,
+ let jsonString = String(data: data, encoding: .utf8)
+ else {
+ throw KeychainError.invalidData
+ }
+
+ return try parseClaudeCodeCredentials(jsonString)
+ }
+
+ private func parseClaudeCodeCredentials(_ json: String) throws -> String {
+ guard let data = json.data(using: .utf8) else {
+ throw KeychainError.invalidData
+ }
+
+ struct Credentials: Codable {
+ let claudeAiOauth: OAuth
+
+ struct OAuth: Codable {
+ let accessToken: String
+ }
+ }
+
+ let credentials = try JSONDecoder().decode(Credentials.self, from: data)
+ return credentials.claudeAiOauth.accessToken
+ }
+
+ /// Reads the manually-entered token from the app's Keychain entry.
+ /// Note: Without keychain access groups, this may not work in widget context.
+ func readManualToken() throws -> String {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: appServiceName,
+ kSecAttrAccount as String: appAccountName,
+ kSecReturnData as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne
+ ]
+
+ var result: AnyObject?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ if status == errSecItemNotFound {
+ throw KeychainError.itemNotFound
+ }
+ throw KeychainError.unexpectedStatus(status)
+ }
+
+ guard let data = result as? Data,
+ let token = String(data: data, encoding: .utf8)
+ else {
+ throw KeychainError.invalidData
+ }
+
+ return token
+ }
+
+ /// Resolves the best available token.
+ func resolveToken(preferredSource: TokenSource) -> (token: String, source: TokenSource)? {
+ switch preferredSource {
+ case .claudeCode:
+ if let token = try? readClaudeCodeToken() {
+ return (token, .claudeCode)
+ }
+ case .manual:
+ if let token = try? readManualToken() {
+ return (token, .manual)
+ }
+ }
+
+ // Fallback: try the other source
+ if let token = try? readClaudeCodeToken() {
+ return (token, .claudeCode)
+ }
+ if let token = try? readManualToken() {
+ return (token, .manual)
+ }
+
+ return nil
+ }
+}
diff --git a/Claude Monitor Widget/UsageEntry.swift b/Claude Monitor Widget/UsageEntry.swift
new file mode 100644
index 0000000..8c1e2bf
--- /dev/null
+++ b/Claude Monitor Widget/UsageEntry.swift
@@ -0,0 +1,62 @@
+//
+// UsageEntry.swift
+// Claude Monitor Widget
+//
+
+import WidgetKit
+
+/// A timeline entry containing usage data for the widget.
+struct UsageEntry: TimelineEntry {
+ /// The date for this entry.
+ let date: Date
+
+ /// The usage limits to display.
+ let limits: [UsageLimit]
+
+ /// The session (5-hour) utilization for the gauge, or nil if unavailable.
+ let sessionUtilization: Double?
+
+ /// An error message if data could not be fetched.
+ let error: String?
+
+ /// Whether this is placeholder data.
+ let isPlaceholder: Bool
+
+ init(
+ date: Date,
+ limits: [UsageLimit],
+ sessionUtilization: Double?,
+ error: String? = nil,
+ isPlaceholder: Bool = false
+ ) {
+ self.date = date
+ self.limits = limits
+ self.sessionUtilization = sessionUtilization
+ self.error = error
+ self.isPlaceholder = isPlaceholder
+ }
+
+ /// A placeholder entry for the widget gallery.
+ static var placeholder: UsageEntry {
+ UsageEntry(
+ date: Date(),
+ limits: [
+ UsageLimit(id: "session", title: "Session", utilization: 0.45, resetsAt: Date().addingTimeInterval(3600 * 3)),
+ UsageLimit(id: "all", title: "All", utilization: 0.32, resetsAt: Date().addingTimeInterval(3600 * 48)),
+ UsageLimit(id: "opus", title: "Opus", utilization: 0.12, resetsAt: Date().addingTimeInterval(3600 * 48))
+ ],
+ sessionUtilization: 0.45,
+ isPlaceholder: true
+ )
+ }
+
+ /// An error entry when data cannot be fetched.
+ static func error(_ message: String) -> UsageEntry {
+ UsageEntry(
+ date: Date(),
+ limits: [],
+ sessionUtilization: nil,
+ error: message
+ )
+ }
+}
diff --git a/Claude Monitor Widget/UsageTimelineProvider.swift b/Claude Monitor Widget/UsageTimelineProvider.swift
new file mode 100644
index 0000000..633837a
--- /dev/null
+++ b/Claude Monitor Widget/UsageTimelineProvider.swift
@@ -0,0 +1,131 @@
+//
+// UsageTimelineProvider.swift
+// Claude Monitor Widget
+//
+
+import WidgetKit
+
+/// Provides timeline entries for the Claude Monitor widget.
+struct UsageTimelineProvider: TimelineProvider {
+ private let apiService = ClaudeAPIService.shared
+ private let keychainService = KeychainService.shared
+
+ /// The user's preferred token source.
+ /// Note: Without App Groups, we default to Claude Code token.
+ /// To enable preference sharing, configure App Groups in Developer Portal.
+ private var preferredTokenSource: TokenSource {
+ // App Groups not configured - default to Claude Code
+ return .claudeCode
+ }
+
+ func placeholder(in context: Context) -> UsageEntry {
+ .placeholder
+ }
+
+ func getSnapshot(in context: Context, completion: @escaping @Sendable (UsageEntry) -> Void) {
+ if context.isPreview {
+ completion(.placeholder)
+ return
+ }
+
+ Task { @MainActor in
+ let entry = await fetchUsageEntry()
+ completion(entry)
+ }
+ }
+
+ func getTimeline(in context: Context, completion: @escaping @Sendable (Timeline) -> Void) {
+ Task { @MainActor in
+ let entry = await fetchUsageEntry()
+
+ // Refresh in 15 minutes
+ let nextUpdate = Date().addingTimeInterval(15 * 60)
+ let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
+
+ completion(timeline)
+ }
+ }
+
+ /// Fetches usage data and creates a timeline entry.
+ @MainActor
+ private func fetchUsageEntry() async -> UsageEntry {
+ // Get token
+ guard let (token, _) = keychainService.resolveToken(preferredSource: preferredTokenSource) else {
+ return .error("No API token configured")
+ }
+
+ // Fetch usage
+ do {
+ let response = try await apiService.fetchUsage(token: token)
+ let limits = parseUsageLimits(from: response)
+ let sessionUtilization = response.fiveHour.map { $0.utilization / 100.0 }
+
+ return UsageEntry(
+ date: Date(),
+ limits: limits,
+ sessionUtilization: sessionUtilization
+ )
+ } catch {
+ return .error("Failed to fetch usage")
+ }
+ }
+
+ /// Parses the API response into UsageLimit objects.
+ private func parseUsageLimits(from response: UsageResponse) -> [UsageLimit] {
+ var limits: [UsageLimit] = []
+
+ if let bucket = response.fiveHour {
+ limits.append(UsageLimit(
+ id: "five_hour",
+ title: "Session",
+ utilization: bucket.utilization / 100.0,
+ resetsAt: parseDate(bucket.resetsAt)
+ ))
+ }
+
+ if let bucket = response.sevenDay {
+ limits.append(UsageLimit(
+ id: "seven_day",
+ title: "All",
+ utilization: bucket.utilization / 100.0,
+ resetsAt: parseDate(bucket.resetsAt)
+ ))
+ }
+
+ if let bucket = response.sevenDayOpus, bucket.utilization > 0 {
+ limits.append(UsageLimit(
+ id: "seven_day_opus",
+ title: "Opus",
+ utilization: bucket.utilization / 100.0,
+ resetsAt: parseDate(bucket.resetsAt)
+ ))
+ }
+
+ if let bucket = response.sevenDaySonnet, bucket.utilization > 0 {
+ limits.append(UsageLimit(
+ id: "seven_day_sonnet",
+ title: "Sonnet",
+ utilization: bucket.utilization / 100.0,
+ resetsAt: parseDate(bucket.resetsAt)
+ ))
+ }
+
+ return limits
+ }
+
+ /// Parses an ISO 8601 date string.
+ private func parseDate(_ string: String?) -> Date? {
+ guard let string else { return nil }
+
+ let formatter = ISO8601DateFormatter()
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+
+ if let date = formatter.date(from: string) {
+ return date
+ }
+
+ // Try without fractional seconds
+ formatter.formatOptions = [.withInternetDateTime]
+ return formatter.date(from: string)
+ }
+}
diff --git a/Claude Monitor Widget/Views/GaugeView.swift b/Claude Monitor Widget/Views/GaugeView.swift
new file mode 100644
index 0000000..58d0a55
--- /dev/null
+++ b/Claude Monitor Widget/Views/GaugeView.swift
@@ -0,0 +1,143 @@
+//
+// GaugeView.swift
+// Claude Monitor Widget
+//
+
+import SwiftUI
+
+/// A circular gauge view showing utilization with the Claude logo in the center.
+struct GaugeView: View {
+ let utilization: Double
+ let size: CGFloat
+
+ private var clampedUtilization: Double {
+ max(0, min(1, utilization))
+ }
+
+ var body: some View {
+ ZStack {
+ // Background track (270° arc, open at bottom)
+ Circle()
+ .trim(from: 0, to: 0.75)
+ .stroke(
+ Color.secondary.opacity(0.2),
+ style: StrokeStyle(lineWidth: size * 0.08, lineCap: .round)
+ )
+ .rotationEffect(.degrees(135))
+
+ // Filled arc based on utilization
+ Circle()
+ .trim(from: 0, to: 0.75 * clampedUtilization)
+ .stroke(
+ gaugeColor,
+ style: StrokeStyle(lineWidth: size * 0.08, lineCap: .round)
+ )
+ .rotationEffect(.degrees(135))
+
+ // Claude logo in center
+ ClaudeLogoView()
+ .frame(width: size * 0.55, height: size * 0.55)
+
+ // Percentage below
+ VStack {
+ Spacer()
+ Text("\(Int(clampedUtilization * 100))%")
+ .font(.system(size: size * 0.15, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ }
+ .padding(.bottom, size * 0.05)
+ }
+ .frame(width: size, height: size)
+ }
+
+ private var gaugeColor: Color {
+ switch clampedUtilization {
+ case 0..<0.5: .blue
+ case 0.5..<0.8: .yellow
+ case 0.8..<0.95: .orange
+ default: .red
+ }
+ }
+}
+
+/// The Claude "C" logo as a SwiftUI shape.
+struct ClaudeLogoView: View {
+ var body: some View {
+ GeometryReader { geometry in
+ let scale = min(geometry.size.width, geometry.size.height) / 24
+
+ Path { path in
+ // Simplified Claude logo path (star shape)
+ let points: [(Double, Double)] = [
+ (7.55, 14.25), (10.11, 12.81), (10.15, 12.69), (10.11, 12.62),
+ (9.98, 12.62), (9.56, 12.59), (8.09, 12.56), (6.83, 12.50),
+ (5.60, 12.44), (5.29, 12.37), (5.0, 11.99), (5.03, 11.80),
+ (5.29, 11.62), (5.66, 11.66), (6.48, 11.71), (7.72, 11.80),
+ (8.61, 11.85), (9.94, 11.99), (10.15, 11.99), (10.18, 11.90),
+ (10.11, 11.85), (10.05, 11.80), (8.78, 10.93), (7.39, 10.02),
+ (6.67, 9.49), (6.28, 9.23), (6.08, 8.98), (5.99, 8.43),
+ (6.35, 8.04), (6.83, 8.07), (6.95, 8.10), (7.43, 8.48),
+ (8.47, 9.27), (9.82, 10.27), (10.01, 10.43), (10.09, 10.38),
+ (10.10, 10.34), (10.01, 10.19), (9.28, 8.86), (8.50, 7.51),
+ (8.15, 6.96), (8.06, 6.62), (8.0, 6.23), (8.40, 5.68),
+ (8.63, 5.60), (9.17, 5.68), (9.40, 5.87), (9.73, 6.64),
+ (10.27, 7.85), (11.12, 9.49), (11.36, 9.98), (11.50, 10.43),
+ (11.54, 10.56), (11.63, 10.56), (11.63, 10.49), (11.70, 9.56),
+ (11.83, 8.43), (11.95, 6.96), (11.99, 6.55), (12.20, 6.06),
+ (12.60, 5.79), (12.92, 5.94), (13.18, 6.32), (13.14, 6.56),
+ (12.99, 7.56), (12.69, 9.13), (12.49, 10.19), (12.60, 10.19),
+ (12.73, 10.05), (13.27, 9.35), (14.16, 8.23), (14.56, 7.78),
+ (15.02, 7.29), (15.31, 7.06), (15.87, 7.06), (16.29, 7.67),
+ (16.10, 8.30), (15.53, 9.03), (15.05, 9.65), (14.36, 10.57),
+ (13.94, 11.31), (13.98, 11.37), (14.08, 11.36), (15.62, 11.03),
+ (16.46, 10.88), (17.46, 10.71), (17.91, 10.92), (17.96, 11.13),
+ (17.78, 11.57), (16.71, 11.83), (15.46, 12.08), (13.60, 12.52),
+ (13.58, 12.54), (13.60, 12.57), (14.44, 12.65), (14.80, 12.67),
+ (15.68, 12.67), (17.32, 12.79), (17.74, 13.08), (18.0, 13.42),
+ (17.96, 13.69), (17.30, 14.02), (16.41, 13.81), (14.34, 13.32),
+ (13.63, 13.14), (13.53, 13.14), (13.53, 13.20), (14.12, 13.78),
+ (15.21, 14.76), (16.57, 16.02), (16.63, 16.33), (16.46, 16.58),
+ (16.28, 16.55), (15.08, 15.65), (14.62, 15.25), (13.58, 14.37),
+ (13.51, 14.37), (13.51, 14.46), (13.75, 14.82), (15.02, 16.72),
+ (15.08, 17.31), (14.99, 17.50), (14.66, 17.61), (14.30, 17.55),
+ (13.56, 16.51), (12.79, 15.33), (12.17, 14.28), (12.10, 14.32),
+ (11.73, 18.25), (11.56, 18.45), (11.16, 18.60), (10.84, 18.35),
+ (10.66, 17.95), (10.84, 17.15), (11.05, 16.11), (11.22, 15.28),
+ (11.37, 14.25), (11.46, 13.91), (11.46, 13.88), (11.38, 13.89),
+ (10.61, 14.96), (9.42, 16.55), (8.49, 17.55), (8.26, 17.64),
+ (7.88, 17.44), (7.91, 17.08), (8.13, 16.76), (9.42, 15.12),
+ (10.20, 14.10), (10.71, 13.51), (10.70, 13.43), (10.67, 13.43),
+ (7.24, 15.66), (6.63, 15.74), (6.36, 15.49), (6.39, 15.08),
+ (6.52, 14.95), (7.55, 14.24)
+ ]
+
+ let centerX = geometry.size.width / 2
+ let centerY = geometry.size.height / 2
+ let offsetX = 11.5 * scale
+ let offsetY = 12.1 * scale
+
+ for (index, point) in points.enumerated() {
+ let x = centerX + (CGFloat(point.0) * scale - offsetX)
+ let y = centerY - (CGFloat(point.1) * scale - offsetY)
+
+ if index == 0 {
+ path.move(to: CGPoint(x: x, y: y))
+ } else {
+ path.addLine(to: CGPoint(x: x, y: y))
+ }
+ }
+ path.closeSubpath()
+ }
+ .fill(Color.primary)
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 20) {
+ GaugeView(utilization: 0.25, size: 80)
+ GaugeView(utilization: 0.67, size: 80)
+ GaugeView(utilization: 0.92, size: 80)
+ }
+ .padding()
+}
diff --git a/Claude Monitor Widget/Views/ShadedProgressView.swift b/Claude Monitor Widget/Views/ShadedProgressView.swift
new file mode 100644
index 0000000..56809c0
--- /dev/null
+++ b/Claude Monitor Widget/Views/ShadedProgressView.swift
@@ -0,0 +1,31 @@
+//
+// ShadedProgressView.swift
+// Claude Monitor Widget
+//
+
+import SwiftUI
+
+/// A custom progress bar with a solid fill and rounded corners.
+struct ShadedProgressView: View {
+ let value: Double
+ let tint: Color
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack(alignment: .leading) {
+ RoundedRectangle(cornerRadius: 3)
+ .fill(Color.secondary.opacity(0.2))
+
+ RoundedRectangle(cornerRadius: 3)
+ .fill(tint)
+ .frame(width: geometry.size.width * min(max(value, 0), 1))
+ }
+ }
+ .frame(height: 6)
+ }
+
+ init(value: Double, tint: Color = .accentColor) {
+ self.value = value
+ self.tint = tint
+ }
+}
diff --git a/Claude Monitor Widget/Views/UsageWidgetView.swift b/Claude Monitor Widget/Views/UsageWidgetView.swift
new file mode 100644
index 0000000..2b0d83d
--- /dev/null
+++ b/Claude Monitor Widget/Views/UsageWidgetView.swift
@@ -0,0 +1,106 @@
+//
+// UsageWidgetView.swift
+// Claude Monitor Widget
+//
+
+import SwiftUI
+import WidgetKit
+import AppIntents
+
+/// The main widget view for the medium size widget.
+struct UsageWidgetView: View {
+ let entry: UsageEntry
+
+ var body: some View {
+ if let error = entry.error {
+ errorView(message: error)
+ } else if entry.limits.isEmpty {
+ emptyView
+ } else {
+ contentView
+ }
+ }
+
+ private var contentView: some View {
+ Button(intent: RefreshIntent()) {
+ HStack(spacing: 28) {
+ // Gauge on the left
+ GaugeView(
+ utilization: entry.sessionUtilization ?? 0,
+ size: 70
+ )
+ .padding(.leading, 4)
+
+ // Limits on the right
+ VStack(alignment: .leading, spacing: 6) {
+ Spacer(minLength: 4)
+
+ ForEach(entry.limits.prefix(3)) { limit in
+ WidgetLimitRow(limit: limit)
+ }
+
+ Spacer(minLength: 8)
+ }
+ .padding(.trailing, 4)
+ }
+ .padding(.vertical, 8)
+ .padding(.horizontal, 4)
+ }
+ .buttonStyle(.plain)
+ }
+
+ private func errorView(message: String) -> some View {
+ VStack(spacing: 8) {
+ Image(systemName: "exclamationmark.triangle")
+ .font(.title2)
+ .foregroundStyle(.secondary)
+
+ Text(message)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+
+ Button(intent: RefreshIntent()) {
+ Text("Retry")
+ .font(.caption)
+ }
+ .buttonStyle(.plain)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+
+ private var emptyView: some View {
+ VStack(spacing: 8) {
+ Image(systemName: "chart.bar")
+ .font(.title2)
+ .foregroundStyle(.secondary)
+
+ Text("No usage data")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+
+ Button(intent: RefreshIntent()) {
+ Text("Refresh")
+ .font(.caption)
+ }
+ .buttonStyle(.plain)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+}
+
+#Preview(as: .systemMedium) {
+ Claude_Monitor_Widget()
+} timeline: {
+ UsageEntry.placeholder
+ UsageEntry(
+ date: Date(),
+ limits: [
+ UsageLimit(id: "session", title: "Session", utilization: 0.67, resetsAt: Date().addingTimeInterval(3600 * 3)),
+ UsageLimit(id: "all", title: "All", utilization: 0.34, resetsAt: Date().addingTimeInterval(3600 * 48)),
+ UsageLimit(id: "opus", title: "Opus", utilization: 0.15, resetsAt: Date().addingTimeInterval(3600 * 48))
+ ],
+ sessionUtilization: 0.67
+ )
+ UsageEntry.error("No API token configured")
+}
diff --git a/Claude Monitor Widget/Views/WidgetLimitRow.swift b/Claude Monitor Widget/Views/WidgetLimitRow.swift
new file mode 100644
index 0000000..7a3822a
--- /dev/null
+++ b/Claude Monitor Widget/Views/WidgetLimitRow.swift
@@ -0,0 +1,90 @@
+//
+// WidgetLimitRow.swift
+// Claude Monitor Widget
+//
+
+import SwiftUI
+
+/// A compact row displaying a single usage limit for the widget.
+struct WidgetLimitRow: View {
+ let limit: UsageLimit
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 2) {
+ HStack {
+ Text(limit.title)
+ .font(.system(size: 11, weight: .medium))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+
+ Spacer()
+
+ Text(limit.utilization, format: .percent.precision(.fractionLength(0)))
+ .font(.system(size: 11, weight: .regular, design: .monospaced))
+ .foregroundStyle(.secondary)
+
+ if let resetsAt = limit.resetsAt {
+ Text(resetTimeString(for: resetsAt))
+ .font(.system(size: 10))
+ .foregroundStyle(.tertiary)
+ }
+ }
+
+ ShadedProgressView(value: limit.utilization, tint: progressColor)
+ }
+ .frame(maxWidth: 150)
+ }
+
+ private var progressColor: Color {
+ switch limit.utilization {
+ case 0..<0.5: .blue
+ case 0.5..<0.8: .yellow
+ case 0.8..<0.95: .orange
+ default: .red
+ }
+ }
+
+ private func resetTimeString(for date: Date) -> String {
+ let now = Date()
+ let interval = date.timeIntervalSince(now)
+
+ guard interval > 0 else { return "" }
+
+ let hours = Int(interval) / 3600
+ let days = hours / 24
+
+ if days > 0 {
+ return "in \(days)d"
+ } else if hours > 0 {
+ return "in \(hours)h"
+ } else {
+ let minutes = Int(interval) / 60
+ return "in \(max(1, minutes))m"
+ }
+ }
+}
+
+#Preview {
+ VStack(spacing: 8) {
+ WidgetLimitRow(limit: UsageLimit(
+ id: "session",
+ title: "Session",
+ utilization: 0.67,
+ resetsAt: Date().addingTimeInterval(3600 * 3)
+ ))
+ WidgetLimitRow(limit: UsageLimit(
+ id: "all",
+ title: "All",
+ utilization: 0.34,
+ resetsAt: Date().addingTimeInterval(3600 * 48)
+ ))
+ WidgetLimitRow(limit: UsageLimit(
+ id: "opus",
+ title: "Opus",
+ utilization: 0.15,
+ resetsAt: Date().addingTimeInterval(3600 * 48)
+ ))
+ }
+ .padding()
+ .frame(width: 200)
+}
diff --git a/Claude Monitor.xcodeproj/project.pbxproj b/Claude Monitor.xcodeproj/project.pbxproj
index 09b89e7..1a4bf1c 100644
--- a/Claude Monitor.xcodeproj/project.pbxproj
+++ b/Claude Monitor.xcodeproj/project.pbxproj
@@ -9,6 +9,9 @@
/* Begin PBXBuildFile section */
01024E652EE1FDF0002CC293 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 01024E642EE1FDF0002CC293 /* Sentry */; };
01F78B9E2EE1739B006D357C /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 01F78B9D2EE1739B006D357C /* Logging */; };
+ 01WIDGET012EE30000000001 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01WIDGET002EE30000000001 /* WidgetKit.framework */; };
+ 01WIDGET022EE30000000001 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01WIDGET002EE30000000002 /* SwiftUI.framework */; };
+ 01WIDGET032EE30000000001 /* Claude Monitor Widget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -26,12 +29,34 @@
remoteGlobalIDString = 0160FC542EE0CEC1006E6A22;
remoteInfo = "Claude Monitor";
};
+ 01WIDGET042EE30000000001 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 0160FC4D2EE0CEC1006E6A22 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 01WIDGET102EE30000000001;
+ remoteInfo = "Claude Monitor Widget";
+ };
/* End PBXContainerItemProxy section */
+/* Begin PBXCopyFilesBuildPhase section */
+ 01WIDGET062EE30000000001 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ dstPath = "";
+ dstSubfolder = PlugIns;
+ files = (
+ 01WIDGET032EE30000000001 /* Claude Monitor Widget.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ };
+/* End PBXCopyFilesBuildPhase section */
+
/* Begin PBXFileReference section */
0160FC552EE0CEC1006E6A22 /* Claude Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Claude Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
0160FC622EE0CEC3006E6A22 /* Claude MonitorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Claude MonitorTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
0160FC6C2EE0CEC3006E6A22 /* Claude MonitorUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Claude MonitorUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 01WIDGET002EE30000000001 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
+ 01WIDGET002EE30000000002 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
+ 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Claude Monitor Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -50,6 +75,11 @@
path = "Claude MonitorUITests";
sourceTree = "";
};
+ 01WIDGET052EE30000000001 /* Claude Monitor Widget */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = "Claude Monitor Widget";
+ sourceTree = "";
+ };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -70,6 +100,13 @@
files = (
);
};
+ 01WIDGET072EE30000000001 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ files = (
+ 01WIDGET012EE30000000001 /* WidgetKit.framework in Frameworks */,
+ 01WIDGET022EE30000000001 /* SwiftUI.framework in Frameworks */,
+ );
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -77,8 +114,10 @@
isa = PBXGroup;
children = (
0160FC572EE0CEC1006E6A22 /* Claude Monitor */,
+ 01WIDGET052EE30000000001 /* Claude Monitor Widget */,
0160FC652EE0CEC3006E6A22 /* Claude MonitorTests */,
0160FC6F2EE0CEC3006E6A22 /* Claude MonitorUITests */,
+ 01WIDGET082EE30000000001 /* Frameworks */,
0160FC562EE0CEC1006E6A22 /* Products */,
);
sourceTree = "";
@@ -89,10 +128,20 @@
0160FC552EE0CEC1006E6A22 /* Claude Monitor.app */,
0160FC622EE0CEC3006E6A22 /* Claude MonitorTests.xctest */,
0160FC6C2EE0CEC3006E6A22 /* Claude MonitorUITests.xctest */,
+ 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */,
);
name = Products;
sourceTree = "";
};
+ 01WIDGET082EE30000000001 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 01WIDGET002EE30000000001 /* WidgetKit.framework */,
+ 01WIDGET002EE30000000002 /* SwiftUI.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -103,9 +152,13 @@
0160FC512EE0CEC1006E6A22 /* Sources */,
0160FC522EE0CEC1006E6A22 /* Frameworks */,
0160FC532EE0CEC1006E6A22 /* Resources */,
+ 01WIDGET062EE30000000001 /* Embed Foundation Extensions */,
);
buildRules = (
);
+ dependencies = (
+ 01WIDGET052EE30000000002 /* PBXTargetDependency */,
+ );
fileSystemSynchronizedGroups = (
0160FC572EE0CEC1006E6A22 /* Claude Monitor */,
);
@@ -160,6 +213,24 @@
productReference = 0160FC6C2EE0CEC3006E6A22 /* Claude MonitorUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
+ 01WIDGET102EE30000000001 /* Claude Monitor Widget */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = B3BB623B2EE2ABC1003188A1 /* Build configuration list for PBXNativeTarget "Claude Monitor Widget" */;
+ buildPhases = (
+ 01WIDGET092EE30000000001 /* Sources */,
+ 01WIDGET072EE30000000001 /* Frameworks */,
+ 01WIDGET0A2EE30000000001 /* Resources */,
+ );
+ buildRules = (
+ );
+ fileSystemSynchronizedGroups = (
+ 01WIDGET052EE30000000001 /* Claude Monitor Widget */,
+ );
+ name = "Claude Monitor Widget";
+ productName = "Claude Monitor Widget";
+ productReference = 01WIDGET002EE30000000003 /* Claude Monitor Widget.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -181,6 +252,9 @@
CreatedOnToolsVersion = 26.1.1;
TestTargetID = 0160FC542EE0CEC1006E6A22;
};
+ 01WIDGET102EE30000000001 = {
+ CreatedOnToolsVersion = 26.1.1;
+ };
};
};
buildConfigurationList = 0160FC502EE0CEC1006E6A22 /* Build configuration list for PBXProject "Claude Monitor" */;
@@ -202,6 +276,7 @@
projectRoot = "";
targets = (
0160FC542EE0CEC1006E6A22 /* Claude Monitor */,
+ 01WIDGET102EE30000000001 /* Claude Monitor Widget */,
0160FC612EE0CEC3006E6A22 /* Claude MonitorTests */,
0160FC6B2EE0CEC3006E6A22 /* Claude MonitorUITests */,
);
@@ -224,6 +299,11 @@
files = (
);
};
+ 01WIDGET0A2EE30000000001 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ files = (
+ );
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -242,6 +322,11 @@
files = (
);
};
+ 01WIDGET092EE30000000001 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ files = (
+ );
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -255,6 +340,11 @@
target = 0160FC542EE0CEC1006E6A22 /* Claude Monitor */;
targetProxy = 0160FC6D2EE0CEC3006E6A22 /* PBXContainerItemProxy */;
};
+ 01WIDGET052EE30000000002 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 01WIDGET102EE30000000001 /* Claude Monitor Widget */;
+ targetProxy = 01WIDGET042EE30000000001 /* PBXContainerItemProxy */;
+ };
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
@@ -385,10 +475,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
+ CODE_SIGN_ENTITLEMENTS = "Claude Monitor/Claude_Monitor.entitlements";
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2NFSK2WB24;
+ DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
@@ -413,9 +505,9 @@
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-Monitor";
+ PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor";
PRODUCT_NAME = "$(TARGET_NAME)";
- REGISTER_APP_GROUPS = YES;
+ PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -428,10 +520,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_STYLE = Automatic;
+ CODE_SIGN_ENTITLEMENTS = "Claude Monitor/Claude_Monitor.entitlements";
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2NFSK2WB24;
+ DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
@@ -456,9 +550,9 @@
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-Monitor";
+ PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor";
PRODUCT_NAME = "$(TARGET_NAME)";
- REGISTER_APP_GROUPS = YES;
+ PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -472,7 +566,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2NFSK2WB24;
+ DEVELOPMENT_TEAM = P7BV7FZWPF;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.1;
MARKETING_VERSION = 1.0;
@@ -492,7 +586,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2NFSK2WB24;
+ DEVELOPMENT_TEAM = P7BV7FZWPF;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.1;
MARKETING_VERSION = 1.0;
@@ -511,7 +605,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2NFSK2WB24;
+ DEVELOPMENT_TEAM = P7BV7FZWPF;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-MonitorUITests";
@@ -529,7 +623,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 2NFSK2WB24;
+ DEVELOPMENT_TEAM = P7BV7FZWPF;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "codes.tim.Claude-MonitorUITests";
@@ -542,6 +636,72 @@
};
name = Release;
};
+ B3BB62392EE2ABA9003188A1 /* Debug configuration for PBXNativeTarget "Claude Monitor Widget" */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CODE_SIGN_ENTITLEMENTS = "Claude Monitor Widget/Claude_Monitor_Widget.entitlements";
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_APP_SANDBOX = YES;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "Claude Monitor Widget/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "Claude Monitor Widget";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor.widget";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SKIP_INSTALL = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ };
+ name = Debug;
+ };
+ B3BB623A2EE2ABA9003188A1 /* Release configuration for PBXNativeTarget "Claude Monitor Widget" */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ CODE_SIGN_ENTITLEMENTS = "Claude Monitor Widget/Claude_Monitor_Widget.entitlements";
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_APP_SANDBOX = YES;
+ ENABLE_HARDENED_RUNTIME = YES;
+ ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "Claude Monitor Widget/Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "Claude Monitor Widget";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ "@executable_path/../../../../Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = "--codes.tim.Claude-Monitor.widget";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SKIP_INSTALL = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -577,6 +737,14 @@
);
defaultConfigurationName = Release;
};
+ B3BB623B2EE2ABC1003188A1 /* Build configuration list for PBXNativeTarget "Claude Monitor Widget" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ B3BB62392EE2ABA9003188A1 /* Debug configuration for PBXNativeTarget "Claude Monitor Widget" */,
+ B3BB623A2EE2ABA9003188A1 /* Release configuration for PBXNativeTarget "Claude Monitor Widget" */,
+ );
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
diff --git a/Claude Monitor/Claude_Monitor.entitlements b/Claude Monitor/Claude_Monitor.entitlements
new file mode 100644
index 0000000..ee95ab7
--- /dev/null
+++ b/Claude Monitor/Claude_Monitor.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+
+