tokens-metric 0.4.1 → 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.
@@ -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)}`;
@@ -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;
@@ -14,6 +14,7 @@ export async function tailTranscript(path) {
14
14
  totals: EMPTY_USAGE(),
15
15
  byModel: {},
16
16
  messageCount: 0,
17
+ lastMsgUsage: null,
17
18
  };
18
19
  const listeners = [];
19
20
  const notify = () => listeners.forEach((l) => l(stats));
package/dist/tui/index.js CHANGED
@@ -7,7 +7,7 @@ import { spawnSync } from 'node:child_process';
7
7
  import { findActiveTranscript, listTranscripts } from '../core/parser.js';
8
8
  import { tailTranscript } from '../core/tailer.js';
9
9
  import { detectAuth } from '../core/detect.js';
10
- import { categoryCostUSD, estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
10
+ import { categoryCostUSD, contextWindowSize, estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
11
11
  import { buildHistory, bucketCostUSD, bucketTokens, bucketTopModel, getTodaySessions, } from '../core/history.js';
12
12
  import { totalTokens } from '../core/types.js';
13
13
  import { anonymizePath } from '../core/privacy.js';
@@ -245,7 +245,14 @@ function BreakdownPanel({ stats, series, ratePerSec, }) {
245
245
  const model = stats.lastModel ?? '';
246
246
  const cacheDenom = u.input_tokens + u.cache_read_input_tokens;
247
247
  const hitRatio = cacheDenom > 0 ? u.cache_read_input_tokens / cacheDenom : null;
248
- return (_jsxs(Box, { flexDirection: "column", children: [_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]) => {
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]) => {
249
256
  const c = estimateCostUSD(m, mu);
250
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));
251
258
  })] }))] }));
@@ -350,6 +357,10 @@ function displayCwd(cwd) {
350
357
  return '~ (home)';
351
358
  return out;
352
359
  }
360
+ function ctxWindowBar(ratio, width) {
361
+ const filled = Math.round(Math.min(1, ratio) * width);
362
+ return '█'.repeat(filled) + '░'.repeat(width - filled);
363
+ }
353
364
  function intensityColor(ratio) {
354
365
  if (ratio <= 0)
355
366
  return 'gray';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {