ai-usage-analyzer 0.1.0 → 0.2.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 CHANGED
@@ -76,10 +76,12 @@ npx -y github:adetxt/ai-usage-analyzer --help
76
76
  ## Usage
77
77
 
78
78
  ```bash
79
- ai-usage # default TUI
80
- ai-usage --top 10 # show top 10 heaviest sessions
81
- ai-usage --json # machine-readable JSON output
82
- ai-usage --help
79
+ ai-usage # default TUI
80
+ ai-usage --top 10 # show top 10 heaviest sessions
81
+ ai-usage --json # machine-readable JSON output
82
+ ai-usage --markdown # GitHub-flavored Markdown report
83
+ ai-usage --md > report.md # same, save to file
84
+ ai-usage -h # help (also --help)
83
85
  ```
84
86
 
85
87
  ### Environment overrides
@@ -179,6 +181,38 @@ ai-usage --json | jq '.summary'
179
181
  # }
180
182
  ```
181
183
 
184
+ ### Markdown (`--markdown` / `--md`)
185
+
186
+ GitHub-flavored markdown report with the same sections as the TUI (header,
187
+ detected tools, overview, token breakdown, per-project, per-month, per-week,
188
+ top sessions, notes). Designed to be pasted into GitHub issues, PRs, or
189
+ Notion pages.
190
+
191
+ ```bash
192
+ ai-usage --md > report.md
193
+ cat report.md # or just paste the output into a GitHub comment
194
+ ```
195
+
196
+ Distribution bars use Unicode block characters (`█░`) so they render in
197
+ plain markdown without colors. Sample output:
198
+
199
+ ```markdown
200
+ # AI Token Usage Report
201
+
202
+ **Range**: 2026-04-01 → 2026-06-27
203
+ **Sessions**: 411
204
+ **Total tokens**: 1.58B
205
+ **Cost**: $29.40 (opencode only)
206
+
207
+ ## Detected AI Tools
208
+
209
+ | Tool | Status | Path | Count | Tokens |
210
+ |---|---|---|---:|---|
211
+ | Codex | ✅ present | `~/.codex/sessions` | 94 | ✅ |
212
+ | OpenCode | ✅ present | `~/.local/share/opencode/opencode.db` | 328 | ✅ |
213
+ | ...
214
+ ```
215
+
182
216
  ## License
183
217
 
184
218
  MIT
package/bin/ai-usage.js CHANGED
@@ -10,14 +10,18 @@ import {
10
10
  renderPerProject, renderPerMonth, renderPerWeek,
11
11
  renderTopSessions, renderNotes,
12
12
  } from '../src/render.js';
13
+ import { renderMarkdown } from '../src/markdown.js';
13
14
 
14
15
  const args = process.argv.slice(2);
15
- const flags = new Set(args.filter(a => a.startsWith('--')));
16
- const showHelp = flags.has('--help') || flags.has('-h');
17
- const jsonOut = flags.has('--json');
16
+ const hasFlag = (name) => args.includes(`--${name}`) || args.includes(`-${name}`);
17
+ const showHelp = hasFlag('help') || hasFlag('h');
18
+ const jsonOut = hasFlag('json');
19
+ const mdOut = hasFlag('markdown') || hasFlag('md');
18
20
  const topN = (() => {
19
21
  const i = args.indexOf('--top');
20
- return i >= 0 ? parseInt(args[i + 1], 10) || 5 : 5;
22
+ if (i < 0) return 5;
23
+ const v = parseInt(args[i + 1], 10);
24
+ return Number.isFinite(v) && v > 0 ? v : 5;
21
25
  })();
22
26
 
23
27
  if (showHelp) {
@@ -28,9 +32,16 @@ Usage:
28
32
  ai-usage [options]
29
33
 
30
34
  Options:
31
- --top N Show top N heaviest sessions (default: 5)
32
- --json Output machine-readable JSON instead of TUI
33
35
  -h, --help Show this help
36
+ --json Output machine-readable JSON instead of TUI
37
+ --markdown, --md Output as a Markdown report (GitHub-flavored tables)
38
+ --top N Show top N heaviest sessions (default: 5)
39
+
40
+ Examples:
41
+ ai-usage # default TUI
42
+ ai-usage --json | jq .summary # pipe to jq
43
+ ai-usage --md > report.md # save as markdown
44
+ ai-usage --top 20 # show top 20 sessions
34
45
 
35
46
  Environment overrides (per-tool data path):
36
47
  CLAUDE_HOME, CODEX_HOME, OPENCODE_HOME, MIMOCODE_HOME,
@@ -49,6 +60,11 @@ Supported tools:
49
60
  process.exit(0);
50
61
  }
51
62
 
63
+ if (jsonOut && mdOut) {
64
+ console.error('Error: --json and --markdown are mutually exclusive.');
65
+ process.exit(2);
66
+ }
67
+
52
68
  async function main() {
53
69
  const t0 = Date.now();
54
70
  const detections = detectAll();
@@ -70,6 +86,15 @@ async function main() {
70
86
  return;
71
87
  }
72
88
 
89
+ if (mdOut) {
90
+ const out = renderMarkdown({
91
+ records, detections, errors,
92
+ dateRange: range, topN, generatedAt: new Date().toISOString(),
93
+ });
94
+ process.stdout.write(out);
95
+ return;
96
+ }
97
+
73
98
  // TUI render
74
99
  const sections = [
75
100
  renderHeader({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-usage-analyzer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TUI analyzer for local AI coding agent token usage. Auto-detects Claude Code, Codex, OpenCode, MimoCode, Copilot, Antigravity, and Gemini.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -0,0 +1,257 @@
1
+ // Markdown report renderer for AI token usage analysis.
2
+ // Produces a self-contained GitHub-flavored markdown document that
3
+ // copies cleanly into issues, PRs, Notion, etc.
4
+
5
+ import {
6
+ perProject, perMonth, perWeek, perTool, overall, topSessions, tokenBreakdown,
7
+ MONTH_NAMES,
8
+ } from './aggregate.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Number formatting helpers (re-implemented locally to avoid TUI deps)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function fmtInt(n) {
15
+ return Number(n || 0).toLocaleString('en-US');
16
+ }
17
+
18
+ export function fmtCompact(n) {
19
+ const a = Math.abs(n);
20
+ if (a >= 1e9) return (n / 1e9).toFixed(2) + 'B';
21
+ if (a >= 1e6) return (n / 1e6).toFixed(2) + 'M';
22
+ if (a >= 1e3) return (n / 1e3).toFixed(1) + 'K';
23
+ return String(n || 0);
24
+ }
25
+
26
+ function fmtCost(n) {
27
+ if (!n) return '—';
28
+ return '$' + Number(n).toFixed(2);
29
+ }
30
+
31
+ function compactHome(p) {
32
+ if (!p) return '—';
33
+ const home = process.env.HOME || '';
34
+ return p.startsWith(home) ? '~' + p.slice(home.length) : p;
35
+ }
36
+
37
+ function bar(value, max, width) {
38
+ if (!max || max <= 0) return '░'.repeat(width);
39
+ const pct = Math.max(0, Math.min(1, value / max));
40
+ const filled = Math.round(pct * width);
41
+ return '█'.repeat(filled) + '░'.repeat(width - filled);
42
+ }
43
+
44
+ function pct(x) { return (x * 100).toFixed(1) + '%'; }
45
+
46
+ function mdEscape(s) {
47
+ if (s == null) return '';
48
+ return String(s).replace(/\|/g, '\\|').replace(/\n/g, ' ');
49
+ }
50
+
51
+ function shortModel(m) {
52
+ if (!m) return '—';
53
+ if (typeof m === 'string' && m.trim().startsWith('{')) {
54
+ try {
55
+ const d = JSON.parse(m);
56
+ const id = d.id || d.model || m;
57
+ const prov = d.providerId || d.provider;
58
+ return id + (prov ? ` (${prov})` : '');
59
+ } catch { return m.slice(0, 30); }
60
+ }
61
+ return m;
62
+ }
63
+
64
+ function today() {
65
+ return new Date().toISOString().split('T')[0];
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Public: renderMarkdown
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export function renderMarkdown({
73
+ records = [], detections = [], errors = [],
74
+ dateRange = [null, null], topN = 5, generatedAt = null,
75
+ }) {
76
+ const out = [];
77
+ const tot = overall(records);
78
+ const breakdown = tokenBreakdown(tot);
79
+ const tools = perTool(records);
80
+ const hasData = records.length > 0;
81
+
82
+ // Header --------------------------------------------------------------
83
+ out.push(`# AI Token Usage Report`);
84
+ out.push(``);
85
+ out.push(`> Generated by [\`ai-usage-analyzer\`](https://github.com/adetxt/ai-usage-analyzer) on ${generatedAt ? generatedAt.split('T')[0] : today()}`);
86
+ out.push(``);
87
+
88
+ if (hasData) {
89
+ out.push(`**Range**: ${dateRange[0] ?? '—'} → ${dateRange[1] ?? '—'} `);
90
+ out.push(`**Sessions**: ${fmtInt(records.length)} `);
91
+ out.push(`**Total tokens**: ${fmtCompact(tot.tokensTotal)} `);
92
+ if (tot.cost > 0) {
93
+ out.push(`**Cost**: ${fmtCost(tot.cost)} (opencode only) `);
94
+ }
95
+ } else {
96
+ out.push(`**No session data available** — only detection status below.`);
97
+ }
98
+ out.push(``);
99
+
100
+ // Detected tools ------------------------------------------------------
101
+ out.push(`## Detected AI Tools`);
102
+ out.push(``);
103
+ out.push(`| Tool | Status | Path | Count | Tokens |`);
104
+ out.push(`|---|---|---|---:|---|`);
105
+ for (const d of detections) {
106
+ const status = d.status === 'present' ? '✅ present' : '❌ absent';
107
+ const path = d.path ? '`' + mdEscape(compactHome(d.path)) + '`' : '—';
108
+ const count = d.count ? fmtInt(d.count) : '—';
109
+ const tok = d.hasTokens
110
+ ? (d.count ? '✅' : '—')
111
+ : 'n/a';
112
+ out.push(`| ${mdEscape(d.name)} | ${status} | ${path} | ${count} | ${tok} |`);
113
+ }
114
+ out.push(``);
115
+
116
+ if (!hasData) {
117
+ if (errors && errors.length > 0) {
118
+ out.push(`## Errors`);
119
+ out.push(``);
120
+ for (const e of errors) out.push(`- ${mdEscape(e)}`);
121
+ out.push(``);
122
+ }
123
+ return out.join('\n');
124
+ }
125
+
126
+ // Overview ------------------------------------------------------------
127
+ out.push(`## Overview`);
128
+ out.push(``);
129
+ out.push(`### Per-tool Summary`);
130
+ out.push(``);
131
+ out.push(`| Tool | Sessions | Total | Avg/sess | Cost |`);
132
+ out.push(`|---|---:|---:|---:|---:|`);
133
+ for (const t of tools) {
134
+ out.push(`| ${mdEscape(t.tool)} | ${fmtInt(t.n)} | ${fmtCompact(t.tokensTotal)} | ${fmtCompact(t.avg)} | ${fmtCost(t.cost)} |`);
135
+ }
136
+ out.push(`| **TOTAL** | **${fmtInt(records.length)}** | **${fmtCompact(tot.tokensTotal)}** | **${fmtCompact(tot.avg)}** | **${fmtCost(tot.cost)}** |`);
137
+ out.push(``);
138
+
139
+ out.push(`### Token Breakdown`);
140
+ out.push(``);
141
+ out.push(`| Type | Tokens | Share |`);
142
+ out.push(`|---|---:|---:|`);
143
+ out.push(`| Input | ${fmtCompact(breakdown.input)} | ${pct(breakdown.ratios.input)} |`);
144
+ out.push(`| Output | ${fmtCompact(breakdown.output)} | ${pct(breakdown.ratios.output)} |`);
145
+ out.push(`| Cache Read | ${fmtCompact(breakdown.cacheRead)} | ${pct(breakdown.ratios.cacheRead)} |`);
146
+ out.push(`| Cache Write | ${fmtCompact(breakdown.cacheWrite)} | ${pct(breakdown.ratios.cacheWrite)} |`);
147
+ out.push(`| Reasoning | ${fmtCompact(breakdown.reasoning)} | ${pct(breakdown.ratios.reasoning)} |`);
148
+ out.push(`| **Total** | **${fmtCompact(breakdown.total)}** | **100.0%** |`);
149
+ out.push(``);
150
+
151
+ // Per Project ---------------------------------------------------------
152
+ const projects = perProject(records);
153
+ if (projects.length > 0) {
154
+ const max = projects[0].tokensTotal;
155
+ out.push(`## Per Project`);
156
+ out.push(``);
157
+ out.push(`| Tool | Project | Sessions | Input | Output | Cache | Total | Dist |`);
158
+ out.push(`|---|---|---:|---:|---:|---:|---:|---|`);
159
+ for (const p of projects) {
160
+ const cache = p.tokensCacheRead + p.tokensCacheWrite;
161
+ const proj = compactHome(p.project);
162
+ out.push(`| ${mdEscape(p.tool)} | \`${mdEscape(proj)}\` | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${bar(p.tokensTotal, max, 20)} |`);
163
+ }
164
+ out.push(``);
165
+ }
166
+
167
+ // Per Month -----------------------------------------------------------
168
+ const months = perMonth(records);
169
+ if (months.length > 0) {
170
+ const max = Math.max(...months.map(m => m.tokensTotal));
171
+ out.push(`## Per Month`);
172
+ out.push(``);
173
+ out.push(`| Month | Sessions | Input | Output | Total | OC | CX | MM | Dist |`);
174
+ out.push(`|---|---:|---:|---:|---:|---:|---:|---:|---|`);
175
+ for (const m of months) {
176
+ const yyyy = m.month.slice(0, 4);
177
+ const mm = m.month.slice(5, 7);
178
+ const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
179
+ out.push(`| ${label} | ${fmtInt(m.n)} | ${fmtCompact(m.tokensInput)} | ${fmtCompact(m.tokensOutput)} | ${fmtCompact(m.tokensTotal)} | ${m.byTool.opencode ? fmtCompact(m.byTool.opencode) : '—'} | ${m.byTool.codex ? fmtCompact(m.byTool.codex) : '—'} | ${m.byTool.mimocode ? fmtCompact(m.byTool.mimocode) : '—'} | ${bar(m.tokensTotal, max, 20)} |`);
180
+ }
181
+ out.push(``);
182
+ }
183
+
184
+ // Per Week ------------------------------------------------------------
185
+ const weeks = perWeek(records);
186
+ if (weeks.length > 0) {
187
+ const max = Math.max(...weeks.map(w => w.tokensTotal));
188
+ out.push(`## Per Week`);
189
+ out.push(``);
190
+ out.push(`| Week | Sessions | Input | Output | Total | OC | CX | MM | Dist |`);
191
+ out.push(`|---|---:|---:|---:|---:|---:|---:|---:|---|`);
192
+ for (const w of weeks) {
193
+ out.push(`| ${w.week} | ${fmtInt(w.n)} | ${fmtCompact(w.tokensInput)} | ${fmtCompact(w.tokensOutput)} | ${fmtCompact(w.tokensTotal)} | ${w.byTool.opencode ? fmtCompact(w.byTool.opencode) : '—'} | ${w.byTool.codex ? fmtCompact(w.byTool.codex) : '—'} | ${w.byTool.mimocode ? fmtCompact(w.byTool.mimocode) : '—'} | ${bar(w.tokensTotal, max, 18)} |`);
194
+ }
195
+ out.push(``);
196
+ }
197
+
198
+ // Top N ---------------------------------------------------------------
199
+ const top = topSessions(records, topN);
200
+ if (top.length > 0) {
201
+ out.push(`## Top ${top.length} Heaviest Sessions`);
202
+ out.push(``);
203
+ out.push(`| # | Total | Input | Output | Cost | Model | Project | Title |`);
204
+ out.push(`|---:|---:|---:|---:|---:|---|---|---|`);
205
+ top.forEach((r, i) => {
206
+ const proj = compactHome(r.project);
207
+ 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 || '—')} |`);
208
+ });
209
+ out.push(``);
210
+ }
211
+
212
+ // Notes ---------------------------------------------------------------
213
+ out.push(`## Notes`);
214
+ out.push(``);
215
+ out.push(`### Token Definitions`);
216
+ out.push(``);
217
+ out.push(`- **Input** — prompt tokens sent to the model`);
218
+ out.push(`- **Output** — completion tokens generated by the model`);
219
+ out.push(`- **Cache Read** — prompt tokens served from the provider's cache (cheap)`);
220
+ out.push(`- **Cache Write** — prompt tokens cached for future use (OpenCode only)`);
221
+ out.push(`- **Reasoning** — extended thinking / chain-of-thought tokens`);
222
+ out.push(``);
223
+
224
+ const withTokens = detections.filter(d => d.hasTokens && d.status === 'present');
225
+ if (withTokens.length > 0) {
226
+ out.push(`### Detected Tools With Token Data`);
227
+ out.push(``);
228
+ for (const d of withTokens) {
229
+ out.push(`- **${mdEscape(d.name)}** — \`${mdEscape(d.description)}\``);
230
+ }
231
+ out.push(``);
232
+ }
233
+
234
+ const noTokens = detections.filter(d => !d.hasTokens && d.status === 'present');
235
+ if (noTokens.length > 0) {
236
+ out.push(`### Detected Tools Without Token Data`);
237
+ out.push(``);
238
+ for (const d of noTokens) {
239
+ out.push(`- **${mdEscape(d.name)}** — \`${mdEscape(d.description)}\``);
240
+ }
241
+ out.push(``);
242
+ }
243
+
244
+ if (errors && errors.length > 0) {
245
+ out.push(`### Errors`);
246
+ out.push(``);
247
+ for (const e of errors.slice(0, 10)) {
248
+ out.push(`- ${mdEscape(e)}`);
249
+ }
250
+ if (errors.length > 10) {
251
+ out.push(`- … and ${errors.length - 10} more`);
252
+ }
253
+ out.push(``);
254
+ }
255
+
256
+ return out.join('\n');
257
+ }