tledger 0.1.4 → 0.2.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 +122 -80
- package/bin/token-ledger-rates.mjs +62 -0
- package/bin/token-ledger-terminal.mjs +133 -257
- package/bin/token-ledger-trend-image.mjs +945 -0
- package/bin/token-ledger-trend-terminal.mjs +609 -0
- package/bin/token-ledger-trend.mjs +745 -0
- package/bin/token-ledger-tui.mjs +15 -21
- package/bin/token-ledger.mjs +408 -248
- package/lib/{token-ledger-collector.mjs → token-ledger-importer.mjs} +256 -409
- package/package.json +18 -14
- package/lib/token-ledger-models.mjs +0 -113
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
2
|
+
normalizeQuotaTimeline,
|
|
3
|
+
weeklyQuotaObservations,
|
|
4
|
+
} from "./token-ledger-trend.mjs";
|
|
5
5
|
|
|
6
6
|
const RESET = "\u001b[0m";
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const PRIMARY_STYLE = [38, 2, 255, 255, 255];
|
|
8
|
+
const SECONDARY_STYLE = [38, 2, 155, 155, 155];
|
|
9
|
+
const ACCENT_STYLE = [38, 2, 51, 156, 255];
|
|
10
|
+
const BORDER_STYLE = [38, 2, 88, 88, 88];
|
|
11
|
+
const TRACK_STYLE = [38, 2, 59, 59, 59];
|
|
9
12
|
export const MODEL_COLORS = {
|
|
10
|
-
sol: [38,
|
|
11
|
-
luna:
|
|
12
|
-
terra: [38,
|
|
13
|
-
gpt: [38,
|
|
14
|
-
|
|
15
|
-
autoReview: [38, 5, 153],
|
|
16
|
-
other: [38, 5, 60],
|
|
13
|
+
sol: [38, 2, 120, 185, 242],
|
|
14
|
+
luna: ACCENT_STYLE,
|
|
15
|
+
terra: [38, 2, 214, 168, 95],
|
|
16
|
+
gpt: [38, 2, 174, 139, 219],
|
|
17
|
+
other: [38, 2, 116, 125, 144],
|
|
17
18
|
};
|
|
18
|
-
const TEXT_STYLE =
|
|
19
|
-
const TITLE_STYLE = [1,
|
|
20
|
-
const SUBTITLE_STYLE =
|
|
19
|
+
const TEXT_STYLE = PRIMARY_STYLE;
|
|
20
|
+
const TITLE_STYLE = [1, ...PRIMARY_STYLE];
|
|
21
|
+
const SUBTITLE_STYLE = SECONDARY_STYLE;
|
|
22
|
+
const SELECTED_BACKGROUND = "\u001b[48;2;42;42;42m";
|
|
21
23
|
|
|
22
24
|
function colorsEnabled(options) {
|
|
23
25
|
return options.forceColor ?? (!options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY));
|
|
@@ -29,16 +31,7 @@ function colorize(value, code, enabled) {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function stripAnsi(value) {
|
|
32
|
-
return String(value)
|
|
33
|
-
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
34
|
-
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function sanitizeText(value) {
|
|
38
|
-
return stripAnsi(value)
|
|
39
|
-
.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
|
|
40
|
-
.replace(/\s+/g, " ")
|
|
41
|
-
.trim();
|
|
34
|
+
return String(value).replace(/\u001b\[[0-9;]*m/g, "");
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
function visibleLength(value) {
|
|
@@ -100,16 +93,23 @@ function plural(value, singular, pluralForm = `${singular}s`) {
|
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
function modelLabel(value) {
|
|
103
|
-
const model =
|
|
104
|
-
|
|
96
|
+
const model = String(value || "Unknown model");
|
|
97
|
+
const lower = model.toLowerCase();
|
|
98
|
+
if (lower.includes("sol")) return "Sol";
|
|
99
|
+
if (lower.includes("luna")) return "Luna";
|
|
100
|
+
if (lower.includes("terra")) return "Terra";
|
|
101
|
+
if (lower.includes("gpt-5.5") || lower.includes("gpt-5.4")) return "GPT";
|
|
102
|
+
return "Other";
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
function modelColor(model) {
|
|
108
|
-
|
|
106
|
+
const key = String(model || "").toLowerCase();
|
|
107
|
+
return MODEL_COLORS[key] ?? MODEL_COLORS.other;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
function usageTypeLabel(value) {
|
|
112
|
-
const words = (
|
|
111
|
+
const words = String(value || "unknown")
|
|
112
|
+
.trim()
|
|
113
113
|
.replace(/[_-]+/g, " ")
|
|
114
114
|
.split(/\s+/)
|
|
115
115
|
.filter(Boolean);
|
|
@@ -124,11 +124,11 @@ function usageTypeLabel(value) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function displayProject(row) {
|
|
127
|
-
return
|
|
127
|
+
return row.displayProject || row.project || "Unlabelled activity";
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
function dateLabel(bounds, range = "day") {
|
|
131
|
-
if (range
|
|
131
|
+
if (range === "week" && bounds.startDateString && bounds.endDateString) {
|
|
132
132
|
const startParts = bounds.startDateString.split("-").map(Number);
|
|
133
133
|
const endParts = bounds.endDateString.split("-").map(Number);
|
|
134
134
|
const monthNames = [
|
|
@@ -137,12 +137,7 @@ function dateLabel(bounds, range = "day") {
|
|
|
137
137
|
];
|
|
138
138
|
const start = `${monthNames[startParts[1] - 1]} ${String(startParts[2]).padStart(2, "0")}`;
|
|
139
139
|
const end = `${monthNames[endParts[1] - 1]} ${String(endParts[2]).padStart(2, "0")}`;
|
|
140
|
-
|
|
141
|
-
return `${end} ${endParts[0]}`;
|
|
142
|
-
}
|
|
143
|
-
return startParts[0] === endParts[0]
|
|
144
|
-
? `${start} – ${end} ${endParts[0]}`
|
|
145
|
-
: `${start} ${startParts[0]} – ${end} ${endParts[0]}`;
|
|
140
|
+
return `${start} – ${end} ${endParts[0]}`;
|
|
146
141
|
}
|
|
147
142
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
148
143
|
timeZone: bounds.timeZone,
|
|
@@ -159,13 +154,6 @@ function dateLabel(bounds, range = "day") {
|
|
|
159
154
|
return `${values.weekday} ${values.day} ${values.month} ${values.year}`.toUpperCase();
|
|
160
155
|
}
|
|
161
156
|
|
|
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
|
-
|
|
169
157
|
function modelTotals(events) {
|
|
170
158
|
const totals = new Map();
|
|
171
159
|
for (const event of events) {
|
|
@@ -180,9 +168,7 @@ function modelTotals(events) {
|
|
|
180
168
|
function usageTypeTotals(events) {
|
|
181
169
|
const totals = new Map();
|
|
182
170
|
for (const event of events) {
|
|
183
|
-
const key =
|
|
184
|
-
? "auto-review"
|
|
185
|
-
: String(event.useType || "unknown").trim().toLowerCase() || "unknown";
|
|
171
|
+
const key = String(event.useType || "unknown").trim().toLowerCase() || "unknown";
|
|
186
172
|
totals.set(key, (totals.get(key) ?? 0) + (Number(event.totalTokens) || 0));
|
|
187
173
|
}
|
|
188
174
|
return [...totals.entries()]
|
|
@@ -194,22 +180,17 @@ function usageTypeTotals(events) {
|
|
|
194
180
|
.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
function latestWeeklyQuotaObservation(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
.sort(
|
|
205
|
-
(left, right) =>
|
|
206
|
-
new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime(),
|
|
207
|
-
)
|
|
208
|
-
.at(-1) ?? null;
|
|
183
|
+
function latestWeeklyQuotaObservation(snapshot) {
|
|
184
|
+
// The epoch-keyed timeline drops stale readings from superseded windows,
|
|
185
|
+
// so the last entry is the newest reading of the currently-live window.
|
|
186
|
+
const normalized = normalizeQuotaTimeline(weeklyQuotaObservations(snapshot));
|
|
187
|
+
const latest = normalized.at(-1);
|
|
188
|
+
if (!latest) return null;
|
|
189
|
+
return { ...latest, usedPercent: latest.normalizedUsedPercent };
|
|
209
190
|
}
|
|
210
191
|
|
|
211
192
|
export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
|
|
212
|
-
const observation = latestWeeklyQuotaObservation(snapshot
|
|
193
|
+
const observation = latestWeeklyQuotaObservation(snapshot);
|
|
213
194
|
if (!observation) {
|
|
214
195
|
return {
|
|
215
196
|
available: false,
|
|
@@ -297,32 +278,6 @@ function summary(events) {
|
|
|
297
278
|
const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
|
|
298
279
|
return sum + Math.min(input, cached);
|
|
299
280
|
}, 0);
|
|
300
|
-
const turnCount = (rows) => {
|
|
301
|
-
const keys = new Set();
|
|
302
|
-
for (const [index, event] of rows.entries()) {
|
|
303
|
-
const turnId = String(event.turnId || "").trim();
|
|
304
|
-
keys.add(turnId ? `turn:${turnId}` : `event:${event.id || index}`);
|
|
305
|
-
}
|
|
306
|
-
return keys.size;
|
|
307
|
-
};
|
|
308
|
-
const totalTurns = turnCount(events);
|
|
309
|
-
const autoReviewEvents = events.filter(
|
|
310
|
-
(event) => modelLabel(event.model) === "Auto Review",
|
|
311
|
-
);
|
|
312
|
-
const autoReviewTokens = autoReviewEvents.reduce(
|
|
313
|
-
(sum, event) => sum + (Number(event.totalTokens) || 0),
|
|
314
|
-
0,
|
|
315
|
-
);
|
|
316
|
-
const autoReviewInputTokens = autoReviewEvents.reduce(
|
|
317
|
-
(sum, event) => sum + Math.max(0, Number(event.inputTokens) || 0),
|
|
318
|
-
0,
|
|
319
|
-
);
|
|
320
|
-
const autoReviewCachedInputTokens = autoReviewEvents.reduce((sum, event) => {
|
|
321
|
-
const input = Math.max(0, Number(event.inputTokens) || 0);
|
|
322
|
-
const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
|
|
323
|
-
return sum + Math.min(input, cached);
|
|
324
|
-
}, 0);
|
|
325
|
-
const autoReviewTurns = turnCount(autoReviewEvents);
|
|
326
281
|
return {
|
|
327
282
|
totalTokens,
|
|
328
283
|
calls,
|
|
@@ -333,48 +288,25 @@ function summary(events) {
|
|
|
333
288
|
uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens),
|
|
334
289
|
models: modelTotals(events),
|
|
335
290
|
usageTypes: usageTypeTotals(events),
|
|
336
|
-
autoReview: {
|
|
337
|
-
present: autoReviewEvents.length > 0,
|
|
338
|
-
totalTokens: autoReviewTokens,
|
|
339
|
-
turns: autoReviewTurns,
|
|
340
|
-
turnShare: totalTurns > 0 ? (autoReviewTurns / totalTurns) * 100 : 0,
|
|
341
|
-
cachedInputShare: autoReviewInputTokens > 0
|
|
342
|
-
? (autoReviewCachedInputTokens / autoReviewInputTokens) * 100
|
|
343
|
-
: 0,
|
|
344
|
-
},
|
|
345
291
|
};
|
|
346
292
|
}
|
|
347
293
|
|
|
348
294
|
function modelLegendItems(models, totalTokens) {
|
|
349
|
-
|
|
350
|
-
|
|
295
|
+
const known = new Map();
|
|
296
|
+
for (const model of models) {
|
|
297
|
+
const key = ["Sol", "Luna", "Terra", "GPT"].includes(model.model)
|
|
298
|
+
? model.model
|
|
299
|
+
: "Other";
|
|
300
|
+
known.set(key, (known.get(key) ?? 0) + model.totalTokens);
|
|
301
|
+
}
|
|
302
|
+
return ["Luna", "Sol", "Terra", "GPT", "Other"]
|
|
351
303
|
.map((model) => ({
|
|
352
|
-
|
|
353
|
-
|
|
304
|
+
model,
|
|
305
|
+
totalTokens: known.get(model) ?? 0,
|
|
306
|
+
share: totalTokens > 0 ? ((known.get(model) ?? 0) / totalTokens) * 100 : 0,
|
|
354
307
|
}));
|
|
355
308
|
}
|
|
356
309
|
|
|
357
|
-
function visibleUsageTypeItems(items, limit = 5) {
|
|
358
|
-
if (items.length <= limit) return items;
|
|
359
|
-
const visible = items.slice(0, limit - 1);
|
|
360
|
-
const autoReview = items.find((item) => item.key === "auto-review");
|
|
361
|
-
if (autoReview && !visible.includes(autoReview)) {
|
|
362
|
-
visible[visible.length - 1] = autoReview;
|
|
363
|
-
visible.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
364
|
-
}
|
|
365
|
-
const visibleKeys = new Set(visible.map((item) => item.key));
|
|
366
|
-
return [
|
|
367
|
-
...visible,
|
|
368
|
-
{
|
|
369
|
-
key: "other",
|
|
370
|
-
label: "Other",
|
|
371
|
-
totalTokens: items
|
|
372
|
-
.filter((item) => !visibleKeys.has(item.key))
|
|
373
|
-
.reduce((sum, item) => sum + item.totalTokens, 0),
|
|
374
|
-
},
|
|
375
|
-
];
|
|
376
|
-
}
|
|
377
|
-
|
|
378
310
|
function stackedBar(row, width, maximumTokens, options, enabled) {
|
|
379
311
|
const symbol = options.ascii ? "#" : "█";
|
|
380
312
|
const trackSymbol = options.ascii ? "." : "░";
|
|
@@ -404,7 +336,7 @@ function stackedBar(row, width, maximumTokens, options, enabled) {
|
|
|
404
336
|
.join("");
|
|
405
337
|
const blank = colorize(
|
|
406
338
|
trackSymbol.repeat(Math.max(0, width - visibleLength(filled))),
|
|
407
|
-
|
|
339
|
+
TRACK_STYLE,
|
|
408
340
|
enabled,
|
|
409
341
|
);
|
|
410
342
|
return `${filled}${blank}`;
|
|
@@ -426,9 +358,13 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
|
|
|
426
358
|
panelWidth - labelWidth - shareWidth - totalWidth - 1 - rightPadding,
|
|
427
359
|
);
|
|
428
360
|
const maxTokens = allRows[0]?.totalTokens ?? 0;
|
|
429
|
-
const
|
|
361
|
+
const totalCredits = allRows.reduce((sum, item) => sum + item.rateCardCredits, 0) || 1;
|
|
362
|
+
const selectedIndex = Math.min(
|
|
363
|
+
Math.max(0, Math.trunc(Number(options.selectedIndex) || 0)),
|
|
364
|
+
Math.max(0, rows.length - 1),
|
|
365
|
+
);
|
|
430
366
|
const lines = [];
|
|
431
|
-
const heading = colorize("TOKENS BY PROJECT",
|
|
367
|
+
const heading = colorize("TOKENS BY PROJECT", ACCENT_STYLE, enabled);
|
|
432
368
|
const barHeaderWidth = barWidth;
|
|
433
369
|
const maximumLabel = compactMode ? `${compact(maxTokens)} max` : `${compact(maxTokens)} (max)`;
|
|
434
370
|
const axisLabel = barHeaderWidth >= maximumLabel.length + 2 ? maximumLabel : compact(maxTokens);
|
|
@@ -438,29 +374,33 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
|
|
|
438
374
|
lines.push(
|
|
439
375
|
`${fit(heading, labelWidth)}${fit(axisText, barHeaderWidth)}${fit("TOKENS", totalWidth, "right")}${fit("SHARE", shareWidth, "right")}${" ".repeat(rightPadding)}`,
|
|
440
376
|
);
|
|
441
|
-
lines.push(colorize("─".repeat(panelWidth),
|
|
377
|
+
lines.push(colorize("─".repeat(panelWidth), BORDER_STYLE, enabled));
|
|
442
378
|
|
|
443
379
|
for (const [index, row] of rows.entries()) {
|
|
444
|
-
const rankValue = rowOffset + index + 1;
|
|
445
380
|
const share = totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0;
|
|
446
|
-
const selected =
|
|
381
|
+
const selected = index === selectedIndex;
|
|
447
382
|
const caretValue = selected ? (options.ascii ? ">" : "▶") : " ";
|
|
448
|
-
const prefix = `${caretValue} ${
|
|
383
|
+
const prefix = `${caretValue} ${index + 1}. `;
|
|
449
384
|
const projectText = truncateText(displayProject(row), labelWidth - prefix.length);
|
|
450
|
-
const caret = selected ? colorize(caretValue, [1, ...
|
|
451
|
-
const rank = colorize(`${
|
|
385
|
+
const caret = selected ? colorize(caretValue, [1, ...ACCENT_STYLE], enabled) : caretValue;
|
|
386
|
+
const rank = colorize(`${index + 1}.`, TITLE_STYLE, enabled);
|
|
452
387
|
const title = `${caret} ${rank} ${colorize(projectText, TITLE_STYLE, enabled)}`;
|
|
453
388
|
const label = fit(title, labelWidth);
|
|
454
389
|
const metrics = `${fit(compact(row.totalTokens), totalWidth, "right")}${fit(percent(share), shareWidth, "right")}${" ".repeat(rightPadding)}`;
|
|
455
390
|
const bar = stackedBar(row, barWidth, maxTokens, options, enabled);
|
|
456
391
|
const rowLine = `${label}${bar} ${metrics}`;
|
|
457
|
-
const
|
|
392
|
+
const creditShare = row.rateCardCredits > 0 && row.rateCardCredits <= Number.MAX_SAFE_INTEGER
|
|
393
|
+
? row.rateCardCredits
|
|
394
|
+
: 0;
|
|
395
|
+
const detail = `${plural(row.threads, "thread")} · ${percent(
|
|
396
|
+
creditShare > 0 ? (creditShare / totalCredits) * 100 : 0,
|
|
397
|
+
)}${labelWidth >= 35 ? " credits" : ""}`;
|
|
458
398
|
const detailText = truncateText(detail, labelWidth - 4);
|
|
459
399
|
const subtitle = colorize(detailText, SUBTITLE_STYLE, enabled);
|
|
460
400
|
const detailLine = `${fit(` ${subtitle}`, labelWidth)}${" ".repeat(barWidth + 1 + totalWidth + shareWidth + rightPadding)}`;
|
|
461
401
|
if (selected && enabled && options.highlight !== false) {
|
|
462
|
-
lines.push(
|
|
463
|
-
lines.push(
|
|
402
|
+
lines.push(`${SELECTED_BACKGROUND}${fit(rowLine, panelWidth)}${RESET}`);
|
|
403
|
+
lines.push(`${SELECTED_BACKGROUND}${fit(detailLine, panelWidth)}${RESET}`);
|
|
464
404
|
} else {
|
|
465
405
|
lines.push(rowLine);
|
|
466
406
|
lines.push(detailLine);
|
|
@@ -477,41 +417,36 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
|
|
|
477
417
|
const push = (line = "") => {
|
|
478
418
|
lines.push(`${" ".repeat(inset)}${fit(line, contentWidth)}${" ".repeat(inset)}`);
|
|
479
419
|
};
|
|
480
|
-
const divider = () => push(colorize("─".repeat(contentWidth),
|
|
481
|
-
const heading = (value) => colorize(value,
|
|
420
|
+
const divider = () => push(colorize("─".repeat(contentWidth), BORDER_STYLE, enabled));
|
|
421
|
+
const heading = (value) => colorize(value, ACCENT_STYLE, enabled);
|
|
482
422
|
push(heading("MODEL MIX"));
|
|
483
423
|
if (!compactSidebar) push();
|
|
484
|
-
const
|
|
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) {
|
|
424
|
+
for (const item of modelLegendItems(stats.models, stats.totalTokens)) {
|
|
492
425
|
const swatch = colorize("■", modelColor(item.model), enabled);
|
|
493
|
-
push(`${swatch} ${fit(item.model,
|
|
494
|
-
if (item.model === "Auto Review" && stats.autoReview.present) {
|
|
495
|
-
const turnLabel = stats.autoReview.turns === 1 ? "turn" : "turns";
|
|
496
|
-
push(` ${compact(stats.autoReview.turns)} ${turnLabel} · ${percent(stats.autoReview.turnShare)}`);
|
|
497
|
-
push(` ${compact(stats.autoReview.totalTokens)} · ${percent(stats.autoReview.cachedInputShare)} cached`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
const hiddenModelCount = modelItems.length - visibleModelItems.length;
|
|
501
|
-
if (hiddenModelCount > 0) {
|
|
502
|
-
push(colorize(`… ${plural(hiddenModelCount, "more model")}`, DIM, enabled));
|
|
426
|
+
push(`${swatch} ${fit(item.model, Math.max(1, contentWidth - 10))}${fit(percent(item.share), 8, "right")}`);
|
|
503
427
|
}
|
|
504
|
-
|
|
428
|
+
push();
|
|
505
429
|
divider();
|
|
506
430
|
push(heading("USAGE TYPE · TOKENS"));
|
|
507
431
|
if (!compactSidebar) push();
|
|
508
|
-
const usageItems =
|
|
432
|
+
const usageItems = stats.usageTypes.length > 5
|
|
433
|
+
? [
|
|
434
|
+
...stats.usageTypes.slice(0, 4),
|
|
435
|
+
{
|
|
436
|
+
key: "other",
|
|
437
|
+
label: "Other",
|
|
438
|
+
totalTokens: stats.usageTypes
|
|
439
|
+
.slice(4)
|
|
440
|
+
.reduce((sum, item) => sum + item.totalTokens, 0),
|
|
441
|
+
},
|
|
442
|
+
]
|
|
443
|
+
: stats.usageTypes;
|
|
509
444
|
for (const item of usageItems) {
|
|
510
445
|
const swatch = "■";
|
|
511
446
|
const share = stats.totalTokens > 0 ? (item.totalTokens / stats.totalTokens) * 100 : 0;
|
|
512
447
|
push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
|
|
513
448
|
}
|
|
514
|
-
|
|
449
|
+
push();
|
|
515
450
|
divider();
|
|
516
451
|
push(heading("CACHE · INPUT"));
|
|
517
452
|
if (!compactSidebar) push();
|
|
@@ -525,7 +460,7 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
|
|
|
525
460
|
push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
|
|
526
461
|
}
|
|
527
462
|
if (quota?.available) {
|
|
528
|
-
|
|
463
|
+
push();
|
|
529
464
|
divider();
|
|
530
465
|
push(heading("RESET CYCLE"));
|
|
531
466
|
if (!compactSidebar) push();
|
|
@@ -545,28 +480,29 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
|
|
|
545
480
|
? { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", tm: "+", bm: "+" }
|
|
546
481
|
: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", tm: "┬", bm: "┴" };
|
|
547
482
|
const rows = Math.max(leftLines.length, rightLines?.length ?? 0);
|
|
548
|
-
const
|
|
483
|
+
const border = (value) => colorize(value, BORDER_STYLE, enabled);
|
|
484
|
+
const top = border(`${glyphs.tl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.tm : glyphs.tr}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.tr : ""}`);
|
|
549
485
|
const body = [];
|
|
550
486
|
for (let index = 0; index < rows; index += 1) {
|
|
551
487
|
const left = fit(leftLines[index] ?? "", leftWidth);
|
|
552
488
|
if (rightLines) {
|
|
553
489
|
const right = fit(rightLines[index] ?? "", rightWidth);
|
|
554
|
-
body.push(`${glyphs.v}${left}${glyphs.v}${right}${glyphs.v}`);
|
|
490
|
+
body.push(`${border(glyphs.v)}${left}${border(glyphs.v)}${right}${border(glyphs.v)}`);
|
|
555
491
|
} else {
|
|
556
|
-
body.push(`${glyphs.v}${left}${glyphs.v}`);
|
|
492
|
+
body.push(`${border(glyphs.v)}${left}${border(glyphs.v)}`);
|
|
557
493
|
}
|
|
558
494
|
}
|
|
559
|
-
const bottom = `${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}
|
|
495
|
+
const bottom = border(`${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}`);
|
|
560
496
|
return [top, ...body, bottom];
|
|
561
497
|
}
|
|
562
498
|
|
|
563
499
|
function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
564
500
|
const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
|
|
565
501
|
const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
|
|
566
|
-
const mode = colorize(
|
|
502
|
+
const mode = colorize(options.range === "week" ? "7 DAYS" : "DAY", [1, ...ACCENT_STYLE], enabled);
|
|
567
503
|
const metric = (value, label) =>
|
|
568
|
-
`${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label,
|
|
569
|
-
const separator = colorize("·",
|
|
504
|
+
`${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, SECONDARY_STYLE, enabled)}`;
|
|
505
|
+
const separator = colorize("·", SECONDARY_STYLE, enabled);
|
|
570
506
|
const join = ` ${separator} `;
|
|
571
507
|
const alignHeader = (line) => fit(` ${line}`, frameWidth);
|
|
572
508
|
const fullLine = [
|
|
@@ -582,18 +518,14 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
582
518
|
return [alignHeader(fullLine)];
|
|
583
519
|
}
|
|
584
520
|
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
? dateLabel(bounds, options.range)
|
|
590
|
-
: dateLabel(bounds, options.range).replace(/ 20\d{2}$/, "")
|
|
591
|
-
).replace(" – ", "–");
|
|
592
|
-
const compactMode = rangeModeLabel(bounds, options.range, true);
|
|
521
|
+
const compactDate = dateLabel(bounds, options.range)
|
|
522
|
+
.replace(/ 20\d{2}$/, "")
|
|
523
|
+
.replace(" – ", "–");
|
|
524
|
+
const compactMode = options.range === "week" ? "7D" : "DAY";
|
|
593
525
|
const compactLine = [
|
|
594
526
|
left,
|
|
595
527
|
colorize(compactDate, TEXT_STYLE, enabled),
|
|
596
|
-
colorize(compactMode, [1, ...
|
|
528
|
+
colorize(compactMode, [1, ...ACCENT_STYLE], enabled),
|
|
597
529
|
metric(`${compact(stats.totalTokens)}`, "T"),
|
|
598
530
|
metric(stats.calls.toLocaleString("en-US"), "C"),
|
|
599
531
|
metric(stats.threads.toLocaleString("en-US"), "TH"),
|
|
@@ -607,7 +539,7 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
|
607
539
|
const minimalLine = [
|
|
608
540
|
minimalTitle,
|
|
609
541
|
colorize(compactDate.replaceAll(" ", ""), TEXT_STYLE, enabled),
|
|
610
|
-
colorize(compactMode, [1, ...
|
|
542
|
+
colorize(compactMode, [1, ...ACCENT_STYLE], enabled),
|
|
611
543
|
compact(stats.totalTokens),
|
|
612
544
|
compact(stats.calls),
|
|
613
545
|
compact(stats.threads),
|
|
@@ -624,7 +556,7 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
|
|
|
624
556
|
const columns = options.width ?? (Number(process.stdout.columns) || 120);
|
|
625
557
|
const frameWidth = Math.max(38, Math.min(158, columns - 2));
|
|
626
558
|
const sideBySide = options.forceSideBySide ?? frameWidth >= 100;
|
|
627
|
-
const sideWidth = sideBySide ?
|
|
559
|
+
const sideWidth = sideBySide ? 26 : 0;
|
|
628
560
|
const leftWidth = sideBySide ? frameWidth - sideWidth - 1 : frameWidth;
|
|
629
561
|
const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
|
|
630
562
|
const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
|
|
@@ -636,23 +568,21 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
|
|
|
636
568
|
lines.push("");
|
|
637
569
|
lines.push(...sidebarLines(stats, frameWidth, enabled, options, quota));
|
|
638
570
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
);
|
|
650
|
-
}
|
|
571
|
+
lines.push("");
|
|
572
|
+
lines.push(
|
|
573
|
+
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",
|
|
577
|
+
SECONDARY_STYLE,
|
|
578
|
+
enabled,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
651
581
|
return lines.join("\n");
|
|
652
582
|
}
|
|
653
583
|
|
|
654
|
-
export const SCREEN_BASE = "\u001b[38;
|
|
655
|
-
const PANEL_BASE = "\u001b[38;
|
|
584
|
+
export const SCREEN_BASE = "\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m";
|
|
585
|
+
const PANEL_BASE = "\u001b[38;2;255;255;255m\u001b[48;2;24;24;24m";
|
|
656
586
|
|
|
657
587
|
function paintFullscreenLine(line, width, background, enabled) {
|
|
658
588
|
const fitted = fit(line, width);
|
|
@@ -666,84 +596,30 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
|
|
|
666
596
|
const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
|
|
667
597
|
const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
|
|
668
598
|
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
|
-
|
|
723
599
|
const staticOutput = renderTerminal({
|
|
724
|
-
options:
|
|
600
|
+
options: {
|
|
601
|
+
...options,
|
|
602
|
+
forceColor: enabled,
|
|
603
|
+
forceSideBySide: frameWidth >= 84,
|
|
604
|
+
highlight: false,
|
|
605
|
+
compactSidebar: true,
|
|
606
|
+
width: frameWidth + 2,
|
|
607
|
+
},
|
|
725
608
|
snapshot,
|
|
726
609
|
bounds,
|
|
727
610
|
events,
|
|
728
|
-
rows
|
|
611
|
+
rows,
|
|
729
612
|
allRows,
|
|
730
613
|
});
|
|
731
|
-
|
|
614
|
+
const staticLines = staticOutput.split("\n");
|
|
615
|
+
staticLines.pop();
|
|
732
616
|
if (staticLines.at(-1) === "") staticLines.pop();
|
|
733
|
-
const
|
|
734
|
-
const
|
|
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();
|
|
617
|
+
const summaryLine = staticLines.shift() ?? "";
|
|
618
|
+
const help = colorize("↑/↓ move • j/k move • q/esc quit", SECONDARY_STYLE, enabled);
|
|
741
619
|
const content = [
|
|
742
|
-
|
|
743
|
-
? []
|
|
744
|
-
: [{ line: summaryLine, background: SCREEN_BASE }]),
|
|
620
|
+
{ line: summaryLine, background: SCREEN_BASE },
|
|
745
621
|
...staticLines.map((line) => ({ line, background: PANEL_BASE })),
|
|
746
|
-
|
|
622
|
+
{ line: "", background: SCREEN_BASE },
|
|
747
623
|
{ line: help, background: SCREEN_BASE },
|
|
748
624
|
];
|
|
749
625
|
const topPadding = Math.max(0, Math.floor((screenHeight - content.length) / 2));
|