ai-usage-analyzer 0.1.0 → 0.2.1
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 +29 -133
- package/bin/ai-usage.js +32 -7
- package/package.json +1 -1
- package/src/detectors.js +16 -13
- package/src/markdown.js +257 -0
package/README.md
CHANGED
|
@@ -17,166 +17,62 @@ 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
23
|
|
|
30
|
-
|
|
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
|
|
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
|
|
77
30
|
|
|
78
31
|
```bash
|
|
79
|
-
ai-usage
|
|
80
|
-
ai-usage --top 10
|
|
81
|
-
ai-usage --json
|
|
82
|
-
ai-usage --
|
|
32
|
+
ai-usage # default TUI
|
|
33
|
+
ai-usage --top 10 # show top 10 heaviest sessions
|
|
34
|
+
ai-usage --json # machine-readable JSON
|
|
35
|
+
ai-usage --md > report.md # save as markdown
|
|
83
36
|
```
|
|
84
37
|
|
|
85
|
-
|
|
38
|
+
## Supported tools
|
|
86
39
|
|
|
87
|
-
|
|
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 |
|
|
88
49
|
|
|
89
|
-
|
|
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
|
-
```
|
|
50
|
+
## Path overrides
|
|
98
51
|
|
|
99
|
-
|
|
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:
|
|
100
55
|
|
|
101
56
|
```bash
|
|
102
57
|
export AI_USAGE_PATHS_JSON='{"codex":"/data/codex","opencode":"/data/oc.db"}'
|
|
103
58
|
```
|
|
104
59
|
|
|
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
60
|
## Token breakdown
|
|
118
61
|
|
|
119
|
-
For tools that record token data, the analyzer shows
|
|
120
|
-
|
|
121
|
-
-
|
|
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.
|
|
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.
|
|
130
65
|
|
|
131
|
-
##
|
|
66
|
+
## How it works
|
|
132
67
|
|
|
133
68
|
```
|
|
134
69
|
src/
|
|
135
|
-
├── detectors.js
|
|
136
|
-
├── loaders.js
|
|
137
|
-
├── aggregate.js
|
|
138
|
-
├── 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
|
|
139
74
|
bin/
|
|
140
|
-
└── ai-usage.js
|
|
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
|
-
# }
|
|
75
|
+
└── ai-usage.js entry point
|
|
180
76
|
```
|
|
181
77
|
|
|
182
78
|
## License
|
package/bin/ai-usage.js
CHANGED
|
@@ -10,14 +10,18 @@ import {
|
|
|
10
10
|
renderPerProject, renderPerMonth, renderPerWeek,
|
|
11
11
|
renderTopSessions, renderNotes,
|
|
12
12
|
} from '../src/render.js';
|
|
13
|
+
import { renderMarkdown } from '../src/markdown.js';
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2);
|
|
15
|
-
const
|
|
16
|
-
const showHelp =
|
|
17
|
-
const jsonOut =
|
|
16
|
+
const hasFlag = (name) => args.includes(`--${name}`) || args.includes(`-${name}`);
|
|
17
|
+
const showHelp = hasFlag('help') || hasFlag('h');
|
|
18
|
+
const jsonOut = hasFlag('json');
|
|
19
|
+
const mdOut = hasFlag('markdown') || hasFlag('md');
|
|
18
20
|
const topN = (() => {
|
|
19
21
|
const i = args.indexOf('--top');
|
|
20
|
-
|
|
22
|
+
if (i < 0) return 5;
|
|
23
|
+
const v = parseInt(args[i + 1], 10);
|
|
24
|
+
return Number.isFinite(v) && v > 0 ? v : 5;
|
|
21
25
|
})();
|
|
22
26
|
|
|
23
27
|
if (showHelp) {
|
|
@@ -28,9 +32,16 @@ Usage:
|
|
|
28
32
|
ai-usage [options]
|
|
29
33
|
|
|
30
34
|
Options:
|
|
31
|
-
--top N Show top N heaviest sessions (default: 5)
|
|
32
|
-
--json Output machine-readable JSON instead of TUI
|
|
33
35
|
-h, --help Show this help
|
|
36
|
+
--json Output machine-readable JSON instead of TUI
|
|
37
|
+
--markdown, --md Output as a Markdown report (GitHub-flavored tables)
|
|
38
|
+
--top N Show top N heaviest sessions (default: 5)
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
ai-usage # default TUI
|
|
42
|
+
ai-usage --json | jq .summary # pipe to jq
|
|
43
|
+
ai-usage --md > report.md # save as markdown
|
|
44
|
+
ai-usage --top 20 # show top 20 sessions
|
|
34
45
|
|
|
35
46
|
Environment overrides (per-tool data path):
|
|
36
47
|
CLAUDE_HOME, CODEX_HOME, OPENCODE_HOME, MIMOCODE_HOME,
|
|
@@ -38,7 +49,7 @@ Environment overrides (per-tool data path):
|
|
|
38
49
|
AI_USAGE_PATHS_JSON='{"codex":"/custom/path",...}'
|
|
39
50
|
|
|
40
51
|
Supported tools:
|
|
41
|
-
• Claude Code — ~/.claude/
|
|
52
|
+
• Claude Code — ~/.claude/projects (presence only)
|
|
42
53
|
• Codex — ~/.codex/sessions (tokens from token_count events)
|
|
43
54
|
• OpenCode — ~/.local/share/opencode/opencode.db (tokens + cost)
|
|
44
55
|
• MimoCode — ~/.local/share/mimocode/mimocode.db (tokens + cost)
|
|
@@ -49,6 +60,11 @@ Supported tools:
|
|
|
49
60
|
process.exit(0);
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
if (jsonOut && mdOut) {
|
|
64
|
+
console.error('Error: --json and --markdown are mutually exclusive.');
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
async function main() {
|
|
53
69
|
const t0 = Date.now();
|
|
54
70
|
const detections = detectAll();
|
|
@@ -70,6 +86,15 @@ async function main() {
|
|
|
70
86
|
return;
|
|
71
87
|
}
|
|
72
88
|
|
|
89
|
+
if (mdOut) {
|
|
90
|
+
const out = renderMarkdown({
|
|
91
|
+
records, detections, errors,
|
|
92
|
+
dateRange: range, topN, generatedAt: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
process.stdout.write(out);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
73
98
|
// TUI render
|
|
74
99
|
const sections = [
|
|
75
100
|
renderHeader({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-usage-analyzer",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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/detectors.js
CHANGED
|
@@ -56,25 +56,28 @@ const DETECTORS = [
|
|
|
56
56
|
name: 'Claude Code',
|
|
57
57
|
kind: 'jsonl',
|
|
58
58
|
envVar: 'CLAUDE_HOME',
|
|
59
|
-
candidatePaths: () =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
join(
|
|
63
|
-
|
|
64
|
-
];
|
|
65
|
-
},
|
|
59
|
+
candidatePaths: () => [
|
|
60
|
+
env.CLAUDE_HOME
|
|
61
|
+
? join(env.CLAUDE_HOME, 'projects')
|
|
62
|
+
: join(HOME, '.claude', 'projects'),
|
|
63
|
+
],
|
|
66
64
|
count: (p) => {
|
|
67
65
|
if (!p) return 0;
|
|
68
66
|
let n = 0;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
function walk(dir) {
|
|
68
|
+
try {
|
|
69
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
70
|
+
const full = join(dir, e.name);
|
|
71
|
+
if (e.isDirectory()) walk(full);
|
|
72
|
+
else if (e.isFile() && e.name.endsWith('.jsonl')) n++;
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
walk(p);
|
|
74
77
|
return n;
|
|
75
78
|
},
|
|
76
79
|
hasTokens: false, // transcripts only contain text, no token counts
|
|
77
|
-
description: '~/.claude/
|
|
80
|
+
description: '~/.claude/projects/*/<UUID>.jsonl (no token data stored locally)',
|
|
78
81
|
},
|
|
79
82
|
|
|
80
83
|
// -----------------------------------------------------------------------
|
package/src/markdown.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Markdown report renderer for AI token usage analysis.
|
|
2
|
+
// Produces a self-contained GitHub-flavored markdown document that
|
|
3
|
+
// copies cleanly into issues, PRs, Notion, etc.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
perProject, perMonth, perWeek, perTool, overall, topSessions, tokenBreakdown,
|
|
7
|
+
MONTH_NAMES,
|
|
8
|
+
} from './aggregate.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Number formatting helpers (re-implemented locally to avoid TUI deps)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export function fmtInt(n) {
|
|
15
|
+
return Number(n || 0).toLocaleString('en-US');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function fmtCompact(n) {
|
|
19
|
+
const a = Math.abs(n);
|
|
20
|
+
if (a >= 1e9) return (n / 1e9).toFixed(2) + 'B';
|
|
21
|
+
if (a >= 1e6) return (n / 1e6).toFixed(2) + 'M';
|
|
22
|
+
if (a >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
23
|
+
return String(n || 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmtCost(n) {
|
|
27
|
+
if (!n) return '—';
|
|
28
|
+
return '$' + Number(n).toFixed(2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compactHome(p) {
|
|
32
|
+
if (!p) return '—';
|
|
33
|
+
const home = process.env.HOME || '';
|
|
34
|
+
return p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function bar(value, max, width) {
|
|
38
|
+
if (!max || max <= 0) return '░'.repeat(width);
|
|
39
|
+
const pct = Math.max(0, Math.min(1, value / max));
|
|
40
|
+
const filled = Math.round(pct * width);
|
|
41
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pct(x) { return (x * 100).toFixed(1) + '%'; }
|
|
45
|
+
|
|
46
|
+
function mdEscape(s) {
|
|
47
|
+
if (s == null) return '';
|
|
48
|
+
return String(s).replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shortModel(m) {
|
|
52
|
+
if (!m) return '—';
|
|
53
|
+
if (typeof m === 'string' && m.trim().startsWith('{')) {
|
|
54
|
+
try {
|
|
55
|
+
const d = JSON.parse(m);
|
|
56
|
+
const id = d.id || d.model || m;
|
|
57
|
+
const prov = d.providerId || d.provider;
|
|
58
|
+
return id + (prov ? ` (${prov})` : '');
|
|
59
|
+
} catch { return m.slice(0, 30); }
|
|
60
|
+
}
|
|
61
|
+
return m;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function today() {
|
|
65
|
+
return new Date().toISOString().split('T')[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Public: renderMarkdown
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export function renderMarkdown({
|
|
73
|
+
records = [], detections = [], errors = [],
|
|
74
|
+
dateRange = [null, null], topN = 5, generatedAt = null,
|
|
75
|
+
}) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const tot = overall(records);
|
|
78
|
+
const breakdown = tokenBreakdown(tot);
|
|
79
|
+
const tools = perTool(records);
|
|
80
|
+
const hasData = records.length > 0;
|
|
81
|
+
|
|
82
|
+
// Header --------------------------------------------------------------
|
|
83
|
+
out.push(`# AI Token Usage Report`);
|
|
84
|
+
out.push(``);
|
|
85
|
+
out.push(`> Generated by [\`ai-usage-analyzer\`](https://github.com/adetxt/ai-usage-analyzer) on ${generatedAt ? generatedAt.split('T')[0] : today()}`);
|
|
86
|
+
out.push(``);
|
|
87
|
+
|
|
88
|
+
if (hasData) {
|
|
89
|
+
out.push(`**Range**: ${dateRange[0] ?? '—'} → ${dateRange[1] ?? '—'} `);
|
|
90
|
+
out.push(`**Sessions**: ${fmtInt(records.length)} `);
|
|
91
|
+
out.push(`**Total tokens**: ${fmtCompact(tot.tokensTotal)} `);
|
|
92
|
+
if (tot.cost > 0) {
|
|
93
|
+
out.push(`**Cost**: ${fmtCost(tot.cost)} (opencode only) `);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
out.push(`**No session data available** — only detection status below.`);
|
|
97
|
+
}
|
|
98
|
+
out.push(``);
|
|
99
|
+
|
|
100
|
+
// Detected tools ------------------------------------------------------
|
|
101
|
+
out.push(`## Detected AI Tools`);
|
|
102
|
+
out.push(``);
|
|
103
|
+
out.push(`| Tool | Status | Path | Count | Tokens |`);
|
|
104
|
+
out.push(`|---|---|---|---:|---|`);
|
|
105
|
+
for (const d of detections) {
|
|
106
|
+
const status = d.status === 'present' ? '✅ present' : '❌ absent';
|
|
107
|
+
const path = d.path ? '`' + mdEscape(compactHome(d.path)) + '`' : '—';
|
|
108
|
+
const count = d.count ? fmtInt(d.count) : '—';
|
|
109
|
+
const tok = d.hasTokens
|
|
110
|
+
? (d.count ? '✅' : '—')
|
|
111
|
+
: 'n/a';
|
|
112
|
+
out.push(`| ${mdEscape(d.name)} | ${status} | ${path} | ${count} | ${tok} |`);
|
|
113
|
+
}
|
|
114
|
+
out.push(``);
|
|
115
|
+
|
|
116
|
+
if (!hasData) {
|
|
117
|
+
if (errors && errors.length > 0) {
|
|
118
|
+
out.push(`## Errors`);
|
|
119
|
+
out.push(``);
|
|
120
|
+
for (const e of errors) out.push(`- ${mdEscape(e)}`);
|
|
121
|
+
out.push(``);
|
|
122
|
+
}
|
|
123
|
+
return out.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Overview ------------------------------------------------------------
|
|
127
|
+
out.push(`## Overview`);
|
|
128
|
+
out.push(``);
|
|
129
|
+
out.push(`### Per-tool Summary`);
|
|
130
|
+
out.push(``);
|
|
131
|
+
out.push(`| Tool | Sessions | Total | Avg/sess | Cost |`);
|
|
132
|
+
out.push(`|---|---:|---:|---:|---:|`);
|
|
133
|
+
for (const t of tools) {
|
|
134
|
+
out.push(`| ${mdEscape(t.tool)} | ${fmtInt(t.n)} | ${fmtCompact(t.tokensTotal)} | ${fmtCompact(t.avg)} | ${fmtCost(t.cost)} |`);
|
|
135
|
+
}
|
|
136
|
+
out.push(`| **TOTAL** | **${fmtInt(records.length)}** | **${fmtCompact(tot.tokensTotal)}** | **${fmtCompact(tot.avg)}** | **${fmtCost(tot.cost)}** |`);
|
|
137
|
+
out.push(``);
|
|
138
|
+
|
|
139
|
+
out.push(`### Token Breakdown`);
|
|
140
|
+
out.push(``);
|
|
141
|
+
out.push(`| Type | Tokens | Share |`);
|
|
142
|
+
out.push(`|---|---:|---:|`);
|
|
143
|
+
out.push(`| Input | ${fmtCompact(breakdown.input)} | ${pct(breakdown.ratios.input)} |`);
|
|
144
|
+
out.push(`| Output | ${fmtCompact(breakdown.output)} | ${pct(breakdown.ratios.output)} |`);
|
|
145
|
+
out.push(`| Cache Read | ${fmtCompact(breakdown.cacheRead)} | ${pct(breakdown.ratios.cacheRead)} |`);
|
|
146
|
+
out.push(`| Cache Write | ${fmtCompact(breakdown.cacheWrite)} | ${pct(breakdown.ratios.cacheWrite)} |`);
|
|
147
|
+
out.push(`| Reasoning | ${fmtCompact(breakdown.reasoning)} | ${pct(breakdown.ratios.reasoning)} |`);
|
|
148
|
+
out.push(`| **Total** | **${fmtCompact(breakdown.total)}** | **100.0%** |`);
|
|
149
|
+
out.push(``);
|
|
150
|
+
|
|
151
|
+
// Per Project ---------------------------------------------------------
|
|
152
|
+
const projects = perProject(records);
|
|
153
|
+
if (projects.length > 0) {
|
|
154
|
+
const max = projects[0].tokensTotal;
|
|
155
|
+
out.push(`## Per Project`);
|
|
156
|
+
out.push(``);
|
|
157
|
+
out.push(`| Tool | Project | Sessions | Input | Output | Cache | Total | Dist |`);
|
|
158
|
+
out.push(`|---|---|---:|---:|---:|---:|---:|---|`);
|
|
159
|
+
for (const p of projects) {
|
|
160
|
+
const cache = p.tokensCacheRead + p.tokensCacheWrite;
|
|
161
|
+
const proj = compactHome(p.project);
|
|
162
|
+
out.push(`| ${mdEscape(p.tool)} | \`${mdEscape(proj)}\` | ${fmtInt(p.n)} | ${fmtCompact(p.tokensInput)} | ${fmtCompact(p.tokensOutput)} | ${fmtCompact(cache)} | ${fmtCompact(p.tokensTotal)} | ${bar(p.tokensTotal, max, 20)} |`);
|
|
163
|
+
}
|
|
164
|
+
out.push(``);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Per Month -----------------------------------------------------------
|
|
168
|
+
const months = perMonth(records);
|
|
169
|
+
if (months.length > 0) {
|
|
170
|
+
const max = Math.max(...months.map(m => m.tokensTotal));
|
|
171
|
+
out.push(`## Per Month`);
|
|
172
|
+
out.push(``);
|
|
173
|
+
out.push(`| Month | Sessions | Input | Output | Total | OC | CX | MM | Dist |`);
|
|
174
|
+
out.push(`|---|---:|---:|---:|---:|---:|---:|---:|---|`);
|
|
175
|
+
for (const m of months) {
|
|
176
|
+
const yyyy = m.month.slice(0, 4);
|
|
177
|
+
const mm = m.month.slice(5, 7);
|
|
178
|
+
const label = `${MONTH_NAMES[mm] || mm} ${yyyy}`;
|
|
179
|
+
out.push(`| ${label} | ${fmtInt(m.n)} | ${fmtCompact(m.tokensInput)} | ${fmtCompact(m.tokensOutput)} | ${fmtCompact(m.tokensTotal)} | ${m.byTool.opencode ? fmtCompact(m.byTool.opencode) : '—'} | ${m.byTool.codex ? fmtCompact(m.byTool.codex) : '—'} | ${m.byTool.mimocode ? fmtCompact(m.byTool.mimocode) : '—'} | ${bar(m.tokensTotal, max, 20)} |`);
|
|
180
|
+
}
|
|
181
|
+
out.push(``);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Per Week ------------------------------------------------------------
|
|
185
|
+
const weeks = perWeek(records);
|
|
186
|
+
if (weeks.length > 0) {
|
|
187
|
+
const max = Math.max(...weeks.map(w => w.tokensTotal));
|
|
188
|
+
out.push(`## Per Week`);
|
|
189
|
+
out.push(``);
|
|
190
|
+
out.push(`| Week | Sessions | Input | Output | Total | OC | CX | MM | Dist |`);
|
|
191
|
+
out.push(`|---|---:|---:|---:|---:|---:|---:|---:|---|`);
|
|
192
|
+
for (const w of weeks) {
|
|
193
|
+
out.push(`| ${w.week} | ${fmtInt(w.n)} | ${fmtCompact(w.tokensInput)} | ${fmtCompact(w.tokensOutput)} | ${fmtCompact(w.tokensTotal)} | ${w.byTool.opencode ? fmtCompact(w.byTool.opencode) : '—'} | ${w.byTool.codex ? fmtCompact(w.byTool.codex) : '—'} | ${w.byTool.mimocode ? fmtCompact(w.byTool.mimocode) : '—'} | ${bar(w.tokensTotal, max, 18)} |`);
|
|
194
|
+
}
|
|
195
|
+
out.push(``);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Top N ---------------------------------------------------------------
|
|
199
|
+
const top = topSessions(records, topN);
|
|
200
|
+
if (top.length > 0) {
|
|
201
|
+
out.push(`## Top ${top.length} Heaviest Sessions`);
|
|
202
|
+
out.push(``);
|
|
203
|
+
out.push(`| # | Total | Input | Output | Cost | Model | Project | Title |`);
|
|
204
|
+
out.push(`|---:|---:|---:|---:|---:|---|---|---|`);
|
|
205
|
+
top.forEach((r, i) => {
|
|
206
|
+
const proj = compactHome(r.project);
|
|
207
|
+
out.push(`| ${i + 1} | ${fmtCompact(r.tokensTotal)} | ${fmtCompact(r.tokensInput)} | ${fmtCompact(r.tokensOutput)} | ${r.cost ? '$' + r.cost.toFixed(4) : '—'} | ${mdEscape(shortModel(r.model))} | \`${mdEscape(proj)}\` | ${mdEscape(r.title || '—')} |`);
|
|
208
|
+
});
|
|
209
|
+
out.push(``);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Notes ---------------------------------------------------------------
|
|
213
|
+
out.push(`## Notes`);
|
|
214
|
+
out.push(``);
|
|
215
|
+
out.push(`### Token Definitions`);
|
|
216
|
+
out.push(``);
|
|
217
|
+
out.push(`- **Input** — prompt tokens sent to the model`);
|
|
218
|
+
out.push(`- **Output** — completion tokens generated by the model`);
|
|
219
|
+
out.push(`- **Cache Read** — prompt tokens served from the provider's cache (cheap)`);
|
|
220
|
+
out.push(`- **Cache Write** — prompt tokens cached for future use (OpenCode only)`);
|
|
221
|
+
out.push(`- **Reasoning** — extended thinking / chain-of-thought tokens`);
|
|
222
|
+
out.push(``);
|
|
223
|
+
|
|
224
|
+
const withTokens = detections.filter(d => d.hasTokens && d.status === 'present');
|
|
225
|
+
if (withTokens.length > 0) {
|
|
226
|
+
out.push(`### Detected Tools With Token Data`);
|
|
227
|
+
out.push(``);
|
|
228
|
+
for (const d of withTokens) {
|
|
229
|
+
out.push(`- **${mdEscape(d.name)}** — \`${mdEscape(d.description)}\``);
|
|
230
|
+
}
|
|
231
|
+
out.push(``);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const noTokens = detections.filter(d => !d.hasTokens && d.status === 'present');
|
|
235
|
+
if (noTokens.length > 0) {
|
|
236
|
+
out.push(`### Detected Tools Without Token Data`);
|
|
237
|
+
out.push(``);
|
|
238
|
+
for (const d of noTokens) {
|
|
239
|
+
out.push(`- **${mdEscape(d.name)}** — \`${mdEscape(d.description)}\``);
|
|
240
|
+
}
|
|
241
|
+
out.push(``);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (errors && errors.length > 0) {
|
|
245
|
+
out.push(`### Errors`);
|
|
246
|
+
out.push(``);
|
|
247
|
+
for (const e of errors.slice(0, 10)) {
|
|
248
|
+
out.push(`- ${mdEscape(e)}`);
|
|
249
|
+
}
|
|
250
|
+
if (errors.length > 10) {
|
|
251
|
+
out.push(`- … and ${errors.length - 10} more`);
|
|
252
|
+
}
|
|
253
|
+
out.push(``);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return out.join('\n');
|
|
257
|
+
}
|