tledger 0.1.3 → 0.1.4
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 +19 -7
- package/bin/token-ledger-terminal.mjs +120 -53
- package/bin/token-ledger.mjs +159 -31
- package/lib/token-ledger-collector.mjs +3 -13
- package/lib/token-ledger-models.mjs +113 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Token Ledger
|
|
2
2
|
|
|
3
3
|
Token Ledger is a lightweight, local-only terminal dashboard for Codex token
|
|
4
|
-
usage. It ranks projects,
|
|
5
|
-
context without sending usage data anywhere. Codex
|
|
6
|
-
separately with its token total, distinct-turn share, and
|
|
4
|
+
usage. It ranks projects, names every active model—including Daybreak Blue—and
|
|
5
|
+
shows cache and reset-cycle context without sending usage data anywhere. Codex
|
|
6
|
+
Auto Review is shown separately with its token total, distinct-turn share, and
|
|
7
|
+
cached-input share.
|
|
7
8
|
|
|
8
9
|
Token Ledger reads Codex history stored on the computer where it runs. It does
|
|
9
10
|
not sign in, fetch account-wide usage, or combine activity from other machines.
|
|
@@ -29,7 +30,7 @@ Token Ledger has no runtime npm dependencies and runs no installation script.
|
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
32
|
npm install --global tledger
|
|
32
|
-
tledger --
|
|
33
|
+
tledger --version
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
## Use
|
|
@@ -40,8 +41,15 @@ tledger week # same default, stated explicitly
|
|
|
40
41
|
tledger day # today
|
|
41
42
|
tledger day yesterday
|
|
42
43
|
tledger week 2026-08-05
|
|
44
|
+
tledger month # rolling 30-day window
|
|
45
|
+
tledger 90d # rolling 90-day window
|
|
46
|
+
tledger 90d 2026-08-05 # 90 days ending on a chosen day
|
|
47
|
+
tledger all # every dated event in the local snapshot
|
|
43
48
|
```
|
|
44
49
|
|
|
50
|
+
`week`, `month`, and custom ranges such as `90d` are inclusive rolling
|
|
51
|
+
calendar-day windows ending today unless an end day is supplied.
|
|
52
|
+
|
|
45
53
|
The local timezone and top 10 projects are selected automatically. The default
|
|
46
54
|
view is interactive in a terminal; use arrow keys or `j`/`k` to move and `q` or
|
|
47
55
|
Escape to exit. If the selected period is empty, Token Ledger reports the most
|
|
@@ -58,11 +66,13 @@ Useful options:
|
|
|
58
66
|
--codex-home <dir> Read another Codex data directory
|
|
59
67
|
--no-archived Skip archived sessions during collection
|
|
60
68
|
--raw-projects Keep singleton project labels separate
|
|
69
|
+
-anon Show Project 1, Project 2, etc. instead of project names
|
|
61
70
|
--static Print once instead of opening the interactive view
|
|
62
71
|
--plain Print once without ANSI color
|
|
63
72
|
--ascii Use ASCII bars
|
|
64
73
|
--width <40-200> Set the static layout width
|
|
65
74
|
--date <day> Set today, yesterday, or YYYY-MM-DD
|
|
75
|
+
-v, --version Show the installed version
|
|
66
76
|
--help Show complete CLI help
|
|
67
77
|
```
|
|
68
78
|
|
|
@@ -70,6 +80,7 @@ Useful options:
|
|
|
70
80
|
|
|
71
81
|
```bash
|
|
72
82
|
npm install --global tledger@latest
|
|
83
|
+
tledger --version
|
|
73
84
|
npm uninstall --global tledger
|
|
74
85
|
```
|
|
75
86
|
|
|
@@ -87,15 +98,16 @@ workers; unchanged runs read the existing cache. Token Ledger makes no network
|
|
|
87
98
|
requests and excludes
|
|
88
99
|
message bodies, reasoning text, tool payloads, credentials, and full local
|
|
89
100
|
paths from the cache. Project labels can still reveal local context, so keep
|
|
90
|
-
snapshots private unless you have reviewed them.
|
|
91
|
-
|
|
101
|
+
snapshots private unless you have reviewed them. Use `-anon` to hide project
|
|
102
|
+
names in terminal output; it does not rewrite the cached snapshot. Reset-cycle
|
|
103
|
+
burn is an estimate, not official quota or billing data.
|
|
92
104
|
|
|
93
105
|
## Develop
|
|
94
106
|
|
|
95
107
|
```bash
|
|
96
108
|
npm test
|
|
97
109
|
npm run demo
|
|
98
|
-
npm
|
|
110
|
+
npm run verify:release
|
|
99
111
|
```
|
|
100
112
|
|
|
101
113
|
## License
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
modelColorKey,
|
|
3
|
+
modelDisplayName,
|
|
4
|
+
} from "../lib/token-ledger-models.mjs";
|
|
5
|
+
|
|
1
6
|
const RESET = "\u001b[0m";
|
|
2
7
|
const DIM = [38, 5, 245];
|
|
3
8
|
const TEAL = [38, 5, 80];
|
|
@@ -6,6 +11,7 @@ export const MODEL_COLORS = {
|
|
|
6
11
|
luna: [38, 5, 80],
|
|
7
12
|
terra: [38, 5, 179],
|
|
8
13
|
gpt: [38, 5, 176],
|
|
14
|
+
daybreakBlue: [38, 5, 69],
|
|
9
15
|
autoReview: [38, 5, 153],
|
|
10
16
|
other: [38, 5, 60],
|
|
11
17
|
};
|
|
@@ -95,23 +101,11 @@ function plural(value, singular, pluralForm = `${singular}s`) {
|
|
|
95
101
|
|
|
96
102
|
function modelLabel(value) {
|
|
97
103
|
const model = sanitizeText(value) || "Unknown model";
|
|
98
|
-
|
|
99
|
-
if (lower === "codex-auto-review" || lower.startsWith("codex-auto-review-")) {
|
|
100
|
-
return "Auto Review";
|
|
101
|
-
}
|
|
102
|
-
if (lower.includes("sol")) return "Sol";
|
|
103
|
-
if (lower.includes("luna")) return "Luna";
|
|
104
|
-
if (lower.includes("terra")) return "Terra";
|
|
105
|
-
if (lower.includes("gpt-5.5") || lower.includes("gpt-5.4")) return "GPT";
|
|
106
|
-
return "Other";
|
|
104
|
+
return modelDisplayName(model);
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
function modelColor(model) {
|
|
110
|
-
|
|
111
|
-
.toLowerCase()
|
|
112
|
-
.replace(/[\s_-]+/g, "-");
|
|
113
|
-
if (key === "auto-review") return MODEL_COLORS.autoReview;
|
|
114
|
-
return MODEL_COLORS[key] ?? MODEL_COLORS.other;
|
|
108
|
+
return MODEL_COLORS[modelColorKey(model)] ?? MODEL_COLORS.other;
|
|
115
109
|
}
|
|
116
110
|
|
|
117
111
|
function usageTypeLabel(value) {
|
|
@@ -134,7 +128,7 @@ function displayProject(row) {
|
|
|
134
128
|
}
|
|
135
129
|
|
|
136
130
|
function dateLabel(bounds, range = "day") {
|
|
137
|
-
if (range
|
|
131
|
+
if (range !== "day" && bounds.rangeDays !== 1 && bounds.startDateString && bounds.endDateString) {
|
|
138
132
|
const startParts = bounds.startDateString.split("-").map(Number);
|
|
139
133
|
const endParts = bounds.endDateString.split("-").map(Number);
|
|
140
134
|
const monthNames = [
|
|
@@ -143,7 +137,12 @@ function dateLabel(bounds, range = "day") {
|
|
|
143
137
|
];
|
|
144
138
|
const start = `${monthNames[startParts[1] - 1]} ${String(startParts[2]).padStart(2, "0")}`;
|
|
145
139
|
const end = `${monthNames[endParts[1] - 1]} ${String(endParts[2]).padStart(2, "0")}`;
|
|
146
|
-
|
|
140
|
+
if (bounds.startDateString === bounds.endDateString) {
|
|
141
|
+
return `${end} ${endParts[0]}`;
|
|
142
|
+
}
|
|
143
|
+
return startParts[0] === endParts[0]
|
|
144
|
+
? `${start} – ${end} ${endParts[0]}`
|
|
145
|
+
: `${start} ${startParts[0]} – ${end} ${endParts[0]}`;
|
|
147
146
|
}
|
|
148
147
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
149
148
|
timeZone: bounds.timeZone,
|
|
@@ -160,6 +159,13 @@ function dateLabel(bounds, range = "day") {
|
|
|
160
159
|
return `${values.weekday} ${values.day} ${values.month} ${values.year}`.toUpperCase();
|
|
161
160
|
}
|
|
162
161
|
|
|
162
|
+
function rangeModeLabel(bounds, range = "day", compact = false) {
|
|
163
|
+
if (range === "all") return "ALL";
|
|
164
|
+
if (range === "day") return "DAY";
|
|
165
|
+
const days = bounds.rangeDays ?? 1;
|
|
166
|
+
return compact ? `${days}D` : `${days} ${days === 1 ? "DAY" : "DAYS"}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
163
169
|
function modelTotals(events) {
|
|
164
170
|
const totals = new Map();
|
|
165
171
|
for (const event of events) {
|
|
@@ -340,21 +346,11 @@ function summary(events) {
|
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
function modelLegendItems(models, totalTokens) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const key = ["Sol", "Luna", "Terra", "GPT", "Auto Review"].includes(model.model)
|
|
346
|
-
? model.model
|
|
347
|
-
: "Other";
|
|
348
|
-
known.set(key, (known.get(key) ?? 0) + model.totalTokens);
|
|
349
|
-
}
|
|
350
|
-
const order = ["Luna", "Sol", "Terra", "GPT"];
|
|
351
|
-
if ((known.get("Auto Review") ?? 0) > 0) order.push("Auto Review");
|
|
352
|
-
order.push("Other");
|
|
353
|
-
return order
|
|
349
|
+
return models
|
|
350
|
+
.filter((model) => model.totalTokens > 0)
|
|
354
351
|
.map((model) => ({
|
|
355
|
-
model,
|
|
356
|
-
|
|
357
|
-
share: totalTokens > 0 ? ((known.get(model) ?? 0) / totalTokens) * 100 : 0,
|
|
352
|
+
...model,
|
|
353
|
+
share: totalTokens > 0 ? (model.totalTokens / totalTokens) * 100 : 0,
|
|
358
354
|
}));
|
|
359
355
|
}
|
|
360
356
|
|
|
@@ -430,6 +426,7 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
|
|
|
430
426
|
panelWidth - labelWidth - shareWidth - totalWidth - 1 - rightPadding,
|
|
431
427
|
);
|
|
432
428
|
const maxTokens = allRows[0]?.totalTokens ?? 0;
|
|
429
|
+
const rowOffset = Number.isInteger(options.rowOffset) ? options.rowOffset : 0;
|
|
433
430
|
const lines = [];
|
|
434
431
|
const heading = colorize("TOKENS BY PROJECT", TEAL, enabled);
|
|
435
432
|
const barHeaderWidth = barWidth;
|
|
@@ -444,13 +441,14 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
|
|
|
444
441
|
lines.push(colorize("─".repeat(panelWidth), DIM, enabled));
|
|
445
442
|
|
|
446
443
|
for (const [index, row] of rows.entries()) {
|
|
444
|
+
const rankValue = rowOffset + index + 1;
|
|
447
445
|
const share = totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0;
|
|
448
446
|
const selected = !options.static && index === (options.selectedIndex ?? 0);
|
|
449
447
|
const caretValue = selected ? (options.ascii ? ">" : "▶") : " ";
|
|
450
|
-
const prefix = `${caretValue} ${
|
|
448
|
+
const prefix = `${caretValue} ${rankValue}. `;
|
|
451
449
|
const projectText = truncateText(displayProject(row), labelWidth - prefix.length);
|
|
452
450
|
const caret = selected ? colorize(caretValue, [1, ...TEAL], enabled) : caretValue;
|
|
453
|
-
const rank = colorize(`${
|
|
451
|
+
const rank = colorize(`${rankValue}.`, TITLE_STYLE, enabled);
|
|
454
452
|
const title = `${caret} ${rank} ${colorize(projectText, TITLE_STYLE, enabled)}`;
|
|
455
453
|
const label = fit(title, labelWidth);
|
|
456
454
|
const metrics = `${fit(compact(row.totalTokens), totalWidth, "right")}${fit(percent(share), shareWidth, "right")}${" ".repeat(rightPadding)}`;
|
|
@@ -483,15 +481,26 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
|
|
|
483
481
|
const heading = (value) => colorize(value, TEAL, enabled);
|
|
484
482
|
push(heading("MODEL MIX"));
|
|
485
483
|
if (!compactSidebar) push();
|
|
486
|
-
|
|
484
|
+
const modelShareWidth = 6;
|
|
485
|
+
const modelNameWidth = Math.max(1, contentWidth - modelShareWidth - 2);
|
|
486
|
+
const modelItems = modelLegendItems(stats.models, stats.totalTokens);
|
|
487
|
+
const modelLimit = Number.isInteger(options.modelLimit)
|
|
488
|
+
? Math.max(0, options.modelLimit)
|
|
489
|
+
: modelItems.length;
|
|
490
|
+
const visibleModelItems = modelItems.slice(0, modelLimit);
|
|
491
|
+
for (const item of visibleModelItems) {
|
|
487
492
|
const swatch = colorize("■", modelColor(item.model), enabled);
|
|
488
|
-
push(`${swatch} ${fit(item.model,
|
|
493
|
+
push(`${swatch} ${fit(item.model, modelNameWidth)}${fit(percent(item.share), modelShareWidth, "right")}`);
|
|
489
494
|
if (item.model === "Auto Review" && stats.autoReview.present) {
|
|
490
495
|
const turnLabel = stats.autoReview.turns === 1 ? "turn" : "turns";
|
|
491
496
|
push(` ${compact(stats.autoReview.turns)} ${turnLabel} · ${percent(stats.autoReview.turnShare)}`);
|
|
492
497
|
push(` ${compact(stats.autoReview.totalTokens)} · ${percent(stats.autoReview.cachedInputShare)} cached`);
|
|
493
498
|
}
|
|
494
499
|
}
|
|
500
|
+
const hiddenModelCount = modelItems.length - visibleModelItems.length;
|
|
501
|
+
if (hiddenModelCount > 0) {
|
|
502
|
+
push(colorize(`… ${plural(hiddenModelCount, "more model")}`, DIM, enabled));
|
|
503
|
+
}
|
|
495
504
|
if (!compactSidebar) push();
|
|
496
505
|
divider();
|
|
497
506
|
push(heading("USAGE TYPE · TOKENS"));
|
|
@@ -554,7 +563,7 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
|
|
|
554
563
|
function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
555
564
|
const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
|
|
556
565
|
const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
|
|
557
|
-
const mode = colorize(options.range
|
|
566
|
+
const mode = colorize(rangeModeLabel(bounds, options.range), [1, ...TEAL], enabled);
|
|
558
567
|
const metric = (value, label) =>
|
|
559
568
|
`${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, DIM, enabled)}`;
|
|
560
569
|
const separator = colorize("·", DIM, enabled);
|
|
@@ -573,10 +582,14 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
573
582
|
return [alignHeader(fullLine)];
|
|
574
583
|
}
|
|
575
584
|
|
|
576
|
-
const
|
|
577
|
-
.
|
|
578
|
-
|
|
579
|
-
|
|
585
|
+
const crossesCalendarYear =
|
|
586
|
+
bounds.startDateString?.slice(0, 4) !== bounds.endDateString?.slice(0, 4);
|
|
587
|
+
const compactDate = (
|
|
588
|
+
crossesCalendarYear
|
|
589
|
+
? dateLabel(bounds, options.range)
|
|
590
|
+
: dateLabel(bounds, options.range).replace(/ 20\d{2}$/, "")
|
|
591
|
+
).replace(" – ", "–");
|
|
592
|
+
const compactMode = rangeModeLabel(bounds, options.range, true);
|
|
580
593
|
const compactLine = [
|
|
581
594
|
left,
|
|
582
595
|
colorize(compactDate, TEXT_STYLE, enabled),
|
|
@@ -653,30 +666,84 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
|
|
|
653
666
|
const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
|
|
654
667
|
const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
|
|
655
668
|
const frameWidth = Math.max(38, Math.min(158, columns - 4));
|
|
669
|
+
const forceSideBySide = frameWidth >= 84;
|
|
670
|
+
const staticBudget = Math.max(0, screenHeight - 1);
|
|
671
|
+
let visibleRows = rows;
|
|
672
|
+
let fullscreenOptions = {
|
|
673
|
+
...options,
|
|
674
|
+
forceColor: enabled,
|
|
675
|
+
forceSideBySide,
|
|
676
|
+
highlight: false,
|
|
677
|
+
compactSidebar: true,
|
|
678
|
+
hideHelp: true,
|
|
679
|
+
width: frameWidth + 2,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
if (forceSideBySide && staticBudget >= 3) {
|
|
683
|
+
const stats = summary(events);
|
|
684
|
+
const quota = quotaCycleSummary(snapshot, events);
|
|
685
|
+
const panelContentBudget = Math.max(1, staticBudget - 3);
|
|
686
|
+
const visibleRowCount = Math.min(
|
|
687
|
+
rows.length,
|
|
688
|
+
Math.max(1, Math.floor((panelContentBudget - 2) / 2)),
|
|
689
|
+
);
|
|
690
|
+
const selectedIndex = Math.max(
|
|
691
|
+
0,
|
|
692
|
+
Math.min(rows.length - 1, Number(options.selectedIndex) || 0),
|
|
693
|
+
);
|
|
694
|
+
const rowOffset = Math.min(
|
|
695
|
+
Math.max(0, selectedIndex - visibleRowCount + 1),
|
|
696
|
+
Math.max(0, rows.length - visibleRowCount),
|
|
697
|
+
);
|
|
698
|
+
visibleRows = rows.slice(rowOffset, rowOffset + visibleRowCount);
|
|
699
|
+
|
|
700
|
+
const sideWidth = stats.autoReview.present ? 28 : 26;
|
|
701
|
+
let modelLimit = modelLegendItems(stats.models, stats.totalTokens).length;
|
|
702
|
+
while (
|
|
703
|
+
modelLimit > 0 &&
|
|
704
|
+
sidebarLines(
|
|
705
|
+
stats,
|
|
706
|
+
sideWidth,
|
|
707
|
+
enabled,
|
|
708
|
+
{ ...fullscreenOptions, modelLimit },
|
|
709
|
+
quota,
|
|
710
|
+
).length > panelContentBudget
|
|
711
|
+
) {
|
|
712
|
+
modelLimit -= 1;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
fullscreenOptions = {
|
|
716
|
+
...fullscreenOptions,
|
|
717
|
+
modelLimit,
|
|
718
|
+
rowOffset,
|
|
719
|
+
selectedIndex: selectedIndex - rowOffset,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
656
723
|
const staticOutput = renderTerminal({
|
|
657
|
-
options:
|
|
658
|
-
...options,
|
|
659
|
-
forceColor: enabled,
|
|
660
|
-
forceSideBySide: frameWidth >= 84,
|
|
661
|
-
highlight: false,
|
|
662
|
-
compactSidebar: true,
|
|
663
|
-
hideHelp: true,
|
|
664
|
-
width: frameWidth + 2,
|
|
665
|
-
},
|
|
724
|
+
options: fullscreenOptions,
|
|
666
725
|
snapshot,
|
|
667
726
|
bounds,
|
|
668
727
|
events,
|
|
669
|
-
rows,
|
|
728
|
+
rows: visibleRows,
|
|
670
729
|
allRows,
|
|
671
730
|
});
|
|
672
|
-
|
|
731
|
+
let staticLines = staticOutput.split("\n");
|
|
673
732
|
if (staticLines.at(-1) === "") staticLines.pop();
|
|
674
|
-
const summaryLine = staticLines.shift() ?? "";
|
|
675
733
|
const help = colorize("↑/↓ move • j/k move • q/esc quit", DIM, enabled);
|
|
734
|
+
const includeSpacer = staticLines.length + 2 <= screenHeight;
|
|
735
|
+
const availableStaticLines = Math.max(
|
|
736
|
+
0,
|
|
737
|
+
screenHeight - 1 - (includeSpacer ? 1 : 0),
|
|
738
|
+
);
|
|
739
|
+
staticLines = staticLines.slice(0, availableStaticLines);
|
|
740
|
+
const summaryLine = staticLines.shift();
|
|
676
741
|
const content = [
|
|
677
|
-
|
|
742
|
+
...(summaryLine === undefined
|
|
743
|
+
? []
|
|
744
|
+
: [{ line: summaryLine, background: SCREEN_BASE }]),
|
|
678
745
|
...staticLines.map((line) => ({ line, background: PANEL_BASE })),
|
|
679
|
-
{ line: "", background: SCREEN_BASE },
|
|
746
|
+
...(includeSpacer ? [{ line: "", background: SCREEN_BASE }] : []),
|
|
680
747
|
{ line: help, background: SCREEN_BASE },
|
|
681
748
|
];
|
|
682
749
|
const topPadding = Math.max(0, Math.floor((screenHeight - content.length) / 2));
|
package/bin/token-ledger.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import {
|
|
5
6
|
readFile,
|
|
6
7
|
stat,
|
|
@@ -11,6 +12,10 @@ import { fileURLToPath } from "node:url";
|
|
|
11
12
|
|
|
12
13
|
import { renderTerminal } from "./token-ledger-terminal.mjs";
|
|
13
14
|
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;
|
|
14
19
|
|
|
15
20
|
export const DEFAULT_SNAPSHOT = resolve(
|
|
16
21
|
homedir(),
|
|
@@ -18,6 +23,7 @@ export const DEFAULT_SNAPSHOT = resolve(
|
|
|
18
23
|
"token-ledger-snapshot.json",
|
|
19
24
|
);
|
|
20
25
|
const DEFAULT_TOP = 10;
|
|
26
|
+
const MAX_RANGE_DAYS = 100_000;
|
|
21
27
|
const DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
22
28
|
|
|
23
29
|
function usage() {
|
|
@@ -27,6 +33,16 @@ Usage:
|
|
|
27
33
|
tledger
|
|
28
34
|
tledger week [end-day]
|
|
29
35
|
tledger day [day]
|
|
36
|
+
tledger month [end-day]
|
|
37
|
+
tledger <number>d [end-day]
|
|
38
|
+
tledger all
|
|
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
|
|
30
46
|
|
|
31
47
|
Options:
|
|
32
48
|
--date <day> Date as YYYY-MM-DD, today, or yesterday
|
|
@@ -38,10 +54,12 @@ Options:
|
|
|
38
54
|
--top <number> Number of projects to show (default: 10)
|
|
39
55
|
--width <number> Terminal layout width in columns
|
|
40
56
|
--raw-projects Keep singleton thread labels instead of grouping them
|
|
57
|
+
-anon Replace project names with Project 1, Project 2, and so on
|
|
41
58
|
--no-archived Skip archived_sessions when refreshing
|
|
42
59
|
--plain Disable terminal colors
|
|
43
60
|
--ascii Use ASCII bars instead of Unicode blocks
|
|
44
61
|
--static Print once instead of opening the interactive dashboard
|
|
62
|
+
-v, --version Show the installed version
|
|
45
63
|
--help Show this help
|
|
46
64
|
|
|
47
65
|
The default view is the seven-day window ending today. Token Ledger never
|
|
@@ -56,14 +74,38 @@ function readOption(argv, index, name) {
|
|
|
56
74
|
return value;
|
|
57
75
|
}
|
|
58
76
|
|
|
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
|
+
|
|
59
97
|
export function parseArgs(argv) {
|
|
60
|
-
const
|
|
98
|
+
const requestedRange = rangeSpec(argv[0]);
|
|
99
|
+
const commandExplicit = Boolean(requestedRange);
|
|
61
100
|
if (argv[0] && !argv[0].startsWith("-") && !commandExplicit) {
|
|
62
|
-
throw new Error(
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Unknown command: ${argv[0]}. Use day, week, month, all, or a duration like 90d.`,
|
|
103
|
+
);
|
|
63
104
|
}
|
|
64
|
-
const command =
|
|
105
|
+
const command = requestedRange ?? { range: "week", rangeDays: 7 };
|
|
65
106
|
const options = {
|
|
66
|
-
range: command,
|
|
107
|
+
range: command.range,
|
|
108
|
+
rangeDays: command.rangeDays,
|
|
67
109
|
date: null,
|
|
68
110
|
input: DEFAULT_SNAPSHOT,
|
|
69
111
|
inputExplicit: false,
|
|
@@ -75,9 +117,11 @@ export function parseArgs(argv) {
|
|
|
75
117
|
top: DEFAULT_TOP,
|
|
76
118
|
width: null,
|
|
77
119
|
rawProjects: false,
|
|
120
|
+
anonymizeProjects: false,
|
|
78
121
|
plain: false,
|
|
79
122
|
ascii: false,
|
|
80
123
|
static: false,
|
|
124
|
+
version: false,
|
|
81
125
|
help: false,
|
|
82
126
|
};
|
|
83
127
|
|
|
@@ -86,6 +130,8 @@ export function parseArgs(argv) {
|
|
|
86
130
|
const argument = argv[index];
|
|
87
131
|
if (argument === "--help" || argument === "-h") {
|
|
88
132
|
options.help = true;
|
|
133
|
+
} else if (argument === "--version" || argument === "-v") {
|
|
134
|
+
options.version = true;
|
|
89
135
|
} else if (argument === "--date") {
|
|
90
136
|
options.date = readOption(argv, index, "--date");
|
|
91
137
|
index += 1;
|
|
@@ -119,6 +165,8 @@ export function parseArgs(argv) {
|
|
|
119
165
|
index += 1;
|
|
120
166
|
} else if (argument === "--raw-projects") {
|
|
121
167
|
options.rawProjects = true;
|
|
168
|
+
} else if (argument === "-anon") {
|
|
169
|
+
options.anonymizeProjects = true;
|
|
122
170
|
} else if (argument === "--no-archived") {
|
|
123
171
|
options.includeArchived = false;
|
|
124
172
|
} else if (argument === "--plain") {
|
|
@@ -134,13 +182,16 @@ export function parseArgs(argv) {
|
|
|
134
182
|
}
|
|
135
183
|
}
|
|
136
184
|
|
|
137
|
-
if (!options.help && !options.date) {
|
|
185
|
+
if (!options.help && !options.version && options.range === "all" && options.date) {
|
|
186
|
+
throw new Error("The all range does not accept an end day.");
|
|
187
|
+
}
|
|
188
|
+
if (!options.help && !options.version && !options.date && options.range !== "all") {
|
|
138
189
|
options.date = "today";
|
|
139
190
|
}
|
|
140
|
-
if (!options.help && options.refresh && !options.autoRefresh) {
|
|
191
|
+
if (!options.help && !options.version && options.refresh && !options.autoRefresh) {
|
|
141
192
|
throw new Error("--refresh cannot be combined with --no-refresh.");
|
|
142
193
|
}
|
|
143
|
-
if (!options.help && options.refresh && options.inputExplicit) {
|
|
194
|
+
if (!options.help && !options.version && options.refresh && options.inputExplicit) {
|
|
144
195
|
throw new Error("--refresh cannot be combined with --input.");
|
|
145
196
|
}
|
|
146
197
|
return options;
|
|
@@ -243,21 +294,99 @@ export function dayBounds(value, timeZone) {
|
|
|
243
294
|
const nextDateString = shiftCalendarDate(dateString, 1);
|
|
244
295
|
const start = zonedMidnight(dateString, timeZone);
|
|
245
296
|
const end = zonedMidnight(nextDateString, timeZone);
|
|
246
|
-
return {
|
|
297
|
+
return {
|
|
298
|
+
dateString,
|
|
299
|
+
startDateString: dateString,
|
|
300
|
+
endDateString: dateString,
|
|
301
|
+
start,
|
|
302
|
+
end,
|
|
303
|
+
timeZone,
|
|
304
|
+
rangeDays: 1,
|
|
305
|
+
};
|
|
247
306
|
}
|
|
248
307
|
|
|
249
|
-
export function
|
|
308
|
+
export function rollingBounds(value, timeZone, rangeDays) {
|
|
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
|
+
}
|
|
250
318
|
const endDay = dayBounds(value, timeZone);
|
|
251
|
-
const startDateString = shiftCalendarDate(endDay.dateString, -
|
|
319
|
+
const startDateString = shiftCalendarDate(endDay.dateString, -(rangeDays - 1));
|
|
252
320
|
return {
|
|
253
321
|
...endDay,
|
|
254
322
|
startDateString,
|
|
255
323
|
endDateString: endDay.dateString,
|
|
256
324
|
start: zonedMidnight(startDateString, timeZone),
|
|
257
|
-
rangeDays
|
|
325
|
+
rangeDays,
|
|
258
326
|
};
|
|
259
327
|
}
|
|
260
328
|
|
|
329
|
+
export function weekBounds(value, timeZone) {
|
|
330
|
+
return rollingBounds(value, timeZone, 7);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function monthBounds(value, timeZone) {
|
|
334
|
+
return rollingBounds(value, timeZone, 30);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function eventTimestamp(event) {
|
|
338
|
+
if (typeof event?.timestamp !== "string" || !event.timestamp.trim()) {
|
|
339
|
+
return Number.NaN;
|
|
340
|
+
}
|
|
341
|
+
return new Date(event.timestamp).getTime();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function allBounds(snapshot, timeZone) {
|
|
345
|
+
validateTimeZone(timeZone);
|
|
346
|
+
let earliest = Number.POSITIVE_INFINITY;
|
|
347
|
+
let latest = Number.NEGATIVE_INFINITY;
|
|
348
|
+
for (const event of snapshot.events ?? []) {
|
|
349
|
+
const timestamp = eventTimestamp(event);
|
|
350
|
+
if (!Number.isFinite(timestamp)) continue;
|
|
351
|
+
earliest = Math.min(earliest, timestamp);
|
|
352
|
+
latest = Math.max(latest, timestamp);
|
|
353
|
+
}
|
|
354
|
+
if (!Number.isFinite(earliest)) {
|
|
355
|
+
return {
|
|
356
|
+
...dayBounds("today", timeZone),
|
|
357
|
+
rangeDays: null,
|
|
358
|
+
allTime: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const dateString = (timestamp) => dateStringFromParts(numericDateParts({
|
|
362
|
+
value: new Date(timestamp),
|
|
363
|
+
timeZone,
|
|
364
|
+
}));
|
|
365
|
+
const startDateString = dateString(earliest);
|
|
366
|
+
const endDateString = dateString(latest);
|
|
367
|
+
return {
|
|
368
|
+
dateString: endDateString,
|
|
369
|
+
startDateString,
|
|
370
|
+
endDateString,
|
|
371
|
+
start: zonedMidnight(startDateString, timeZone),
|
|
372
|
+
end: zonedMidnight(shiftCalendarDate(endDateString, 1), timeZone),
|
|
373
|
+
timeZone,
|
|
374
|
+
rangeDays: null,
|
|
375
|
+
allTime: true,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function boundsForOptions(options, snapshot) {
|
|
380
|
+
if (options.range === "all") return allBounds(snapshot, options.timeZone);
|
|
381
|
+
return rollingBounds(options.date, options.timeZone, options.rangeDays);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function describeRange(options, bounds) {
|
|
385
|
+
if (options.range === "all") return "all time";
|
|
386
|
+
if (bounds.rangeDays === 1) return bounds.dateString;
|
|
387
|
+
return `${bounds.startDateString} through ${bounds.endDateString}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
261
390
|
export function sanitizeTerminalText(value) {
|
|
262
391
|
return String(value ?? "")
|
|
263
392
|
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
@@ -298,20 +427,14 @@ export function oneOffProjects(snapshot) {
|
|
|
298
427
|
|
|
299
428
|
function modelLabel(value) {
|
|
300
429
|
const model = cleanLabel(value, "Unknown model");
|
|
301
|
-
|
|
302
|
-
if (lower.includes("sol")) return "Sol";
|
|
303
|
-
if (lower.includes("luna")) return "Luna";
|
|
304
|
-
if (lower.includes("terra")) return "Terra";
|
|
305
|
-
if (lower === "gpt-5.5") return "GPT-5.5";
|
|
306
|
-
if (lower === "gpt-5.4") return "GPT-5.4";
|
|
307
|
-
return model;
|
|
430
|
+
return modelDisplayName(model);
|
|
308
431
|
}
|
|
309
432
|
|
|
310
433
|
export function filterDayEvents(snapshot, bounds) {
|
|
311
434
|
const start = bounds.start.getTime();
|
|
312
435
|
const end = bounds.end.getTime();
|
|
313
436
|
return (snapshot.events ?? []).filter((event) => {
|
|
314
|
-
const timestamp =
|
|
437
|
+
const timestamp = eventTimestamp(event);
|
|
315
438
|
return Number.isFinite(timestamp) && timestamp >= start && timestamp < end;
|
|
316
439
|
});
|
|
317
440
|
}
|
|
@@ -369,7 +492,13 @@ export function aggregateProjects(snapshot, events, options = {}) {
|
|
|
369
492
|
return right.totalTokens - left.totalTokens;
|
|
370
493
|
}
|
|
371
494
|
return left.project.localeCompare(right.project);
|
|
372
|
-
})
|
|
495
|
+
})
|
|
496
|
+
.map((row, index) => ({
|
|
497
|
+
...row,
|
|
498
|
+
displayProject: options.anonymizeProjects
|
|
499
|
+
? `Project ${index + 1}`
|
|
500
|
+
: row.displayProject,
|
|
501
|
+
}));
|
|
373
502
|
}
|
|
374
503
|
|
|
375
504
|
function sourceLabel(snapshotPath, snapshot) {
|
|
@@ -385,7 +514,7 @@ function sourceLabel(snapshotPath, snapshot) {
|
|
|
385
514
|
function latestActivityDateString(snapshot, timeZone) {
|
|
386
515
|
let latestTimestamp = Number.NEGATIVE_INFINITY;
|
|
387
516
|
for (const event of snapshot.events ?? []) {
|
|
388
|
-
const timestamp =
|
|
517
|
+
const timestamp = eventTimestamp(event);
|
|
389
518
|
if (Number.isFinite(timestamp) && timestamp > latestTimestamp) {
|
|
390
519
|
latestTimestamp = timestamp;
|
|
391
520
|
}
|
|
@@ -408,11 +537,8 @@ function displayCalendarDate(dateString) {
|
|
|
408
537
|
}
|
|
409
538
|
|
|
410
539
|
function emptyState(options, snapshot, bounds) {
|
|
411
|
-
const rangeDescription = options.range === "week"
|
|
412
|
-
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
413
|
-
: bounds.dateString;
|
|
414
540
|
const lines = [
|
|
415
|
-
`No model-call events found for ${
|
|
541
|
+
`No model-call events found for ${describeRange(options, bounds)} (${bounds.timeZone}).`,
|
|
416
542
|
"Token Ledger reads only Codex history stored on this computer.",
|
|
417
543
|
];
|
|
418
544
|
const latestDate = latestActivityDateString(snapshot, bounds.timeZone);
|
|
@@ -525,10 +651,9 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
525
651
|
}
|
|
526
652
|
|
|
527
653
|
export async function run(options) {
|
|
528
|
-
|
|
529
|
-
? weekBounds(options.date, options.timeZone)
|
|
530
|
-
: dayBounds(options.date, options.timeZone);
|
|
654
|
+
if (options.range !== "all") boundsForOptions(options);
|
|
531
655
|
const snapshot = await loadSnapshot(options);
|
|
656
|
+
const bounds = boundsForOptions(options, snapshot);
|
|
532
657
|
const events = filterDayEvents(snapshot, bounds);
|
|
533
658
|
if (events.length === 0) {
|
|
534
659
|
return emptyState(options, snapshot, bounds);
|
|
@@ -549,10 +674,9 @@ function shouldUseInteractive(options) {
|
|
|
549
674
|
}
|
|
550
675
|
|
|
551
676
|
async function runInteractive(options) {
|
|
552
|
-
|
|
553
|
-
? weekBounds(options.date, options.timeZone)
|
|
554
|
-
: dayBounds(options.date, options.timeZone);
|
|
677
|
+
if (options.range !== "all") boundsForOptions(options);
|
|
555
678
|
const snapshot = await loadSnapshot(options);
|
|
679
|
+
const bounds = boundsForOptions(options, snapshot);
|
|
556
680
|
const events = filterDayEvents(snapshot, bounds);
|
|
557
681
|
if (events.length === 0) {
|
|
558
682
|
process.stdout.write(`${emptyState(options, snapshot, bounds)}\n`);
|
|
@@ -577,6 +701,10 @@ async function main() {
|
|
|
577
701
|
process.stdout.write(`${usage()}\n`);
|
|
578
702
|
return;
|
|
579
703
|
}
|
|
704
|
+
if (options.version) {
|
|
705
|
+
process.stdout.write(`${VERSION}\n`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
580
708
|
if (shouldUseInteractive(options)) {
|
|
581
709
|
await runInteractive(options);
|
|
582
710
|
} else {
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
workerData,
|
|
40
40
|
} from "node:worker_threads";
|
|
41
41
|
|
|
42
|
+
import { normalizeModelIdentifier } from "./token-ledger-models.mjs";
|
|
43
|
+
|
|
42
44
|
const SCHEMA_VERSION = 1;
|
|
43
45
|
const WEEK_MINUTES = 10_080;
|
|
44
46
|
const DEFAULT_MAX_SCAN_WORKERS = 4;
|
|
@@ -230,19 +232,7 @@ function hasDetailedBreakdown(usage) {
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
function normalizeModel(model) {
|
|
233
|
-
|
|
234
|
-
.toLowerCase()
|
|
235
|
-
.replaceAll("_", "-");
|
|
236
|
-
if (value.startsWith("gpt-5.6-sol")) return "gpt-5.6-sol";
|
|
237
|
-
if (value.startsWith("gpt-5.6-terra")) return "gpt-5.6-terra";
|
|
238
|
-
if (value.startsWith("gpt-5.6-luna")) return "gpt-5.6-luna";
|
|
239
|
-
if (value.startsWith("gpt-5.5-cyber")) return "gpt-5.5-cyber";
|
|
240
|
-
if (value.startsWith("gpt-5.5")) return "gpt-5.5";
|
|
241
|
-
if (value.startsWith("gpt-5.4-mini")) return "gpt-5.4-mini";
|
|
242
|
-
if (value.startsWith("gpt-5.4")) return "gpt-5.4";
|
|
243
|
-
if (value.startsWith("gpt-5.3-codex")) return "gpt-5.3-codex";
|
|
244
|
-
if (value.startsWith("gpt-5.2")) return "gpt-5.2";
|
|
245
|
-
return value || "unknown";
|
|
235
|
+
return normalizeModelIdentifier(sanitizeLabel(model, "unknown"));
|
|
246
236
|
}
|
|
247
237
|
|
|
248
238
|
function parseStructuredSource(value) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const MODEL_PRESENTATIONS = [
|
|
2
|
+
{
|
|
3
|
+
canonical: "codex-auto-review",
|
|
4
|
+
families: ["auto-review", "codex-auto-review"],
|
|
5
|
+
label: "Auto Review",
|
|
6
|
+
colorKey: "autoReview",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
canonical: "gpt-daybreak-blue",
|
|
10
|
+
families: ["daybreak-blue", "gpt-daybreak-blue"],
|
|
11
|
+
label: "Daybreak Blue",
|
|
12
|
+
colorKey: "daybreakBlue",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
canonical: "gpt-5.6-sol",
|
|
16
|
+
families: ["sol", "gpt-5.6-sol"],
|
|
17
|
+
label: "Sol",
|
|
18
|
+
colorKey: "sol",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
canonical: "gpt-5.6-luna",
|
|
22
|
+
families: ["luna", "gpt-5.6-luna"],
|
|
23
|
+
label: "Luna",
|
|
24
|
+
colorKey: "luna",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
canonical: "gpt-5.6-terra",
|
|
28
|
+
families: ["terra", "gpt-5.6-terra"],
|
|
29
|
+
label: "Terra",
|
|
30
|
+
colorKey: "terra",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
canonical: "gpt-5.5-cyber",
|
|
34
|
+
families: ["gpt-5.5-cyber"],
|
|
35
|
+
label: "GPT-5.5 Cyber",
|
|
36
|
+
colorKey: "gpt",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
canonical: "gpt-5.5",
|
|
40
|
+
families: ["gpt-5.5"],
|
|
41
|
+
label: "GPT-5.5",
|
|
42
|
+
colorKey: "gpt",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
canonical: "gpt-5.4-mini",
|
|
46
|
+
families: ["gpt-5.4-mini"],
|
|
47
|
+
label: "GPT-5.4 Mini",
|
|
48
|
+
colorKey: "gpt",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
canonical: "gpt-5.4",
|
|
52
|
+
families: ["gpt-5.4"],
|
|
53
|
+
label: "GPT-5.4",
|
|
54
|
+
colorKey: "gpt",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
canonical: "gpt-5.3-codex",
|
|
58
|
+
families: ["gpt-5.3-codex"],
|
|
59
|
+
label: "GPT-5.3 Codex",
|
|
60
|
+
colorKey: "gpt",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
canonical: "gpt-5.2",
|
|
64
|
+
families: ["gpt-5.2"],
|
|
65
|
+
label: "GPT-5.2",
|
|
66
|
+
colorKey: "gpt",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
canonical: "gpt",
|
|
70
|
+
families: ["gpt"],
|
|
71
|
+
label: "GPT",
|
|
72
|
+
colorKey: "gpt",
|
|
73
|
+
exact: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
canonical: "unknown",
|
|
77
|
+
families: ["unknown", "unknown-model"],
|
|
78
|
+
label: "Unknown model",
|
|
79
|
+
colorKey: "other",
|
|
80
|
+
exact: true,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
function normalizedModel(value) {
|
|
85
|
+
return String(value || "")
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replaceAll("_", "-")
|
|
89
|
+
.replace(/\s+/g, "-");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function presentationFor(value) {
|
|
93
|
+
const normalized = normalizedModel(value);
|
|
94
|
+
return MODEL_PRESENTATIONS.find((presentation) => presentation.families.some(
|
|
95
|
+
(family) => normalized === family || (
|
|
96
|
+
presentation.exact !== true && normalized.startsWith(`${family}-`)
|
|
97
|
+
),
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function normalizeModelIdentifier(value) {
|
|
102
|
+
const normalized = normalizedModel(value) || "unknown";
|
|
103
|
+
return presentationFor(normalized)?.canonical || normalized;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function modelDisplayName(value) {
|
|
107
|
+
const model = String(value || "").trim();
|
|
108
|
+
return presentationFor(model)?.label || model || "Unknown model";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function modelColorKey(value) {
|
|
112
|
+
return presentationFor(value)?.colorKey || "other";
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tledger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A local-only terminal dashboard for Codex token usage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"test:cli": "node --test tests/token-ledger-cli.test.mjs",
|
|
37
37
|
"test:collector": "node --test tests/token-ledger-collector.test.mjs",
|
|
38
38
|
"demo": "node scripts/generate-demo-screenshot.mjs",
|
|
39
|
-
"
|
|
39
|
+
"demo:check": "node scripts/generate-demo-screenshot.mjs --check",
|
|
40
|
+
"verify:packed": "node scripts/verify-release.mjs",
|
|
41
|
+
"verify:release": "npm test && npm run verify:packed",
|
|
42
|
+
"prepublishOnly": "npm run verify:release"
|
|
40
43
|
},
|
|
41
44
|
"publishConfig": {
|
|
42
45
|
"access": "public"
|