tledger 0.2.0 → 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 +25 -3
- package/bin/token-ledger-controls.mjs +24 -0
- package/bin/token-ledger-terminal.mjs +57 -13
- package/bin/token-ledger-tui.mjs +13 -11
- package/bin/token-ledger.mjs +255 -67
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -46,6 +46,8 @@ is interactive; press `q` or `esc` to exit.
|
|
|
46
46
|
Other common views:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
+
tledger 1d
|
|
50
|
+
tledger 1d --static
|
|
49
51
|
tledger day 2026-08-05
|
|
50
52
|
tledger week --top 5
|
|
51
53
|
tledger week --static
|
|
@@ -54,6 +56,10 @@ tledger trend 7d --image --image-output artifacts/token-ledger-trend-7d.png
|
|
|
54
56
|
tledger report 7d
|
|
55
57
|
```
|
|
56
58
|
|
|
59
|
+
`tledger 1d` shows the `TOKENS BY PROJECT` breakdown for the rolling 24 hours
|
|
60
|
+
ending when the command starts. It is different from `tledger day today`, which
|
|
61
|
+
covers the current calendar day from local midnight.
|
|
62
|
+
|
|
57
63
|
`tledger report [7d|14d|30d]` is the one-step report output: it writes the
|
|
58
64
|
dashboard PNG (identical to `trend --image`) to
|
|
59
65
|
`token-ledger-report-<period>.png` in the current directory. It accepts the
|
|
@@ -107,6 +113,12 @@ The first refresh may scan historical rollout files; later runs use the cache
|
|
|
107
113
|
for one hour before checking source freshness again. Use `--refresh` when you
|
|
108
114
|
need to force an immediate rebuild.
|
|
109
115
|
|
|
116
|
+
The `1d` project dashboard shows a compact snapshot-age line such as
|
|
117
|
+
`SNAPSHOT · fresh · 12m old`. `fresh` means the snapshot is within the
|
|
118
|
+
one-hour cache window, `stale` means it is older, and `age unknown` means the
|
|
119
|
+
snapshot has no usable capture-time metadata. The indicator does not print a
|
|
120
|
+
local path or trigger another source scan.
|
|
121
|
+
|
|
110
122
|
Useful overrides:
|
|
111
123
|
|
|
112
124
|
```bash
|
|
@@ -139,19 +151,29 @@ writes its privacy-reduced cache to:
|
|
|
139
151
|
|
|
140
152
|
The collector does not export message bodies, reasoning text, tool arguments or
|
|
141
153
|
results, credentials, file contents, or full local paths. Display titles may
|
|
142
|
-
contain user-written text.
|
|
154
|
+
contain user-written text. CLI errors and empty-state source labels show only a
|
|
155
|
+
safe filename label, not an absolute input or source path. When a PNG or report
|
|
156
|
+
is written, the explicit output path is reported so you can find the file. The
|
|
157
|
+
CLI makes no network requests.
|
|
143
158
|
|
|
144
159
|
## Keyboard controls
|
|
145
160
|
|
|
146
161
|
In the interactive dashboard:
|
|
147
162
|
|
|
148
163
|
- `↑` / `↓` or `j` / `k` moves between projects.
|
|
149
|
-
- `q` or `
|
|
164
|
+
- `q`, `Q`, `Esc`, or `Ctrl-C` exits.
|
|
165
|
+
- Enter does not inspect a project, and `d` / `w` / `m` do not change the
|
|
166
|
+
range; choose the desired range in the command instead.
|
|
150
167
|
|
|
151
168
|
## Verify from source
|
|
152
169
|
|
|
153
170
|
```bash
|
|
154
171
|
npm test
|
|
155
172
|
npm run lint
|
|
156
|
-
npm
|
|
173
|
+
npm run verify:release
|
|
174
|
+
npm pack --dry-run --json
|
|
157
175
|
```
|
|
176
|
+
|
|
177
|
+
`npm run verify:release` packs the allowlisted artifact, installs that tarball
|
|
178
|
+
in a clean temporary directory with no network or Codex data access, and runs
|
|
179
|
+
the installed `tledger --help` and synthetic `tledger 1d --static` smoke checks.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const INTERACTIVE_KEY_INPUTS = Object.freeze({
|
|
2
|
+
up: Object.freeze(["\u001b[A", "k"]),
|
|
3
|
+
down: Object.freeze(["\u001b[B", "j"]),
|
|
4
|
+
quit: Object.freeze(["q", "Q", "\u0003", "\u001b"]),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const INTERACTIVE_FOOTER = Object.freeze({
|
|
8
|
+
ascii: "[j/k] select [q/Q/esc/ctrl-c] quit",
|
|
9
|
+
unicode: "[↑↓/j/k] select [q/Q/esc/ctrl-c] quit",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const INTERACTIVE_HELP = "↑/↓ or j/k move • q/Q, esc, or ctrl-c quit";
|
|
13
|
+
|
|
14
|
+
export function actionFor(input) {
|
|
15
|
+
const value = String(input);
|
|
16
|
+
if (value.includes(INTERACTIVE_KEY_INPUTS.up[0]) || value === INTERACTIVE_KEY_INPUTS.up[1]) {
|
|
17
|
+
return "up";
|
|
18
|
+
}
|
|
19
|
+
if (value.includes(INTERACTIVE_KEY_INPUTS.down[0]) || value === INTERACTIVE_KEY_INPUTS.down[1]) {
|
|
20
|
+
return "down";
|
|
21
|
+
}
|
|
22
|
+
if (INTERACTIVE_KEY_INPUTS.quit.includes(value)) return "quit";
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
normalizeQuotaTimeline,
|
|
3
3
|
weeklyQuotaObservations,
|
|
4
4
|
} from "./token-ledger-trend.mjs";
|
|
5
|
+
import {
|
|
6
|
+
INTERACTIVE_FOOTER,
|
|
7
|
+
INTERACTIVE_HELP,
|
|
8
|
+
} from "./token-ledger-controls.mjs";
|
|
5
9
|
|
|
6
10
|
const RESET = "\u001b[0m";
|
|
7
11
|
const PRIMARY_STYLE = [38, 2, 255, 255, 255];
|
|
@@ -128,6 +132,7 @@ function displayProject(row) {
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
function dateLabel(bounds, range = "day") {
|
|
135
|
+
if (range === "rolling24h") return "LAST 24 HOURS";
|
|
131
136
|
if (range === "week" && bounds.startDateString && bounds.endDateString) {
|
|
132
137
|
const startParts = bounds.startDateString.split("-").map(Number);
|
|
133
138
|
const endParts = bounds.endDateString.split("-").map(Number);
|
|
@@ -496,10 +501,22 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
|
|
|
496
501
|
return [top, ...body, bottom];
|
|
497
502
|
}
|
|
498
503
|
|
|
499
|
-
function
|
|
504
|
+
function snapshotLine(freshness, enabled) {
|
|
505
|
+
const detail = freshness?.status === "fresh" || freshness?.status === "stale"
|
|
506
|
+
? `${freshness.status} · ${freshness.ageLabel}`
|
|
507
|
+
: "age unknown";
|
|
508
|
+
return `${colorize("SNAPSHOT", ACCENT_STYLE, enabled)} ${colorize("·", SECONDARY_STYLE, enabled)} ${colorize(detail, SECONDARY_STYLE, enabled)}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
|
|
500
512
|
const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
|
|
501
513
|
const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
|
|
502
|
-
const
|
|
514
|
+
const modeLabel = options.range === "rolling24h"
|
|
515
|
+
? "24 HOURS"
|
|
516
|
+
: options.range === "week"
|
|
517
|
+
? "7 DAYS"
|
|
518
|
+
: "DAY";
|
|
519
|
+
const mode = colorize(modeLabel, [1, ...ACCENT_STYLE], enabled);
|
|
503
520
|
const metric = (value, label) =>
|
|
504
521
|
`${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, SECONDARY_STYLE, enabled)}`;
|
|
505
522
|
const separator = colorize("·", SECONDARY_STYLE, enabled);
|
|
@@ -515,13 +532,19 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
515
532
|
metric(stats.projectCount.toLocaleString("en-US"), "PROJECTS"),
|
|
516
533
|
].join(join);
|
|
517
534
|
if (visibleLength(fullLine) < frameWidth) {
|
|
518
|
-
|
|
535
|
+
const lines = [alignHeader(fullLine)];
|
|
536
|
+
if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
|
|
537
|
+
return lines;
|
|
519
538
|
}
|
|
520
539
|
|
|
521
540
|
const compactDate = dateLabel(bounds, options.range)
|
|
522
541
|
.replace(/ 20\d{2}$/, "")
|
|
523
542
|
.replace(" – ", "–");
|
|
524
|
-
const compactMode = options.range === "
|
|
543
|
+
const compactMode = options.range === "rolling24h"
|
|
544
|
+
? "24H"
|
|
545
|
+
: options.range === "week"
|
|
546
|
+
? "7D"
|
|
547
|
+
: "DAY";
|
|
525
548
|
const compactLine = [
|
|
526
549
|
left,
|
|
527
550
|
colorize(compactDate, TEXT_STYLE, enabled),
|
|
@@ -532,7 +555,9 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
532
555
|
metric(stats.projectCount.toLocaleString("en-US"), "P"),
|
|
533
556
|
].join(join);
|
|
534
557
|
if (visibleLength(compactLine) < frameWidth) {
|
|
535
|
-
|
|
558
|
+
const lines = [alignHeader(compactLine)];
|
|
559
|
+
if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
|
|
560
|
+
return lines;
|
|
536
561
|
}
|
|
537
562
|
|
|
538
563
|
const minimalTitle = colorize(frameWidth >= 45 ? "LEDGER" : "L", TITLE_STYLE, enabled);
|
|
@@ -545,10 +570,20 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
545
570
|
compact(stats.threads),
|
|
546
571
|
compact(stats.projectCount),
|
|
547
572
|
].join(" ");
|
|
548
|
-
|
|
573
|
+
const lines = [alignHeader(minimalLine)];
|
|
574
|
+
if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
|
|
575
|
+
return lines;
|
|
549
576
|
}
|
|
550
577
|
|
|
551
|
-
export function renderTerminal({
|
|
578
|
+
export function renderTerminal({
|
|
579
|
+
options,
|
|
580
|
+
snapshot,
|
|
581
|
+
snapshotFreshness,
|
|
582
|
+
bounds,
|
|
583
|
+
events,
|
|
584
|
+
rows,
|
|
585
|
+
allRows,
|
|
586
|
+
}) {
|
|
552
587
|
const enabled = colorsEnabled(options);
|
|
553
588
|
const stats = summary(events);
|
|
554
589
|
const quota = quotaCycleSummary(snapshot, events);
|
|
@@ -561,7 +596,7 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
|
|
|
561
596
|
const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
|
|
562
597
|
const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
|
|
563
598
|
const lines = [
|
|
564
|
-
...headerLines(stats, bounds, frameWidth, options, enabled),
|
|
599
|
+
...headerLines(stats, bounds, frameWidth, options, enabled, snapshotFreshness),
|
|
565
600
|
...panel(left, right, leftWidth, sideWidth, enabled, options.ascii),
|
|
566
601
|
];
|
|
567
602
|
if (!sideBySide) {
|
|
@@ -571,9 +606,7 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
|
|
|
571
606
|
lines.push("");
|
|
572
607
|
lines.push(
|
|
573
608
|
colorize(
|
|
574
|
-
options.ascii
|
|
575
|
-
? "[j/k] select [enter] inspect [d/w/m] range [q] quit"
|
|
576
|
-
: "[↑↓] select [enter] inspect [d/w/m] range [q] quit",
|
|
609
|
+
options.ascii ? INTERACTIVE_FOOTER.ascii : INTERACTIVE_FOOTER.unicode,
|
|
577
610
|
SECONDARY_STYLE,
|
|
578
611
|
enabled,
|
|
579
612
|
),
|
|
@@ -591,7 +624,17 @@ function paintFullscreenLine(line, width, background, enabled) {
|
|
|
591
624
|
return `${background}${restored}${RESET}`;
|
|
592
625
|
}
|
|
593
626
|
|
|
594
|
-
export function renderFullscreen({
|
|
627
|
+
export function renderFullscreen({
|
|
628
|
+
options,
|
|
629
|
+
snapshot,
|
|
630
|
+
snapshotFreshness,
|
|
631
|
+
bounds,
|
|
632
|
+
events,
|
|
633
|
+
rows,
|
|
634
|
+
allRows,
|
|
635
|
+
width,
|
|
636
|
+
height,
|
|
637
|
+
}) {
|
|
595
638
|
const enabled = options.forceColor ?? colorsEnabled(options);
|
|
596
639
|
const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
|
|
597
640
|
const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
|
|
@@ -606,6 +649,7 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
|
|
|
606
649
|
width: frameWidth + 2,
|
|
607
650
|
},
|
|
608
651
|
snapshot,
|
|
652
|
+
snapshotFreshness,
|
|
609
653
|
bounds,
|
|
610
654
|
events,
|
|
611
655
|
rows,
|
|
@@ -615,7 +659,7 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
|
|
|
615
659
|
staticLines.pop();
|
|
616
660
|
if (staticLines.at(-1) === "") staticLines.pop();
|
|
617
661
|
const summaryLine = staticLines.shift() ?? "";
|
|
618
|
-
const help = colorize(
|
|
662
|
+
const help = colorize(INTERACTIVE_HELP, SECONDARY_STYLE, enabled);
|
|
619
663
|
const content = [
|
|
620
664
|
{ line: summaryLine, background: SCREEN_BASE },
|
|
621
665
|
...staticLines.map((line) => ({ line, background: PANEL_BASE })),
|
package/bin/token-ledger-tui.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { renderFullscreen, SCREEN_BASE } from "./token-ledger-terminal.mjs";
|
|
2
|
+
import { actionFor } from "./token-ledger-controls.mjs";
|
|
3
|
+
|
|
4
|
+
export { actionFor };
|
|
2
5
|
|
|
3
6
|
const ENTER_ALT_SCREEN = "\u001b[?1049h";
|
|
4
7
|
const EXIT_ALT_SCREEN = "\u001b[?1049l";
|
|
@@ -7,17 +10,16 @@ const SHOW_CURSOR = "\u001b[?25h";
|
|
|
7
10
|
const CLEAR_SCREEN = "\u001b[2J\u001b[H";
|
|
8
11
|
const RESET = "\u001b[0m";
|
|
9
12
|
|
|
10
|
-
function actionFor(input) {
|
|
11
|
-
const value = String(input);
|
|
12
|
-
if (value.includes("\u001b[A") || value === "k") return "up";
|
|
13
|
-
if (value.includes("\u001b[B") || value === "j") return "down";
|
|
14
|
-
if (value === "q" || value === "Q" || value === "\u0003" || value === "\u001b") return "quit";
|
|
15
|
-
if (value === "\r" || value === "\n") return "inspect";
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
13
|
export function startInteractive(view) {
|
|
20
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
options,
|
|
16
|
+
snapshot,
|
|
17
|
+
snapshotFreshness,
|
|
18
|
+
bounds,
|
|
19
|
+
events,
|
|
20
|
+
rows,
|
|
21
|
+
allRows,
|
|
22
|
+
} = view;
|
|
21
23
|
const stdin = process.stdin;
|
|
22
24
|
const stdout = process.stdout;
|
|
23
25
|
if (!stdin.isTTY || !stdout.isTTY || typeof stdin.setRawMode !== "function") {
|
|
@@ -34,6 +36,7 @@ export function startInteractive(view) {
|
|
|
34
36
|
const screen = renderFullscreen({
|
|
35
37
|
options: { ...options, forceColor: true, selectedIndex },
|
|
36
38
|
snapshot,
|
|
39
|
+
snapshotFreshness,
|
|
37
40
|
bounds,
|
|
38
41
|
events,
|
|
39
42
|
rows,
|
|
@@ -74,7 +77,6 @@ export function startInteractive(view) {
|
|
|
74
77
|
draw();
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
|
-
if (action === "inspect") draw();
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
stdin.setRawMode(true);
|
package/bin/token-ledger.mjs
CHANGED
|
@@ -31,6 +31,7 @@ export const DEFAULT_SNAPSHOT = resolve(
|
|
|
31
31
|
const DEFAULT_TOP = 10;
|
|
32
32
|
const DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
33
33
|
export const SNAPSHOT_CACHE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
34
|
+
export const ROLLING_24_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
34
35
|
const ANSI_RESET = "\u001b[0m";
|
|
35
36
|
const MODEL_COLORS = {
|
|
36
37
|
sol: TERMINAL_MODEL_COLORS.sol,
|
|
@@ -45,6 +46,7 @@ function usage() {
|
|
|
45
46
|
return `Token Ledger terminal usage
|
|
46
47
|
|
|
47
48
|
Usage:
|
|
49
|
+
tledger 1d Rolling 24-hour project breakdown (ends now)
|
|
48
50
|
tledger day <YYYY-MM-DD>
|
|
49
51
|
tledger week [end-day]
|
|
50
52
|
tledger trend [7d|14d|30d]
|
|
@@ -78,7 +80,8 @@ The report command writes the dashboard PNG (same as trend --image) to
|
|
|
78
80
|
token-ledger-report-<period>.png; use --image-output to choose the path.
|
|
79
81
|
|
|
80
82
|
The command reads a privacy-reduced Token Ledger snapshot. It never uploads
|
|
81
|
-
the snapshot or prints message bodies, tool payloads, credentials, or
|
|
83
|
+
the snapshot or prints message bodies, tool payloads, credentials, or local
|
|
84
|
+
input/source paths. Explicit PNG output paths are reported after writing.`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
function readOption(argv, index, name) {
|
|
@@ -90,14 +93,18 @@ function readOption(argv, index, name) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export function parseArgs(argv) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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";
|
|
98
104
|
const options = {
|
|
99
105
|
range: command,
|
|
100
106
|
view: command === "trend" ? "trend" : "projects",
|
|
107
|
+
rolling24h: rolling24hCommand,
|
|
101
108
|
report: argv[0] === "report",
|
|
102
109
|
trendDays: 7,
|
|
103
110
|
date: null,
|
|
@@ -123,7 +130,7 @@ export function parseArgs(argv) {
|
|
|
123
130
|
};
|
|
124
131
|
|
|
125
132
|
let trendPeriodSeen = false;
|
|
126
|
-
let index = ["day", "week", "trend", "report"].includes(argv[0]) ? 1 : 0;
|
|
133
|
+
let index = ["1d", "day", "week", "trend", "report"].includes(argv[0]) ? 1 : 0;
|
|
127
134
|
for (; index < argv.length; index += 1) {
|
|
128
135
|
const argument = argv[index];
|
|
129
136
|
if (argument === "--help" || argument === "-h") {
|
|
@@ -217,6 +224,19 @@ export function parseArgs(argv) {
|
|
|
217
224
|
index += 1;
|
|
218
225
|
} else if (argument === "--youplot") {
|
|
219
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;
|
|
220
240
|
} else if (!argument.startsWith("-") && options.view === "trend") {
|
|
221
241
|
if (trendPeriodSeen) {
|
|
222
242
|
throw new Error("Trend period can only be specified once.");
|
|
@@ -234,10 +254,13 @@ export function parseArgs(argv) {
|
|
|
234
254
|
}
|
|
235
255
|
|
|
236
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.");
|
|
259
|
+
}
|
|
237
260
|
if (!options.help && !options.date && (options.range === "week" || options.view === "trend")) {
|
|
238
261
|
options.date = "today";
|
|
239
262
|
}
|
|
240
|
-
if (!options.help && !options.date) {
|
|
263
|
+
if (!options.help && !options.date && !options.rolling24h) {
|
|
241
264
|
throw new Error("A day is required, for example: tledger day 2026-08-01");
|
|
242
265
|
}
|
|
243
266
|
if (!options.help && options.refresh && !options.autoRefresh) {
|
|
@@ -364,6 +387,20 @@ export function weekBounds(value, timeZone) {
|
|
|
364
387
|
};
|
|
365
388
|
}
|
|
366
389
|
|
|
390
|
+
export function rolling24hBounds(value = new Date(), timeZone = DEFAULT_TIME_ZONE) {
|
|
391
|
+
validateTimeZone(timeZone);
|
|
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.");
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
start: new Date(end.getTime() - ROLLING_24_HOURS_MS),
|
|
398
|
+
end,
|
|
399
|
+
timeZone,
|
|
400
|
+
rangeHours: 24,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
367
404
|
export function sanitizeTerminalText(value) {
|
|
368
405
|
return String(value ?? "")
|
|
369
406
|
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
@@ -378,6 +415,62 @@ function cleanLabel(value, fallback) {
|
|
|
378
415
|
return label || fallback;
|
|
379
416
|
}
|
|
380
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
|
+
|
|
381
474
|
function displayLabel(value) {
|
|
382
475
|
const label = cleanLabel(value, "Unlabelled activity");
|
|
383
476
|
if (label.length <= 30) return label;
|
|
@@ -582,7 +675,7 @@ function sourceLabel(snapshotPath, snapshot) {
|
|
|
582
675
|
timeStyle: "short",
|
|
583
676
|
})
|
|
584
677
|
: "unknown time";
|
|
585
|
-
return `${
|
|
678
|
+
return `${safeDisplayLabel(snapshotPath, "snapshot")} · captured ${generated}`;
|
|
586
679
|
}
|
|
587
680
|
|
|
588
681
|
function runYouPlot(rows, options, dateLabel, unit) {
|
|
@@ -627,43 +720,57 @@ function runYouPlot(rows, options, dateLabel, unit) {
|
|
|
627
720
|
}
|
|
628
721
|
|
|
629
722
|
async function readSnapshot(snapshotPath) {
|
|
723
|
+
const snapshotLabel = safeDisplayLabel(snapshotPath, "snapshot");
|
|
630
724
|
let parsed;
|
|
631
725
|
try {
|
|
632
726
|
parsed = JSON.parse(await readFile(snapshotPath, "utf8"));
|
|
633
727
|
} catch (error) {
|
|
634
728
|
if (error?.code === "ENOENT") {
|
|
635
|
-
throw new Error(`Snapshot not found: ${
|
|
729
|
+
throw new Error(`Snapshot not found: ${snapshotLabel}`);
|
|
636
730
|
}
|
|
637
|
-
throw new Error(
|
|
731
|
+
throw new Error(
|
|
732
|
+
`Could not read snapshot ${snapshotLabel}: ${safeErrorMessage(error, [snapshotPath])}`,
|
|
733
|
+
);
|
|
638
734
|
}
|
|
639
735
|
if (!parsed || !Array.isArray(parsed.events)) {
|
|
640
|
-
throw new Error(`Snapshot is missing its events array: ${
|
|
736
|
+
throw new Error(`Snapshot is missing its events array: ${snapshotLabel}`);
|
|
641
737
|
}
|
|
642
738
|
return parsed;
|
|
643
739
|
}
|
|
644
740
|
|
|
645
741
|
async function refreshSnapshot(options) {
|
|
646
742
|
if (!existsSync(options.codexHome)) {
|
|
647
|
-
throw new Error(
|
|
743
|
+
throw new Error(
|
|
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])}`,
|
|
772
|
+
);
|
|
648
773
|
}
|
|
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
774
|
}
|
|
668
775
|
|
|
669
776
|
export function snapshotNeedsRefresh(snapshotMtimeMs, latestJsonlMtimeMs) {
|
|
@@ -682,13 +789,41 @@ export function snapshotCacheIsFresh(
|
|
|
682
789
|
);
|
|
683
790
|
}
|
|
684
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
|
+
|
|
685
818
|
async function loadSnapshot(options) {
|
|
686
819
|
if (options.refresh) {
|
|
687
820
|
return refreshSnapshot(options);
|
|
688
821
|
}
|
|
689
822
|
if (!existsSync(options.input)) {
|
|
690
823
|
if (options.inputExplicit || !options.autoRefresh) {
|
|
691
|
-
throw new Error(
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Snapshot not found: ${safeDisplayLabel(options.input, "snapshot")}`,
|
|
826
|
+
);
|
|
692
827
|
}
|
|
693
828
|
return refreshSnapshot(options);
|
|
694
829
|
}
|
|
@@ -696,23 +831,37 @@ async function loadSnapshot(options) {
|
|
|
696
831
|
return readSnapshot(options.input);
|
|
697
832
|
}
|
|
698
833
|
|
|
699
|
-
|
|
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
|
+
}
|
|
700
842
|
if (snapshotCacheIsFresh(snapshotStat.mtimeMs)) {
|
|
701
843
|
return readSnapshot(options.input);
|
|
702
844
|
}
|
|
703
845
|
|
|
704
846
|
const { latestSourceModifiedAt } = await import("../lib/token-ledger-importer.mjs");
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
+
}
|
|
709
858
|
if (snapshotNeedsRefresh(snapshotStat.mtimeMs, latestSourceMtimeMs)) {
|
|
710
859
|
return refreshSnapshot(options);
|
|
711
860
|
}
|
|
712
861
|
return readSnapshot(options.input);
|
|
713
862
|
}
|
|
714
863
|
|
|
715
|
-
function render(options, snapshot, bounds, events, rows, allRows) {
|
|
864
|
+
function render(options, snapshot, bounds, events, rows, allRows, freshness) {
|
|
716
865
|
if (options.view === "trend") {
|
|
717
866
|
const trend = buildUsageTrend(snapshot, bounds);
|
|
718
867
|
if (options.image) {
|
|
@@ -733,20 +882,30 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
733
882
|
});
|
|
734
883
|
}
|
|
735
884
|
if (!options.legacyPlot) {
|
|
736
|
-
return renderTerminal({
|
|
885
|
+
return renderTerminal({
|
|
886
|
+
options,
|
|
887
|
+
snapshot,
|
|
888
|
+
snapshotFreshness: freshness,
|
|
889
|
+
bounds,
|
|
890
|
+
events,
|
|
891
|
+
rows,
|
|
892
|
+
allRows,
|
|
893
|
+
});
|
|
737
894
|
}
|
|
738
895
|
const enabled = !options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY);
|
|
739
896
|
const summary = totalSummary(events);
|
|
740
897
|
const totalTokens = summary.totalTokens;
|
|
741
|
-
const dateLabel = options.range === "
|
|
742
|
-
?
|
|
743
|
-
:
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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);
|
|
750
909
|
const unit = chartUnit(rows[0]?.totalTokens ?? 0);
|
|
751
910
|
const shares = rows.map((row) =>
|
|
752
911
|
totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0,
|
|
@@ -774,20 +933,36 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
774
933
|
return `${header.join("\n")}\n\n${details.join("\n")}`;
|
|
775
934
|
}
|
|
776
935
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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);
|
|
947
|
+
}
|
|
948
|
+
|
|
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);
|
|
783
961
|
const snapshot = await loadSnapshot(options);
|
|
784
962
|
const events = filterDayEvents(snapshot, bounds);
|
|
785
963
|
if (events.length === 0) {
|
|
786
|
-
const rangeDescription = options.range === "week"
|
|
787
|
-
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
788
|
-
: bounds.dateString;
|
|
789
964
|
return [
|
|
790
|
-
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
965
|
+
`No model-call events found for ${rangeDescription(options, bounds)} (${bounds.timeZone}).`,
|
|
791
966
|
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
792
967
|
].join("\n");
|
|
793
968
|
}
|
|
@@ -805,7 +980,18 @@ export async function run(options) {
|
|
|
805
980
|
if (writingImage) {
|
|
806
981
|
process.stderr.write(`Token Ledger: generating ${imageLabel} PNG…\n`);
|
|
807
982
|
}
|
|
808
|
-
const output = render(
|
|
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
|
+
);
|
|
809
995
|
if (writingImage) {
|
|
810
996
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
811
997
|
process.stderr.write(`Token Ledger: encoding ${imageLabel} PNG…\n`);
|
|
@@ -832,17 +1018,12 @@ function shouldUseInteractive(options) {
|
|
|
832
1018
|
}
|
|
833
1019
|
|
|
834
1020
|
async function runInteractive(options) {
|
|
835
|
-
const bounds = options
|
|
836
|
-
? weekBounds(options.date, options.timeZone)
|
|
837
|
-
: dayBounds(options.date, options.timeZone);
|
|
1021
|
+
const bounds = boundsForOptions(options);
|
|
838
1022
|
const snapshot = await loadSnapshot(options);
|
|
839
1023
|
const events = filterDayEvents(snapshot, bounds);
|
|
840
1024
|
if (events.length === 0) {
|
|
841
|
-
const rangeDescription = options.range === "week"
|
|
842
|
-
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
843
|
-
: bounds.dateString;
|
|
844
1025
|
process.stdout.write([
|
|
845
|
-
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
1026
|
+
`No model-call events found for ${rangeDescription(options, bounds)} (${bounds.timeZone}).`,
|
|
846
1027
|
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
847
1028
|
"",
|
|
848
1029
|
].join("\n"));
|
|
@@ -852,6 +1033,7 @@ async function runInteractive(options) {
|
|
|
852
1033
|
await startInteractive({
|
|
853
1034
|
options,
|
|
854
1035
|
snapshot,
|
|
1036
|
+
snapshotFreshness: snapshotFreshness(snapshot),
|
|
855
1037
|
bounds,
|
|
856
1038
|
events,
|
|
857
1039
|
rows: allRows.slice(0, options.top),
|
|
@@ -873,7 +1055,13 @@ async function main() {
|
|
|
873
1055
|
process.stdout.write(`${await run(options)}\n`);
|
|
874
1056
|
}
|
|
875
1057
|
} catch (error) {
|
|
876
|
-
process.stderr.write(
|
|
1058
|
+
process.stderr.write(
|
|
1059
|
+
`Token Ledger CLI failed: ${safeErrorMessage(error, [
|
|
1060
|
+
options?.input,
|
|
1061
|
+
options?.codexHome,
|
|
1062
|
+
options?.imageOutput,
|
|
1063
|
+
])}\n\n${usage()}\n`,
|
|
1064
|
+
);
|
|
877
1065
|
process.exitCode = 1;
|
|
878
1066
|
}
|
|
879
1067
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tledger",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A local-only terminal dashboard for Codex token usage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"usage:snapshot": "node lib/token-ledger-importer.mjs --output outputs/token-ledger-snapshot.json",
|
|
41
41
|
"usage:day": "node bin/token-ledger.mjs day",
|
|
42
42
|
"usage:week": "node bin/token-ledger.mjs week",
|
|
43
|
-
"
|
|
43
|
+
"verify:release": "node tools/verify-release.mjs",
|
|
44
|
+
"prepublishOnly": "npm test && npm run verify:release"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"eslint": "9.39.4"
|