tokens-metric 0.4.7 → 0.4.9

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.
Files changed (2) hide show
  1. package/dist/tui/index.js +59 -9
  2. package/package.json +1 -1
package/dist/tui/index.js CHANGED
@@ -49,6 +49,7 @@ function App() {
49
49
  // openTab: which panel is expanded (null = all collapsed)
50
50
  const [focusedTab, setFocusedTab] = useState(1);
51
51
  const [openTab, setOpenTab] = useState(null);
52
+ const [sessionCursor, setSessionCursor] = useState(0);
52
53
  const startedAtRef = useRef(Date.now());
53
54
  // useInput requires raw mode (interactive TTY). Skip it when stdin is piped
54
55
  // or otherwise non-interactive, so `node dist/tui/index.js | cat` still
@@ -63,7 +64,18 @@ function App() {
63
64
  setOpenTab(null);
64
65
  return;
65
66
  }
66
- // Arrow keys move the cursor
67
+ // ↑↓ navigate sessions when Sessions tab is open
68
+ if (openTab === 3) {
69
+ if (key.upArrow) {
70
+ setSessionCursor((c) => Math.max(0, c - 1));
71
+ return;
72
+ }
73
+ if (key.downArrow) {
74
+ setSessionCursor((c) => c + 1);
75
+ return;
76
+ }
77
+ }
78
+ // Arrow keys move the tab cursor
67
79
  if (key.leftArrow) {
68
80
  setFocusedTab((t) => (t > 1 ? (t - 1) : t));
69
81
  return;
@@ -238,7 +250,7 @@ function App() {
238
250
  : claudeLastTailAt ?? codexLastTailAt;
239
251
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, codexDetected: codexDetected, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now, updateAvailable: updateAvailable }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [claudeStats
240
252
  ? _jsx(SessionStatusBar, { stats: claudeStats, ratePerSec: claudeRate, now: now, series: claudeSeries })
241
- : !codexStats && _jsx(SessionStatusBar, { stats: null, ratePerSec: 0, now: now, series: claudeSeries }), codexStats && (_jsx(Box, { marginTop: claudeStats ? 1 : 0, children: _jsx(SessionStatusBar, { stats: codexStats, ratePerSec: codexRate, now: now, series: codexSeries }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TabBar, { focusedTab: focusedTab, openTab: openTab }) }), openTab !== null && (_jsxs(Box, { marginTop: 1, children: [openTab === 1 && (_jsx(BreakdownPanel, { stats: primaryStats, series: primarySeries, ratePerSec: primaryRate })), openTab === 2 && _jsx(HistoryPanel, { history: history }), openTab === 3 && _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now }), openTab === 4 && (_jsx(TranscriptsPanel, { transcripts: transcripts, activePath: claudePath, now: now }))] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "magenta", children: "q" }), " quit \u00B7", ' ', _jsx(Text, { color: "magenta", children: "\u2190\u2192" }), " move \u00B7", ' ', _jsx(Text, { color: "magenta", children: "enter" }), " open/close \u00B7", ' ', _jsx(Text, { color: "magenta", children: "1\u20134" }), " jump \u00B7 pricing is", ' ', _jsx(Text, { italic: true, children: "API-equivalent" }), ", not your real bill on a subscription"] }) })] }));
253
+ : !codexStats && _jsx(SessionStatusBar, { stats: null, ratePerSec: 0, now: now, series: claudeSeries }), codexStats && (_jsx(Box, { marginTop: claudeStats ? 1 : 0, children: _jsx(SessionStatusBar, { stats: codexStats, ratePerSec: codexRate, now: now, series: codexSeries }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TabBar, { focusedTab: focusedTab, openTab: openTab }) }), openTab !== null && (_jsxs(Box, { marginTop: 1, children: [openTab === 1 && (_jsx(BreakdownPanel, { stats: primaryStats, series: primarySeries, ratePerSec: primaryRate })), openTab === 2 && _jsx(HistoryPanel, { history: history }), openTab === 3 && _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now, cursor: Math.min(sessionCursor, Math.max(0, todaySessions.length - 1)) }), openTab === 4 && (_jsx(TranscriptsPanel, { transcripts: transcripts, activePath: claudePath, now: now }))] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "magenta", children: "q" }), " quit \u00B7", ' ', _jsx(Text, { color: "magenta", children: "\u2190\u2192" }), " move \u00B7", ' ', _jsx(Text, { color: "magenta", children: "enter" }), " open/close \u00B7", ' ', _jsx(Text, { color: "magenta", children: "1\u20134" }), " jump", openTab === 3 && (_jsxs(Text, { dimColor: true, children: [' · ', _jsx(Text, { color: "magenta", children: "\u2191\u2193" }), " select session"] })), _jsx(Text, { dimColor: true, children: " \u00B7 pricing is " }), _jsx(Text, { italic: true, dimColor: true, children: "API-equivalent" }), _jsx(Text, { dimColor: true, children: ", not your real bill on a subscription" })] }) })] }));
242
254
  }
