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.
Files changed (4) hide show
  1. package/README.md +84 -0
  2. package/cli.mjs +221 -0
  3. package/index.html +314 -0
  4. 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
+ }