token-larper 1.0.0

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.
Files changed (42) hide show
  1. package/README.md +334 -0
  2. package/Start-TokenLarper.vbs +5 -0
  3. package/bin/cli.js +34 -0
  4. package/llms.txt +59 -0
  5. package/package.json +61 -0
  6. package/scripts/docs-demo-server.ts +141 -0
  7. package/scripts/launch-on-default-desktop.ps1 +42 -0
  8. package/scripts/launch-silent.vbs +6 -0
  9. package/scripts/run-server.ps1 +59 -0
  10. package/scripts/tray-host.ps1 +181 -0
  11. package/src/ccusage.ts +1075 -0
  12. package/src/client/App.tsx +560 -0
  13. package/src/client/charts.tsx +341 -0
  14. package/src/client/components/Calendar.tsx +207 -0
  15. package/src/client/components/DateRangePicker.tsx +131 -0
  16. package/src/client/components/KpiCards.tsx +122 -0
  17. package/src/client/components/RankChip.tsx +22 -0
  18. package/src/client/components/SelectMenu.tsx +116 -0
  19. package/src/client/components/SettingsDialog.tsx +207 -0
  20. package/src/client/components/ThemeSettings.tsx +131 -0
  21. package/src/client/context.tsx +60 -0
  22. package/src/client/index.html +54 -0
  23. package/src/client/lib/aggregate.ts +266 -0
  24. package/src/client/lib/range.ts +87 -0
  25. package/src/client/lib/rank.ts +323 -0
  26. package/src/client/lib/shareCard.ts +139 -0
  27. package/src/client/logo.svg +8 -0
  28. package/src/client/main.tsx +12 -0
  29. package/src/client/styles.css +1113 -0
  30. package/src/client/themes.ts +268 -0
  31. package/src/client/utils.ts +61 -0
  32. package/src/client/views/Models.tsx +71 -0
  33. package/src/client/views/Overview.tsx +280 -0
  34. package/src/client/views/Projects.tsx +88 -0
  35. package/src/client/views/Rank.tsx +255 -0
  36. package/src/client/views/Sessions.tsx +108 -0
  37. package/src/client/views/Tools.tsx +114 -0
  38. package/src/projects.ts +214 -0
  39. package/src/server.ts +173 -0
  40. package/src/startup.ts +110 -0
  41. package/src/tray.ts +293 -0
  42. package/src/types.ts +155 -0
