ai-usage-analyzer 0.2.0 → 0.3.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 +27 -165
- package/bin/ai-usage.js +22 -3
- package/package.json +1 -1
- package/src/aggregate.js +32 -17
- package/src/detectors.js +11 -254
- package/src/loaders.js +112 -1
- package/src/markdown.js +73 -13
- package/src/render.js +154 -98
- package/src/tools.js +259 -0
package/src/tools.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Single source of truth for every AI coding tool this analyzer supports.
|
|
2
|
+
//
|
|
3
|
+
// Each tool entry describes both:
|
|
4
|
+
// - How to find it on disk (kind, envVar, candidatePaths, count)
|
|
5
|
+
// - How to render it in the report (name, label, color, barChar)
|
|
6
|
+
//
|
|
7
|
+
// To add a new tool, append one entry to TOOLS. If the tool exposes token
|
|
8
|
+
// data, also wire a loader branch in src/loaders.js.
|
|
9
|
+
|
|
10
|
+
import { statSync, readdirSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir, platform } from 'node:os';
|
|
13
|
+
import { env } from 'node:process';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
const HOME = homedir();
|
|
18
|
+
const OS = platform(); // 'darwin' | 'linux' | 'win32'
|
|
19
|
+
|
|
20
|
+
const APP_SUPPORT = OS === 'darwin'
|
|
21
|
+
? join(HOME, 'Library', 'Application Support')
|
|
22
|
+
: env.XDG_DATA_HOME
|
|
23
|
+
? join(env.XDG_DATA_HOME, '..') // XDG_DATA_HOME/../ = ~/.local/share
|
|
24
|
+
: join(HOME, '.local', 'share');
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Reusable count helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function countJsonlRecursive(dir) {
|
|
31
|
+
if (!dir) return 0;
|
|
32
|
+
let n = 0;
|
|
33
|
+
function walk(d) {
|
|
34
|
+
try {
|
|
35
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
36
|
+
const full = join(d, e.name);
|
|
37
|
+
if (e.isDirectory()) walk(full);
|
|
38
|
+
else if (e.isFile() && e.name.endsWith('.jsonl')) n++;
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
walk(dir);
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function countSqliteRows(dbPath) {
|
|
47
|
+
if (!dbPath) return 0;
|
|
48
|
+
try {
|
|
49
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
50
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
51
|
+
return db.prepare('SELECT COUNT(*) AS c FROM session').get().c;
|
|
52
|
+
} catch { return 0; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function countSubdirs(dir) {
|
|
56
|
+
if (!dir) return 0;
|
|
57
|
+
let n = 0;
|
|
58
|
+
try {
|
|
59
|
+
for (const e of readdirSync(dir)) {
|
|
60
|
+
const full = join(dir, e);
|
|
61
|
+
if (statSync(full).isDirectory()) n++;
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
return n;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function countProtobuf(dir) {
|
|
68
|
+
if (!dir) return 0;
|
|
69
|
+
let n = 0;
|
|
70
|
+
try {
|
|
71
|
+
for (const f of readdirSync(dir)) {
|
|
72
|
+
if (f.endsWith('.pb')) n++;
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Tool definitions
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export const TOOLS = [
|
|
83
|
+
// -----------------------------------------------------------------------
|
|
84
|
+
// Claude Code
|
|
85
|
+
// -----------------------------------------------------------------------
|
|
86
|
+
{
|
|
87
|
+
key: 'claude',
|
|
88
|
+
name: 'Claude Code',
|
|
89
|
+
label: 'claude',
|
|
90
|
+
color: '#ff9e64', // orange
|
|
91
|
+
barChar: '█',
|
|
92
|
+
kind: 'jsonl',
|
|
93
|
+
envVar: 'CLAUDE_HOME',
|
|
94
|
+
candidatePaths: () => [
|
|
95
|
+
env.CLAUDE_HOME
|
|
96
|
+
? join(env.CLAUDE_HOME, 'projects')
|
|
97
|
+
: join(HOME, '.claude', 'projects'),
|
|
98
|
+
],
|
|
99
|
+
count: countJsonlRecursive,
|
|
100
|
+
hasTokens: true,
|
|
101
|
+
description: '~/.claude/projects/*/<UUID>.jsonl (per-message usage in assistant lines)',
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// -----------------------------------------------------------------------
|
|
105
|
+
// Codex
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
{
|
|
108
|
+
key: 'codex',
|
|
109
|
+
name: 'Codex',
|
|
110
|
+
label: 'codex',
|
|
111
|
+
color: '#87ceeb', // sky blue
|
|
112
|
+
barChar: '▓',
|
|
113
|
+
kind: 'jsonl-rollout',
|
|
114
|
+
envVar: 'CODEX_HOME',
|
|
115
|
+
candidatePaths: () => [
|
|
116
|
+
env.CODEX_HOME || join(HOME, '.codex', 'sessions'),
|
|
117
|
+
],
|
|
118
|
+
count: countJsonlRecursive,
|
|
119
|
+
hasTokens: true,
|
|
120
|
+
description: '~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl',
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
// OpenCode
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
{
|
|
127
|
+
key: 'opencode',
|
|
128
|
+
name: 'OpenCode',
|
|
129
|
+
label: 'opencode',
|
|
130
|
+
color: '#f8f8f2', // white
|
|
131
|
+
barChar: '▒',
|
|
132
|
+
kind: 'sqlite',
|
|
133
|
+
envVar: 'OPENCODE_HOME',
|
|
134
|
+
candidatePaths: () => [
|
|
135
|
+
env.OPENCODE_HOME
|
|
136
|
+
? join(env.OPENCODE_HOME, 'opencode.db')
|
|
137
|
+
: join(HOME, '.local', 'share', 'opencode', 'opencode.db'),
|
|
138
|
+
],
|
|
139
|
+
count: countSqliteRows,
|
|
140
|
+
hasTokens: true,
|
|
141
|
+
description: '~/.local/share/opencode/opencode.db (tokens + cost)',
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// -----------------------------------------------------------------------
|
|
145
|
+
// MimoCode (same schema as OpenCode)
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
{
|
|
148
|
+
key: 'mimocode',
|
|
149
|
+
name: 'MimoCode',
|
|
150
|
+
label: 'mimocode',
|
|
151
|
+
color: '#f1fa8c', // yellow
|
|
152
|
+
barChar: '░',
|
|
153
|
+
kind: 'sqlite',
|
|
154
|
+
envVar: 'MIMOCODE_HOME',
|
|
155
|
+
candidatePaths: () => [
|
|
156
|
+
env.MIMOCODE_HOME
|
|
157
|
+
? join(env.MIMOCODE_HOME, 'mimocode.db')
|
|
158
|
+
: join(HOME, '.local', 'share', 'mimocode', 'mimocode.db'),
|
|
159
|
+
],
|
|
160
|
+
count: countSqliteRows,
|
|
161
|
+
hasTokens: true,
|
|
162
|
+
description: '~/.local/share/mimocode/mimocode.db (tokens + cost)',
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// -----------------------------------------------------------------------
|
|
166
|
+
// GitHub Copilot CLI (presence only)
|
|
167
|
+
// -----------------------------------------------------------------------
|
|
168
|
+
{
|
|
169
|
+
key: 'copilot',
|
|
170
|
+
name: 'GitHub Copilot',
|
|
171
|
+
label: 'copilot',
|
|
172
|
+
color: '#3b82f6', // deep blue
|
|
173
|
+
barChar: '·',
|
|
174
|
+
kind: 'jsonl-events',
|
|
175
|
+
envVar: 'COPILOT_HOME',
|
|
176
|
+
candidatePaths: () => {
|
|
177
|
+
const base = env.COPILOT_HOME || join(HOME, '.copilot');
|
|
178
|
+
return [
|
|
179
|
+
join(base, 'session-state'),
|
|
180
|
+
base,
|
|
181
|
+
];
|
|
182
|
+
},
|
|
183
|
+
count: countJsonlRecursive,
|
|
184
|
+
hasTokens: false,
|
|
185
|
+
description: '~/.copilot/session-state/*/events.jsonl (no token data)',
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
// -----------------------------------------------------------------------
|
|
189
|
+
// Antigravity (VS Code variant) - mostly cache; no token data
|
|
190
|
+
// -----------------------------------------------------------------------
|
|
191
|
+
{
|
|
192
|
+
key: 'antigravity',
|
|
193
|
+
name: 'Antigravity',
|
|
194
|
+
label: 'antigravity',
|
|
195
|
+
color: '#ff5555', // red
|
|
196
|
+
barChar: '·',
|
|
197
|
+
kind: 'dir',
|
|
198
|
+
envVar: 'ANTIGRAVITY_HOME',
|
|
199
|
+
candidatePaths: () => [
|
|
200
|
+
env.ANTIGRAVITY_HOME || APP_SUPPORT + '/Antigravity',
|
|
201
|
+
join(HOME, '.antigravity'),
|
|
202
|
+
],
|
|
203
|
+
count: countSubdirs,
|
|
204
|
+
hasTokens: false,
|
|
205
|
+
description: '~/Library/Application Support/Antigravity (no token data)',
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// -----------------------------------------------------------------------
|
|
209
|
+
// Gemini CLI
|
|
210
|
+
// -----------------------------------------------------------------------
|
|
211
|
+
{
|
|
212
|
+
key: 'gemini',
|
|
213
|
+
name: 'Gemini CLI',
|
|
214
|
+
label: 'gemini',
|
|
215
|
+
color: '#14b8a6', // teal
|
|
216
|
+
barChar: '·',
|
|
217
|
+
kind: 'protobuf',
|
|
218
|
+
envVar: 'GEMINI_HOME',
|
|
219
|
+
candidatePaths: () => {
|
|
220
|
+
const base = env.GEMINI_HOME || join(HOME, '.gemini');
|
|
221
|
+
return [
|
|
222
|
+
join(base, 'antigravity', 'conversations'),
|
|
223
|
+
join(base, 'conversations'),
|
|
224
|
+
];
|
|
225
|
+
},
|
|
226
|
+
count: countProtobuf,
|
|
227
|
+
hasTokens: false,
|
|
228
|
+
description: '~/.gemini/antigravity/conversations/*.pb (binary, no token data)',
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Lookup helpers — consumers should use these instead of indexing TOOLS
|
|
234
|
+
// directly so the null-handling stays in one place.
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
const TOOL_BY_KEY = new Map(TOOLS.map(t => [t.key, t]));
|
|
238
|
+
const FALLBACK_COLOR = '#ffffff';
|
|
239
|
+
const FALLBACK_BAR_CHAR = '·';
|
|
240
|
+
|
|
241
|
+
export function getTool(key) {
|
|
242
|
+
return TOOL_BY_KEY.get(key) || null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function getToolColor(key) {
|
|
246
|
+
return TOOL_BY_KEY.get(key)?.color ?? FALLBACK_COLOR;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function getToolBarChar(key) {
|
|
250
|
+
return TOOL_BY_KEY.get(key)?.barChar ?? FALLBACK_BAR_CHAR;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function getToolLabel(key) {
|
|
254
|
+
return TOOL_BY_KEY.get(key)?.label ?? key;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Stable UI column order — derived from TOOLS so adding a tool only
|
|
258
|
+
// requires one edit in this file.
|
|
259
|
+
export const TOOL_ORDER = TOOLS.map(t => t.key);
|