tokens-metric 0.4.0 → 0.4.2
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/dist/core/format.js +8 -0
- package/dist/core/parser.js +3 -0
- package/dist/core/tailer.js +1 -0
- package/dist/tui/index.js +43 -2
- package/package.json +1 -1
package/dist/core/format.js
CHANGED
|
@@ -53,6 +53,14 @@ export function categoryCostUSD(model, category, tokens) {
|
|
|
53
53
|
: p.cacheRead;
|
|
54
54
|
return (tokens * rate) / 1_000_000;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns the context window size in tokens for a given model.
|
|
58
|
+
* All current Claude models share a 200k window. Falls back to 200k for
|
|
59
|
+
* unknown models so the bar is always renderable.
|
|
60
|
+
*/
|
|
61
|
+
export function contextWindowSize(_model) {
|
|
62
|
+
return 200_000;
|
|
63
|
+
}
|
|
56
64
|
export function fmtUSD(n) {
|
|
57
65
|
if (n < 0.01)
|
|
58
66
|
return `$${n.toFixed(4)}`;
|
package/dist/core/parser.js
CHANGED
|
@@ -79,6 +79,7 @@ export async function aggregateTranscript(path) {
|
|
|
79
79
|
totals: EMPTY_USAGE(),
|
|
80
80
|
byModel: {},
|
|
81
81
|
messageCount: 0,
|
|
82
|
+
lastMsgUsage: null,
|
|
82
83
|
};
|
|
83
84
|
const stream = createReadStream(path, { encoding: 'utf8' });
|
|
84
85
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
@@ -129,6 +130,8 @@ export function applyLine(stats, line) {
|
|
|
129
130
|
stats.totals = addUsage(stats.totals, u);
|
|
130
131
|
stats.byModel[model] = addUsage(stats.byModel[model] ?? EMPTY_USAGE(), u);
|
|
131
132
|
stats.messageCount += 1;
|
|
133
|
+
// Overwrite — we only care about the most recent turn's context footprint.
|
|
134
|
+
stats.lastMsgUsage = { ...EMPTY_USAGE(), ...u };
|
|
132
135
|
}
|
|
133
136
|
function numberOr0(v) {
|
|
134
137
|
return typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
package/dist/core/tailer.js
CHANGED
package/dist/tui/index.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
5
7
|
import { findActiveTranscript, listTranscripts } from '../core/parser.js';
|
|
6
8
|
import { tailTranscript } from '../core/tailer.js';
|
|
7
9
|
import { detectAuth } from '../core/detect.js';
|
|
8
|
-
import { categoryCostUSD, estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
|
|
10
|
+
import { categoryCostUSD, contextWindowSize, estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
|
|
9
11
|
import { buildHistory, bucketCostUSD, bucketTokens, bucketTopModel, getTodaySessions, } from '../core/history.js';
|
|
10
12
|
import { totalTokens } from '../core/types.js';
|
|
11
13
|
import { anonymizePath } from '../core/privacy.js';
|
|
@@ -243,7 +245,14 @@ function BreakdownPanel({ stats, series, ratePerSec, }) {
|
|
|
243
245
|
const model = stats.lastModel ?? '';
|
|
244
246
|
const cacheDenom = u.input_tokens + u.cache_read_input_tokens;
|
|
245
247
|
const hitRatio = cacheDenom > 0 ? u.cache_read_input_tokens / cacheDenom : null;
|
|
246
|
-
|
|
248
|
+
// Context window fill — based on last message's input footprint
|
|
249
|
+
const ctxLimit = contextWindowSize(model);
|
|
250
|
+
const lastMsg = stats.lastMsgUsage;
|
|
251
|
+
const ctxUsed = lastMsg
|
|
252
|
+
? lastMsg.input_tokens + lastMsg.cache_read_input_tokens + lastMsg.cache_creation_input_tokens
|
|
253
|
+
: null;
|
|
254
|
+
const ctxRatio = ctxUsed !== null ? ctxUsed / ctxLimit : null;
|
|
255
|
+
return (_jsxs(Box, { flexDirection: "column", children: [ctxRatio !== null && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Context " }), _jsx(Text, { color: ctxRatio > 0.9 ? 'red' : ctxRatio > 0.7 ? 'yellow' : 'green', children: ctxWindowBar(ctxRatio, BAR_WIDTH) }), _jsx(Text, { children: ' ' }), _jsx(Text, { bold: true, color: ctxRatio > 0.9 ? 'red' : ctxRatio > 0.7 ? 'yellow' : 'green', children: fmtNumber(ctxUsed) }), _jsxs(Text, { dimColor: true, children: [" / ", fmtNumber(ctxLimit), " "] }), _jsxs(Text, { color: ctxRatio > 0.9 ? 'red' : ctxRatio > 0.7 ? 'yellow' : 'green', children: [(ctxRatio * 100).toFixed(1), "%"] }), ctxRatio > 0.9 && _jsx(Text, { color: "red", children: " \u26A0 near limit" })] }), _jsxs(Text, { dimColor: true, children: [" last turn \u00B7 ", fmtNumber(ctxLimit - ctxUsed), " tokens remaining"] })] })), _jsx(BarRow, { label: "Input ", value: u.input_tokens, max: max, total: total, color: "cyan", cost: categoryCostUSD(model, 'input', u.input_tokens) }), _jsx(BarRow, { label: "Output ", value: u.output_tokens, max: max, total: total, color: "green", cost: categoryCostUSD(model, 'output', u.output_tokens) }), _jsx(BarRow, { label: "C. write ", value: u.cache_creation_input_tokens, max: max, total: total, color: "yellow", cost: categoryCostUSD(model, 'cacheWrite', u.cache_creation_input_tokens) }), _jsx(BarRow, { label: "C. read ", value: u.cache_read_input_tokens, max: max, total: total, color: "magenta", cost: categoryCostUSD(model, 'cacheRead', u.cache_read_input_tokens) }), hitRatio !== null && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Cache hit" }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsxs(Text, { color: hitRatio > 0.9 ? 'green' : hitRatio > 0.6 ? 'yellow' : 'red', children: [(hitRatio * 100).toFixed(1), "%"] }), _jsx(Text, { color: hitRatio > 0.9 ? 'green' : hitRatio > 0.6 ? 'yellow' : 'red', children: hitRatio > 0.9 ? ' ✓ excellent' : hitRatio > 0.6 ? ' ⚠ degraded' : ' ✗ poor' })] }) })), Object.keys(stats.byModel).length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "By model" }), Object.entries(stats.byModel).map(([m, mu]) => {
|
|
247
256
|
const c = estimateCostUSD(m, mu);
|
|
248
257
|
return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: shortModel(m) }), _jsx(Text, { dimColor: true, children: " \u03A3 " }), _jsx(Text, { children: fmtNumber(totalTokens(mu)) }), c !== null && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " ~" }), _jsx(Text, { children: fmtUSD(c) })] }))] }, m));
|
|
249
258
|
})] }))] }));
|
|
@@ -348,6 +357,10 @@ function displayCwd(cwd) {
|
|
|
348
357
|
return '~ (home)';
|
|
349
358
|
return out;
|
|
350
359
|
}
|
|
360
|
+
function ctxWindowBar(ratio, width) {
|
|
361
|
+
const filled = Math.round(Math.min(1, ratio) * width);
|
|
362
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
363
|
+
}
|
|
351
364
|
function intensityColor(ratio) {
|
|
352
365
|
if (ratio <= 0)
|
|
353
366
|
return 'gray';
|
|
@@ -380,4 +393,32 @@ function timeAgo(ms) {
|
|
|
380
393
|
return `${h}h`;
|
|
381
394
|
return `${Math.floor(h / 24)}d`;
|
|
382
395
|
}
|
|
396
|
+
// ── Pre-start update prompt ───────────────────────────────────────────────────
|
|
397
|
+
async function promptForUpdate() {
|
|
398
|
+
const latest = await checkForUpdate(pkg.version);
|
|
399
|
+
if (!latest || !process.stdin.isTTY)
|
|
400
|
+
return;
|
|
401
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
402
|
+
const answer = await new Promise((resolve) => {
|
|
403
|
+
rl.question(`\n⚡ tokens-metric v${latest} available (you have v${pkg.version})\n` +
|
|
404
|
+
` Install now? [Y/n] `, (ans) => { rl.close(); resolve(ans); });
|
|
405
|
+
});
|
|
406
|
+
process.stdout.write('\n');
|
|
407
|
+
if (answer.trim().toLowerCase() !== 'n') {
|
|
408
|
+
process.stdout.write('Installing tokens-metric@latest…\n');
|
|
409
|
+
const result = spawnSync('npm', ['install', '-g', 'tokens-metric'], {
|
|
410
|
+
stdio: 'inherit',
|
|
411
|
+
shell: true,
|
|
412
|
+
});
|
|
413
|
+
if (result.status === 0) {
|
|
414
|
+
process.stdout.write(`\n✓ Updated to v${latest} — restarting…\n\n`);
|
|
415
|
+
spawnSync(process.execPath, process.argv.slice(1), { stdio: 'inherit', shell: false });
|
|
416
|
+
process.exit(0);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
process.stdout.write('\n✗ Install failed — starting current version anyway.\n\n');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
await promptForUpdate();
|
|
383
424
|
render(_jsx(App, {}));
|