-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathUsageDataModel.swift
More file actions
147 lines (121 loc) · 4.68 KB
/
UsageDataModel.swift
File metadata and controls
147 lines (121 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import Foundation
/// Model representing Claude usage data
struct UsageData {
let usedPct: Int
let leftPct: Int
let remainingMinutes: Int
let elapsedMinutes: Int
let totalTokens: Int
let tokensLeft: Int
let costUsed: Double
let costLeft: Double
let resetTime: String
}
/// Configuration for display options
struct DisplayOptions {
var percentage: Bool
var timeLeft: Bool
var tokens: Bool
var money: Bool
var showUsed: Bool
static let defaultOptions = DisplayOptions(
percentage: true,
timeLeft: true,
tokens: false,
money: false,
showUsed: true
)
}
/// Service for fetching and parsing usage data
class UsageDataService {
private let blockDurationMinutes = 300 // 5 hours
func fetchUsageData() async throws -> UsageData {
let ccusagePath = ProcessInfo.processInfo.environment["CCUSAGE_PATH"] ?? "npx"
let arguments = ccusagePath == "npx" ? ["npx", "ccusage"] : [ccusagePath]
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = arguments + ["blocks", "--active", "--json", "--token-limit", "max"]
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8),
let jsonData = output.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let blocks = json["blocks"] as? [[String: Any]],
let block = blocks.first else {
throw UsageError.noData
}
return parseUsageData(from: block)
}
private func parseUsageData(from block: [String: Any]) -> UsageData {
let totalTokens = block["totalTokens"] as? Int ?? 1
let projection = block["projection"] as? [String: Any]
let projectedTotal = projection?["totalTokens"] as? Int ?? totalTokens
let remainingMinutes = projection?["remainingMinutes"] as? Int ?? 0
let costUsed = block["costUSD"] as? Double ?? 0.0
let projectedCost = projection?["totalCost"] as? Double ?? 0.0
let tokenLimitStatus = block["tokenLimitStatus"] as? [String: Any]
let limit = tokenLimitStatus?["limit"] as? Int ?? totalTokens
let elapsedMinutes = blockDurationMinutes - remainingMinutes
let actualPercentUsed = (Double(totalTokens) / Double(limit)) * 100.0
let usedPct = Int(actualPercentUsed.rounded())
let leftPct = max(0, 100 - usedPct)
let tokensLeft = max(0, limit - totalTokens)
let costLeft = projectedCost - costUsed
return UsageData(
usedPct: usedPct,
leftPct: leftPct,
remainingMinutes: remainingMinutes,
elapsedMinutes: elapsedMinutes,
totalTokens: totalTokens,
tokensLeft: tokensLeft,
costUsed: costUsed,
costLeft: costLeft,
resetTime: formatResetTime(remainingMinutes: remainingMinutes)
)
}
private func formatResetTime(remainingMinutes: Int) -> String {
let now = Date()
let resetDate = Calendar.current.date(byAdding: .minute, value: remainingMinutes, to: now)!
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, h:mm a"
return formatter.string(from: resetDate)
}
}
/// Formatter for usage display
class UsageFormatter {
func formatTokens(_ tokens: Int) -> String {
if tokens >= 1_000_000 {
return String(format: "%.1fM", Double(tokens) / 1_000_000)
} else if tokens >= 1_000 {
return String(format: "%.1fK", Double(tokens) / 1_000)
} else {
return "\(tokens)"
}
}
func buildDisplayString(from usage: UsageData, options: DisplayOptions) -> [String] {
var display: [String] = []
if options.percentage {
display.append("\(options.showUsed ? usage.usedPct : usage.leftPct)%")
}
if options.timeLeft {
let minutes = options.showUsed ? usage.elapsedMinutes : usage.remainingMinutes
display.append("\(minutes / 60)h \(minutes % 60)m")
}
if options.tokens {
let tokens = options.showUsed ? usage.totalTokens : usage.tokensLeft
display.append("\(formatTokens(tokens))t")
}
if options.money && options.showUsed {
display.append("$\(String(format: "%.2f", usage.costUsed))")
}
return display
}
}
enum UsageError: Error {
case noData
case invalidData
case processError
}