ai-usage-analyzer 0.1.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 +184 -0
- package/bin/ai-usage.js +100 -0
- package/package.json +52 -0
- package/src/aggregate.js +117 -0
- package/src/detectors.js +300 -0
- package/src/loaders.js +249 -0
- package/src/render.js +544 -0
package/src/render.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// TUI renderer using chalk + cli-table3 + boxen + gradient-string.
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import Table from 'cli-table3';
|
|
5
|
+
import boxen from 'boxen';
|
|
6
|
+
import gradient from 'gradient-string';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import {
|
|
9
|
+
perProject, perMonth, perWeek, perTool, overall, topSessions, tokenBreakdown,
|
|
10
|
+
MONTH_NAMES,
|
|
11
|
+
} from './aggregate.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Terminal width detection
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function detectWidth() {
|
|
18
|
+
// 1. process.stdout.columns (when piped to terminal)
|
|
19
|
+
if (process.stdout.columns && Number.isFinite(process.stdout.columns)) {
|
|
20
|
+
return process.stdout.columns;
|
|
21
|
+
}
|
|
22
|
+
// 2. COLUMNS env var (some terminals set this)
|
|
23
|
+
if (process.env.COLUMNS) {
|
|
24
|
+
const n = parseInt(process.env.COLUMNS, 10);
|
|
25
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
26
|
+
}
|
|
27
|
+
// 3. tput cols (POSIX)
|
|
28
|
+
try {
|
|
29
|
+
const { execSync } = require('node:child_process');
|
|
30
|
+
const out = execSync('tput cols 2>/dev/null', { encoding: 'utf8' });
|
|
31
|
+
const n = parseInt(out.trim(), 10);
|
|
32
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
33
|
+
} catch {}
|
|
34
|
+
// 4. default
|
|
35
|
+
return 100;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const TERM_WIDTH = detectWidth();
|
|
39
|
+
export const NARROW = TERM_WIDTH < 110;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Number formatting
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export function fmtInt(n) {
|
|
46
|
+
return Number(n).toLocaleString('en-US');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function fmtCompact(n) {
|
|
50
|
+
const a = Math.abs(n);
|
|
51
|
+
if (a >= 1e9) return (n / 1e9).toFixed(2) + 'B';
|
|
52
|
+
if (a >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
|
53
|
+
if (a >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
54
|
+
return String(n);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fmtCost(n) {
|
|
58
|
+
if (!n) return '—';
|
|
59
|
+
return '$' + Number(n).toFixed(2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Color helpers
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const TOOL_COLORS = {
|
|
67
|
+
opencode: 'cyan',
|
|
68
|
+
codex: 'magenta',
|
|
69
|
+
mimocode: 'yellow',
|
|
70
|
+
claude: 'blue',
|
|
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
|
+
}
|
|
87
|
+
|
|
88
|
+
function bar(value, max, width, color) {
|
|
89
|
+
if (max <= 0) return ' '.repeat(width);
|
|
90
|
+
const pct = Math.max(0, Math.min(1, value / max));
|
|
91
|
+
const filled = Math.round(pct * width);
|
|
92
|
+
const empty = width - filled;
|
|
93
|
+
return chalk.hex(toHex(color))('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shortModel(m) {
|
|
97
|
+
if (!m) return '—';
|
|
98
|
+
if (typeof m === 'string' && m.trim().startsWith('{')) {
|
|
99
|
+
try {
|
|
100
|
+
const d = JSON.parse(m);
|
|
101
|
+
const id = d.id || d.model || m;
|
|
102
|
+
const prov = d.providerId || d.provider;
|
|
103
|
+
return id + (prov ? ` (${prov})` : '');
|
|
104
|
+
} catch { return m.slice(0, 30); }
|
|
105
|
+
}
|
|
106
|
+
return m;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function truncMiddle(s, w) {
|
|
110
|
+
if (!s) return '';
|
|
111
|
+
if (s.length <= w) return s;
|
|
112
|
+
if (w <= 1) return s.slice(0, w);
|
|
113
|
+
const head = Math.ceil((w - 1) / 2);
|
|
114
|
+
const tail = Math.floor((w - 1) / 2);
|
|
115
|
+
return s.slice(0, head) + '…' + s.slice(s.length - tail);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function truncEnd(s, w) {
|
|
119
|
+
if (!s) return '';
|
|
120
|
+
if (s.length <= w) return s;
|
|
121
|
+
return s.slice(0, w - 1) + '…';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Header
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
export function renderHeader({ totalSessions, totalTokens, totalCost, dateRange }) {
|
|
129
|
+
const title = gradient.pastel.multiline('◆ AI TOKEN ANALYZER ◆');
|
|
130
|
+
const sub = chalk.dim(`${totalSessions} sessions`) + ' • ' +
|
|
131
|
+
chalk.bold.cyan(`${fmtCompact(totalTokens)} total tokens`) +
|
|
132
|
+
(totalCost ? ' • ' + chalk.bold.yellow(`$${totalCost.toFixed(2)} USD`) : '');
|
|
133
|
+
const range = (dateRange[0] && dateRange[1])
|
|
134
|
+
? chalk.dim.italic(`\n range: ${dateRange[0]} → ${dateRange[1]}`)
|
|
135
|
+
: '';
|
|
136
|
+
return boxen(`${title}\n\n${sub}${range}`, {
|
|
137
|
+
borderStyle: 'round',
|
|
138
|
+
borderColor: 'magenta',
|
|
139
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
140
|
+
align: 'center',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Tool detection panel
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export function renderDetections(detections) {
|
|
149
|
+
// For narrow terminals, simplify to Tool + Status + Count + Tokens (drop Path)
|
|
150
|
+
if (NARROW) {
|
|
151
|
+
const t = new Table({
|
|
152
|
+
head: [chalk.bold('Tool'), chalk.bold('Status'), chalk.bold('Count'), chalk.bold('Tokens')],
|
|
153
|
+
style: { head: [], border: [] },
|
|
154
|
+
});
|
|
155
|
+
for (const d of detections) {
|
|
156
|
+
const status = d.status === 'present' ? chalk.green('● present') : chalk.red('○ absent');
|
|
157
|
+
const count = d.count ? fmtInt(d.count) : chalk.dim('—');
|
|
158
|
+
const tok = d.hasTokens ? (d.count ? chalk.cyan('yes') : chalk.dim('—')) : chalk.dim('n/a');
|
|
159
|
+
t.push([colorize(d.name, toolColor(d.key)), status, count, tok]);
|
|
160
|
+
}
|
|
161
|
+
return boxen(t.toString() + '\n' + chalk.dim('(paths hidden in narrow mode — use $AI_USAGE_PATHS_JSON to inspect)'),
|
|
162
|
+
{ title: chalk.bold('AI Tools Detected'), borderStyle: 'round', borderColor: 'cyan',
|
|
163
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 } });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const t = new Table({
|
|
167
|
+
head: [chalk.bold('Tool'), chalk.bold('Status'), chalk.bold('Path'), chalk.bold('Count'), chalk.bold('Tokens')],
|
|
168
|
+
style: { head: [], border: [] },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
for (const d of detections) {
|
|
172
|
+
const status = d.status === 'present' ? chalk.green('● present') : chalk.red('○ absent');
|
|
173
|
+
const path = d.path ? truncEnd(d.path.replace(process.env.HOME || '', '~'), 50) : chalk.dim('—');
|
|
174
|
+
const count = d.count ? fmtInt(d.count) : chalk.dim('—');
|
|
175
|
+
const tok = d.hasTokens
|
|
176
|
+
? (d.count ? chalk.cyan('yes') : chalk.dim('—'))
|
|
177
|
+
: chalk.dim('n/a');
|
|
178
|
+
t.push([colorize(d.name, toolColor(d.key)), status, path, count, tok]);
|
|
179
|
+
}
|
|
180
|
+
return boxen(t.toString(), {
|
|
181
|
+
title: chalk.bold('AI Tools Detected'),
|
|
182
|
+
borderStyle: 'round',
|
|
183
|
+
borderColor: 'cyan',
|
|
184
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Overview (totals + token breakdown)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
export function renderOverview(records, detections) {
|
|
193
|
+
const t = overall(records);
|
|
194
|
+
const breakdown = tokenBreakdown(t);
|
|
195
|
+
|
|
196
|
+
// Token breakdown sub-table
|
|
197
|
+
const bt = new Table({
|
|
198
|
+
style: { border: ['gray'] },
|
|
199
|
+
colWidths: [14, 12, 12],
|
|
200
|
+
});
|
|
201
|
+
bt.push(
|
|
202
|
+
[chalk.bold('Type'), chalk.bold('Tokens'), chalk.bold('Share')],
|
|
203
|
+
[chalk.cyan('Input'), fmtCompact(breakdown.input), pct(breakdown.ratios.input)],
|
|
204
|
+
[chalk.green('Output'), fmtCompact(breakdown.output), pct(breakdown.ratios.output)],
|
|
205
|
+
[chalk.cyan('Cache Read'), fmtCompact(breakdown.cacheRead), pct(breakdown.ratios.cacheRead)],
|
|
206
|
+
[chalk.cyan('Cache Write'), fmtCompact(breakdown.cacheWrite), pct(breakdown.ratios.cacheWrite)],
|
|
207
|
+
[chalk.yellow('Reasoning'), fmtCompact(breakdown.reasoning), pct(breakdown.ratios.reasoning)],
|
|
208
|
+
[chalk.bold('Total'), chalk.bold(fmtCompact(breakdown.total)), '100.0%'],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Per-tool mini summary
|
|
212
|
+
const pt = new Table({
|
|
213
|
+
head: [chalk.bold('Tool'), chalk.bold('n'), chalk.bold('Total'), chalk.bold('Avg/sess'), chalk.bold('Cost')],
|
|
214
|
+
style: { head: [], border: ['gray'] },
|
|
215
|
+
colWidths: [14, 5, 10, 11, 9],
|
|
216
|
+
});
|
|
217
|
+
const byTool = perTool(records);
|
|
218
|
+
for (const p of byTool) {
|
|
219
|
+
pt.push([
|
|
220
|
+
colorize(p.tool, toolColor(p.tool)),
|
|
221
|
+
fmtInt(p.n),
|
|
222
|
+
fmtCompact(p.tokensTotal),
|
|
223
|
+
fmtCompact(p.avg),
|
|
224
|
+
fmtCost(p.cost),
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
pt.push([
|
|
228
|
+
chalk.bold('TOTAL'),
|
|
229
|
+
chalk.bold(fmtInt(records.length)),
|
|
230
|
+
chalk.bold(fmtCompact(t.tokensTotal)),
|
|
231
|
+
chalk.bold(fmtCompact(t.avg)),
|
|
232
|
+
chalk.bold(fmtCost(t.cost)),
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
const body = pt.toString() + '\n\n' + chalk.bold.underline('Token Breakdown') + '\n' + bt.toString();
|
|
236
|
+
return boxen(body, {
|
|
237
|
+
title: chalk.bold('Overview'),
|
|
238
|
+
borderStyle: 'round',
|
|
239
|
+
borderColor: 'blue',
|
|
240
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function pct(x) { return (x * 100).toFixed(1) + '%'; }
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Per Project
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
export function renderPerProject(records) {
|
|
251
|
+
const items = perProject(records);
|
|
252
|
+
if (items.length === 0) return '';
|
|
253
|
+
const max = items[0].tokensTotal;
|
|
254
|
+
const barW = NARROW ? 10 : 18;
|
|
255
|
+
|
|
256
|
+
// Narrow mode: drop In/Out/Cache columns, just show n + Total + Dist
|
|
257
|
+
const head = NARROW
|
|
258
|
+
? [chalk.bold('Tool'), chalk.bold('Project'), chalk.bold('n'),
|
|
259
|
+
chalk.bold('Total'), chalk.bold('Dist')]
|
|
260
|
+
: [chalk.bold('Tool'), chalk.bold('Project'), chalk.bold('n'),
|
|
261
|
+
chalk.bold('In'), chalk.bold('Out'),
|
|
262
|
+
chalk.bold('Cache'), chalk.bold('Total'), chalk.bold('Dist')];
|
|
263
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
264
|
+
|
|
265
|
+
for (const p of items) {
|
|
266
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
267
|
+
const row = [
|
|
268
|
+
colorize(p.tool, toolColor(p.tool)),
|
|
269
|
+
truncEnd(p.project, NARROW ? 26 : 38),
|
|
270
|
+
fmtInt(p.n),
|
|
271
|
+
];
|
|
272
|
+
if (!NARROW) {
|
|
273
|
+
row.push(
|
|
274
|
+
fmtCompact(p.tokensInput),
|
|
275
|
+
fmtCompact(p.tokensOutput),
|
|
276
|
+
fmtCompact(cache),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
row.push(
|
|
280
|
+
fmtCompact(p.tokensTotal),
|
|
281
|
+
bar(p.tokensTotal, max, barW, toolColor(p.tool)),
|
|
282
|
+
);
|
|
283
|
+
t.push(row);
|
|
284
|
+
}
|
|
285
|
+
return boxen(t.toString(), {
|
|
286
|
+
title: chalk.bold('Per Project'),
|
|
287
|
+
borderStyle: 'round',
|
|
288
|
+
borderColor: 'cyan',
|
|
289
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Per Month
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
export function renderPerMonth(records) {
|
|
298
|
+
const items = perMonth(records);
|
|
299
|
+
if (items.length === 0) return '';
|
|
300
|
+
const max = Math.max(...items.map(p => p.tokensTotal));
|
|
301
|
+
const barW = NARROW ? 10 : 16;
|
|
302
|
+
const t1 = perTool(records);
|
|
303
|
+
|
|
304
|
+
// Narrow mode: drop the byTool columns (OC/CX/MM), keep core metrics
|
|
305
|
+
const head = NARROW
|
|
306
|
+
? [chalk.bold('Month'), chalk.bold('n'),
|
|
307
|
+
chalk.bold('Input'), chalk.bold('Output'),
|
|
308
|
+
chalk.bold('Total'), chalk.bold('Dist')]
|
|
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')];
|
|
313
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
314
|
+
|
|
315
|
+
for (const p of items) {
|
|
316
|
+
const yyyy = p.month.slice(0, 4);
|
|
317
|
+
const mm = p.month.slice(5, 7);
|
|
318
|
+
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
319
|
+
const row = [
|
|
320
|
+
chalk.bold(label),
|
|
321
|
+
fmtInt(p.n),
|
|
322
|
+
fmtCompact(p.tokensInput),
|
|
323
|
+
fmtCompact(p.tokensOutput),
|
|
324
|
+
fmtCompact(p.tokensTotal),
|
|
325
|
+
];
|
|
326
|
+
if (!NARROW) {
|
|
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);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// TOTAL row
|
|
338
|
+
const tot = overall(records);
|
|
339
|
+
const totalRow = [
|
|
340
|
+
chalk.bgGray.white.bold(' TOTAL '),
|
|
341
|
+
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
342
|
+
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
343
|
+
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
344
|
+
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
345
|
+
];
|
|
346
|
+
if (!NARROW) {
|
|
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);
|
|
355
|
+
|
|
356
|
+
return boxen(t.toString(), {
|
|
357
|
+
title: chalk.bold('Per Bulan (Monthly)'),
|
|
358
|
+
borderStyle: 'round',
|
|
359
|
+
borderColor: 'green',
|
|
360
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Per Week
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
export function renderPerWeek(records) {
|
|
369
|
+
const items = perWeek(records);
|
|
370
|
+
if (items.length === 0) return '';
|
|
371
|
+
const max = Math.max(...items.map(p => p.tokensTotal));
|
|
372
|
+
const barW = NARROW ? 10 : 14;
|
|
373
|
+
const t1 = perTool(records);
|
|
374
|
+
|
|
375
|
+
const head = NARROW
|
|
376
|
+
? [chalk.bold('ISO Week'), chalk.bold('n'),
|
|
377
|
+
chalk.bold('Input'), chalk.bold('Output'),
|
|
378
|
+
chalk.bold('Total'), chalk.bold('Dist')]
|
|
379
|
+
: [chalk.bold('ISO Week'), chalk.bold('n'),
|
|
380
|
+
chalk.bold('Input'), chalk.bold('Output'),
|
|
381
|
+
chalk.bold('Total'), chalk.bold('OC'),
|
|
382
|
+
chalk.bold('CX'), chalk.bold('MM'), chalk.bold('Dist')];
|
|
383
|
+
const t = new Table({ head, style: { head: [], border: [] } });
|
|
384
|
+
|
|
385
|
+
for (const p of items) {
|
|
386
|
+
const dominantTool = Object.entries(p.byTool).sort((a, b) => b[1] - a[1])[0]?.[0] || 'opencode';
|
|
387
|
+
const row = [
|
|
388
|
+
chalk.bold(p.week),
|
|
389
|
+
fmtInt(p.n),
|
|
390
|
+
fmtCompact(p.tokensInput),
|
|
391
|
+
fmtCompact(p.tokensOutput),
|
|
392
|
+
fmtCompact(p.tokensTotal),
|
|
393
|
+
];
|
|
394
|
+
if (!NARROW) {
|
|
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);
|
|
403
|
+
}
|
|
404
|
+
// TOTAL row
|
|
405
|
+
const tot = overall(records);
|
|
406
|
+
const totalRow = [
|
|
407
|
+
chalk.bgGray.white.bold(' TOTAL '),
|
|
408
|
+
chalk.bgGray.white.bold(fmtInt(records.length)),
|
|
409
|
+
chalk.bgGray.white.bold(fmtCompact(tot.tokensInput)),
|
|
410
|
+
chalk.bgGray.white.bold(fmtCompact(tot.tokensOutput)),
|
|
411
|
+
chalk.bgGray.white.bold(fmtCompact(tot.tokensTotal)),
|
|
412
|
+
];
|
|
413
|
+
if (!NARROW) {
|
|
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);
|
|
422
|
+
|
|
423
|
+
return boxen(t.toString(), {
|
|
424
|
+
title: chalk.bold('Per Minggu (Weekly)'),
|
|
425
|
+
borderStyle: 'round',
|
|
426
|
+
borderColor: 'magenta',
|
|
427
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Top N heaviest sessions
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
export function renderTopSessions(records, n = 5) {
|
|
436
|
+
const top = topSessions(records, n);
|
|
437
|
+
if (top.length === 0) return '';
|
|
438
|
+
|
|
439
|
+
if (NARROW) {
|
|
440
|
+
const t = new Table({
|
|
441
|
+
head: [chalk.bold('#'), chalk.bold('Total'), chalk.bold('In/Out'),
|
|
442
|
+
chalk.bold('Cost'), chalk.bold('Title')],
|
|
443
|
+
style: { head: [], border: [] },
|
|
444
|
+
});
|
|
445
|
+
top.forEach((r, i) => {
|
|
446
|
+
t.push([
|
|
447
|
+
chalk.bold(String(i + 1)),
|
|
448
|
+
chalk.bold.yellow(fmtCompact(r.tokensTotal)),
|
|
449
|
+
`${fmtCompact(r.tokensInput)}/${fmtCompact(r.tokensOutput)}`,
|
|
450
|
+
r.cost ? '$' + r.cost.toFixed(4) : '—',
|
|
451
|
+
truncEnd(r.title, 30) || '—',
|
|
452
|
+
]);
|
|
453
|
+
});
|
|
454
|
+
return boxen(t.toString() + '\n' + chalk.dim('(full project + model in wide mode)'),
|
|
455
|
+
{ title: chalk.bold.yellow(`Top ${n} Heaviest Sessions`),
|
|
456
|
+
borderStyle: 'round', borderColor: 'yellow',
|
|
457
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 } });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const t = new Table({
|
|
461
|
+
head: [
|
|
462
|
+
chalk.bold('#'), chalk.bold('Total'),
|
|
463
|
+
chalk.bold('Input'), chalk.bold('Output'),
|
|
464
|
+
chalk.bold('Cost'), chalk.bold('Model'),
|
|
465
|
+
chalk.bold('Project'), chalk.bold('Title'),
|
|
466
|
+
],
|
|
467
|
+
style: { head: [], border: [] },
|
|
468
|
+
});
|
|
469
|
+
top.forEach((r, i) => {
|
|
470
|
+
t.push([
|
|
471
|
+
chalk.bold(String(i + 1)),
|
|
472
|
+
chalk.bold.yellow(fmtCompact(r.tokensTotal)),
|
|
473
|
+
fmtCompact(r.tokensInput),
|
|
474
|
+
fmtCompact(r.tokensOutput),
|
|
475
|
+
r.cost ? '$' + r.cost.toFixed(4) : '—',
|
|
476
|
+
shortModel(r.model),
|
|
477
|
+
truncEnd(r.project, 30),
|
|
478
|
+
truncEnd(r.title, 40) || '—',
|
|
479
|
+
]);
|
|
480
|
+
});
|
|
481
|
+
return boxen(t.toString(), {
|
|
482
|
+
title: chalk.bold.yellow(`Top ${n} Heaviest Sessions`),
|
|
483
|
+
borderStyle: 'round',
|
|
484
|
+
borderColor: 'yellow',
|
|
485
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
// Notes
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
export function renderNotes(detections, errors) {
|
|
494
|
+
const lines = [];
|
|
495
|
+
lines.push(chalk.bold.underline('Token definitions'));
|
|
496
|
+
lines.push(` • ${chalk.cyan('Input')} — prompt tokens (what was sent to the model)`);
|
|
497
|
+
lines.push(` • ${chalk.green('Output')} — completion tokens (what the model generated)`);
|
|
498
|
+
lines.push(` • ${chalk.cyan('Cache Read')} — prompt tokens served from provider cache (cheap)`);
|
|
499
|
+
lines.push(` • ${chalk.cyan('Cache Write')} — prompt tokens cached for future use (OpenCode only)`);
|
|
500
|
+
lines.push(` • ${chalk.yellow('Reasoning')} — extended thinking / chain-of-thought tokens`);
|
|
501
|
+
|
|
502
|
+
lines.push('');
|
|
503
|
+
lines.push(chalk.bold.underline('Detected tools with token data'));
|
|
504
|
+
const withTokens = detections.filter(d => d.hasTokens && d.status === 'present');
|
|
505
|
+
if (withTokens.length === 0) {
|
|
506
|
+
lines.push(' ' + chalk.dim('(none — only presence info available)'));
|
|
507
|
+
} else {
|
|
508
|
+
for (const d of withTokens) {
|
|
509
|
+
lines.push(` ${chalk.green('●')} ${colorize(d.name, toolColor(d.key))} ${chalk.dim(d.description)}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
lines.push('');
|
|
514
|
+
lines.push(chalk.bold.underline('Detected tools without token data (presence only)'));
|
|
515
|
+
const noTokens = detections.filter(d => !d.hasTokens && d.status === 'present');
|
|
516
|
+
if (noTokens.length === 0) {
|
|
517
|
+
lines.push(' ' + chalk.dim('(none)'));
|
|
518
|
+
} else {
|
|
519
|
+
for (const d of noTokens) {
|
|
520
|
+
lines.push(` ${chalk.green('●')} ${colorize(d.name, toolColor(d.key))} ${chalk.dim(d.description)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
lines.push('');
|
|
525
|
+
lines.push(chalk.bold.underline('Env overrides'));
|
|
526
|
+
lines.push(' $CLAUDE_HOME $CODEX_HOME $OPENCODE_HOME $MIMOCODE_HOME');
|
|
527
|
+
lines.push(' $COPILOT_HOME $ANTIGRAVITY_HOME $GEMINI_HOME');
|
|
528
|
+
lines.push(' $AI_USAGE_PATHS_JSON = ' + chalk.italic('\'{"codex":"/custom/path"}\''));
|
|
529
|
+
|
|
530
|
+
if (errors && errors.length) {
|
|
531
|
+
lines.push('');
|
|
532
|
+
lines.push(chalk.bold.underline(chalk.red(`Errors (${errors.length})`)));
|
|
533
|
+
for (const e of errors.slice(0, 5)) {
|
|
534
|
+
lines.push(' ' + chalk.red('! ') + e);
|
|
535
|
+
}
|
|
536
|
+
if (errors.length > 5) lines.push(' ' + chalk.dim(`... and ${errors.length - 5} more`));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return boxen(lines.join('\n'), {
|
|
540
|
+
borderStyle: 'round',
|
|
541
|
+
borderColor: 'gray',
|
|
542
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
543
|
+
});
|
|
544
|
+
}
|