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/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);