243
255
  // ── Header ───────────────────────────────────────────────────────────────────
244
256
  function Header({ auth, codexDetected, sessionsToday, projectsToday, lastTailAt, startedAt, now, updateAvailable, }) {
@@ -327,7 +339,7 @@ function BarRow({ label, value, max, total, color, cost, }) {
327
339
  }
328
340
  // ── History panel ────────────────────────────────────────────────────────────
329
341
  function HistoryPanel({ history }) {
330
- return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ['Usage history', _jsx(Text, { bold: false, color: "blue", children: " \u00B7 refreshes every 60s" })] }), !history ? (_jsx(Text, { dimColor: true, children: "Scanning\u2026" })) : history.scannedFiles === 0 ? (_jsx(Text, { dimColor: true, children: "No transcripts found." })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(DualBarChart, { days: history.last7Days, now: history.generatedAt }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(HistoryRow, { label: "", today: "Today", d7: "7d", d30: "30d", dim: true }), _jsx(HistoryRow, { label: "Tokens ", today: fmtNumber(bucketTokens(history.today)), d7: fmtNumber(bucketTokens(history.d7)), d30: fmtNumber(bucketTokens(history.d30)) }), _jsx(HistoryRow, { label: "Cost~ ", today: fmtCost(bucketCostUSD(history.today)), d7: fmtCost(bucketCostUSD(history.d7)), d30: fmtCost(bucketCostUSD(history.d30)) }), _jsx(HistoryRow, { label: "Sessions", today: String(history.today.sessions.size), d7: String(history.d7.sessions.size), d30: String(history.d30.sessions.size) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [`scanned ${history.scannedFiles} transcripts`, history.oldestMtimeMs !== null &&
342
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ['Usage history', _jsx(Text, { bold: false, color: "blue", children: " \u00B7 refreshes every 60s" })] }), !history ? (_jsx(Text, { dimColor: true, children: "Scanning\u2026" })) : history.scannedFiles === 0 ? (_jsx(Text, { dimColor: true, children: "No transcripts found." })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(DualBarChart, { days: history.last7Days, now: history.generatedAt }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HistoryRow, { label: "", today: "Today", d7: "7d", d30: "30d", dim: true }), _jsx(HistoryRow, { label: "Tokens ", today: fmtNumber(bucketTokens(history.today)), d7: fmtNumber(bucketTokens(history.d7)), d30: fmtNumber(bucketTokens(history.d30)) }), _jsx(HistoryRow, { label: "Cost~ ", today: fmtCost(bucketCostUSD(history.today)), d7: fmtCost(bucketCostUSD(history.d7)), d30: fmtCost(bucketCostUSD(history.d30)) }), _jsx(HistoryRow, { label: "Sessions", today: String(history.today.sessions.size), d7: String(history.d7.sessions.size), d30: String(history.d30.sessions.size) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [`scanned ${history.scannedFiles} transcripts`, history.oldestMtimeMs !== null &&
331
343
  ` · data since ${fmtDate(history.oldestMtimeMs)} (${daysAgo(history.oldestMtimeMs, history.generatedAt)} days)`] }) })] }))] }));
332
344
  }
333
345
  // ── Dual bar chart ────────────────────────────────────────────────────────────