package/src/ccusage.ts ADDED
@@ -0,0 +1,1075 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import type {
5
+ DashboardPayload,
6
+ HarnessId,
7
+ HarnessMetadata,
8
+ HarnessSummary,
9
+ ModelMetric,
10
+ PeriodHarnessBreakdown,
11
+ SessionEntry,
12
+ TimePeriodRow,
13
+ } from "./types.ts";
14
+ import { isoWeek, localDateKey } from "./client/utils.ts";
15
+ import { opencodeSessionUpdated, resolveSessionProject, saveSessionProjects } from "./projects.ts";
16
+
17
+ const ROOT_DIR = resolve(import.meta.dir, "..");
18
+ const CACHE_DIR = join(ROOT_DIR, ".cache");
19
+ const DASHBOARD_CACHE_FILE = join(CACHE_DIR, "dashboard-cache.json");
20
+ const ANTIGRAVITY_DAILY_CACHE = join(CACHE_DIR, "antigravity-daily.json");
21
+ const ANTIGRAVITY_SESSION_CACHE = join(CACHE_DIR, "antigravity-session.json");
22
+
23
+ const HOME = homedir();
24
+ const APPDATA = process.env.APPDATA || join(HOME, "AppData", "Roaming");
25
+ const LOCALAPPDATA = process.env.LOCALAPPDATA || join(HOME, "AppData", "Local");
26
+
27
+ interface HarnessDefinition {
28
+ id: HarnessId;
29
+ name: string;
30
+ vendor: string;
31
+ color: string;
32
+ accentBg: string;
33
+ description: string;
34
+ configPathHint: string;
35
+ detectPaths: string[];
36
+ slowScan?: boolean;
37
+ }
38
+
39
+ export const HARNESS_DEFINITIONS: HarnessDefinition[] = [
40
+ {
41
+ id: "claude",
42
+ name: "Claude Code",
43
+ vendor: "Anthropic",
44
+ color: "#f97316",
45
+ accentBg: "rgba(249, 115, 22, 0.14)",
46
+ description: "Anthropic's agentic terminal harness (~/.claude)",
47
+ configPathHint: "~/.claude",
48
+ detectPaths: [join(HOME, ".claude"), join(HOME, ".claude.json")],
49
+ },
50
+ {
51
+ id: "codex",
52
+ name: "Codex CLI",
53
+ vendor: "OpenAI",
54
+ color: "#10b981",
55
+ accentBg: "rgba(16, 185, 129, 0.14)",
56
+ description: "OpenAI Codex coding agent & rollout sessions (~/.codex)",
57
+ configPathHint: "~/.codex",
58
+ detectPaths: [join(HOME, ".codex")],
59
+ },
60
+ {
61
+ id: "antigravity",
62
+ name: "Antigravity",
63
+ vendor: "Google DeepMind",
64
+ color: "#8b5cf6",
65
+ accentBg: "rgba(139, 92, 246, 0.14)",
66
+ description: "Google DeepMind Antigravity agentic IDE & brain transcripts",
67
+ configPathHint: "~/.gemini/antigravity",
68
+ detectPaths: [join(HOME, ".gemini", "antigravity")],
69
+ slowScan: true,
70
+ },
71
+ {
72
+ id: "pi",
73
+ name: "pi-agent",
74
+ vendor: "pi",
75
+ color: "#ec4899",
76
+ accentBg: "rgba(236, 72, 153, 0.14)",
77
+ description: "Pi coding harness & multi-model terminal sessions (~/.pi)",
78
+ configPathHint: "~/.pi",
79
+ detectPaths: [join(HOME, ".pi")],
80
+ },
81
+ {
82
+ id: "opencode",
83
+ name: "OpenCode",
84
+ vendor: "SST / OpenCode",
85
+ color: "#06b6d4",
86
+ accentBg: "rgba(6, 182, 212, 0.14)",
87
+ description: "Open-source terminal coding agent (opencode)",
88
+ configPathHint: "~/.local/share/opencode",
89
+ detectPaths: [
90
+ join(HOME, ".local", "share", "opencode"),
91
+ join(HOME, ".opencode"),
92
+ join(LOCALAPPDATA, "opencode"),
93
+ ],
94
+ },
95
+ {
96
+ id: "gemini",
97
+ name: "Gemini CLI",
98
+ vendor: "Google",
99
+ color: "#3b82f6",
100
+ accentBg: "rgba(59, 130, 246, 0.14)",
101
+ description: "Google Gemini terminal coding agent (~/.gemini)",
102
+ configPathHint: "~/.gemini",
103
+ detectPaths: [join(HOME, ".gemini")],
104
+ },
105
+ {
106
+ id: "copilot",
107
+ name: "GitHub Copilot CLI",
108
+ vendor: "GitHub",
109
+ color: "#eab308",
110
+ accentBg: "rgba(234, 179, 8, 0.14)",
111
+ description: "GitHub Copilot agentic CLI (~/.copilot)",
112
+ configPathHint: "~/.copilot",
113
+ detectPaths: [join(HOME, ".copilot")],
114
+ },
115
+ {
116
+ id: "amp",
117
+ name: "Amp",
118
+ vendor: "Sourcegraph",
119
+ color: "#f43f5e",
120
+ accentBg: "rgba(244, 63, 94, 0.14)",
121
+ description: "Sourcegraph Amp coding agent",
122
+ configPathHint: "~/.amp",
123
+ detectPaths: [join(HOME, ".amp"), join(LOCALAPPDATA, "amp")],
124
+ },
125
+ {
126
+ id: "droid",
127
+ name: "Droid",
128
+ vendor: "Factory",
129
+ color: "#14b8a6",
130
+ accentBg: "rgba(20, 184, 166, 0.14)",
131
+ description: "Factory Droid autonomous engineering harness",
132
+ configPathHint: "~/.factory",
133
+ detectPaths: [join(HOME, ".factory"), join(HOME, ".droid")],
134
+ },
135
+ {
136
+ id: "codebuff",
137
+ name: "Codebuff",
138
+ vendor: "Codebuff",
139
+ color: "#a855f7",
140
+ accentBg: "rgba(168, 85, 247, 0.14)",
141
+ description: "Codebuff CLI coding agent",
142
+ configPathHint: "~/.codebuff",
143
+ detectPaths: [join(HOME, ".codebuff"), join(APPDATA, "codebuff")],
144
+ },
145
+ {
146
+ id: "hermes",
147
+ name: "Hermes Agent",
148
+ vendor: "Nous Research",
149
+ color: "#f59e0b",
150
+ accentBg: "rgba(245, 158, 11, 0.14)",
151
+ description: "Hermes terminal agent harness",
152
+ configPathHint: "~/.hermes",
153
+ detectPaths: [join(HOME, ".hermes")],
154
+ },
155
+ {
156
+ id: "goose",
157
+ name: "Goose",
158
+ vendor: "Block",
159
+ color: "#6366f1",
160
+ accentBg: "rgba(99, 102, 241, 0.14)",
161
+ description: "Block's open-source developer agent",
162
+ configPathHint: "~/.config/goose",
163
+ detectPaths: [join(HOME, ".config", "goose"), join(APPDATA, "Block", "goose")],
164
+ },
165
+ {
166
+ id: "kilo",
167
+ name: "Kilo Code",
168
+ vendor: "Kilo",
169
+ color: "#84cc16",
170
+ accentBg: "rgba(132, 204, 22, 0.14)",
171
+ description: "Kilo Code agent harness",
172
+ configPathHint: "~/.kilo",
173
+ detectPaths: [join(HOME, ".kilo")],
174
+ },
175
+ {
176
+ id: "kimi",
177
+ name: "Kimi CLI",
178
+ vendor: "Moonshot AI",
179
+ color: "#22d3ee",
180
+ accentBg: "rgba(34, 211, 238, 0.14)",
181
+ description: "Moonshot Kimi coding agent CLI",
182
+ configPathHint: "~/.kimi",
183
+ detectPaths: [join(HOME, ".kimi")],
184
+ },
185
+ {
186
+ id: "qwen",
187
+ name: "Qwen Code",
188
+ vendor: "Alibaba Cloud",
189
+ color: "#c084fc",
190
+ accentBg: "rgba(192, 132, 252, 0.14)",
191
+ description: "Qwen Code CLI harness",
192
+ configPathHint: "~/.qwen",
193
+ detectPaths: [join(HOME, ".qwen")],
194
+ },
195
+ {
196
+ id: "openclaw",
197
+ name: "OpenClaw",
198
+ vendor: "OpenClaw",
199
+ color: "#fb7185",
200
+ accentBg: "rgba(251, 113, 133, 0.14)",
201
+ description: "OpenClaw autonomous agent CLI",
202
+ configPathHint: "~/.openclaw",
203
+ detectPaths: [join(HOME, ".openclaw")],
204
+ },
205
+ {
206
+ id: "grok",
207
+ name: "Grok Build CLI",
208
+ vendor: "xAI",
209
+ color: "#38bdf8",
210
+ accentBg: "rgba(56, 189, 248, 0.14)",
211
+ description: "xAI Grok Build coding CLI",
212
+ configPathHint: "~/.grok",
213
+ detectPaths: [join(HOME, ".grok")],
214
+ },
215
+ {
216
+ id: "zcode",
217
+ name: "ZCode",
218
+ vendor: "ZCode",
219
+ color: "#4ade80",
220
+ accentBg: "rgba(74, 222, 128, 0.14)",
221
+ description: "ZCode CLI agent harness",
222
+ configPathHint: "~/.zcode",
223
+ detectPaths: [join(HOME, ".zcode")],
224
+ },
225
+ ];
226
+
227
+ function resolveCcusageBinary(): { cmd: string[]; version: string } {
228
+ let version = "20.0.24";
229
+ try {
230
+ const pkgPath = join(ROOT_DIR, "node_modules", "ccusage", "package.json");
231
+ if (existsSync(pkgPath)) {
232
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
233
+ if (pkg.version) version = pkg.version;
234
+ }
235
+ } catch {
236
+ // ignore
237
+ }
238
+
239
+ const candidates = [
240
+ join(ROOT_DIR, "node_modules", "@ccusage", "ccusage-win32-x64", "bin", "ccusage.exe"),
241
+ join(ROOT_DIR, "node_modules", "@ccusage", "ccusage-win32-arm64", "bin", "ccusage.exe"),
242
+ join(ROOT_DIR, "node_modules", "@ccusage", "ccusage-darwin-arm64", "bin", "ccusage"),
243
+ join(ROOT_DIR, "node_modules", "@ccusage", "ccusage-darwin-x64", "bin", "ccusage"),
244
+ join(ROOT_DIR, "node_modules", "@ccusage", "ccusage-linux-x64", "bin", "ccusage"),
245
+ join(ROOT_DIR, "node_modules", "@ccusage", "ccusage-linux-arm64", "bin", "ccusage"),
246
+ ];
247
+
248
+ for (const bin of candidates) {
249
+ if (existsSync(bin)) {
250
+ return { cmd: [bin], version };
251
+ }
252
+ }
253
+
254
+ const cliJs = join(ROOT_DIR, "node_modules", "ccusage", "src", "cli.js");
255
+ if (existsSync(cliJs)) {
256
+ return { cmd: [process.execPath, cliJs], version };
257
+ }
258
+
259
+ return { cmd: ["bunx", "ccusage"], version };
260
+ }
261
+
262
+ /**
263
+ * Estimates realistic frontier API cost in USD when ccusage reports missingPricing: true or 0 cost
264
+ * (e.g. for bleeding-edge models like claude-opus-5-5, gpt-6-sol, minimax-m3-free, etc.).
265
+ */
266
+ export function estimateFrontierCost(params: {
267
+ modelName: string;
268
+ inputTokens: number;
269
+ outputTokens: number;
270
+ cacheCreationTokens: number;
271
+ cacheReadTokens: number;
272
+ reasoningOutputTokens?: number;
273
+ }): number {
274
+ const m = params.modelName.toLowerCase();
275
+ const M = 1_000_000;
276
+
277
+ // Pricing in USD per 1M tokens: [input, output, cacheWrite, cacheRead]
278
+ let rates: [number, number, number, number] = [2.0, 8.0, 2.5, 0.2];
279
+
280
+ if (m.includes("opus")) {
281
+ rates = [15.0, 75.0, 18.75, 1.5];
282
+ } else if (m.includes("sonnet")) {
283
+ rates = [3.0, 15.0, 3.75, 0.3];
284
+ } else if (m.includes("haiku")) {
285
+ rates = [0.8, 4.0, 1.0, 0.08];
286
+ } else if (m.includes("gpt-6") || m.includes("gpt-5") || m.includes("o3") || m.includes("o4")) {
287
+ rates = [2.5, 10.0, 2.5, 0.25];
288
+ } else if (m.includes("gemini") && m.includes("pro")) {
289
+ rates = [1.25, 10.0, 1.25, 0.31];
290
+ } else if (m.includes("flash")) {
291
+ rates = [0.15, 0.6, 0.15, 0.0375];
292
+ } else if (
293
+ m.includes("minimax") ||
294
+ m.includes("glm") ||
295
+ m.includes("kimi") ||
296
+ m.includes("mimo") ||
297
+ m.includes("qwen")
298
+ ) {
299
+ rates = [0.5, 2.0, 0.5, 0.1];
300
+ }
301
+
302
+ const [inRate, outRate, cwRate, crRate] = rates;
303
+ return (
304
+ (params.inputTokens / M) * inRate +
305
+ (params.outputTokens / M) * outRate +
306
+ (params.cacheCreationTokens / M) * cwRate +
307
+ (params.cacheReadTokens / M) * crRate
308
+ );
309
+ }
310
+
311
+ async function runCcusageJson(subcommandArgs: string[], timeoutMs = 25000): Promise<any | null> {
312
+ const { cmd } = resolveCcusageBinary();
313
+ let timer: ReturnType<typeof setTimeout> | undefined;
314
+ try {
315
+ // stderr is never read, so piping it could fill the buffer and stall the child.
316
+ const proc = Bun.spawn([...cmd, ...subcommandArgs, "--json", "-O"], {
317
+ stdout: "pipe",
318
+ stderr: "ignore",
319
+ });
320
+
321
+ timer = setTimeout(() => {
322
+ try {
323
+ proc.kill();
324
+ } catch {
325
+ // ignore
326
+ }
327
+ }, timeoutMs);
328
+
329
+ const text = await new Response(proc.stdout).text();
330
+ const code = await proc.exited;
331
+ if (code !== 0 || !text.trim()) {
332
+ return null;
333
+ }
334
+ return JSON.parse(text);
335
+ } catch {
336
+ return null;
337
+ } finally {
338
+ if (timer) clearTimeout(timer);
339
+ }
340
+ }
341
+
342
+ interface RawHarnessData {
343
+ dailyRaw: any;
344
+ sessionRaw: any;
345
+ syncStatus: "ready" | "syncing" | "idle" | "error";
346
+ lastSyncMs?: number;
347
+ }
348
+
349
+ const inMemoryHarnessStore = new Map<HarnessId, RawHarnessData>();
350
+ let antigravitySyncInFlight = false;
351
+ let cachedDashboardPayload: DashboardPayload | null = null;
352
+ let refreshInFlight: Promise<DashboardPayload> | null = null;
353
+
354
+ function normalizeModelBreakdowns(row: any, harness: HarnessId): ModelMetric[] {
355
+ const list: ModelMetric[] = [];
356
+
357
+ if (Array.isArray(row.modelBreakdowns) && row.modelBreakdowns.length > 0) {
358
+ for (const mb of row.modelBreakdowns) {
359
+ const modelName = String(mb.modelName || "unknown");
360
+ const inputTokens = Number(mb.inputTokens || 0);
361
+ const outputTokens = Number(mb.outputTokens || 0);
362
+ const cacheCreationTokens = Number(mb.cacheCreationTokens || 0);
363
+ const cacheReadTokens = Number(mb.cacheReadTokens || 0);
364
+ const reasoningOutputTokens = Number(mb.reasoningOutputTokens || 0);
365
+ const totalTokens =
366
+ Number(mb.totalTokens || 0) ||
367
+ inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
368
+ const rawCost = Number(mb.cost ?? mb.costUSD ?? 0);
369
+ const missingPricing = Boolean(mb.missingPricing) || (rawCost === 0 && totalTokens > 0);
370
+ const verifiedCost = Math.max(0, rawCost);
371
+ const estimatedCost =
372
+ verifiedCost > 0
373
+ ? verifiedCost
374
+ : estimateFrontierCost({
375
+ modelName,
376
+ inputTokens,
377
+ outputTokens,
378
+ cacheCreationTokens,
379
+ cacheReadTokens,
380
+ reasoningOutputTokens,
381
+ });
382
+
383
+ list.push({
384
+ modelName,
385
+ harness,
386
+ inputTokens,
387
+ outputTokens,
388
+ cacheCreationTokens,
389
+ cacheReadTokens,
390
+ reasoningOutputTokens,
391
+ totalTokens,
392
+ verifiedCost,
393
+ estimatedCost,
394
+ missingPricing,
395
+ });
396
+ }
397
+ } else if (row.models && typeof row.models === "object" && !Array.isArray(row.models)) {
398
+ // Codex format: models is a Record<modelName, metrics>
399
+ const entries = Object.entries(row.models as Record<string, any>);
400
+ const rowCost = Math.max(0, Number(row.costUSD ?? row.totalCost ?? 0));
401
+ const rowTotalTokens = Number(row.totalTokens || 0);
402
+
403
+ for (const [modelName, mb] of entries) {
404
+ const inputTokens = Number(mb.inputTokens || 0);
405
+ const outputTokens = Number(mb.outputTokens || 0);
406
+ const cacheCreationTokens = Number(mb.cacheCreationTokens || 0);
407
+ const cacheReadTokens = Number(mb.cacheReadTokens || 0);
408
+ const reasoningOutputTokens = Number(mb.reasoningOutputTokens || 0);
409
+ const totalTokens =
410
+ Number(mb.totalTokens || 0) ||
411
+ inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
412
+ const missingPricing = Boolean(mb.missingPricing);
413
+
414
+ let verifiedCost = Number(mb.cost ?? mb.costUSD ?? 0);
415
+ if (verifiedCost === 0 && !missingPricing && rowCost > 0 && rowTotalTokens > 0) {
416
+ verifiedCost = rowCost * (totalTokens / rowTotalTokens);
417
+ }
418
+
419
+ const estimatedCost =
420
+ verifiedCost > 0
421
+ ? verifiedCost
422
+ : estimateFrontierCost({
423
+ modelName,
424
+ inputTokens,
425
+ outputTokens,
426
+ cacheCreationTokens,
427
+ cacheReadTokens,
428
+ reasoningOutputTokens,
429
+ });
430
+
431
+ list.push({
432
+ modelName,
433
+ harness,
434
+ inputTokens,
435
+ outputTokens,
436
+ cacheCreationTokens,
437
+ cacheReadTokens,
438
+ reasoningOutputTokens,
439
+ totalTokens,
440
+ verifiedCost,
441
+ estimatedCost,
442
+ missingPricing: missingPricing || (verifiedCost === 0 && totalTokens > 0),
443
+ });
444
+ }
445
+ }
446
+
447
+ return list;
448
+ }
449
+
450
+ function getIsoWeekKey(dateStr: string): { key: string; label: string } {
451
+ const parts = dateStr.split("-").map(Number);
452
+ if (parts.length !== 3 || parts.some(Number.isNaN)) {
453
+ return { key: dateStr, label: dateStr };
454
+ }
455
+ return isoWeek(parts[0]!, parts[1]!, parts[2]!);
456
+ }
457
+
458
+ function formatMonthLabel(ym: string): string {
459
+ const [y, m] = ym.split("-").map(Number);
460
+ if (!y || !m) return ym;
461
+ const date = new Date(Date.UTC(y, m - 1, 1));
462
+ return date.toLocaleDateString("en-US", { month: "short", year: "numeric", timeZone: "UTC" });
463
+ }
464
+
465
+ function loadAntigravityFromDiskCache(): RawHarnessData | null {
466
+ try {
467
+ if (existsSync(ANTIGRAVITY_DAILY_CACHE)) {
468
+ const dailyText = readFileSync(ANTIGRAVITY_DAILY_CACHE, "utf8").trim();
469
+ const sessionText = existsSync(ANTIGRAVITY_SESSION_CACHE)
470
+ ? readFileSync(ANTIGRAVITY_SESSION_CACHE, "utf8").trim()
471
+ : "";
472
+ if (dailyText) {
473
+ const dailyRaw = JSON.parse(dailyText);
474
+ const sessionRaw = sessionText ? JSON.parse(sessionText) : { sessions: [] };
475
+ return {
476
+ dailyRaw,
477
+ sessionRaw,
478
+ syncStatus: "ready",
479
+ };
480
+ }
481
+ }
482
+ } catch {
483
+ // ignore corrupt cache
484
+ }
485
+ return null;
486
+ }
487
+
488
+ async function syncAntigravityInBackground(): Promise<void> {
489
+ if (antigravitySyncInFlight) return;
490
+ antigravitySyncInFlight = true;
491
+
492
+ const existing = inMemoryHarnessStore.get("antigravity");
493
+ if (!existing) {
494
+ const fromDisk = loadAntigravityFromDiskCache();
495
+ if (fromDisk) {
496
+ inMemoryHarnessStore.set("antigravity", { ...fromDisk, syncStatus: "syncing" });
497
+ } else {
498
+ inMemoryHarnessStore.set("antigravity", {
499
+ dailyRaw: { daily: [] },
500
+ sessionRaw: { sessions: [] },
501
+ syncStatus: "syncing",
502
+ });
503
+ }
504
+ } else {
505
+ existing.syncStatus = "syncing";
506
+ }
507
+
508
+ const t0 = performance.now();
509
+ try {
510
+ const [dailyRaw, sessionRaw] = await Promise.all([
511
+ runCcusageJson(["antigravity", "daily"], 180_000),
512
+ runCcusageJson(["antigravity", "session"], 180_000),
513
+ ]);
514
+ const elapsed = Math.round(performance.now() - t0);
515
+
516
+ if (dailyRaw) {
517
+ const previousSessions = inMemoryHarnessStore.get("antigravity")?.sessionRaw;
518
+ inMemoryHarnessStore.set("antigravity", {
519
+ dailyRaw,
520
+ sessionRaw: sessionRaw ?? previousSessions ?? { sessions: [] },
521
+ syncStatus: "ready",
522
+ lastSyncMs: elapsed,
523
+ });
524
+ try {
525
+ mkdirSync(CACHE_DIR, { recursive: true });
526
+ writeFileSync(ANTIGRAVITY_DAILY_CACHE, JSON.stringify(dailyRaw, null, 2), "utf8");
527
+ if (sessionRaw) writeFileSync(ANTIGRAVITY_SESSION_CACHE, JSON.stringify(sessionRaw, null, 2), "utf8");
528
+ } catch {
529
+ // Fresh in-memory results remain usable if disk caching is unavailable.
530
+ }
531
+ // A fast refresh may finish before the deep scan. Rebuild after it settles so
532
+ // the finished scan is always reflected in the dashboard and on disk.
533
+ if (refreshInFlight) {
534
+ try { await refreshInFlight; } catch { /* Keep the deep-scan result. */ }
535
+ }
536
+ cachedDashboardPayload = buildDashboardPayloadFromStore();
537
+ try {
538
+ writeFileSync(DASHBOARD_CACHE_FILE, JSON.stringify(cachedDashboardPayload), "utf8");
539
+ } catch {
540
+ // Fresh in-memory results remain usable if disk caching is unavailable.
541
+ }
542
+ } else {
543
+ const prev = inMemoryHarnessStore.get("antigravity");
544
+ if (prev) prev.syncStatus = "error";
545
+ }
546
+ } catch {
547
+ const prev = inMemoryHarnessStore.get("antigravity");
548
+ if (prev) prev.syncStatus = "error";
549
+ } finally {
550
+ antigravitySyncInFlight = false;
551
+ }
552
+ }
553
+
554
+ async function runHarnessRefresh(options?: { forceDeepScan?: boolean }): Promise<DashboardPayload> {
555
+ mkdirSync(CACHE_DIR, { recursive: true });
556
+
557
+ // Seed antigravity from disk cache immediately if available
558
+ const diskAg = loadAntigravityFromDiskCache();
559
+ if (diskAg && !inMemoryHarnessStore.has("antigravity")) {
560
+ inMemoryHarnessStore.set("antigravity", diskAg);
561
+ }
562
+
563
+ // Trigger background sync for Antigravity if we don't have disk cache or if forceDeepScan is true
564
+ if (!diskAg || options?.forceDeepScan) {
565
+ void syncAntigravityInBackground();
566
+ }
567
+
568
+ // Run all 17 fast harnesses in parallel
569
+ const fastHarnesses = HARNESS_DEFINITIONS.filter((h) => !h.slowScan);
570
+ await Promise.all(
571
+ fastHarnesses.map(async (h) => {
572
+ const t0 = performance.now();
573
+ const [dailyRaw, sessionRaw] = await Promise.all([
574
+ runCcusageJson([h.id, "daily"], 15_000),
575
+ runCcusageJson([h.id, "session"], 15_000),
576
+ ]);
577
+ const elapsed = Math.round(performance.now() - t0);
578
+ // null means ccusage failed or timed out (a harness with no data still returns
579
+ // empty JSON), so keep the last good data rather than wiping it to zero.
580
+ const prev = inMemoryHarnessStore.get(h.id);
581
+ inMemoryHarnessStore.set(h.id, {
582
+ dailyRaw: dailyRaw ?? prev?.dailyRaw ?? { daily: [] },
583
+ sessionRaw: sessionRaw ?? prev?.sessionRaw ?? { sessions: [] },
584
+ syncStatus: dailyRaw && sessionRaw ? "ready" : "error",
585
+ lastSyncMs: elapsed,
586
+ });
587
+ })
588
+ );
589
+
590
+ // Also check if antigravity cache arrived on disk while fast harnesses ran
591
+ if (!inMemoryHarnessStore.get("antigravity")?.dailyRaw?.daily?.length) {
592
+ const freshDiskAg = loadAntigravityFromDiskCache();
593
+ if (freshDiskAg) {
594
+ inMemoryHarnessStore.set("antigravity", freshDiskAg);
595
+ }
596
+ }
597
+
598
+ cachedDashboardPayload = buildDashboardPayloadFromStore();
599
+ try {
600
+ writeFileSync(DASHBOARD_CACHE_FILE, JSON.stringify(cachedDashboardPayload), "utf8");
601
+ } catch {
602
+ // ignore
603
+ }
604
+ return cachedDashboardPayload;
605
+ }
606
+
607
+ export function refreshAllHarnesses(options?: { forceDeepScan?: boolean }): Promise<DashboardPayload> {
608
+ if (refreshInFlight) {
609
+ if (options?.forceDeepScan) void syncAntigravityInBackground();
610
+ return refreshInFlight;
611
+ }
612
+ const run = runHarnessRefresh(options);
613
+ refreshInFlight = run;
614
+ void run.then(
615
+ () => { if (refreshInFlight === run) refreshInFlight = null; },
616
+ () => { if (refreshInFlight === run) refreshInFlight = null; },
617
+ );
618
+ return run;
619
+ }
620
+
621
+ export async function getDashboardData(options?: {
622
+ refresh?: boolean;
623
+ forceDeepScan?: boolean;
624
+ }): Promise<DashboardPayload> {
625
+ if (!options?.refresh && cachedDashboardPayload) {
626
+ // Check if antigravity disk cache newly appeared
627
+ const agSummary = cachedDashboardPayload.harnesses.find((h) => h.meta.id === "antigravity");
628
+ if (agSummary && agSummary.totalTokens === 0 && existsSync(ANTIGRAVITY_DAILY_CACHE)) {
629
+ const diskAg = loadAntigravityFromDiskCache();
630
+ if (diskAg && diskAg.dailyRaw?.daily?.length > 0) {
631
+ inMemoryHarnessStore.set("antigravity", diskAg);
632
+ cachedDashboardPayload = buildDashboardPayloadFromStore();
633
+ }
634
+ }
635
+ return cachedDashboardPayload;
636
+ }
637
+
638
+ if (!options?.refresh && existsSync(DASHBOARD_CACHE_FILE) && inMemoryHarnessStore.size === 0) {
639
+ try {
640
+ const cached = JSON.parse(readFileSync(DASHBOARD_CACHE_FILE, "utf8")) as DashboardPayload;
641
+ cachedDashboardPayload = cached;
642
+ // Refresh fast harnesses in background
643
+ void refreshAllHarnesses({ forceDeepScan: false });
644
+ return cached;
645
+ } catch {
646
+ // fall through
647
+ }
648
+ }
649
+
650
+ return refreshAllHarnesses({ forceDeepScan: options?.forceDeepScan });
651
+ }
652
+
653
+ function addMetricToPeriod(
654
+ map: Map<string, TimePeriodRow>,
655
+ periodKey: string,
656
+ periodLabel: string,
657
+ harnessId: HarnessId,
658
+ rowMetrics: {
659
+ inputTokens: number;
660
+ outputTokens: number;
661
+ cacheCreationTokens: number;
662
+ cacheReadTokens: number;
663
+ reasoningOutputTokens: number;
664
+ totalTokens: number;
665
+ verifiedCost: number;
666
+ estimatedCost: number;
667
+ models: ModelMetric[];
668
+ }
669
+ ): void {
670
+ let periodRow = map.get(periodKey);
671
+ if (!periodRow) {
672
+ periodRow = {
673
+ period: periodKey,
674
+ label: periodLabel,
675
+ inputTokens: 0,
676
+ outputTokens: 0,
677
+ cacheCreationTokens: 0,
678
+ cacheReadTokens: 0,
679
+ reasoningOutputTokens: 0,
680
+ totalTokens: 0,
681
+ verifiedCost: 0,
682
+ estimatedCost: 0,
683
+ byHarness: {},
684
+ modelsUsed: [],
685
+ };
686
+ map.set(periodKey, periodRow);
687
+ }
688
+
689
+ periodRow.inputTokens += rowMetrics.inputTokens;
690
+ periodRow.outputTokens += rowMetrics.outputTokens;
691
+ periodRow.cacheCreationTokens += rowMetrics.cacheCreationTokens;
692
+ periodRow.cacheReadTokens += rowMetrics.cacheReadTokens;
693
+ periodRow.reasoningOutputTokens += rowMetrics.reasoningOutputTokens;
694
+ periodRow.totalTokens += rowMetrics.totalTokens;
695
+ periodRow.verifiedCost += rowMetrics.verifiedCost;
696
+ periodRow.estimatedCost += rowMetrics.estimatedCost;
697
+
698
+ let hb: PeriodHarnessBreakdown | undefined = periodRow.byHarness[harnessId];
699
+ if (!hb) {
700
+ hb = {
701
+ harness: harnessId,
702
+ inputTokens: 0,
703
+ outputTokens: 0,
704
+ cacheCreationTokens: 0,
705
+ cacheReadTokens: 0,
706
+ reasoningOutputTokens: 0,
707
+ totalTokens: 0,
708
+ verifiedCost: 0,
709
+ estimatedCost: 0,
710
+ models: [],
711
+ };
712
+ periodRow.byHarness[harnessId] = hb;
713
+ }
714
+
715
+ hb.inputTokens += rowMetrics.inputTokens;
716
+ hb.outputTokens += rowMetrics.outputTokens;
717
+ hb.cacheCreationTokens += rowMetrics.cacheCreationTokens;
718
+ hb.cacheReadTokens += rowMetrics.cacheReadTokens;
719
+ hb.reasoningOutputTokens += rowMetrics.reasoningOutputTokens;
720
+ hb.totalTokens += rowMetrics.totalTokens;
721
+ hb.verifiedCost += rowMetrics.verifiedCost;
722
+ hb.estimatedCost += rowMetrics.estimatedCost;
723
+
724
+ for (const m of rowMetrics.models) {
725
+ if (!periodRow.modelsUsed.includes(m.modelName)) {
726
+ periodRow.modelsUsed.push(m.modelName);
727
+ }
728
+ const existingModel = hb.models.find((x) => x.modelName === m.modelName);
729
+ if (existingModel) {
730
+ existingModel.inputTokens += m.inputTokens;
731
+ existingModel.outputTokens += m.outputTokens;
732
+ existingModel.cacheCreationTokens += m.cacheCreationTokens;
733
+ existingModel.cacheReadTokens += m.cacheReadTokens;
734
+ existingModel.reasoningOutputTokens += m.reasoningOutputTokens;
735
+ existingModel.totalTokens += m.totalTokens;
736
+ existingModel.verifiedCost += m.verifiedCost;
737
+ existingModel.estimatedCost += m.estimatedCost;
738
+ } else {
739
+ hb.models.push({ ...m });
740
+ }
741
+ }
742
+ }
743
+
744
+ function buildDashboardPayloadFromStore(): DashboardPayload {
745
+ const { version } = resolveCcusageBinary();
746
+
747
+ const dailyMap = new Map<string, TimePeriodRow>();
748
+ const weeklyMap = new Map<string, TimePeriodRow>();
749
+ const monthlyMap = new Map<string, TimePeriodRow>();
750
+ const yearlyMap = new Map<string, TimePeriodRow>();
751
+ const globalModelMap = new Map<string, ModelMetric>();
752
+ const allSessions: SessionEntry[] = [];
753
+ const harnessSummaries: HarnessSummary[] = [];
754
+ const syncingHarnesses: HarnessId[] = [];
755
+
756
+ for (const def of HARNESS_DEFINITIONS) {
757
+ const raw = inMemoryHarnessStore.get(def.id);
758
+ const dailyRows: any[] = Array.isArray(raw?.dailyRaw?.daily) ? raw.dailyRaw.daily : [];
759
+ const sessionRows: any[] = Array.isArray(raw?.sessionRaw?.sessions)
760
+ ? raw.sessionRaw.sessions
761
+ : [];
762
+ const unpricedModels: string[] = Array.isArray(raw?.dailyRaw?.totals?.unpricedModels)
763
+ ? raw.dailyRaw.totals.unpricedModels
764
+ : [];
765
+
766
+ const installedOnDisk = def.detectPaths.some((p) => existsSync(p));
767
+ const hasUsage = dailyRows.length > 0 || sessionRows.length > 0;
768
+ const installed = installedOnDisk || hasUsage;
769
+ const syncStatus = raw?.syncStatus ?? (def.slowScan ? "syncing" : "idle");
770
+
771
+ if (syncStatus === "syncing") {
772
+ syncingHarnesses.push(def.id);
773
+ }
774
+
775
+ const harnessModelMap = new Map<string, ModelMetric>();
776
+ let hInput = 0;
777
+ let hOutput = 0;
778
+ let hCacheCreate = 0;
779
+ let hCacheRead = 0;
780
+ let hReasoning = 0;
781
+ let hTotal = 0;
782
+ let hVerifiedCost = 0;
783
+ let hEstimatedCost = 0;
784
+ let lastActiveDate: string | null = null;
785
+
786
+ for (const dRow of dailyRows) {
787
+ const dateStr = String(dRow.date || "");
788
+ if (!dateStr) continue;
789
+ if (!lastActiveDate || dateStr > lastActiveDate) {
790
+ lastActiveDate = dateStr;
791
+ }
792
+
793
+ const inputTokens = Number(dRow.inputTokens || 0);
794
+ const outputTokens = Number(dRow.outputTokens || 0);
795
+ const cacheCreationTokens = Number(dRow.cacheCreationTokens || 0);
796
+ const cacheReadTokens = Number(dRow.cacheReadTokens || 0);
797
+ const reasoningOutputTokens = Number(dRow.reasoningOutputTokens || 0);
798
+ const totalTokens =
799
+ Number(dRow.totalTokens || 0) ||
800
+ inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
801
+
802
+ const models = normalizeModelBreakdowns(dRow, def.id);
803
+ const rowRawCost = Math.max(0, Number(dRow.totalCost ?? dRow.costUSD ?? 0));
804
+ const modelsVerifiedSum = models.reduce((acc, m) => acc + m.verifiedCost, 0);
805
+ const modelsEstimatedSum = models.reduce((acc, m) => acc + m.estimatedCost, 0);
806
+
807
+ const verifiedCost = Math.max(rowRawCost, modelsVerifiedSum);
808
+ const estimatedCost =
809
+ modelsEstimatedSum > 0
810
+ ? Math.max(verifiedCost, modelsEstimatedSum)
811
+ : verifiedCost > 0
812
+ ? verifiedCost
813
+ : estimateFrontierCost({
814
+ modelName: def.id,
815
+ inputTokens,
816
+ outputTokens,
817
+ cacheCreationTokens,
818
+ cacheReadTokens,
819
+ reasoningOutputTokens,
820
+ });
821
+
822
+ hInput += inputTokens;
823
+ hOutput += outputTokens;
824
+ hCacheCreate += cacheCreationTokens;
825
+ hCacheRead += cacheReadTokens;
826
+ hReasoning += reasoningOutputTokens;
827
+ hTotal += totalTokens;
828
+ hVerifiedCost += verifiedCost;
829
+ hEstimatedCost += estimatedCost;
830
+
831
+ for (const m of models) {
832
+ const prevH = harnessModelMap.get(m.modelName);
833
+ if (prevH) {
834
+ prevH.inputTokens += m.inputTokens;
835
+ prevH.outputTokens += m.outputTokens;
836
+ prevH.cacheCreationTokens += m.cacheCreationTokens;
837
+ prevH.cacheReadTokens += m.cacheReadTokens;
838
+ prevH.reasoningOutputTokens += m.reasoningOutputTokens;
839
+ prevH.totalTokens += m.totalTokens;
840
+ prevH.verifiedCost += m.verifiedCost;
841
+ prevH.estimatedCost += m.estimatedCost;
842
+ prevH.missingPricing = prevH.missingPricing || m.missingPricing;
843
+ } else {
844
+ harnessModelMap.set(m.modelName, { ...m });
845
+ }
846
+
847
+ const gKey = `${def.id}::${m.modelName}`;
848
+ const prevG = globalModelMap.get(gKey);
849
+ if (prevG) {
850
+ prevG.inputTokens += m.inputTokens;
851
+ prevG.outputTokens += m.outputTokens;
852
+ prevG.cacheCreationTokens += m.cacheCreationTokens;
853
+ prevG.cacheReadTokens += m.cacheReadTokens;
854
+ prevG.reasoningOutputTokens += m.reasoningOutputTokens;
855
+ prevG.totalTokens += m.totalTokens;
856
+ prevG.verifiedCost += m.verifiedCost;
857
+ prevG.estimatedCost += m.estimatedCost;
858
+ prevG.missingPricing = prevG.missingPricing || m.missingPricing;
859
+ } else {
860
+ globalModelMap.set(gKey, { ...m });
861
+ }
862
+ }
863
+
864
+ const periodMetrics = {
865
+ inputTokens,
866
+ outputTokens,
867
+ cacheCreationTokens,
868
+ cacheReadTokens,
869
+ reasoningOutputTokens,
870
+ totalTokens,
871
+ verifiedCost,
872
+ estimatedCost,
873
+ models,
874
+ };
875
+
876
+ // 1. Daily
877
+ addMetricToPeriod(dailyMap, dateStr, dateStr, def.id, periodMetrics);
878
+
879
+ // 2. Weekly
880
+ const weekInfo = getIsoWeekKey(dateStr);
881
+ addMetricToPeriod(weeklyMap, weekInfo.key, weekInfo.label, def.id, periodMetrics);
882
+
883
+ // 3. Monthly
884
+ const monthKey = dateStr.slice(0, 7);
885
+ addMetricToPeriod(
886
+ monthlyMap,
887
+ monthKey,
888
+ formatMonthLabel(monthKey),
889
+ def.id,
890
+ periodMetrics
891
+ );
892
+
893
+ // 4. Yearly
894
+ const yearKey = dateStr.slice(0, 4);
895
+ addMetricToPeriod(yearlyMap, yearKey, yearKey, def.id, periodMetrics);
896
+ }
897
+
898
+ // Parse sessions
899
+ for (const [sIndex, sRow] of sessionRows.entries()) {
900
+ const inputTokens = Number(sRow.inputTokens || 0);
901
+ const outputTokens = Number(sRow.outputTokens || 0);
902
+ const cacheCreationTokens = Number(sRow.cacheCreationTokens || 0);
903
+ const cacheReadTokens = Number(sRow.cacheReadTokens || 0);
904
+ const reasoningOutputTokens = Number(sRow.reasoningOutputTokens || 0);
905
+ const totalTokens =
906
+ Number(sRow.totalTokens || 0) ||
907
+ inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
908
+
909
+ const modelBreakdowns = normalizeModelBreakdowns(sRow, def.id);
910
+ const rawCost = Math.max(0, Number(sRow.totalCost ?? sRow.costUSD ?? 0));
911
+ const modelsEst = modelBreakdowns.reduce((acc, m) => acc + m.estimatedCost, 0);
912
+ const verifiedCost = rawCost;
913
+ const estimatedCost = Math.max(verifiedCost, modelsEst);
914
+
915
+ const sessionId = sRow.sessionId ? String(sRow.sessionId) : "";
916
+ const opencodeUpdated = def.id === "opencode" && sessionId ? opencodeSessionUpdated(sessionId) : null;
917
+ const lastActivity = sRow.lastActivity
918
+ ? String(sRow.lastActivity)
919
+ : opencodeUpdated
920
+ ? new Date(opencodeUpdated).toISOString()
921
+ : undefined;
922
+ const firstActivity = sRow.firstActivity ? String(sRow.firstActivity) : undefined;
923
+ // Timestamps are UTC; daily rows are local days, so bucket sessions the same way.
924
+ let date = lastActivity ? localDateKey(new Date(lastActivity)) : "";
925
+ // Codex groups rollouts under YYYY/MM/DD folders.
926
+ if (!date && /^\d{4}\/\d{2}\/\d{2}$/.test(String(sRow.directory ?? ""))) {
927
+ date = String(sRow.directory).replace(/\//g, "-");
928
+ }
929
+
930
+ const rawProject = String(
931
+ sRow.projectPath || sRow.directory || sRow.sessionFile || sRow.sessionId || def.name
932
+ );
933
+ const project = sessionId
934
+ ? resolveSessionProject(def.id, sessionId, String(sRow.projectPath ?? ""))
935
+ : null;
936
+ const modelsUsed = modelBreakdowns.length
937
+ ? modelBreakdowns.map((m) => m.modelName)
938
+ : Array.isArray(sRow.modelsUsed)
939
+ ? sRow.modelsUsed.map(String)
940
+ : [];
941
+
942
+ allSessions.push({
943
+ id: `${def.id}-${sessionId || `${date}-${sIndex}`}`,
944
+ harness: def.id,
945
+ projectPath: rawProject,
946
+ cwd: project?.cwd,
947
+ projectRoot: project?.root,
948
+ projectName: project?.name,
949
+ firstActivity,
950
+ lastActivity,
951
+ date: date || "Unknown",
952
+ inputTokens,
953
+ outputTokens,
954
+ cacheCreationTokens,
955
+ cacheReadTokens,
956
+ reasoningOutputTokens,
957
+ totalTokens,
958
+ verifiedCost,
959
+ estimatedCost,
960
+ modelsUsed,
961
+ modelBreakdowns,
962
+ });
963
+ }
964
+
965
+ const harnessModels = Array.from(harnessModelMap.values()).sort(
966
+ (a, b) => b.totalTokens - a.totalTokens
967
+ );
968
+ const topModel = harnessModels[0]?.modelName ?? null;
969
+ const totalPromptSide = hInput + hCacheRead + hCacheCreate;
970
+ const cacheHitRate =
971
+ totalPromptSide > 0 ? Math.round((hCacheRead / totalPromptSide) * 1000) / 10 : 0;
972
+
973
+ const meta: HarnessMetadata = {
974
+ id: def.id,
975
+ name: def.name,
976
+ vendor: def.vendor,
977
+ color: def.color,
978
+ accentBg: def.accentBg,
979
+ description: def.description,
980
+ configPathHint: def.configPathHint,
981
+ installed,
982
+ hasUsage,
983
+ syncStatus,
984
+ lastSyncMs: raw?.lastSyncMs,
985
+ };
986
+
987
+ harnessSummaries.push({
988
+ meta,
989
+ inputTokens: hInput,
990
+ outputTokens: hOutput,
991
+ cacheCreationTokens: hCacheCreate,
992
+ cacheReadTokens: hCacheRead,
993
+ reasoningOutputTokens: hReasoning,
994
+ totalTokens: hTotal,
995
+ verifiedCost: hVerifiedCost,
996
+ estimatedCost: hEstimatedCost,
997
+ activeDays: dailyRows.length,
998
+ sessionCount: sessionRows.length || dailyRows.length,
999
+ lastActiveDate,
1000
+ topModel,
1001
+ cacheHitRate,
1002
+ models: harnessModels,
1003
+ unpricedModels,
1004
+ });
1005
+ }
1006
+
1007
+ // Sort harnesses: active with most tokens first, then installed, then alphabetical
1008
+ harnessSummaries.sort((a, b) => {
1009
+ if (b.totalTokens !== a.totalTokens) return b.totalTokens - a.totalTokens;
1010
+ if (a.meta.installed !== b.meta.installed) return a.meta.installed ? -1 : 1;
1011
+ return a.meta.name.localeCompare(b.meta.name);
1012
+ });
1013
+
1014
+ const daily = Array.from(dailyMap.values()).sort((a, b) => a.period.localeCompare(b.period));
1015
+ const weekly = Array.from(weeklyMap.values()).sort((a, b) => a.period.localeCompare(b.period));
1016
+ const monthly = Array.from(monthlyMap.values()).sort((a, b) =>
1017
+ a.period.localeCompare(b.period)
1018
+ );
1019
+ const yearly = Array.from(yearlyMap.values()).sort((a, b) => a.period.localeCompare(b.period));
1020
+ const models = Array.from(globalModelMap.values()).sort(
1021
+ (a, b) => b.totalTokens - a.totalTokens
1022
+ );
1023
+
1024
+ allSessions.sort((a, b) => {
1025
+ const da = a.lastActivity || a.date || "";
1026
+ const db = b.lastActivity || b.date || "";
1027
+ if (db !== da) return db.localeCompare(da);
1028
+ return b.totalTokens - a.totalTokens;
1029
+ });
1030
+
1031
+ const totalInput = harnessSummaries.reduce((acc, h) => acc + h.inputTokens, 0);
1032
+ const totalOutput = harnessSummaries.reduce((acc, h) => acc + h.outputTokens, 0);
1033
+ const totalCacheCreate = harnessSummaries.reduce((acc, h) => acc + h.cacheCreationTokens, 0);
1034
+ const totalCacheRead = harnessSummaries.reduce((acc, h) => acc + h.cacheReadTokens, 0);
1035
+ const totalReasoning = harnessSummaries.reduce((acc, h) => acc + h.reasoningOutputTokens, 0);
1036
+ const grandTotalTokens = harnessSummaries.reduce((acc, h) => acc + h.totalTokens, 0);
1037
+ const grandVerifiedCost = harnessSummaries.reduce((acc, h) => acc + h.verifiedCost, 0);
1038
+ const grandEstimatedCost = harnessSummaries.reduce((acc, h) => acc + h.estimatedCost, 0);
1039
+
1040
+ const promptTotal = totalInput + totalCacheRead + totalCacheCreate;
1041
+ const globalCacheHitRate =
1042
+ promptTotal > 0 ? Math.round((totalCacheRead / promptTotal) * 1000) / 10 : 0;
1043
+
1044
+ saveSessionProjects();
1045
+
1046
+ return {
1047
+ generatedAt: new Date().toISOString(),
1048
+ ccusageVersion: version,
1049
+ syncingHarnesses,
1050
+ totals: {
1051
+ inputTokens: totalInput,
1052
+ outputTokens: totalOutput,
1053
+ cacheCreationTokens: totalCacheCreate,
1054
+ cacheReadTokens: totalCacheRead,
1055
+ reasoningOutputTokens: totalReasoning,
1056
+ totalTokens: grandTotalTokens,
1057
+ verifiedCost: grandVerifiedCost,
1058
+ estimatedCost: grandEstimatedCost,
1059
+ activeHarnesses: harnessSummaries.filter((h) => h.meta.hasUsage).length,
1060
+ installedHarnesses: harnessSummaries.filter((h) => h.meta.installed).length,
1061
+ totalHarnesses: HARNESS_DEFINITIONS.length,
1062
+ activeDays: daily.length,
1063
+ totalSessions: allSessions.length,
1064
+ cacheHitRate: globalCacheHitRate,
1065
+ },
1066
+ harnesses: harnessSummaries,
1067
+ daily,
1068
+ weekly,
1069
+ monthly,
1070
+ yearly,
1071
+ models,
1072
+ // Date-range filters and the Projects view need the full history, not just recent sessions.
1073
+ sessions: allSessions.slice(0, 5000),
1074
+ };
1075
+ }