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 CHANGED
@@ -17,60 +17,13 @@ TUI analyzer for local AI coding agent token consumption. Auto-detects
17
17
 
18
18
  Requires **Node.js ≥ 22.5** (for built-in `node:sqlite`).
19
19
 
20
- Pick whichever fits your workflow:
21
-
22
- ### Option 1: `npx` from GitHub (zero install, fastest to try)
23
-
24
20
  ```bash
21
+ # Try it (no install)
25
22
  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
23
 
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
24
+ # Or install globally
48
25
  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
26
  ai-usage --help
72
- # or via npx
73
- npx -y github:adetxt/ai-usage-analyzer --help
74
27
  ```
75
28
 
76
29
  ## Usage
@@ -78,139 +31,48 @@ npx -y github:adetxt/ai-usage-analyzer --help
78
31
  ```bash
79
32
  ai-usage # default TUI
80
33
  ai-usage --top 10 # show top 10 heaviest sessions
81
- ai-usage --json # machine-readable JSON output
82
- ai-usage --markdown # GitHub-flavored Markdown report
83
- ai-usage --md > report.md # same, save to file
84
- ai-usage -h # help (also --help)
34
+ ai-usage --json # machine-readable JSON
35
+ ai-usage --md > report.md # save as markdown
85
36
  ```
86
37
 
87
- ### Environment overrides
38
+ ## Supported tools
88
39
 
89
- If a tool's data lives outside the default location, override its base path:
40
+ | Tool | Default path | Tokens |
41
+ |---|---|---|
42
+ | Claude Code | `~/.claude/projects` | presence only |
43
+ | Codex | `~/.codex/sessions/YYYY/MM/DD/` | yes |
44
+ | OpenCode | `~/.local/share/opencode/opencode.db` | yes (+cost) |
45
+ | MimoCode | `~/.local/share/mimocode/mimocode.db` | yes (+cost) |
46
+ | GitHub Copilot | `~/.copilot/session-state/` | presence only |
47
+ | Antigravity | `~/Library/Application Support/Antigravity` | presence only |
48
+ | Gemini CLI | `~/.gemini/antigravity/conversations` | presence only |
90
49
 
91
- ```bash
92
- export CLAUDE_HOME=/custom/path/to/.claude
93
- export CODEX_HOME=/custom/path/to/.codex
94
- export OPENCODE_HOME=/custom/path/to/.local/share/opencode
95
- export MIMOCODE_HOME=/custom/path/to/.local/share/mimocode
96
- export COPILOT_HOME=/custom/path/to/.copilot
97
- export ANTIGRAVITY_HOME=/custom/path/to/Antigravity
98
- export GEMINI_HOME=/custom/path/to/.gemini
99
- ```
50
+ ## Path overrides
100
51
 
101
- Or pass all at once via JSON:
52
+ Override any tool's base path with an env var (`CLAUDE_HOME`, `CODEX_HOME`,
53
+ `OPENCODE_HOME`, `MIMOCODE_HOME`, `COPILOT_HOME`, `ANTIGRAVITY_HOME`,
54
+ `GEMINI_HOME`), or pass all at once via JSON:
102
55
 
103
56
  ```bash
104
57
  export AI_USAGE_PATHS_JSON='{"codex":"/data/codex","opencode":"/data/oc.db"}'
105
58
  ```
106
59
 
107
- ## Supported tools
108
-
109
- | Tool | Path | Token data | Source format |
110
- |---|---|---|---|
111
- | Claude Code | `~/.claude/transcripts` | presence only | `ses_*.jsonl` |
112
- | Codex | `~/.codex/sessions/YYYY/MM/DD/` | **yes** | `rollout-*.jsonl` `token_count` events |
113
- | OpenCode | `~/.local/share/opencode/opencode.db` | **yes** (+cost) | SQLite |
114
- | MimoCode | `~/.local/share/mimocode/mimocode.db` | **yes** (+cost, when present) | SQLite |
115
- | GitHub Copilot | `~/.copilot/session-state/` | presence only | `events.jsonl` |
116
- | Antigravity | `~/Library/Application Support/Antigravity` | presence only | dir scan |
117
- | Gemini CLI | `~/.gemini/antigravity/conversations` | presence only | `*.pb` protobuf |
118
-
119
60
  ## Token breakdown
120
61
 
121
- For tools that record token data, the analyzer shows the full breakdown:
62
+ For tools that record token data, the analyzer shows input, output, cache
63
+ read, cache write, and reasoning tokens. Cache hits are cheap; reasoning is
64
+ the extended-thinking/chain-of-thought cost.
122
65
 
