tledger 0.1.3 → 0.2.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 +121 -67
- package/bin/token-ledger-rates.mjs +62 -0
- package/bin/token-ledger-terminal.mjs +97 -154
- package/bin/token-ledger-trend-image.mjs +945 -0
- package/bin/token-ledger-trend-terminal.mjs +609 -0
- package/bin/token-ledger-trend.mjs +745 -0
- package/bin/token-ledger-tui.mjs +15 -21
- package/bin/token-ledger.mjs +390 -102
- package/lib/{token-ledger-collector.mjs → token-ledger-importer.mjs} +244 -407
- package/package.json +17 -10
package/bin/token-ledger.mjs
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
5
|
import {
|
|
6
|
+
mkdir,
|
|
5
7
|
readFile,
|
|
6
8
|
stat,
|
|
7
9
|
} from "node:fs/promises";
|
|
8
10
|
import { homedir } from "node:os";
|
|
9
|
-
import { basename, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, resolve } from "node:path";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
11
13
|
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
MODEL_COLORS as TERMINAL_MODEL_COLORS,
|
|
16
|
+
renderTerminal,
|
|
17
|
+
} from "./token-ledger-terminal.mjs";
|
|
18
|
+
import { buildUsageTrend, multiDayBounds } from "./token-ledger-trend.mjs";
|
|
19
|
+
import {
|
|
20
|
+
renderTrendImage,
|
|
21
|
+
writeTrendPng,
|
|
22
|
+
} from "./token-ledger-trend-image.mjs";
|
|
23
|
+
import { renderTrendCombo } from "./token-ledger-trend-terminal.mjs";
|
|
13
24
|
import { startInteractive } from "./token-ledger-tui.mjs";
|
|
14
25
|
|
|
15
26
|
export const DEFAULT_SNAPSHOT = resolve(
|
|
@@ -19,17 +30,31 @@ export const DEFAULT_SNAPSHOT = resolve(
|
|
|
19
30
|
);
|
|
20
31
|
const DEFAULT_TOP = 10;
|
|
21
32
|
const DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
33
|
+
export const SNAPSHOT_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
34
|
+
const ANSI_RESET = "\u001b[0m";
|
|
35
|
+
const MODEL_COLORS = {
|
|
36
|
+
sol: TERMINAL_MODEL_COLORS.sol,
|
|
37
|
+
luna: TERMINAL_MODEL_COLORS.luna,
|
|
38
|
+
terra: TERMINAL_MODEL_COLORS.terra,
|
|
39
|
+
"gpt-5.5": TERMINAL_MODEL_COLORS.gpt,
|
|
40
|
+
"gpt-5.4": TERMINAL_MODEL_COLORS.gpt,
|
|
41
|
+
other: TERMINAL_MODEL_COLORS.other,
|
|
42
|
+
};
|
|
22
43
|
|
|
23
44
|
function usage() {
|
|
24
45
|
return `Token Ledger terminal usage
|
|
25
46
|
|
|
26
47
|
Usage:
|
|
27
|
-
tledger
|
|
48
|
+
tledger day <YYYY-MM-DD>
|
|
28
49
|
tledger week [end-day]
|
|
29
|
-
tledger
|
|
50
|
+
tledger trend [7d|14d|30d]
|
|
51
|
+
tledger report [7d|14d|30d]
|
|
52
|
+
npm run usage:day -- <YYYY-MM-DD>
|
|
53
|
+
npm run usage:week -- [end-day]
|
|
30
54
|
|
|
31
55
|
Options:
|
|
32
56
|
--date <day> Date as YYYY-MM-DD, today, or yesterday
|
|
57
|
+
--period <window> Trend window: 7d, 14d, or 30d
|
|
33
58
|
--input <file> Snapshot to read (default: ~/.token-ledger/token-ledger-snapshot.json)
|
|
34
59
|
--refresh Rebuild the default snapshot from CODEX_HOME or ~/.codex
|
|
35
60
|
--no-refresh Use the cached snapshot without checking local JSONL files
|
|
@@ -42,10 +67,18 @@ Options:
|
|
|
42
67
|
--plain Disable terminal colors
|
|
43
68
|
--ascii Use ASCII bars instead of Unicode blocks
|
|
44
69
|
--static Print once instead of opening the interactive dashboard
|
|
70
|
+
--drain Trend columns show observed limit drain percent instead of token volume
|
|
71
|
+
--image Write trend view as a PNG image
|
|
72
|
+
--image-output <file> PNG output path for trend view
|
|
73
|
+
--image-width <px> PNG image width from 900 to 2400 pixels
|
|
74
|
+
--youplot Use the legacy single-series YouPlot renderer
|
|
45
75
|
--help Show this help
|
|
46
76
|
|
|
47
|
-
The
|
|
48
|
-
|
|
77
|
+
The report command writes the dashboard PNG (same as trend --image) to
|
|
78
|
+
token-ledger-report-<period>.png; use --image-output to choose the path.
|
|
79
|
+
|
|
80
|
+
The command reads a privacy-reduced Token Ledger snapshot. It never uploads
|
|
81
|
+
the snapshot or prints message bodies, tool payloads, credentials, or paths.`;
|
|
49
82
|
}
|
|
50
83
|
|
|
51
84
|
function readOption(argv, index, name) {
|
|
@@ -57,13 +90,16 @@ function readOption(argv, index, name) {
|
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
export function parseArgs(argv) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
const command = argv[0] === "week"
|
|
94
|
+
? "week"
|
|
95
|
+
: argv[0] === "trend" || argv[0] === "report"
|
|
96
|
+
? "trend"
|
|
97
|
+
: "day";
|
|
65
98
|
const options = {
|
|
66
99
|
range: command,
|
|
100
|
+
view: command === "trend" ? "trend" : "projects",
|
|
101
|
+
report: argv[0] === "report",
|
|
102
|
+
trendDays: 7,
|
|
67
103
|
date: null,
|
|
68
104
|
input: DEFAULT_SNAPSHOT,
|
|
69
105
|
inputExplicit: false,
|
|
@@ -78,10 +114,16 @@ export function parseArgs(argv) {
|
|
|
78
114
|
plain: false,
|
|
79
115
|
ascii: false,
|
|
80
116
|
static: false,
|
|
117
|
+
image: false,
|
|
118
|
+
imageOutput: null,
|
|
119
|
+
imageWidth: null,
|
|
120
|
+
drain: false,
|
|
121
|
+
legacyPlot: false,
|
|
81
122
|
help: false,
|
|
82
123
|
};
|
|
83
124
|
|
|
84
|
-
let
|
|
125
|
+
let trendPeriodSeen = false;
|
|
126
|
+
let index = ["day", "week", "trend", "report"].includes(argv[0]) ? 1 : 0;
|
|
85
127
|
for (; index < argv.length; index += 1) {
|
|
86
128
|
const argument = argv[index];
|
|
87
129
|
if (argument === "--help" || argument === "-h") {
|
|
@@ -89,6 +131,20 @@ export function parseArgs(argv) {
|
|
|
89
131
|
} else if (argument === "--date") {
|
|
90
132
|
options.date = readOption(argv, index, "--date");
|
|
91
133
|
index += 1;
|
|
134
|
+
} else if (argument === "--period") {
|
|
135
|
+
if (options.view !== "trend") {
|
|
136
|
+
throw new Error("--period is only available for the trend view.");
|
|
137
|
+
}
|
|
138
|
+
if (trendPeriodSeen) {
|
|
139
|
+
throw new Error("Trend period can only be specified once.");
|
|
140
|
+
}
|
|
141
|
+
const value = readOption(argv, index, "--period");
|
|
142
|
+
if (!["7d", "14d", "30d"].includes(value)) {
|
|
143
|
+
throw new Error("Trend period must be 7d, 14d, or 30d.");
|
|
144
|
+
}
|
|
145
|
+
options.trendDays = Number.parseInt(value, 10);
|
|
146
|
+
trendPeriodSeen = true;
|
|
147
|
+
index += 1;
|
|
92
148
|
} else if (argument === "--input") {
|
|
93
149
|
options.input = resolve(readOption(argv, index, "--input"));
|
|
94
150
|
options.inputExplicit = true;
|
|
@@ -127,22 +183,72 @@ export function parseArgs(argv) {
|
|
|
127
183
|
options.ascii = true;
|
|
128
184
|
} else if (argument === "--static") {
|
|
129
185
|
options.static = true;
|
|
130
|
-
} else if (
|
|
186
|
+
} else if (argument === "--drain") {
|
|
187
|
+
if (options.view !== "trend") {
|
|
188
|
+
throw new Error("--drain is only available for the trend view.");
|
|
189
|
+
}
|
|
190
|
+
options.drain = true;
|
|
191
|
+
} else if (argument === "--image") {
|
|
192
|
+
if (options.view !== "trend") {
|
|
193
|
+
throw new Error("--image is only available for the trend view.");
|
|
194
|
+
}
|
|
195
|
+
options.image = true;
|
|
196
|
+
} else if (argument === "--image-output") {
|
|
197
|
+
if (options.view !== "trend") {
|
|
198
|
+
throw new Error("--image-output is only available for the trend view.");
|
|
199
|
+
}
|
|
200
|
+
const value = readOption(argv, index, "--image-output");
|
|
201
|
+
if (!value.toLowerCase().endsWith(".png")) {
|
|
202
|
+
throw new Error("--image-output must end in .png.");
|
|
203
|
+
}
|
|
204
|
+
options.image = true;
|
|
205
|
+
options.imageOutput = resolve(value);
|
|
206
|
+
index += 1;
|
|
207
|
+
} else if (argument === "--image-width") {
|
|
208
|
+
if (options.view !== "trend") {
|
|
209
|
+
throw new Error("--image-width is only available for the trend view.");
|
|
210
|
+
}
|
|
211
|
+
const value = Number(readOption(argv, index, "--image-width"));
|
|
212
|
+
if (!Number.isInteger(value) || value < 900 || value > 2400) {
|
|
213
|
+
throw new Error("--image-width must be an integer from 900 to 2400.");
|
|
214
|
+
}
|
|
215
|
+
options.image = true;
|
|
216
|
+
options.imageWidth = value;
|
|
217
|
+
index += 1;
|
|
218
|
+
} else if (argument === "--youplot") {
|
|
219
|
+
options.legacyPlot = true;
|
|
220
|
+
} else if (!argument.startsWith("-") && options.view === "trend") {
|
|
221
|
+
if (trendPeriodSeen) {
|
|
222
|
+
throw new Error("Trend period can only be specified once.");
|
|
223
|
+
}
|
|
224
|
+
if (!["7d", "14d", "30d"].includes(argument)) {
|
|
225
|
+
throw new Error("Trend period must be 7d, 14d, or 30d.");
|
|
226
|
+
}
|
|
227
|
+
options.trendDays = Number.parseInt(argument, 10);
|
|
228
|
+
trendPeriodSeen = true;
|
|
229
|
+
} else if (!argument.startsWith("-") && !options.date) {
|
|
131
230
|
options.date = argument;
|
|
132
231
|
} else {
|
|
133
232
|
throw new Error(`Unknown option: ${argument}`);
|
|
134
233
|
}
|
|
135
234
|
}
|
|
136
235
|
|
|
137
|
-
if (
|
|
236
|
+
if (options.report) options.image = true;
|
|
237
|
+
if (!options.help && !options.date && (options.range === "week" || options.view === "trend")) {
|
|
138
238
|
options.date = "today";
|
|
139
239
|
}
|
|
240
|
+
if (!options.help && !options.date) {
|
|
241
|
+
throw new Error("A day is required, for example: tledger day 2026-08-01");
|
|
242
|
+
}
|
|
140
243
|
if (!options.help && options.refresh && !options.autoRefresh) {
|
|
141
244
|
throw new Error("--refresh cannot be combined with --no-refresh.");
|
|
142
245
|
}
|
|
143
246
|
if (!options.help && options.refresh && options.inputExplicit) {
|
|
144
247
|
throw new Error("--refresh cannot be combined with --input.");
|
|
145
248
|
}
|
|
249
|
+
if (!options.help && options.view === "trend" && options.legacyPlot) {
|
|
250
|
+
throw new Error("--youplot is only available for the project view.");
|
|
251
|
+
}
|
|
146
252
|
return options;
|
|
147
253
|
}
|
|
148
254
|
|
|
@@ -336,6 +442,8 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
336
442
|
toolCalls: 0,
|
|
337
443
|
events: 0,
|
|
338
444
|
threadIds: new Set(),
|
|
445
|
+
rateCardCredits: 0,
|
|
446
|
+
knownCreditTokens: 0,
|
|
339
447
|
models: new Map(),
|
|
340
448
|
};
|
|
341
449
|
row.totalTokens += Number(event.totalTokens) || 0;
|
|
@@ -344,14 +452,23 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
344
452
|
row.toolCalls += Number(event.toolCalls) || 0;
|
|
345
453
|
row.events += 1;
|
|
346
454
|
if (event.threadId) row.threadIds.add(event.threadId);
|
|
455
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
456
|
+
row.rateCardCredits += Number(event.rateCardCredits);
|
|
457
|
+
row.knownCreditTokens += Number(event.totalTokens) || 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
347
460
|
const model = modelLabel(event.model);
|
|
348
461
|
const modelRow = row.models.get(model) ?? {
|
|
349
462
|
model,
|
|
350
463
|
totalTokens: 0,
|
|
351
464
|
events: 0,
|
|
465
|
+
rateCardCredits: 0,
|
|
352
466
|
};
|
|
353
467
|
modelRow.totalTokens += Number(event.totalTokens) || 0;
|
|
354
468
|
modelRow.events += 1;
|
|
469
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
470
|
+
modelRow.rateCardCredits += Number(event.rateCardCredits);
|
|
471
|
+
}
|
|
355
472
|
row.models.set(model, modelRow);
|
|
356
473
|
grouped.set(project, row);
|
|
357
474
|
}
|
|
@@ -372,56 +489,141 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
372
489
|
});
|
|
373
490
|
}
|
|
374
491
|
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
492
|
+
function totalSummary(events) {
|
|
493
|
+
return events.reduce(
|
|
494
|
+
(summary, event) => {
|
|
495
|
+
summary.totalTokens += Number(event.totalTokens) || 0;
|
|
496
|
+
summary.outputTokens += Number(event.outputTokens) || 0;
|
|
497
|
+
summary.toolCalls += Number(event.toolCalls) || 0;
|
|
498
|
+
if (event.threadId) summary.threadIds.add(event.threadId);
|
|
499
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
500
|
+
summary.rateCardCredits += Number(event.rateCardCredits);
|
|
501
|
+
summary.knownCreditTokens += Number(event.totalTokens) || 0;
|
|
502
|
+
}
|
|
503
|
+
return summary;
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
totalTokens: 0,
|
|
507
|
+
outputTokens: 0,
|
|
508
|
+
toolCalls: 0,
|
|
509
|
+
rateCardCredits: 0,
|
|
510
|
+
knownCreditTokens: 0,
|
|
511
|
+
threadIds: new Set(),
|
|
512
|
+
},
|
|
513
|
+
);
|
|
383
514
|
}
|
|
384
515
|
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
516
|
+
function compact(value, digits = 2) {
|
|
517
|
+
if (!Number.isFinite(value)) return "—";
|
|
518
|
+
const absolute = Math.abs(value);
|
|
519
|
+
const units = [
|
|
520
|
+
[1_000_000_000, "B"],
|
|
521
|
+
[1_000_000, "M"],
|
|
522
|
+
[1_000, "K"],
|
|
523
|
+
];
|
|
524
|
+
for (const [divisor, suffix] of units) {
|
|
525
|
+
if (absolute >= divisor) {
|
|
526
|
+
const scaled = value / divisor;
|
|
527
|
+
const precision = scaled >= 100 ? 0 : scaled >= 10 ? 1 : digits;
|
|
528
|
+
return `${scaled.toFixed(precision)}${suffix}`;
|
|
391
529
|
}
|
|
392
530
|
}
|
|
393
|
-
|
|
394
|
-
return dateStringFromParts(numericDateParts({
|
|
395
|
-
value: new Date(latestTimestamp),
|
|
396
|
-
timeZone,
|
|
397
|
-
}));
|
|
531
|
+
return Math.round(value).toLocaleString("en-US");
|
|
398
532
|
}
|
|
399
533
|
|
|
400
|
-
function
|
|
401
|
-
|
|
402
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
403
|
-
month: "long",
|
|
404
|
-
day: "numeric",
|
|
405
|
-
year: "numeric",
|
|
406
|
-
timeZone: "UTC",
|
|
407
|
-
}).format(new Date(Date.UTC(year, month - 1, day)));
|
|
534
|
+
function percent(value) {
|
|
535
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}%`;
|
|
408
536
|
}
|
|
409
537
|
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
538
|
+
function chartUnit(maximum) {
|
|
539
|
+
if (maximum >= 1_000_000_000) return { divisor: 1_000_000_000, suffix: "B" };
|
|
540
|
+
if (maximum >= 1_000_000) return { divisor: 1_000_000, suffix: "M" };
|
|
541
|
+
if (maximum >= 1_000) return { divisor: 1_000, suffix: "K" };
|
|
542
|
+
return { divisor: 1, suffix: "tokens" };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function chartNumber(value, divisor) {
|
|
546
|
+
const scaled = value / divisor;
|
|
547
|
+
if (scaled >= 100) return scaled.toFixed(0);
|
|
548
|
+
if (scaled >= 10) return scaled.toFixed(1);
|
|
549
|
+
return scaled.toFixed(2);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function colorize(value, code, enabled) {
|
|
553
|
+
const codes = Array.isArray(code) ? code.join(";") : code;
|
|
554
|
+
return enabled ? `\u001b[${codes}m${value}${ANSI_RESET}` : value;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function modelColor(model) {
|
|
558
|
+
const lower = model.toLowerCase();
|
|
559
|
+
if (lower.includes("sol")) return MODEL_COLORS.sol;
|
|
560
|
+
if (lower.includes("luna")) return MODEL_COLORS.luna;
|
|
561
|
+
if (lower.includes("terra")) return MODEL_COLORS.terra;
|
|
562
|
+
if (lower.includes("gpt-5.5") || lower.includes("gpt-5.4")) {
|
|
563
|
+
return MODEL_COLORS["gpt-5.5"];
|
|
564
|
+
}
|
|
565
|
+
return MODEL_COLORS.other;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function modelMix(row, enabled) {
|
|
569
|
+
return row.models
|
|
570
|
+
.slice(0, 4)
|
|
571
|
+
.map((model) => {
|
|
572
|
+
const share = row.totalTokens > 0 ? (model.totalTokens / row.totalTokens) * 100 : 0;
|
|
573
|
+
return `${colorize(model.model, modelColor(model.model), enabled)} ${percent(share)}`;
|
|
574
|
+
})
|
|
575
|
+
.join(" · ");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function sourceLabel(snapshotPath, snapshot) {
|
|
579
|
+
const generated = snapshot.generatedAt
|
|
580
|
+
? new Date(snapshot.generatedAt).toLocaleString("en-US", {
|
|
581
|
+
dateStyle: "medium",
|
|
582
|
+
timeStyle: "short",
|
|
583
|
+
})
|
|
584
|
+
: "unknown time";
|
|
585
|
+
return `${basename(snapshotPath)} · captured ${generated}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function runYouPlot(rows, options, dateLabel, unit) {
|
|
589
|
+
const chartInput = [
|
|
590
|
+
"project\tvalue",
|
|
591
|
+
...rows.map(
|
|
592
|
+
(row) => `${row.displayProject.replace(/[\t\r\n]+/g, " ")}\t${chartNumber(row.totalTokens, unit.divisor)}`,
|
|
593
|
+
),
|
|
594
|
+
].join("\n");
|
|
595
|
+
const terminalWidth = Number(process.stdout.columns) || 100;
|
|
596
|
+
const width = options.width ?? Math.max(56, Math.min(110, terminalWidth - 4));
|
|
597
|
+
const useColor = !options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY);
|
|
598
|
+
const args = [
|
|
599
|
+
"bar",
|
|
600
|
+
"-H",
|
|
601
|
+
"-o",
|
|
602
|
+
"-",
|
|
603
|
+
"-t",
|
|
604
|
+
`Top ${rows.length} projects · tokens (${unit.suffix}) · ${dateLabel}`,
|
|
605
|
+
"-w",
|
|
606
|
+
String(width),
|
|
607
|
+
"--symbol",
|
|
608
|
+
options.ascii ? "#" : "█",
|
|
417
609
|
];
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
610
|
+
if (useColor) args.push("-C", "-c", "blue");
|
|
611
|
+
else args.push("-M");
|
|
612
|
+
|
|
613
|
+
const result = spawnSync("uplot", args, {
|
|
614
|
+
input: `${chartInput}\n`,
|
|
615
|
+
encoding: "utf8",
|
|
616
|
+
maxBuffer: 1_000_000,
|
|
617
|
+
});
|
|
618
|
+
if (result.error?.code === "ENOENT") {
|
|
619
|
+
throw new Error(
|
|
620
|
+
"YouPlot is required. Install it with `brew install youplot`, then rerun this command.",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (result.status !== 0) {
|
|
624
|
+
throw new Error(result.stderr?.trim() || "YouPlot failed to render the chart.");
|
|
422
625
|
}
|
|
423
|
-
|
|
424
|
-
return lines.join("\n");
|
|
626
|
+
return result.stdout;
|
|
425
627
|
}
|
|
426
628
|
|
|
427
629
|
async function readSnapshot(snapshotPath) {
|
|
@@ -430,28 +632,22 @@ async function readSnapshot(snapshotPath) {
|
|
|
430
632
|
parsed = JSON.parse(await readFile(snapshotPath, "utf8"));
|
|
431
633
|
} catch (error) {
|
|
432
634
|
if (error?.code === "ENOENT") {
|
|
433
|
-
throw new Error(`Snapshot not found: ${
|
|
635
|
+
throw new Error(`Snapshot not found: ${snapshotPath}`);
|
|
434
636
|
}
|
|
435
|
-
throw new Error(
|
|
436
|
-
`Could not read snapshot ${sanitizeTerminalText(snapshotPath)}: ${sanitizeTerminalText(error.message)}`,
|
|
437
|
-
);
|
|
637
|
+
throw new Error(`Could not read snapshot ${snapshotPath}: ${error.message}`);
|
|
438
638
|
}
|
|
439
639
|
if (!parsed || !Array.isArray(parsed.events)) {
|
|
440
|
-
throw new Error(
|
|
441
|
-
`Snapshot is missing its events array: ${sanitizeTerminalText(snapshotPath)}`,
|
|
442
|
-
);
|
|
640
|
+
throw new Error(`Snapshot is missing its events array: ${snapshotPath}`);
|
|
443
641
|
}
|
|
444
642
|
return parsed;
|
|
445
643
|
}
|
|
446
644
|
|
|
447
645
|
async function refreshSnapshot(options) {
|
|
448
646
|
if (!existsSync(options.codexHome)) {
|
|
449
|
-
throw new Error(
|
|
450
|
-
`Codex data directory not found: ${sanitizeTerminalText(options.codexHome)}`,
|
|
451
|
-
);
|
|
647
|
+
throw new Error(`Codex data directory not found: ${options.codexHome}`);
|
|
452
648
|
}
|
|
453
649
|
const { collectUsage, writePrivateSnapshot } = await import(
|
|
454
|
-
"../lib/token-ledger-
|
|
650
|
+
"../lib/token-ledger-importer.mjs"
|
|
455
651
|
);
|
|
456
652
|
process.stderr.write("Token Ledger: refreshing local snapshot…\n");
|
|
457
653
|
const snapshot = await collectUsage(
|
|
@@ -470,18 +666,19 @@ async function refreshSnapshot(options) {
|
|
|
470
666
|
return snapshot;
|
|
471
667
|
}
|
|
472
668
|
|
|
473
|
-
export function snapshotNeedsRefresh(
|
|
669
|
+
export function snapshotNeedsRefresh(snapshotMtimeMs, latestJsonlMtimeMs) {
|
|
670
|
+
return latestJsonlMtimeMs > snapshotMtimeMs;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function snapshotCacheIsFresh(
|
|
474
674
|
snapshotMtimeMs,
|
|
475
|
-
|
|
476
|
-
cachedSourceFingerprint,
|
|
477
|
-
expectedSourceFingerprint,
|
|
478
|
-
cachedSourceFileCount,
|
|
479
|
-
currentSourceFileCount,
|
|
675
|
+
nowMs = Date.now(),
|
|
480
676
|
) {
|
|
481
677
|
return (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
678
|
+
Number.isFinite(snapshotMtimeMs) &&
|
|
679
|
+
Number.isFinite(nowMs) &&
|
|
680
|
+
snapshotMtimeMs <= nowMs &&
|
|
681
|
+
nowMs - snapshotMtimeMs < SNAPSHOT_CACHE_MAX_AGE_MS
|
|
485
682
|
);
|
|
486
683
|
}
|
|
487
684
|
|
|
@@ -491,7 +688,7 @@ async function loadSnapshot(options) {
|
|
|
491
688
|
}
|
|
492
689
|
if (!existsSync(options.input)) {
|
|
493
690
|
if (options.inputExplicit || !options.autoRefresh) {
|
|
494
|
-
throw new Error(`Snapshot not found: ${
|
|
691
|
+
throw new Error(`Snapshot not found: ${options.input}`);
|
|
495
692
|
}
|
|
496
693
|
return refreshSnapshot(options);
|
|
497
694
|
}
|
|
@@ -499,49 +696,135 @@ async function loadSnapshot(options) {
|
|
|
499
696
|
return readSnapshot(options.input);
|
|
500
697
|
}
|
|
501
698
|
|
|
502
|
-
const
|
|
503
|
-
|
|
699
|
+
const snapshotStat = await stat(options.input);
|
|
700
|
+
if (snapshotCacheIsFresh(snapshotStat.mtimeMs)) {
|
|
701
|
+
return readSnapshot(options.input);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const { latestSourceModifiedAt } = await import("../lib/token-ledger-importer.mjs");
|
|
705
|
+
const latestSourceMtimeMs = await latestSourceModifiedAt(
|
|
706
|
+
options.codexHome,
|
|
707
|
+
options.includeArchived,
|
|
504
708
|
);
|
|
505
|
-
|
|
506
|
-
stat(options.input),
|
|
507
|
-
sourceState(options.codexHome, options.includeArchived),
|
|
508
|
-
readSnapshot(options.input),
|
|
509
|
-
]);
|
|
510
|
-
if (snapshotNeedsRefresh(
|
|
511
|
-
snapshotStat.mtimeMs,
|
|
512
|
-
currentSourceState.latestMtimeMs,
|
|
513
|
-
snapshot.provenance?.sourceFingerprint,
|
|
514
|
-
sourceFingerprint(options.codexHome, options.includeArchived),
|
|
515
|
-
snapshot.coverage?.sourceFileCount,
|
|
516
|
-
currentSourceState.fileCount,
|
|
517
|
-
)) {
|
|
709
|
+
if (snapshotNeedsRefresh(snapshotStat.mtimeMs, latestSourceMtimeMs)) {
|
|
518
710
|
return refreshSnapshot(options);
|
|
519
711
|
}
|
|
520
|
-
return
|
|
712
|
+
return readSnapshot(options.input);
|
|
521
713
|
}
|
|
522
714
|
|
|
523
715
|
function render(options, snapshot, bounds, events, rows, allRows) {
|
|
524
|
-
|
|
716
|
+
if (options.view === "trend") {
|
|
717
|
+
const trend = buildUsageTrend(snapshot, bounds);
|
|
718
|
+
if (options.image) {
|
|
719
|
+
return renderTrendImage({
|
|
720
|
+
snapshot,
|
|
721
|
+
bounds,
|
|
722
|
+
trend,
|
|
723
|
+
days: options.trendDays,
|
|
724
|
+
options,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
return renderTrendCombo({
|
|
728
|
+
snapshot,
|
|
729
|
+
bounds,
|
|
730
|
+
trend,
|
|
731
|
+
days: options.trendDays,
|
|
732
|
+
options,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (!options.legacyPlot) {
|
|
736
|
+
return renderTerminal({ options, snapshot, bounds, events, rows, allRows });
|
|
737
|
+
}
|
|
738
|
+
const enabled = !options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY);
|
|
739
|
+
const summary = totalSummary(events);
|
|
740
|
+
const totalTokens = summary.totalTokens;
|
|
741
|
+
const dateLabel = options.range === "week"
|
|
742
|
+
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
743
|
+
: new Intl.DateTimeFormat("en-US", {
|
|
744
|
+
timeZone: bounds.timeZone,
|
|
745
|
+
weekday: "short",
|
|
746
|
+
month: "short",
|
|
747
|
+
day: "numeric",
|
|
748
|
+
year: "numeric",
|
|
749
|
+
}).format(bounds.start);
|
|
750
|
+
const unit = chartUnit(rows[0]?.totalTokens ?? 0);
|
|
751
|
+
const shares = rows.map((row) =>
|
|
752
|
+
totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0,
|
|
753
|
+
);
|
|
754
|
+
const chart = runYouPlot(rows, options, dateLabel, unit).trimEnd();
|
|
755
|
+
|
|
756
|
+
const header = [
|
|
757
|
+
`Token Ledger · ${dateLabel} · ${bounds.timeZone}`,
|
|
758
|
+
`${compact(totalTokens)} tokens · ${summary.threadIds.size.toLocaleString()} threads · ${events.length.toLocaleString()} calls · ${compact(summary.outputTokens)} output`,
|
|
759
|
+
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
760
|
+
"",
|
|
761
|
+
chart,
|
|
762
|
+
"",
|
|
763
|
+
`Model mix · colors: ${colorize("Sol", MODEL_COLORS.sol, enabled)} ${colorize("Luna", MODEL_COLORS.luna, enabled)} ${colorize("Terra", MODEL_COLORS.terra, enabled)} ${colorize("GPT", MODEL_COLORS["gpt-5.5"], enabled)} ${colorize("Other", MODEL_COLORS.other, enabled)}`,
|
|
764
|
+
];
|
|
765
|
+
|
|
766
|
+
const details = rows.map((row, index) => {
|
|
767
|
+
const knownCreditShare =
|
|
768
|
+
summary.rateCardCredits > 0 && row.rateCardCredits > 0
|
|
769
|
+
? ` · ${percent((row.rateCardCredits / summary.rateCardCredits) * 100)} credits`
|
|
770
|
+
: "";
|
|
771
|
+
return `${String(index + 1).padStart(2, " ")} ${row.displayProject} · ${compact(row.totalTokens)} · ${percent(shares[index])} · ${row.threads.toLocaleString()} threads${knownCreditShare}\n ${modelMix(row, enabled)}`;
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return `${header.join("\n")}\n\n${details.join("\n")}`;
|
|
525
775
|
}
|
|
526
776
|
|
|
527
777
|
export async function run(options) {
|
|
528
|
-
const bounds = options.
|
|
529
|
-
?
|
|
530
|
-
:
|
|
778
|
+
const bounds = options.view === "trend"
|
|
779
|
+
? multiDayBounds(options.date, options.timeZone, options.trendDays)
|
|
780
|
+
: options.range === "week"
|
|
781
|
+
? weekBounds(options.date, options.timeZone)
|
|
782
|
+
: dayBounds(options.date, options.timeZone);
|
|
531
783
|
const snapshot = await loadSnapshot(options);
|
|
532
784
|
const events = filterDayEvents(snapshot, bounds);
|
|
533
785
|
if (events.length === 0) {
|
|
534
|
-
|
|
786
|
+
const rangeDescription = options.range === "week"
|
|
787
|
+
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
788
|
+
: bounds.dateString;
|
|
789
|
+
return [
|
|
790
|
+
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
791
|
+
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
792
|
+
].join("\n");
|
|
535
793
|
}
|
|
536
794
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
537
795
|
const rows = allRows.slice(0, options.top);
|
|
538
|
-
|
|
796
|
+
const writingImage = options.view === "trend" && options.image;
|
|
797
|
+
const outputPath = writingImage
|
|
798
|
+
? options.imageOutput ??
|
|
799
|
+
resolve(
|
|
800
|
+
process.cwd(),
|
|
801
|
+
`token-ledger-${options.report ? "report" : "trend"}-${options.trendDays}d.png`,
|
|
802
|
+
)
|
|
803
|
+
: null;
|
|
804
|
+
const imageLabel = options.report ? "report" : "trend image";
|
|
805
|
+
if (writingImage) {
|
|
806
|
+
process.stderr.write(`Token Ledger: generating ${imageLabel} PNG…\n`);
|
|
807
|
+
}
|
|
808
|
+
const output = render(options, snapshot, bounds, events, rows, allRows);
|
|
809
|
+
if (writingImage) {
|
|
810
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
811
|
+
process.stderr.write(`Token Ledger: encoding ${imageLabel} PNG…\n`);
|
|
812
|
+
await writeTrendPng(output, outputPath);
|
|
813
|
+
process.stderr.write(`Token Ledger: finished ${imageLabel} PNG.\n`);
|
|
814
|
+
return [
|
|
815
|
+
`Wrote ${options.report ? "report" : "trend image"}: ${outputPath}`,
|
|
816
|
+
`Range: ${bounds.startDateString} through ${bounds.endDateString} (${bounds.timeZone})`,
|
|
817
|
+
].join("\n");
|
|
818
|
+
}
|
|
819
|
+
return output;
|
|
539
820
|
}
|
|
540
821
|
|
|
541
822
|
function shouldUseInteractive(options) {
|
|
542
823
|
return Boolean(
|
|
543
824
|
!options.static &&
|
|
825
|
+
options.view !== "trend" &&
|
|
544
826
|
!options.plain &&
|
|
827
|
+
!options.legacyPlot &&
|
|
545
828
|
!process.env.NO_COLOR &&
|
|
546
829
|
process.stdin.isTTY &&
|
|
547
830
|
process.stdout.isTTY,
|
|
@@ -555,7 +838,14 @@ async function runInteractive(options) {
|
|
|
555
838
|
const snapshot = await loadSnapshot(options);
|
|
556
839
|
const events = filterDayEvents(snapshot, bounds);
|
|
557
840
|
if (events.length === 0) {
|
|
558
|
-
|
|
841
|
+
const rangeDescription = options.range === "week"
|
|
842
|
+
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
843
|
+
: bounds.dateString;
|
|
844
|
+
process.stdout.write([
|
|
845
|
+
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
846
|
+
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
847
|
+
"",
|
|
848
|
+
].join("\n"));
|
|
559
849
|
return;
|
|
560
850
|
}
|
|
561
851
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
@@ -580,12 +870,10 @@ async function main() {
|
|
|
580
870
|
if (shouldUseInteractive(options)) {
|
|
581
871
|
await runInteractive(options);
|
|
582
872
|
} else {
|
|
583
|
-
process.stdout.write(`${await run(
|
|
873
|
+
process.stdout.write(`${await run(options)}\n`);
|
|
584
874
|
}
|
|
585
875
|
} catch (error) {
|
|
586
|
-
process.stderr.write(
|
|
587
|
-
`Token Ledger CLI failed: ${sanitizeTerminalText(error.message)}\n\n${usage()}\n`,
|
|
588
|
-
);
|
|
876
|
+
process.stderr.write(`Token Ledger CLI failed: ${error.message}\n\n${usage()}\n`);
|
|
589
877
|
process.exitCode = 1;
|
|
590
878
|
}
|
|
591
879
|
}
|