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/src/markdown.js CHANGED
@@ -3,43 +3,50 @@
3
3
  // copies cleanly into issues, PRs, Notion, etc.
4
4
 
5
5
  import {
6
- perProject, perMonth, perWeek, perTool, perToolPerMonth, overall, topSessions, tokenBreakdown,
7
- MONTH_NAMES,
8
- } from './aggregate.js';
9
- import { getToolBarChar, TOOLS } from './tools.js';
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('en-US');
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) + 'B';
22
- if (a >= 1e6) return (n / 1e6).toFixed(2) + 'M';
23
- if (a >= 1e3) return (n / 1e3).toFixed(1) + 'K';
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 '$' + Number(n).toFixed(2);
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) ? '~' + p.slice(home.length) : p;
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 '░'.repeat(width);
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 '█'.repeat(filled) + '░'.repeat(width - filled);
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 ' '.repeat(Math.max(0, width));
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 = i === entries.length - 1
59
- ? width - x
60
- : Math.round((value / total) * width);
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) { return (x * 100).toFixed(1) + '%'; }
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, '\\|').replace(/\n/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 === 'string' && m.trim().startsWith('{')) {
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 { return m.slice(0, 30); }
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('T')[0];
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 = [], detections = [], errors = [],
98
- dateRange = [null, null], topN = 5, generatedAt = null,
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(`> Generated by [\`ai-usage-analyzer\`](https://github.com/adetxt/ai-usage-analyzer) on ${generatedAt ? generatedAt.split('T')[0] : today()}`);
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] ?? '—'} → ${dateRange[1] ?? '—'} `);
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 === 'present' ? '✅ present' : '❌ absent';
131
- const path = d.path ? '`' + mdEscape(compactHome(d.path)) + '`' : '—';
132
- const count = d.count ? fmtInt(d.count) : '—';
133
- const tok = d.hasTokens
134
- ? (d.count ? '✅' : '—')
135
- : 'n/a';
136
- out.push(`| ${mdEscape(d.name)} | ${status} | ${path} | ${count} | ${tok} |`);
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('\n');
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(`| ${mdEscape(t.tool)} | ${fmtInt(t.n)} | ${fmtCompact(t.tokensTotal)} | ${fmtCompact(t.avg)} | ${fmtCost(t.cost)} |`);
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(`| **TOTAL** | **${fmtInt(records.length)}** | **${fmtCompact(tot.tokensTotal)}** | **${fmtCompact(tot.avg)}** | **${fmtCost(tot.cost)}** |`);
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(`| Input | ${fmtCompact(breakdown.input)} | ${pct(breakdown.ratios.input)} |`);
168
- out.push(`| Output | ${fmtCompact(breakdown.output)} | ${pct(breakdown.ratios.output)} |`);
169
- out.push(`| Cache Read | ${fmtCompact(breakdown.cacheRead)} | ${pct(breakdown.ratios.cacheRead)} |`);
170
- out.push(`| Cache Write | ${fmtCompact(breakdown.cacheWrite)} | ${pct(breakdown.ratios.cacheWrite)} |`);
171
- out.push(`| Reasoning | ${fmtCompact(breakdown.reasoning)} | ${pct(breakdown.ratios.reasoning)} |`);
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(`| Project | Sessions | Input | Output | Cache | Total | Dist |`);
181
- out.push(`|---|---:|---:|---:|---:|---:|---|`);
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
- out.push(`| \`${mdEscape(proj)}\` | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${stackedBar(p.byTool, 20)} |`);
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
- .filter(t => t.hasTokens)
190
- .map(t => `\`${t.barChar}\` ${t.label}`)
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(`| Tool | Sessions | Input | Output | Cache | Total | Cost | Avg/sess | Dist |`);
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(`| ${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)} |`);
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(`| Tool | Month | Sessions | Input | Output | Cache | Total | Dist |`);
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(`| ${mdEscape(p.tool)} | ${label} | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${bar(p.tokensTotal, max, 18)} |`);
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(`| ${label} | ${fmtInt(m.n)} | ${fmtCompact(m.tokensInput)} | ${fmtCompact(m.tokensOutput)} | ${fmtCompact(m.tokensTotal)} | ${stackedBar(m.byTool, 20)} |`);
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(`| ${w.week} | ${fmtInt(w.n)} | ${fmtCompact(w.tokensInput)} | ${fmtCompact(w.tokensOutput)} | ${fmtCompact(w.tokensTotal)} | ${stackedBar(w.byTool, 18)} |`);
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(`| ${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 || '—')} |`);
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(`- **Cache Read** — prompt tokens served from the provider's cache (cheap)`);
280
- out.push(`- **Cache Write** — prompt tokens cached for future use (OpenCode only)`);
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(d => d.hasTokens && d.status === 'present');
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(d => !d.hasTokens && d.status === 'present');
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('\n');
377
+ return out.join("\n");
317
378
  }