101 lines
3.8 KiB
Swift
101 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
/// Aggregated stats from local `~/.grok/sessions/**/signals.json` files.
|
|
/// Used as a local fallback view when the JSON-RPC billing call is unavailable.
|
|
public struct GrokLocalSessionSummary: Sendable {
|
|
public let sessionCount: Int
|
|
public let totalTokens: Int
|
|
public let lastSessionAt: Date?
|
|
public let primaryModel: String?
|
|
public let models: [String]
|
|
|
|
public init(
|
|
sessionCount: Int,
|
|
totalTokens: Int,
|
|
lastSessionAt: Date?,
|
|
primaryModel: String?,
|
|
models: [String])
|
|
{
|
|
self.sessionCount = sessionCount
|
|
self.totalTokens = totalTokens
|
|
self.lastSessionAt = lastSessionAt
|
|
self.primaryModel = primaryModel
|
|
self.models = models
|
|
}
|
|
}
|
|
|
|
public enum GrokLocalSessionScanner {
|
|
public static let defaultLookbackDays = 30
|
|
|
|
/// Walk `~/.grok/sessions/<encoded_cwd>/<session_id>/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,
|
|
lastSessionAt: nil,
|
|
primaryModel: nil,
|
|
models: [])
|
|
}
|
|
|
|
let lookbackCutoff = Calendar.current.date(byAdding: .day, value: -lookbackDays, to: now) ?? now
|
|
var sessionCount = 0
|
|
var totalTokens = 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
|
|
let beforeCompaction = (json["totalTokensBeforeCompaction"] as? Int) ?? 0
|
|
let contextUsed = (json["contextTokensUsed"] as? Int) ?? 0
|
|
totalTokens += beforeCompaction + contextUsed
|
|
|
|
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,
|
|
lastSessionAt: lastSessionAt,
|
|
primaryModel: sortedModels.first,
|
|
models: sortedModels)
|
|
}
|
|
}
|