@@ -349,7 +361,11 @@ function DualBarChart({ days, now }) {
349
361
  const maxTokens = Math.max(1, ...days.flatMap((d) => [claudeTokens(d.byModel), codexTokens(d.byModel)]));
350
362
  const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
351
363
  const todayStart = (() => { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.getTime(); })();
352
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: 'last 7 days' }), days.map((day) => {
364
+ const fmtDay = (ms) => {
365
+ const d = new Date(ms);
366
+ return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
367
+ };
368
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["last 7 days \u00B7 ", _jsx(Text, { color: "cyan", children: "\u2588" }), " claude ", _jsx(Text, { color: "magenta", children: "\u2588" }), " codex"] }), days.map((day) => {
353
369
  const ct = claudeTokens(day.byModel);
354
370
  const cxt = codexTokens(day.byModel);
355
371
  const cc = claudeCost(day.byModel);
@@ -357,9 +373,16 @@ function DualBarChart({ days, now }) {
357
373
  .filter(([m]) => m === 'codex')
358
374
  .reduce((s, [m, u]) => s + (estimateCostUSD(m, u) ?? 0), 0);
359
375
  const isToday = day.dayStart === todayStart;
360
- const label = isToday ? 'today' : DAY_LABELS[new Date(day.dayStart).getDay()];
376
+ const dayName = isToday ? 'today' : DAY_LABELS[new Date(day.dayStart).getDay()];
377
+ const label = `${dayName.padEnd(5)} ${fmtDay(day.dayStart)}`;
361
378
  const hasData = ct > 0 || cxt > 0;
362
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { bold: isToday, color: isToday ? 'white' : undefined, children: [label.padEnd(6), !hasData && _jsx(Text, { dimColor: true, children: " no data" })] }), ct > 0 && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: ' claude ' }), _jsx(Text, { color: "cyan", children: solidBar(ct / maxTokens, CHART_BAR_WIDTH) }), _jsx(Text, { children: ' ' }), _jsx(Text, { bold: true, children: fmtNumber(ct).padStart(7) }), cc > 0 && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cc)}` })] })), cxt > 0 && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: ' codex ' }), _jsx(Text, { color: "magenta", children: solidBar(cxt / maxTokens, CHART_BAR_WIDTH) }), _jsx(Text, { children: ' ' }), _jsx(Text, { bold: true, children: fmtNumber(cxt).padStart(7) }), cxc > 0 && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cxc)}` })] }))] }, day.dayStart));
379
+ if (!hasData) {
380
+ return (_jsxs(Text, { dimColor: true, children: [label, " \u2500"] }, day.dayStart));
381
+ }
382
+ const labelPad = ' '.repeat(label.length + 2);
383
+ return (_jsxs(Box, { flexDirection: "column", children: [ct > 0 && (_jsxs(Text, { children: [_jsxs(Text, { bold: isToday, color: isToday ? 'white' : undefined, children: [label, " "] }), _jsx(Text, { dimColor: true, children: "claude " }), _jsx(Text, { color: "cyan", children: solidBar(ct / maxTokens, CHART_BAR_WIDTH) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: fmtNumber(ct).padStart(7) }), cc > 0 && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cc)}` })] })), cxt > 0 && (_jsxs(Text, { children: [ct > 0
384
+ ? _jsx(Text, { children: labelPad })
385
+ : _jsxs(Text, { bold: isToday, color: isToday ? 'white' : undefined, children: [label, " "] }), _jsx(Text, { dimColor: true, children: "codex " }), _jsx(Text, { color: "magenta", children: solidBar(cxt / maxTokens, CHART_BAR_WIDTH) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: fmtNumber(cxt).padStart(7) }), cxc > 0 && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cxc)}` })] }))] }, day.dayStart));
363
386
  })] }));
364
387
  }
