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/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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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) =>
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
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(
|
|
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(
|
|
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
|
|
43
|
-
--json
|
|
44
|
-
--markdown, --md
|
|
45
|
-
--
|
|
46
|
-
--
|
|
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(
|
|
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 =
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
|
109
|
-
|
|
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
|
|
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(
|
|
161
|
+
console.log(sections.join("\n\n"));
|
|
139
162
|
}
|
|
140
163
|
|
|
141
|
-
main().catch(err => {
|
|
142
|
-
console.error(
|
|
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
|
+
"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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|
|
31
|
-
tokensInput: sum(records,
|
|
32
|
-
tokensOutput: sum(records,
|
|
33
|
-
tokensCacheRead: sum(records,
|
|
34
|
-
tokensCacheWrite: sum(records,
|
|
35
|
-
tokensReasoning: sum(records,
|
|
36
|
-
cost: sum(records,
|
|
37
|
-
avg: avg(records,
|
|
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({
|
|
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(
|
|
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) {
|