tledger 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -116
- package/bin/token-ledger-cache-image.mjs +1150 -0
- package/bin/token-ledger-controls.mjs +24 -0
- package/bin/token-ledger-rates.mjs +4 -1
- package/bin/token-ledger-terminal.mjs +143 -39
- package/bin/token-ledger-trend-image.mjs +1527 -584
- package/bin/token-ledger-trend-terminal.mjs +73 -28
- package/bin/token-ledger-trend.mjs +25 -11
- package/bin/token-ledger-tui.mjs +20 -14
- package/bin/token-ledger.mjs +536 -137
- package/docs/token-ledger-cli-week.png +0 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-importer.mjs +589 -279
- package/lib/token-ledger-snapshot.mjs +267 -0
- package/lib/token-ledger-usage.mjs +524 -0
- package/package.json +11 -5
|
@@ -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
|
+
}
|
|
@@ -18,10 +18,13 @@ export const RATE_CARD = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export function normalizeModel(model) {
|
|
21
|
+
// Collapse underscore and whitespace separators to dashes so variants like
|
|
22
|
+
// "gpt-5.4 mini" resolve to their own rate-card entry. Keep in lockstep
|
|
23
|
+
// with normalizeModel in lib/token-ledger-importer.mjs.
|
|
21
24
|
const value = String(model || "unknown")
|
|
22
25
|
.trim()
|
|
23
26
|
.toLowerCase()
|
|
24
|
-
.
|
|
27
|
+
.replace(/[\s_]+/g, "-");
|
|
25
28
|
if (RATE_CARD[value]) return value;
|
|
26
29
|
if (value.startsWith("gpt-5.6-sol")) return "gpt-5.6-sol";
|
|
27
30
|
if (value.startsWith("gpt-5.6-terra")) return "gpt-5.6-terra";
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
eventCredits,
|
|
2
3
|
normalizeQuotaTimeline,
|
|
3
4
|
weeklyQuotaObservations,
|
|
4
5
|
} from "./token-ledger-trend.mjs";
|
|
6
|
+
import {
|
|
7
|
+
INTERACTIVE_FOOTER,
|
|
8
|
+
INTERACTIVE_HELP,
|
|
9
|
+
} from "./token-ledger-controls.mjs";
|
|
10
|
+
import {
|
|
11
|
+
usageBucketsInRange,
|
|
12
|
+
usageCallCount,
|
|
13
|
+
usageThreadIds,
|
|
14
|
+
} from "../lib/token-ledger-usage.mjs";
|
|
5
15
|
|
|
6
16
|
const RESET = "\u001b[0m";
|
|
7
17
|
const PRIMARY_STYLE = [38, 2, 255, 255, 255];
|
|
@@ -74,12 +84,18 @@ function compact(value) {
|
|
|
74
84
|
[1_000_000, "M"],
|
|
75
85
|
[1_000, "K"],
|
|
76
86
|
];
|
|
77
|
-
for (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
for (let index = 0; index < units.length; index += 1) {
|
|
88
|
+
const [divisor, suffix] = units[index];
|
|
89
|
+
if (absolute < divisor) continue;
|
|
90
|
+
const scaled = value / divisor;
|
|
91
|
+
const magnitude = Math.abs(scaled);
|
|
92
|
+
const precision = magnitude >= 100 ? 0 : magnitude >= 10 ? 1 : 2;
|
|
93
|
+
// Values that round to 1000 of a unit belong to the next unit up
|
|
94
|
+
// (999,999 → 1.00M, not 1000K).
|
|
95
|
+
if (index > 0 && Number(magnitude.toFixed(precision)) >= 1_000) {
|
|
96
|
+
return compact(Math.sign(value) * divisor * 1_000);
|
|
82
97
|
}
|
|
98
|
+
return `${scaled.toFixed(precision)}${suffix}`;
|
|
83
99
|
}
|
|
84
100
|
return Math.round(value).toLocaleString("en-US");
|
|
85
101
|
}
|
|
@@ -127,7 +143,13 @@ function displayProject(row) {
|
|
|
127
143
|
return row.displayProject || row.project || "Unlabelled activity";
|
|
128
144
|
}
|
|
129
145
|
|
|
130
|
-
function
|
|
146
|
+
function isRollingRange(range) {
|
|
147
|
+
return range === "rolling24h" || range === "rolling";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function dateLabel(bounds, range = "day", rollingLabel = "1 day") {
|
|
151
|
+
if (range === "rolling24h") return "LAST 24 HOURS";
|
|
152
|
+
if (range === "rolling") return `LAST ${rollingLabel.toUpperCase()}`;
|
|
131
153
|
if (range === "week" && bounds.startDateString && bounds.endDateString) {
|
|
132
154
|
const startParts = bounds.startDateString.split("-").map(Number);
|
|
133
155
|
const endParts = bounds.endDateString.split("-").map(Number);
|
|
@@ -227,30 +249,52 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
|
|
|
227
249
|
};
|
|
228
250
|
}
|
|
229
251
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
252
|
+
const sumUsage = (events) =>
|
|
253
|
+
events.reduce(
|
|
254
|
+
(acc, event) => {
|
|
255
|
+
const tokens = Number(event.totalTokens) || 0;
|
|
256
|
+
acc.tokens += tokens;
|
|
257
|
+
const credits = eventCredits(event);
|
|
258
|
+
if (Number.isFinite(credits) && credits >= 0) {
|
|
259
|
+
acc.credits += credits;
|
|
260
|
+
acc.ratedTokens += tokens;
|
|
261
|
+
}
|
|
262
|
+
return acc;
|
|
263
|
+
},
|
|
264
|
+
{ tokens: 0, credits: 0, ratedTokens: 0 },
|
|
236
265
|
);
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
266
|
+
const cycleEndMs = observedThroughMs + 1;
|
|
267
|
+
const cycle = sumUsage(
|
|
268
|
+
usageBucketsInRange(snapshot, windowStartMs, cycleEndMs),
|
|
269
|
+
);
|
|
270
|
+
const displayed = sumUsage(
|
|
271
|
+
usageBucketsInRange(
|
|
272
|
+
{ events: displayedEvents },
|
|
273
|
+
windowStartMs,
|
|
274
|
+
cycleEndMs,
|
|
275
|
+
),
|
|
276
|
+
);
|
|
242
277
|
const usedPercent = Math.min(100, Math.max(0, Number(observation.usedPercent) || 0));
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
278
|
+
// The weekly meter weights usage by model, token type, and fast mode;
|
|
279
|
+
// rate-card credits carry those weights. Allocate burn by credit share
|
|
280
|
+
// when every event in the cycle is rated, and fall back to raw token
|
|
281
|
+
// share otherwise.
|
|
282
|
+
const creditsUsable =
|
|
283
|
+
cycle.tokens > 0 && cycle.ratedTokens === cycle.tokens && cycle.credits > 0;
|
|
284
|
+
const displayedSharePercent = creditsUsable
|
|
285
|
+
? (displayed.credits / cycle.credits) * 100
|
|
286
|
+
: cycle.tokens
|
|
287
|
+
? (displayed.tokens / cycle.tokens) * 100
|
|
288
|
+
: null;
|
|
246
289
|
|
|
247
290
|
return {
|
|
248
291
|
available: true,
|
|
249
292
|
usedPercent,
|
|
250
293
|
remainingPercent: 100 - usedPercent,
|
|
251
|
-
cycleTokens,
|
|
252
|
-
displayedTokens,
|
|
294
|
+
cycleTokens: cycle.tokens,
|
|
295
|
+
displayedTokens: displayed.tokens,
|
|
253
296
|
displayedSharePercent,
|
|
297
|
+
shareBasis: creditsUsable ? "credits" : "tokens",
|
|
254
298
|
estimatedDisplayedBurnPercent:
|
|
255
299
|
displayedSharePercent === null
|
|
256
300
|
? null
|
|
@@ -263,8 +307,13 @@ function summary(events) {
|
|
|
263
307
|
(sum, event) => sum + (Number(event.totalTokens) || 0),
|
|
264
308
|
0,
|
|
265
309
|
);
|
|
266
|
-
const calls = events.
|
|
267
|
-
|
|
310
|
+
const calls = events.reduce(
|
|
311
|
+
(sum, event) => sum + usageCallCount(event),
|
|
312
|
+
0,
|
|
313
|
+
);
|
|
314
|
+
const threadIds = new Set(
|
|
315
|
+
events.flatMap((event) => usageThreadIds(event)),
|
|
316
|
+
);
|
|
268
317
|
const outputTokens = events.reduce(
|
|
269
318
|
(sum, event) => sum + (Number(event.outputTokens) || 0),
|
|
270
319
|
0,
|
|
@@ -496,10 +545,28 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
|
|
|
496
545
|
return [top, ...body, bottom];
|
|
497
546
|
}
|
|
498
547
|
|
|
499
|
-
function
|
|
548
|
+
function snapshotLine(freshness, enabled) {
|
|
549
|
+
const detail = freshness?.status === "fresh" || freshness?.status === "stale"
|
|
550
|
+
? `${freshness.status} · ${freshness.ageLabel}`
|
|
551
|
+
: "age unknown";
|
|
552
|
+
return `${colorize("SNAPSHOT", ACCENT_STYLE, enabled)} ${colorize("·", SECONDARY_STYLE, enabled)} ${colorize(detail, SECONDARY_STYLE, enabled)}`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
|
|
500
556
|
const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
|
|
501
|
-
const date = colorize(
|
|
502
|
-
|
|
557
|
+
const date = colorize(
|
|
558
|
+
dateLabel(bounds, options.range, options.rollingLabel),
|
|
559
|
+
TEXT_STYLE,
|
|
560
|
+
enabled,
|
|
561
|
+
);
|
|
562
|
+
const modeLabel = options.range === "rolling24h"
|
|
563
|
+
? "24 HOURS"
|
|
564
|
+
: options.range === "rolling"
|
|
565
|
+
? options.rollingLabel.toUpperCase()
|
|
566
|
+
: options.range === "week"
|
|
567
|
+
? "7 DAYS"
|
|
568
|
+
: "DAY";
|
|
569
|
+
const mode = colorize(modeLabel, [1, ...ACCENT_STYLE], enabled);
|
|
503
570
|
const metric = (value, label) =>
|
|
504
571
|
`${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, SECONDARY_STYLE, enabled)}`;
|
|
505
572
|
const separator = colorize("·", SECONDARY_STYLE, enabled);
|
|
@@ -515,13 +582,21 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
515
582
|
metric(stats.projectCount.toLocaleString("en-US"), "PROJECTS"),
|
|
516
583
|
].join(join);
|
|
517
584
|
if (visibleLength(fullLine) < frameWidth) {
|
|
518
|
-
|
|
585
|
+
const lines = [alignHeader(fullLine)];
|
|
586
|
+
if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
|
|
587
|
+
return lines;
|
|
519
588
|
}
|
|
520
589
|
|
|
521
|
-
const compactDate = dateLabel(bounds, options.range)
|
|
590
|
+
const compactDate = dateLabel(bounds, options.range, options.rollingLabel)
|
|
522
591
|
.replace(/ 20\d{2}$/, "")
|
|
523
592
|
.replace(" – ", "–");
|
|
524
|
-
const compactMode = options.range === "
|
|
593
|
+
const compactMode = options.range === "rolling24h"
|
|
594
|
+
? "24H"
|
|
595
|
+
: options.range === "rolling"
|
|
596
|
+
? `${options.rollingAmount}${options.rollingUnit.toUpperCase()}`
|
|
597
|
+
: options.range === "week"
|
|
598
|
+
? "7D"
|
|
599
|
+
: "DAY";
|
|
525
600
|
const compactLine = [
|
|
526
601
|
left,
|
|
527
602
|
colorize(compactDate, TEXT_STYLE, enabled),
|
|
@@ -532,7 +607,9 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
532
607
|
metric(stats.projectCount.toLocaleString("en-US"), "P"),
|
|
533
608
|
].join(join);
|
|
534
609
|
if (visibleLength(compactLine) < frameWidth) {
|
|
535
|
-
|
|
610
|
+
const lines = [alignHeader(compactLine)];
|
|
611
|
+
if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
|
|
612
|
+
return lines;
|
|
536
613
|
}
|
|
537
614
|
|
|
538
615
|
const minimalTitle = colorize(frameWidth >= 45 ? "LEDGER" : "L", TITLE_STYLE, enabled);
|
|
@@ -545,10 +622,20 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
545
622
|
compact(stats.threads),
|
|
546
623
|
compact(stats.projectCount),
|
|
547
624
|
].join(" ");
|
|
548
|
-
|
|
625
|
+
const lines = [alignHeader(minimalLine)];
|
|
626
|
+
if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
|
|
627
|
+
return lines;
|
|
549
628
|
}
|
|
550
629
|
|
|
551
|
-
export function renderTerminal({
|
|
630
|
+
export function renderTerminal({
|
|
631
|
+
options,
|
|
632
|
+
snapshot,
|
|
633
|
+
snapshotFreshness,
|
|
634
|
+
bounds,
|
|
635
|
+
events,
|
|
636
|
+
rows,
|
|
637
|
+
allRows,
|
|
638
|
+
}) {
|
|
552
639
|
const enabled = colorsEnabled(options);
|
|
553
640
|
const stats = summary(events);
|
|
554
641
|
const quota = quotaCycleSummary(snapshot, events);
|
|
@@ -561,19 +648,25 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
|
|
|
561
648
|
const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
|
|
562
649
|
const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
|
|
563
650
|
const lines = [
|
|
564
|
-
...headerLines(stats, bounds, frameWidth, options, enabled),
|
|
651
|
+
...headerLines(stats, bounds, frameWidth, options, enabled, snapshotFreshness),
|
|
565
652
|
...panel(left, right, leftWidth, sideWidth, enabled, options.ascii),
|
|
566
653
|
];
|
|
567
654
|
if (!sideBySide) {
|
|
568
655
|
lines.push("");
|
|
569
656
|
lines.push(...sidebarLines(stats, frameWidth, enabled, options, quota));
|
|
570
657
|
}
|
|
658
|
+
if (events.some((event) => event.rangeAllocationEstimated === true)) {
|
|
659
|
+
lines.push("");
|
|
660
|
+
lines.push(colorize(
|
|
661
|
+
"≈ Boundary-spanning compact history is allocated proportionally.",
|
|
662
|
+
SECONDARY_STYLE,
|
|
663
|
+
enabled,
|
|
664
|
+
));
|
|
665
|
+
}
|
|
571
666
|
lines.push("");
|
|
572
667
|
lines.push(
|
|
573
668
|
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",
|
|
669
|
+
options.ascii ? INTERACTIVE_FOOTER.ascii : INTERACTIVE_FOOTER.unicode,
|
|
577
670
|
SECONDARY_STYLE,
|
|
578
671
|
enabled,
|
|
579
672
|
),
|
|
@@ -591,7 +684,17 @@ function paintFullscreenLine(line, width, background, enabled) {
|
|
|
591
684
|
return `${background}${restored}${RESET}`;
|
|
592
685
|
}
|
|
593
686
|
|
|
594
|
-
export function renderFullscreen({
|
|
687
|
+
export function renderFullscreen({
|
|
688
|
+
options,
|
|
689
|
+
snapshot,
|
|
690
|
+
snapshotFreshness,
|
|
691
|
+
bounds,
|
|
692
|
+
events,
|
|
693
|
+
rows,
|
|
694
|
+
allRows,
|
|
695
|
+
width,
|
|
696
|
+
height,
|
|
697
|
+
}) {
|
|
595
698
|
const enabled = options.forceColor ?? colorsEnabled(options);
|
|
596
699
|
const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
|
|
597
700
|
const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
|
|
@@ -606,6 +709,7 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
|
|
|
606
709
|
width: frameWidth + 2,
|
|
607
710
|
},
|
|
608
711
|
snapshot,
|
|
712
|
+
snapshotFreshness,
|
|
609
713
|
bounds,
|
|
610
714
|
events,
|
|
611
715
|
rows,
|
|
@@ -615,7 +719,7 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
|
|
|
615
719
|
staticLines.pop();
|
|
616
720
|
if (staticLines.at(-1) === "") staticLines.pop();
|
|
617
721
|
const summaryLine = staticLines.shift() ?? "";
|
|
618
|
-
const help = colorize(
|
|
722
|
+
const help = colorize(INTERACTIVE_HELP, SECONDARY_STYLE, enabled);
|
|
619
723
|
const content = [
|
|
620
724
|
{ line: summaryLine, background: SCREEN_BASE },
|
|
621
725
|
...staticLines.map((line) => ({ line, background: PANEL_BASE })),
|