123
- - **Input** prompt tokens sent to the model
124
- - **Output** — completion tokens generated by the model
125
- - **Cache Read** — prompt tokens served from the provider's cache (cheap)
126
- - **Cache Write** — prompt tokens cached for future use (OpenCode only)
127
- - **Reasoning** — extended thinking / chain-of-thought tokens
128
-
129
- The TUI is adaptive: at terminal widths below 110 columns it drops
130
- non-essential columns; at 110+ it shows the full breakdown with project
131
- paths, per-tool token columns, and longer distribution bars.
132
-
133
- ## Architecture
66
+ ## How it works
134
67
 
135
68
  ```
136
69
  src/
137
- ├── detectors.js auto-path discovery (env → well-known locations)
138
- ├── loaders.js SQLite + JSONL parsers → unified session record
139
- ├── aggregate.js per-project / per-week / per-month grouping
140
- ├── render.js TUI (chalk + cli-table3 + boxen + gradient-string)
70
+ ├── detectors.js auto-path discovery (env → well-known locations)
71
+ ├── loaders.js SQLite + JSONL parsers → unified session record
72
+ ├── aggregate.js per-project / per-week / per-month grouping
73
+ ├── render.js TUI • markdown.js → Markdown report
141
74
  bin/
142
- └── ai-usage.js entry point
143
- ```
144
-
145
- The loader produces a unified record shape:
146
-
147
- ```ts
148
- {
149
- tool, sessionId, project, title, week, month, ts,
150
- tokensInput, tokensOutput, tokensCacheRead, tokensCacheWrite,
151
- tokensReasoning, tokensTotal, cost, model
152
- }
153
- ```
154
-
155
- The aggregator then groups by project / week / month, and the renderer
156
- turns each grouping into a colored table with a distribution bar.
157
-
158
- ## Output modes
159
-
160
- ### TUI (default)
161
-
162
- Color-coded, boxed tables with bar charts. Adapts to terminal width.
163
-
164
- ### JSON (`--json`)
165
-
166
- Single JSON object with detection results, summary, raw sessions, and errors.
167
- Suitable for piping into `jq` or feeding a dashboard.
168
-
169
- ```bash
170
- ai-usage --json | jq '.summary'
171
- # {
172
- # "n": 411,
173
- # "tokensTotal": 1558000000,
174
- # "tokensInput": 420900000,
175
- # "tokensOutput": 4450000,
176
- # "tokensCacheRead": 1130000000,
177
- # "tokensCacheWrite": 49000,
178
- # "tokensReasoning": 2210000,
179
- # "cost": 29.4,
180
- # "avg": 3790000
181
- # }
182
- ```
183
-
184
- ### Markdown (`--markdown` / `--md`)
185
-
186
- GitHub-flavored markdown report with the same sections as the TUI (header,
187
- detected tools, overview, token breakdown, per-project, per-month, per-week,
188
- top sessions, notes). Designed to be pasted into GitHub issues, PRs, or
189
- Notion pages.
190
-
191
- ```bash
192
- ai-usage --md > report.md
193
- cat report.md # or just paste the output into a GitHub comment
194
- ```
195
-
196
- Distribution bars use Unicode block characters (`█░`) so they render in
197
- plain markdown without colors. Sample output:
198
-
199
- ```markdown
200
- # AI Token Usage Report
201
-
202
- **Range**: 2026-04-01 → 2026-06-27
203
- **Sessions**: 411
204
- **Total tokens**: 1.58B
205
- **Cost**: $29.40 (opencode only)
206
-
207
- ## Detected AI Tools
208
-
209
- | Tool | Status | Path | Count | Tokens |
210
- |---|---|---|---:|---|
211
- | Codex | ✅ present | `~/.codex/sessions` | 94 | ✅ |
212
- | OpenCode | ✅ present | `~/.local/share/opencode/opencode.db` | 328 | ✅ |
213
- | ...
75
+ └── ai-usage.js entry point
214
76
  ```
215
77
 
216
78
  ## License
package/bin/ai-usage.js CHANGED
@@ -7,7 +7,8 @@ import { loadAll, dateRange } from '../src/loaders.js';
7
7
  import { overall } from '../src/aggregate.js';
