setVolume(v => Math.min(100, v + 5))}>
{/* up */}
diff --git a/package.json b/package.json
index 3f1d539..1738e98 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.13",
- "@types/node": "^24.3.1",
+ "@types/node": "^24.3.3",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"eslint": "^9.35.0",
diff --git a/public/data/cc.json b/public/data/cc.json
index 7acbffb..67f84fa 100644
--- a/public/data/cc.json
+++ b/public/data/cc.json
@@ -1,24 +1,148 @@
{
"daily": [
{
- "date": "2025-08-12",
- "inputTokens": 182,
- "outputTokens": 7432,
- "cacheCreationTokens": 260815,
- "cacheReadTokens": 1814702,
- "totalTokens": 2083131,
- "totalCost": 1.63449285,
+ "date": "2025-08-08",
+ "inputTokens": 14919,
+ "outputTokens": 23378,
+ "cacheCreationTokens": 480031,
+ "cacheReadTokens": 11034031,
+ "totalTokens": 11552359,
+ "totalCost": 6.777273749999996,
"modelsUsed": [
+ "claude-opus-4-1-20250805",
"claude-sonnet-4-20250514"
],
"modelBreakdowns": [
{
"modelName": "claude-sonnet-4-20250514",
- "inputTokens": 182,
- "outputTokens": 7432,
- "cacheCreationTokens": 260815,
- "cacheReadTokens": 1814702,
- "cost": 1.63449285
+ "inputTokens": 4837,
+ "outputTokens": 20788,
+ "cacheCreationTokens": 443453,
+ "cacheReadTokens": 10661975,
+ "cost": 5.18787225
+ },
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "inputTokens": 10082,
+ "outputTokens": 2590,
+ "cacheCreationTokens": 36578,
+ "cacheReadTokens": 372056,
+ "cost": 1.5894014999999997
+ }
+ ]
+ },
+ {
+ "date": "2025-08-09",
+ "inputTokens": 3142,
+ "outputTokens": 20594,
+ "cacheCreationTokens": 513312,
+ "cacheReadTokens": 13270007,
+ "totalTokens": 13807055,
+ "totalCost": 20.561232300000007,
+ "modelsUsed": [
+ "claude-sonnet-4-20250514",
+ "claude-opus-4-1-20250805"
+ ],
+ "modelBreakdowns": [
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "inputTokens": 373,
+ "outputTokens": 10485,
+ "cacheCreationTokens": 294339,
+ "cacheReadTokens": 7740261,
+ "cost": 17.92121775
+ },
+ {
+ "modelName": "claude-sonnet-4-20250514",
+ "inputTokens": 2769,
+ "outputTokens": 10109,
+ "cacheCreationTokens": 218973,
+ "cacheReadTokens": 5529746,
+ "cost": 2.640014549999999
+ }
+ ]
+ },
+ {
+ "date": "2025-08-10",
+ "inputTokens": 2384,
+ "outputTokens": 33087,
+ "cacheCreationTokens": 752268,
+ "cacheReadTokens": 12833548,
+ "totalTokens": 13621287,
+ "totalCost": 24.83825640000001,
+ "modelsUsed": [
+ "claude-opus-4-1-20250805",
+ "claude-sonnet-4-20250514"
+ ],
+ "modelBreakdowns": [
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "inputTokens": 983,
+ "outputTokens": 24065,
+ "cacheCreationTokens": 320876,
+ "cacheReadTokens": 9495745,
+ "cost": 22.079662499999998
+ },
+ {
+ "modelName": "claude-sonnet-4-20250514",
+ "inputTokens": 1401,
+ "outputTokens": 9022,
+ "cacheCreationTokens": 431392,
+ "cacheReadTokens": 3337803,
+ "cost": 2.7585938999999993
+ }
+ ]
+ },
+ {
+ "date": "2025-08-11",
+ "inputTokens": 1127,
+ "outputTokens": 23663,
+ "cacheCreationTokens": 746606,
+ "cacheReadTokens": 10310633,
+ "totalTokens": 11082029,
+ "totalCost": 31.256441999999993,
+ "modelsUsed": [
+ "claude-opus-4-1-20250805"
+ ],
+ "modelBreakdowns": [
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "inputTokens": 1127,
+ "outputTokens": 23663,
+ "cacheCreationTokens": 746606,
+ "cacheReadTokens": 10310633,
+ "cost": 31.256441999999993
+ }
+ ]
+ },
+ {
+ "date": "2025-08-12",
+ "inputTokens": 17245,
+ "outputTokens": 164864,
+ "cacheCreationTokens": 2646250,
+ "cacheReadTokens": 49767559,
+ "totalTokens": 52595918,
+ "totalCost": 85.49760780000005,
+ "modelsUsed": [
+ "claude-opus-4-1-20250805",
+ "claude-sonnet-4-20250514"
+ ],
+ "modelBreakdowns": [
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "inputTokens": 13710,
+ "outputTokens": 77330,
+ "cacheCreationTokens": 1413354,
+ "cacheReadTokens": 26762148,
+ "cost": 72.64900950000008
+ },
+ {
+ "modelName": "claude-sonnet-4-20250514",
+ "inputTokens": 3535,
+ "outputTokens": 87534,
+ "cacheCreationTokens": 1232896,
+ "cacheReadTokens": 23005411,
+ "cost": 12.848598300000004
}
]
},
@@ -658,14 +782,45 @@
"cost": 2.6278957499999995
}
]
+ },
+ {
+ "date": "2025-09-13",
+ "inputTokens": 461,
+ "outputTokens": 21931,
+ "cacheCreationTokens": 653276,
+ "cacheReadTokens": 5601864,
+ "totalTokens": 6277532,
+ "totalCost": 21.709448999999996,
+ "modelsUsed": [
+ "claude-sonnet-4-20250514",
+ "claude-opus-4-1-20250805"
+ ],
+ "modelBreakdowns": [
+ {
+ "modelName": "claude-opus-4-1-20250805",
+ "inputTokens": 425,
+ "outputTokens": 21677,
+ "cacheCreationTokens": 623184,
+ "cacheReadTokens": 5496064,
+ "cost": 21.560945999999998
+ },
+ {
+ "modelName": "claude-sonnet-4-20250514",
+ "inputTokens": 36,
+ "outputTokens": 254,
+ "cacheCreationTokens": 30092,
+ "cacheReadTokens": 105800,
+ "cost": 0.148503
+ }
+ ]
}
],
"totals": {
- "inputTokens": 206952,
- "outputTokens": 1248290,
- "cacheCreationTokens": 29625753,
- "cacheReadTokens": 558468397,
- "totalCost": 785.74532145,
- "totalTokens": 589549392
+ "inputTokens": 246048,
+ "outputTokens": 1528375,
+ "cacheCreationTokens": 35156681,
+ "cacheReadTokens": 659471337,
+ "totalTokens": 696402441,
+ "totalCost": 974.7510898500002
}
}
diff --git a/tools/ccombine.ts b/tools/ccombine.ts
new file mode 100644
index 0000000..ab9ff23
--- /dev/null
+++ b/tools/ccombine.ts
@@ -0,0 +1,237 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+
+type NumberLike = number | undefined | null;
+
+interface ModelBreakdown {
+ modelName: string;
+ inputTokens: number;
+ outputTokens: number;
+ cacheCreationTokens: number;
+ cacheReadTokens: number;
+ cost: number;
+}
+
+interface DailyEntry {
+ date: string; // YYYY-MM-DD
+ inputTokens: number;
+ outputTokens: number;
+ cacheCreationTokens: number;
+ cacheReadTokens: number;
+ totalTokens: number;
+ totalCost: number;
+ modelsUsed?: string[];
+ modelBreakdowns?: ModelBreakdown[];
+}
+
+interface Totals {
+ inputTokens: number;
+ outputTokens: number;
+ cacheCreationTokens: number;
+ cacheReadTokens: number;
+ totalTokens: number;
+ totalCost: number;
+}
+
+interface CcFile {
+ daily: DailyEntry[];
+ totals?: Totals;
+}
+
+function toNumber(n: NumberLike): number {
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
+}
+
+function computeTotals(daily: DailyEntry[]): Totals {
+ const totals: Totals = {
+ inputTokens: 0,
+ outputTokens: 0,
+ cacheCreationTokens: 0,
+ cacheReadTokens: 0,
+ totalTokens: 0,
+ totalCost: 0,
+ };
+ for (const d of daily) {
+ totals.inputTokens += toNumber(d.inputTokens);
+ totals.outputTokens += toNumber(d.outputTokens);
+ totals.cacheCreationTokens += toNumber(d.cacheCreationTokens);
+ totals.cacheReadTokens += toNumber(d.cacheReadTokens);
+ totals.totalTokens += toNumber(d.totalTokens);
+ totals.totalCost += toNumber(d.totalCost);
+ }
+ return totals;
+}
+
+function isReplacementBetter(a: DailyEntry, b: DailyEntry): boolean {
+ const aTokens = toNumber(a.totalTokens);
+ const bTokens = toNumber(b.totalTokens);
+ if (bTokens !== aTokens) return bTokens > aTokens;
+ const aCost = toNumber(a.totalCost);
+ const bCost = toNumber(b.totalCost);
+ if (bCost !== aCost) return bCost > aCost;
+
+ const aBreakdowns = a.modelBreakdowns?.length ?? 0;
+ const bBreakdowns = b.modelBreakdowns?.length ?? 0;
+ if (bBreakdowns !== aBreakdowns) return bBreakdowns > aBreakdowns;
+
+ return false;
+}
+
+async function readJson
(filePath: string): Promise {
+ const raw = await fs.readFile(filePath, "utf8");
+ return JSON.parse(raw) as T;
+}
+
+async function fileExists(filePath: string): Promise {
+ try {
+ await fs.access(filePath);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function isObject(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
+}
+
+function coerceTotals(t: unknown): Totals {
+ const r = isObject(t) ? t : {};
+ return {
+ inputTokens: toNumber(r["inputTokens"] as NumberLike),
+ outputTokens: toNumber(r["outputTokens"] as NumberLike),
+ cacheCreationTokens: toNumber(r["cacheCreationTokens"] as NumberLike),
+ cacheReadTokens: toNumber(r["cacheReadTokens"] as NumberLike),
+ totalTokens: toNumber(r["totalTokens"] as NumberLike),
+ totalCost: toNumber(r["totalCost"] as NumberLike),
+ };
+}
+
+function coerceDailyEntry(item: unknown): DailyEntry {
+ const r = isObject(item) ? item : {};
+
+ const modelBreakdownsRaw = Array.isArray(r["modelBreakdowns"]) ? (r["modelBreakdowns"] as unknown[]) : [];
+ const modelBreakdowns: ModelBreakdown[] = modelBreakdownsRaw.map((mb) => {
+ const m = isObject(mb) ? mb : {};
+ return {
+ modelName: typeof m["modelName"] === "string" ? (m["modelName"] as string) : "",
+ inputTokens: toNumber(m["inputTokens"] as NumberLike),
+ outputTokens: toNumber(m["outputTokens"] as NumberLike),
+ cacheCreationTokens: toNumber(m["cacheCreationTokens"] as NumberLike),
+ cacheReadTokens: toNumber(m["cacheReadTokens"] as NumberLike),
+ cost: toNumber(m["cost"] as NumberLike),
+ };
+ });
+
+ const modelsUsed = Array.isArray(r["modelsUsed"]) ? (r["modelsUsed"] as unknown[]).filter((x): x is string => typeof x === "string") : undefined;
+
+ return {
+ date: String((r["date"] as unknown) ?? ""),
+ inputTokens: toNumber(r["inputTokens"] as NumberLike),
+ outputTokens: toNumber(r["outputTokens"] as NumberLike),
+ cacheCreationTokens: toNumber(r["cacheCreationTokens"] as NumberLike),
+ cacheReadTokens: toNumber(r["cacheReadTokens"] as NumberLike),
+ totalTokens: toNumber(r["totalTokens"] as NumberLike),
+ totalCost: toNumber(r["totalCost"] as NumberLike),
+ modelsUsed,
+ modelBreakdowns: modelBreakdowns.length ? modelBreakdowns : undefined,
+ };
+}
+
+function normalizeCcShape(obj: unknown): CcFile {
+ const o = isObject(obj) ? obj : {};
+ const rawDaily = Array.isArray(o["daily"]) ? (o["daily"] as unknown[]) : [];
+ const daily = rawDaily.map(coerceDailyEntry);
+ const totals = isObject(o["totals"]) ? coerceTotals(o["totals"]) : undefined;
+ return { daily, totals };
+}
+
+function sortByDateAsc(entries: DailyEntry[]): DailyEntry[] {
+ return entries.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
+}
+
+async function main() {
+ const args = process.argv.slice(2);
+ if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
+ console.log(`Usage: tsx tools/ccombine.ts [--base public/data/cc.json] [--out ] [--dry]`);
+ process.exit(args.length === 0 ? 1 : 0);
+ }
+
+ let inputPath = "";
+ let basePath = path.join(process.cwd(), "public", "data", "cc.json");
+ let outPath: string | undefined;
+ let dryRun = false;
+
+ for (let i = 0; i < args.length; i++) {
+ const a = args[i];
+ if (a === "--base") {
+ basePath = path.resolve(args[++i]);
+ } else if (a === "--out") {
+ outPath = path.resolve(args[++i]);
+ } else if (a === "--dry" || a === "--dry-run") {
+ dryRun = true;
+ } else if (!a.startsWith("-")) {
+ inputPath = path.resolve(a);
+ } else {
+ console.error(`Unknown option: ${a}`);
+ process.exit(1);
+ }
+ }
+
+ if (!inputPath) {
+ console.error("Error: missing input path");
+ process.exit(1);
+ }
+ if (!outPath) outPath = basePath;
+
+ if (!(await fileExists(inputPath))) {
+ console.error(`Error: input file not found: ${inputPath}`);
+ process.exit(1);
+ }
+
+ const baseExists = await fileExists(basePath);
+ const baseCc = baseExists ? normalizeCcShape(await readJson(basePath)) : { daily: [] };
+ const newCc = normalizeCcShape(await readJson(inputPath));
+
+ const baseByDate = new Map();
+ for (const d of baseCc.daily) baseByDate.set(d.date, d);
+
+ const added: string[] = [];
+ const replaced: string[] = [];
+ const unchanged: string[] = [];
+
+ for (const incoming of newCc.daily) {
+ const existing = baseByDate.get(incoming.date);
+ if (!existing) {
+ baseByDate.set(incoming.date, incoming);
+ added.push(incoming.date);
+ continue;
+ }
+ if (isReplacementBetter(existing, incoming)) {
+ baseByDate.set(incoming.date, incoming);
+ replaced.push(incoming.date);
+ } else {
+ unchanged.push(incoming.date);
+ }
+ }
+
+ const mergedDaily = sortByDateAsc(Array.from(baseByDate.values()));
+ const totals = computeTotals(mergedDaily);
+ const merged: CcFile = { daily: mergedDaily, totals };
+
+ if (dryRun) {
+ console.log("[ccombine] Dry run. No files written.");
+ } else {
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
+ await fs.writeFile(outPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
+ }
+
+ const outDisplay = dryRun ? "(dry run)" : outPath;
+ console.log("[ccombine] Output:", outDisplay);
+ console.log(`[ccombine] Added: ${added.length} | Replaced: ${replaced.length} | Unchanged (overlap): ${unchanged.length}`);
+}
+
+main().catch((err) => {
+ console.error("[ccombine] Error:", err?.message || err);
+ process.exit(1);
+});