tledger 0.1.4 → 0.2.1
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 +142 -78
- package/bin/token-ledger-controls.mjs +24 -0
- package/bin/token-ledger-rates.mjs +62 -0
- package/bin/token-ledger-terminal.mjs +184 -264
- 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 +26 -30
- package/bin/token-ledger.mjs +601 -253
- package/lib/{token-ledger-collector.mjs → token-ledger-importer.mjs} +256 -409
- package/package.json +19 -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,34 @@ 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
|
+
export const ROLLING_24_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
const ANSI_RESET = "\u001b[0m";
|
|
36
|
+
const MODEL_COLORS = {
|
|
37
|
+
sol: TERMINAL_MODEL_COLORS.sol,
|
|
38
|
+
luna: TERMINAL_MODEL_COLORS.luna,
|
|
39
|
+
terra: TERMINAL_MODEL_COLORS.terra,
|
|
40
|
+
"gpt-5.5": TERMINAL_MODEL_COLORS.gpt,
|
|
41
|
+
"gpt-5.4": TERMINAL_MODEL_COLORS.gpt,
|
|
42
|
+
other: TERMINAL_MODEL_COLORS.other,
|
|
43
|
+
};
|
|
28
44
|
|
|
29
45
|
function usage() {
|
|
30
46
|
return `Token Ledger terminal usage
|
|
31
47
|
|
|
32
48
|
Usage:
|
|
33
|
-
tledger
|
|
49
|
+
tledger 1d Rolling 24-hour project breakdown (ends now)
|
|
50
|
+
tledger day <YYYY-MM-DD>
|
|
34
51
|
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
|
|
52
|
+
tledger trend [7d|14d|30d]
|
|
53
|
+
tledger report [7d|14d|30d]
|
|
54
|
+
npm run usage:day -- <YYYY-MM-DD>
|
|
55
|
+
npm run usage:week -- [end-day]
|
|
46
56
|
|
|
47
57
|
Options:
|
|
48
58
|
--date <day> Date as YYYY-MM-DD, today, or yesterday
|
|
59
|
+
--period <window> Trend window: 7d, 14d, or 30d
|
|
49
60
|
--input <file> Snapshot to read (default: ~/.token-ledger/token-ledger-snapshot.json)
|
|
50
61
|
--refresh Rebuild the default snapshot from CODEX_HOME or ~/.codex
|
|
51
62
|
--no-refresh Use the cached snapshot without checking local JSONL files
|
|
@@ -54,16 +65,23 @@ Options:
|
|
|
54
65
|
--top <number> Number of projects to show (default: 10)
|
|
55
66
|
--width <number> Terminal layout width in columns
|
|
56
67
|
--raw-projects Keep singleton thread labels instead of grouping them
|
|
57
|
-
-anon Replace project names with Project 1, Project 2, and so on
|
|
58
68
|
--no-archived Skip archived_sessions when refreshing
|
|
59
69
|
--plain Disable terminal colors
|
|
60
70
|
--ascii Use ASCII bars instead of Unicode blocks
|
|
61
71
|
--static Print once instead of opening the interactive dashboard
|
|
62
|
-
|
|
72
|
+
--drain Trend columns show observed limit drain percent instead of token volume
|
|
73
|
+
--image Write trend view as a PNG image
|
|
74
|
+
--image-output <file> PNG output path for trend view
|
|
75
|
+
--image-width <px> PNG image width from 900 to 2400 pixels
|
|
76
|
+
--youplot Use the legacy single-series YouPlot renderer
|
|
63
77
|
--help Show this help
|
|
64
78
|
|
|
65
|
-
The
|
|
66
|
-
|
|
79
|
+
The report command writes the dashboard PNG (same as trend --image) to
|
|
80
|
+
token-ledger-report-<period>.png; use --image-output to choose the path.
|
|
81
|
+
|
|
82
|
+
The command reads a privacy-reduced Token Ledger snapshot. It never uploads
|
|
83
|
+
the snapshot or prints message bodies, tool payloads, credentials, or local
|
|
84
|
+
input/source paths. Explicit PNG output paths are reported after writing.`;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
function readOption(argv, index, name) {
|
|
@@ -74,38 +92,21 @@ function readOption(argv, index, name) {
|
|
|
74
92
|
return value;
|
|
75
93
|
}
|
|
76
94
|
|
|
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
95
|
export function parseArgs(argv) {
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
const rolling24hCommand = argv[0] === "1d";
|
|
97
|
+
const command = rolling24hCommand
|
|
98
|
+
? "rolling24h"
|
|
99
|
+
: argv[0] === "week"
|
|
100
|
+
? "week"
|
|
101
|
+
: argv[0] === "trend" || argv[0] === "report"
|
|
102
|
+
? "trend"
|
|
103
|
+
: "day";
|
|
106
104
|
const options = {
|
|
107
|
-
range: command
|
|
108
|
-
|
|
105
|
+
range: command,
|
|
106
|
+
view: command === "trend" ? "trend" : "projects",
|
|
107
|
+
rolling24h: rolling24hCommand,
|
|
108
|
+
report: argv[0] === "report",
|
|
109
|
+
trendDays: 7,
|
|
109
110
|
date: null,
|
|
110
111
|
input: DEFAULT_SNAPSHOT,
|
|
111
112
|
inputExplicit: false,
|
|
@@ -117,24 +118,40 @@ export function parseArgs(argv) {
|
|
|
117
118
|
top: DEFAULT_TOP,
|
|
118
119
|
width: null,
|
|
119
120
|
rawProjects: false,
|
|
120
|
-
anonymizeProjects: false,
|
|
121
121
|
plain: false,
|
|
122
122
|
ascii: false,
|
|
123
123
|
static: false,
|
|
124
|
-
|
|
124
|
+
image: false,
|
|
125
|
+
imageOutput: null,
|
|
126
|
+
imageWidth: null,
|
|
127
|
+
drain: false,
|
|
128
|
+
legacyPlot: false,
|
|
125
129
|
help: false,
|
|
126
130
|
};
|
|
127
131
|
|
|
128
|
-
let
|
|
132
|
+
let trendPeriodSeen = false;
|
|
133
|
+
let index = ["1d", "day", "week", "trend", "report"].includes(argv[0]) ? 1 : 0;
|
|
129
134
|
for (; index < argv.length; index += 1) {
|
|
130
135
|
const argument = argv[index];
|
|
131
136
|
if (argument === "--help" || argument === "-h") {
|
|
132
137
|
options.help = true;
|
|
133
|
-
} else if (argument === "--version" || argument === "-v") {
|
|
134
|
-
options.version = true;
|
|
135
138
|
} else if (argument === "--date") {
|
|
136
139
|
options.date = readOption(argv, index, "--date");
|
|
137
140
|
index += 1;
|
|
141
|
+
} else if (argument === "--period") {
|
|
142
|
+
if (options.view !== "trend") {
|
|
143
|
+
throw new Error("--period is only available for the trend view.");
|
|
144
|
+
}
|
|
145
|
+
if (trendPeriodSeen) {
|
|
146
|
+
throw new Error("Trend period can only be specified once.");
|
|
147
|
+
}
|
|
148
|
+
const value = readOption(argv, index, "--period");
|
|
149
|
+
if (!["7d", "14d", "30d"].includes(value)) {
|
|
150
|
+
throw new Error("Trend period must be 7d, 14d, or 30d.");
|
|
151
|
+
}
|
|
152
|
+
options.trendDays = Number.parseInt(value, 10);
|
|
153
|
+
trendPeriodSeen = true;
|
|
154
|
+
index += 1;
|
|
138
155
|
} else if (argument === "--input") {
|
|
139
156
|
options.input = resolve(readOption(argv, index, "--input"));
|
|
140
157
|
options.inputExplicit = true;
|
|
@@ -165,8 +182,6 @@ export function parseArgs(argv) {
|
|
|
165
182
|
index += 1;
|
|
166
183
|
} else if (argument === "--raw-projects") {
|
|
167
184
|
options.rawProjects = true;
|
|
168
|
-
} else if (argument === "-anon") {
|
|
169
|
-
options.anonymizeProjects = true;
|
|
170
185
|
} else if (argument === "--no-archived") {
|
|
171
186
|
options.includeArchived = false;
|
|
172
187
|
} else if (argument === "--plain") {
|
|
@@ -175,25 +190,88 @@ export function parseArgs(argv) {
|
|
|
175
190
|
options.ascii = true;
|
|
176
191
|
} else if (argument === "--static") {
|
|
177
192
|
options.static = true;
|
|
178
|
-
} else if (
|
|
193
|
+
} else if (argument === "--drain") {
|
|
194
|
+
if (options.view !== "trend") {
|
|
195
|
+
throw new Error("--drain is only available for the trend view.");
|
|
196
|
+
}
|
|
197
|
+
options.drain = true;
|
|
198
|
+
} else if (argument === "--image") {
|
|
199
|
+
if (options.view !== "trend") {
|
|
200
|
+
throw new Error("--image is only available for the trend view.");
|
|
201
|
+
}
|
|
202
|
+
options.image = true;
|
|
203
|
+
} else if (argument === "--image-output") {
|
|
204
|
+
if (options.view !== "trend") {
|
|
205
|
+
throw new Error("--image-output is only available for the trend view.");
|
|
206
|
+
}
|
|
207
|
+
const value = readOption(argv, index, "--image-output");
|
|
208
|
+
if (!value.toLowerCase().endsWith(".png")) {
|
|
209
|
+
throw new Error("--image-output must end in .png.");
|
|
210
|
+
}
|
|
211
|
+
options.image = true;
|
|
212
|
+
options.imageOutput = resolve(value);
|
|
213
|
+
index += 1;
|
|
214
|
+
} else if (argument === "--image-width") {
|
|
215
|
+
if (options.view !== "trend") {
|
|
216
|
+
throw new Error("--image-width is only available for the trend view.");
|
|
217
|
+
}
|
|
218
|
+
const value = Number(readOption(argv, index, "--image-width"));
|
|
219
|
+
if (!Number.isInteger(value) || value < 900 || value > 2400) {
|
|
220
|
+
throw new Error("--image-width must be an integer from 900 to 2400.");
|
|
221
|
+
}
|
|
222
|
+
options.image = true;
|
|
223
|
+
options.imageWidth = value;
|
|
224
|
+
index += 1;
|
|
225
|
+
} else if (argument === "--youplot") {
|
|
226
|
+
options.legacyPlot = true;
|
|
227
|
+
} else if (
|
|
228
|
+
!argument.startsWith("-") &&
|
|
229
|
+
options.view === "projects" &&
|
|
230
|
+
argument === "1d" &&
|
|
231
|
+
!options.date
|
|
232
|
+
) {
|
|
233
|
+
if (argv[0] !== "day") {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"The 1d alias is only available as `tledger 1d` or `tledger day 1d`.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
options.range = "rolling24h";
|
|
239
|
+
options.rolling24h = true;
|
|
240
|
+
} else if (!argument.startsWith("-") && options.view === "trend") {
|
|
241
|
+
if (trendPeriodSeen) {
|
|
242
|
+
throw new Error("Trend period can only be specified once.");
|
|
243
|
+
}
|
|
244
|
+
if (!["7d", "14d", "30d"].includes(argument)) {
|
|
245
|
+
throw new Error("Trend period must be 7d, 14d, or 30d.");
|
|
246
|
+
}
|
|
247
|
+
options.trendDays = Number.parseInt(argument, 10);
|
|
248
|
+
trendPeriodSeen = true;
|
|
249
|
+
} else if (!argument.startsWith("-") && !options.date) {
|
|
179
250
|
options.date = argument;
|
|
180
251
|
} else {
|
|
181
252
|
throw new Error(`Unknown option: ${argument}`);
|
|
182
253
|
}
|
|
183
254
|
}
|
|
184
255
|
|
|
185
|
-
if (
|
|
186
|
-
|
|
256
|
+
if (options.report) options.image = true;
|
|
257
|
+
if (!options.help && options.rolling24h && options.date) {
|
|
258
|
+
throw new Error("1d does not accept --date; its rolling window ends now.");
|
|
187
259
|
}
|
|
188
|
-
if (!options.help && !options.
|
|
260
|
+
if (!options.help && !options.date && (options.range === "week" || options.view === "trend")) {
|
|
189
261
|
options.date = "today";
|
|
190
262
|
}
|
|
191
|
-
if (!options.help && !options.
|
|
263
|
+
if (!options.help && !options.date && !options.rolling24h) {
|
|
264
|
+
throw new Error("A day is required, for example: tledger day 2026-08-01");
|
|
265
|
+
}
|
|
266
|
+
if (!options.help && options.refresh && !options.autoRefresh) {
|
|
192
267
|
throw new Error("--refresh cannot be combined with --no-refresh.");
|
|
193
268
|
}
|
|
194
|
-
if (!options.help &&
|
|
269
|
+
if (!options.help && options.refresh && options.inputExplicit) {
|
|
195
270
|
throw new Error("--refresh cannot be combined with --input.");
|
|
196
271
|
}
|
|
272
|
+
if (!options.help && options.view === "trend" && options.legacyPlot) {
|
|
273
|
+
throw new Error("--youplot is only available for the project view.");
|
|
274
|
+
}
|
|
197
275
|
return options;
|
|
198
276
|
}
|
|
199
277
|
|
|
@@ -294,99 +372,35 @@ export function dayBounds(value, timeZone) {
|
|
|
294
372
|
const nextDateString = shiftCalendarDate(dateString, 1);
|
|
295
373
|
const start = zonedMidnight(dateString, timeZone);
|
|
296
374
|
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
|
-
};
|
|
375
|
+
return { dateString, start, end, timeZone };
|
|
306
376
|
}
|
|
307
377
|
|
|
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
|
-
}
|
|
378
|
+
export function weekBounds(value, timeZone) {
|
|
318
379
|
const endDay = dayBounds(value, timeZone);
|
|
319
|
-
const startDateString = shiftCalendarDate(endDay.dateString, -
|
|
380
|
+
const startDateString = shiftCalendarDate(endDay.dateString, -6);
|
|
320
381
|
return {
|
|
321
382
|
...endDay,
|
|
322
383
|
startDateString,
|
|
323
384
|
endDateString: endDay.dateString,
|
|
324
385
|
start: zonedMidnight(startDateString, timeZone),
|
|
325
|
-
rangeDays,
|
|
386
|
+
rangeDays: 7,
|
|
326
387
|
};
|
|
327
388
|
}
|
|
328
389
|
|
|
329
|
-
export function
|
|
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) {
|
|
390
|
+
export function rolling24hBounds(value = new Date(), timeZone = DEFAULT_TIME_ZONE) {
|
|
345
391
|
validateTimeZone(timeZone);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
};
|
|
392
|
+
const end = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
393
|
+
if (!Number.isFinite(end.getTime())) {
|
|
394
|
+
throw new Error("Rolling 24-hour window requires a valid end time.");
|
|
360
395
|
}
|
|
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
396
|
return {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
endDateString,
|
|
371
|
-
start: zonedMidnight(startDateString, timeZone),
|
|
372
|
-
end: zonedMidnight(shiftCalendarDate(endDateString, 1), timeZone),
|
|
397
|
+
start: new Date(end.getTime() - ROLLING_24_HOURS_MS),
|
|
398
|
+
end,
|
|
373
399
|
timeZone,
|
|
374
|
-
|
|
375
|
-
allTime: true,
|
|
400
|
+
rangeHours: 24,
|
|
376
401
|
};
|
|
377
402
|
}
|
|
378
403
|
|
|
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
404
|
export function sanitizeTerminalText(value) {
|
|
391
405
|
return String(value ?? "")
|
|
392
406
|
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
@@ -401,6 +415,62 @@ function cleanLabel(value, fallback) {
|
|
|
401
415
|
return label || fallback;
|
|
402
416
|
}
|
|
403
417
|
|
|
418
|
+
const QUOTED_ABSOLUTE_PATH =
|
|
419
|
+
/(["'])(\/(?!\/)[^"'\r\n]*|(?:\/\/|\\\\)[^"'\r\n]+|[A-Za-z]:[\\/][^"'\r\n]*)\1/g;
|
|
420
|
+
const UNQUOTED_ABSOLUTE_PATH =
|
|
421
|
+
/(^|[\s([{=])((?:\/(?!\/)|\/\/|\\\\|[A-Za-z]:[\\/])[^\s"'`)\]},;]+)/g;
|
|
422
|
+
|
|
423
|
+
function isAbsoluteLocalPath(path) {
|
|
424
|
+
return (
|
|
425
|
+
path.startsWith("/") ||
|
|
426
|
+
path.startsWith("\\\\") ||
|
|
427
|
+
/^[A-Za-z]:[\\/]/.test(path)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function safeDisplayLabel(value, fallback = "local path") {
|
|
432
|
+
const normalized = String(value ?? "").replaceAll("\\", "/");
|
|
433
|
+
const label = sanitizeTerminalText(basename(normalized))
|
|
434
|
+
.replace(/\s+/g, " ")
|
|
435
|
+
.trim();
|
|
436
|
+
return label && label !== "." && label !== ".." ? label : fallback;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function redactLocalPaths(value, paths = []) {
|
|
440
|
+
let redacted = String(value ?? "");
|
|
441
|
+
const explicitPaths = new Set(
|
|
442
|
+
paths
|
|
443
|
+
.filter(Boolean)
|
|
444
|
+
.map((path) => String(path))
|
|
445
|
+
.filter(isAbsoluteLocalPath),
|
|
446
|
+
);
|
|
447
|
+
const pathsToRedact = [...new Set([
|
|
448
|
+
...explicitPaths,
|
|
449
|
+
homedir(),
|
|
450
|
+
process.cwd(),
|
|
451
|
+
])]
|
|
452
|
+
.filter((path) => path && path !== "/")
|
|
453
|
+
.sort((left, right) => right.length - left.length);
|
|
454
|
+
|
|
455
|
+
for (const path of pathsToRedact) {
|
|
456
|
+
redacted = redacted.replaceAll(
|
|
457
|
+
path,
|
|
458
|
+
explicitPaths.has(path) ? safeDisplayLabel(path) : "[local path]",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return redacted
|
|
463
|
+
.replace(QUOTED_ABSOLUTE_PATH, (_match, quote) => `${quote}[local path]${quote}`)
|
|
464
|
+
.replace(UNQUOTED_ABSOLUTE_PATH, (_match, prefix) => `${prefix}[local path]`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function safeErrorMessage(error, paths = []) {
|
|
468
|
+
return redactLocalPaths(
|
|
469
|
+
error instanceof Error ? error.message : String(error),
|
|
470
|
+
paths,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
404
474
|
function displayLabel(value) {
|
|
405
475
|
const label = cleanLabel(value, "Unlabelled activity");
|
|
406
476
|
if (label.length <= 30) return label;
|
|
@@ -427,14 +497,20 @@ export function oneOffProjects(snapshot) {
|
|
|
427
497
|
|
|
428
498
|
function modelLabel(value) {
|
|
429
499
|
const model = cleanLabel(value, "Unknown model");
|
|
430
|
-
|
|
500
|
+
const lower = model.toLowerCase();
|
|
501
|
+
if (lower.includes("sol")) return "Sol";
|
|
502
|
+
if (lower.includes("luna")) return "Luna";
|
|
503
|
+
if (lower.includes("terra")) return "Terra";
|
|
504
|
+
if (lower === "gpt-5.5") return "GPT-5.5";
|
|
505
|
+
if (lower === "gpt-5.4") return "GPT-5.4";
|
|
506
|
+
return model;
|
|
431
507
|
}
|
|
432
508
|
|
|
433
509
|
export function filterDayEvents(snapshot, bounds) {
|
|
434
510
|
const start = bounds.start.getTime();
|
|
435
511
|
const end = bounds.end.getTime();
|
|
436
512
|
return (snapshot.events ?? []).filter((event) => {
|
|
437
|
-
const timestamp =
|
|
513
|
+
const timestamp = new Date(event.timestamp).getTime();
|
|
438
514
|
return Number.isFinite(timestamp) && timestamp >= start && timestamp < end;
|
|
439
515
|
});
|
|
440
516
|
}
|
|
@@ -459,6 +535,8 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
459
535
|
toolCalls: 0,
|
|
460
536
|
events: 0,
|
|
461
537
|
threadIds: new Set(),
|
|
538
|
+
rateCardCredits: 0,
|
|
539
|
+
knownCreditTokens: 0,
|
|
462
540
|
models: new Map(),
|
|
463
541
|
};
|
|
464
542
|
row.totalTokens += Number(event.totalTokens) || 0;
|
|
@@ -467,14 +545,23 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
467
545
|
row.toolCalls += Number(event.toolCalls) || 0;
|
|
468
546
|
row.events += 1;
|
|
469
547
|
if (event.threadId) row.threadIds.add(event.threadId);
|
|
548
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
549
|
+
row.rateCardCredits += Number(event.rateCardCredits);
|
|
550
|
+
row.knownCreditTokens += Number(event.totalTokens) || 0;
|
|
551
|
+
}
|
|
552
|
+
|
|
470
553
|
const model = modelLabel(event.model);
|
|
471
554
|
const modelRow = row.models.get(model) ?? {
|
|
472
555
|
model,
|
|
473
556
|
totalTokens: 0,
|
|
474
557
|
events: 0,
|
|
558
|
+
rateCardCredits: 0,
|
|
475
559
|
};
|
|
476
560
|
modelRow.totalTokens += Number(event.totalTokens) || 0;
|
|
477
561
|
modelRow.events += 1;
|
|
562
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
563
|
+
modelRow.rateCardCredits += Number(event.rateCardCredits);
|
|
564
|
+
}
|
|
478
565
|
row.models.set(model, modelRow);
|
|
479
566
|
grouped.set(project, row);
|
|
480
567
|
}
|
|
@@ -492,13 +579,93 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
492
579
|
return right.totalTokens - left.totalTokens;
|
|
493
580
|
}
|
|
494
581
|
return left.project.localeCompare(right.project);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function totalSummary(events) {
|
|
586
|
+
return events.reduce(
|
|
587
|
+
(summary, event) => {
|
|
588
|
+
summary.totalTokens += Number(event.totalTokens) || 0;
|
|
589
|
+
summary.outputTokens += Number(event.outputTokens) || 0;
|
|
590
|
+
summary.toolCalls += Number(event.toolCalls) || 0;
|
|
591
|
+
if (event.threadId) summary.threadIds.add(event.threadId);
|
|
592
|
+
if (event.rateCardCredits !== null && Number.isFinite(Number(event.rateCardCredits))) {
|
|
593
|
+
summary.rateCardCredits += Number(event.rateCardCredits);
|
|
594
|
+
summary.knownCreditTokens += Number(event.totalTokens) || 0;
|
|
595
|
+
}
|
|
596
|
+
return summary;
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
totalTokens: 0,
|
|
600
|
+
outputTokens: 0,
|
|
601
|
+
toolCalls: 0,
|
|
602
|
+
rateCardCredits: 0,
|
|
603
|
+
knownCreditTokens: 0,
|
|
604
|
+
threadIds: new Set(),
|
|
605
|
+
},
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function compact(value, digits = 2) {
|
|
610
|
+
if (!Number.isFinite(value)) return "—";
|
|
611
|
+
const absolute = Math.abs(value);
|
|
612
|
+
const units = [
|
|
613
|
+
[1_000_000_000, "B"],
|
|
614
|
+
[1_000_000, "M"],
|
|
615
|
+
[1_000, "K"],
|
|
616
|
+
];
|
|
617
|
+
for (const [divisor, suffix] of units) {
|
|
618
|
+
if (absolute >= divisor) {
|
|
619
|
+
const scaled = value / divisor;
|
|
620
|
+
const precision = scaled >= 100 ? 0 : scaled >= 10 ? 1 : digits;
|
|
621
|
+
return `${scaled.toFixed(precision)}${suffix}`;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return Math.round(value).toLocaleString("en-US");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function percent(value) {
|
|
628
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}%`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function chartUnit(maximum) {
|
|
632
|
+
if (maximum >= 1_000_000_000) return { divisor: 1_000_000_000, suffix: "B" };
|
|
633
|
+
if (maximum >= 1_000_000) return { divisor: 1_000_000, suffix: "M" };
|
|
634
|
+
if (maximum >= 1_000) return { divisor: 1_000, suffix: "K" };
|
|
635
|
+
return { divisor: 1, suffix: "tokens" };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function chartNumber(value, divisor) {
|
|
639
|
+
const scaled = value / divisor;
|
|
640
|
+
if (scaled >= 100) return scaled.toFixed(0);
|
|
641
|
+
if (scaled >= 10) return scaled.toFixed(1);
|
|
642
|
+
return scaled.toFixed(2);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function colorize(value, code, enabled) {
|
|
646
|
+
const codes = Array.isArray(code) ? code.join(";") : code;
|
|
647
|
+
return enabled ? `\u001b[${codes}m${value}${ANSI_RESET}` : value;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function modelColor(model) {
|
|
651
|
+
const lower = model.toLowerCase();
|
|
652
|
+
if (lower.includes("sol")) return MODEL_COLORS.sol;
|
|
653
|
+
if (lower.includes("luna")) return MODEL_COLORS.luna;
|
|
654
|
+
if (lower.includes("terra")) return MODEL_COLORS.terra;
|
|
655
|
+
if (lower.includes("gpt-5.5") || lower.includes("gpt-5.4")) {
|
|
656
|
+
return MODEL_COLORS["gpt-5.5"];
|
|
657
|
+
}
|
|
658
|
+
return MODEL_COLORS.other;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function modelMix(row, enabled) {
|
|
662
|
+
return row.models
|
|
663
|
+
.slice(0, 4)
|
|
664
|
+
.map((model) => {
|
|
665
|
+
const share = row.totalTokens > 0 ? (model.totalTokens / row.totalTokens) * 100 : 0;
|
|
666
|
+
return `${colorize(model.model, modelColor(model.model), enabled)} ${percent(share)}`;
|
|
495
667
|
})
|
|
496
|
-
.
|
|
497
|
-
...row,
|
|
498
|
-
displayProject: options.anonymizeProjects
|
|
499
|
-
? `Project ${index + 1}`
|
|
500
|
-
: row.displayProject,
|
|
501
|
-
}));
|
|
668
|
+
.join(" · ");
|
|
502
669
|
}
|
|
503
670
|
|
|
504
671
|
function sourceLabel(snapshotPath, snapshot) {
|
|
@@ -508,64 +675,65 @@ function sourceLabel(snapshotPath, snapshot) {
|
|
|
508
675
|
timeStyle: "short",
|
|
509
676
|
})
|
|
510
677
|
: "unknown time";
|
|
511
|
-
return `${
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function latestActivityDateString(snapshot, timeZone) {
|
|
515
|
-
let latestTimestamp = Number.NEGATIVE_INFINITY;
|
|
516
|
-
for (const event of snapshot.events ?? []) {
|
|
517
|
-
const timestamp = eventTimestamp(event);
|
|
518
|
-
if (Number.isFinite(timestamp) && timestamp > latestTimestamp) {
|
|
519
|
-
latestTimestamp = timestamp;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (!Number.isFinite(latestTimestamp)) return null;
|
|
523
|
-
return dateStringFromParts(numericDateParts({
|
|
524
|
-
value: new Date(latestTimestamp),
|
|
525
|
-
timeZone,
|
|
526
|
-
}));
|
|
678
|
+
return `${safeDisplayLabel(snapshotPath, "snapshot")} · captured ${generated}`;
|
|
527
679
|
}
|
|
528
680
|
|
|
529
|
-
function
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"
|
|
681
|
+
function runYouPlot(rows, options, dateLabel, unit) {
|
|
682
|
+
const chartInput = [
|
|
683
|
+
"project\tvalue",
|
|
684
|
+
...rows.map(
|
|
685
|
+
(row) => `${row.displayProject.replace(/[\t\r\n]+/g, " ")}\t${chartNumber(row.totalTokens, unit.divisor)}`,
|
|
686
|
+
),
|
|
687
|
+
].join("\n");
|
|
688
|
+
const terminalWidth = Number(process.stdout.columns) || 100;
|
|
689
|
+
const width = options.width ?? Math.max(56, Math.min(110, terminalWidth - 4));
|
|
690
|
+
const useColor = !options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY);
|
|
691
|
+
const args = [
|
|
692
|
+
"bar",
|
|
693
|
+
"-H",
|
|
694
|
+
"-o",
|
|
695
|
+
"-",
|
|
696
|
+
"-t",
|
|
697
|
+
`Top ${rows.length} projects · tokens (${unit.suffix}) · ${dateLabel}`,
|
|
698
|
+
"-w",
|
|
699
|
+
String(width),
|
|
700
|
+
"--symbol",
|
|
701
|
+
options.ascii ? "#" : "█",
|
|
543
702
|
];
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
703
|
+
if (useColor) args.push("-C", "-c", "blue");
|
|
704
|
+
else args.push("-M");
|
|
705
|
+
|
|
706
|
+
const result = spawnSync("uplot", args, {
|
|
707
|
+
input: `${chartInput}\n`,
|
|
708
|
+
encoding: "utf8",
|
|
709
|
+
maxBuffer: 1_000_000,
|
|
710
|
+
});
|
|
711
|
+
if (result.error?.code === "ENOENT") {
|
|
712
|
+
throw new Error(
|
|
713
|
+
"YouPlot is required. Install it with `brew install youplot`, then rerun this command.",
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (result.status !== 0) {
|
|
717
|
+
throw new Error(result.stderr?.trim() || "YouPlot failed to render the chart.");
|
|
548
718
|
}
|
|
549
|
-
|
|
550
|
-
return lines.join("\n");
|
|
719
|
+
return result.stdout;
|
|
551
720
|
}
|
|
552
721
|
|
|
553
722
|
async function readSnapshot(snapshotPath) {
|
|
723
|
+
const snapshotLabel = safeDisplayLabel(snapshotPath, "snapshot");
|
|
554
724
|
let parsed;
|
|
555
725
|
try {
|
|
556
726
|
parsed = JSON.parse(await readFile(snapshotPath, "utf8"));
|
|
557
727
|
} catch (error) {
|
|
558
728
|
if (error?.code === "ENOENT") {
|
|
559
|
-
throw new Error(`Snapshot not found: ${
|
|
729
|
+
throw new Error(`Snapshot not found: ${snapshotLabel}`);
|
|
560
730
|
}
|
|
561
731
|
throw new Error(
|
|
562
|
-
`Could not read snapshot ${
|
|
732
|
+
`Could not read snapshot ${snapshotLabel}: ${safeErrorMessage(error, [snapshotPath])}`,
|
|
563
733
|
);
|
|
564
734
|
}
|
|
565
735
|
if (!parsed || !Array.isArray(parsed.events)) {
|
|
566
|
-
throw new Error(
|
|
567
|
-
`Snapshot is missing its events array: ${sanitizeTerminalText(snapshotPath)}`,
|
|
568
|
-
);
|
|
736
|
+
throw new Error(`Snapshot is missing its events array: ${snapshotLabel}`);
|
|
569
737
|
}
|
|
570
738
|
return parsed;
|
|
571
739
|
}
|
|
@@ -573,51 +741,89 @@ async function readSnapshot(snapshotPath) {
|
|
|
573
741
|
async function refreshSnapshot(options) {
|
|
574
742
|
if (!existsSync(options.codexHome)) {
|
|
575
743
|
throw new Error(
|
|
576
|
-
`Codex data directory not found: ${
|
|
744
|
+
`Codex data directory not found: ${safeDisplayLabel(options.codexHome, "Codex data directory")}`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
let progressStarted = false;
|
|
748
|
+
try {
|
|
749
|
+
const { collectUsage, writePrivateSnapshot } = await import(
|
|
750
|
+
"../lib/token-ledger-importer.mjs"
|
|
751
|
+
);
|
|
752
|
+
process.stderr.write("Token Ledger: refreshing local snapshot…\n");
|
|
753
|
+
progressStarted = true;
|
|
754
|
+
const snapshot = await collectUsage(
|
|
755
|
+
{
|
|
756
|
+
output: options.input,
|
|
757
|
+
codexHome: options.codexHome,
|
|
758
|
+
includeArchived: options.includeArchived,
|
|
759
|
+
since: null,
|
|
760
|
+
},
|
|
761
|
+
({ current, total }) => {
|
|
762
|
+
process.stderr.write(`\rToken Ledger: scanned ${current}/${total} rollout files`);
|
|
763
|
+
},
|
|
764
|
+
);
|
|
765
|
+
process.stderr.write("\n");
|
|
766
|
+
await writePrivateSnapshot(options.input, snapshot);
|
|
767
|
+
return snapshot;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (progressStarted) process.stderr.write("\n");
|
|
770
|
+
throw new Error(
|
|
771
|
+
`Could not refresh local snapshot: ${safeErrorMessage(error, [options.input, options.codexHome])}`,
|
|
577
772
|
);
|
|
578
773
|
}
|
|
579
|
-
const { collectUsage, writePrivateSnapshot } = await import(
|
|
580
|
-
"../lib/token-ledger-collector.mjs"
|
|
581
|
-
);
|
|
582
|
-
process.stderr.write("Token Ledger: refreshing local snapshot…\n");
|
|
583
|
-
const snapshot = await collectUsage(
|
|
584
|
-
{
|
|
585
|
-
output: options.input,
|
|
586
|
-
codexHome: options.codexHome,
|
|
587
|
-
includeArchived: options.includeArchived,
|
|
588
|
-
since: null,
|
|
589
|
-
},
|
|
590
|
-
({ current, total }) => {
|
|
591
|
-
process.stderr.write(`\rToken Ledger: scanned ${current}/${total} rollout files`);
|
|
592
|
-
},
|
|
593
|
-
);
|
|
594
|
-
process.stderr.write("\n");
|
|
595
|
-
await writePrivateSnapshot(options.input, snapshot);
|
|
596
|
-
return snapshot;
|
|
597
774
|
}
|
|
598
775
|
|
|
599
|
-
export function snapshotNeedsRefresh(
|
|
776
|
+
export function snapshotNeedsRefresh(snapshotMtimeMs, latestJsonlMtimeMs) {
|
|
777
|
+
return latestJsonlMtimeMs > snapshotMtimeMs;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function snapshotCacheIsFresh(
|
|
600
781
|
snapshotMtimeMs,
|
|
601
|
-
|
|
602
|
-
cachedSourceFingerprint,
|
|
603
|
-
expectedSourceFingerprint,
|
|
604
|
-
cachedSourceFileCount,
|
|
605
|
-
currentSourceFileCount,
|
|
782
|
+
nowMs = Date.now(),
|
|
606
783
|
) {
|
|
607
784
|
return (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
785
|
+
Number.isFinite(snapshotMtimeMs) &&
|
|
786
|
+
Number.isFinite(nowMs) &&
|
|
787
|
+
snapshotMtimeMs <= nowMs &&
|
|
788
|
+
nowMs - snapshotMtimeMs < SNAPSHOT_CACHE_MAX_AGE_MS
|
|
611
789
|
);
|
|
612
790
|
}
|
|
613
791
|
|
|
792
|
+
function snapshotAgeLabel(ageMs) {
|
|
793
|
+
if (ageMs < 60 * 1_000) return "now";
|
|
794
|
+
const minutes = Math.floor(ageMs / (60 * 1_000));
|
|
795
|
+
if (minutes < 60) return `${minutes}m old`;
|
|
796
|
+
const hours = Math.floor(minutes / 60);
|
|
797
|
+
if (hours < 24) return `${hours}h old`;
|
|
798
|
+
return `${Math.floor(hours / 24)}d old`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function snapshotFreshness(snapshot = {}, nowMs = Date.now()) {
|
|
802
|
+
const generatedAtMs = typeof snapshot.generatedAt === "string"
|
|
803
|
+
? Date.parse(snapshot.generatedAt)
|
|
804
|
+
: NaN;
|
|
805
|
+
if (
|
|
806
|
+
!Number.isFinite(generatedAtMs) ||
|
|
807
|
+
!Number.isFinite(nowMs) ||
|
|
808
|
+
generatedAtMs > nowMs
|
|
809
|
+
) {
|
|
810
|
+
return { status: "unknown", ageLabel: "age unknown" };
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
status: snapshotCacheIsFresh(generatedAtMs, nowMs) ? "fresh" : "stale",
|
|
814
|
+
ageLabel: snapshotAgeLabel(nowMs - generatedAtMs),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
614
818
|
async function loadSnapshot(options) {
|
|
615
819
|
if (options.refresh) {
|
|
616
820
|
return refreshSnapshot(options);
|
|
617
821
|
}
|
|
618
822
|
if (!existsSync(options.input)) {
|
|
619
823
|
if (options.inputExplicit || !options.autoRefresh) {
|
|
620
|
-
throw new Error(
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Snapshot not found: ${safeDisplayLabel(options.input, "snapshot")}`,
|
|
826
|
+
);
|
|
621
827
|
}
|
|
622
828
|
return refreshSnapshot(options);
|
|
623
829
|
}
|
|
@@ -625,48 +831,186 @@ async function loadSnapshot(options) {
|
|
|
625
831
|
return readSnapshot(options.input);
|
|
626
832
|
}
|
|
627
833
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
834
|
+
let snapshotStat;
|
|
835
|
+
try {
|
|
836
|
+
snapshotStat = await stat(options.input);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`Could not inspect snapshot ${safeDisplayLabel(options.input, "snapshot")}: ${safeErrorMessage(error, [options.input])}`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
if (snapshotCacheIsFresh(snapshotStat.mtimeMs)) {
|
|
843
|
+
return readSnapshot(options.input);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const { latestSourceModifiedAt } = await import("../lib/token-ledger-importer.mjs");
|
|
847
|
+
let latestSourceMtimeMs;
|
|
848
|
+
try {
|
|
849
|
+
latestSourceMtimeMs = await latestSourceModifiedAt(
|
|
850
|
+
options.codexHome,
|
|
851
|
+
options.includeArchived,
|
|
852
|
+
);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`Could not inspect local Codex source: ${safeErrorMessage(error, [options.codexHome])}`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (snapshotNeedsRefresh(snapshotStat.mtimeMs, latestSourceMtimeMs)) {
|
|
644
859
|
return refreshSnapshot(options);
|
|
645
860
|
}
|
|
646
|
-
return
|
|
861
|
+
return readSnapshot(options.input);
|
|
647
862
|
}
|
|
648
863
|
|
|
649
|
-
function render(options, snapshot, bounds, events, rows, allRows) {
|
|
650
|
-
|
|
864
|
+
function render(options, snapshot, bounds, events, rows, allRows, freshness) {
|
|
865
|
+
if (options.view === "trend") {
|
|
866
|
+
const trend = buildUsageTrend(snapshot, bounds);
|
|
867
|
+
if (options.image) {
|
|
868
|
+
return renderTrendImage({
|
|
869
|
+
snapshot,
|
|
870
|
+
bounds,
|
|
871
|
+
trend,
|
|
872
|
+
days: options.trendDays,
|
|
873
|
+
options,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return renderTrendCombo({
|
|
877
|
+
snapshot,
|
|
878
|
+
bounds,
|
|
879
|
+
trend,
|
|
880
|
+
days: options.trendDays,
|
|
881
|
+
options,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (!options.legacyPlot) {
|
|
885
|
+
return renderTerminal({
|
|
886
|
+
options,
|
|
887
|
+
snapshot,
|
|
888
|
+
snapshotFreshness: freshness,
|
|
889
|
+
bounds,
|
|
890
|
+
events,
|
|
891
|
+
rows,
|
|
892
|
+
allRows,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
const enabled = !options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY);
|
|
896
|
+
const summary = totalSummary(events);
|
|
897
|
+
const totalTokens = summary.totalTokens;
|
|
898
|
+
const dateLabel = options.range === "rolling24h"
|
|
899
|
+
? "last 24 hours"
|
|
900
|
+
: options.range === "week"
|
|
901
|
+
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
902
|
+
: new Intl.DateTimeFormat("en-US", {
|
|
903
|
+
timeZone: bounds.timeZone,
|
|
904
|
+
weekday: "short",
|
|
905
|
+
month: "short",
|
|
906
|
+
day: "numeric",
|
|
907
|
+
year: "numeric",
|
|
908
|
+
}).format(bounds.start);
|
|
909
|
+
const unit = chartUnit(rows[0]?.totalTokens ?? 0);
|
|
910
|
+
const shares = rows.map((row) =>
|
|
911
|
+
totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0,
|
|
912
|
+
);
|
|
913
|
+
const chart = runYouPlot(rows, options, dateLabel, unit).trimEnd();
|
|
914
|
+
|
|
915
|
+
const header = [
|
|
916
|
+
`Token Ledger · ${dateLabel} · ${bounds.timeZone}`,
|
|
917
|
+
`${compact(totalTokens)} tokens · ${summary.threadIds.size.toLocaleString()} threads · ${events.length.toLocaleString()} calls · ${compact(summary.outputTokens)} output`,
|
|
918
|
+
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
919
|
+
"",
|
|
920
|
+
chart,
|
|
921
|
+
"",
|
|
922
|
+
`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)}`,
|
|
923
|
+
];
|
|
924
|
+
|
|
925
|
+
const details = rows.map((row, index) => {
|
|
926
|
+
const knownCreditShare =
|
|
927
|
+
summary.rateCardCredits > 0 && row.rateCardCredits > 0
|
|
928
|
+
? ` · ${percent((row.rateCardCredits / summary.rateCardCredits) * 100)} credits`
|
|
929
|
+
: "";
|
|
930
|
+
return `${String(index + 1).padStart(2, " ")} ${row.displayProject} · ${compact(row.totalTokens)} · ${percent(shares[index])} · ${row.threads.toLocaleString()} threads${knownCreditShare}\n ${modelMix(row, enabled)}`;
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
return `${header.join("\n")}\n\n${details.join("\n")}`;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function boundsForOptions(options, now = new Date()) {
|
|
937
|
+
if (options.view === "trend") {
|
|
938
|
+
return multiDayBounds(options.date, options.timeZone, options.trendDays);
|
|
939
|
+
}
|
|
940
|
+
if (options.range === "week") {
|
|
941
|
+
return weekBounds(options.date, options.timeZone);
|
|
942
|
+
}
|
|
943
|
+
if (options.range === "rolling24h") {
|
|
944
|
+
return rolling24hBounds(now, options.timeZone);
|
|
945
|
+
}
|
|
946
|
+
return dayBounds(options.date, options.timeZone);
|
|
651
947
|
}
|
|
652
948
|
|
|
653
|
-
|
|
654
|
-
if (options.range
|
|
949
|
+
function rangeDescription(options, bounds) {
|
|
950
|
+
if (options.range === "rolling24h") return "the last 24 hours";
|
|
951
|
+
if (bounds.startDateString && bounds.endDateString) {
|
|
952
|
+
return `${bounds.startDateString} through ${bounds.endDateString}`;
|
|
953
|
+
}
|
|
954
|
+
return bounds.dateString;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export async function run(options, { nowMs } = {}) {
|
|
958
|
+
const hasInjectedNow = nowMs !== undefined;
|
|
959
|
+
const now = new Date(hasInjectedNow ? nowMs : Date.now());
|
|
960
|
+
const bounds = boundsForOptions(options, now);
|
|
655
961
|
const snapshot = await loadSnapshot(options);
|
|
656
|
-
const bounds = boundsForOptions(options, snapshot);
|
|
657
962
|
const events = filterDayEvents(snapshot, bounds);
|
|
658
963
|
if (events.length === 0) {
|
|
659
|
-
return
|
|
964
|
+
return [
|
|
965
|
+
`No model-call events found for ${rangeDescription(options, bounds)} (${bounds.timeZone}).`,
|
|
966
|
+
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
967
|
+
].join("\n");
|
|
660
968
|
}
|
|
661
969
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
662
970
|
const rows = allRows.slice(0, options.top);
|
|
663
|
-
|
|
971
|
+
const writingImage = options.view === "trend" && options.image;
|
|
972
|
+
const outputPath = writingImage
|
|
973
|
+
? options.imageOutput ??
|
|
974
|
+
resolve(
|
|
975
|
+
process.cwd(),
|
|
976
|
+
`token-ledger-${options.report ? "report" : "trend"}-${options.trendDays}d.png`,
|
|
977
|
+
)
|
|
978
|
+
: null;
|
|
979
|
+
const imageLabel = options.report ? "report" : "trend image";
|
|
980
|
+
if (writingImage) {
|
|
981
|
+
process.stderr.write(`Token Ledger: generating ${imageLabel} PNG…\n`);
|
|
982
|
+
}
|
|
983
|
+
const output = render(
|
|
984
|
+
options,
|
|
985
|
+
snapshot,
|
|
986
|
+
bounds,
|
|
987
|
+
events,
|
|
988
|
+
rows,
|
|
989
|
+
allRows,
|
|
990
|
+
snapshotFreshness(
|
|
991
|
+
snapshot,
|
|
992
|
+
hasInjectedNow ? now.getTime() : Date.now(),
|
|
993
|
+
),
|
|
994
|
+
);
|
|
995
|
+
if (writingImage) {
|
|
996
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
997
|
+
process.stderr.write(`Token Ledger: encoding ${imageLabel} PNG…\n`);
|
|
998
|
+
await writeTrendPng(output, outputPath);
|
|
999
|
+
process.stderr.write(`Token Ledger: finished ${imageLabel} PNG.\n`);
|
|
1000
|
+
return [
|
|
1001
|
+
`Wrote ${options.report ? "report" : "trend image"}: ${outputPath}`,
|
|
1002
|
+
`Range: ${bounds.startDateString} through ${bounds.endDateString} (${bounds.timeZone})`,
|
|
1003
|
+
].join("\n");
|
|
1004
|
+
}
|
|
1005
|
+
return output;
|
|
664
1006
|
}
|
|
665
1007
|
|
|
666
1008
|
function shouldUseInteractive(options) {
|
|
667
1009
|
return Boolean(
|
|
668
1010
|
!options.static &&
|
|
1011
|
+
options.view !== "trend" &&
|
|
669
1012
|
!options.plain &&
|
|
1013
|
+
!options.legacyPlot &&
|
|
670
1014
|
!process.env.NO_COLOR &&
|
|
671
1015
|
process.stdin.isTTY &&
|
|
672
1016
|
process.stdout.isTTY,
|
|
@@ -674,18 +1018,22 @@ function shouldUseInteractive(options) {
|
|
|
674
1018
|
}
|
|
675
1019
|
|
|
676
1020
|
async function runInteractive(options) {
|
|
677
|
-
|
|
1021
|
+
const bounds = boundsForOptions(options);
|
|
678
1022
|
const snapshot = await loadSnapshot(options);
|
|
679
|
-
const bounds = boundsForOptions(options, snapshot);
|
|
680
1023
|
const events = filterDayEvents(snapshot, bounds);
|
|
681
1024
|
if (events.length === 0) {
|
|
682
|
-
process.stdout.write(
|
|
1025
|
+
process.stdout.write([
|
|
1026
|
+
`No model-call events found for ${rangeDescription(options, bounds)} (${bounds.timeZone}).`,
|
|
1027
|
+
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
1028
|
+
"",
|
|
1029
|
+
].join("\n"));
|
|
683
1030
|
return;
|
|
684
1031
|
}
|
|
685
1032
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
686
1033
|
await startInteractive({
|
|
687
1034
|
options,
|
|
688
1035
|
snapshot,
|
|
1036
|
+
snapshotFreshness: snapshotFreshness(snapshot),
|
|
689
1037
|
bounds,
|
|
690
1038
|
events,
|
|
691
1039
|
rows: allRows.slice(0, options.top),
|
|
@@ -701,18 +1049,18 @@ async function main() {
|
|
|
701
1049
|
process.stdout.write(`${usage()}\n`);
|
|
702
1050
|
return;
|
|
703
1051
|
}
|
|
704
|
-
if (options.version) {
|
|
705
|
-
process.stdout.write(`${VERSION}\n`);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
1052
|
if (shouldUseInteractive(options)) {
|
|
709
1053
|
await runInteractive(options);
|
|
710
1054
|
} else {
|
|
711
|
-
process.stdout.write(`${await run(
|
|
1055
|
+
process.stdout.write(`${await run(options)}\n`);
|
|
712
1056
|
}
|
|
713
1057
|
} catch (error) {
|
|
714
1058
|
process.stderr.write(
|
|
715
|
-
`Token Ledger CLI failed: ${
|
|
1059
|
+
`Token Ledger CLI failed: ${safeErrorMessage(error, [
|
|
1060
|
+
options?.input,
|
|
1061
|
+
options?.codexHome,
|
|
1062
|
+
options?.imageOutput,
|
|
1063
|
+
])}\n\n${usage()}\n`,
|
|
716
1064
|
);
|
|
717
1065
|
process.exitCode = 1;
|
|
718
1066
|
}
|