cc-model 1.0.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 +84 -0
- package/cli.mjs +221 -0
- package/index.html +314 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# cc-model
|
|
2
|
+
|
|
3
|
+
Which Claude AI models are powering your sessions?
|
|
4
|
+
|
|
5
|
+
Shows the distribution of Opus, Sonnet, and Haiku usage across your Claude Code history — and how that's changed over time.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx cc-model
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
cc-model — Which Claude models power your sessions
|
|
15
|
+
|
|
16
|
+
Opus 4.5 ████████████████████████████ 249 ( 53%)
|
|
17
|
+
Opus 4.6 █████████████████░░░░░░░░░░░ 147 ( 31%)
|
|
18
|
+
Sonnet 4.6 █████░░░░░░░░░░░░░░░░░░░░░░░ 46 ( 10%)
|
|
19
|
+
Haiku 4.5 ██░░░░░░░░░░░░░░░░░░░░░░░░░░ 17 ( 4%)
|
|
20
|
+
Sonnet 4.5 █░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8 ( 2%)
|
|
21
|
+
|
|
22
|
+
───────────────────────────────────────────────────────
|
|
23
|
+
Total sessions: 467
|
|
24
|
+
Current model: Opus 4.6
|
|
25
|
+
Primary model: Opus 4.5 (249 sessions)
|
|
26
|
+
|
|
27
|
+
Run with --timeline to see week-by-week model history
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Timeline view
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx cc-model --timeline
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
cc-model — Week-by-week model usage
|
|
38
|
+
|
|
39
|
+
2026-W02 20 Opus 4.5×20
|
|
40
|
+
2026-W03 33 Opus 4.5×30 Haiku 4.5×3
|
|
41
|
+
2026-W05 76 Opus 4.5×75 Haiku 4.5×1
|
|
42
|
+
2026-W06 146 Opus 4.5×115 Opus 4.6×15 Haiku 4.5×8 Sonnet 4.5×8
|
|
43
|
+
2026-W07 37 Opus 4.6×36 Haiku 4.5×1
|
|
44
|
+
2026-W08 91 Opus 4.6×53 Sonnet 4.6×38
|
|
45
|
+
2026-W09 50 Opus 4.6×41 Sonnet 4.6×8
|
|
46
|
+
2026-W10 2 Opus 4.6×2
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The timeline reveals the exact week you switched models — and whether you ever mix them within a week.
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx cc-model # Model distribution
|
|
55
|
+
npx cc-model --timeline # Week-by-week breakdown
|
|
56
|
+
npx cc-model --json # JSON output
|
|
57
|
+
npx cc-model --help # Show help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What the data shows
|
|
61
|
+
|
|
62
|
+
- **Primary model**: Which model has handled the most sessions overall
|
|
63
|
+
- **Current model**: The model used in your most recent session
|
|
64
|
+
- **Timeline**: When you switched models, and how often you mix Opus/Sonnet/Haiku
|
|
65
|
+
|
|
66
|
+
## Browser Version
|
|
67
|
+
|
|
68
|
+
→ **[yurukusa.github.io/cc-model](https://yurukusa.github.io/cc-model/)**
|
|
69
|
+
|
|
70
|
+
Drag in your `~/.claude` folder. Includes distribution chart and interactive timeline. Runs locally.
|
|
71
|
+
|
|
72
|
+
## Part of cc-toolkit
|
|
73
|
+
|
|
74
|
+
cc-model is tool #50 in [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 50 free tools for Claude Code users.
|
|
75
|
+
|
|
76
|
+
Related:
|
|
77
|
+
- [cc-session-length](https://github.com/yurukusa/cc-session-length) — Session duration distribution
|
|
78
|
+
- [cc-momentum](https://github.com/yurukusa/cc-momentum) — Week-by-week session trend
|
|
79
|
+
- [cc-depth](https://github.com/yurukusa/cc-depth) — Conversation turns per session
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
**GitHub**: [yurukusa/cc-model](https://github.com/yurukusa/cc-model)
|
|
84
|
+
**Try it**: `npx cc-model`
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-model — Which Claude AI models power your sessions?
|
|
4
|
+
* Shows the distribution of model usage across your Claude Code sessions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const jsonMode = args.includes('--json');
|
|
13
|
+
const timelineMode = args.includes('--timeline');
|
|
14
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
15
|
+
|
|
16
|
+
if (showHelp) {
|
|
17
|
+
console.log(`cc-model — Which Claude AI models power your sessions?
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
npx cc-model # Model distribution
|
|
21
|
+
npx cc-model --timeline # Week-by-week model usage
|
|
22
|
+
npx cc-model --json # JSON output
|
|
23
|
+
`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalize model ID to a short human-readable name.
|
|
31
|
+
* "claude-opus-4-6" -> "Opus 4.6"
|
|
32
|
+
* "claude-opus-4-5-20251101" -> "Opus 4.5"
|
|
33
|
+
* "claude-haiku-4-5-20251001" -> "Haiku 4.5"
|
|
34
|
+
*/
|
|
35
|
+
function normalizeModel(raw) {
|
|
36
|
+
// Strip date suffix
|
|
37
|
+
const base = raw.replace(/-20\d{6}$/, '');
|
|
38
|
+
// "claude-opus-4-6" -> split by "-"
|
|
39
|
+
const parts = base.replace(/^claude-/, '').split('-');
|
|
40
|
+
// parts = ["opus", "4", "6"] or ["sonnet", "4", "5"]
|
|
41
|
+
if (parts.length >= 3) {
|
|
42
|
+
const tier = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
43
|
+
const major = parts[1];
|
|
44
|
+
const minor = parts[2];
|
|
45
|
+
return `${tier} ${major}.${minor}`;
|
|
46
|
+
}
|
|
47
|
+
return base;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scanProjects(dir) {
|
|
51
|
+
const sessions = [];
|
|
52
|
+
let projectDirs;
|
|
53
|
+
try {
|
|
54
|
+
projectDirs = readdirSync(dir);
|
|
55
|
+
} catch {
|
|
56
|
+
return sessions;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const projDir of projectDirs) {
|
|
60
|
+
const projPath = join(dir, projDir);
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
const stat = statSync(projPath);
|
|
64
|
+
if (!stat.isDirectory()) continue;
|
|
65
|
+
entries = readdirSync(projPath);
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (!entry.endsWith('.jsonl')) continue;
|
|
72
|
+
const filePath = join(projPath, entry);
|
|
73
|
+
try {
|
|
74
|
+
const stat = statSync(filePath);
|
|
75
|
+
if (!stat.isFile()) continue;
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let content;
|
|
81
|
+
try {
|
|
82
|
+
content = readFileSync(filePath, 'utf8');
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Extract all model references and find the dominant one
|
|
88
|
+
const modelMatches = content.match(/"model":"claude-[^"]+"/g);
|
|
89
|
+
if (!modelMatches || modelMatches.length === 0) continue;
|
|
90
|
+
|
|
91
|
+
// Count each model
|
|
92
|
+
const modelCount = {};
|
|
93
|
+
for (const m of modelMatches) {
|
|
94
|
+
const rawModel = m.replace('"model":"', '').replace('"', '');
|
|
95
|
+
const normalized = normalizeModel(rawModel);
|
|
96
|
+
modelCount[normalized] = (modelCount[normalized] || 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Primary model for this session = most frequent
|
|
100
|
+
const primaryModel = Object.entries(modelCount).sort((a, b) => b[1] - a[1])[0][0];
|
|
101
|
+
|
|
102
|
+
// Get first timestamp for timeline
|
|
103
|
+
let firstTs = null;
|
|
104
|
+
const tsMatch = content.match(/"timestamp":"([^"]+)"/);
|
|
105
|
+
if (tsMatch) firstTs = new Date(tsMatch[1]);
|
|
106
|
+
|
|
107
|
+
sessions.push({ model: primaryModel, date: firstTs, modelCount });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return sessions;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sessions = scanProjects(claudeDir);
|
|
115
|
+
|
|
116
|
+
if (sessions.length === 0) {
|
|
117
|
+
console.error('No session files with model data found.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Aggregate by model
|
|
122
|
+
const modelTotals = {};
|
|
123
|
+
for (const s of sessions) {
|
|
124
|
+
modelTotals[s.model] = (modelTotals[s.model] || 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sortedModels = Object.entries(modelTotals).sort((a, b) => b[1] - a[1]);
|
|
128
|
+
const total = sessions.length;
|
|
129
|
+
|
|
130
|
+
// Most recent session's model = "current"
|
|
131
|
+
const sortedByDate = sessions.filter(s => s.date).sort((a, b) => b.date - a.date);
|
|
132
|
+
const currentModel = sortedByDate.length > 0 ? sortedByDate[0].model : null;
|
|
133
|
+
|
|
134
|
+
if (jsonMode) {
|
|
135
|
+
console.log(JSON.stringify({
|
|
136
|
+
total_sessions: total,
|
|
137
|
+
current_model: currentModel,
|
|
138
|
+
primary_model: sortedModels[0]?.[0],
|
|
139
|
+
distribution: sortedModels.map(([model, count]) => ({
|
|
140
|
+
model,
|
|
141
|
+
sessions: count,
|
|
142
|
+
pct: Math.round(count / total * 100),
|
|
143
|
+
})),
|
|
144
|
+
}, null, 2));
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Timeline mode
|
|
149
|
+
if (timelineMode) {
|
|
150
|
+
function getISOWeek(date) {
|
|
151
|
+
const d = new Date(date);
|
|
152
|
+
d.setHours(0, 0, 0, 0);
|
|
153
|
+
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
|
|
154
|
+
const week1 = new Date(d.getFullYear(), 0, 4);
|
|
155
|
+
const weekNum = 1 + Math.round(((d - week1) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7);
|
|
156
|
+
return `${d.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Group by week, track model usage
|
|
160
|
+
const weekData = {};
|
|
161
|
+
for (const s of sessions) {
|
|
162
|
+
if (!s.date) continue;
|
|
163
|
+
const wk = getISOWeek(s.date);
|
|
164
|
+
if (!weekData[wk]) weekData[wk] = {};
|
|
165
|
+
weekData[wk][s.model] = (weekData[wk][s.model] || 0) + 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const weeks = Object.keys(weekData).sort();
|
|
169
|
+
if (weeks.length === 0) {
|
|
170
|
+
console.log('No timeline data available.');
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Only show last 12 weeks
|
|
175
|
+
const recentWeeks = weeks.slice(-12);
|
|
176
|
+
const allModels = [...new Set(sessions.map(s => s.model))].sort();
|
|
177
|
+
const modelColors = ['Opus', 'Sonnet', 'Haiku'];
|
|
178
|
+
|
|
179
|
+
console.log('cc-model — Week-by-week model usage\n');
|
|
180
|
+
for (const wk of recentWeeks) {
|
|
181
|
+
const data = weekData[wk];
|
|
182
|
+
const wkTotal = Object.values(data).reduce((a, b) => a + b, 0);
|
|
183
|
+
const dominant = Object.entries(data).sort((a, b) => b[1] - a[1])[0][0];
|
|
184
|
+
const models = Object.entries(data).sort((a, b) => b[1] - a[1])
|
|
185
|
+
.map(([m, c]) => `${m}×${c}`)
|
|
186
|
+
.join(' ');
|
|
187
|
+
console.log(` ${wk} ${String(wkTotal).padStart(3)} ${models}`);
|
|
188
|
+
}
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Default: distribution chart
|
|
193
|
+
const BAR_WIDTH = 28;
|
|
194
|
+
const maxCount = sortedModels[0]?.[1] || 1;
|
|
195
|
+
|
|
196
|
+
function bar(count) {
|
|
197
|
+
const filled = Math.round((count / maxCount) * BAR_WIDTH);
|
|
198
|
+
return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function rpad(str, len) {
|
|
202
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Find max label length
|
|
206
|
+
const maxLabel = Math.max(...sortedModels.map(([m]) => m.length));
|
|
207
|
+
|
|
208
|
+
console.log('cc-model — Which Claude models power your sessions\n');
|
|
209
|
+
|
|
210
|
+
for (const [model, count] of sortedModels) {
|
|
211
|
+
const label = rpad(model, maxLabel);
|
|
212
|
+
const countStr = String(count).padStart(4);
|
|
213
|
+
const pct = (count / total * 100).toFixed(0).padStart(3);
|
|
214
|
+
console.log(` ${label} ${bar(count)} ${countStr} (${pct}%)`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log('\n' + '─'.repeat(55));
|
|
218
|
+
console.log(` Total sessions: ${total}`);
|
|
219
|
+
if (currentModel) console.log(` Current model: ${currentModel}`);
|
|
220
|
+
console.log(` Primary model: ${sortedModels[0]?.[0]} (${sortedModels[0]?.[1]} sessions)`);
|
|
221
|
+
console.log(`\n Run with --timeline to see week-by-week model history`);
|
package/index.html
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>cc-model — Claude model distribution</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
background: #0d1117;
|
|
11
|
+
color: #c9d1d9;
|
|
12
|
+
font-family: 'SF Mono', 'Consolas', 'Cascadia Code', monospace;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
align-items: center;
|
|
17
|
+
padding: 40px 20px;
|
|
18
|
+
}
|
|
19
|
+
h1 { font-size: 1.5rem; color: #d2a8ff; margin-bottom: 6px; letter-spacing: -0.5px; }
|
|
20
|
+
.subtitle { color: #8b949e; font-size: 0.875rem; margin-bottom: 32px; }
|
|
21
|
+
|
|
22
|
+
.drop-zone {
|
|
23
|
+
border: 2px dashed #30363d;
|
|
24
|
+
border-radius: 12px;
|
|
25
|
+
padding: 48px 64px;
|
|
26
|
+
text-align: center;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: all 0.2s;
|
|
29
|
+
max-width: 480px;
|
|
30
|
+
width: 100%;
|
|
31
|
+
margin-bottom: 32px;
|
|
32
|
+
}
|
|
33
|
+
.drop-zone:hover, .drop-zone.drag-over {
|
|
34
|
+
border-color: #d2a8ff;
|
|
35
|
+
background: rgba(210,168,255,0.05);
|
|
36
|
+
}
|
|
37
|
+
.drop-icon { font-size: 2.5rem; margin-bottom: 12px; }
|
|
38
|
+
.drop-text { color: #8b949e; font-size: 0.875rem; line-height: 1.6; }
|
|
39
|
+
.drop-text strong { color: #c9d1d9; }
|
|
40
|
+
#file-input { display: none; }
|
|
41
|
+
|
|
42
|
+
.result { display: none; max-width: 600px; width: 100%; }
|
|
43
|
+
.result.visible { display: block; }
|
|
44
|
+
|
|
45
|
+
.card {
|
|
46
|
+
background: #161b22;
|
|
47
|
+
border: 1px solid #30363d;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
padding: 20px;
|
|
50
|
+
margin-bottom: 16px;
|
|
51
|
+
}
|
|
52
|
+
.card-title {
|
|
53
|
+
color: #8b949e;
|
|
54
|
+
font-size: 0.75rem;
|
|
55
|
+
text-transform: uppercase;
|
|
56
|
+
letter-spacing: 1px;
|
|
57
|
+
margin-bottom: 16px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.stats-grid {
|
|
61
|
+
display: grid;
|
|
62
|
+
grid-template-columns: repeat(3, 1fr);
|
|
63
|
+
gap: 16px;
|
|
64
|
+
margin-bottom: 16px;
|
|
65
|
+
}
|
|
66
|
+
.stat-item { text-align: center; }
|
|
67
|
+
.stat-val { font-size: 1.3rem; color: #d2a8ff; font-weight: 700; }
|
|
68
|
+
.stat-lbl { font-size: 0.7rem; color: #8b949e; margin-top: 2px; }
|
|
69
|
+
|
|
70
|
+
/* Model tier colors */
|
|
71
|
+
.opus { background: linear-gradient(90deg, #d2a8ff 0%, #a56bff 100%); }
|
|
72
|
+
.sonnet { background: linear-gradient(90deg, #79c0ff 0%, #388bfd 100%); }
|
|
73
|
+
.haiku { background: linear-gradient(90deg, #56d364 0%, #2ea043 100%); }
|
|
74
|
+
|
|
75
|
+
.bar-chart { font-size: 0.8rem; }
|
|
76
|
+
.bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
77
|
+
.bar-label { width: 88px; text-align: right; color: #c9d1d9; flex-shrink: 0; font-size: 0.8rem; }
|
|
78
|
+
.bar-track { flex: 1; height: 16px; background: #21262d; border-radius: 3px; overflow: hidden; }
|
|
79
|
+
.bar-fill { height: 100%; border-radius: 3px; transition: width 0.6s ease; }
|
|
80
|
+
.bar-count { width: 72px; color: #8b949e; text-align: right; flex-shrink: 0; font-size: 0.75rem; }
|
|
81
|
+
|
|
82
|
+
/* Timeline */
|
|
83
|
+
.timeline { font-size: 0.78rem; }
|
|
84
|
+
.tl-row { display: flex; gap: 12px; margin-bottom: 4px; align-items: flex-start; }
|
|
85
|
+
.tl-week { width: 64px; color: #8b949e; flex-shrink: 0; }
|
|
86
|
+
.tl-bars { flex: 1; display: flex; gap: 4px; flex-wrap: wrap; }
|
|
87
|
+
.tl-chip {
|
|
88
|
+
padding: 2px 7px;
|
|
89
|
+
border-radius: 3px;
|
|
90
|
+
font-size: 0.72rem;
|
|
91
|
+
color: #0d1117;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.tab-row { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
96
|
+
.tab-btn {
|
|
97
|
+
background: none;
|
|
98
|
+
border: 1px solid #30363d;
|
|
99
|
+
color: #8b949e;
|
|
100
|
+
padding: 6px 14px;
|
|
101
|
+
border-radius: 6px;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
font-family: inherit;
|
|
104
|
+
font-size: 0.8rem;
|
|
105
|
+
transition: all 0.2s;
|
|
106
|
+
}
|
|
107
|
+
.tab-btn.active { border-color: #d2a8ff; color: #d2a8ff; background: rgba(210,168,255,0.1); }
|
|
108
|
+
.tab-btn:hover:not(.active) { border-color: #8b949e; color: #c9d1d9; }
|
|
109
|
+
|
|
110
|
+
.tab-panel { display: none; }
|
|
111
|
+
.tab-panel.active { display: block; }
|
|
112
|
+
|
|
113
|
+
.reset-btn {
|
|
114
|
+
margin-top: 16px;
|
|
115
|
+
background: none;
|
|
116
|
+
border: 1px solid #30363d;
|
|
117
|
+
color: #8b949e;
|
|
118
|
+
padding: 8px 16px;
|
|
119
|
+
border-radius: 6px;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
font-family: inherit;
|
|
122
|
+
font-size: 0.8rem;
|
|
123
|
+
display: block;
|
|
124
|
+
width: 100%;
|
|
125
|
+
transition: all 0.2s;
|
|
126
|
+
}
|
|
127
|
+
.reset-btn:hover { border-color: #d2a8ff; color: #d2a8ff; }
|
|
128
|
+
|
|
129
|
+
.footer { color: #8b949e; font-size: 0.75rem; text-align: center; margin-top: 12px; }
|
|
130
|
+
.footer a { color: #d2a8ff; text-decoration: none; }
|
|
131
|
+
.footer a:hover { text-decoration: underline; }
|
|
132
|
+
</style>
|
|
133
|
+
</head>
|
|
134
|
+
<body>
|
|
135
|
+
|
|
136
|
+
<h1>🤖 cc-model</h1>
|
|
137
|
+
<p class="subtitle">Which Claude AI models power your sessions?</p>
|
|
138
|
+
|
|
139
|
+
<div class="drop-zone" id="drop-zone">
|
|
140
|
+
<div class="drop-icon">📁</div>
|
|
141
|
+
<div class="drop-text">
|
|
142
|
+
<strong>Drop your ~/.claude folder here</strong><br>
|
|
143
|
+
or click to select<br><br>
|
|
144
|
+
Reads session files locally.<br>
|
|
145
|
+
Nothing is uploaded.
|
|
146
|
+
</div>
|
|
147
|
+
<input type="file" id="file-input" webkitdirectory multiple accept=".jsonl">
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div class="result" id="result">
|
|
151
|
+
<div class="card">
|
|
152
|
+
<div class="card-title">Model Summary</div>
|
|
153
|
+
<div class="stats-grid">
|
|
154
|
+
<div class="stat-item">
|
|
155
|
+
<div class="stat-val" id="stat-total">—</div>
|
|
156
|
+
<div class="stat-lbl">sessions</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="stat-item">
|
|
159
|
+
<div class="stat-val" id="stat-current">—</div>
|
|
160
|
+
<div class="stat-lbl">current model</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="stat-item">
|
|
163
|
+
<div class="stat-val" id="stat-primary">—</div>
|
|
164
|
+
<div class="stat-lbl">primary model</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="card">
|
|
170
|
+
<div class="tab-row">
|
|
171
|
+
<button class="tab-btn active" data-tab="dist">Distribution</button>
|
|
172
|
+
<button class="tab-btn" data-tab="timeline">Timeline</button>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="tab-panel active" id="tab-dist">
|
|
176
|
+
<div class="bar-chart" id="bar-chart"></div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="tab-panel" id="tab-timeline">
|
|
179
|
+
<div class="timeline" id="timeline-chart"></div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<button class="reset-btn" id="reset-btn">← Analyze another folder</button>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="footer">
|
|
187
|
+
<a href="https://github.com/yurukusa/cc-model" target="_blank">cc-model</a> ·
|
|
188
|
+
Part of <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> · 106 free tools for Claude Code
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<script>
|
|
192
|
+
const dropZone = document.getElementById('drop-zone');
|
|
193
|
+
const fileInput = document.getElementById('file-input');
|
|
194
|
+
const resultEl = document.getElementById('result');
|
|
195
|
+
const resetBtn = document.getElementById('reset-btn');
|
|
196
|
+
|
|
197
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
198
|
+
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
199
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
200
|
+
dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(e.dataTransfer.files); });
|
|
201
|
+
fileInput.addEventListener('change', () => processFiles(fileInput.files));
|
|
202
|
+
resetBtn.addEventListener('click', () => { resultEl.classList.remove('visible'); dropZone.style.display = ''; fileInput.value = ''; });
|
|
203
|
+
|
|
204
|
+
// Tab switching
|
|
205
|
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
206
|
+
btn.addEventListener('click', () => {
|
|
207
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
208
|
+
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
209
|
+
btn.classList.add('active');
|
|
210
|
+
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
function normalizeModel(raw) {
|
|
215
|
+
const base = raw.replace(/-20\d{6}$/, '');
|
|
216
|
+
const parts = base.replace(/^claude-/, '').split('-');
|
|
217
|
+
if (parts.length >= 3) {
|
|
218
|
+
const tier = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
219
|
+
return `${tier} ${parts[1]}.${parts[2]}`;
|
|
220
|
+
}
|
|
221
|
+
return base;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getModelClass(model) {
|
|
225
|
+
if (model.startsWith('Opus')) return 'opus';
|
|
226
|
+
if (model.startsWith('Sonnet')) return 'sonnet';
|
|
227
|
+
if (model.startsWith('Haiku')) return 'haiku';
|
|
228
|
+
return 'opus';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getISOWeek(date) {
|
|
232
|
+
const d = new Date(date);
|
|
233
|
+
d.setHours(0,0,0,0);
|
|
234
|
+
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
|
|
235
|
+
const week1 = new Date(d.getFullYear(), 0, 4);
|
|
236
|
+
const wn = 1 + Math.round(((d - week1)/86400000 - 3 + ((week1.getDay()+6)%7))/7);
|
|
237
|
+
return `${d.getFullYear()}-W${String(wn).padStart(2,'0')}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function processFiles(files) {
|
|
241
|
+
const jsonlFiles = Array.from(files).filter(f => {
|
|
242
|
+
const p = f.webkitRelativePath || f.name;
|
|
243
|
+
return p.endsWith('.jsonl') && !p.includes('/subagents/');
|
|
244
|
+
});
|
|
245
|
+
if (jsonlFiles.length === 0) { alert('No session files found.'); return; }
|
|
246
|
+
dropZone.style.display = 'none';
|
|
247
|
+
|
|
248
|
+
const sessions = [];
|
|
249
|
+
const BATCH = 40;
|
|
250
|
+
for (let i = 0; i < jsonlFiles.length; i += BATCH) {
|
|
251
|
+
const batch = jsonlFiles.slice(i, i + BATCH);
|
|
252
|
+
await Promise.all(batch.map(async f => {
|
|
253
|
+
try {
|
|
254
|
+
const text = await f.text();
|
|
255
|
+
const matches = text.match(/"model":"claude-[^"]+"/g);
|
|
256
|
+
if (!matches) return;
|
|
257
|
+
const mc = {};
|
|
258
|
+
for (const m of matches) {
|
|
259
|
+
const nm = normalizeModel(m.replace('"model":"','').replace('"',''));
|
|
260
|
+
mc[nm] = (mc[nm]||0) + 1;
|
|
261
|
+
}
|
|
262
|
+
const primary = Object.entries(mc).sort((a,b)=>b[1]-a[1])[0][0];
|
|
263
|
+
const tsMatch = text.match(/"timestamp":"([^"]+)"/);
|
|
264
|
+
sessions.push({ model: primary, date: tsMatch ? new Date(tsMatch[1]) : null });
|
|
265
|
+
} catch {}
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!sessions.length) { alert('No model data found.'); return; }
|
|
270
|
+
|
|
271
|
+
const modelTotals = {};
|
|
272
|
+
for (const s of sessions) modelTotals[s.model] = (modelTotals[s.model]||0) + 1;
|
|
273
|
+
const sorted = Object.entries(modelTotals).sort((a,b)=>b[1]-a[1]);
|
|
274
|
+
const total = sessions.length;
|
|
275
|
+
const recent = sessions.filter(s=>s.date).sort((a,b)=>b.date-a.date)[0];
|
|
276
|
+
const current = recent ? recent.model : sorted[0][0];
|
|
277
|
+
|
|
278
|
+
document.getElementById('stat-total').textContent = total;
|
|
279
|
+
document.getElementById('stat-current').textContent = current;
|
|
280
|
+
document.getElementById('stat-primary').textContent = sorted[0][0];
|
|
281
|
+
|
|
282
|
+
const maxCount = sorted[0][1];
|
|
283
|
+
document.getElementById('bar-chart').innerHTML = sorted.map(([m, c]) => `
|
|
284
|
+
<div class="bar-row">
|
|
285
|
+
<div class="bar-label">${m}</div>
|
|
286
|
+
<div class="bar-track">
|
|
287
|
+
<div class="bar-fill ${getModelClass(m)}" style="width:${(c/maxCount*100).toFixed(1)}%"></div>
|
|
288
|
+
</div>
|
|
289
|
+
<div class="bar-count">${c} (${Math.round(c/total*100)}%)</div>
|
|
290
|
+
</div>
|
|
291
|
+
`).join('');
|
|
292
|
+
|
|
293
|
+
// Timeline
|
|
294
|
+
const weekData = {};
|
|
295
|
+
for (const s of sessions) {
|
|
296
|
+
if (!s.date) continue;
|
|
297
|
+
const wk = getISOWeek(s.date);
|
|
298
|
+
if (!weekData[wk]) weekData[wk] = {};
|
|
299
|
+
weekData[wk][s.model] = (weekData[wk][s.model]||0) + 1;
|
|
300
|
+
}
|
|
301
|
+
const weeks = Object.keys(weekData).sort().slice(-12);
|
|
302
|
+
document.getElementById('timeline-chart').innerHTML = weeks.map(wk => {
|
|
303
|
+
const data = weekData[wk];
|
|
304
|
+
const chips = Object.entries(data).sort((a,b)=>b[1]-a[1]).map(([m,c]) =>
|
|
305
|
+
`<span class="tl-chip ${getModelClass(m)}">${m} ×${c}</span>`
|
|
306
|
+
).join('');
|
|
307
|
+
return `<div class="tl-row"><div class="tl-week">${wk}</div><div class="tl-bars">${chips}</div></div>`;
|
|
308
|
+
}).join('');
|
|
309
|
+
|
|
310
|
+
resultEl.classList.add('visible');
|
|
311
|
+
}
|
|
312
|
+
</script>
|
|
313
|
+
</body>
|
|
314
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-model",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Which Claude AI models power your sessions? Distribution and timeline of model usage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-model": "./cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"ai",
|
|
16
|
+
"developer-tools",
|
|
17
|
+
"analytics",
|
|
18
|
+
"productivity"
|
|
19
|
+
],
|
|
20
|
+
"author": "yurukusa",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
}
|
|
25
|
+
}
|