tledger 0.1.2 → 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/LICENSE +21 -0
- package/README.md +54 -9
- package/bin/token-ledger-terminal.mjs +120 -53
- package/bin/token-ledger.mjs +198 -41
- package/lib/token-ledger-collector.mjs +3 -13
- package/lib/token-ledger-models.mjs +113 -0
- package/package.json +7 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jskoiz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
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.
|
|
8
|
+
|
|
9
|
+
Token Ledger reads Codex history stored on the computer where it runs. It does
|
|
10
|
+
not sign in, fetch account-wide usage, or combine activity from other machines.
|
|
7
11
|
|
|
8
12
|

|
|
9
13
|
|
|
10
14
|
_The screenshot is generated from an intentionally synthetic fixture._
|
|
11
15
|
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
Token Ledger requires Node.js 22.13 or newer and npm. Install a supported LTS
|
|
19
|
+
release from [nodejs.org](https://nodejs.org/en/download) if either command is
|
|
20
|
+
missing, then confirm the versions in a new terminal:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node --version
|
|
24
|
+
npm --version
|
|
25
|
+
```
|
|
26
|
+
|
|
12
27
|
## Install
|
|
13
28
|
|
|
14
|
-
Token Ledger
|
|
15
|
-
dependencies.
|
|
29
|
+
Token Ledger has no runtime npm dependencies and runs no installation script.
|
|
16
30
|
|
|
17
31
|
```bash
|
|
18
32
|
npm install --global tledger
|
|
33
|
+
tledger --version
|
|
19
34
|
```
|
|
20
35
|
|
|
21
36
|
## Use
|
|
@@ -26,11 +41,19 @@ tledger week # same default, stated explicitly
|
|
|
26
41
|
tledger day # today
|
|
27
42
|
tledger day yesterday
|
|
28
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
|
|
29
48
|
```
|
|
30
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
|
+
|
|
31
53
|
The local timezone and top 10 projects are selected automatically. The default
|
|
32
54
|
view is interactive in a terminal; use arrow keys or `j`/`k` to move and `q` or
|
|
33
|
-
Escape to exit.
|
|
55
|
+
Escape to exit. If the selected period is empty, Token Ledger reports the most
|
|
56
|
+
recent local activity date and prints the exact command for opening it.
|
|
34
57
|
|
|
35
58
|
Useful options:
|
|
36
59
|
|
|
@@ -43,14 +66,31 @@ Useful options:
|
|
|
43
66
|
--codex-home <dir> Read another Codex data directory
|
|
44
67
|
--no-archived Skip archived sessions during collection
|
|
45
68
|
--raw-projects Keep singleton project labels separate
|
|
69
|
+
-anon Show Project 1, Project 2, etc. instead of project names
|
|
46
70
|
--static Print once instead of opening the interactive view
|
|
47
71
|
--plain Print once without ANSI color
|
|
48
72
|
--ascii Use ASCII bars
|
|
49
73
|
--width <40-200> Set the static layout width
|
|
50
74
|
--date <day> Set today, yesterday, or YYYY-MM-DD
|
|
75
|
+
-v, --version Show the installed version
|
|
51
76
|
--help Show complete CLI help
|
|
52
77
|
```
|
|
53
78
|
|
|
79
|
+
## Update or uninstall
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install --global tledger@latest
|
|
83
|
+
tledger --version
|
|
84
|
+
npm uninstall --global tledger
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Uninstalling the npm package leaves the privacy-reduced cache at
|
|
88
|
+
`~/.token-ledger/token-ledger-snapshot.json`. Remove that directory separately
|
|
89
|
+
only when you also want the next installation to perform a completely fresh
|
|
90
|
+
scan.
|
|
91
|
+
|
|
92
|
+
## Local data and privacy
|
|
93
|
+
|
|
54
94
|
By default, Token Ledger reads `CODEX_HOME` or `~/.codex` and keeps a
|
|
55
95
|
privacy-reduced cache at `~/.token-ledger/token-ledger-snapshot.json`. It checks
|
|
56
96
|
source freshness automatically. Fresh scans use a bounded pool of up to four
|
|
@@ -58,13 +98,18 @@ workers; unchanged runs read the existing cache. Token Ledger makes no network
|
|
|
58
98
|
requests and excludes
|
|
59
99
|
message bodies, reasoning text, tool payloads, credentials, and full local
|
|
60
100
|
paths from the cache. Project labels can still reveal local context, so keep
|
|
61
|
-
snapshots private unless you have reviewed them.
|
|
62
|
-
|
|
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.
|
|
63
104
|
|
|
64
105
|
## Develop
|
|
65
106
|
|
|
66
107
|
```bash
|
|
67
108
|
npm test
|
|
68
109
|
npm run demo
|
|
69
|
-
npm
|
|
110
|
+
npm run verify:release
|
|
70
111
|
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
[MIT](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) {
|
|
@@ -382,6 +511,45 @@ function sourceLabel(snapshotPath, snapshot) {
|
|
|
382
511
|
return `${cleanLabel(basename(snapshotPath), "snapshot")} · captured ${generated}`;
|
|
383
512
|
}
|
|
384
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
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function displayCalendarDate(dateString) {
|
|
530
|
+
const [year, month, day] = dateString.split("-").map(Number);
|
|
531
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
532
|
+
month: "long",
|
|
533
|
+
day: "numeric",
|
|
534
|
+
year: "numeric",
|
|
535
|
+
timeZone: "UTC",
|
|
536
|
+
}).format(new Date(Date.UTC(year, month - 1, day)));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function emptyState(options, snapshot, bounds) {
|
|
540
|
+
const lines = [
|
|
541
|
+
`No model-call events found for ${describeRange(options, bounds)} (${bounds.timeZone}).`,
|
|
542
|
+
"Token Ledger reads only Codex history stored on this computer.",
|
|
543
|
+
];
|
|
544
|
+
const latestDate = latestActivityDateString(snapshot, bounds.timeZone);
|
|
545
|
+
if (latestDate) {
|
|
546
|
+
lines.push(`Latest local activity: ${displayCalendarDate(latestDate)}.`);
|
|
547
|
+
lines.push(`Try: tledger ${options.range} ${latestDate}`);
|
|
548
|
+
}
|
|
549
|
+
lines.push(`Source: ${sourceLabel(options.input, snapshot)}`);
|
|
550
|
+
return lines.join("\n");
|
|
551
|
+
}
|
|
552
|
+
|
|
385
553
|
async function readSnapshot(snapshotPath) {
|
|
386
554
|
let parsed;
|
|
387
555
|
try {
|
|
@@ -483,19 +651,12 @@ function render(options, snapshot, bounds, events, rows, allRows) {
|
|
|
483
651
|
}
|
|
484
652
|
|
|
485
653
|
export async function run(options) {
|
|
486
|
-
|
|
487
|
-
? weekBounds(options.date, options.timeZone)
|
|
488
|
-
: dayBounds(options.date, options.timeZone);
|
|
654
|
+
if (options.range !== "all") boundsForOptions(options);
|
|
489
655
|
const snapshot = await loadSnapshot(options);
|
|
656
|
+
const bounds = boundsForOptions(options, snapshot);
|
|
490
657
|
const events = filterDayEvents(snapshot, bounds);
|
|
491
658
|
if (events.length === 0) {
|
|
492
|
-
|
|
493
|
-
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
494
|
-
: bounds.dateString;
|
|
495
|
-
return [
|
|
496
|
-
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
497
|
-
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
498
|
-
].join("\n");
|
|
659
|
+
return emptyState(options, snapshot, bounds);
|
|
499
660
|
}
|
|
500
661
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
501
662
|
const rows = allRows.slice(0, options.top);
|
|
@@ -513,20 +674,12 @@ function shouldUseInteractive(options) {
|
|
|
513
674
|
}
|
|
514
675
|
|
|
515
676
|
async function runInteractive(options) {
|
|
516
|
-
|
|
517
|
-
? weekBounds(options.date, options.timeZone)
|
|
518
|
-
: dayBounds(options.date, options.timeZone);
|
|
677
|
+
if (options.range !== "all") boundsForOptions(options);
|
|
519
678
|
const snapshot = await loadSnapshot(options);
|
|
679
|
+
const bounds = boundsForOptions(options, snapshot);
|
|
520
680
|
const events = filterDayEvents(snapshot, bounds);
|
|
521
681
|
if (events.length === 0) {
|
|
522
|
-
|
|
523
|
-
? `${bounds.startDateString} through ${bounds.endDateString}`
|
|
524
|
-
: bounds.dateString;
|
|
525
|
-
process.stdout.write([
|
|
526
|
-
`No model-call events found for ${rangeDescription} (${bounds.timeZone}).`,
|
|
527
|
-
`Source: ${sourceLabel(options.input, snapshot)}`,
|
|
528
|
-
"",
|
|
529
|
-
].join("\n"));
|
|
682
|
+
process.stdout.write(`${emptyState(options, snapshot, bounds)}\n`);
|
|
530
683
|
return;
|
|
531
684
|
}
|
|
532
685
|
const allRows = aggregateProjects(snapshot, events, options);
|
|
@@ -548,6 +701,10 @@ async function main() {
|
|
|
548
701
|
process.stdout.write(`${usage()}\n`);
|
|
549
702
|
return;
|
|
550
703
|
}
|
|
704
|
+
if (options.version) {
|
|
705
|
+
process.stdout.write(`${VERSION}\n`);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
551
708
|
if (shouldUseInteractive(options)) {
|
|
552
709
|
await runInteractive(options);
|
|
553
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,7 +1,8 @@
|
|
|
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
|
+
"license": "MIT",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"codex",
|
|
7
8
|
"cli",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"files": [
|
|
25
26
|
"bin/",
|
|
26
27
|
"lib/",
|
|
28
|
+
"LICENSE",
|
|
27
29
|
"README.md"
|
|
28
30
|
],
|
|
29
31
|
"bin": {
|
|
@@ -34,7 +36,10 @@
|
|
|
34
36
|
"test:cli": "node --test tests/token-ledger-cli.test.mjs",
|
|
35
37
|
"test:collector": "node --test tests/token-ledger-collector.test.mjs",
|
|
36
38
|
"demo": "node scripts/generate-demo-screenshot.mjs",
|
|
37
|
-
"
|
|
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"
|
|
38
43
|
},
|
|
39
44
|
"publishConfig": {
|
|
40
45
|
"access": "public"
|