modelstat 0.0.29 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +56 -6
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
- package/vendor/tray-mac/Sources/ModelstatTray/main.swift +115 -18
package/package.json
CHANGED
|
@@ -54,6 +54,12 @@ struct AgentStats: Decodable {
|
|
|
54
54
|
let agent_url: String?
|
|
55
55
|
let device: DeviceInfo?
|
|
56
56
|
let analyzed: AnalyzedInfo?
|
|
57
|
+
/// Daemon heartbeat snapshot mirrored to ~/.modelstat/last-status.json.
|
|
58
|
+
/// Present on both unclaimed and claimed responses post-0.0.30 so the
|
|
59
|
+
/// tray can show real numbers even when the public device-view 404s
|
|
60
|
+
/// (claimed devices). Older daemons that don't write the file leave
|
|
61
|
+
/// this nil.
|
|
62
|
+
let local: LocalStatus?
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
struct DeviceInfo: Decodable {
|
|
@@ -65,10 +71,32 @@ struct DeviceInfo: Decodable {
|
|
|
65
71
|
|
|
66
72
|
struct AnalyzedInfo: Decodable {
|
|
67
73
|
let count: Int?
|
|
74
|
+
/// `processing` = sessions with at least one un-classified segment;
|
|
75
|
+
/// `finished` = every segment classified. From device-view endpoint.
|
|
76
|
+
let processing: Int?
|
|
77
|
+
let finished: Int?
|
|
68
78
|
let totalTokens: String?
|
|
69
79
|
let totalCostUsd: Double?
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
struct LocalStatus: Decodable {
|
|
83
|
+
let status: String?
|
|
84
|
+
let message: String?
|
|
85
|
+
let queue_size: Int?
|
|
86
|
+
let last_event_at: String?
|
|
87
|
+
let agent_version: String?
|
|
88
|
+
let stats: LocalStatsCounters?
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
struct LocalStatsCounters: Decodable {
|
|
92
|
+
let installations_detected: Int?
|
|
93
|
+
let identities_detected: Int?
|
|
94
|
+
let files_scanned: Int?
|
|
95
|
+
let files_unchanged: Int?
|
|
96
|
+
let events_uploaded: Int?
|
|
97
|
+
let batches_uploaded: Int?
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
@MainActor
|
|
73
101
|
final class TrayController: NSObject {
|
|
74
102
|
private let statusItem: NSStatusItem
|
|
@@ -80,9 +108,13 @@ final class TrayController: NSObject {
|
|
|
80
108
|
private var pollTimer: Timer?
|
|
81
109
|
|
|
82
110
|
// Menu items we update on every poll
|
|
83
|
-
private let statusMI = NSMenuItem(title: "
|
|
111
|
+
private let statusMI = NSMenuItem(title: "Loading…", action: nil, keyEquivalent: "")
|
|
84
112
|
private let deviceMI = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
|
85
113
|
private let analyzedMI = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
|
114
|
+
/// Pipeline activity — sessions processing/finished + events uploaded.
|
|
115
|
+
private let pipelineMI = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
|
116
|
+
/// What the agent has discovered on this machine — installations + identities.
|
|
117
|
+
private let detectedMI = NSMenuItem(title: "", action: nil, keyEquivalent: "")
|
|
86
118
|
private let claimMI = NSMenuItem(title: "Open device page", action: #selector(openDashboard), keyEquivalent: "o")
|
|
87
119
|
private let copyClaimMI = NSMenuItem(title: "Copy claim URL", action: #selector(copyClaimUrl), keyEquivalent: "c")
|
|
88
120
|
private let jobsMI = NSMenuItem(title: "View pipeline…", action: #selector(openJobs), keyEquivalent: "j")
|
|
@@ -120,7 +152,11 @@ final class TrayController: NSObject {
|
|
|
120
152
|
statusMI.isEnabled = false
|
|
121
153
|
deviceMI.isEnabled = false
|
|
122
154
|
analyzedMI.isEnabled = false
|
|
123
|
-
|
|
155
|
+
pipelineMI.isEnabled = false
|
|
156
|
+
detectedMI.isEnabled = false
|
|
157
|
+
for mi in [statusMI, deviceMI, analyzedMI, pipelineMI, detectedMI] {
|
|
158
|
+
menu.addItem(mi)
|
|
159
|
+
}
|
|
124
160
|
menu.addItem(NSMenuItem.separator())
|
|
125
161
|
claimMI.target = self
|
|
126
162
|
copyClaimMI.target = self
|
|
@@ -233,41 +269,102 @@ final class TrayController: NSObject {
|
|
|
233
269
|
|
|
234
270
|
private func renderStats() {
|
|
235
271
|
guard let s = latest else {
|
|
236
|
-
statusMI.title = "
|
|
272
|
+
statusMI.title = "Loading…"
|
|
237
273
|
return
|
|
238
274
|
}
|
|
239
275
|
if s.paired == false {
|
|
240
|
-
statusMI.title = "
|
|
276
|
+
statusMI.title = "Not paired — run `npx modelstat@latest`"
|
|
241
277
|
deviceMI.title = ""
|
|
242
278
|
analyzedMI.title = ""
|
|
279
|
+
pipelineMI.title = ""
|
|
280
|
+
detectedMI.title = ""
|
|
243
281
|
claimMI.title = "Open modelstat.ai"
|
|
244
282
|
copyClaimMI.isHidden = true
|
|
245
283
|
return
|
|
246
284
|
}
|
|
285
|
+
|
|
286
|
+
// Live agent phase comes from the local heartbeat mirror.
|
|
287
|
+
// Falls back to the device-view's reported agent_status. If
|
|
288
|
+
// both are missing we say "running" rather than "starting" so
|
|
289
|
+
// the menu doesn't lie about the daemon's state.
|
|
290
|
+
let phase = s.local?.status ?? s.device?.agent_status ?? "running"
|
|
291
|
+
let phaseMsg = s.local?.message
|
|
292
|
+
if let m = phaseMsg, !m.isEmpty {
|
|
293
|
+
statusMI.title = "● \(phase) — \(m)"
|
|
294
|
+
} else {
|
|
295
|
+
statusMI.title = "● \(phase)"
|
|
296
|
+
}
|
|
297
|
+
|
|
247
298
|
if s.claimed == true {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
299
|
+
// Claimed device: device-view 404s for the tray (no auth) so
|
|
300
|
+
// we lean entirely on the local heartbeat snapshot for live
|
|
301
|
+
// numbers, plus point the menu items at the dashboard.
|
|
302
|
+
deviceMI.title = s.local?.agent_version ?? "Claimed ✓ — synced to your account"
|
|
251
303
|
claimMI.title = "Open dashboard"
|
|
252
304
|
copyClaimMI.isHidden = true
|
|
253
|
-
|
|
305
|
+
} else {
|
|
306
|
+
// Unclaimed: device-view fills in the rich numbers.
|
|
307
|
+
let host = s.device?.hostname ?? "unknown"
|
|
308
|
+
let os = s.device?.os_family ?? ""
|
|
309
|
+
deviceMI.title = "\(host) · \(os)"
|
|
310
|
+
claimMI.title = "Open device page"
|
|
311
|
+
copyClaimMI.isHidden = (s.claim_url == nil || s.claim_url?.isEmpty == true)
|
|
254
312
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let os = s.device?.os_family ?? ""
|
|
260
|
-
deviceMI.title = "\(host) · \(os)"
|
|
313
|
+
|
|
314
|
+
// Sessions / tokens / cost (only available for unclaimed since
|
|
315
|
+
// the device-view exposes them). Claimed devices show pipeline
|
|
316
|
+
// + detected counts instead — see below.
|
|
261
317
|
if let a = s.analyzed {
|
|
262
318
|
let tok = a.totalTokens ?? "0"
|
|
263
319
|
let cnt = a.count ?? 0
|
|
264
320
|
let usd = String(format: "%.2f", a.totalCostUsd ?? 0.0)
|
|
265
|
-
|
|
321
|
+
let proc = a.processing ?? 0
|
|
322
|
+
let done = a.finished ?? cnt
|
|
323
|
+
let breakdown = proc > 0 ? " (\(done) finished · \(proc) processing)" : ""
|
|
324
|
+
analyzedMI.title = "\(cnt) sessions\(breakdown) · \(fmtTokens(tok)) tokens · $\(usd)"
|
|
325
|
+
} else {
|
|
326
|
+
analyzedMI.title = ""
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Pipeline activity — events + batches uploaded since daemon
|
|
330
|
+
// started. Sourced from the local heartbeat mirror so it works
|
|
331
|
+
// for both claimed and unclaimed devices.
|
|
332
|
+
if let c = s.local?.stats {
|
|
333
|
+
let events = c.events_uploaded ?? 0
|
|
334
|
+
let batches = c.batches_uploaded ?? 0
|
|
335
|
+
let scanned = c.files_scanned ?? 0
|
|
336
|
+
let queue = s.local?.queue_size ?? 0
|
|
337
|
+
var bits: [String] = []
|
|
338
|
+
if events > 0 || batches > 0 {
|
|
339
|
+
bits.append("\(fmtCount(events)) events · \(batches) batches uploaded")
|
|
340
|
+
}
|
|
341
|
+
if scanned > 0 { bits.append("\(scanned) files scanned") }
|
|
342
|
+
if queue > 0 { bits.append("\(queue) in queue") }
|
|
343
|
+
pipelineMI.title = bits.isEmpty ? "" : bits.joined(separator: " · ")
|
|
266
344
|
} else {
|
|
267
|
-
|
|
345
|
+
pipelineMI.title = ""
|
|
268
346
|
}
|
|
269
|
-
|
|
270
|
-
|
|
347
|
+
|
|
348
|
+
// What the agent found on this machine — installations +
|
|
349
|
+
// identities (Claude Keychain, Codex JWT, …). Mirror of the
|
|
350
|
+
// discover() output the daemon ran at startup.
|
|
351
|
+
if let c = s.local?.stats {
|
|
352
|
+
let installs = c.installations_detected ?? 0
|
|
353
|
+
let ids = c.identities_detected ?? 0
|
|
354
|
+
if installs > 0 || ids > 0 {
|
|
355
|
+
detectedMI.title = "\(installs) tools · \(ids) accounts detected"
|
|
356
|
+
} else {
|
|
357
|
+
detectedMI.title = ""
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
detectedMI.title = ""
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private func fmtCount(_ n: Int) -> String {
|
|
365
|
+
if n >= 1_000_000 { return String(format: "%.1fM", Double(n) / 1_000_000) }
|
|
366
|
+
if n >= 1_000 { return String(format: "%.1fK", Double(n) / 1_000) }
|
|
367
|
+
return String(n)
|
|
271
368
|
}
|
|
272
369
|
|
|
273
370
|
private func fmtTokens(_ raw: String) -> String {
|