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 CHANGED
@@ -2,30 +2,39 @@
2
2
  // AI Usage Analyzer - TUI for local AI coding agent token consumption
3
3
  // Auto-detects: Claude Code, Codex, OpenCode, MimoCode, Copilot, Antigravity, Gemini
4
4
 
5
- import { detectAll } from '../src/detectors.js';
6
- import { loadAll, dateRange } from '../src/loaders.js';
7
- import { overall } from '../src/aggregate.js';
5
+ import { overall } from "../src/aggregate.js";
6
+ import { detectAll } from "../src/detectors.js";
7
+ import { dateRange, loadAll } from "../src/loaders.js";
8
+ import { renderMarkdown } from "../src/markdown.js";
8
9
  import {
9
- renderHeader, renderDetections, renderOverview,
10
- renderPerProject, renderPerTool, renderPerToolPerMonth,
11
- renderPerMonth, renderPerWeek,
12
- renderTopSessions, renderNotes,
13
- } from '../src/render.js';
14
- import { renderMarkdown } from '../src/markdown.js';
10
+ renderDetections,
11
+ renderHeader,
12
+ renderNotes,
13
+ renderOverview,
14
+ renderPerMonth,
15
+ renderPerProject,
16
+ renderPerTool,
17
+ renderPerToolPerMonth,
18
+ renderPerWeek,
19
+ renderTopSessions,
20
+ } from "../src/render.js";
21
+ import { runInteractive } from "../src/tui.js";
15
22
 
16
23
  const args = process.argv.slice(2);
17
- const hasFlag = (name) => args.includes(`--${name}`) || args.includes(`-${name}`);
18
- const showHelp = hasFlag('help') || hasFlag('h');
19
- const jsonOut = hasFlag('json');
20
- const mdOut = hasFlag('markdown') || hasFlag('md');
24
+ const hasFlag = (name) =>
25
+ args.includes(`--${name}`) || args.includes(`-${name}`);
26
+ const showHelp = hasFlag("help") || hasFlag("h");
27
+ const jsonOut = hasFlag("json");
28
+ const mdOut = hasFlag("markdown") || hasFlag("md");
29
+ const staticOut = hasFlag("static") || hasFlag("no-interactive");
21
30
  const topN = (() => {
22
- const i = args.indexOf('--top');
31
+ const i = args.indexOf("--top");
23
32
  if (i < 0) return 5;
24
33
  const v = parseInt(args[i + 1], 10);
25
34
  return Number.isFinite(v) && v > 0 ? v : 5;
26
35
  })();
