tokens-metric 0.4.6 → 0.4.7

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.
@@ -91,10 +91,16 @@ function aggregate(now) {
91
91
  if (ts !== null && (oldest === null || ts < oldest))
92
92
  oldest = ts;
93
93
  }
94
+ // last7Days: the 7 most recent days with data, oldest→newest
95
+ const last7Days = Array.from({ length: 7 }, (_, i) => ({
96
+ dayStart: startToday - (6 - i) * 86_400_000,
97
+ byModel: { ...(merged.get(startToday - (6 - i) * 86_400_000)?.byModel ?? {}) },
98
+ }));
94
99
  const snap = {
95
100
  today: { byModel: {}, sessions: new Set() },
96
101
  d7: { byModel: {}, sessions: new Set() },
97
102
  d30: { byModel: {}, sessions: new Set() },
103
+ last7Days,
98
104
  scannedFiles: cache.size,
99
105
  generatedAt: now,
100
106
  oldestMtimeMs: oldest,
@@ -150,10 +156,15 @@ export function bucketTopModel(b) {
150
156
  return topModel;
151
157
  }
152
158
  function emptySnapshot(now, scannedFiles) {
159
+ const startToday = startOfDay(now);
153
160
  return {
154
161
  today: { byModel: {}, sessions: new Set() },
155
162
  d7: { byModel: {}, sessions: new Set() },
156
163
  d30: { byModel: {}, sessions: new Set() },
164
+ last7Days: Array.from({ length: 7 }, (_, i) => ({
165
+ dayStart: startToday - (6 - i) * 86_400_000,
166
+ byModel: {},
167
+ })),
157
168
  scannedFiles,
158
169
  generatedAt: now,
159
170
  oldestMtimeMs: null,
package/dist/tui/index.js CHANGED
@@ -327,9 +327,45 @@ function BarRow({ label, value, max, total, color, cost, }) {
327
327
  }
328
328
  // ── History panel ────────────────────────────────────────────────────────────
329
329
  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 ~/.claude/projects\u2026" })) : history.scannedFiles === 0 ? (_jsx(Text, { dimColor: true, children: "No transcripts found." })) : (_jsxs(_Fragment, { 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(HistoryRow, { label: "Top model", today: fmtTopModel(history.today), d7: fmtTopModel(history.d7), d30: fmtTopModel(history.d30) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [`scanned ${history.scannedFiles} transcripts`, history.oldestMtimeMs !== null &&
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 &&
331
331
  ` · data since ${fmtDate(history.oldestMtimeMs)} (${daysAgo(history.oldestMtimeMs, history.generatedAt)} days)`] }) })] }))] }));
332
332
  }
333
+ // ── Dual bar chart ────────────────────────────────────────────────────────────
334
+ const CHART_BAR_WIDTH = 28;
335
+ function claudeTokens(byModel) {
336
+ return Object.entries(byModel)
337
+ .filter(([m]) => m !== 'codex')
338
+ .reduce((s, [, u]) => s + totalTokens(u), 0);
339
+ }
340
+ function codexTokens(byModel) {
341
+ return totalTokens(byModel['codex'] ?? { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 });
342
+ }
343
+ function claudeCost(byModel) {
344
+ return Object.entries(byModel)
345
+ .filter(([m]) => m !== 'codex')
346
+ .reduce((s, [m, u]) => s + (estimateCostUSD(m, u) ?? 0), 0);
347
+ }
348
+ function DualBarChart({ days, now }) {
349
+ const maxTokens = Math.max(1, ...days.flatMap((d) => [claudeTokens(d.byModel), codexTokens(d.byModel)]));
350
+ const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
351
+ 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) => {
353
+ const ct = claudeTokens(day.byModel);
354
+ const cxt = codexTokens(day.byModel);
355
+ const cc = claudeCost(day.byModel);
356
+ const cxc = Object.entries(day.byModel)
357
+ .filter(([m]) => m === 'codex')
358
+ .reduce((s, [m, u]) => s + (estimateCostUSD(m, u) ?? 0), 0);
359
+ const isToday = day.dayStart === todayStart;
360
+ const label = isToday ? 'today' : DAY_LABELS[new Date(day.dayStart).getDay()];
361
+ 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));
363
+ })] }));
364
+ }
365
+ function solidBar(ratio, width) {
366
+ const filled = Math.round(Math.min(1, ratio) * width);
367
+ return '█'.repeat(filled) + '░'.repeat(width - filled);
368
+ }
333
369
  function fmtDate(ms) {
334
370
  const d = new Date(ms);
335
371
  const y = d.getFullYear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {