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/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
|
-
|
|
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
|
|
82
|
-
ai-usage --
|
|
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
|
-
|
|
38
|
+
## Supported tools
|
|
88
39
|
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
138
|
-
├── loaders.js
|
|
139
|
-
├── aggregate.js
|
|
140
|
-
├── render.js
|
|
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
|
|
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,
|
|
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/
|
|
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.
|
|
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': '
|
|
6
|
-
'09': 'Sep', '10': '
|
|
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
|
-
|
|
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 [
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
39
|
-
try { return
|
|
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
|
|
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
|
-
|
|
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 };
|