ai-usage-analyzer 0.1.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 ADDED
@@ -0,0 +1,184 @@
1
+ # ai-usage-analyzer
2
+
3
+ TUI analyzer for local AI coding agent token consumption. Auto-detects
4
+ **Claude Code**, **Codex**, **OpenCode**, **MimoCode**, **GitHub Copilot**,
5
+ **Antigravity**, and **Gemini CLI** data directories — no hardcoded paths.
6
+
7
+ ```
8
+ ╭──────────────────────────────────────────────────────╮
9
+ │ ◆ AI TOKEN ANALYZER ◆ │
10
+ │ │
11
+ │ 411 sessions • 1.56B total tokens • $29.40 USD │
12
+ │ range: 2026-04-01 → 2026-06-27 │
13
+ ╰──────────────────────────────────────────────────────╯
14
+ ```
15
+
16
+ ## Install
17
+
18
+ Requires **Node.js ≥ 22.5** (for built-in `node:sqlite`).
19
+
20
+ Pick whichever fits your workflow:
21
+
22
+ ### Option 1: `npx` from GitHub (zero install, fastest to try)
23
+
24
+ ```bash
25
+ npx -y github:adetxt/ai-usage-analyzer
26
+ # or with a specific ref
27
+ npx -y github:adetxt/ai-usage-analyzer#v0.1.0
28
+ ```
29
+
30
+ No clone, no `node_modules`, no global install. npx downloads the repo
31
+ on first run and caches it. Subsequent runs are instant.
32
+
33
+ ### Option 2: Clone + install (best for development / contribution)
34
+
35
+ ```bash
36
+ git clone https://github.com/adetxt/ai-usage-analyzer.git
37
+ cd ai-usage-analyzer
38
+ pnpm install
39
+ pnpm link --global # exposes the `ai-usage` command globally
40
+ ```
41
+
42
+ > Uses `pnpm` (declared in the `packageManager` field). `pnpm-lock.yaml`
43
+ > is committed as the source of truth for reproducible installs.
44
+
45
+ ### Option 3: Install globally from GitHub (no clone)
46
+
47
+ ```bash
48
+ pnpm add -g git+https://github.com/adetxt/ai-usage-analyzer.git
49
+ # or with npm
50
+ npm install -g git+https://github.com/adetxt/ai-usage-analyzer.git
51
+ # then
52
+ ai-usage
53
+ ```
54
+
55
+ ### Option 4: From npm registry (when published)
56
+
57
+ ```bash
58
+ npx -y ai-usage-analyzer
59
+ # or
60
+ pnpm add -g ai-usage-analyzer
61
+ ```
62
+
63
+ To publish your own copy: `npm login && npm publish --access public`
64
+ (requires the `ai-usage-analyzer` name to be available on npmjs.com,
65
+ or use a scoped name like `@yourname/ai-usage-analyzer` and update
66
+ the `name` field in `package.json` first).
67
+
68
+ ### Verify install
69
+
70
+ ```bash
71
+ ai-usage --help
72
+ # or via npx
73
+ npx -y github:adetxt/ai-usage-analyzer --help
74
+ ```
75
+
76
+ ## Usage
77
+
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
83
+ ```
84
+
85
+ ### Environment overrides
86
+
87
+ If a tool's data lives outside the default location, override its base path:
88
+
89
+ ```bash
90
+ export CLAUDE_HOME=/custom/path/to/.claude
91
+ export CODEX_HOME=/custom/path/to/.codex
92
+ export OPENCODE_HOME=/custom/path/to/.local/share/opencode
93
+ export MIMOCODE_HOME=/custom/path/to/.local/share/mimocode
94
+ export COPILOT_HOME=/custom/path/to/.copilot
95
+ export ANTIGRAVITY_HOME=/custom/path/to/Antigravity
96
+ export GEMINI_HOME=/custom/path/to/.gemini
97
+ ```
98
+
99
+ Or pass all at once via JSON:
100
+
101
+ ```bash
102
+ export AI_USAGE_PATHS_JSON='{"codex":"/data/codex","opencode":"/data/oc.db"}'
103
+ ```
104
+
105
+ ## Supported tools
106
+
107
+ | Tool | Path | Token data | Source format |
108
+ |---|---|---|---|
109
+ | Claude Code | `~/.claude/transcripts` | presence only | `ses_*.jsonl` |
110
+ | Codex | `~/.codex/sessions/YYYY/MM/DD/` | **yes** | `rollout-*.jsonl` `token_count` events |
111
+ | OpenCode | `~/.local/share/opencode/opencode.db` | **yes** (+cost) | SQLite |
112
+ | MimoCode | `~/.local/share/mimocode/mimocode.db` | **yes** (+cost, when present) | SQLite |
113
+ | GitHub Copilot | `~/.copilot/session-state/` | presence only | `events.jsonl` |
114
+ | Antigravity | `~/Library/Application Support/Antigravity` | presence only | dir scan |
115
+ | Gemini CLI | `~/.gemini/antigravity/conversations` | presence only | `*.pb` protobuf |
116
+
117
+ ## Token breakdown
118
+
119
+ For tools that record token data, the analyzer shows the full breakdown:
120
+
121
+ - **Input** — prompt tokens sent to the model
122
+ - **Output** — completion tokens generated by the model
123
+ - **Cache Read** — prompt tokens served from the provider's cache (cheap)
124
+ - **Cache Write** — prompt tokens cached for future use (OpenCode only)
125
+ - **Reasoning** — extended thinking / chain-of-thought tokens
126
+
127
+ The TUI is adaptive: at terminal widths below 110 columns it drops
128
+ non-essential columns; at 110+ it shows the full breakdown with project
129
+ paths, per-tool token columns, and longer distribution bars.
130
+
131
+ ## Architecture
132
+
133
+ ```
134
+ src/
135
+ ├── detectors.js auto-path discovery (env → well-known locations)
136
+ ├── loaders.js SQLite + JSONL parsers → unified session record
137
+ ├── aggregate.js per-project / per-week / per-month grouping
138
+ ├── render.js TUI (chalk + cli-table3 + boxen + gradient-string)
139
+ bin/
140
+ └── ai-usage.js entry point
141
+ ```
142
+
143
+ The loader produces a unified record shape:
144
+
145
+ ```ts
146
+ {
147
+ tool, sessionId, project, title, week, month, ts,
148
+ tokensInput, tokensOutput, tokensCacheRead, tokensCacheWrite,
149
+ tokensReasoning, tokensTotal, cost, model
150
+ }
151
+ ```
152
+
153
+ The aggregator then groups by project / week / month, and the renderer
154
+ turns each grouping into a colored table with a distribution bar.
155
+
156
+ ## Output modes
157
+
158
+ ### TUI (default)
159
+
160
+ Color-coded, boxed tables with bar charts. Adapts to terminal width.
161
+
162
+ ### JSON (`--json`)
163
+
164
+ Single JSON object with detection results, summary, raw sessions, and errors.
165
+ Suitable for piping into `jq` or feeding a dashboard.
166
+
167
+ ```bash
168
+ ai-usage --json | jq '.summary'
169
+ # {
170
+ # "n": 411,
171
+ # "tokensTotal": 1558000000,
172
+ # "tokensInput": 420900000,
173
+ # "tokensOutput": 4450000,
174
+ # "tokensCacheRead": 1130000000,
175
+ # "tokensCacheWrite": 49000,
176
+ # "tokensReasoning": 2210000,
177
+ # "cost": 29.4,
178
+ # "avg": 3790000
179
+ # }
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // AI Usage Analyzer - TUI for local AI coding agent token consumption
3
+ // Auto-detects: Claude Code, Codex, OpenCode, MimoCode, Copilot, Antigravity, Gemini
4
+
5
+ import { detectAll } from '../src/detectors.js';
6
+ import { loadAll, dateRange } from '../src/loaders.js';
7
+ import { overall } from '../src/aggregate.js';
8
+ import {
9
+ renderHeader, renderDetections, renderOverview,
10
+ renderPerProject, renderPerMonth, renderPerWeek,
11
+ renderTopSessions, renderNotes,
12
+ } from '../src/render.js';
13
+
14
+ 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');
18
+ const topN = (() => {
19
+ const i = args.indexOf('--top');
20
+ return i >= 0 ? parseInt(args[i + 1], 10) || 5 : 5;
21
+ })();
22
+
23
+ if (showHelp) {
24
+ console.log(`
25
+ ai-usage-analyzer — local AI coding agent token consumption TUI
26
+
27
+ Usage:
28
+ ai-usage [options]
29
+
30
+ Options:
31
+ --top N Show top N heaviest sessions (default: 5)
32
+ --json Output machine-readable JSON instead of TUI
33
+ -h, --help Show this help
34
+
35
+ Environment overrides (per-tool data path):
36
+ CLAUDE_HOME, CODEX_HOME, OPENCODE_HOME, MIMOCODE_HOME,
37
+ COPILOT_HOME, ANTIGRAVITY_HOME, GEMINI_HOME,
38
+ AI_USAGE_PATHS_JSON='{"codex":"/custom/path",...}'
39
+
40
+ Supported tools:
41
+ • Claude Code — ~/.claude/transcripts (presence only)
42
+ • Codex — ~/.codex/sessions (tokens from token_count events)
43
+ • OpenCode — ~/.local/share/opencode/opencode.db (tokens + cost)
44
+ • MimoCode — ~/.local/share/mimocode/mimocode.db (tokens + cost)
45
+ • GitHub Copilot — ~/.copilot/session-state (presence only)
46
+ • Antigravity — ~/Library/Application Support/Antigravity (presence only)
47
+ • Gemini CLI — ~/.gemini/antigravity/conversations (presence only)
48
+ `);
49
+ process.exit(0);
50
+ }
51
+
52
+ async function main() {
53
+ const t0 = Date.now();
54
+ const detections = detectAll();
55
+ const { records, errors } = await loadAll(detections);
56
+ const range = dateRange(records);
57
+ const tot = overall(records);
58
+
59
+ if (jsonOut) {
60
+ const out = {
61
+ detections,
62
+ summary: tot,
63
+ dateRange: range,
64
+ sessions: records,
65
+ errors,
66
+ generatedAt: new Date().toISOString(),
67
+ durationMs: Date.now() - t0,
68
+ };
69
+ console.log(JSON.stringify(out, null, 2));
70
+ return;
71
+ }
72
+
73
+ // TUI render
74
+ const sections = [
75
+ renderHeader({
76
+ totalSessions: records.length,
77
+ totalTokens: tot.tokensTotal,
78
+ totalCost: tot.cost,
79
+ dateRange: range,
80
+ }),
81
+ renderDetections(detections),
82
+ ];
83
+ if (records.length > 0) {
84
+ sections.push(
85
+ renderOverview(records, detections),
86
+ renderPerProject(records),
87
+ renderPerMonth(records),
88
+ renderPerWeek(records),
89
+ renderTopSessions(records, topN),
90
+ );
91
+ }
92
+ sections.push(renderNotes(detections, errors));
93
+
94
+ console.log(sections.join('\n\n'));
95
+ }
96
+
97
+ main().catch(err => {
98
+ console.error('Fatal error:', err);
99
+ process.exit(1);
100
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "ai-usage-analyzer",
3
+ "version": "0.1.0",
4
+ "description": "TUI analyzer for local AI coding agent token usage. Auto-detects Claude Code, Codex, OpenCode, MimoCode, Copilot, Antigravity, and Gemini.",
5
+ "type": "module",
6
+ "packageManager": "pnpm@10.33.0",
7
+ "bin": {
8
+ "ai-usage": "bin/ai-usage.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/ai-usage.js",
17
+ "test": "node --test test/*.test.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.5.0",
21
+ "pnpm": ">=10.0.0"
22
+ },
23
+ "keywords": [
24
+ "ai",
25
+ "tokens",
26
+ "claude-code",
27
+ "codex",
28
+ "opencode",
29
+ "mimocode",
30
+ "copilot",
31
+ "antigravity",
32
+ "gemini",
33
+ "tui",
34
+ "cli"
35
+ ],
36
+ "author": "ade",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/adetxt/ai-usage-analyzer.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/adetxt/ai-usage-analyzer/issues"
44
+ },
45
+ "homepage": "https://github.com/adetxt/ai-usage-analyzer#readme",
46
+ "dependencies": {
47
+ "boxen": "^7.1.1",
48
+ "chalk": "^5.3.0",
49
+ "cli-table3": "^0.6.5",
50
+ "gradient-string": "^2.0.2"
51
+ }
52
+ }
@@ -0,0 +1,117 @@
1
+ // Group session records by various dimensions and compute stats.
2
+
3
+ const MONTH_NAMES = {
4
+ '01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
5
+ '05': 'Mei', '06': 'Jun', '07': 'Jul', '08': 'Agu',
6
+ '09': 'Sep', '10': 'Okt', '11': 'Nov', '12': 'Des',
7
+ };
8
+
9
+ function sum(arr, key) {
10
+ return arr.reduce((a, b) => a + (b[key] || 0), 0);
11
+ }
12
+
13
+ function avg(arr, key) {
14
+ return arr.length ? sum(arr, key) / arr.length : 0;
15
+ }
16
+
17
+ export function groupBy(records, keyFn) {
18
+ const out = new Map();
19
+ for (const r of records) {
20
+ const k = keyFn(r);
21
+ if (!out.has(k)) out.set(k, []);
22
+ out.get(k).push(r);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function summarize(records) {
28
+ return {
29
+ 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'),
38
+ };
39
+ }
40
+
41
+ export function perProject(records) {
42
+ const m = groupBy(records, r => `${r.tool}\u0001${r.project}`);
43
+ const out = [];
44
+ for (const [k, arr] of m) {
45
+ const [tool, project] = k.split('\u0001');
46
+ const s = summarize(arr);
47
+ out.push({ tool, project, ...s });
48
+ }
49
+ return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
50
+ }
51
+
52
+ export function perMonth(records) {
53
+ const m = groupBy(records, r => r.month);
54
+ const out = [];
55
+ for (const [month, arr] of m) {
56
+ const byTool = {};
57
+ for (const r of arr) {
58
+ byTool[r.tool] = (byTool[r.tool] || 0) + r.tokensTotal;
59
+ }
60
+ out.push({ month, ...summarize(arr), byTool });
61
+ }
62
+ return out.sort((a, b) => a.month.localeCompare(b.month));
63
+ }
64
+
65
+ export function perWeek(records) {
66
+ const m = groupBy(records, r => r.week);
67
+ const out = [];
68
+ for (const [week, arr] of m) {
69
+ const byTool = {};
70
+ for (const r of arr) {
71
+ byTool[r.tool] = (byTool[r.tool] || 0) + r.tokensTotal;
72
+ }
73
+ out.push({ week, ...summarize(arr), byTool });
74
+ }
75
+ return out.sort((a, b) => a.week.localeCompare(b.week));
76
+ }
77
+
78
+ export function perTool(records) {
79
+ const m = groupBy(records, r => r.tool);
80
+ const out = [];
81
+ for (const [tool, arr] of m) {
82
+ out.push({ tool, ...summarize(arr) });
83
+ }
84
+ return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
85
+ }
86
+
87
+ export function overall(records) {
88
+ return summarize(records);
89
+ }
90
+
91
+ export function topSessions(records, n = 5) {
92
+ return [...records]
93
+ .sort((a, b) => b.tokensTotal - a.tokensTotal)
94
+ .slice(0, n);
95
+ }
96
+
97
+ export function tokenBreakdown(s) {
98
+ // Returns { input, output, cacheRead, cacheWrite, reasoning, total, ratios }
99
+ const total = s.tokensTotal || 1;
100
+ return {
101
+ input: s.tokensInput,
102
+ output: s.tokensOutput,
103
+ cacheRead: s.tokensCacheRead,
104
+ cacheWrite: s.tokensCacheWrite,
105
+ reasoning: s.tokensReasoning,
106
+ total: s.tokensTotal,
107
+ ratios: {
108
+ input: s.tokensInput / total,
109
+ output: s.tokensOutput / total,
110
+ cacheRead: s.tokensCacheRead / total,
111
+ cacheWrite: s.tokensCacheWrite / total,
112
+ reasoning: s.tokensReasoning / total,
113
+ },
114
+ };
115
+ }
116
+
117
+ export { MONTH_NAMES };