tledger 0.2.0 → 0.3.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 +152 -116
- package/bin/token-ledger-cache-image.mjs +1150 -0
- package/bin/token-ledger-controls.mjs +24 -0
- package/bin/token-ledger-rates.mjs +4 -1
- package/bin/token-ledger-terminal.mjs +143 -39
- package/bin/token-ledger-trend-image.mjs +1527 -584
- package/bin/token-ledger-trend-terminal.mjs +73 -28
- package/bin/token-ledger-trend.mjs +25 -11
- package/bin/token-ledger-tui.mjs +20 -14
- package/bin/token-ledger.mjs +536 -137
- package/docs/token-ledger-cli-week.png +0 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-importer.mjs +589 -279
- package/lib/token-ledger-snapshot.mjs +267 -0
- package/lib/token-ledger-usage.mjs +524 -0
- package/package.json +11 -5
package/bin/token-ledger.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import {
|
|
6
6
|
mkdir,
|
|
7
|
-
readFile,
|
|
8
7
|
stat,
|
|
9
8
|
} from "node:fs/promises";
|
|
10
9
|
import { homedir } from "node:os";
|
|
@@ -20,17 +19,32 @@ import {
|
|
|
20
19
|
renderTrendImage,
|
|
21
20
|
writeTrendPng,
|
|
22
21
|
} from "./token-ledger-trend-image.mjs";
|
|
22
|
+
import { renderCacheReportImage } from "./token-ledger-cache-image.mjs";
|
|
23
23
|
import { renderTrendCombo } from "./token-ledger-trend-terminal.mjs";
|
|
24
24
|
import { startInteractive } from "./token-ledger-tui.mjs";
|
|
25
|
+
import {
|
|
26
|
+
readPrivateSnapshot,
|
|
27
|
+
writePrivateSnapshot,
|
|
28
|
+
} from "../lib/token-ledger-snapshot.mjs";
|
|
29
|
+
import {
|
|
30
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
31
|
+
usageBuckets,
|
|
32
|
+
usageBucketsInRange,
|
|
33
|
+
usageCallCount,
|
|
34
|
+
usageThreadIds,
|
|
35
|
+
} from "../lib/token-ledger-usage.mjs";
|
|
25
36
|
|
|
26
37
|
export const DEFAULT_SNAPSHOT = resolve(
|
|
27
38
|
homedir(),
|
|
28
39
|
".token-ledger",
|
|
29
|
-
"token-ledger-snapshot.json",
|
|
40
|
+
"token-ledger-snapshot-v2.json.gz",
|
|
30
41
|
);
|
|
31
42
|
const DEFAULT_TOP = 10;
|
|
32
43
|
const DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
33
44
|
export const SNAPSHOT_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
45
|
+
export const ROLLING_24_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
46
|
+
const MAX_ROLLING_DAYS = 3_650;
|
|
47
|
+
const DURATION_ALIAS = /^(\d+)(d|w)$/i;
|
|
34
48
|
const ANSI_RESET = "\u001b[0m";
|
|
35
49
|
const MODEL_COLORS = {
|
|
36
50
|
sol: TERMINAL_MODEL_COLORS.sol,
|
|
@@ -41,44 +55,111 @@ const MODEL_COLORS = {
|
|
|
41
55
|
other: TERMINAL_MODEL_COLORS.other,
|
|
42
56
|
};
|
|
43
57
|
|
|
44
|
-
function usage() {
|
|
45
|
-
return `Token Ledger
|
|
58
|
+
export function usage() {
|
|
59
|
+
return `Token Ledger
|
|
46
60
|
|
|
47
61
|
Usage:
|
|
48
|
-
tledger
|
|
49
|
-
tledger week
|
|
50
|
-
tledger
|
|
51
|
-
tledger report
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
--
|
|
57
|
-
--
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
62
|
+
tledger 1d Last 24 hours in the terminal
|
|
63
|
+
tledger week Last 7 calendar days in the terminal
|
|
64
|
+
tledger 30d Rolling 30 days in the terminal
|
|
65
|
+
tledger report 7d Write the 7-day PNG report
|
|
66
|
+
tledger report 7d --cache-rate Write the cache-only PNG report
|
|
67
|
+
|
|
68
|
+
Common options:
|
|
69
|
+
--static Print once instead of opening the dashboard
|
|
70
|
+
--refresh Rebuild the local usage cache
|
|
71
|
+
--image-output <file> Choose where to save a PNG
|
|
72
|
+
--no-open Do not open a generated PNG
|
|
73
|
+
-h, --help Show this quick guide
|
|
74
|
+
--help-all Show every command and option
|
|
75
|
+
|
|
76
|
+
Token Ledger reads local Codex data only. It does not upload your usage.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function advancedUsage() {
|
|
80
|
+
return `Token Ledger command reference
|
|
81
|
+
|
|
82
|
+
Terminal commands:
|
|
83
|
+
tledger 1d Rolling 24-hour project breakdown
|
|
84
|
+
tledger <N>d Rolling N-day project breakdown
|
|
85
|
+
tledger <N>w Rolling N-week project breakdown
|
|
86
|
+
tledger day <YYYY-MM-DD> One local calendar day
|
|
87
|
+
tledger week [end-day] Seven local calendar days
|
|
88
|
+
tledger trend [Nd|Nw] Multi-day terminal trend
|
|
89
|
+
|
|
90
|
+
Report commands:
|
|
91
|
+
tledger report [Nd|Nw] Write the usage dashboard PNG
|
|
92
|
+
tledger report [Nd|Nw] --cache-rate
|
|
93
|
+
Write the cache-only PNG
|
|
94
|
+
|
|
95
|
+
Dates and ranges:
|
|
96
|
+
--date <day> YYYY-MM-DD, today, or yesterday
|
|
97
|
+
--period <window> Trend window such as 7d, 14d, or 2w
|
|
98
|
+
--tz <name> IANA timezone (default: machine timezone)
|
|
99
|
+
|
|
100
|
+
Data and refresh:
|
|
101
|
+
--input <file> Read an explicit snapshot
|
|
102
|
+
--refresh Rebuild the default snapshot from local Codex data
|
|
103
|
+
--no-refresh Use the cached snapshot without checking source files
|
|
104
|
+
--codex-home <dir> Codex data root used when refreshing
|
|
105
|
+
--no-archived Skip archived sessions when refreshing
|
|
106
|
+
|
|
107
|
+
Terminal output:
|
|
108
|
+
--top <number> Projects to show, from 1 to 100 (default: 10)
|
|
109
|
+
--width <number> Layout width, from 40 to 200 columns
|
|
110
|
+
--raw-projects Keep singleton thread labels ungrouped
|
|
111
|
+
--plain Disable terminal colors
|
|
112
|
+
--ascii Use ASCII bars instead of Unicode blocks
|
|
113
|
+
--static Print once instead of opening the dashboard
|
|
114
|
+
--youplot Use the legacy single-series renderer
|
|
115
|
+
|
|
116
|
+
Report output:
|
|
117
|
+
--drain Chart estimated meter drain instead of token volume
|
|
118
|
+
--cache-rate Write the cache-only report (report command only)
|
|
119
|
+
--image Write the trend view as a PNG
|
|
120
|
+
--image-output <file> Choose the PNG output path
|
|
121
|
+
--image-width <px> Set PNG width from 900 to 2400 pixels
|
|
122
|
+
--no-open Do not open the finished PNG
|
|
123
|
+
|
|
124
|
+
Help:
|
|
125
|
+
-h, --help Show the quick guide
|
|
126
|
+
--help-all Show this complete reference
|
|
127
|
+
|
|
128
|
+
The default snapshot is ~/.token-ledger/token-ledger-snapshot-v2.json.gz.
|
|
129
|
+
Token Ledger reads local Codex data only. It does not upload your usage.`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function durationAlias(value) {
|
|
133
|
+
const match = DURATION_ALIAS.exec(String(value ?? ""));
|
|
134
|
+
if (!match) return null;
|
|
135
|
+
const amount = Number(match[1]);
|
|
136
|
+
const unit = match[2].toLowerCase();
|
|
137
|
+
const days = unit === "w" ? amount * 7 : amount;
|
|
138
|
+
if (
|
|
139
|
+
!Number.isSafeInteger(amount) ||
|
|
140
|
+
!Number.isSafeInteger(days) ||
|
|
141
|
+
amount < 1 ||
|
|
142
|
+
days > MAX_ROLLING_DAYS
|
|
143
|
+
) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Duration must be between 1d and ${MAX_ROLLING_DAYS}d (or the equivalent in weeks).`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const noun = unit === "w"
|
|
149
|
+
? amount === 1 ? "week" : "weeks"
|
|
150
|
+
: amount === 1 ? "day" : "days";
|
|
151
|
+
return {
|
|
152
|
+
amount,
|
|
153
|
+
unit,
|
|
154
|
+
days,
|
|
155
|
+
label: `${amount} ${noun}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function rollingRangeDescription(options) {
|
|
160
|
+
return options.range === "rolling24h"
|
|
161
|
+
? "the last 24 hours"
|
|
162
|
+
: `the last ${options.rollingLabel}`;
|
|
82
163
|
}
|
|
83
164
|
|
|
84
165
|
function readOption(argv, index, name) {
|
|
@@ -90,14 +171,28 @@ function readOption(argv, index, name) {
|
|
|
90
171
|
}
|
|
91
172
|
|
|
92
173
|
export function parseArgs(argv) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
174
|
+
const helpCommand = argv[0] === "help";
|
|
175
|
+
const alias = durationAlias(argv[0]);
|
|
176
|
+
const rolling24hCommand = argv[0] === "1d";
|
|
177
|
+
const rollingDurationCommand = Boolean(alias) && !rolling24hCommand;
|
|
178
|
+
const command = rolling24hCommand
|
|
179
|
+
? "rolling24h"
|
|
180
|
+
: rollingDurationCommand
|
|
181
|
+
? "rolling"
|
|
182
|
+
: argv[0] === "week"
|
|
183
|
+
? "week"
|
|
184
|
+
: argv[0] === "trend" || argv[0] === "report"
|
|
185
|
+
? "trend"
|
|
186
|
+
: "day";
|
|
98
187
|
const options = {
|
|
99
188
|
range: command,
|
|
100
189
|
view: command === "trend" ? "trend" : "projects",
|
|
190
|
+
rolling24h: rolling24hCommand,
|
|
191
|
+
rollingDuration: rollingDurationCommand,
|
|
192
|
+
rollingDays: alias?.days ?? (rolling24hCommand ? 1 : null),
|
|
193
|
+
rollingAmount: alias?.amount ?? (rolling24hCommand ? 1 : null),
|
|
194
|
+
rollingUnit: alias?.unit ?? (rolling24hCommand ? "d" : null),
|
|
195
|
+
rollingLabel: alias?.label ?? "1 day",
|
|
101
196
|
report: argv[0] === "report",
|
|
102
197
|
trendDays: 7,
|
|
103
198
|
date: null,
|
|
@@ -117,17 +212,23 @@ export function parseArgs(argv) {
|
|
|
117
212
|
image: false,
|
|
118
213
|
imageOutput: null,
|
|
119
214
|
imageWidth: null,
|
|
215
|
+
openImage: true,
|
|
120
216
|
drain: false,
|
|
217
|
+
cacheRate: false,
|
|
121
218
|
legacyPlot: false,
|
|
122
|
-
help:
|
|
219
|
+
help: argv.length === 0 || helpCommand,
|
|
220
|
+
helpAll: false,
|
|
123
221
|
};
|
|
124
222
|
|
|
125
223
|
let trendPeriodSeen = false;
|
|
126
|
-
let index = ["day", "week", "trend", "report"].includes(argv[0]) ? 1 : 0;
|
|
224
|
+
let index = alias || ["day", "week", "trend", "report", "help"].includes(argv[0]) ? 1 : 0;
|
|
127
225
|
for (; index < argv.length; index += 1) {
|
|
128
226
|
const argument = argv[index];
|
|
129
227
|
if (argument === "--help" || argument === "-h") {
|
|
130
228
|
options.help = true;
|
|
229
|
+
} else if (argument === "--help-all") {
|
|
230
|
+
options.help = true;
|
|
231
|
+
options.helpAll = true;
|
|
131
232
|
} else if (argument === "--date") {
|
|
132
233
|
options.date = readOption(argv, index, "--date");
|
|
133
234
|
index += 1;
|
|
@@ -139,10 +240,13 @@ export function parseArgs(argv) {
|
|
|
139
240
|
throw new Error("Trend period can only be specified once.");
|
|
140
241
|
}
|
|
141
242
|
const value = readOption(argv, index, "--period");
|
|
142
|
-
|
|
143
|
-
|
|
243
|
+
const period = durationAlias(value);
|
|
244
|
+
if (!period) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"Trend period must use a positive number of days or weeks, such as 7d or 2w.",
|
|
247
|
+
);
|
|
144
248
|
}
|
|
145
|
-
options.trendDays =
|
|
249
|
+
options.trendDays = period.days;
|
|
146
250
|
trendPeriodSeen = true;
|
|
147
251
|
index += 1;
|
|
148
252
|
} else if (argument === "--input") {
|
|
@@ -188,11 +292,21 @@ export function parseArgs(argv) {
|
|
|
188
292
|
throw new Error("--drain is only available for the trend view.");
|
|
189
293
|
}
|
|
190
294
|
options.drain = true;
|
|
295
|
+
} else if (argument === "--cache-rate") {
|
|
296
|
+
if (!options.report) {
|
|
297
|
+
throw new Error("--cache-rate is only available with the report command.");
|
|
298
|
+
}
|
|
299
|
+
options.cacheRate = true;
|
|
191
300
|
} else if (argument === "--image") {
|
|
192
301
|
if (options.view !== "trend") {
|
|
193
302
|
throw new Error("--image is only available for the trend view.");
|
|
194
303
|
}
|
|
195
304
|
options.image = true;
|
|
305
|
+
} else if (argument === "--no-open") {
|
|
306
|
+
if (options.view !== "trend") {
|
|
307
|
+
throw new Error("--no-open is only available for the trend view.");
|
|
308
|
+
}
|
|
309
|
+
options.openImage = false;
|
|
196
310
|
} else if (argument === "--image-output") {
|
|
197
311
|
if (options.view !== "trend") {
|
|
198
312
|
throw new Error("--image-output is only available for the trend view.");
|
|
@@ -217,14 +331,30 @@ export function parseArgs(argv) {
|
|
|
217
331
|
index += 1;
|
|
218
332
|
} else if (argument === "--youplot") {
|
|
219
333
|
options.legacyPlot = true;
|
|
334
|
+
} else if (
|
|
335
|
+
!argument.startsWith("-") &&
|
|
336
|
+
options.view === "projects" &&
|
|
337
|
+
argument === "1d" &&
|
|
338
|
+
!options.date
|
|
339
|
+
) {
|
|
340
|
+
if (argv[0] !== "day") {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"The 1d alias is only available as `tledger 1d` or `tledger day 1d`.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
options.range = "rolling24h";
|
|
346
|
+
options.rolling24h = true;
|
|
220
347
|
} else if (!argument.startsWith("-") && options.view === "trend") {
|
|
221
348
|
if (trendPeriodSeen) {
|
|
222
349
|
throw new Error("Trend period can only be specified once.");
|
|
223
350
|
}
|
|
224
|
-
|
|
225
|
-
|
|
351
|
+
const period = durationAlias(argument);
|
|
352
|
+
if (!period) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
"Trend period must use a positive number of days or weeks, such as 7d or 2w.",
|
|
355
|
+
);
|
|
226
356
|
}
|
|
227
|
-
options.trendDays =
|
|
357
|
+
options.trendDays = period.days;
|
|
228
358
|
trendPeriodSeen = true;
|
|
229
359
|
} else if (!argument.startsWith("-") && !options.date) {
|
|
230
360
|
options.date = argument;
|
|
@@ -234,10 +364,13 @@ export function parseArgs(argv) {
|
|
|
234
364
|
}
|
|
235
365
|
|
|
236
366
|
if (options.report) options.image = true;
|
|
367
|
+
if (!options.help && (options.rolling24h || options.rollingDuration) && options.date) {
|
|
368
|
+
throw new Error(`${options.rollingLabel} does not accept --date; its rolling window ends now.`);
|
|
369
|
+
}
|
|
237
370
|
if (!options.help && !options.date && (options.range === "week" || options.view === "trend")) {
|
|
238
371
|
options.date = "today";
|
|
239
372
|
}
|
|
240
|
-
if (!options.help && !options.date) {
|
|
373
|
+
if (!options.help && !options.date && !options.rolling24h && !options.rollingDuration) {
|
|
241
374
|
throw new Error("A day is required, for example: tledger day 2026-08-01");
|
|
242
375
|
}
|
|
243
376
|
if (!options.help && options.refresh && !options.autoRefresh) {
|
|
@@ -249,6 +382,9 @@ export function parseArgs(argv) {
|
|
|
249
382
|
if (!options.help && options.view === "trend" && options.legacyPlot) {
|
|
250
383
|
throw new Error("--youplot is only available for the project view.");
|
|
251
384
|
}
|
|
385
|
+
if (!options.help && options.cacheRate && options.drain) {
|
|
386
|
+
throw new Error("--cache-rate cannot be combined with --drain.");
|
|
387
|
+
}
|
|
252
388
|
return options;
|
|
253
389
|
}
|
|
254
390
|
|
|
@@ -364,6 +500,35 @@ export function weekBounds(value, timeZone) {
|
|
|
364
500
|
};
|
|
365
501
|
}
|
|
366
502
|
|
|
503
|
+
export function rollingDurationBounds(
|
|
504
|
+
value = new Date(),
|
|
505
|
+
timeZone = DEFAULT_TIME_ZONE,
|
|
506
|
+
rangeDays = 1,
|
|
507
|
+
) {
|
|
508
|
+
validateTimeZone(timeZone);
|
|
509
|
+
const end = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
510
|
+
if (!Number.isFinite(end.getTime())) {
|
|
511
|
+
throw new Error("Rolling window requires a valid end time.");
|
|
512
|
+
}
|
|
513
|
+
const days = Number(rangeDays);
|
|
514
|
+
if (!Number.isSafeInteger(days) || days < 1 || days > MAX_ROLLING_DAYS) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Rolling window must be between 1 and ${MAX_ROLLING_DAYS} days.`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
start: new Date(end.getTime() - days * ROLLING_24_HOURS_MS),
|
|
521
|
+
end,
|
|
522
|
+
timeZone,
|
|
523
|
+
rangeHours: days * 24,
|
|
524
|
+
rangeDays: days,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function rolling24hBounds(value = new Date(), timeZone = DEFAULT_TIME_ZONE) {
|
|
529
|
+
return rollingDurationBounds(value, timeZone, 1);
|
|
530
|
+
}
|
|
531
|
+
|
|
367
532
|
export function sanitizeTerminalText(value) {
|
|
368
533
|
return String(value ?? "")
|
|
369
534
|
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
@@ -378,6 +543,62 @@ function cleanLabel(value, fallback) {
|
|
|
378
543
|
return label || fallback;
|
|
379
544
|
}
|
|
380
545
|
|
|
546
|
+
const QUOTED_ABSOLUTE_PATH =
|
|
547
|
+
/(["'])(\/(?!\/)[^"'\r\n]*|(?:\/\/|\\\\)[^"'\r\n]+|[A-Za-z]:[\\/][^"'\r\n]*)\1/g;
|
|
548
|
+
const UNQUOTED_ABSOLUTE_PATH =
|
|
549
|
+
/(^|[\s([{=])((?:\/(?!\/)|\/\/|\\\\|[A-Za-z]:[\\/])[^\s"'`)\]},;]+)/g;
|
|
550
|
+
|
|
551
|
+
function isAbsoluteLocalPath(path) {
|
|
552
|
+
return (
|
|
553
|
+
path.startsWith("/") ||
|
|
554
|
+
path.startsWith("\\\\") ||
|
|
555
|
+
/^[A-Za-z]:[\\/]/.test(path)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function safeDisplayLabel(value, fallback = "local path") {
|
|
560
|
+
const normalized = String(value ?? "").replaceAll("\\", "/");
|
|
561
|
+
const label = sanitizeTerminalText(basename(normalized))
|
|
562
|
+
.replace(/\s+/g, " ")
|
|
563
|
+
.trim();
|
|
564
|
+
return label && label !== "." && label !== ".." ? label : fallback;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function redactLocalPaths(value, paths = []) {
|
|
568
|
+
let redacted = String(value ?? "");
|
|
569
|
+
const explicitPaths = new Set(
|
|
570
|
+
paths
|
|
571
|
+
.filter(Boolean)
|
|
572
|
+
.map((path) => String(path))
|
|
573
|
+
.filter(isAbsoluteLocalPath),
|
|
574
|
+
);
|
|
575
|
+
const pathsToRedact = [...new Set([
|
|
576
|
+
...explicitPaths,
|
|
577
|
+
homedir(),
|
|
578
|
+
process.cwd(),
|
|
579
|
+
])]
|
|
580
|
+
.filter((path) => path && path !== "/")
|
|
581
|
+
.sort((left, right) => right.length - left.length);
|
|
582
|
+
|
|
583
|
+
for (const path of pathsToRedact) {
|
|
584
|
+
redacted = redacted.replaceAll(
|
|
585
|
+
path,
|
|
586
|
+
explicitPaths.has(path) ? safeDisplayLabel(path) : "[local path]",
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return redacted
|
|
591
|
+
.replace(QUOTED_ABSOLUTE_PATH, (_match, quote) => `${quote}[local path]${quote}`)
|
|
592
|
+
.replace(UNQUOTED_ABSOLUTE_PATH, (_match, prefix) => `${prefix}[local path]`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function safeErrorMessage(error, paths = []) {
|
|
596
|
+
return redactLocalPaths(
|
|
597
|
+
error instanceof Error ? error.message : String(error),
|
|
598
|
+
paths,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
381
602
|
function displayLabel(value) {
|
|
382
603
|
const label = cleanLabel(value, "Unlabelled activity");
|
|
383
604
|
if (label.length <= 30) return label;
|
|
@@ -393,7 +614,9 @@ export function oneOffProjects(snapshot) {
|
|
|
393
614
|
ids.add(threadId);
|
|
394
615
|
threadIdsByProject.set(normalizedProject, ids);
|
|
395
616
|
};
|
|
396
|
-
for (const
|
|
617
|
+
for (const bucket of usageBuckets(snapshot)) {
|
|
618
|
+
for (const threadId of usageThreadIds(bucket)) add(bucket.project, threadId);
|
|
619
|
+
}
|
|
397
620
|
for (const thread of snapshot.threads ?? []) add(thread.project, thread.id);
|
|
398
621
|
return new Set(
|
|
399
622
|
[...threadIdsByProject.entries()]
|
|
@@ -416,10 +639,7 @@ function modelLabel(value) {
|
|
|
416
639
|
export function filterDayEvents(snapshot, bounds) {
|
|
417
640
|
const start = bounds.start.getTime();
|
|
418
641
|
const end = bounds.end.getTime();
|
|
419
|
-
return (snapshot
|
|
420
|
-
const timestamp = new Date(event.timestamp).getTime();
|
|
421
|
-
return Number.isFinite(timestamp) && timestamp >= start && timestamp < end;
|
|
422
|
-
});
|
|
642
|
+
return usageBucketsInRange(snapshot, start, end);
|
|
423
643
|
}
|
|
424
644
|
|
|
425
645
|
export function aggregateProjects(snapshot, events, options = {}) {
|
|
@@ -450,8 +670,8 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
450
670
|
row.outputTokens += Number(event.outputTokens) || 0;
|
|
451
671
|
row.reasoningTokens += Number(event.reasoningTokens) || 0;
|
|
452
672
|
row.toolCalls += Number(event.toolCalls) || 0;
|
|
453
|
-
row.events +=
|
|
454
|
-
|
|
673
|
+
row.events += usageCallCount(event);
|
|
674
|
+
for (const threadId of usageThreadIds(event)) row.threadIds.add(threadId);
|
|
455
675
|
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
456
676
|
row.rateCardCredits += Number(event.rateCardCredits);
|
|
457
677
|
row.knownCreditTokens += Number(event.totalTokens) || 0;
|
|
@@ -465,7 +685,7 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
465
685
|
rateCardCredits: 0,
|
|
466
686
|
};
|
|
467
687
|
modelRow.totalTokens += Number(event.totalTokens) || 0;
|
|
468
|
-
modelRow.events +=
|
|
688
|
+
modelRow.events += usageCallCount(event);
|
|
469
689
|
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
470
690
|
modelRow.rateCardCredits += Number(event.rateCardCredits);
|
|
471
691
|
}
|
|
@@ -495,7 +715,10 @@ function totalSummary(events) {
|
|
|
495
715
|
summary.totalTokens += Number(event.totalTokens) || 0;
|
|
496
716
|
summary.outputTokens += Number(event.outputTokens) || 0;
|
|
497
717
|
summary.toolCalls += Number(event.toolCalls) || 0;
|
|
498
|
-
|
|
718
|
+
summary.calls += usageCallCount(event);
|
|
719
|
+
for (const threadId of usageThreadIds(event)) {
|
|
720
|
+
summary.threadIds.add(threadId);
|
|
721
|
+
}
|
|
499
722
|
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
500
723
|
summary.rateCardCredits += Number(event.rateCardCredits);
|
|
501
724
|
summary.knownCreditTokens += Number(event.totalTokens) || 0;
|
|
@@ -506,6 +729,7 @@ function totalSummary(events) {
|
|
|
506
729
|
totalTokens: 0,
|
|
507
730
|
outputTokens: 0,
|
|
508
731
|
toolCalls: 0,
|
|
732
|
+
calls: 0,
|
|
509
733
|
rateCardCredits: 0,
|
|
510
734
|
knownCreditTokens: 0,
|
|
511
735
|
threadIds: new Set(),
|
|
@@ -521,12 +745,18 @@ function compact(value, digits = 2) {
|
|
|
521
745
|
[1_000_000, "M"],
|
|
522
746
|
[1_000, "K"],
|
|
523
747
|
];
|
|
524
|
-
for (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
748
|
+
for (let index = 0; index < units.length; index += 1) {
|
|
749
|
+
const [divisor, suffix] = units[index];
|
|
750
|
+
if (absolute < divisor) continue;
|
|
751
|
+
const scaled = value / divisor;
|
|
752
|
+
const magnitude = Math.abs(scaled);
|
|
753
|
+
const precision = magnitude >= 100 ? 0 : magnitude >= 10 ? 1 : digits;
|
|
754
|
+
// Values that round to 1000 of a unit belong to the next unit up
|
|
755
|
+
// (999,999 → 1.00M, not 1000K).
|
|
756
|
+
if (index > 0 && Number(magnitude.toFixed(precision)) >= 1_000) {
|
|
757
|
+
return compact(Math.sign(value) * divisor * 1_000, digits);
|
|
529
758
|
}
|
|
759
|
+
return `${scaled.toFixed(precision)}${suffix}`;
|
|
530
760
|
}
|
|
531
761
|
return Math.round(value).toLocaleString("en-US");
|
|
532
762
|
}
|
|
@@ -582,7 +812,7 @@ function sourceLabel(snapshotPath, snapshot) {
|
|
|
582
812
|
timeStyle: "short",
|
|
583
813
|
})
|
|
584
814
|
: "unknown time";
|
|
585
|
-
return `${
|
|
815
|
+
return `${safeDisplayLabel(snapshotPath, "snapshot")} · captured ${generated}`;
|
|
586
816
|
}
|
|
587
817
|
|
|
588
818
|
function runYouPlot(rows, options, dateLabel, unit) {
|
|
@@ -627,43 +857,78 @@ function runYouPlot(rows, options, dateLabel, unit) {
|
|
|
627
857
|
}
|
|
628
858
|
|
|
629
859
|
async function readSnapshot(snapshotPath) {
|
|
860
|
+
const snapshotLabel = safeDisplayLabel(snapshotPath, "snapshot");
|
|
630
861
|
let parsed;
|
|
631
862
|
try {
|
|
632
|
-
parsed =
|
|
863
|
+
parsed = await readPrivateSnapshot(snapshotPath);
|
|
633
864
|
} catch (error) {
|
|
634
865
|
if (error?.code === "ENOENT") {
|
|
635
|
-
throw new Error(`Snapshot not found: ${
|
|
866
|
+
throw new Error(`Snapshot not found: ${snapshotLabel}`);
|
|
636
867
|
}
|
|
637
|
-
throw new Error(
|
|
868
|
+
throw new Error(
|
|
869
|
+
`Could not read snapshot ${snapshotLabel}: ${safeErrorMessage(error, [snapshotPath])}`,
|
|
870
|
+
);
|
|
638
871
|
}
|
|
639
|
-
if (
|
|
640
|
-
|
|
872
|
+
if (
|
|
873
|
+
!parsed ||
|
|
874
|
+
parsed.schemaVersion !== SNAPSHOT_SCHEMA_VERSION ||
|
|
875
|
+
!Array.isArray(parsed.events)
|
|
876
|
+
) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`Snapshot uses an unsupported schema: ${snapshotLabel}. Rebuild it with --refresh.`,
|
|
879
|
+
);
|
|
641
880
|
}
|
|
642
881
|
return parsed;
|
|
643
882
|
}
|
|
644
883
|
|
|
645
884
|
async function refreshSnapshot(options) {
|
|
646
885
|
if (!existsSync(options.codexHome)) {
|
|
647
|
-
throw new Error(
|
|
886
|
+
throw new Error(
|
|
887
|
+
`Codex data directory not found: ${safeDisplayLabel(options.codexHome, "Codex data directory")}`,
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
let progressStarted = false;
|
|
891
|
+
try {
|
|
892
|
+
const { collectUsage } = await import(
|
|
893
|
+
"../lib/token-ledger-importer.mjs"
|
|
894
|
+
);
|
|
895
|
+
process.stderr.write("Token Ledger: refreshing local snapshot…\n");
|
|
896
|
+
progressStarted = true;
|
|
897
|
+
const snapshot = await collectUsage(
|
|
898
|
+
{
|
|
899
|
+
output: options.input,
|
|
900
|
+
codexHome: options.codexHome,
|
|
901
|
+
includeArchived: options.includeArchived,
|
|
902
|
+
since: null,
|
|
903
|
+
},
|
|
904
|
+
({ current, total }) => {
|
|
905
|
+
process.stderr.write(`\rToken Ledger: scanned ${current}/${total} rollout files`);
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
process.stderr.write("\n");
|
|
909
|
+
const writeResult = await writePrivateSnapshot(options.input, snapshot);
|
|
910
|
+
const storedSnapshot = writeResult.snapshot;
|
|
911
|
+
process.stderr.write(
|
|
912
|
+
`Token Ledger: cached ${(writeResult.bytesWritten / 1_000_000).toFixed(1)} MB ${writeResult.encoding} snapshot (${(writeResult.jsonBytes / 1_000_000).toFixed(1)} MB JSON before encoding; ${storedSnapshot.events.length.toLocaleString()} buckets for ${storedSnapshot.coverage.observedModelCalls.toLocaleString()} calls; ${(writeResult.maxBytes / 1_000_000).toFixed(1)} MB limit).\n`,
|
|
913
|
+
);
|
|
914
|
+
if (writeResult.bytesWritten / writeResult.maxBytes >= 0.7) {
|
|
915
|
+
process.stderr.write(
|
|
916
|
+
"Token Ledger: snapshot is above 70% of its safety limit; older buckets will compact automatically as it grows.\n",
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
return storedSnapshot;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (progressStarted) process.stderr.write("\n");
|
|
922
|
+
if (error?.code === "ERR_SNAPSHOT_SIZE_LIMIT" && existsSync(options.input)) {
|
|
923
|
+
process.stderr.write(
|
|
924
|
+
"Token Ledger: refresh exceeded the safety limit; continuing with the previous cache, which may be stale.\n",
|
|
925
|
+
);
|
|
926
|
+
return readSnapshot(options.input);
|
|
927
|
+
}
|
|
928
|
+
throw new Error(
|
|
929
|
+
`Could not refresh local snapshot: ${safeErrorMessage(error, [options.input, options.codexHome])}`,
|
|
930
|
+
);
|
|
648
931
|
}
|
|
649
|
-
const { collectUsage, writePrivateSnapshot } = await import(
|
|
650
|
-
"../lib/token-ledger-importer.mjs"
|
|
651
|
-
);
|
|
652
|
-
process.stderr.write("Token Ledger: refreshing local snapshot…\n");
|
|
653
|
-
const snapshot = await collectUsage(
|
|
654
|
-
{
|
|
655
|
-
output: options.input,
|
|
656
|
-
codexHome: options.codexHome,
|
|
657
|
-
includeArchived: options.includeArchived,
|
|
658
|
-
since: null,
|
|
659
|
-
},
|
|
660
|
-
({ current, total }) => {
|
|
661
|
-
process.stderr.write(`\rToken Ledger: scanned ${current}/${total} rollout files`);
|
|
662
|
-
},
|
|
663
|
-
);
|
|
664
|
-
process.stderr.write("\n");
|
|
665
|
-
await writePrivateSnapshot(options.input, snapshot);
|
|
666
|
-
return snapshot;
|
|
667
932
|
}
|
|
668
933
|
|
|
669
934
|
export function snapshotNeedsRefresh(snapshotMtimeMs, latestJsonlMtimeMs) {
|
|
@@ -682,13 +947,49 @@ export function snapshotCacheIsFresh(
|
|
|
682
947
|
);
|
|
683
948
|
}
|
|
684
949
|
|
|
950
|
+
function snapshotAgeLabel(ageMs) {
|
|
951
|
+
if (ageMs < 60 * 1_000) return "now";
|
|
952
|
+
const minutes = Math.floor(ageMs / (60 * 1_000));
|
|
953
|
+
if (minutes < 60) return `${minutes}m old`;
|
|
954
|
+
const hours = Math.floor(minutes / 60);
|
|
955
|
+
if (hours < 24) return `${hours}h old`;
|
|
956
|
+
return `${Math.floor(hours / 24)}d old`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function primitiveString(value) {
|
|
960
|
+
try {
|
|
961
|
+
const text = String.prototype.valueOf.call(value);
|
|
962
|
+
return text === value ? text : null;
|
|
963
|
+
} catch {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
export function snapshotFreshness(snapshot = {}, nowMs = Date.now()) {
|
|
969
|
+
const generatedAt = primitiveString(snapshot.generatedAt);
|
|
970
|
+
const generatedAtMs = generatedAt === null ? NaN : Date.parse(generatedAt);
|
|
971
|
+
if (
|
|
972
|
+
!Number.isFinite(generatedAtMs) ||
|
|
973
|
+
!Number.isFinite(nowMs) ||
|
|
974
|
+
generatedAtMs > nowMs
|
|
975
|
+
) {
|
|
976
|
+
return { status: "unknown", ageLabel: "age unknown" };
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
status: snapshotCacheIsFresh(generatedAtMs, nowMs) ? "fresh" : "stale",
|
|
980
|
+
ageLabel: snapshotAgeLabel(nowMs - generatedAtMs),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
685
984
|
async function loadSnapshot(options) {
|
|
686
985
|
if (options.refresh) {
|
|
687
986
|
return refreshSnapshot(options);
|
|
688
987
|
}
|
|
689
988
|
if (!existsSync(options.input)) {
|
|
690
989
|
if (options.inputExplicit || !options.autoRefresh) {
|
|
691
|
-
throw new Error(
|
|
990
|
+
throw new Error(
|
|
991
|
+
`Snapshot not found: ${safeDisplayLabel(options.input, "snapshot")}`,
|
|
992
|
+
);
|
|
692
993
|
}
|
|
693
994
|
return refreshSnapshot(options);
|
|
694
995
|
}
|
|
@@ -696,24 +997,46 @@ async function loadSnapshot(options) {
|
|
|
696
997
|
return readSnapshot(options.input);
|
|
697
998
|
}
|
|
698
999
|
|
|
699
|
-
|
|
1000
|
+
let snapshotStat;
|
|
1001
|
+
try {
|
|
1002
|
+
snapshotStat = await stat(options.input);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`Could not inspect snapshot ${safeDisplayLabel(options.input, "snapshot")}: ${safeErrorMessage(error, [options.input])}`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
700
1008
|
if (snapshotCacheIsFresh(snapshotStat.mtimeMs)) {
|
|
701
1009
|
return readSnapshot(options.input);
|
|
702
1010
|
}
|
|
703
1011
|
|
|
704
1012
|
const { latestSourceModifiedAt } = await import("../lib/token-ledger-importer.mjs");
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1013
|
+
let latestSourceMtimeMs;
|
|
1014
|
+
try {
|
|
1015
|
+
latestSourceMtimeMs = await latestSourceModifiedAt(
|
|
1016
|
+
options.codexHome,
|
|
1017
|
+
options.includeArchived,
|
|
1018
|
+
);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`Could not inspect local Codex source: ${safeErrorMessage(error, [options.codexHome])}`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
709
1024
|
if (snapshotNeedsRefresh(snapshotStat.mtimeMs, latestSourceMtimeMs)) {
|
|
710
1025
|
return refreshSnapshot(options);
|
|
711
1026
|
}
|
|
712
1027
|
return readSnapshot(options.input);
|
|
713
1028
|
}
|
|
714
1029
|
|
|
715
|
-
function render(options, snapshot, bounds, events, rows, allRows) {
|
|
1030
|
+
function render(options, snapshot, bounds, events, rows, allRows, freshness) {
|
|
716
1031
|
if (options.view === "trend") {
|
|
1032
|
+
if (options.image && options.cacheRate) {
|
|
1033
|
+
return renderCacheReportImage({
|
|
1034
|
+
snapshot,
|
|
1035
|
+
bounds,
|
|
1036
|
+
days: options.trendDays,
|
|
1037
|
+
options,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
717
1040
|
const trend = buildUsageTrend(snapshot, bounds);
|
|
718
1041
|
if (options.image) {
|
|
719
1042
|
return renderTrendImage({
|
|
@@ -722,6 +1045,7 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
722
1045
|
trend,
|
|
723
1046
|
days: options.trendDays,
|
|
724
1047
|
options,
|
|
1048
|
+
projectRows: allRows,
|
|
725
1049
|
});
|
|
726
1050
|
}
|
|
727
1051
|
return renderTrendCombo({
|
|
@@ -733,20 +1057,32 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
733
1057
|
});
|
|
734
1058
|
}
|
|
735
1059
|
if (!options.legacyPlot) {
|
|
736
|
-
return renderTerminal({
|
|
1060
|
+
return renderTerminal({
|
|
1061
|
+
options,
|
|
1062
|
+
snapshot,
|
|
1063
|
+
snapshotFreshness: freshness,
|
|
1064
|
+
bounds,
|
|
1065
|
+
events,
|
|
1066
|
+
rows,
|
|
1067
|
+
allRows,
|
|
1068
|
+
});
|
|
737
1069
|
}
|
|
738
1070
|
const enabled = !options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY);
|
|
739
1071
|
const summary = totalSummary(events);
|
|
740
1072
|
const totalTokens = summary.totalTokens;
|
|
741
|
-
const dateLabel = options.range === "
|
|
742
|
-
?
|
|
743
|
-
:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1073
|
+
const dateLabel = options.range === "rolling24h"
|
|
1074
|
+
? "last 24 hours"
|
|
1075
|
+
: options.range === "rolling"
|
|
1076
|
+
? `last ${options.rollingLabel}`
|
|
1077
|
+
: options.range === "week"
|
|
1078
|
+
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
1079
|
+
: new Intl.DateTimeFormat("en-US", {
|
|
1080
|
+
timeZone: bounds.timeZone,
|
|
1081
|
+
weekday: "short",
|
|
1082
|
+
month: "short",
|
|
1083
|
+
day: "numeric",
|
|
1084
|
+
year: "numeric",
|
|
1085
|
+
}).format(bounds.start);
|
|
750
1086
|
const unit = chartUnit(rows[0]?.totalTokens ?? 0);
|
|
751
1087
|
const shares = rows.map((row) =>
|
|
752
1088
|
totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0,
|
|
@@ -755,7 +1091,7 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
755
1091
|
|
|
756
1092
|
const header = [
|
|
757
1093
|
`Token Ledger · ${dateLabel} · ${bounds.timeZone}`,
|
|
758
|
-
`${compact(totalTokens)} tokens · ${summary.threadIds.size.toLocaleString()} threads · ${
|
|
1094
|
+
`${compact(totalTokens)} tokens · ${summary.threadIds.size.toLocaleString()} threads · ${summary.calls.toLocaleString()} calls · ${compact(summary.outputTokens)} output`,
|
|
759
1095
|
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
760
1096
|
"",
|
|
761
1097
|
chart,
|
|
@@ -774,51 +1110,112 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
774
1110
|
return `${header.join("\n")}\n\n${details.join("\n")}`;
|
|
775
1111
|
}
|
|
776
1112
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1113
|
+
function boundsForOptions(options, now = new Date()) {
|
|
1114
|
+
if (options.view === "trend") {
|
|
1115
|
+
return multiDayBounds(options.date, options.timeZone, options.trendDays);
|
|
1116
|
+
}
|
|
1117
|
+
if (options.range === "week") {
|
|
1118
|
+
return weekBounds(options.date, options.timeZone);
|
|
1119
|
+
}
|
|
1120
|
+
if (options.range === "rolling24h") {
|
|
1121
|
+
return rolling24hBounds(now, options.timeZone);
|
|
1122
|
+
}
|
|
1123
|
+
if (options.range === "rolling") {
|
|
1124
|
+
return rollingDurationBounds(now, options.timeZone, options.rollingDays);
|
|
1125
|
+
}
|
|
1126
|
+
return dayBounds(options.date, options.timeZone);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function rangeDescription(options, bounds) {
|
|
1130
|
+
if (options.range === "rolling24h" || options.range === "rolling") {
|
|
1131
|
+
return rollingRangeDescription(options);
|
|
1132
|
+
}
|
|
1133
|
+
if (bounds.startDateString && bounds.endDateString) {
|
|
1134
|
+
return `${bounds.startDateString} through ${bounds.endDateString}`;
|
|
1135
|
+
}
|
|
1136
|
+
return bounds.dateString;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export async function run(options, { nowMs } = {}) {
|
|
1140
|
+
const hasInjectedNow = nowMs !== undefined;
|
|
1141
|
+
const now = new Date(hasInjectedNow ? nowMs : Date.now());
|
|
1142
|
+
const bounds = boundsForOptions(options, now);
|
|
783
1143
|
const snapshot = await loadSnapshot(options);
|
|
784
1144
|
const events = filterDayEvents(snapshot, bounds);
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
: bounds.dateString;
|
|
1145
|
+
const writingImage = options.view === "trend" && options.image;
|
|
1146
|
+
const writingEmptyCacheReport = writingImage && options.cacheRate;
|
|
1147
|
+
if (events.length === 0 && !writingEmptyCacheReport) {
|
|
789
1148
|
return [
|
|
790
|
-
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
1149
|
+
`No model-call events found for ${rangeDescription(options, bounds)} (${bounds.timeZone}).`,
|
|
791
1150
|
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
792
1151
|
].join("\n");
|
|
793
1152
|
}
|
|
794
|
-
const allRows =
|
|
1153
|
+
const allRows = options.cacheRate
|
|
1154
|
+
? []
|
|
1155
|
+
: aggregateProjects(snapshot, events, options);
|
|
795
1156
|
const rows = allRows.slice(0, options.top);
|
|
796
|
-
const writingImage = options.view === "trend" && options.image;
|
|
797
1157
|
const outputPath = writingImage
|
|
798
1158
|
? options.imageOutput ??
|
|
799
1159
|
resolve(
|
|
800
1160
|
process.cwd(),
|
|
801
|
-
`token-ledger-${options.report ? "report" : "trend"}-${options.trendDays}d.png`,
|
|
1161
|
+
`token-ledger-${options.cacheRate ? "cache-report" : options.report ? "report" : "trend"}-${options.trendDays}d.png`,
|
|
802
1162
|
)
|
|
803
1163
|
: null;
|
|
804
|
-
const imageLabel = options.
|
|
1164
|
+
const imageLabel = options.cacheRate
|
|
1165
|
+
? "cache report"
|
|
1166
|
+
: options.report
|
|
1167
|
+
? "report"
|
|
1168
|
+
: "trend image";
|
|
805
1169
|
if (writingImage) {
|
|
806
1170
|
process.stderr.write(`Token Ledger: generating ${imageLabel} PNG…\n`);
|
|
807
1171
|
}
|
|
808
|
-
const output = render(
|
|
1172
|
+
const output = render(
|
|
1173
|
+
options,
|
|
1174
|
+
snapshot,
|
|
1175
|
+
bounds,
|
|
1176
|
+
events,
|
|
1177
|
+
rows,
|
|
1178
|
+
allRows,
|
|
1179
|
+
snapshotFreshness(
|
|
1180
|
+
snapshot,
|
|
1181
|
+
hasInjectedNow ? now.getTime() : Date.now(),
|
|
1182
|
+
),
|
|
1183
|
+
);
|
|
809
1184
|
if (writingImage) {
|
|
810
1185
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
811
1186
|
process.stderr.write(`Token Ledger: encoding ${imageLabel} PNG…\n`);
|
|
812
1187
|
await writeTrendPng(output, outputPath);
|
|
813
1188
|
process.stderr.write(`Token Ledger: finished ${imageLabel} PNG.\n`);
|
|
814
|
-
|
|
815
|
-
`Wrote ${
|
|
1189
|
+
const lines = [
|
|
1190
|
+
`Wrote ${imageLabel}: ${outputPath}`,
|
|
816
1191
|
`Range: ${bounds.startDateString} through ${bounds.endDateString} (${bounds.timeZone})`,
|
|
817
|
-
]
|
|
1192
|
+
];
|
|
1193
|
+
// Show the finished report on screen right away instead of leaving it to
|
|
1194
|
+
// be dug out of a file browser. Skipped for piped/scripted runs so
|
|
1195
|
+
// automation and CI never pop windows.
|
|
1196
|
+
if (options.openImage && process.stdout.isTTY) {
|
|
1197
|
+
lines.push(
|
|
1198
|
+
openInViewer(outputPath)
|
|
1199
|
+
? "Opened the report in your default image viewer."
|
|
1200
|
+
: "Could not open a viewer automatically; open the file above to see the report.",
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
return lines.join("\n");
|
|
818
1204
|
}
|
|
819
1205
|
return output;
|
|
820
1206
|
}
|
|
821
1207
|
|
|
1208
|
+
function openInViewer(path) {
|
|
1209
|
+
const platform = process.platform;
|
|
1210
|
+
const [command, args] = platform === "darwin"
|
|
1211
|
+
? ["open", [path]]
|
|
1212
|
+
: platform === "win32"
|
|
1213
|
+
? ["cmd", ["/c", "start", "", path]]
|
|
1214
|
+
: ["xdg-open", [path]];
|
|
1215
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
1216
|
+
return result.status === 0;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
822
1219
|
function shouldUseInteractive(options) {
|
|
823
1220
|
return Boolean(
|
|
824
1221
|
!options.static &&
|
|
@@ -832,17 +1229,12 @@ function shouldUseInteractive(options) {
|
|
|
832
1229
|
}
|
|
833
1230
|
|
|
834
1231
|
async function runInteractive(options) {
|
|
835
|
-
const bounds = options
|
|
836
|
-
? weekBounds(options.date, options.timeZone)
|
|
837
|
-
: dayBounds(options.date, options.timeZone);
|
|
1232
|
+
const bounds = boundsForOptions(options);
|
|
838
1233
|
const snapshot = await loadSnapshot(options);
|
|
839
1234
|
const events = filterDayEvents(snapshot, bounds);
|
|
840
1235
|
if (events.length === 0) {
|
|
841
|
-
const rangeDescription = options.range === "week"
|
|
842
|
-
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
843
|
-
: bounds.dateString;
|
|
844
1236
|
process.stdout.write([
|
|
845
|
-
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
1237
|
+
`No model-call events found for ${rangeDescription(options, bounds)} (${bounds.timeZone}).`,
|
|
846
1238
|
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
847
1239
|
"",
|
|
848
1240
|
].join("\n"));
|
|
@@ -852,6 +1244,7 @@ async function runInteractive(options) {
|
|
|
852
1244
|
await startInteractive({
|
|
853
1245
|
options,
|
|
854
1246
|
snapshot,
|
|
1247
|
+
snapshotFreshness: snapshotFreshness(snapshot),
|
|
855
1248
|
bounds,
|
|
856
1249
|
events,
|
|
857
1250
|
rows: allRows.slice(0, options.top),
|
|
@@ -864,7 +1257,7 @@ async function main() {
|
|
|
864
1257
|
try {
|
|
865
1258
|
options = parseArgs(process.argv.slice(2));
|
|
866
1259
|
if (options.help) {
|
|
867
|
-
process.stdout.write(`${usage()}\n`);
|
|
1260
|
+
process.stdout.write(`${options.helpAll ? advancedUsage() : usage()}\n`);
|
|
868
1261
|
return;
|
|
869
1262
|
}
|
|
870
1263
|
if (shouldUseInteractive(options)) {
|
|
@@ -873,7 +1266,13 @@ async function main() {
|
|
|
873
1266
|
process.stdout.write(`${await run(options)}\n`);
|
|
874
1267
|
}
|
|
875
1268
|
} catch (error) {
|
|
876
|
-
process.stderr.write(
|
|
1269
|
+
process.stderr.write(
|
|
1270
|
+
`Token Ledger: ${safeErrorMessage(error, [
|
|
1271
|
+
options?.input,
|
|
1272
|
+
options?.codexHome,
|
|
1273
|
+
options?.imageOutput,
|
|
1274
|
+
])}\nRun \`tledger --help\` for examples or \`tledger --help-all\` for every option.\n`,
|
|
1275
|
+
);
|
|
877
1276
|
process.exitCode = 1;
|
|
878
1277
|
}
|
|
879
1278
|
}
|