tledger 0.1.4 → 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 +122 -80
- package/bin/token-ledger-rates.mjs +62 -0
- package/bin/token-ledger-terminal.mjs +133 -257
- 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 +408 -248
- package/lib/{token-ledger-collector.mjs → token-ledger-importer.mjs} +256 -409
- package/package.json +18 -14
- package/lib/token-ledger-models.mjs +0 -113
package/bin/token-ledger.mjs
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
5
|
import {
|
|
6
|
+
mkdir,
|
|
6
7
|
readFile,
|
|
7
8
|
stat,
|
|
8
9
|
} from "node:fs/promises";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
|
-
import { basename, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, resolve } from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
|
|
13
|
-
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";
|
|
14
24
|
import { startInteractive } from "./token-ledger-tui.mjs";
|
|
15
|
-
import { modelDisplayName } from "../lib/token-ledger-models.mjs";
|
|
16
|
-
|
|
17
|
-
const require = createRequire(import.meta.url);
|
|
18
|
-
export const VERSION = require("../package.json").version;
|
|
19
25
|
|
|
20
26
|
export const DEFAULT_SNAPSHOT = resolve(
|
|
21
27
|
homedir(),
|
|
@@ -23,29 +29,32 @@ export const DEFAULT_SNAPSHOT = resolve(
|
|
|
23
29
|
"token-ledger-snapshot.json",
|
|
24
30
|
);
|
|
25
31
|
const DEFAULT_TOP = 10;
|
|
26
|
-
const MAX_RANGE_DAYS = 100_000;
|
|
27
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
|
+
};
|
|
28
43
|
|
|
29
44
|
function usage() {
|
|
30
45
|
return `Token Ledger terminal usage
|
|
31
46
|
|
|
32
47
|
Usage:
|
|
33
|
-
tledger
|
|
48
|
+
tledger day <YYYY-MM-DD>
|
|
34
49
|
tledger week [end-day]
|
|
35
|
-
tledger
|
|
36
|
-
tledger
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Ranges:
|
|
41
|
-
day One calendar day
|
|
42
|
-
week 7 days ending on end-day (default: today)
|
|
43
|
-
month 30 days ending on end-day (default: today)
|
|
44
|
-
<number>d That many days ending on end-day, for example 90d
|
|
45
|
-
all Every dated event in the snapshot
|
|
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]
|
|
46
54
|
|
|
47
55
|
Options:
|
|
48
56
|
--date <day> Date as YYYY-MM-DD, today, or yesterday
|
|
57
|
+
--period <window> Trend window: 7d, 14d, or 30d
|
|
49
58
|
--input <file> Snapshot to read (default: ~/.token-ledger/token-ledger-snapshot.json)
|
|
50
59
|
--refresh Rebuild the default snapshot from CODEX_HOME or ~/.codex
|
|
51
60
|
--no-refresh Use the cached snapshot without checking local JSONL files
|
|
@@ -54,16 +63,22 @@ Options:
|
|
|
54
63
|
--top <number> Number of projects to show (default: 10)
|
|
55
64
|
--width <number> Terminal layout width in columns
|
|
56
65
|
--raw-projects Keep singleton thread labels instead of grouping them
|
|
57
|
-
-anon Replace project names with Project 1, Project 2, and so on
|
|
58
66
|
--no-archived Skip archived_sessions when refreshing
|
|
59
67
|
--plain Disable terminal colors
|
|
60
68
|
--ascii Use ASCII bars instead of Unicode blocks
|
|
61
69
|
--static Print once instead of opening the interactive dashboard
|
|
62
|
-
|
|
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
|
|
63
75
|
--help Show this help
|
|
64
76
|
|
|
65
|
-
The
|
|
66
|
-
|
|
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.`;
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
function readOption(argv, index, name) {
|
|
@@ -74,38 +89,17 @@ function readOption(argv, index, name) {
|
|
|
74
89
|
return value;
|
|
75
90
|
}
|
|
76
91
|
|
|
77
|
-
function rangeSpec(value) {
|
|
78
|
-
if (value === "day") return { range: "day", rangeDays: 1 };
|
|
79
|
-
if (value === "week") return { range: "week", rangeDays: 7 };
|
|
80
|
-
if (value === "month") return { range: "month", rangeDays: 30 };
|
|
81
|
-
if (value === "all") return { range: "all", rangeDays: null };
|
|
82
|
-
const custom = /^(\d+)d$/.exec(value ?? "");
|
|
83
|
-
if (!custom) return null;
|
|
84
|
-
const rangeDays = Number(custom[1]);
|
|
85
|
-
if (
|
|
86
|
-
!Number.isSafeInteger(rangeDays) ||
|
|
87
|
-
rangeDays < 1 ||
|
|
88
|
-
rangeDays > MAX_RANGE_DAYS
|
|
89
|
-
) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Day range must be an integer from 1 to ${MAX_RANGE_DAYS.toLocaleString("en-US")}, for example 90d.`,
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
return { range: `${rangeDays}d`, rangeDays };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
92
|
export function parseArgs(argv) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
const command = requestedRange ?? { range: "week", rangeDays: 7 };
|
|
93
|
+
const command = argv[0] === "week"
|
|
94
|
+
? "week"
|
|
95
|
+
: argv[0] === "trend" || argv[0] === "report"
|
|
96
|
+
? "trend"
|
|
97
|
+
: "day";
|
|
106
98
|
const options = {
|
|
107
|
-
range: command
|
|
108
|
-
|
|
99
|
+
range: command,
|
|
100
|
+
view: command === "trend" ? "trend" : "projects",
|
|
101
|
+
report: argv[0] === "report",
|
|
102
|
+
trendDays: 7,
|
|
109
103
|
date: null,
|
|
110
104
|
input: DEFAULT_SNAPSHOT,
|
|
111
105
|
inputExplicit: false,
|
|
@@ -117,24 +111,40 @@ export function parseArgs(argv) {
|
|
|
117
111
|
top: DEFAULT_TOP,
|
|
118
112
|
width: null,
|
|
119
113
|
rawProjects: false,
|
|
120
|
-
anonymizeProjects: false,
|
|
121
114
|
plain: false,
|
|
122
115
|
ascii: false,
|
|
123
116
|
static: false,
|
|
124
|
-
|
|
117
|
+
image: false,
|
|
118
|
+
imageOutput: null,
|
|
119
|
+
imageWidth: null,
|
|
120
|
+
drain: false,
|
|
121
|
+
legacyPlot: false,
|
|
125
122
|
help: false,
|
|
126
123
|
};
|
|
127
124
|
|
|
128
|
-
let
|
|
125
|
+
let trendPeriodSeen = false;
|
|
126
|
+
let index = ["day", "week", "trend", "report"].includes(argv[0]) ? 1 : 0;
|
|
129
127
|
for (; index < argv.length; index += 1) {
|
|
130
128
|
const argument = argv[index];
|
|
131
129
|
if (argument === "--help" || argument === "-h") {
|
|
132
130
|
options.help = true;
|
|
133
|
-
} else if (argument === "--version" || argument === "-v") {
|
|
134
|
-
options.version = true;
|
|
135
131
|
} else if (argument === "--date") {
|
|
136
132
|
options.date = readOption(argv, index, "--date");
|
|
137
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;
|
|
138
148
|
} else if (argument === "--input") {
|
|
139
149
|
options.input = resolve(readOption(argv, index, "--input"));
|
|
140
150
|
options.inputExplicit = true;
|
|
@@ -165,8 +175,6 @@ export function parseArgs(argv) {
|
|
|
165
175
|
index += 1;
|
|
166
176
|
} else if (argument === "--raw-projects") {
|
|
167
177
|
options.rawProjects = true;
|
|
168
|
-
} else if (argument === "-anon") {
|
|
169
|
-
options.anonymizeProjects = true;
|
|
170
178
|
} else if (argument === "--no-archived") {
|
|
171
179
|
options.includeArchived = false;
|
|
172
180
|
} else if (argument === "--plain") {
|
|
@@ -175,25 +183,72 @@ export function parseArgs(argv) {
|
|
|
175
183
|
options.ascii = true;
|
|
176
184
|
} else if (argument === "--static") {
|
|
177
185
|
options.static = true;
|
|
178
|
-
} 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) {
|
|
179
230
|
options.date = argument;
|
|
180
231
|
} else {
|
|
181
232
|
throw new Error(`Unknown option: ${argument}`);
|
|
182
233
|
}
|
|
183
234
|
}
|
|
184
235
|
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
if (!options.help && !options.version && !options.date && options.range !== "all") {
|
|
236
|
+
if (options.report) options.image = true;
|
|
237
|
+
if (!options.help && !options.date && (options.range === "week" || options.view === "trend")) {
|
|
189
238
|
options.date = "today";
|
|
190
239
|
}
|
|
191
|
-
if (!options.help && !options.
|
|
240
|
+
if (!options.help && !options.date) {
|
|
241
|
+
throw new Error("A day is required, for example: tledger day 2026-08-01");
|
|
242
|
+
}
|
|
243
|
+
if (!options.help && options.refresh && !options.autoRefresh) {
|
|
192
244
|
throw new Error("--refresh cannot be combined with --no-refresh.");
|
|
193
245
|
}
|
|
194
|
-
if (!options.help &&
|
|
246
|
+
if (!options.help && options.refresh && options.inputExplicit) {
|
|
195
247
|
throw new Error("--refresh cannot be combined with --input.");
|
|
196
248
|
}
|
|
249
|
+
if (!options.help && options.view === "trend" && options.legacyPlot) {
|
|
250
|
+
throw new Error("--youplot is only available for the project view.");
|
|
251
|
+
}
|
|
197
252
|
return options;
|
|
198
253
|
}
|
|
199
254
|
|
|
@@ -294,99 +349,21 @@ export function dayBounds(value, timeZone) {
|
|
|
294
349
|
const nextDateString = shiftCalendarDate(dateString, 1);
|
|
295
350
|
const start = zonedMidnight(dateString, timeZone);
|
|
296
351
|
const end = zonedMidnight(nextDateString, timeZone);
|
|
297
|
-
return {
|
|
298
|
-
dateString,
|
|
299
|
-
startDateString: dateString,
|
|
300
|
-
endDateString: dateString,
|
|
301
|
-
start,
|
|
302
|
-
end,
|
|
303
|
-
timeZone,
|
|
304
|
-
rangeDays: 1,
|
|
305
|
-
};
|
|
352
|
+
return { dateString, start, end, timeZone };
|
|
306
353
|
}
|
|
307
354
|
|
|
308
|
-
export function
|
|
309
|
-
if (
|
|
310
|
-
!Number.isSafeInteger(rangeDays) ||
|
|
311
|
-
rangeDays < 1 ||
|
|
312
|
-
rangeDays > MAX_RANGE_DAYS
|
|
313
|
-
) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`Range days must be an integer from 1 to ${MAX_RANGE_DAYS.toLocaleString("en-US")}.`,
|
|
316
|
-
);
|
|
317
|
-
}
|
|
355
|
+
export function weekBounds(value, timeZone) {
|
|
318
356
|
const endDay = dayBounds(value, timeZone);
|
|
319
|
-
const startDateString = shiftCalendarDate(endDay.dateString, -
|
|
357
|
+
const startDateString = shiftCalendarDate(endDay.dateString, -6);
|
|
320
358
|
return {
|
|
321
359
|
...endDay,
|
|
322
360
|
startDateString,
|
|
323
361
|
endDateString: endDay.dateString,
|
|
324
362
|
start: zonedMidnight(startDateString, timeZone),
|
|
325
|
-
rangeDays,
|
|
363
|
+
rangeDays: 7,
|
|
326
364
|
};
|
|
327
365
|
}
|
|
328
366
|
|
|
329
|
-
export function weekBounds(value, timeZone) {
|
|
330
|
-
return rollingBounds(value, timeZone, 7);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function monthBounds(value, timeZone) {
|
|
334
|
-
return rollingBounds(value, timeZone, 30);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function eventTimestamp(event) {
|
|
338
|
-
if (typeof event?.timestamp !== "string" || !event.timestamp.trim()) {
|
|
339
|
-
return Number.NaN;
|
|
340
|
-
}
|
|
341
|
-
return new Date(event.timestamp).getTime();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export function allBounds(snapshot, timeZone) {
|
|
345
|
-
validateTimeZone(timeZone);
|
|
346
|
-
let earliest = Number.POSITIVE_INFINITY;
|
|
347
|
-
let latest = Number.NEGATIVE_INFINITY;
|
|
348
|
-
for (const event of snapshot.events ?? []) {
|
|
349
|
-
const timestamp = eventTimestamp(event);
|
|
350
|
-
if (!Number.isFinite(timestamp)) continue;
|
|
351
|
-
earliest = Math.min(earliest, timestamp);
|
|
352
|
-
latest = Math.max(latest, timestamp);
|
|
353
|
-
}
|
|
354
|
-
if (!Number.isFinite(earliest)) {
|
|
355
|
-
return {
|
|
356
|
-
...dayBounds("today", timeZone),
|
|
357
|
-
rangeDays: null,
|
|
358
|
-
allTime: true,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
const dateString = (timestamp) => dateStringFromParts(numericDateParts({
|
|
362
|
-
value: new Date(timestamp),
|
|
363
|
-
timeZone,
|
|
364
|
-
}));
|
|
365
|
-
const startDateString = dateString(earliest);
|
|
366
|
-
const endDateString = dateString(latest);
|
|
367
|
-
return {
|
|
368
|
-
dateString: endDateString,
|
|
369
|
-
startDateString,
|
|
370
|
-
endDateString,
|
|
371
|
-
start: zonedMidnight(startDateString, timeZone),
|
|
372
|
-
end: zonedMidnight(shiftCalendarDate(endDateString, 1), timeZone),
|
|
373
|
-
timeZone,
|
|
374
|
-
rangeDays: null,
|
|
375
|
-
allTime: true,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function boundsForOptions(options, snapshot) {
|
|
380
|
-
if (options.range === "all") return allBounds(snapshot, options.timeZone);
|
|
381
|
-
return rollingBounds(options.date, options.timeZone, options.rangeDays);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function describeRange(options, bounds) {
|
|
385
|
-
if (options.range === "all") return "all time";
|
|
386
|
-
if (bounds.rangeDays === 1) return bounds.dateString;
|
|
387
|
-
return `${bounds.startDateString} through ${bounds.endDateString}`;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
367
|
export function sanitizeTerminalText(value) {
|
|
391
368
|
return String(value ?? "")
|
|
392
369
|
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
@@ -427,14 +404,20 @@ export function oneOffProjects(snapshot) {
|
|
|
427
404
|
|
|
428
405
|
function modelLabel(value) {
|
|
429
406
|
const model = cleanLabel(value, "Unknown model");
|
|
430
|
-
|
|
407
|
+
const lower = model.toLowerCase();
|
|
408
|
+
if (lower.includes("sol")) return "Sol";
|
|
409
|
+
if (lower.includes("luna")) return "Luna";
|
|
410
|
+
if (lower.includes("terra")) return "Terra";
|
|
411
|
+
if (lower === "gpt-5.5") return "GPT-5.5";
|
|
412
|
+
if (lower === "gpt-5.4") return "GPT-5.4";
|
|
413
|
+
return model;
|
|
431
414
|
}
|
|
432
415
|
|
|
433
416
|
export function filterDayEvents(snapshot, bounds) {
|
|
434
417
|
const start = bounds.start.getTime();
|
|
435
418
|
const end = bounds.end.getTime();
|
|
436
419
|
return (snapshot.events ?? []).filter((event) => {
|
|
437
|
-
const timestamp =
|
|
420
|
+
const timestamp = new Date(event.timestamp).getTime();
|
|
438
421
|
return Number.isFinite(timestamp) && timestamp >= start && timestamp < end;
|
|
439
422
|
});
|
|
440
423
|
}
|
|
@@ -459,6 +442,8 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
459
442
|
toolCalls: 0,
|
|
460
443
|
events: 0,
|
|
461
444
|
threadIds: new Set(),
|
|
445
|
+
rateCardCredits: 0,
|
|
446
|
+
knownCreditTokens: 0,
|
|
462
447
|
models: new Map(),
|
|
463
448
|
};
|
|
464
449
|
row.totalTokens += Number(event.totalTokens) || 0;
|
|
@@ -467,14 +452,23 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
467
452
|
row.toolCalls += Number(event.toolCalls) || 0;
|
|
468
453
|
row.events += 1;
|
|
469
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
|
+
|
|
470
460
|
const model = modelLabel(event.model);
|
|
471
461
|
const modelRow = row.models.get(model) ?? {
|
|
472
462
|
model,
|
|
473
463
|
totalTokens: 0,
|
|
474
464
|
events: 0,
|
|
465
|
+
rateCardCredits: 0,
|
|
475
466
|
};
|
|
476
467
|
modelRow.totalTokens += Number(event.totalTokens) || 0;
|
|
477
468
|
modelRow.events += 1;
|
|
469
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
470
|
+
modelRow.rateCardCredits += Number(event.rateCardCredits);
|
|
471
|
+
}
|
|
478
472
|
row.models.set(model, modelRow);
|
|
479
473
|
grouped.set(project, row);
|
|
480
474
|
}
|
|
@@ -492,13 +486,93 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
492
486
|
return right.totalTokens - left.totalTokens;
|
|
493
487
|
}
|
|
494
488
|
return left.project.localeCompare(right.project);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
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
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
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}`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return Math.round(value).toLocaleString("en-US");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function percent(value) {
|
|
535
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}%`;
|
|
536
|
+
}
|
|
537
|
+
|
|
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)}`;
|
|
495
574
|
})
|
|
496
|
-
.
|
|
497
|
-
...row,
|
|
498
|
-
displayProject: options.anonymizeProjects
|
|
499
|
-
? `Project ${index + 1}`
|
|
500
|
-
: row.displayProject,
|
|
501
|
-
}));
|
|
575
|
+
.join(" · ");
|
|
502
576
|
}
|
|
503
577
|
|
|
504
578
|
function sourceLabel(snapshotPath, snapshot) {
|
|
@@ -508,46 +582,48 @@ function sourceLabel(snapshotPath, snapshot) {
|
|
|
508
582
|
timeStyle: "short",
|
|
509
583
|
})
|
|
510
584
|
: "unknown time";
|
|
511
|
-
return `${
|
|
512
|
-
}
|
|
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 ? "#" : "█",
|
|
609
|
+
];
|
|
610
|
+
if (useColor) args.push("-C", "-c", "blue");
|
|
611
|
+
else args.push("-M");
|
|
513
612
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
);
|
|
521
622
|
}
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
value: new Date(latestTimestamp),
|
|
525
|
-
timeZone,
|
|
526
|
-
}));
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function displayCalendarDate(dateString) {
|
|
530
|
-
const [year, month, day] = dateString.split("-").map(Number);
|
|
531
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
532
|
-
month: "long",
|
|
533
|
-
day: "numeric",
|
|
534
|
-
year: "numeric",
|
|
535
|
-
timeZone: "UTC",
|
|
536
|
-
}).format(new Date(Date.UTC(year, month - 1, day)));
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function emptyState(options, snapshot, bounds) {
|
|
540
|
-
const lines = [
|
|
541
|
-
`No model-call events found for ${describeRange(options, bounds)} (${bounds.timeZone}).`,
|
|
542
|
-
"Token Ledger reads only Codex history stored on this computer.",
|
|
543
|
-
];
|
|
544
|
-
const latestDate = latestActivityDateString(snapshot, bounds.timeZone);
|
|
545
|
-
if (latestDate) {
|
|
546
|
-
lines.push(`Latest local activity: ${displayCalendarDate(latestDate)}.`);
|
|
547
|
-
lines.push(`Try: tledger ${options.range} ${latestDate}`);
|
|
623
|
+
if (result.status !== 0) {
|
|
624
|
+
throw new Error(result.stderr?.trim() || "YouPlot failed to render the chart.");
|
|
548
625
|
}
|
|
549
|
-
|
|
550
|
-
return lines.join("\n");
|
|
626
|
+
return result.stdout;
|
|
551
627
|
}
|
|
552
628
|
|
|
553
629
|
async function readSnapshot(snapshotPath) {
|
|
@@ -556,28 +632,22 @@ async function readSnapshot(snapshotPath) {
|
|
|
556
632
|
parsed = JSON.parse(await readFile(snapshotPath, "utf8"));
|
|
557
633
|
} catch (error) {
|
|
558
634
|
if (error?.code === "ENOENT") {
|
|
559
|
-
throw new Error(`Snapshot not found: ${
|
|
635
|
+
throw new Error(`Snapshot not found: ${snapshotPath}`);
|
|
560
636
|
}
|
|
561
|
-
throw new Error(
|
|
562
|
-
`Could not read snapshot ${sanitizeTerminalText(snapshotPath)}: ${sanitizeTerminalText(error.message)}`,
|
|
563
|
-
);
|
|
637
|
+
throw new Error(`Could not read snapshot ${snapshotPath}: ${error.message}`);
|
|
564
638
|
}
|
|
565
639
|
if (!parsed || !Array.isArray(parsed.events)) {
|
|
566
|
-
throw new Error(
|
|
567
|
-
`Snapshot is missing its events array: ${sanitizeTerminalText(snapshotPath)}`,
|
|
568
|
-
);
|
|
640
|
+
throw new Error(`Snapshot is missing its events array: ${snapshotPath}`);
|
|
569
641
|
}
|
|
570
642
|
return parsed;
|
|
571
643
|
}
|
|
572
644
|
|
|
573
645
|
async function refreshSnapshot(options) {
|
|
574
646
|
if (!existsSync(options.codexHome)) {
|
|
575
|
-
throw new Error(
|
|
576
|
-
`Codex data directory not found: ${sanitizeTerminalText(options.codexHome)}`,
|
|
577
|
-
);
|
|
647
|
+
throw new Error(`Codex data directory not found: ${options.codexHome}`);
|
|
578
648
|
}
|
|
579
649
|
const { collectUsage, writePrivateSnapshot } = await import(
|
|
580
|
-
"../lib/token-ledger-
|
|
650
|
+
"../lib/token-ledger-importer.mjs"
|
|
581
651
|
);
|
|
582
652
|
process.stderr.write("Token Ledger: refreshing local snapshot…\n");
|
|
583
653
|
const snapshot = await collectUsage(
|
|
@@ -596,18 +666,19 @@ async function refreshSnapshot(options) {
|
|
|
596
666
|
return snapshot;
|
|
597
667
|
}
|
|
598
668
|
|
|
599
|
-
export function snapshotNeedsRefresh(
|
|
669
|
+
export function snapshotNeedsRefresh(snapshotMtimeMs, latestJsonlMtimeMs) {
|
|
670
|
+
return latestJsonlMtimeMs > snapshotMtimeMs;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function snapshotCacheIsFresh(
|
|
600
674
|
snapshotMtimeMs,
|
|
601
|
-
|
|
602
|
-
cachedSourceFingerprint,
|
|
603
|
-
expectedSourceFingerprint,
|
|
604
|
-
cachedSourceFileCount,
|
|
605
|
-
currentSourceFileCount,
|
|
675
|
+
nowMs = Date.now(),
|
|
606
676
|
) {
|
|
607
677
|
return (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
678
|
+
Number.isFinite(snapshotMtimeMs) &&
|
|
679
|
+
Number.isFinite(nowMs) &&
|
|
680
|
+
snapshotMtimeMs <= nowMs &&
|
|
681
|
+
nowMs - snapshotMtimeMs < SNAPSHOT_CACHE_MAX_AGE_MS
|
|
611
682
|
);
|
|
612
683
|
}
|
|
613
684
|
|
|
@@ -617,7 +688,7 @@ async function loadSnapshot(options) {
|
|
|
617
688
|
}
|
|
618
689
|
if (!existsSync(options.input)) {
|
|
619
690
|
if (options.inputExplicit || !options.autoRefresh) {
|
|
620
|
-
throw new Error(`Snapshot not found: ${
|
|
691
|
+
throw new Error(`Snapshot not found: ${options.input}`);
|
|
621
692
|
}
|
|
622
693
|
return refreshSnapshot(options);
|
|
623
694
|
}
|
|
@@ -625,48 +696,135 @@ async function loadSnapshot(options) {
|
|
|
625
696
|
return readSnapshot(options.input);
|
|
626
697
|
}
|
|
627
698
|
|
|
628
|
-
const
|
|
629
|
-
|
|
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,
|
|
630
708
|
);
|
|
631
|
-
|
|
632
|
-
stat(options.input),
|
|
633
|
-
sourceState(options.codexHome, options.includeArchived),
|
|
634
|
-
readSnapshot(options.input),
|
|
635
|
-
]);
|
|
636
|
-
if (snapshotNeedsRefresh(
|
|
637
|
-
snapshotStat.mtimeMs,
|
|
638
|
-
currentSourceState.latestMtimeMs,
|
|
639
|
-
snapshot.provenance?.sourceFingerprint,
|
|
640
|
-
sourceFingerprint(options.codexHome, options.includeArchived),
|
|
641
|
-
snapshot.coverage?.sourceFileCount,
|
|
642
|
-
currentSourceState.fileCount,
|
|
643
|
-
)) {
|
|
709
|
+
if (snapshotNeedsRefresh(snapshotStat.mtimeMs, latestSourceMtimeMs)) {
|
|
644
710
|
return refreshSnapshot(options);
|
|
645
711
|
}
|
|
646
|
-
return
|
|
712
|
+
return readSnapshot(options.input);
|
|
647
713
|
}
|
|
648
714
|
|
|
649
715
|
function render(options, snapshot, bounds, events, rows, allRows) {
|
|
650
|
-
|
|
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")}`;
|
|
651
775
|
}
|
|
652
776
|
|
|
653
777
|
export async function run(options) {
|
|
654
|
-
|
|
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);
|
|
655
783
|
const snapshot = await loadSnapshot(options);
|
|
656
|
-
const bounds = boundsForOptions(options, snapshot);
|
|
657
784
|
const events = filterDayEvents(snapshot, bounds);
|
|
658
785
|
if (events.length === 0) {
|
|
659
|
-
|
|
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");
|
|
660
793
|
}
|
|
661
794
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
662
795
|
const rows = allRows.slice(0, options.top);
|
|
663
|
-
|
|
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;
|
|
664
820
|
}
|
|
665
821
|
|
|
666
822
|
function shouldUseInteractive(options) {
|
|
667
823
|
return Boolean(
|
|
668
824
|
!options.static &&
|
|
825
|
+
options.view !== "trend" &&
|
|
669
826
|
!options.plain &&
|
|
827
|
+
!options.legacyPlot &&
|
|
670
828
|
!process.env.NO_COLOR &&
|
|
671
829
|
process.stdin.isTTY &&
|
|
672
830
|
process.stdout.isTTY,
|
|
@@ -674,12 +832,20 @@ function shouldUseInteractive(options) {
|
|
|
674
832
|
}
|
|
675
833
|
|
|
676
834
|
async function runInteractive(options) {
|
|
677
|
-
|
|
835
|
+
const bounds = options.range === "week"
|
|
836
|
+
? weekBounds(options.date, options.timeZone)
|
|
837
|
+
: dayBounds(options.date, options.timeZone);
|
|
678
838
|
const snapshot = await loadSnapshot(options);
|
|
679
|
-
const bounds = boundsForOptions(options, snapshot);
|
|
680
839
|
const events = filterDayEvents(snapshot, bounds);
|
|
681
840
|
if (events.length === 0) {
|
|
682
|
-
|
|
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"));
|
|
683
849
|
return;
|
|
684
850
|
}
|
|
685
851
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
@@ -701,19 +867,13 @@ async function main() {
|
|
|
701
867
|
process.stdout.write(`${usage()}\n`);
|
|
702
868
|
return;
|
|
703
869
|
}
|
|
704
|
-
if (options.version) {
|
|
705
|
-
process.stdout.write(`${VERSION}\n`);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
870
|
if (shouldUseInteractive(options)) {
|
|
709
871
|
await runInteractive(options);
|
|
710
872
|
} else {
|
|
711
|
-
process.stdout.write(`${await run(
|
|
873
|
+
process.stdout.write(`${await run(options)}\n`);
|
|
712
874
|
}
|
|
713
875
|
} catch (error) {
|
|
714
|
-
process.stderr.write(
|
|
715
|
-
`Token Ledger CLI failed: ${sanitizeTerminalText(error.message)}\n\n${usage()}\n`,
|
|
716
|
-
);
|
|
876
|
+
process.stderr.write(`Token Ledger CLI failed: ${error.message}\n\n${usage()}\n`);
|
|
717
877
|
process.exitCode = 1;
|
|
718
878
|
}
|
|
719
879
|
}
|