ai-usage-analyzer 0.3.2 → 0.4.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/bin/ai-usage.js +53 -30
- package/package.json +1 -1
- package/src/aggregate.js +41 -21
- package/src/loaders.js +173 -94
- package/src/markdown.js +127 -66
- package/src/render.js +503 -185
- package/src/tui.js +310 -0
package/src/markdown.js
CHANGED
|
@@ -3,43 +3,50 @@
|
|
|
3
3
|
// copies cleanly into issues, PRs, Notion, etc.
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
MONTH_NAMES,
|
|
7
|
+
overall,
|
|
8
|
+
perMonth,
|
|
9
|
+
perProject,
|
|
10
|
+
perTool,
|
|
11
|
+
perToolPerMonth,
|
|
12
|
+
perWeek,
|
|
13
|
+
tokenBreakdown,
|
|
14
|
+
topSessions,
|
|
15
|
+
} from "./aggregate.js";
|
|
16
|
+
import { getToolBarChar, TOOLS } from "./tools.js";
|
|
10
17
|
|
|
11
18
|
// ---------------------------------------------------------------------------
|
|
12
19
|
// Number formatting helpers (re-implemented locally to avoid TUI deps)
|
|
13
20
|
// ---------------------------------------------------------------------------
|
|
14
21
|
|
|
15
22
|
export function fmtInt(n) {
|
|
16
|
-
return Number(n || 0).toLocaleString(
|
|
23
|
+
return Number(n || 0).toLocaleString("en-US");
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export function fmtCompact(n) {
|
|
20
27
|
const a = Math.abs(n);
|
|
21
|
-
if (a >= 1e9) return (n / 1e9).toFixed(2) +
|
|
22
|
-
if (a >= 1e6) return (n / 1e6).toFixed(2) +
|
|
23
|
-
if (a >= 1e3) return (n / 1e3).toFixed(1) +
|
|
28
|
+
if (a >= 1e9) return (n / 1e9).toFixed(2) + "B";
|
|
29
|
+
if (a >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
30
|
+
if (a >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
24
31
|
return String(n || 0);
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
function fmtCost(n) {
|
|
28
|
-
if (!n) return
|
|
29
|
-
return
|
|
35
|
+
if (!n) return "—";
|
|
36
|
+
return "$" + Number(n).toFixed(2);
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
function compactHome(p) {
|
|
33
|
-
if (!p) return
|
|
34
|
-
const home = process.env.HOME ||
|
|
35
|
-
return p.startsWith(home) ?
|
|
40
|
+
if (!p) return "—";
|
|
41
|
+
const home = process.env.HOME || "";
|
|
42
|
+
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
function bar(value, max, width) {
|
|
39
|
-
if (!max || max <= 0) return
|
|
46
|
+
if (!max || max <= 0) return "░".repeat(width);
|
|
40
47
|
const pct = Math.max(0, Math.min(1, value / max));
|
|
41
48
|
const filled = Math.round(pct * width);
|
|
42
|
-
return
|
|
49
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
// Markdown has no color, so each tool's stacked-bar segment is shown with
|
|
@@ -47,17 +54,18 @@ function bar(value, max, width) {
|
|
|
47
54
|
// src/tools.js alongside its color.
|
|
48
55
|
function stackedBar(byTool, width) {
|
|
49
56
|
const total = Object.values(byTool).reduce((a, b) => a + b, 0);
|
|
50
|
-
if (total <= 0 || width <= 0) return
|
|
57
|
+
if (total <= 0 || width <= 0) return " ".repeat(Math.max(0, width));
|
|
51
58
|
const entries = Object.entries(byTool)
|
|
52
59
|
.filter(([, v]) => v > 0)
|
|
53
60
|
.sort((a, b) => b[1] - a[1]);
|
|
54
|
-
let out =
|
|
61
|
+
let out = "";
|
|
55
62
|
let x = 0;
|
|
56
63
|
for (let i = 0; i < entries.length; i++) {
|
|
57
64
|
const [tool, value] = entries[i];
|
|
58
|
-
const segW =
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
const segW =
|
|
66
|
+
i === entries.length - 1
|
|
67
|
+
? width - x
|
|
68
|
+
: Math.round((value / total) * width);
|
|
61
69
|
if (segW <= 0) continue;
|
|
62
70
|
out += getToolBarChar(tool).repeat(segW);
|
|
63
71
|
x += segW;
|
|
@@ -65,28 +73,32 @@ function stackedBar(byTool, width) {
|
|
|
65
73
|
return out;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
function pct(x) {
|
|
76
|
+
function pct(x) {
|
|
77
|
+
return (x * 100).toFixed(1) + "%";
|
|
78
|
+
}
|
|
69
79
|
|
|
70
80
|
function mdEscape(s) {
|
|
71
|
-
if (s == null) return
|
|
72
|
-
return String(s).replace(/\|/g,
|
|
81
|
+
if (s == null) return "";
|
|
82
|
+
return String(s).replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
function shortModel(m) {
|
|
76
|
-
if (!m) return
|
|
77
|
-
if (typeof m ===
|
|
86
|
+
if (!m) return "—";
|
|
87
|
+
if (typeof m === "string" && m.trim().startsWith("{")) {
|
|
78
88
|
try {
|
|
79
89
|
const d = JSON.parse(m);
|
|
80
90
|
const id = d.id || d.model || m;
|
|
81
91
|
const prov = d.providerId || d.provider;
|
|
82
|
-
return id + (prov ? ` (${prov})` :
|
|
83
|
-
} catch {
|
|
92
|
+
return id + (prov ? ` (${prov})` : "");
|
|
93
|
+
} catch {
|
|
94
|
+
return m.slice(0, 30);
|
|
95
|
+
}
|
|
84
96
|
}
|
|
85
97
|
return m;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
function today() {
|
|
89
|
-
return new Date().toISOString().split(
|
|
101
|
+
return new Date().toISOString().split("T")[0];
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
// ---------------------------------------------------------------------------
|
|
@@ -94,8 +106,12 @@ function today() {
|
|
|
94
106
|
// ---------------------------------------------------------------------------
|
|
95
107
|
|
|
96
108
|
export function renderMarkdown({
|
|
97
|
-
records = [],
|
|
98
|
-
|
|
109
|
+
records = [],
|
|
110
|
+
detections = [],
|
|
111
|
+
errors = [],
|
|
112
|
+
dateRange = [null, null],
|
|
113
|
+
topN = 5,
|
|
114
|
+
generatedAt = null,
|
|
99
115
|
}) {
|
|
100
116
|
const out = [];
|
|
101
117
|
const tot = overall(records);
|
|
@@ -106,11 +122,13 @@ export function renderMarkdown({
|
|
|
106
122
|
// Header --------------------------------------------------------------
|
|
107
123
|
out.push(`# AI Token Usage Report`);
|
|
108
124
|
out.push(``);
|
|
109
|
-
out.push(
|
|
125
|
+
out.push(
|
|
126
|
+
`> Generated by [\`ai-usage-analyzer\`](https://github.com/adetxt/ai-usage-analyzer) on ${generatedAt ? generatedAt.split("T")[0] : today()}`,
|
|
127
|
+
);
|
|
110
128
|
out.push(``);
|
|
111
129
|
|
|
112
130
|
if (hasData) {
|
|
113
|
-
out.push(`**Range**: ${dateRange[0] ??
|
|
131
|
+
out.push(`**Range**: ${dateRange[0] ?? "—"} → ${dateRange[1] ?? "—"} `);
|
|
114
132
|
out.push(`**Sessions**: ${fmtInt(records.length)} `);
|
|
115
133
|
out.push(`**Total tokens**: ${fmtCompact(tot.tokensTotal)} `);
|
|
116
134
|
if (tot.cost > 0) {
|
|
@@ -127,13 +145,13 @@ export function renderMarkdown({
|
|
|
127
145
|
out.push(`| Tool | Status | Path | Count | Tokens |`);
|
|
128
146
|
out.push(`|---|---|---|---:|---|`);
|
|
129
147
|
for (const d of detections) {
|
|
130
|
-
const status = d.status ===
|
|
131
|
-
const path = d.path ?
|
|
132
|
-
const count = d.count ? fmtInt(d.count) :
|
|
133
|
-
const tok = d.hasTokens
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
const status = d.status === "present" ? "✅ present" : "❌ absent";
|
|
149
|
+
const path = d.path ? "`" + mdEscape(compactHome(d.path)) + "`" : "—";
|
|
150
|
+
const count = d.count ? fmtInt(d.count) : "—";
|
|
151
|
+
const tok = d.hasTokens ? (d.count ? "✅" : "—") : "n/a";
|
|
152
|
+
out.push(
|
|
153
|
+
`| ${mdEscape(d.name)} | ${status} | ${path} | ${count} | ${tok} |`,
|
|
154
|
+
);
|
|
137
155
|
}
|
|
138
156
|
out.push(``);
|
|
139
157
|
|
|
@@ -144,7 +162,7 @@ export function renderMarkdown({
|
|
|
144
162
|
for (const e of errors) out.push(`- ${mdEscape(e)}`);
|
|
145
163
|
out.push(``);
|
|
146
164
|
}
|
|
147
|
-
return out.join(
|
|
165
|
+
return out.join("\n");
|
|
148
166
|
}
|
|
149
167
|
|
|
150
168
|
// Overview ------------------------------------------------------------
|
|
@@ -155,20 +173,34 @@ export function renderMarkdown({
|
|
|
155
173
|
out.push(`| Tool | Sessions | Total | Avg/sess | Cost |`);
|
|
156
174
|
out.push(`|---|---:|---:|---:|---:|`);
|
|
157
175
|
for (const t of tools) {
|
|
158
|
-
out.push(
|
|
176
|
+
out.push(
|
|
177
|
+
`| ${mdEscape(t.tool)} | ${fmtInt(t.n)} | ${fmtCompact(t.tokensTotal)} | ${fmtCompact(t.avg)} | ${fmtCost(t.cost)} |`,
|
|
178
|
+
);
|
|
159
179
|
}
|
|
160
|
-
out.push(
|
|
180
|
+
out.push(
|
|
181
|
+
`| **TOTAL** | **${fmtInt(records.length)}** | **${fmtCompact(tot.tokensTotal)}** | **${fmtCompact(tot.avg)}** | **${fmtCost(tot.cost)}** |`,
|
|
182
|
+
);
|
|
161
183
|
out.push(``);
|
|
162
184
|
|
|
163
185
|
out.push(`### Token Breakdown`);
|
|
164
186
|
out.push(``);
|
|
165
187
|
out.push(`| Type | Tokens | Share |`);
|
|
166
188
|
out.push(`|---|---:|---:|`);
|
|
167
|
-
out.push(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
out.push(
|
|
171
|
-
|
|
189
|
+
out.push(
|
|
190
|
+
`| Input | ${fmtCompact(breakdown.input)} | ${pct(breakdown.ratios.input)} |`,
|
|
191
|
+
);
|
|
192
|
+
out.push(
|
|
193
|
+
`| Output | ${fmtCompact(breakdown.output)} | ${pct(breakdown.ratios.output)} |`,
|
|
194
|
+
);
|
|
195
|
+
out.push(
|
|
196
|
+
`| Cache Read | ${fmtCompact(breakdown.cacheRead)} | ${pct(breakdown.ratios.cacheRead)} |`,
|
|
197
|
+
);
|
|
198
|
+
out.push(
|
|
199
|
+
`| Cache Write | ${fmtCompact(breakdown.cacheWrite)} | ${pct(breakdown.ratios.cacheWrite)} |`,
|
|
200
|
+
);
|
|
201
|
+
out.push(
|
|
202
|
+
`| Reasoning | ${fmtCompact(breakdown.reasoning)} | ${pct(breakdown.ratios.reasoning)} |`,
|
|
203
|
+
);
|
|
172
204
|
out.push(`| **Total** | **${fmtCompact(breakdown.total)}** | **100.0%** |`);
|
|
173
205
|
out.push(``);
|
|
174
206
|
|
|
@@ -177,18 +209,25 @@ export function renderMarkdown({
|
|
|
177
209
|
if (projects.length > 0) {
|
|
178
210
|
out.push(`## Per Project`);
|
|
179
211
|
out.push(``);
|
|
180
|
-
out.push(
|
|
181
|
-
|
|
212
|
+
out.push(
|
|
213
|
+
`| Project | Sessions | By Tool | Input | Output | Cache | Total | Dist |`,
|
|
214
|
+
);
|
|
215
|
+
out.push(`|---|---:|---|---:|---:|---:|---:|---|`);
|
|
182
216
|
for (const p of projects) {
|
|
183
217
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
184
218
|
const proj = compactHome(p.project);
|
|
185
|
-
|
|
219
|
+
const nByToolStr = Object.entries(p.nByTool)
|
|
220
|
+
.sort((a, b) => b[1] - a[1])
|
|
221
|
+
.map(([tool, count]) => `${tool}×${count}`)
|
|
222
|
+
.join(" ");
|
|
223
|
+
out.push(
|
|
224
|
+
`| \`${mdEscape(proj)}\` | ${fmtInt(p.n)} | ${mdEscape(nByToolStr)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${stackedBar(p.byTool, 20)} |`,
|
|
225
|
+
);
|
|
186
226
|
}
|
|
187
227
|
out.push(``);
|
|
188
|
-
const legend = TOOLS
|
|
189
|
-
.
|
|
190
|
-
.
|
|
191
|
-
.join(' · ');
|
|
228
|
+
const legend = TOOLS.filter((t) => t.hasTokens)
|
|
229
|
+
.map((t) => `\`${t.barChar}\` ${t.label}`)
|
|
230
|
+
.join(" · ");
|
|
192
231
|
out.push(`_Bar legend: ${legend} · \`·\` other_`);
|
|
193
232
|
out.push(``);
|
|
194
233
|
}
|
|
@@ -199,11 +238,15 @@ export function renderMarkdown({
|
|
|
199
238
|
const max = toolRows[0].tokensTotal;
|
|
200
239
|
out.push(`## Per Tool`);
|
|
201
240
|
out.push(``);
|
|
202
|
-
out.push(
|
|
241
|
+
out.push(
|
|
242
|
+
`| Tool | Sessions | Input | Output | Cache | Total | Cost | Avg/sess | Dist |`,
|
|
243
|
+
);
|
|
203
244
|
out.push(`|---|---:|---:|---:|---:|---:|---:|---:|---|`);
|
|
204
245
|
for (const p of toolRows) {
|
|
205
246
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
206
|
-
out.push(
|
|
247
|
+
out.push(
|
|
248
|
+
`| ${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)} |`,
|
|
249
|
+
);
|
|
207
250
|
}
|
|
208
251
|
out.push(``);
|
|
209
252
|
}
|
|
@@ -214,14 +257,18 @@ export function renderMarkdown({
|
|
|
214
257
|
const max = tpmRows[0].tokensTotal;
|
|
215
258
|
out.push(`## Per Tool per Month`);
|
|
216
259
|
out.push(``);
|
|
217
|
-
out.push(
|
|
260
|
+
out.push(
|
|
261
|
+
`| Tool | Month | Sessions | Input | Output | Cache | Total | Dist |`,
|
|
262
|
+
);
|
|
218
263
|
out.push(`|---|---|---:|---:|---:|---:|---:|---|`);
|
|
219
264
|
for (const p of tpmRows) {
|
|
220
265
|
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
221
266
|
const yyyy = p.month.slice(0, 4);
|
|
222
267
|
const mm = p.month.slice(5, 7);
|
|
223
268
|
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
224
|
-
out.push(
|
|
269
|
+
out.push(
|
|
270
|
+
`| ${mdEscape(p.tool)} | ${label} | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${bar(p.tokensTotal, max, 18)} |`,
|
|
271
|
+
);
|
|
225
272
|
}
|
|
226
273
|
out.push(``);
|
|
227
274
|
}
|
|
@@ -237,7 +284,9 @@ export function renderMarkdown({
|
|
|
237
284
|
const yyyy = m.month.slice(0, 4);
|
|
238
285
|
const mm = m.month.slice(5, 7);
|
|
239
286
|
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
240
|
-
out.push(
|
|
287
|
+
out.push(
|
|
288
|
+
`| ${label} | ${fmtInt(m.n)} | ${fmtCompact(m.tokensInput)} | ${fmtCompact(m.tokensOutput)} | ${fmtCompact(m.tokensTotal)} | ${stackedBar(m.byTool, 20)} |`,
|
|
289
|
+
);
|
|
241
290
|
}
|
|
242
291
|
out.push(``);
|
|
243
292
|
}
|
|
@@ -250,7 +299,9 @@ export function renderMarkdown({
|
|
|
250
299
|
out.push(`| Week | Sessions | Input | Output | Total | Dist |`);
|
|
251
300
|
out.push(`|---|---:|---:|---:|---:|---|`);
|
|
252
301
|
for (const w of weeks) {
|
|
253
|
-
out.push(
|
|
302
|
+
out.push(
|
|
303
|
+
`| ${w.week} | ${fmtInt(w.n)} | ${fmtCompact(w.tokensInput)} | ${fmtCompact(w.tokensOutput)} | ${fmtCompact(w.tokensTotal)} | ${stackedBar(w.byTool, 18)} |`,
|
|
304
|
+
);
|
|
254
305
|
}
|
|
255
306
|
out.push(``);
|
|
256
307
|
}
|
|
@@ -264,7 +315,9 @@ export function renderMarkdown({
|
|
|
264
315
|
out.push(`|---:|---:|---:|---:|---:|---|---|---|`);
|
|
265
316
|
top.forEach((r, i) => {
|
|
266
317
|
const proj = compactHome(r.project);
|
|
267
|
-
out.push(
|
|
318
|
+
out.push(
|
|
319
|
+
`| ${i + 1} | ${fmtCompact(r.tokensTotal)} | ${fmtCompact(r.tokensInput)} | ${fmtCompact(r.tokensOutput)} | ${r.cost ? "$" + r.cost.toFixed(4) : "—"} | ${mdEscape(shortModel(r.model))} | \`${mdEscape(proj)}\` | ${mdEscape(r.title || "—")} |`,
|
|
320
|
+
);
|
|
268
321
|
});
|
|
269
322
|
out.push(``);
|
|
270
323
|
}
|
|
@@ -276,12 +329,18 @@ export function renderMarkdown({
|
|
|
276
329
|
out.push(``);
|
|
277
330
|
out.push(`- **Input** — prompt tokens sent to the model`);
|
|
278
331
|
out.push(`- **Output** — completion tokens generated by the model`);
|
|
279
|
-
out.push(
|
|
280
|
-
|
|
332
|
+
out.push(
|
|
333
|
+
`- **Cache Read** — prompt tokens served from the provider's cache (cheap)`,
|
|
334
|
+
);
|
|
335
|
+
out.push(
|
|
336
|
+
`- **Cache Write** — prompt tokens cached for future use (OpenCode only)`,
|
|
337
|
+
);
|
|
281
338
|
out.push(`- **Reasoning** — extended thinking / chain-of-thought tokens`);
|
|
282
339
|
out.push(``);
|
|
283
340
|
|
|
284
|
-
const withTokens = detections.filter(
|
|
341
|
+
const withTokens = detections.filter(
|
|
342
|
+
(d) => d.hasTokens && d.status === "present",
|
|
343
|
+
);
|
|
285
344
|
if (withTokens.length > 0) {
|
|
286
345
|
out.push(`### Detected Tools With Token Data`);
|
|
287
346
|
out.push(``);
|
|
@@ -291,7 +350,9 @@ export function renderMarkdown({
|
|
|
291
350
|
out.push(``);
|
|
292
351
|
}
|
|
293
352
|
|
|
294
|
-
const noTokens = detections.filter(
|
|
353
|
+
const noTokens = detections.filter(
|
|
354
|
+
(d) => !d.hasTokens && d.status === "present",
|
|
355
|
+
);
|
|
295
356
|
if (noTokens.length > 0) {
|
|
296
357
|
out.push(`### Detected Tools Without Token Data`);
|
|
297
358
|
out.push(``);
|
|
@@ -313,5 +374,5 @@ export function renderMarkdown({
|
|
|
313
374
|
out.push(``);
|
|
314
375
|
}
|
|
315
376
|
|
|
316
|
-
return out.join(
|
|
377
|
+
return out.join("\n");
|
|
317
378
|
}
|