27
36
  const yearFilter = (() => {
28
- const i = args.indexOf('--year');
37
+ const i = args.indexOf("--year");
29
38
  if (i < 0) return null;
30
39
  const v = parseInt(args[i + 1], 10);
31
40
  return Number.isFinite(v) && v > 1970 && v < 3000 ? v : null;
@@ -39,11 +48,12 @@ Usage:
39
48
  ai-usage [options]
40
49
 
41
50
  Options:
42
- -h, --help Show this help
43
- --json Output machine-readable JSON instead of TUI
44
- --markdown, --md Output as a Markdown report (GitHub-flavored tables)
45
- --top N Show top N heaviest sessions (default: 5)
46
- --year YYYY Filter records to a single year (e.g. --year 2026)
51
+ -h, --help Show this help
52
+ --json Output machine-readable JSON instead of TUI
53
+ --markdown, --md Output as a Markdown report (GitHub-flavored tables)
54
+ --static, --no-interactive Disable interactive mode; dump all sections at once
55
+ --top N Show top N heaviest sessions (default: 5)
56
+ --year YYYY Filter records to a single year (e.g. --year 2026)
47
57
 
48
58
  Examples:
49
59
  ai-usage # default TUI
@@ -70,7 +80,7 @@ Supported tools:
70
80
  }
71
81
 
72
82
  if (jsonOut && mdOut) {
73
- console.error('Error: --json and --markdown are mutually exclusive.');
83
+ console.error("Error: --json and --markdown are mutually exclusive.");
74
84
  process.exit(2);
75
85
  }
76
86
 
@@ -81,9 +91,12 @@ async function main() {
81
91
 
82
92
  // Apply --year filter before any aggregation so dateRange and totals
83
93
  // reflect the filtered set.
84
- const records = yearFilter !== null
85
- ? allRecords.filter(r => r.month && r.month.startsWith(`${yearFilter}-`))
86
- : allRecords;
94
+ const records =
95
+ yearFilter !== null
96
+ ? allRecords.filter(
97
+ (r) => r.month && r.month.startsWith(`${yearFilter}-`),
98
+ )
99
+ : allRecords;
87
100
 
88
101
  const range = dateRange(records);
89
102
  const tot = overall(records);
@@ -105,14 +118,24 @@ async function main() {
105
118
 
106
119
  if (mdOut) {
107
120
  const out = renderMarkdown({
108
- records, detections, errors,
109
- dateRange: range, topN, generatedAt: new Date().toISOString(),
121
+ records,
122
+ detections,
123
+ errors,
124
+ dateRange: range,
125
+ topN,
126
+ generatedAt: new Date().toISOString(),
110
127
  });
111
128
  process.stdout.write(out);
112
129
  return;
113
130
  }
114
131
 
115
- // TUI render
132
+ // Interactive TUI (default when stdout is a TTY and --static not given)
133
+ if (!staticOut) {
134
+ const handled = await runInteractive({ records, detections, errors, topN });
135
+ if (handled) return;
136
+ }
137
+
138
+ // Static fallback (--static, --no-interactive, or piped output)
116
139
  const sections = [
117
140
  renderHeader({
118
141
  totalSessions: records.length,
@@ -135,10 +158,10 @@ async function main() {
135
158
  }
136
159
  sections.push(renderNotes(detections, errors));
137
160
 
138
- console.log(sections.join('\n\n'));
161
+ console.log(sections.join("\n\n"));
139
162
  }
140
163
 
141
- main().catch(err => {
142
- console.error('Fatal error:', err);
164
+ main().catch((err) => {
165
+ console.error("Fatal error:", err);
143
166
  process.exit(1);
144
167
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-usage-analyzer",
3
- "version": "0.3.2",
3
+ "version": "0.4.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",
package/src/aggregate.js CHANGED
@@ -1,9 +1,18 @@
1
1
  // Group session records by various dimensions and compute stats.
2
2
 
3
3
  const MONTH_NAMES = {
4
- '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
5
- '05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
6
- '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec',
4
+ "01": "Jan",
5
+ "02": "Feb",
6
+ "03": "Mar",
7
+ "04": "Apr",
8
+ "05": "May",
9
+ "06": "Jun",
10
+ "07": "Jul",
11
+ "08": "Aug",
12
+ "09": "Sep",
13
+ 10: "Oct",
14
+ 11: "Nov",
15
+ 12: "Dec",
7
16
  };
8
17
 
9
18
  function sum(arr, key) {
@@ -27,14 +36,14 @@ export function groupBy(records, keyFn) {
27
36
  function summarize(records) {
28
37
  return {
29
38
  n: records.length,
30
- tokensTotal: sum(records, 'tokensTotal'),
31
- tokensInput: sum(records, 'tokensInput'),
32
- tokensOutput: sum(records, 'tokensOutput'),
33
- tokensCacheRead: sum(records, 'tokensCacheRead'),
34
- tokensCacheWrite: sum(records, 'tokensCacheWrite'),
35
- tokensReasoning: sum(records, 'tokensReasoning'),
36
- cost: sum(records, 'cost'),
37
- avg: avg(records, 'tokensTotal'),
39
+ tokensTotal: sum(records, "tokensTotal"),
40
+ tokensInput: sum(records, "tokensInput"),
41
+ tokensOutput: sum(records, "tokensOutput"),
42
+ tokensCacheRead: sum(records, "tokensCacheRead"),
43
+ tokensCacheWrite: sum(records, "tokensCacheWrite"),
44
+ tokensReasoning: sum(records, "tokensReasoning"),
45
+ cost: sum(records, "cost"),
46
+ avg: avg(records, "tokensTotal"),
38
47
  };
39
48
  }
40
49
 
@@ -46,21 +55,34 @@ function toolBreakdown(records) {
46
55
  return byTool;
47
56
  }
48
57
 
58
+ function toolSessionCount(records) {
59
+ const nByTool = {};
60
+ for (const r of records) {
61
+ nByTool[r.tool] = (nByTool[r.tool] || 0) + 1;
62
+ }
63
+ return nByTool;
64
+ }
65
+
49
66
  export function perProject(records) {
50
67
  // One row per project — tool mix is shown via the stacked bar / byTool,
51
68
  // not as a separate column. Detailed per-tool-per-project breakdown
52
69
  // is no longer surfaced here; the "Per Tool per Month" section is the
53
70
  // place to see per-tool data over time.
54
- const m = groupBy(records, r => r.project);
71
+ const m = groupBy(records, (r) => r.project);
55
72
  const out = [];
56
73
  for (const [project, arr] of m) {
57
- out.push({ project, ...summarize(arr), byTool: toolBreakdown(arr) });
74
+ out.push({
75
+ project,
76
+ ...summarize(arr),
77
+ byTool: toolBreakdown(arr),
78
+ nByTool: toolSessionCount(arr),
79
+ });
58
80
  }
59
81
  return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
60
82
  }
61
83
 
62
84
  export function perMonth(records) {
63
- const m = groupBy(records, r => r.month);
85
+ const m = groupBy(records, (r) => r.month);
64
86
  const out = [];
65
87
  for (const [month, arr] of m) {
66
88
  out.push({ month, ...summarize(arr), byTool: toolBreakdown(arr) });
@@ -69,7 +91,7 @@ export function perMonth(records) {
69
91
  }
70
92
 
71
93
  export function perWeek(records) {
72
- const m = groupBy(records, r => r.week);
94
+ const m = groupBy(records, (r) => r.week);
73
95
  const out = [];
74
96
  for (const [week, arr] of m) {
75
97
  out.push({ week, ...summarize(arr), byTool: toolBreakdown(arr) });
@@ -78,7 +100,7 @@ export function perWeek(records) {
78
100
  }
79
101
 
80
102
  export function perTool(records) {
81
- const m = groupBy(records, r => r.tool);
103
+ const m = groupBy(records, (r) => r.tool);
82
104
  const out = [];
83
105
  for (const [tool, arr] of m) {
84
106
  out.push({ tool, ...summarize(arr) });
@@ -90,10 +112,10 @@ export function perToolPerMonth(records) {
90
112
  // Cross-tab: one row per (tool, month). Lets you see how a single tool's
91
113
  // usage is distributed across months — and avoids the hardcoded OC/CX/MM
92
114
  // column problem in the per-month table.
93
- const m = groupBy(records, r => `${r.tool}\u0001${r.month}`);
115
+ const m = groupBy(records, (r) => `${r.tool}\u0001${r.month}`);
94
116
  const out = [];
95
117
  for (const [k, arr] of m) {
96
- const [tool, month] = k.split('\u0001');
118
+ const [tool, month] = k.split("\u0001");
97
119
  out.push({ tool, month, ...summarize(arr) });
98
120
  }
99
121
  return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
@@ -104,9 +126,7 @@ export function overall(records) {
104
126
  }
105
127
 
106
128
  export function topSessions(records, n = 5) {
107
- return [...records]
108
- .sort((a, b) => b.tokensTotal - a.tokensTotal)
109
- .slice(0, n);
129
+ return [...records].sort((a, b) => b.tokensTotal - a.tokensTotal).slice(0, n);
110
130
  }
111
131
 
112
132
  export function tokenBreakdown(s) {