1→import Foundation 2→import Combine 3→import AppKit 4→import SwiftUI 5→ 6→// MARK: - Consumption Tracker 7→ 8→/// Monitors Claude Code CLI processes in real-time and tracks token consumption 9→@MainActor 10→final class ConsumptionTracker: ObservableObject { 11→ static let shared = ConsumptionTracker() 12→ 13→ // MARK: - Published State 14→ 15→ // Session metrics 16→ @Published var sessionCost: Double = 0 17→ @Published var sessionInputTokens: Int = 0 18→ @Published var sessionOutputTokens: Int = 0 19→ @Published var sessionCacheTokens: Int = 0 20→ @Published var sessionStartTime: Date = Date() 21→ 22→ // Rates 23→ @Published var costPerHour: Double = 0 24→ @Published var costPerMinute: Double = 0 25→ @Published var inputTokensPerMinute: Double = 0 26→ @Published var outputTokensPerMinute: Double = 0 27→ @Published var cacheTokensPerMinute: Double = 0 28→ @Published var costTrend: Double = 0 29→ 30→ // Process monitoring 31→ @Published var isMonitoring: Bool = false 32→ @Published var isPaused: Bool = false 33→ @Published var activeProcesses: [ClaudeProcess] = [] 34→ @Published var activeRun: CCRun? 35→ 36→ // History 37→ @Published var consumptionHistory: [ConsumptionDataPoint] = [] 38→ @Published var recentActivities: [ConsumptionActivity] = [] 39→ 40→ // MARK: - Private Properties 41→ 42→ private var monitoringTask: Task? 43→ private var rateCalculationTimer: Timer? 44→ private var historyTimer: Timer? 45→ private var cancellables = Set() 46→ 47→ private let ccService = CCTrackingService() 48→ private var lastCostSnapshot: Double = 0 49→ private var lastSnapshotTime: Date = Date() 50→ 51→ // Token pricing (Claude Opus 4.5) 52→ private let inputTokenPrice: Double = 15.0 / 1_000_000 // $15 per 1M 53→ private let outputTokenPrice: Double = 75.0 / 1_000_000 // $75 per 1M 54→ private let cacheTokenPrice: Double = 1.875 / 1_000_000 // $1.875 per 1M (cache read) 55→ 56→ // MARK: - Initialization 57→ 58→ private init() { 59→ startMonitoring() 60→ startRateCalculation() 61→ startHistoryTracking() 62→ } 63→ 64→ // Note: deinit cannot call @MainActor methods directly 65→ // Cleanup is handled when the app terminates 66→ 67→ // MARK: - Public Methods 68→ 69→ var sessionDuration: String { 70→ let interval = Date().timeIntervalSince(sessionStartTime) 71→ let hours = Int(interval) / 3600 72→ let minutes = (Int(interval) % 3600) / 60 73→ let seconds = Int(interval) % 60 74→ 75→ if hours > 0 { 76→ return String(format: "%dh %dm %ds", hours, minutes, seconds) 77→ } else if minutes > 0 { 78→ return String(format: "%dm %ds", minutes, seconds) 79→ } 80→ return String(format: "%ds", seconds) 81→ } 82→ 83→ func resetSession() { 84→ sessionCost = 0 85→ sessionInputTokens = 0 86→ sessionOutputTokens = 0 87→ sessionCacheTokens = 0 88→ sessionStartTime = Date() 89→ costPerHour = 0 90→ costPerMinute = 0 91→ inputTokensPerMinute = 0 92→ outputTokensPerMinute = 0 93→ cacheTokensPerMinute = 0 94→ costTrend = 0 95→ consumptionHistory.removeAll() 96→ recentActivities.removeAll() 97→ lastCostSnapshot = 0 98→ lastSnapshotTime = Date() 99→ 100→ addActivity(.info, "Session reset") 101→ } 102→ 103→ func startMonitoring() { 104→ guard !isMonitoring else { return } 105→ isMonitoring = true 106→ 107→ monitoringTask = Task { 108→ await monitorClaudeProcesses() 109→ } 110→ 111→ addActivity(.info, "Monitoring started") 112→ } 113→ 114→ func stopMonitoring() { 115→ isMonitoring = false 116→ monitoringTask?.cancel() 117→ monitoringTask = nil 118→ rateCalculationTimer?.invalidate() 119→ historyTimer?.invalidate() 120→ 121→ addActivity(.info, "Monitoring stopped") 122→ } 123→ 124→ // MARK: - Process Monitoring 125→ 126→ private func monitorClaudeProcesses() async { 127→ while isMonitoring && !Task.isCancelled { 128→ if !isPaused { 129→ await detectClaudeProcesses() 130→ await fetchLatestConsumption() 131→ } 132→ 133→ try? await Task.sleep(for: .seconds(2)) 134→ } 135→ } 136→ 137→ private func detectClaudeProcesses() async { 138→ // Find claude processes using pgrep 139→ let process = Process() 140→ process.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep") 141→ process.arguments = ["-f", "claude"] 142→ 143→ let outputPipe = Pipe() 144→ process.standardOutput = outputPipe 145→ 146→ do { 147→ try process.run() 148→ process.waitUntilExit() 149→ 150→ let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 151→ let outputString = String(data: outputData, encoding: .utf8) ?? "" 152→ 153→ let pids = outputString.components(separatedBy: .newlines) 154→ .compactMap { Int($0.trimmingCharacters(in: .whitespaces)) } 155→ 156→ // Get process details for each PID 157→ var processes: [ClaudeProcess] = [] 158→ 159→ for pid in pids { 160→ if let processInfo = await getProcessInfo(pid: pid) { 161→ processes.append(processInfo) 162→ } 163→ } 164→ 165→ // Update active processes 166→ let oldCount = activeProcesses.count 167→ activeProcesses = processes 168→ 169→ if processes.count != oldCount { 170→ if processes.count > oldCount { 171→ addActivity(.process, "Claude Code process detected (PID: \(processes.last?.pid ?? 0))") 172→ } else { 173→ addActivity(.process, "Claude Code process ended") 174→ } 175→ } 176→ 177→ } catch { 178→ print("Error detecting claude processes: \(error)") 179→ } 180→ } 181→ 182→ private func getProcessInfo(pid: Int) async -> ClaudeProcess? { 183→ // Get process command line 184→ let process = Process() 185→ process.executableURL = URL(fileURLWithPath: "/bin/ps") 186→ process.arguments = ["-p", "\(pid)", "-o", "command=", "-o", "etime=", "-o", "%cpu=", "-o", "%mem="] 187→ 188→ let outputPipe = Pipe() 189→ process.standardOutput = outputPipe 190→ 191→ do { 192→ try process.run() 193→ process.waitUntilExit() 194→ 195→ let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 196→ let outputString = String(data: outputData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" 197→ 198→ guard !outputString.isEmpty else { return nil } 199→ 200→ // Parse output 201→ let components = outputString.components(separatedBy: .whitespaces).filter { !$0.isEmpty } 202→ guard components.count >= 1 else { return nil } 203→ 204→ // Check if it's actually a claude process 205→ let command = components.first ?? "" 206→ guard command.contains("claude") || command.contains("node") else { return nil } 207→ 208→ return ClaudeProcess( 209→ pid: pid, 210→ command: command, 211→ startTime: Date(), // Would need to parse etime properly 212→ cpuUsage: Double(components.last ?? "0") ?? 0 213→ ) 214→ 215→ } catch { 216→ return nil 217→ } 218→ } 219→ 220→ // MARK: - Consumption Fetching 221→ 222→ private func fetchLatestConsumption() async { 223→ do { 224→ // Fetch latest ROI summary from API 225→ let summary = try await ccService.getROISummary() 226→ 227→ // Calculate delta from last fetch 228→ let previousCost = sessionCost 229→ let newTotalCost = summary.totalCost 230→ 231→ // Fetch recent runs to aggregate token data 232→ let runs = try await ccService.getRecentRuns(limit: 50) 233→ 234→ // Aggregate tokens from today's runs 235→ var totalInput = 0 236→ var totalOutput = 0 237→ var totalCache = 0 238→ 239→ let calendar = Calendar.current 240→ let today = calendar.startOfDay(for: Date()) 241→ 242→ for run in runs { 243→ if calendar.isDate(run.startedAt, inSameDayAs: today) { 244→ totalInput += run.inputTokens 245→ totalOutput += run.outputTokens 246→ totalCache += run.cacheReadTokens 247→ } 248→ } 249→ 250→ // Update session metrics based on today's data 251→ sessionCost = newTotalCost 252→ sessionInputTokens = totalInput 253→ sessionOutputTokens = totalOutput 254→ sessionCacheTokens = totalCache 255→ 256→ // Track cost trend 257→ if previousCost > 0 { 258→ let costDelta = newTotalCost - previousCost 259→ if costDelta > 0 { 260→ addActivity(.cost, "Cost increased", cost: costDelta) 261→ } 262→ } 263→ 264→ // Check for active run 265→ if let latestRun = runs.first, latestRun.status == .running { 266→ activeRun = latestRun 267→ } else { 268→ activeRun = nil 269→ } 270→ 271→ } catch { 272→ print("Error fetching consumption: \(error)") 273→ } 274→ } 275→ 276→ // MARK: - Rate Calculation 277→ 278→ private func startRateCalculation() { 279→ rateCalculationTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] _ in 280→ Task { @MainActor in 281→ self?.calculateRates() 282→ } 283→ } 284→ } 285→ 286→ private func calculateRates() { 287→ let now = Date() 288→ let elapsed = now.timeIntervalSince(lastSnapshotTime) 289→ 290→ guard elapsed > 0 else { return } 291→ 292→ // Calculate cost rate 293→ let costDelta = sessionCost - lastCostSnapshot 294→ costPerMinute = (costDelta / elapsed) * 60 295→ costPerHour = costPerMinute * 60 296→ 297→ // Calculate token rates 298→ let sessionElapsed = now.timeIntervalSince(sessionStartTime) 299→ if sessionElapsed > 0 { 300→ inputTokensPerMinute = Double(sessionInputTokens) / (sessionElapsed / 60) 301→ outputTokensPerMinute = Double(sessionOutputTokens) / (sessionElapsed / 60) 302→ cacheTokensPerMinute = Double(sessionCacheTokens) / (sessionElapsed / 60) 303→ } 304→ 305→ // Calculate trend (% change from last period) 306→ if lastCostSnapshot > 0 { 307→ costTrend = ((sessionCost - lastCostSnapshot) / lastCostSnapshot) * 100 308→ } 309→ 310→ // Update snapshot 311→ lastCostSnapshot = sessionCost 312→ lastSnapshotTime = now 313→ } 314→ 315→ // MARK: - History Tracking 316→ 317→ private func startHistoryTracking() { 318→ historyTimer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in 319→ Task { @MainActor in 320→ self?.recordHistoryPoint() 321→ } 322→ } 323→ 324→ // Record initial point 325→ recordHistoryPoint() 326→ } 327→ 328→ private func recordHistoryPoint() { 329→ let point = ConsumptionDataPoint( 330→ timestamp: Date(), 331→ cumulativeCost: sessionCost, 332→ inputTokens: sessionInputTokens, 333→ outputTokens: sessionOutputTokens, 334→ cacheTokens: sessionCacheTokens 335→ ) 336→ 337→ consumptionHistory.append(point) 338→ 339→ // Keep last 2 hours of data (240 points at 30s intervals) 340→ if consumptionHistory.count > 240 { 341→ consumptionHistory.removeFirst() 342→ } 343→ } 344→ 345→ // MARK: - Activity Logging 346→ 347→ private func addActivity(_ type: ConsumptionActivity.ActivityType, _ description: String, cost: Double? = nil) { 348→ let activity = ConsumptionActivity( 349→ type: type, 350→ description: description, 351→ timestamp: Date(), 352→ cost: cost 353→ ) 354→ 355→ recentActivities.insert(activity, at: 0) 356→ 357→ // Keep last 50 activities 358→ if recentActivities.count > 50 { 359→ recentActivities.removeLast() 360→ } 361→ } 362→} 363→ 364→// MARK: - Supporting Types 365→ 366→struct ClaudeProcess: Identifiable { 367→ let id = UUID() 368→ let pid: Int 369→ let command: String 370→ let startTime: Date 371→ let cpuUsage: Double 372→} 373→ 374→struct ConsumptionDataPoint: Identifiable { 375→ let id = UUID() 376→ let timestamp: Date 377→ let cumulativeCost: Double 378→ let inputTokens: Int 379→ let outputTokens: Int 380→ let cacheTokens: Int 381→} 382→ 383→struct ConsumptionActivity: Identifiable { 384→ let id = UUID() 385→ let type: ActivityType 386→ let description: String 387→ let timestamp: Date 388→ let cost: Double? 389→ 390→ enum ActivityType { 391→ case info 392→ case cost 393→ case tokens 394→ case process 395→ case warning 396→ case error 397→ 398→ var color: Color { 399→ switch self { 400→ case .info: return .blue 401→ case .cost: return .green 402→ case .tokens: return .purple 403→ case .process: return .orange 404→ case .warning: return .yellow 405→ case .error: return .red 406→ } 407→ } 408→ } 409→} 410→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.