365
388
  function solidBar(ratio, width) {
@@ -388,8 +411,9 @@ function fmtTopModel(b) {
388
411
  return m ? shortModel(m) : '—';
389
412
  }
390
413
  // ── Today's sessions panel ───────────────────────────────────────────────────
391
- function TodaySessionsPanel({ sessions, now }) {
392
- return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ["Today's sessions", _jsx(Text, { bold: false, color: "blue", children: ` · ${sessions.length} ${plural(sessions.length, 'session', 'sessions')} today` })] }), sessions.map((s) => {
414
+ function TodaySessionsPanel({ sessions, now, cursor, }) {
415
+ return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ["Today's sessions", _jsx(Text, { bold: false, color: "blue", children: ` · ${sessions.length} ${plural(sessions.length, 'session', 'sessions')} today` })] }), sessions.map((s, idx) => {
416
+ const isSelected = idx === cursor;
393
417
  const tokens = Object.values(s.byModel).reduce((acc, u) => acc + totalTokens(u), 0);
394
418
  const cost = Object.entries(s.byModel).reduce((acc, [m, u]) => {
395
419
  const c = estimateCostUSD(m, u);
@@ -403,9 +427,35 @@ function TodaySessionsPanel({ sessions, now }) {
403
427
  const duration = s.startedAt !== null && s.endedAt !== null
404
428
  ? fmtDuration(s.endedAt - s.startedAt)
405
429
  : null;
406
- return (_jsxs(Text, { children: [_jsx(Text, { color: s.isActive ? 'green' : 'gray', children: s.isActive ? '▶ ' : ' ' }), _jsx(Text, { bold: s.isActive, children: s.startedAt ? fmtTime(s.startedAt) : '??:??' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { wrap: "truncate-middle", dimColor: !s.isActive, children: OPTS.reveal ? (s.cwd ?? '—') : displayCwd(s.cwd) }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: topModel ? shortModel(topModel) : '—' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { children: fmtNumber(tokens) }), cost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cost)}` }), duration && _jsx(Text, { dimColor: true, children: ` ${duration}` }), s.isActive && _jsx(Text, { color: "green", children: " active" })] }, s.sessionId));
430
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? 'cyan' : s.isActive ? 'green' : 'gray', children: isSelected ? '› ' : s.isActive ? '▶ ' : ' ' }), _jsx(Text, { bold: isSelected || s.isActive, color: isSelected ? 'cyan' : undefined, children: s.startedAt ? fmtTime(s.startedAt) : '??:??' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { wrap: "truncate-middle", dimColor: !isSelected && !s.isActive, color: isSelected ? 'cyan' : undefined, children: OPTS.reveal ? (s.cwd ?? '—') : displayCwd(s.cwd) }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: topModel ? shortModel(topModel) : '—' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { bold: isSelected, children: fmtNumber(tokens) }), cost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cost)}` }), duration && _jsx(Text, { dimColor: true, children: ` ${duration}` }), s.isActive && _jsx(Text, { color: "green", children: " active" })] }), isSelected && _jsx(SessionDetail, { session: s })] }, s.sessionId));
407
431
  })] }));
408
432
  }
433
+ function SessionDetail({ session: s }) {
434
+ const DETAIL_BAR = 16;
435
+ // Aggregate all models into one Usage for the bar chart
436
+ const totals = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
437
+ for (const u of Object.values(s.byModel)) {
438
+ totals.input_tokens += u.input_tokens;
439
+ totals.output_tokens += u.output_tokens;
440
+ totals.cache_creation_input_tokens += u.cache_creation_input_tokens;
441
+ totals.cache_read_input_tokens += u.cache_read_input_tokens;
442
+ }
443
+ const total = totalTokens(totals);
444
+ const max = Math.max(1, totals.input_tokens, totals.output_tokens, totals.cache_creation_input_tokens, totals.cache_read_input_tokens);
445
+ const allModels = Object.keys(s.byModel);
446
+ const primaryModel = allModels.reduce((best, m) => best === null || totalTokens(s.byModel[m]) > totalTokens(s.byModel[best]) ? m : best, null) ?? '';
447
+ const totalCost = Object.entries(s.byModel).reduce((acc, [m, u]) => {
448
+ const c = estimateCostUSD(m, u);
449
+ if (c === null)
450
+ return acc;
451
+ return (acc ?? 0) + c;
452
+ }, null);
453
+ return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Input " }), _jsx(Text, { color: "cyan", children: bar(totals.input_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.input_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.input_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'input', totals.input_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Output " }), _jsx(Text, { color: "green", children: bar(totals.output_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.output_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.output_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'output', totals.output_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] }), totals.cache_creation_input_tokens > 0 && (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "C.write " }), _jsx(Text, { color: "yellow", children: bar(totals.cache_creation_input_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.cache_creation_input_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.cache_creation_input_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'cacheWrite', totals.cache_creation_input_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] })), totals.cache_read_input_tokens > 0 && (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "C.read " }), _jsx(Text, { color: "magenta", children: bar(totals.cache_read_input_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.cache_read_input_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.cache_read_input_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'cacheRead', totals.cache_read_input_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] }))] }), _jsx(Text, { dimColor: true, children: '─'.repeat(DETAIL_BAR + 32) }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: 'Total ' }), _jsx(Text, { bold: true, children: fmtNumber(total).padStart(DETAIL_BAR + 2 + 7) }), totalCost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(totalCost)}` })] }), allModels.length > 1 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "by model" }), allModels.map((m) => {
454
+ const mu = s.byModel[m];
455
+ const mc = estimateCostUSD(m, mu);
456
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [" ", shortModel(m)] }), _jsx(Text, { dimColor: true, children: " \u03A3 " }), _jsx(Text, { children: fmtNumber(totalTokens(mu)) }), mc !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(mc)}` })] }, m));
457
+ })] }))] }) }));
458
+ }
409
459
  function fmtDuration(ms) {
410
460
  const totalSec = Math.floor(ms / 1000);
411
461
  const h = Math.floor(totalSec / 3600);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {