tokentrack 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.
@@ -0,0 +1,1002 @@
1
+ // src/server.ts
2
+ import { serve } from "@hono/node-server";
3
+ import { Hono as Hono2 } from "hono";
4
+
5
+ // src/api/routes.ts
6
+ import { Hono } from "hono";
7
+ import { cors } from "hono/cors";
8
+
9
+ // src/data/providers/claude-code.ts
10
+ import { createReadStream } from "fs";
11
+ import { createInterface } from "readline";
12
+ import { homedir as homedir2 } from "os";
13
+ import { join as join2 } from "path";
14
+ import { glob } from "glob";
15
+ import { existsSync as existsSync2 } from "fs";
16
+
17
+ // src/data/pricing.ts
18
+ var PRICING_TABLE = {
19
+ // Anthropic — current gen (keys use both . and - for matching)
20
+ "claude-opus-4-6": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5, provider: "anthropic" },
21
+ "claude-opus-4.6": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5, provider: "anthropic" },
22
+ "claude-sonnet-4-6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
23
+ "claude-sonnet-4.6": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
24
+ "claude-opus-4-5": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5, provider: "anthropic" },
25
+ "claude-opus-4.5": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5, provider: "anthropic" },
26
+ "claude-sonnet-4-5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
27
+ "claude-sonnet-4.5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
28
+ "claude-haiku-4-5": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1, provider: "anthropic" },
29
+ "claude-haiku-4.5": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1, provider: "anthropic" },
30
+ // Legacy gen
31
+ "claude-opus-4-1": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5, provider: "anthropic" },
32
+ "claude-opus-4.1": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5, provider: "anthropic" },
33
+ "claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5, provider: "anthropic" },
34
+ "claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
35
+ "claude-sonnet-3-5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
36
+ "claude-sonnet-3.5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3, provider: "anthropic" },
37
+ "claude-haiku-3-5": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08, provider: "anthropic" },
38
+ "claude-haiku-3.5": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08, provider: "anthropic" },
39
+ "claude-haiku-3": { input: 0.25, output: 1.25, cacheWrite: 0.3, cacheRead: 0.025, provider: "anthropic" },
40
+ // OpenAI
41
+ "gpt-5.4": { input: 2.5, output: 15, cachedInput: 1.25, provider: "openai" },
42
+ "gpt-5.4-mini": { input: 0.4, output: 1.6, cachedInput: 0.1, provider: "openai" },
43
+ "gpt-5.4-nano": { input: 0.1, output: 0.4, cachedInput: 0.025, provider: "openai" },
44
+ "gpt-5.4-pro": { input: 30, output: 180, cachedInput: 15, provider: "openai" },
45
+ "gpt-5.3-codex": { input: 1.25, output: 10, cachedInput: 0.625, provider: "openai" },
46
+ "gpt-5.3": { input: 1.75, output: 14, cachedInput: 0.875, provider: "openai" },
47
+ "gpt-5.2-codex": { input: 1.25, output: 10, cachedInput: 0.625, provider: "openai" },
48
+ "gpt-5.1-codex-max": { input: 1.25, output: 10, cachedInput: 0.625, provider: "openai" },
49
+ "gpt-5-codex": { input: 1.25, output: 10, cachedInput: 0.625, provider: "openai" },
50
+ "gpt-4.1": { input: 2, output: 8, cachedInput: 0.5, provider: "openai" },
51
+ "gpt-4.1-mini": { input: 0.4, output: 1.6, cachedInput: 0.1, provider: "openai" },
52
+ "gpt-4.1-nano": { input: 0.1, output: 0.4, cachedInput: 0.025, provider: "openai" },
53
+ "gpt-4o": { input: 2.5, output: 10, cachedInput: 1.25, provider: "openai" },
54
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cachedInput: 0.075, provider: "openai" },
55
+ "o3": { input: 10, output: 40, cachedInput: 2.5, provider: "openai" },
56
+ "o3-mini": { input: 1.1, output: 4.4, cachedInput: 0.275, provider: "openai" },
57
+ "o4-mini": { input: 1.1, output: 4.4, cachedInput: 0.275, provider: "openai" },
58
+ // Google
59
+ "gemini-3-pro": { input: 1.25, output: 10, provider: "google" },
60
+ "gemini-2.5-pro": { input: 1.25, output: 10, provider: "google" },
61
+ "gemini-2.5-flash": { input: 0.15, output: 0.6, provider: "google" },
62
+ "gemini-2.0-flash": { input: 0.1, output: 0.4, provider: "google" }
63
+ };
64
+ var FALLBACKS = {
65
+ anthropic: "claude-sonnet-4.6",
66
+ openai: "gpt-5.3-codex",
67
+ google: "gemini-3-pro"
68
+ };
69
+ function detectProvider(model) {
70
+ const lower = model.toLowerCase();
71
+ if (lower.includes("claude") || lower.includes("sonnet") || lower.includes("opus") || lower.includes("haiku")) return "anthropic";
72
+ if (lower.includes("gpt") || lower.includes("o3") || lower.includes("o4") || lower.includes("codex")) return "openai";
73
+ if (lower.includes("gemini")) return "google";
74
+ return "anthropic";
75
+ }
76
+ function normalizeToPricingKey(model) {
77
+ let name = model.toLowerCase().trim();
78
+ name = name.replace(/^anthropic[./]/, "");
79
+ name = name.replace(/^\[.*?\]\s*/, "");
80
+ name = name.replace(/-\d{8}$/, "");
81
+ name = name.replace(/-[a-f0-9]{6,}$/, "");
82
+ return name;
83
+ }
84
+ function findPricing(modelName) {
85
+ const key = normalizeToPricingKey(modelName);
86
+ if (PRICING_TABLE[key]) return PRICING_TABLE[key];
87
+ const keys = Object.keys(PRICING_TABLE).sort((a, b) => b.length - a.length);
88
+ for (const k of keys) {
89
+ if (key.startsWith(k) || key.includes(k)) return PRICING_TABLE[k];
90
+ }
91
+ const provider = detectProvider(modelName);
92
+ return PRICING_TABLE[FALLBACKS[provider]];
93
+ }
94
+ function calculateCost(model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, speed) {
95
+ const pricing = findPricing(model);
96
+ let multiplier = 1;
97
+ if (speed === "fast" && model.toLowerCase().includes("opus-4.6")) {
98
+ multiplier = 6;
99
+ }
100
+ let cost = 0;
101
+ cost += inputTokens * pricing.input * multiplier / 1e6;
102
+ cost += outputTokens * pricing.output * multiplier / 1e6;
103
+ if (pricing.cacheWrite) {
104
+ cost += cacheWriteTokens * pricing.cacheWrite * multiplier / 1e6;
105
+ }
106
+ if (pricing.cacheRead) {
107
+ cost += cacheReadTokens * pricing.cacheRead * multiplier / 1e6;
108
+ }
109
+ if (pricing.cachedInput && !pricing.cacheRead) {
110
+ cost += cacheReadTokens * pricing.cachedInput * multiplier / 1e6;
111
+ }
112
+ return Math.round(cost * 1e6) / 1e6;
113
+ }
114
+ function normalizeModelDisplay(raw) {
115
+ if (!raw) return "unknown";
116
+ let name = raw;
117
+ let prefix = "";
118
+ const prefixMatch = name.match(/^(\[.*?\]\s*)/);
119
+ if (prefixMatch) {
120
+ prefix = prefixMatch[1];
121
+ name = name.slice(prefix.length);
122
+ }
123
+ name = name.replace(/^anthropic[./]/, "");
124
+ name = name.replace(/^claude-/, "");
125
+ name = name.replace(/-\d{8}$/, "");
126
+ return prefix + name;
127
+ }
128
+ function getPricingTable() {
129
+ return { ...PRICING_TABLE };
130
+ }
131
+
132
+ // src/data/litellm-pricing.ts
133
+ import { homedir } from "os";
134
+ import { join } from "path";
135
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
136
+ var LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
137
+ var CACHE_DIR = join(homedir(), ".tokentrack");
138
+ var CACHE_PATH = join(CACHE_DIR, "litellm-pricing.json");
139
+ var CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
140
+ var cachedPricing = null;
141
+ function isCacheFresh() {
142
+ try {
143
+ if (!existsSync(CACHE_PATH)) return false;
144
+ const stat = statSync(CACHE_PATH);
145
+ return Date.now() - stat.mtimeMs < CACHE_MAX_AGE_MS;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ function readCache() {
151
+ try {
152
+ if (!existsSync(CACHE_PATH)) return null;
153
+ const data = JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
154
+ return parseRawPricing(data);
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+ function writeCache(data) {
160
+ try {
161
+ if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
162
+ writeFileSync(CACHE_PATH, JSON.stringify(data), "utf-8");
163
+ } catch {
164
+ }
165
+ }
166
+ function parseRawPricing(data) {
167
+ const map = /* @__PURE__ */ new Map();
168
+ for (const [key, val] of Object.entries(data)) {
169
+ if (val && typeof val === "object" && typeof val.input_cost_per_token === "number") {
170
+ map.set(key, {
171
+ input_cost_per_token: val.input_cost_per_token,
172
+ output_cost_per_token: val.output_cost_per_token || 0,
173
+ cache_read_input_token_cost: val.cache_read_input_token_cost,
174
+ cache_creation_input_token_cost: val.cache_creation_input_token_cost
175
+ });
176
+ }
177
+ }
178
+ return map;
179
+ }
180
+ async function loadLiteLLMPricing() {
181
+ if (cachedPricing) return cachedPricing;
182
+ if (isCacheFresh()) {
183
+ const cached = readCache();
184
+ if (cached) {
185
+ cachedPricing = cached;
186
+ return cached;
187
+ }
188
+ }
189
+ try {
190
+ const controller = new AbortController();
191
+ const timeout = setTimeout(() => controller.abort(), 1e4);
192
+ const res = await fetch(LITELLM_URL, { signal: controller.signal });
193
+ clearTimeout(timeout);
194
+ if (res.ok) {
195
+ const data = await res.json();
196
+ writeCache(data);
197
+ cachedPricing = parseRawPricing(data);
198
+ return cachedPricing;
199
+ }
200
+ } catch {
201
+ }
202
+ const stale = readCache();
203
+ if (stale) {
204
+ cachedPricing = stale;
205
+ return stale;
206
+ }
207
+ cachedPricing = /* @__PURE__ */ new Map();
208
+ return cachedPricing;
209
+ }
210
+ function findLiteLLMPricing(model, pricing) {
211
+ if (pricing.has(model)) return pricing.get(model);
212
+ const stripped = model.replace(/-\d{8}$/, "");
213
+ if (pricing.has(stripped)) return pricing.get(stripped);
214
+ if (!stripped.startsWith("claude-") && (stripped.includes("sonnet") || stripped.includes("opus") || stripped.includes("haiku"))) {
215
+ const withPrefix = "claude-" + stripped;
216
+ if (pricing.has(withPrefix)) return pricing.get(withPrefix);
217
+ }
218
+ const aliases = {
219
+ "claude-opus-4-6": "claude-opus-4-6-20260227",
220
+ "claude-sonnet-4-6": "claude-sonnet-4-6-20260220",
221
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001"
222
+ };
223
+ for (const [alias, full] of Object.entries(aliases)) {
224
+ if (stripped === alias && pricing.has(full)) return pricing.get(full);
225
+ }
226
+ for (const [key, val] of pricing) {
227
+ if (key.startsWith(stripped) || stripped.startsWith(key)) return val;
228
+ }
229
+ return null;
230
+ }
231
+ function calculateCostWithLiteLLM(model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, pricing) {
232
+ const p = findLiteLLMPricing(model, pricing);
233
+ if (!p) return 0;
234
+ let cost = inputTokens * p.input_cost_per_token + outputTokens * p.output_cost_per_token;
235
+ if (cacheWriteTokens > 0 && p.cache_creation_input_token_cost) {
236
+ cost += cacheWriteTokens * p.cache_creation_input_token_cost;
237
+ }
238
+ if (cacheReadTokens > 0) {
239
+ const readRate = p.cache_read_input_token_cost ?? p.input_cost_per_token * 0.5;
240
+ cost += cacheReadTokens * readRate;
241
+ }
242
+ return Math.round(cost * 1e6) / 1e6;
243
+ }
244
+ async function updatePricingCache() {
245
+ try {
246
+ const res = await fetch(LITELLM_URL);
247
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
248
+ const data = await res.json();
249
+ writeCache(data);
250
+ const parsed = parseRawPricing(data);
251
+ cachedPricing = parsed;
252
+ return parsed.size;
253
+ } catch (e) {
254
+ throw new Error(`Failed to update: ${e.message}`);
255
+ }
256
+ }
257
+
258
+ // src/data/providers/claude-code.ts
259
+ function getDataDirectories() {
260
+ const dirs = [];
261
+ const envDir = process.env.CLAUDE_CONFIG_DIR;
262
+ if (envDir) {
263
+ for (const d of envDir.split(",")) {
264
+ const p = join2(d.trim(), "projects");
265
+ if (existsSync2(p)) dirs.push(p);
266
+ }
267
+ }
268
+ const xdg = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
269
+ const xdgPath = join2(xdg, "claude", "projects");
270
+ if (existsSync2(xdgPath)) dirs.push(xdgPath);
271
+ const dotClaude = join2(homedir2(), ".claude", "projects");
272
+ if (existsSync2(dotClaude)) dirs.push(dotClaude);
273
+ return [...new Set(dirs)];
274
+ }
275
+ async function findJsonlFiles() {
276
+ const dirs = getDataDirectories();
277
+ const files = [];
278
+ for (const dir of dirs) {
279
+ const found = await glob("**/*.jsonl", {
280
+ cwd: dir,
281
+ absolute: true,
282
+ ignore: ["**/skill-injections.jsonl"]
283
+ });
284
+ files.push(...found);
285
+ }
286
+ return [...new Set(files)];
287
+ }
288
+ function extractProjectName(filePath) {
289
+ const projectsIdx = filePath.indexOf("/projects/");
290
+ if (projectsIdx === -1) return "unknown";
291
+ const afterProjects = filePath.substring(projectsIdx + "/projects/".length);
292
+ const projectDir = afterProjects.split("/")[0];
293
+ const segments = projectDir.split("-").filter(Boolean);
294
+ return segments[segments.length - 1] || projectDir;
295
+ }
296
+ async function parseFile(filePath, project, litellm) {
297
+ const records = [];
298
+ const rl = createInterface({
299
+ input: createReadStream(filePath, { encoding: "utf-8" }),
300
+ crlfDelay: Infinity
301
+ });
302
+ for await (const line of rl) {
303
+ if (!line.trim()) continue;
304
+ try {
305
+ const raw = JSON.parse(line);
306
+ if (raw.type !== "assistant") continue;
307
+ if (!raw.message?.usage) continue;
308
+ const usage = raw.message.usage;
309
+ const inputTokens = usage.input_tokens || 0;
310
+ const outputTokens = usage.output_tokens || 0;
311
+ const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
312
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
313
+ if (inputTokens + outputTokens === 0) continue;
314
+ const model = raw.message.model || "unknown";
315
+ const speed = usage.speed || "standard";
316
+ let costUSD;
317
+ if (raw.costUSD != null) {
318
+ costUSD = raw.costUSD;
319
+ } else if (litellm && litellm.size > 0) {
320
+ costUSD = calculateCostWithLiteLLM(model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, litellm);
321
+ } else {
322
+ costUSD = calculateCost(model, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, speed);
323
+ }
324
+ records.push({
325
+ id: `cc-${raw.uuid || raw.message.id || ""}`,
326
+ provider: "claude-code",
327
+ timestamp: new Date(raw.timestamp),
328
+ model,
329
+ modelDisplay: normalizeModelDisplay(model),
330
+ project,
331
+ sessionId: raw.sessionId || "unknown",
332
+ inputTokens,
333
+ outputTokens,
334
+ cacheWriteTokens,
335
+ cacheReadTokens,
336
+ totalTokens: inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens,
337
+ costUSD,
338
+ isEstimated: false,
339
+ speed,
340
+ messageId: raw.message.id || raw.uuid || ""
341
+ });
342
+ } catch {
343
+ }
344
+ }
345
+ return records;
346
+ }
347
+ async function parseClaudeCode() {
348
+ const files = await findJsonlFiles();
349
+ const litellm = await loadLiteLLMPricing();
350
+ const fileEntries = files.map((f) => ({
351
+ path: f,
352
+ project: extractProjectName(f)
353
+ }));
354
+ const results = await Promise.all(
355
+ fileEntries.map((f) => parseFile(f.path, f.project, litellm))
356
+ );
357
+ const byMessageId = /* @__PURE__ */ new Map();
358
+ for (const records of results) {
359
+ for (const r of records) {
360
+ const key = r.messageId || `${r.timestamp.getTime()}-${r.sessionId}-${r.model}`;
361
+ const existing = byMessageId.get(key);
362
+ if (!existing || r.outputTokens > 0 && r.outputTokens < existing.outputTokens) {
363
+ byMessageId.set(key, r);
364
+ }
365
+ }
366
+ }
367
+ const allRecords = Array.from(byMessageId.values());
368
+ allRecords.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
369
+ return allRecords;
370
+ }
371
+ async function detectClaudeCode() {
372
+ const dirs = getDataDirectories();
373
+ let count = 0;
374
+ for (const dir of dirs) {
375
+ const found = await glob("**/*.jsonl", {
376
+ cwd: dir,
377
+ absolute: true,
378
+ ignore: ["**/skill-injections.jsonl"]
379
+ });
380
+ count += found.length;
381
+ }
382
+ return {
383
+ id: "claude-code",
384
+ name: "Claude Code",
385
+ color: "#d97757",
386
+ dataPath: dirs,
387
+ entryCount: count,
388
+ isAvailable: count > 0
389
+ };
390
+ }
391
+
392
+ // src/data/providers/codex.ts
393
+ import { createReadStream as createReadStream2 } from "fs";
394
+ import { createInterface as createInterface2 } from "readline";
395
+ import { homedir as homedir3 } from "os";
396
+ import { join as join3 } from "path";
397
+ import { glob as glob2 } from "glob";
398
+ import { existsSync as existsSync3 } from "fs";
399
+ function getCodexSessionsDir() {
400
+ const dirs = [];
401
+ const envDir = process.env.CODEX_HOME;
402
+ if (envDir) {
403
+ const sessionsPath = join3(envDir, "sessions");
404
+ if (existsSync3(sessionsPath)) dirs.push(sessionsPath);
405
+ }
406
+ const defaultPath = join3(homedir3(), ".codex", "sessions");
407
+ if (existsSync3(defaultPath)) dirs.push(defaultPath);
408
+ return [...new Set(dirs)];
409
+ }
410
+ async function findCodexFiles() {
411
+ const dirs = getCodexSessionsDir();
412
+ const files = [];
413
+ for (const dir of dirs) {
414
+ const found = await glob2("**/*.jsonl", { cwd: dir, absolute: true });
415
+ files.push(...found);
416
+ }
417
+ return [...new Set(files)];
418
+ }
419
+ async function parseCodexFile(filePath, litellm) {
420
+ const records = [];
421
+ const sessionId = filePath.split("/").pop()?.replace(".jsonl", "") || "unknown";
422
+ const rl = createInterface2({
423
+ input: createReadStream2(filePath, { encoding: "utf-8" }),
424
+ crlfDelay: Infinity
425
+ });
426
+ let cwd = "";
427
+ let detectedModel = "gpt-5.3-codex";
428
+ for await (const line of rl) {
429
+ if (!line.trim()) continue;
430
+ try {
431
+ const raw = JSON.parse(line);
432
+ if (raw.type === "session_meta" && raw.payload) {
433
+ cwd = raw.payload.cwd || cwd;
434
+ }
435
+ const eventModel = raw.payload?.model || raw.model;
436
+ if (eventModel && typeof eventModel === "string" && eventModel.startsWith("gpt")) {
437
+ detectedModel = eventModel;
438
+ }
439
+ if (raw.type === "event_msg" && raw.payload?.type === "token_count" && raw.payload.info?.last_token_usage) {
440
+ const last = raw.payload.info.last_token_usage;
441
+ const totalInput = last.input_tokens || 0;
442
+ const cachedInput = last.cached_input_tokens || 0;
443
+ const outputTokens = last.output_tokens || 0;
444
+ const freshInput = Math.max(totalInput - cachedInput, 0);
445
+ if (freshInput + outputTokens + cachedInput === 0) continue;
446
+ const model = detectedModel;
447
+ let costUSD;
448
+ if (litellm.size > 0) {
449
+ costUSD = calculateCostWithLiteLLM(model, freshInput, outputTokens, 0, cachedInput, litellm);
450
+ } else {
451
+ costUSD = calculateCost(model, freshInput, outputTokens, 0, cachedInput);
452
+ }
453
+ const projectName = cwd ? cwd.split("/").pop() || "codex" : "codex";
454
+ records.push({
455
+ id: `cx-${sessionId}-${records.length}`,
456
+ provider: "codex",
457
+ timestamp: new Date(raw.timestamp || Date.now()),
458
+ model,
459
+ modelDisplay: normalizeModelDisplay(model),
460
+ project: projectName,
461
+ sessionId,
462
+ inputTokens: freshInput,
463
+ outputTokens,
464
+ cacheWriteTokens: 0,
465
+ cacheReadTokens: cachedInput,
466
+ totalTokens: freshInput + outputTokens + cachedInput,
467
+ costUSD,
468
+ isEstimated: false,
469
+ speed: "standard",
470
+ messageId: `cx-${sessionId}-${records.length}`
471
+ });
472
+ }
473
+ } catch {
474
+ }
475
+ }
476
+ return records;
477
+ }
478
+ async function parseCodex() {
479
+ const files = await findCodexFiles();
480
+ const litellm = await loadLiteLLMPricing();
481
+ const allRecords = [];
482
+ const results = await Promise.all(files.map((f) => parseCodexFile(f, litellm)));
483
+ for (const records of results) {
484
+ allRecords.push(...records);
485
+ }
486
+ allRecords.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
487
+ return allRecords;
488
+ }
489
+ async function detectCodex() {
490
+ const dirs = getCodexSessionsDir();
491
+ let count = 0;
492
+ for (const dir of dirs) {
493
+ const found = await glob2("**/*.jsonl", { cwd: dir, absolute: true });
494
+ count += found.length;
495
+ }
496
+ return {
497
+ id: "codex",
498
+ name: "Codex",
499
+ color: "#10a37f",
500
+ dataPath: dirs,
501
+ entryCount: count,
502
+ isAvailable: count > 0
503
+ };
504
+ }
505
+
506
+ // src/data/providers/antigravity.ts
507
+ import { homedir as homedir4, platform } from "os";
508
+ import { join as join4 } from "path";
509
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
510
+ import { glob as glob3 } from "glob";
511
+ function getAntigravityDir() {
512
+ const dirs = [];
513
+ if (platform() === "darwin") {
514
+ const macPath = join4(homedir4(), "Library", "Application Support", "Antigravity");
515
+ if (existsSync4(macPath)) dirs.push(macPath);
516
+ }
517
+ const linuxPath = join4(homedir4(), ".config", "Antigravity");
518
+ if (existsSync4(linuxPath)) dirs.push(linuxPath);
519
+ return [...new Set(dirs)];
520
+ }
521
+ async function parseAntigravity() {
522
+ const dirs = getAntigravityDir();
523
+ const records = [];
524
+ for (const dir of dirs) {
525
+ const memoryFiles = await glob3("**/memory/**/*.json", { cwd: dir, absolute: true });
526
+ const logFiles = await glob3("**/logs/**/*.json", { cwd: dir, absolute: true });
527
+ const allFiles = [...memoryFiles, ...logFiles];
528
+ for (const file of allFiles) {
529
+ try {
530
+ const content = readFileSync2(file, "utf-8");
531
+ const data = JSON.parse(content);
532
+ if (data.messages && Array.isArray(data.messages)) {
533
+ for (const msg of data.messages) {
534
+ if (!msg.content) continue;
535
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
536
+ const estimatedTokens = Math.ceil(text.length / 4);
537
+ const isAssistant = msg.role === "assistant";
538
+ const model = data.model || "gemini-3-pro";
539
+ const inputTokens = isAssistant ? 0 : estimatedTokens;
540
+ const outputTokens = isAssistant ? estimatedTokens : 0;
541
+ records.push({
542
+ id: `ag-${file.split("/").pop()}-${records.length}`,
543
+ provider: "antigravity",
544
+ timestamp: new Date(msg.timestamp || data.timestamp || Date.now()),
545
+ model,
546
+ modelDisplay: normalizeModelDisplay(model),
547
+ project: data.workspace || data.project || "antigravity",
548
+ sessionId: data.sessionId || file.split("/").pop()?.replace(".json", "") || "unknown",
549
+ inputTokens,
550
+ outputTokens,
551
+ cacheWriteTokens: 0,
552
+ cacheReadTokens: 0,
553
+ totalTokens: estimatedTokens,
554
+ costUSD: calculateCost(model, inputTokens, outputTokens, 0, 0),
555
+ isEstimated: true,
556
+ speed: "standard",
557
+ messageId: `ag-${records.length}`
558
+ });
559
+ }
560
+ }
561
+ } catch {
562
+ }
563
+ }
564
+ }
565
+ records.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
566
+ return records;
567
+ }
568
+ async function detectAntigravity() {
569
+ const dirs = getAntigravityDir();
570
+ let hasData = false;
571
+ for (const dir of dirs) {
572
+ const files = await glob3("**/*.json", { cwd: dir, absolute: true });
573
+ if (files.length > 0) {
574
+ hasData = true;
575
+ break;
576
+ }
577
+ }
578
+ return {
579
+ id: "antigravity",
580
+ name: "Antigravity",
581
+ color: "#4285f4",
582
+ dataPath: dirs,
583
+ entryCount: 0,
584
+ // estimated
585
+ isAvailable: hasData
586
+ };
587
+ }
588
+
589
+ // src/data/providers/registry.ts
590
+ async function detectProviders() {
591
+ const results = await Promise.all([
592
+ detectClaudeCode(),
593
+ detectCodex(),
594
+ detectAntigravity()
595
+ ]);
596
+ return results;
597
+ }
598
+ async function loadAllRecords(providers) {
599
+ const targets = providers || ["claude-code", "codex", "antigravity"];
600
+ const loaders = [];
601
+ if (targets.includes("claude-code")) loaders.push(parseClaudeCode());
602
+ if (targets.includes("codex")) loaders.push(parseCodex());
603
+ if (targets.includes("antigravity")) loaders.push(parseAntigravity());
604
+ const results = await Promise.all(loaders);
605
+ const all = results.flat();
606
+ all.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
607
+ return all;
608
+ }
609
+
610
+ // src/data/cache.ts
611
+ var cachedRecords = null;
612
+ var cachedProviderInfo = null;
613
+ async function getRecords(forceRefresh = false) {
614
+ if (!forceRefresh && cachedRecords) {
615
+ return cachedRecords;
616
+ }
617
+ cachedRecords = await loadAllRecords();
618
+ return cachedRecords;
619
+ }
620
+ async function getProviderInfo() {
621
+ if (cachedProviderInfo) return cachedProviderInfo;
622
+ cachedProviderInfo = await detectProviders();
623
+ return cachedProviderInfo;
624
+ }
625
+
626
+ // src/data/aggregator.ts
627
+ import { format, startOfWeek, startOfMonth } from "date-fns";
628
+ function applyFilters(records, filters) {
629
+ return records.filter((r) => {
630
+ if (filters.from && r.timestamp < filters.from) return false;
631
+ if (filters.to && r.timestamp > filters.to) return false;
632
+ if (filters.project && r.project !== filters.project) return false;
633
+ if (filters.model && normalizeModelDisplay(r.model) !== filters.model) return false;
634
+ if (filters.provider && r.provider !== filters.provider) return false;
635
+ return true;
636
+ });
637
+ }
638
+ function bucketKey(date, granularity) {
639
+ if (granularity === "day") return format(date, "yyyy-MM-dd");
640
+ if (granularity === "week") return format(startOfWeek(date, { weekStartsOn: 1 }), "yyyy-MM-dd");
641
+ return format(startOfMonth(date), "yyyy-MM");
642
+ }
643
+ function aggregateByTime(records, granularity) {
644
+ const buckets = /* @__PURE__ */ new Map();
645
+ for (const r of records) {
646
+ const key = bucketKey(r.timestamp, granularity);
647
+ let bucket = buckets.get(key);
648
+ if (!bucket) {
649
+ bucket = { date: key, inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0, totalCost: 0, models: [], providers: [] };
650
+ buckets.set(key, bucket);
651
+ }
652
+ bucket.inputTokens += r.inputTokens;
653
+ bucket.outputTokens += r.outputTokens;
654
+ bucket.cacheCreationTokens += r.cacheWriteTokens;
655
+ bucket.cacheReadTokens += r.cacheReadTokens;
656
+ bucket.totalCost += r.costUSD;
657
+ const mn = normalizeModelDisplay(r.model);
658
+ if (!bucket.models.includes(mn)) bucket.models.push(mn);
659
+ if (!bucket.providers.includes(r.provider)) bucket.providers.push(r.provider);
660
+ }
661
+ return Array.from(buckets.values()).sort((a, b) => a.date.localeCompare(b.date));
662
+ }
663
+ function aggregateByProject(records) {
664
+ const map = /* @__PURE__ */ new Map();
665
+ for (const r of records) {
666
+ let p = map.get(r.project);
667
+ if (!p) {
668
+ p = { sessions: /* @__PURE__ */ new Set(), input: 0, output: 0, cache: 0, cost: 0, lastActivity: r.timestamp, models: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Set() };
669
+ map.set(r.project, p);
670
+ }
671
+ p.sessions.add(r.sessionId);
672
+ p.input += r.inputTokens;
673
+ p.output += r.outputTokens;
674
+ p.cache += r.cacheWriteTokens + r.cacheReadTokens;
675
+ p.cost += r.costUSD;
676
+ if (r.timestamp > p.lastActivity) p.lastActivity = r.timestamp;
677
+ p.models.add(normalizeModelDisplay(r.model));
678
+ p.providers.add(r.provider);
679
+ }
680
+ return Array.from(map.entries()).map(([project, p]) => ({
681
+ project,
682
+ sessions: p.sessions.size,
683
+ inputTokens: p.input,
684
+ outputTokens: p.output,
685
+ cacheTokens: p.cache,
686
+ totalCost: p.cost,
687
+ lastActivity: format(p.lastActivity, "yyyy-MM-dd"),
688
+ models: Array.from(p.models),
689
+ providers: Array.from(p.providers)
690
+ })).sort((a, b) => b.totalCost - a.totalCost);
691
+ }
692
+ function aggregateByModel(records) {
693
+ const map = /* @__PURE__ */ new Map();
694
+ for (const r of records) {
695
+ const mn = normalizeModelDisplay(r.model);
696
+ let m = map.get(mn);
697
+ if (!m) {
698
+ m = { input: 0, output: 0, cache: 0, cost: 0, sessions: /* @__PURE__ */ new Set(), provider: r.provider };
699
+ map.set(mn, m);
700
+ }
701
+ m.input += r.inputTokens;
702
+ m.output += r.outputTokens;
703
+ m.cache += r.cacheWriteTokens + r.cacheReadTokens;
704
+ m.cost += r.costUSD;
705
+ m.sessions.add(r.sessionId);
706
+ }
707
+ return Array.from(map.entries()).map(([model, m]) => ({
708
+ model,
709
+ inputTokens: m.input,
710
+ outputTokens: m.output,
711
+ cacheTokens: m.cache,
712
+ totalCost: m.cost,
713
+ sessions: m.sessions.size,
714
+ provider: m.provider
715
+ })).sort((a, b) => b.totalCost - a.totalCost);
716
+ }
717
+ var PROVIDER_META = {
718
+ "claude-code": { name: "Claude Code", color: "#d97757" },
719
+ "codex": { name: "Codex", color: "#10a37f" },
720
+ "antigravity": { name: "Antigravity", color: "#4285f4" }
721
+ };
722
+ function aggregateByProvider(records) {
723
+ const map = /* @__PURE__ */ new Map();
724
+ for (const r of records) {
725
+ let p = map.get(r.provider);
726
+ if (!p) {
727
+ p = { records: 0, sessions: /* @__PURE__ */ new Set(), projects: /* @__PURE__ */ new Set(), input: 0, output: 0, cache: 0, cost: 0 };
728
+ map.set(r.provider, p);
729
+ }
730
+ p.records++;
731
+ p.sessions.add(r.sessionId);
732
+ p.projects.add(r.project);
733
+ p.input += r.inputTokens;
734
+ p.output += r.outputTokens;
735
+ p.cache += r.cacheWriteTokens + r.cacheReadTokens;
736
+ p.cost += r.costUSD;
737
+ }
738
+ return Array.from(map.entries()).map(([provider, p]) => ({
739
+ provider,
740
+ ...PROVIDER_META[provider],
741
+ records: p.records,
742
+ sessions: p.sessions.size,
743
+ projects: p.projects.size,
744
+ inputTokens: p.input,
745
+ outputTokens: p.output,
746
+ cacheTokens: p.cache,
747
+ totalCost: p.cost
748
+ })).sort((a, b) => b.totalCost - a.totalCost);
749
+ }
750
+ function getSummary(records) {
751
+ const totalInput = records.reduce((s, r) => s + r.inputTokens, 0);
752
+ const totalOutput = records.reduce((s, r) => s + r.outputTokens, 0);
753
+ const totalCache = records.reduce((s, r) => s + r.cacheWriteTokens + r.cacheReadTokens, 0);
754
+ const totalCost = records.reduce((s, r) => s + r.costUSD, 0);
755
+ const sessions = new Set(records.map((r) => r.sessionId)).size;
756
+ const projects = new Set(records.map((r) => r.project)).size;
757
+ const providers = [...new Set(records.map((r) => r.provider))];
758
+ const dates = records.map((r) => r.timestamp.getTime());
759
+ const dateRange = dates.length > 0 ? { from: format(new Date(Math.min(...dates)), "yyyy-MM-dd"), to: format(new Date(Math.max(...dates)), "yyyy-MM-dd") } : { from: "", to: "" };
760
+ return {
761
+ totalTokens: totalInput + totalOutput + totalCache,
762
+ totalInputTokens: totalInput,
763
+ totalOutputTokens: totalOutput,
764
+ totalCacheTokens: totalCache,
765
+ totalCost,
766
+ totalSessions: sessions,
767
+ totalProjects: projects,
768
+ providers,
769
+ dateRange
770
+ };
771
+ }
772
+ function aggregateSessions(records, limit = 50, offset = 0) {
773
+ const map = /* @__PURE__ */ new Map();
774
+ for (const r of records) {
775
+ let s = map.get(r.sessionId);
776
+ if (!s) {
777
+ s = { project: r.project, timestamp: r.timestamp, models: /* @__PURE__ */ new Set(), input: 0, output: 0, cost: 0, count: 0, provider: r.provider, isEstimated: r.isEstimated };
778
+ map.set(r.sessionId, s);
779
+ }
780
+ s.models.add(normalizeModelDisplay(r.model));
781
+ s.input += r.inputTokens;
782
+ s.output += r.outputTokens;
783
+ s.cost += r.costUSD;
784
+ s.count++;
785
+ if (r.timestamp < s.timestamp) s.timestamp = r.timestamp;
786
+ }
787
+ return Array.from(map.entries()).map(([sessionId, s]) => ({
788
+ sessionId,
789
+ project: s.project,
790
+ timestamp: format(s.timestamp, "yyyy-MM-dd'T'HH:mm:ss"),
791
+ model: Array.from(s.models).join(", "),
792
+ inputTokens: s.input,
793
+ outputTokens: s.output,
794
+ cost: s.cost,
795
+ messageCount: s.count,
796
+ provider: s.provider,
797
+ isEstimated: s.isEstimated
798
+ })).sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(offset, offset + limit);
799
+ }
800
+
801
+ // src/api/routes.ts
802
+ import { format as format2 } from "date-fns";
803
+ var api = new Hono();
804
+ api.use("*", cors());
805
+ function parseFilters(query) {
806
+ return {
807
+ from: query.from ? new Date(query.from) : void 0,
808
+ to: query.to ? /* @__PURE__ */ new Date(query.to + "T23:59:59") : void 0,
809
+ project: query.project || void 0,
810
+ model: query.model || void 0,
811
+ provider: query.provider || void 0
812
+ };
813
+ }
814
+ api.get("/api/providers", async (c) => {
815
+ const info = await getProviderInfo();
816
+ return c.json(info);
817
+ });
818
+ api.get("/api/pricing", async (c) => {
819
+ return c.json(getPricingTable());
820
+ });
821
+ api.get("/api/summary", async (c) => {
822
+ const records = await getRecords();
823
+ const filters = parseFilters(c.req.query());
824
+ const filtered = applyFilters(records, filters);
825
+ const summary = getSummary(filtered);
826
+ const providerBreakdown = aggregateByProvider(filtered);
827
+ return c.json({ ...summary, providerBreakdown });
828
+ });
829
+ api.get("/api/daily", async (c) => {
830
+ const records = await getRecords();
831
+ const filters = parseFilters(c.req.query());
832
+ const filtered = applyFilters(records, filters);
833
+ return c.json(aggregateByTime(filtered, "day"));
834
+ });
835
+ api.get("/api/weekly", async (c) => {
836
+ const records = await getRecords();
837
+ const filters = parseFilters(c.req.query());
838
+ const filtered = applyFilters(records, filters);
839
+ return c.json(aggregateByTime(filtered, "week"));
840
+ });
841
+ api.get("/api/monthly", async (c) => {
842
+ const records = await getRecords();
843
+ const filters = parseFilters(c.req.query());
844
+ const filtered = applyFilters(records, filters);
845
+ return c.json(aggregateByTime(filtered, "month"));
846
+ });
847
+ api.get("/api/projects", async (c) => {
848
+ const records = await getRecords();
849
+ const filters = parseFilters(c.req.query());
850
+ const filtered = applyFilters(records, filters);
851
+ return c.json(aggregateByProject(filtered));
852
+ });
853
+ api.get("/api/models", async (c) => {
854
+ const records = await getRecords();
855
+ const filters = parseFilters(c.req.query());
856
+ const filtered = applyFilters(records, filters);
857
+ return c.json(aggregateByModel(filtered));
858
+ });
859
+ api.get("/api/sessions", async (c) => {
860
+ const records = await getRecords();
861
+ const filters = parseFilters(c.req.query());
862
+ const filtered = applyFilters(records, filters);
863
+ const limit = parseInt(c.req.query("limit") || "50");
864
+ const offset = parseInt(c.req.query("offset") || "0");
865
+ return c.json(aggregateSessions(filtered, limit, offset));
866
+ });
867
+ api.get("/api/sessions/:id", async (c) => {
868
+ const id = c.req.param("id");
869
+ const records = await getRecords();
870
+ const sessionRecords = records.filter((r) => r.sessionId === id);
871
+ return c.json({
872
+ sessionId: id,
873
+ messages: sessionRecords.map((r) => ({
874
+ timestamp: format2(r.timestamp, "yyyy-MM-dd'T'HH:mm:ss"),
875
+ model: normalizeModelDisplay(r.model),
876
+ inputTokens: r.inputTokens,
877
+ outputTokens: r.outputTokens,
878
+ cacheCreationTokens: r.cacheWriteTokens,
879
+ cacheReadTokens: r.cacheReadTokens,
880
+ cost: r.costUSD,
881
+ provider: r.provider,
882
+ isEstimated: r.isEstimated
883
+ }))
884
+ });
885
+ });
886
+ api.get("/api/heatmap", async (c) => {
887
+ const records = await getRecords();
888
+ const filters = parseFilters(c.req.query());
889
+ const filtered = applyFilters(records, filters);
890
+ return c.json(aggregateByTime(filtered, "day"));
891
+ });
892
+ api.get("/api/filters", async (c) => {
893
+ const records = await getRecords();
894
+ const projects = [...new Set(records.map((r) => r.project))].sort();
895
+ const models = [...new Set(records.map((r) => normalizeModelDisplay(r.model)))].sort();
896
+ const providers = [...new Set(records.map((r) => r.provider))].sort();
897
+ return c.json({ projects, models, providers });
898
+ });
899
+ api.get("/api/export", async (c) => {
900
+ const records = await getRecords();
901
+ const filters = parseFilters(c.req.query());
902
+ const filtered = applyFilters(records, filters);
903
+ const fmt = c.req.query("format") || "json";
904
+ if (fmt === "csv") {
905
+ const header = "timestamp,session_id,project,provider,model,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,cost_usd,is_estimated";
906
+ const rows = filtered.map(
907
+ (r) => `${format2(r.timestamp, "yyyy-MM-dd'T'HH:mm:ss")},${r.sessionId},${r.project},${r.provider},${normalizeModelDisplay(r.model)},${r.inputTokens},${r.outputTokens},${r.cacheWriteTokens},${r.cacheReadTokens},${r.costUSD.toFixed(6)},${r.isEstimated}`
908
+ );
909
+ const csv = "\uFEFF" + header + "\n" + rows.join("\n");
910
+ c.header("Content-Type", "text/csv; charset=utf-8");
911
+ c.header("Content-Disposition", 'attachment; filename="tokentrack-export.csv"');
912
+ return c.body(csv);
913
+ }
914
+ return c.json(filtered.map((r) => ({
915
+ timestamp: format2(r.timestamp, "yyyy-MM-dd'T'HH:mm:ss"),
916
+ sessionId: r.sessionId,
917
+ project: r.project,
918
+ provider: r.provider,
919
+ model: normalizeModelDisplay(r.model),
920
+ inputTokens: r.inputTokens,
921
+ outputTokens: r.outputTokens,
922
+ cacheWriteTokens: r.cacheWriteTokens,
923
+ cacheReadTokens: r.cacheReadTokens,
924
+ costUSD: r.costUSD,
925
+ isEstimated: r.isEstimated
926
+ })));
927
+ });
928
+
929
+ // src/server.ts
930
+ import { fileURLToPath } from "url";
931
+ import { dirname, join as join5 } from "path";
932
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
933
+ var __dirname = dirname(fileURLToPath(import.meta.url));
934
+ function createServer(port) {
935
+ const app = new Hono2();
936
+ app.route("/", api);
937
+ const candidates = [
938
+ join5(__dirname, "..", "dist", "web"),
939
+ // from src/ -> dist/web/ (dev via tsx)
940
+ join5(__dirname, "web")
941
+ // dist/web/ (production)
942
+ ];
943
+ const webDir = candidates.find((d) => existsSync5(join5(d, "index.html"))) || candidates[0];
944
+ app.get("/*", async (c) => {
945
+ const urlPath = new URL(c.req.url).pathname;
946
+ const filePath = join5(webDir, urlPath === "/" ? "index.html" : urlPath);
947
+ if (existsSync5(filePath)) {
948
+ const content = readFileSync3(filePath);
949
+ const ext = filePath.split(".").pop() || "";
950
+ const mimeTypes = {
951
+ html: "text/html",
952
+ js: "application/javascript",
953
+ css: "text/css",
954
+ json: "application/json",
955
+ png: "image/png",
956
+ svg: "image/svg+xml",
957
+ ico: "image/x-icon"
958
+ };
959
+ return new Response(content, {
960
+ headers: { "Content-Type": mimeTypes[ext] || "application/octet-stream" }
961
+ });
962
+ }
963
+ const indexPath = join5(webDir, "index.html");
964
+ if (existsSync5(indexPath)) {
965
+ return new Response(readFileSync3(indexPath), {
966
+ headers: { "Content-Type": "text/html" }
967
+ });
968
+ }
969
+ return c.text("Not found", 404);
970
+ });
971
+ return new Promise((resolve) => {
972
+ const tryPort = (p) => {
973
+ const server = serve({ fetch: app.fetch, port: p });
974
+ server.on("error", (err) => {
975
+ if (err.code === "EADDRINUSE") {
976
+ console.log(` Port ${p} in use, trying ${p + 1}...`);
977
+ tryPort(p + 1);
978
+ } else {
979
+ throw err;
980
+ }
981
+ });
982
+ server.on("listening", () => {
983
+ resolve({ port: p });
984
+ });
985
+ };
986
+ tryPort(port);
987
+ });
988
+ }
989
+
990
+ export {
991
+ getPricingTable,
992
+ loadLiteLLMPricing,
993
+ findLiteLLMPricing,
994
+ updatePricingCache,
995
+ getRecords,
996
+ getProviderInfo,
997
+ applyFilters,
998
+ aggregateByProject,
999
+ aggregateByProvider,
1000
+ getSummary,
1001
+ createServer
1002
+ };