8
8
  import {
9
9
  renderHeader, renderDetections, renderOverview,
10
- renderPerProject, renderPerMonth, renderPerWeek,
10
+ renderPerProject, renderPerTool, renderPerToolPerMonth,
11
+ renderPerMonth, renderPerWeek,
11
12
  renderTopSessions, renderNotes,
12
13
  } from '../src/render.js';
13
14
  import { renderMarkdown } from '../src/markdown.js';
@@ -23,6 +24,12 @@ const topN = (() => {
23
24
  const v = parseInt(args[i + 1], 10);
24
25
  return Number.isFinite(v) && v > 0 ? v : 5;
25
26
  })();
27
+ const yearFilter = (() => {
28
+ const i = args.indexOf('--year');
29
+ if (i < 0) return null;
30
+ const v = parseInt(args[i + 1], 10);
31
+ return Number.isFinite(v) && v > 1970 && v < 3000 ? v : null;
32
+ })();
26
33
 
27
34
  if (showHelp) {
28
35
  console.log(`
@@ -36,9 +43,11 @@ Options:
36
43
  --json Output machine-readable JSON instead of TUI
37
44
  --markdown, --md Output as a Markdown report (GitHub-flavored tables)
38
45
  --top N Show top N heaviest sessions (default: 5)
46
+ --year YYYY Filter records to a single year (e.g. --year 2026)
39
47
 
40
48
  Examples:
41
49
  ai-usage # default TUI
50
+ ai-usage --year 2026 # TUI, only 2026 sessions
42
51
  ai-usage --json | jq .summary # pipe to jq
43
52
  ai-usage --md > report.md # save as markdown
44
53
  ai-usage --top 20 # show top 20 sessions
@@ -49,7 +58,7 @@ Environment overrides (per-tool data path):
49
58
  AI_USAGE_PATHS_JSON='{"codex":"/custom/path",...}'
50
59
 
51
60
  Supported tools:
52
- • Claude Code — ~/.claude/transcripts (presence only)
61
+ • Claude Code — ~/.claude/projects (tokens from per-message usage)
53
62
  • Codex — ~/.codex/sessions (tokens from token_count events)
54
63
  • OpenCode — ~/.local/share/opencode/opencode.db (tokens + cost)
55
64
  • MimoCode — ~/.local/share/mimocode/mimocode.db (tokens + cost)
@@ -68,7 +77,14 @@ if (jsonOut && mdOut) {
68
77
  async function main() {
69
78
  const t0 = Date.now();
70
79
  const detections = detectAll();
71
- const { records, errors } = await loadAll(detections);
80
+ const { records: allRecords, errors } = await loadAll(detections);
81
+
82
+ // Apply --year filter before any aggregation so dateRange and totals
83
+ // reflect the filtered set.
84
+ const records = yearFilter !== null
85
+ ? allRecords.filter(r => r.month && r.month.startsWith(`${yearFilter}-`))
86
+ : allRecords;
87
+
72
88
  const range = dateRange(records);
73
89
  const tot = overall(records);
74
90
 
@@ -78,6 +94,7 @@ async function main() {
78
94
  summary: tot,
79
95
  dateRange: range,
80
96
  sessions: records,
97
+ filter: yearFilter !== null ? { year: yearFilter } : null,
81
98
  errors,
82
99
  generatedAt: new Date().toISOString(),
83
100
  durationMs: Date.now() - t0,
@@ -109,6 +126,8 @@ async function main() {
109
126
  sections.push(
110
127
  renderOverview(records, detections),
111
128
  renderPerProject(records),
129
+ renderPerTool(records),
130
+ renderPerToolPerMonth(records),
112
131
  renderPerMonth(records),
113
132
  renderPerWeek(records),
114
133
  renderTopSessions(records, topN),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-usage-analyzer",
3
- "version": "0.2.0",
3
+ "version": "0.3.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
@@ -2,8 +2,8 @@
2
2
 
3
3
  const MONTH_NAMES = {
4
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',
5
+ '05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
6
+ '09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec',
7
7
  };
8
8
 
9
9
  function sum(arr, key) {
@@ -38,13 +38,23 @@ function summarize(records) {
38
38
  };
39
39
  }
40
40
 
41
+ function toolBreakdown(records) {
42
+ const byTool = {};
43
+ for (const r of records) {
44
+ byTool[r.tool] = (byTool[r.tool] || 0) + r.tokensTotal;
45
+ }
46
+ return byTool;
47
+ }
48
+
41
49
  export function perProject(records) {
42
- const m = groupBy(records, r => `${r.tool}\u0001${r.project}`);
50
+ // One row per project tool mix is shown via the stacked bar / byTool,
51
+ // not as a separate column. Detailed per-tool-per-project breakdown
52
+ // is no longer surfaced here; the "Per Tool per Month" section is the
53
+ // place to see per-tool data over time.
54
+ const m = groupBy(records, r => r.project);
43
55
  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 });
56
+ for (const [project, arr] of m) {
57
+ out.push({ project, ...summarize(arr), byTool: toolBreakdown(arr) });
48
58
  }
49
59
  return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
50
60
  }
@@ -53,11 +63,7 @@ export function perMonth(records) {
53
63
  const m = groupBy(records, r => r.month);
54
64
  const out = [];
55
65
  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 });
66
+ out.push({ month, ...summarize(arr), byTool: toolBreakdown(arr) });
61
67
  }
62
68
  return out.sort((a, b) => a.month.localeCompare(b.month));
63
69
  }
@@ -66,11 +72,7 @@ export function perWeek(records) {
66
72
  const m = groupBy(records, r => r.week);
67
73
  const out = [];
68
74
  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 });
75
+ out.push({ week, ...summarize(arr), byTool: toolBreakdown(arr) });
74
76
  }
75
77
  return out.sort((a, b) => a.week.localeCompare(b.week));
76
78
  }
@@ -84,6 +86,19 @@ export function perTool(records) {
84
86
  return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
85
87
  }
86
88
 
89
+ export function perToolPerMonth(records) {
90
+ // Cross-tab: one row per (tool, month). Lets you see how a single tool's
91
+ // usage is distributed across months — and avoids the hardcoded OC/CX/MM
92
+ // column problem in the per-month table.
93
+ const m = groupBy(records, r => `${r.tool}\u0001${r.month}`);
94
+ const out = [];
95
+ for (const [k, arr] of m) {
96
+ const [tool, month] = k.split('\u0001');
97
+ out.push({ tool, month, ...summarize(arr) });
98
+ }
99
+ return out.sort((a, b) => b.tokensTotal - a.tokensTotal);
100
+ }
101
+
87
102
  export function overall(records) {
88
103
  return summarize(records);
89
104
  }
package/src/detectors.js CHANGED
@@ -1,33 +1,14 @@
1
1
  // Auto-detect AI coding agent data directories.
2
2
  //
3
- // Strategy:
4
- // 1. Honor $AI_USAGE_PATHS_JSON if set (JSON map of { toolKey: "/abs/path" })
5
- // 2. Honor per-tool env var (e.g. $CLAUDE_HOME, $CODEX_HOME, $OPENCODE_HOME)
6
- // 3. Probe well-known locations per platform (mac/linux) relative to $HOME
7
- // 4. Return a status for each tool: 'present' | 'absent' | 'disabled'
8
- //
9
- // Tools that share the opencode SQLite schema are auto-registered.
10
-
11
- import { existsSync, statSync, readdirSync } from 'node:fs';
12
- import { join, isAbsolute } from 'node:path';
13
- import { homedir, platform } from 'node:os';
14
- import { env, exitCode } from 'node:process';
15
- import { createRequire } from 'node:module';
16
- const require = createRequire(import.meta.url);
17
-
18
- const HOME = homedir();
19
- const OS = platform(); // 'darwin' | 'linux' | 'win32'
3
+ // All tool configuration (paths, env vars, kinds, display metadata) lives
4
+ // in src/tools.js. This file only orchestrates: probe paths, apply user
5
+ // overrides, and project each tool's metadata into the detection result.
20
6
 
21
- // macOS Application Support helper
22
- const APP_SUPPORT = OS === 'darwin'
23
- ? join(HOME, 'Library', 'Application Support')
24
- : process.env.XDG_DATA_HOME
25
- ? join(process.env.XDG_DATA_HOME, '..') // XDG_DATA_HOME/../ = ~/.local/share
26
- : join(HOME, '.local', 'share');
7
+ import { existsSync } from 'node:fs';
8
+ import { isAbsolute } from 'node:path';
9
+ import { env } from 'node:process';
10
+ import { TOOLS, TOOL_ORDER } from './tools.js';
27
11
 
28
- const CONFIG_HOME = process.env.XDG_CONFIG_HOME || join(HOME, '.config');
29
-
30
- // Probe = array of candidate paths; first one that exists wins.
31
12
  function firstExisting(paths) {
32
13
  for (const p of paths) {
33
14
  if (p && existsSync(p)) return p;
@@ -35,233 +16,17 @@ function firstExisting(paths) {
35
16
  return null;
36
17
  }
37
18
 
38
- function isFile(p) {
39
- try { return statSync(p).isFile(); } catch { return false; }
40
- }
41
- function isDir(p) {
42
- try { return statSync(p).isDirectory(); } catch { return false; }
19
+ function safeParseJSON(s) {
20
+ try { return JSON.parse(s); } catch { return {}; }
43
21
  }
44
22
 
45
- // ---------------------------------------------------------------------------
46
- // Detector definitions. Each entry returns:
47
- // { key, name, kind, status, path, count, details }
48
- // ---------------------------------------------------------------------------
49
-
50
- const DETECTORS = [
51
- // -----------------------------------------------------------------------
52
- // Claude Code
53
- // -----------------------------------------------------------------------
54
- {
55
- key: 'claude',
56
- name: 'Claude Code',
57
- kind: 'jsonl',
58
- envVar: 'CLAUDE_HOME',
59
- candidatePaths: () => {
60
- const base = env.CLAUDE_HOME || join(HOME, '.claude');
61
- return [
62
- join(base, 'transcripts'),
63
- join(base, 'projects'),
64
- ];
65
- },
66
- count: (p) => {
67
- if (!p) return 0;
68
- let n = 0;
69
- try {
70
- for (const f of readdirSync(p)) {
71
- if (f.startsWith('ses_') && f.endsWith('.jsonl')) n++;
72
- }
73
- } catch {}
74
- return n;
75
- },
76
- hasTokens: false, // transcripts only contain text, no token counts
77
- description: '~/.claude/transcripts/*.jsonl (no token data stored locally)',
78
- },
79
-
80
- // -----------------------------------------------------------------------
81
- // Codex
82
- // -----------------------------------------------------------------------
83
- {
84
- key: 'codex',
85
- name: 'Codex',
86
- kind: 'jsonl-rollout',
87
- envVar: 'CODEX_HOME',
88
- candidatePaths: () => [
89
- env.CODEX_HOME || join(HOME, '.codex', 'sessions'),
90
- ],
91
- count: (p) => {
92
- if (!p) return 0;
93
- let n = 0;
94
- function walk(dir) {
95
- try {
96
- for (const e of readdirSync(dir, { withFileTypes: true })) {
97
- const full = join(dir, e.name);
98
- if (e.isDirectory()) walk(full);
99
- else if (e.isFile() && e.name.startsWith('rollout-') && e.name.endsWith('.jsonl')) n++;
100
- }
101
- } catch {}
102
- }
103
- walk(p);
104
- return n;
105
- },
106
- hasTokens: true,
107
- description: '~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl',
108
- },
109
-
110
- // -----------------------------------------------------------------------
111
- // OpenCode
112
- // -----------------------------------------------------------------------
113
- {
114
- key: 'opencode',
115
- name: 'OpenCode',
116
- kind: 'sqlite',
117
- envVar: 'OPENCODE_HOME',
118
- candidatePaths: () => [
119
- env.OPENCODE_HOME
120
- ? join(env.OPENCODE_HOME, 'opencode.db')
121
- : join(HOME, '.local', 'share', 'opencode', 'opencode.db'),
122
- ],
123
- count: (p) => {
124
- if (!p) return 0;
125
- try {
126
- const { DatabaseSync } = require('node:sqlite');
127
- const db = new DatabaseSync(p, { readOnly: true });
128
- return db.prepare('SELECT COUNT(*) AS c FROM session').get().c;
129
- } catch { return 0; }
130
- },
131
- hasTokens: true,
132
- description: '~/.local/share/opencode/opencode.db (tokens + cost)',
133
- },
134
-
135
- // -----------------------------------------------------------------------
136
- // MimoCode (same schema as OpenCode)
137
- // -----------------------------------------------------------------------
138
- {
139
- key: 'mimocode',
140
- name: 'MimoCode',
141
- kind: 'sqlite',
142
- envVar: 'MIMOCODE_HOME',
143
- candidatePaths: () => [
144
- env.MIMOCODE_HOME
145
- ? join(env.MIMOCODE_HOME, 'mimocode.db')
146
- : join(HOME, '.local', 'share', 'mimocode', 'mimocode.db'),
147
- ],
148
- count: (p) => {
149
- if (!p) return 0;
150
- try {
151
- const { DatabaseSync } = require('node:sqlite');
152
- const db = new DatabaseSync(p, { readOnly: true });
153
- return db.prepare('SELECT COUNT(*) AS c FROM session').get().c;
154
- } catch { return 0; }
155
- },
156
- hasTokens: true,
157
- description: '~/.local/share/mimocode/mimocode.db (tokens + cost)',
158
- },
159
-
160
- // -----------------------------------------------------------------------
161
- // GitHub Copilot CLI
162
- // -----------------------------------------------------------------------
163
- {
164
- key: 'copilot',
165
- name: 'GitHub Copilot',
166
- kind: 'jsonl-events',
167
- envVar: 'COPILOT_HOME',
168
- candidatePaths: () => {
169
- const base = env.COPILOT_HOME || join(HOME, '.copilot');
170
- return [
171
- join(base, 'session-state'),
172
- base,
173
- ];
174
- },
175
- count: (p) => {
176
- if (!p) return 0;
177
- let n = 0;
178
- function walk(dir) {
179
- try {
180
- for (const e of readdirSync(dir, { withFileTypes: true })) {
181
- const full = join(dir, e.name);
182
- if (e.isDirectory()) walk(full);
183
- else if (e.isFile() && e.name.endsWith('.jsonl')) n++;
184
- }
185
- } catch {}
186
- }
187
- walk(p);
188
- return n;
189
- },
190
- hasTokens: false,
191
- description: '~/.copilot/session-state/*/events.jsonl (no token data)',
192
- },
193
-
194
- // -----------------------------------------------------------------------
195
- // Antigravity (VS Code variant) - mostly cache; no token data
196
- // -----------------------------------------------------------------------
197
- {
198
- key: 'antigravity',
199
- name: 'Antigravity',
200
- kind: 'dir',
201
- envVar: 'ANTIGRAVITY_HOME',
202
- candidatePaths: () => {
203
- const base = env.ANTIGRAVITY_HOME || join(APP_SUPPORT, 'Antigravity');
204
- return [
205
- base,
206
- join(HOME, '.antigravity'),
207
- ];
208
- },
209
- count: (p) => {
210
- if (!p) return 0;
211
- let n = 0;
212
- try {
213
- for (const e of readdirSync(p)) {
214
- const full = join(p, e);
215
- if (statSync(full).isDirectory()) n++;
216
- }
217
- } catch {}
218
- return n;
219
- },
220
- hasTokens: false,
221
- description: '~/Library/Application Support/Antigravity (no token data)',
222
- },
223
-
224
- // -----------------------------------------------------------------------
225
- // Gemini CLI
226
- // -----------------------------------------------------------------------
227
- {
228
- key: 'gemini',
229
- name: 'Gemini CLI',
230
- kind: 'protobuf',
231
- envVar: 'GEMINI_HOME',
232
- candidatePaths: () => {
233
- const base = env.GEMINI_HOME || join(HOME, '.gemini');
234
- return [
235
- join(base, 'antigravity', 'conversations'),
236
- join(base, 'conversations'),
237
- ];
238
- },
239
- count: (p) => {
240
- if (!p) return 0;
241
- let n = 0;
242
- try {
243
- for (const f of readdirSync(p)) {
244
- if (f.endsWith('.pb')) n++;
245
- }
246
- } catch {}
247
- return n;
248
- },
249
- hasTokens: false,
250
- description: '~/.gemini/antigravity/conversations/*.pb (binary, no token data)',
251
- },
252
- ];
253
-
254
- // ---------------------------------------------------------------------------
255
- // Public: run all detectors
256
- // ---------------------------------------------------------------------------
257
-
258
23
  export function detectAll(opts = {}) {
259
24
  const override = opts.override || (env.AI_USAGE_PATHS_JSON
260
25
  ? safeParseJSON(env.AI_USAGE_PATHS_JSON)
261
26
  : {});
262
27
 
263
28
  const results = [];
264
- for (const def of DETECTORS) {
29
+ for (const def of TOOLS) {
265
30
  let candidatePaths = def.candidatePaths();
266
31
 
267
32
  // Apply override if user supplied one
@@ -289,12 +54,4 @@ export function detectAll(opts = {}) {
289
54
  return results;
290
55
  }
291
56
 
292
- function safeParseJSON(s) {
293
- try { return JSON.parse(s); } catch { return {}; }
294
- }
295
-
296
- // ---------------------------------------------------------------------------
297
- // Public: just keys in order (for stable UI columns)
298
- // ---------------------------------------------------------------------------
299
-
300
- export const TOOL_ORDER = DETECTORS.map(d => d.key);
57
+ export { TOOL_ORDER };