ai-usage-analyzer 0.2.0 → 0.3.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 +27 -165
- package/bin/ai-usage.js +22 -3
- package/package.json +1 -1
- package/src/aggregate.js +32 -17
- package/src/detectors.js +11 -254
- package/src/loaders.js +112 -1
- package/src/markdown.js +73 -13
- package/src/render.js +154 -98
- package/src/tools.js +259 -0
package/src/loaders.js
CHANGED
|
@@ -207,6 +207,113 @@ function walkJsonl(root, prefix) {
|
|
|
207
207
|
return out;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// Walks Claude's projects/ tree. Top-level .jsonl files are sessions; files
|
|
211
|
+
// under any `subagents/` subdir are deliberately skipped (their usage is
|
|
212
|
+
// already represented in the parent session's assistant messages).
|
|
213
|
+
function walkClaudeSessions(root) {
|
|
214
|
+
const out = [];
|
|
215
|
+
function walk(dir) {
|
|
216
|
+
let entries;
|
|
217
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
218
|
+
for (const e of entries) {
|
|
219
|
+
const full = join(dir, e.name);
|
|
220
|
+
if (e.isDirectory()) {
|
|
221
|
+
if (e.name === 'subagents') continue;
|
|
222
|
+
walk(full);
|
|
223
|
+
} else if (e.isFile() && e.name.endsWith('.jsonl')) {
|
|
224
|
+
out.push(full);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
walk(root);
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Claude Code JSONL loader
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
//
|
|
236
|
+
// Each top-level file under projects/<encoded-cwd>/<UUID>.jsonl is one session.
|
|
237
|
+
// Per assistant line, `message.usage` carries the per-turn token counts; we
|
|
238
|
+
// dedupe by `message.id` (Claude Code emits streaming duplicates) and sum
|
|
239
|
+
// across unique messages. cwd / model / timestamp come from the line itself.
|
|
240
|
+
|
|
241
|
+
async function loadClaudeJsonl(rootDir) {
|
|
242
|
+
const records = [];
|
|
243
|
+
const errors = [];
|
|
244
|
+
const files = walkClaudeSessions(rootDir);
|
|
245
|
+
|
|
246
|
+
for (const f of files) {
|
|
247
|
+
try {
|
|
248
|
+
const rec = await parseClaudeSession(f);
|
|
249
|
+
if (rec) records.push(rec);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
errors.push(`${f}: ${e.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { records, errors };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function parseClaudeSession(path) {
|
|
258
|
+
const sessionId = path.split('/').pop().replace('.jsonl', '');
|
|
259
|
+
const seenIds = new Set();
|
|
260
|
+
let cwd = '';
|
|
261
|
+
let model = '';
|
|
262
|
+
let firstTs = null;
|
|
263
|
+
|
|
264
|
+
let tokensInput = 0;
|
|
265
|
+
let tokensOutput = 0;
|
|
266
|
+
let tokensCacheRead = 0;
|
|
267
|
+
let tokensCacheWrite = 0;
|
|
268
|
+
|
|
269
|
+
const stream = createReadStream(path, { encoding: 'utf8' });
|
|
270
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
271
|
+
for await (const line of rl) {
|
|
272
|
+
if (!line) continue;
|
|
273
|
+
let ev;
|
|
274
|
+
try { ev = JSON.parse(line); } catch { continue; }
|
|
275
|
+
if (ev.type !== 'assistant') continue;
|
|
276
|
+
const msg = ev.message || {};
|
|
277
|
+
const usage = msg.usage;
|
|
278
|
+
if (!usage) continue;
|
|
279
|
+
const id = msg.id;
|
|
280
|
+
if (id) {
|
|
281
|
+
if (seenIds.has(id)) continue;
|
|
282
|
+
seenIds.add(id);
|
|
283
|
+
}
|
|
284
|
+
if (!cwd && typeof ev.cwd === 'string') cwd = ev.cwd;
|
|
285
|
+
if (!model && typeof msg.model === 'string') model = msg.model;
|
|
286
|
+
const ts = parseIsoMs(ev.timestamp);
|
|
287
|
+
if (firstTs === null && ts !== null) firstTs = ts;
|
|
288
|
+
tokensInput += usage.input_tokens || 0;
|
|
289
|
+
tokensOutput += usage.output_tokens || 0;
|
|
290
|
+
tokensCacheRead += usage.cache_read_input_tokens || 0;
|
|
291
|
+
tokensCacheWrite += usage.cache_creation_input_tokens || 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const tot = tokensInput + tokensOutput + tokensCacheRead + tokensCacheWrite;
|
|
295
|
+
if (tot === 0) return null;
|
|
296
|
+
const ts = firstTs ?? Date.now();
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
tool: 'claude',
|
|
300
|
+
sessionId,
|
|
301
|
+
project: compactHome(cwd),
|
|
302
|
+
title: '',
|
|
303
|
+
week: isoWeekKey(ts),
|
|
304
|
+
month: monthKey(ts),
|
|
305
|
+
ts,
|
|
306
|
+
tokensInput,
|
|
307
|
+
tokensOutput,
|
|
308
|
+
tokensCacheRead,
|
|
309
|
+
tokensCacheWrite,
|
|
310
|
+
tokensReasoning: 0,
|
|
311
|
+
tokensTotal: tot,
|
|
312
|
+
cost: 0,
|
|
313
|
+
model,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
210
317
|
// ---------------------------------------------------------------------------
|
|
211
318
|
// Public: load all session records for given detectors
|
|
212
319
|
// ---------------------------------------------------------------------------
|
|
@@ -226,8 +333,12 @@ export async function loadAll(detections) {
|
|
|
226
333
|
const r = await loadCodexRollouts(d.path);
|
|
227
334
|
all.push(...r.records);
|
|
228
335
|
if (r.errors) errors.push(...r.errors);
|
|
336
|
+
} else if (d.key === 'claude') {
|
|
337
|
+
const r = await loadClaudeJsonl(d.path);
|
|
338
|
+
all.push(...r.records);
|
|
339
|
+
if (r.errors) errors.push(...r.errors);
|
|
229
340
|
}
|
|
230
|
-
// For other tools (
|
|
341
|
+
// For other tools (copilot, antigravity, gemini), we only have
|
|
231
342
|
// presence info — no token data to load.
|
|
232
343
|
}
|
|
233
344
|
return { records: all, errors };
|
package/src/markdown.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
// copies cleanly into issues, PRs, Notion, etc.
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
perProject, perMonth, perWeek, perTool, overall, topSessions, tokenBreakdown,
|
|
6
|
+
perProject, perMonth, perWeek, perTool, perToolPerMonth, overall, topSessions, tokenBreakdown,
|
|
7
7
|
MONTH_NAMES,
|
|
8
8
|
} from './aggregate.js';
|
|
9
|
+
import { getToolBarChar, TOOLS } from './tools.js';
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
// Number formatting helpers (re-implemented locally to avoid TUI deps)
|
|
@@ -41,6 +42,29 @@ function bar(value, max, width) {
|
|
|
41
42
|
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// Markdown has no color, so each tool's stacked-bar segment is shown with
|
|
46
|
+
// a distinct block character. The character for each tool is defined in
|
|
47
|
+
// src/tools.js alongside its color.
|
|
48
|
+
function stackedBar(byTool, width) {
|
|
49
|
+
const total = Object.values(byTool).reduce((a, b) => a + b, 0);
|
|
50
|
+
if (total <= 0 || width <= 0) return ' '.repeat(Math.max(0, width));
|
|
51
|
+
const entries = Object.entries(byTool)
|
|
52
|
+
.filter(([, v]) => v > 0)
|
|
53
|
+
.sort((a, b) => b[1] - a[1]);
|
|
54
|
+
let out = '';
|
|
55
|
+
let x = 0;
|
|
56
|
+
for (let i = 0; i < entries.length; i++) {
|
|
57
|
+
const [tool, value] = entries[i];
|
|
58
|
+
const segW = i === entries.length - 1
|
|
59
|
+
? width - x
|
|
60
|
+
: Math.round((value / total) * width);
|
|
61
|
+
if (segW <= 0) continue;
|
|
62
|
+
out += getToolBarChar(tool).repeat(segW);
|
|
63
|
+
x += segW;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
function pct(x) { return (x * 100).toFixed(1) + '%'; }
|
|
45
69
|
|
|
46
70
|
function mdEscape(s) {
|
|
@@ -151,15 +175,53 @@ export function renderMarkdown({
|
|
|
151
175
|
// Per Project ---------------------------------------------------------
|
|
152
176
|
const projects = perProject(records);
|
|
153
177
|
if (projects.length > 0) {
|
|
154
|
-
const max = projects[0].tokensTotal;
|
|
155
178
|
out.push(`## Per Project`);
|
|
156
179
|
out.push(``);
|
|
157
|
-
out.push(`|
|
|
158
|
-
out.push(
|
|
180
|
+
out.push(`| Project | Sessions | Input | Output | Cache | Total | Dist |`);
|
|
181
|
+
out.push(`|---|---:|---:|---:|---:|---:|---|`);
|
|
159
182
|
for (const p of projects) {
|
|
160
183
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
161
184
|
const proj = compactHome(p.project);
|
|
162
|
-
out.push(`|
|
|
185
|
+
out.push(`| \`${mdEscape(proj)}\` | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${stackedBar(p.byTool, 20)} |`);
|
|
186
|
+
}
|
|
187
|
+
out.push(``);
|
|
188
|
+
const legend = TOOLS
|
|
189
|
+
.filter(t => t.hasTokens)
|
|
190
|
+
.map(t => `\`${t.barChar}\` ${t.label}`)
|
|
191
|
+
.join(' · ');
|
|
192
|
+
out.push(`_Bar legend: ${legend} · \`·\` other_`);
|
|
193
|
+
out.push(``);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Per Tool ------------------------------------------------------------
|
|
197
|
+
const toolRows = perTool(records);
|
|
198
|
+
if (toolRows.length > 0) {
|
|
199
|
+
const max = toolRows[0].tokensTotal;
|
|
200
|
+
out.push(`## Per Tool`);
|
|
201
|
+
out.push(``);
|
|
202
|
+
out.push(`| Tool | Sessions | Input | Output | Cache | Total | Cost | Avg/sess | Dist |`);
|
|
203
|
+
out.push(`|---|---:|---:|---:|---:|---:|---:|---:|---|`);
|
|
204
|
+
for (const p of toolRows) {
|
|
205
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
206
|
+
out.push(`| ${mdEscape(p.tool)} | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${fmtCost(p.cost)} | ${fmtCompact(p.avg)} | ${bar(p.tokensTotal, max, 20)} |`);
|
|
207
|
+
}
|
|
208
|
+
out.push(``);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Per Tool per Month ---------------------------------------------------
|
|
212
|
+
const tpmRows = perToolPerMonth(records);
|
|
213
|
+
if (tpmRows.length > 0) {
|
|
214
|
+
const max = tpmRows[0].tokensTotal;
|
|
215
|
+
out.push(`## Per Tool per Month`);
|
|
216
|
+
out.push(``);
|
|
217
|
+
out.push(`| Tool | Month | Sessions | Input | Output | Cache | Total | Dist |`);
|
|
218
|
+
out.push(`|---|---|---:|---:|---:|---:|---:|---|`);
|
|
219
|
+
for (const p of tpmRows) {
|
|
220
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
221
|
+
const yyyy = p.month.slice(0, 4);
|
|
222
|
+
const mm = p.month.slice(5, 7);
|
|
223
|
+
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
224
|
+
out.push(`| ${mdEscape(p.tool)} | ${label} | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${bar(p.tokensTotal, max, 18)} |`);
|
|
163
225
|
}
|
|
164
226
|
out.push(``);
|
|
165
227
|
}
|
|
@@ -167,16 +229,15 @@ export function renderMarkdown({
|
|
|
167
229
|
// Per Month -----------------------------------------------------------
|
|
168
230
|
const months = perMonth(records);
|
|
169
231
|
if (months.length > 0) {
|
|
170
|
-
const max = Math.max(...months.map(m => m.tokensTotal));
|
|
171
232
|
out.push(`## Per Month`);
|
|
172
233
|
out.push(``);
|
|
173
|
-
out.push(`| Month | Sessions | Input | Output | Total |
|
|
174
|
-
out.push(
|
|
234
|
+
out.push(`| Month | Sessions | Input | Output | Total | Dist |`);
|
|
235
|
+
out.push(`|---|---:|---:|---:|---:|---|`);
|
|
175
236
|
for (const m of months) {
|
|
176
237
|
const yyyy = m.month.slice(0, 4);
|
|
177
238
|
const mm = m.month.slice(5, 7);
|
|
178
239
|
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
179
|
-
out.push(`| ${label} | ${fmtInt(m.n)} | ${fmtCompact(m.tokensInput)} | ${fmtCompact(m.tokensOutput)} | ${fmtCompact(m.tokensTotal)} | ${
|
|
240
|
+
out.push(`| ${label} | ${fmtInt(m.n)} | ${fmtCompact(m.tokensInput)} | ${fmtCompact(m.tokensOutput)} | ${fmtCompact(m.tokensTotal)} | ${stackedBar(m.byTool, 20)} |`);
|
|
180
241
|
}
|
|
181
242
|
out.push(``);
|
|
182
243
|
}
|
|
@@ -184,13 +245,12 @@ export function renderMarkdown({
|
|
|
184
245
|
// Per Week ------------------------------------------------------------
|
|
185
246
|
const weeks = perWeek(records);
|
|
186
247
|
if (weeks.length > 0) {
|
|
187
|
-
const max = Math.max(...weeks.map(w => w.tokensTotal));
|
|
188
248
|
out.push(`## Per Week`);
|
|
189
249
|
out.push(``);
|
|
190
|
-
out.push(`| Week | Sessions | Input | Output | Total |
|
|
191
|
-
out.push(
|
|
250
|
+
out.push(`| Week | Sessions | Input | Output | Total | Dist |`);
|
|
251
|
+
out.push(`|---|---:|---:|---:|---:|---|`);
|
|
192
252
|
for (const w of weeks) {
|
|
193
|
-
out.push(`| ${w.week} | ${fmtInt(w.n)} | ${fmtCompact(w.tokensInput)} | ${fmtCompact(w.tokensOutput)} | ${fmtCompact(w.tokensTotal)} | ${
|
|
253
|
+
out.push(`| ${w.week} | ${fmtInt(w.n)} | ${fmtCompact(w.tokensInput)} | ${fmtCompact(w.tokensOutput)} | ${fmtCompact(w.tokensTotal)} | ${stackedBar(w.byTool, 18)} |`);
|
|
194
254
|
}
|
|
195
255
|
out.push(``);
|
|
196
256
|
}
|
package/src/render.js
CHANGED
|
@@ -6,9 +6,10 @@ import boxen from 'boxen';
|
|
|
6
6
|
import gradient from 'gradient-string';
|
|
7
7
|
import process from 'node:process';
|
|
8
8
|
import {
|
|
9
|
-
perProject, perMonth, perWeek, perTool, overall, topSessions, tokenBreakdown,
|
|
9
|
+
perProject, perMonth, perWeek, perTool, perToolPerMonth, overall, topSessions, tokenBreakdown,
|
|
10
10
|
MONTH_NAMES,
|
|
11
11
|
} from './aggregate.js';
|
|
12
|
+
import { getToolColor } from './tools.js';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Terminal width detection
|
|
@@ -63,34 +64,45 @@ export function fmtCost(n) {
|
|
|
63
64
|
// Color helpers
|
|
64
65
|
// ---------------------------------------------------------------------------
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
copilot: 'green',
|
|
72
|
-
antigravity: 'red',
|
|
73
|
-
gemini: 'gray',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export function toolColor(t) { return TOOL_COLORS[t] || 'white'; }
|
|
77
|
-
export function colorize(t, c) { return chalk.hex(toHex(c))(t); }
|
|
78
|
-
|
|
79
|
-
function toHex(name) {
|
|
80
|
-
const m = {
|
|
81
|
-
red: '#ff5555', green: '#50fa7b', yellow: '#f1fa8c',
|
|
82
|
-
blue: '#8be9fd', magenta: '#ff79c6', cyan: '#8be9fd',
|
|
83
|
-
white: '#f8f8f2', gray: '#6272a4',
|
|
84
|
-
};
|
|
85
|
-
return m[name] || '#ffffff';
|
|
86
|
-
}
|
|
67
|
+
export function toolColor(t) { return getToolColor(t); }
|
|
68
|
+
export function colorize(t, c) { return chalk.hex(c)(t); }
|
|
69
|
+
|
|
70
|
+
// Cool-to-warm gradient: small bars feel subtle, large bars pop.
|
|
71
|
+
const BAR_GRADIENT = gradient(['#3b82f6', '#14b8a6', '#f1fa8c', '#ff9e64']);
|
|
87
72
|
|
|
88
|
-
function bar(value, max, width
|
|
73
|
+
function bar(value, max, width) {
|
|
89
74
|
if (max <= 0) return ' '.repeat(width);
|
|
90
75
|
const pct = Math.max(0, Math.min(1, value / max));
|
|
91
76
|
const filled = Math.round(pct * width);
|
|
92
77
|
const empty = width - filled;
|
|
93
|
-
|
|
78
|
+
if (filled === 0) return chalk.gray('░'.repeat(width));
|
|
79
|
+
return BAR_GRADIENT('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Stacked bar — one segment per tool, widths proportional to that tool's
|
|
83
|
+
// share of the row's total. Used wherever a row spans multiple tools
|
|
84
|
+
// (per-project / per-month / per-week). Single-tool rows are just one
|
|
85
|
+
// colored segment; no change in feel.
|
|
86
|
+
function stackedBar(byTool, width) {
|
|
87
|
+
const total = Object.values(byTool).reduce((a, b) => a + b, 0);
|
|
88
|
+
if (total <= 0 || width <= 0) return ' '.repeat(Math.max(0, width));
|
|
89
|
+
const entries = Object.entries(byTool)
|
|
90
|
+
.filter(([, v]) => v > 0)
|
|
91
|
+
.sort((a, b) => b[1] - a[1]);
|
|
92
|
+
let out = '';
|
|
93
|
+
let x = 0;
|
|
94
|
+
for (let i = 0; i < entries.length; i++) {
|
|
95
|
+
const [tool, value] = entries[i];
|
|
96
|
+
// Last segment fills the remaining width so rounding doesn't leave
|
|
97
|
+
// a visible gap or overshoot.
|
|
98
|
+
const segW = i === entries.length - 1
|
|
99
|
+
? width - x
|
|
100
|
+
: Math.round((value / total) * width);
|
|
101
|
+
if (segW <= 0) continue;
|
|
102
|
+
out += chalk.hex(toolColor(tool))('█'.repeat(segW));
|
|
103
|
+
x += segW;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
function shortModel(m) {
|
|
@@ -250,23 +262,60 @@ function pct(x) { return (x * 100).toFixed(1) + '%'; }
|
|
|
250
262
|
export function renderPerProject(records) {
|
|
251
263
|
const items = perProject(records);
|
|
252
264
|
if (items.length === 0) return '';
|
|
253
|
-
const max = items[0].tokensTotal;
|
|
254
265
|
const barW = NARROW ? 10 : 18;
|
|
255
266
|
|
|
256
267
|
// Narrow mode: drop In/Out/Cache columns, just show n + Total + Dist
|
|
257
268
|
const head = NARROW
|
|
258
|
-
? [chalk.bold('
|
|
269
|
+
? [chalk.bold('Project'), chalk.bold('n'),
|
|
259
270
|
chalk.bold('Total'), chalk.bold('Dist')]
|
|
260
|
-
: [chalk.bold('
|
|
271
|
+
: [chalk.bold('Project'), chalk.bold('n'),
|
|
261
272
|
chalk.bold('In'), chalk.bold('Out'),
|
|
262
273
|
chalk.bold('Cache'), chalk.bold('Total'), chalk.bold('Dist')];
|
|
263
274
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
264
275
|
|
|
276
|
+
for (const p of items) {
|
|
277
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
278
|
+
const row = [truncEnd(p.project, NARROW ? 28 : 40), fmtInt(p.n)];
|
|
279
|
+
if (!NARROW) {
|
|
280
|
+
row.push(
|
|
281
|
+
fmtCompact(p.tokensInput),
|
|
282
|
+
fmtCompact(p.tokensOutput),
|
|
283
|
+
fmtCompact(cache),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
row.push(fmtCompact(p.tokensTotal), stackedBar(p.byTool, barW));
|
|
287
|
+
t.push(row);
|
|
288
|
+
}
|
|
289
|
+
return boxen(t.toString(), {
|
|
290
|
+
title: chalk.bold('Per Project'),
|
|
291
|
+
borderStyle: 'round',
|
|
292
|
+
borderColor: 'cyan',
|
|
293
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Per Tool (detailed — complements the brief summary in Overview)
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
export function renderPerTool(records) {
|
|
302
|
+
const items = perTool(records);
|
|
303
|
+
if (items.length === 0) return '';
|
|
304
|
+
const max = items[0].tokensTotal;
|
|
305
|
+
const barW = NARROW ? 10 : 18;
|
|
306
|
+
|
|
307
|
+
const head = NARROW
|
|
308
|
+
? [chalk.bold('Tool'), chalk.bold('n'),
|
|
309
|
+
chalk.bold('Total'), chalk.bold('Cost'), chalk.bold('Avg/sess'), chalk.bold('Dist')]
|
|
310
|
+
: [chalk.bold('Tool'), chalk.bold('n'),
|
|
311
|
+
chalk.bold('Input'), chalk.bold('Output'), chalk.bold('Cache'),
|
|
312
|
+
chalk.bold('Total'), chalk.bold('Cost'), chalk.bold('Avg/sess'), chalk.bold('Dist')];
|
|
313
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
314
|
+
|
|
265
315
|
for (const p of items) {
|
|
266
316
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
267
317
|
const row = [
|
|
268
318
|
colorize(p.tool, toolColor(p.tool)),
|
|
269
|
-
truncEnd(p.project, NARROW ? 26 : 38),
|
|
270
319
|
fmtInt(p.n),
|
|
271
320
|
];
|
|
272
321
|
if (!NARROW) {
|
|
@@ -278,12 +327,63 @@ export function renderPerProject(records) {
|
|
|
278
327
|
}
|
|
279
328
|
row.push(
|
|
280
329
|
fmtCompact(p.tokensTotal),
|
|
281
|
-
|
|
330
|
+
fmtCost(p.cost),
|
|
331
|
+
fmtCompact(p.avg),
|
|
332
|
+
bar(p.tokensTotal, max, barW),
|
|
282
333
|
);
|
|
283
334
|
t.push(row);
|
|
284
335
|
}
|
|
285
336
|
return boxen(t.toString(), {
|
|
286
|
-
title: chalk.bold('Per
|
|
337
|
+
title: chalk.bold('Per Tool'),
|
|
338
|
+
borderStyle: 'round',
|
|
339
|
+
borderColor: 'magenta',
|
|
340
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Per Tool per Month (cross-tab — one row per (tool, month))
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
export function renderPerToolPerMonth(records) {
|
|
349
|
+
const items = perToolPerMonth(records);
|
|
350
|
+
if (items.length === 0) return '';
|
|
351
|
+
const max = items[0].tokensTotal;
|
|
352
|
+
const barW = NARROW ? 8 : 16;
|
|
353
|
+
|
|
354
|
+
const head = NARROW
|
|
355
|
+
? [chalk.bold('Tool'), chalk.bold('Month'), chalk.bold('n'),
|
|
356
|
+
chalk.bold('Total'), chalk.bold('Dist')]
|
|
357
|
+
: [chalk.bold('Tool'), chalk.bold('Month'), chalk.bold('n'),
|
|
358
|
+
chalk.bold('Input'), chalk.bold('Output'), chalk.bold('Cache'),
|
|
359
|
+
chalk.bold('Total'), chalk.bold('Dist')];
|
|
360
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
361
|
+
|
|
362
|
+
for (const p of items) {
|
|
363
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
364
|
+
const yyyy = p.month.slice(0, 4);
|
|
365
|
+
const mm = p.month.slice(5, 7);
|
|
366
|
+
const monthLabel = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
367
|
+
const row = [
|
|
368
|
+
colorize(p.tool, toolColor(p.tool)),
|
|
369
|
+
chalk.bold(monthLabel),
|
|
370
|
+
fmtInt(p.n),
|
|
371
|
+
];
|
|
372
|
+
if (!NARROW) {
|
|
373
|
+
row.push(
|
|
374
|
+
fmtCompact(p.tokensInput),
|
|
375
|
+
fmtCompact(p.tokensOutput),
|
|
376
|
+
fmtCompact(cache),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
row.push(
|
|
380
|
+
fmtCompact(p.tokensTotal),
|
|
381
|
+
bar(p.tokensTotal, max, barW),
|
|
382
|
+
);
|
|
383
|
+
t.push(row);
|
|
384
|
+
}
|
|
385
|
+
return boxen(t.toString(), {
|
|
386
|
+
title: chalk.bold('Per Tool per Month'),
|
|
287
387
|
borderStyle: 'round',
|
|
288
388
|
borderColor: 'cyan',
|
|
289
389
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
@@ -297,64 +397,42 @@ export function renderPerProject(records) {
|
|
|
297
397
|
export function renderPerMonth(records) {
|
|
298
398
|
const items = perMonth(records);
|
|
299
399
|
if (items.length === 0) return '';
|
|
300
|
-
const max = Math.max(...items.map(p => p.tokensTotal));
|
|
301
400
|
const barW = NARROW ? 10 : 16;
|
|
302
|
-
const t1 = perTool(records);
|
|
303
401
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
: [chalk.bold('Month'), chalk.bold('n'),
|
|
310
|
-
chalk.bold('Input'), chalk.bold('Output'),
|
|
311
|
-
chalk.bold('Total'), chalk.bold('OC'),
|
|
312
|
-
chalk.bold('CX'), chalk.bold('MM'), chalk.bold('Dist')];
|
|
402
|
+
const head = [
|
|
403
|
+
chalk.bold('Month'), chalk.bold('n'),
|
|
404
|
+
chalk.bold('Input'), chalk.bold('Output'),
|
|
405
|
+
chalk.bold('Total'), chalk.bold('Dist'),
|
|
406
|
+
];
|
|
313
407
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
314
408
|
|
|
315
409
|
for (const p of items) {
|
|
316
410
|
const yyyy = p.month.slice(0, 4);
|
|
317
411
|
const mm = p.month.slice(5, 7);
|
|
318
412
|
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
319
|
-
|
|
413
|
+
t.push([
|
|
320
414
|
chalk.bold(label),
|
|
321
415
|
fmtInt(p.n),
|
|
322
416
|
fmtCompact(p.tokensInput),
|
|
323
417
|
fmtCompact(p.tokensOutput),
|
|
324
418
|
fmtCompact(p.tokensTotal),
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
row.push(
|
|
328
|
-
p.byTool.opencode ? fmtCompact(p.byTool.opencode) : chalk.dim('—'),
|
|
329
|
-
p.byTool.codex ? fmtCompact(p.byTool.codex) : chalk.dim('—'),
|
|
330
|
-
p.byTool.mimocode ? fmtCompact(p.byTool.mimocode) : chalk.dim('—'),
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
row.push(bar(p.tokensTotal, max, barW, 'green'));
|
|
334
|
-
t.push(row);
|
|
419
|
+
stackedBar(p.byTool, barW),
|
|
420
|
+
]);
|
|
335
421
|
}
|
|
336
422
|
|
|
337
423
|
// TOTAL row
|
|
338
424
|
const tot = overall(records);
|
|
339
|
-
|
|
425
|
+
t.push([
|
|
340
426
|
chalk.bgGray.white.bold(' TOTAL '),
|
|
341
427
|
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
342
428
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
343
429
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
344
430
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
totalRow.push(
|
|
348
|
-
chalk.bgGray.white.bold(fmtCompact(t1.find(p => p.tool === 'opencode')?.tokensTotal || 0)),
|
|
349
|
-
chalk.bgGray.white.bold(fmtCompact(t1.find(p => p.tool === 'codex')?.tokensTotal || 0)),
|
|
350
|
-
chalk.bgGray.white.bold(fmtCompact(t1.find(p => p.tool === 'mimocode')?.tokensTotal || 0)),
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
totalRow.push('');
|
|
354
|
-
t.push(totalRow);
|
|
431
|
+
'',
|
|
432
|
+
]);
|
|
355
433
|
|
|
356
434
|
return boxen(t.toString(), {
|
|
357
|
-
title: chalk.bold('Per
|
|
435
|
+
title: chalk.bold('Per Month'),
|
|
358
436
|
borderStyle: 'round',
|
|
359
437
|
borderColor: 'green',
|
|
360
438
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
@@ -368,60 +446,38 @@ export function renderPerMonth(records) {
|
|
|
368
446
|
export function renderPerWeek(records) {
|
|
369
447
|
const items = perWeek(records);
|
|
370
448
|
if (items.length === 0) return '';
|
|
371
|
-
const max = Math.max(...items.map(p => p.tokensTotal));
|
|
372
449
|
const barW = NARROW ? 10 : 14;
|
|
373
|
-
const t1 = perTool(records);
|
|
374
450
|
|
|
375
|
-
const head =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
chalk.bold('Input'), chalk.bold('Output'),
|
|
381
|
-
chalk.bold('Total'), chalk.bold('OC'),
|
|
382
|
-
chalk.bold('CX'), chalk.bold('MM'), chalk.bold('Dist')];
|
|
451
|
+
const head = [
|
|
452
|
+
chalk.bold('ISO Week'), chalk.bold('n'),
|
|
453
|
+
chalk.bold('Input'), chalk.bold('Output'),
|
|
454
|
+
chalk.bold('Total'), chalk.bold('Dist'),
|
|
455
|
+
];
|
|
383
456
|
const t = new Table({ head, style: { head: [], border: [] } });
|
|
384
457
|
|
|
385
458
|
for (const p of items) {
|
|
386
|
-
|
|
387
|
-
const row = [
|
|
459
|
+
t.push([
|
|
388
460
|
chalk.bold(p.week),
|
|
389
461
|
fmtInt(p.n),
|
|
390
462
|
fmtCompact(p.tokensInput),
|
|
391
463
|
fmtCompact(p.tokensOutput),
|
|
392
464
|
fmtCompact(p.tokensTotal),
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
row.push(
|
|
396
|
-
p.byTool.opencode ? fmtCompact(p.byTool.opencode) : chalk.dim('—'),
|
|
397
|
-
p.byTool.codex ? fmtCompact(p.byTool.codex) : chalk.dim('—'),
|
|
398
|
-
p.byTool.mimocode ? fmtCompact(p.byTool.mimocode) : chalk.dim('—'),
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
row.push(bar(p.tokensTotal, max, barW, toolColor(dominantTool)));
|
|
402
|
-
t.push(row);
|
|
465
|
+
stackedBar(p.byTool, barW),
|
|
466
|
+
]);
|
|
403
467
|
}
|
|
404
468
|
// TOTAL row
|
|
405
469
|
const tot = overall(records);
|
|
406
|
-
|
|
470
|
+
t.push([
|
|
407
471
|
chalk.bgGray.white.bold(' TOTAL '),
|
|
408
472
|
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
409
473
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
410
474
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
411
475
|
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
totalRow.push(
|
|
415
|
-
chalk.bgGray.white.bold(fmtCompact(t1.find(p => p.tool === 'opencode')?.tokensTotal || 0)),
|
|
416
|
-
chalk.bgGray.white.bold(fmtCompact(t1.find(p => p.tool === 'codex')?.tokensTotal || 0)),
|
|
417
|
-
chalk.bgGray.white.bold(fmtCompact(t1.find(p => p.tool === 'mimocode')?.tokensTotal || 0)),
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
totalRow.push('');
|
|
421
|
-
t.push(totalRow);
|
|
476
|
+
'',
|
|
477
|
+
]);
|
|
422
478
|
|
|
423
479
|
return boxen(t.toString(), {
|
|
424
|
-
title: chalk.bold('Per
|
|
480
|
+
title: chalk.bold('Per Week'),
|
|
425
481
|
borderStyle: 'round',
|
|
426
482
|
borderColor: 'magenta',
|
|
427
483
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|