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.
- package/README.md +334 -0
- package/Start-TokenLarper.vbs +5 -0
- package/bin/cli.js +34 -0
- package/llms.txt +59 -0
- package/package.json +61 -0
- package/scripts/docs-demo-server.ts +141 -0
- package/scripts/launch-on-default-desktop.ps1 +42 -0
- package/scripts/launch-silent.vbs +6 -0
- package/scripts/run-server.ps1 +59 -0
- package/scripts/tray-host.ps1 +181 -0
- package/src/ccusage.ts +1075 -0
- package/src/client/App.tsx +560 -0
- package/src/client/charts.tsx +341 -0
- package/src/client/components/Calendar.tsx +207 -0
- package/src/client/components/DateRangePicker.tsx +131 -0
- package/src/client/components/KpiCards.tsx +122 -0
- package/src/client/components/RankChip.tsx +22 -0
- package/src/client/components/SelectMenu.tsx +116 -0
- package/src/client/components/SettingsDialog.tsx +207 -0
- package/src/client/components/ThemeSettings.tsx +131 -0
- package/src/client/context.tsx +60 -0
- package/src/client/index.html +54 -0
- package/src/client/lib/aggregate.ts +266 -0
- package/src/client/lib/range.ts +87 -0
- package/src/client/lib/rank.ts +323 -0
- package/src/client/lib/shareCard.ts +139 -0
- package/src/client/logo.svg +8 -0
- package/src/client/main.tsx +12 -0
- package/src/client/styles.css +1113 -0
- package/src/client/themes.ts +268 -0
- package/src/client/utils.ts +61 -0
- package/src/client/views/Models.tsx +71 -0
- package/src/client/views/Overview.tsx +280 -0
- package/src/client/views/Projects.tsx +88 -0
- package/src/client/views/Rank.tsx +255 -0
- package/src/client/views/Sessions.tsx +108 -0
- package/src/client/views/Tools.tsx +114 -0
- package/src/projects.ts +214 -0
- package/src/server.ts +173 -0
- package/src/startup.ts +110 -0
- package/src/tray.ts +293 -0
- 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
|
+
}
|