import Foundation /// Aggregated stats from local `~/.grok/sessions/**/signals.json` files. /// Used as a local fallback view when the JSON-RPC / web billing call is unavailable, /// and as always-on local token cost context when remote usage is available. public struct GrokLocalSessionSummary: Sendable { public let sessionCount: Int /// Tokens over the full lookback window (default 30 days). public let totalTokens: Int /// Tokens from sessions modified in the last 24 hours (local calendar day window). public let todayTokens: Int /// Tokens from sessions modified in the last 7 days. public let weekTokens: Int public let lastSessionAt: Date? public let primaryModel: String? public let models: [String] public init( sessionCount: Int, totalTokens: Int, todayTokens: Int = 0, weekTokens: Int = 0, lastSessionAt: Date?, primaryModel: String?, models: [String]) { self.sessionCount = sessionCount self.totalTokens = totalTokens self.todayTokens = todayTokens self.weekTokens = weekTokens self.lastSessionAt = lastSessionAt self.primaryModel = primaryModel self.models = models } public var hasActivity: Bool { self.sessionCount > 0 || self.totalTokens > 0 } /// Human-readable diagnostic line for menu cards / CLI verbose mode. public var diagnosticLine: String { var parts: [String] = [] if self.todayTokens > 0 { parts.append("today \(Self.formatTokens(self.todayTokens))") } if self.weekTokens > 0 { parts.append("7d \(Self.formatTokens(self.weekTokens))") } if self.totalTokens > 0, self.totalTokens != self.weekTokens { parts.append("30d \(Self.formatTokens(self.totalTokens))") } if self.sessionCount > 0 { parts.append("\(self.sessionCount) session\(self.sessionCount == 1 ? "" : "s")") } if let model = self.primaryModel?.nilIfEmpty { parts.append(model) } guard !parts.isEmpty else { return "No local Grok sessions yet." } return "Local: " + parts.joined(separator: " ยท ") } public func providerCostSnapshot(updatedAt: Date = .init()) -> ProviderCostSnapshot? { // Prefer the most recent non-zero window so the cost chip stays meaningful. let used: Double let period: String if self.todayTokens > 0 { used = Double(self.todayTokens) period = "Local today" } else if self.weekTokens > 0 { used = Double(self.weekTokens) period = "Local 7d" } else if self.totalTokens > 0 { used = Double(self.totalTokens) period = "Local 30d" } else { return nil } return ProviderCostSnapshot( used: used, limit: 0, currencyCode: "tok", period: period, resetsAt: nil, updatedAt: updatedAt) } private static func formatTokens(_ value: Int) -> String { if value >= 1_000_000 { return String(format: "%.1fM", Double(value) / 1_000_000) } if value >= 1_000 { return String(format: "%.1fK", Double(value) / 1_000) } return "\(value)" } } public enum GrokLocalSessionScanner { public static let defaultLookbackDays = 30 /// Walk `~/.grok/sessions///signals.json` and aggregate stats. public static func summarize( env: [String: String] = ProcessInfo.processInfo.environment, fileManager: FileManager = .default, lookbackDays: Int = defaultLookbackDays, now: Date = .init()) -> GrokLocalSessionSummary { let root = GrokCredentialsStore.grokHomeURL(env: env, fileManager: fileManager) .appendingPathComponent("sessions", isDirectory: true) guard let rootEnum = fileManager.enumerator( at: root, includingPropertiesForKeys: [.contentModificationDateKey, .isDirectoryKey], options: [.skipsHiddenFiles]) else { return GrokLocalSessionSummary( sessionCount: 0, totalTokens: 0, todayTokens: 0, weekTokens: 0, lastSessionAt: nil, primaryModel: nil, models: []) } let calendar = Calendar.current let lookbackCutoff = calendar.date(byAdding: .day, value: -lookbackDays, to: now) ?? now let todayCutoff = calendar.date(byAdding: .day, value: -1, to: now) ?? now let weekCutoff = calendar.date(byAdding: .day, value: -7, to: now) ?? now var sessionCount = 0 var totalTokens = 0 var todayTokens = 0 var weekTokens = 0 var lastSessionAt: Date? var modelCounts: [String: Int] = [:] while let url = rootEnum.nextObject() as? URL { guard url.lastPathComponent == "signals.json" else { continue } let attrs = try? url.resourceValues(forKeys: [.contentModificationDateKey]) let mtime = attrs?.contentModificationDate ?? Date.distantPast guard mtime >= lookbackCutoff else { continue } guard let data = try? Data(contentsOf: url), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue } sessionCount += 1 // Prefer cumulative pre-compaction when present; otherwise fall back to // the live context window size so short sessions still count. let beforeCompaction = (json["totalTokensBeforeCompaction"] as? Int) ?? 0 let contextUsed = (json["contextTokensUsed"] as? Int) ?? 0 let sessionTokens = beforeCompaction > 0 ? beforeCompaction : contextUsed totalTokens += sessionTokens if mtime >= weekCutoff { weekTokens += sessionTokens } if mtime >= todayCutoff { todayTokens += sessionTokens } if mtime > (lastSessionAt ?? Date.distantPast) { lastSessionAt = mtime } if let primary = (json["primaryModelId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !primary.isEmpty { modelCounts[primary, default: 0] += 1 } if let models = json["modelsUsed"] as? [String] { for model in models { let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { modelCounts[trimmed, default: 0] += 1 } } } } let sortedModels = modelCounts.sorted { $0.value > $1.value }.map(\.key) return GrokLocalSessionSummary( sessionCount: sessionCount, totalTokens: totalTokens, todayTokens: todayTokens, weekTokens: weekTokens, lastSessionAt: lastSessionAt, primaryModel: sortedModels.first, models: sortedModels) } } extension String { fileprivate var nilIfEmpty: String? { self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self } }