cc-momentum 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 +74 -0
  2. package/cli.mjs +198 -0
  3. package/index.html +215 -0
  4. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # cc-momentum
2
+
3
+ **Is your Claude Code usage growing or declining?**
4
+
5
+ Shows your week-by-week session count trend and classifies your current momentum.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npx cc-momentum
11
+ ```
12
+
13
+ Or use the browser version (no install):
14
+
15
+ → **[yurukusa.github.io/cc-momentum](https://yurukusa.github.io/cc-momentum/)**
16
+
17
+ Drag in your `~/.claude` folder. Everything runs locally — nothing is uploaded.
18
+
19
+ ## Sample output
20
+
21
+ ```
22
+ cc-momentum — Week-by-week Claude Code trend
23
+
24
+ 2026-W02 ████░░░░░░░░░░░░░░░░░░░░░░░░░░ 20
25
+ 2026-W03 ██████░░░░░░░░░░░░░░░░░░░░░░░░ 32
26
+ 2026-W04 ███░░░░░░░░░░░░░░░░░░░░░░░░░░░ 13
27
+ 2026-W05 ████████████████░░░░░░░░░░░░░░ 77
28
+ 2026-W06 ██████████████████████████████ 149 ◀ peak
29
+ 2026-W07 █████████████████░░░░░░░░░░░░░ 83
30
+ 2026-W08 ███████████████░░░░░░░░░░░░░░░ 75
31
+ 2026-W09 ███████░░░░░░░░░░░░░░░░░░░░░░░ 36
32
+ 2026-W10 █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3 (in progress)
33
+
34
+ ───────────────────────────────────────────────────────
35
+ Trend (3w): → Stable
36
+ Recent avg: 65 / week (-19% vs prev 3 weeks)
37
+
38
+ Peak week: 2026-W06 — 149 sessions
39
+ Total: 488 sessions across 9 weeks
40
+ ```
41
+
42
+ ## Trend classifications
43
+
44
+ | Trend | Change | Meaning |
45
+ |-------|--------|---------|
46
+ | 🚀 Accelerating | > +50% | Rapid adoption / intensive phase |
47
+ | 📈 Growing | +20–50% | Steady increase |
48
+ | → Stable | ±20% | Consistent usage pattern |
49
+ | 📉 Declining | -20–50% | Usage tapering off |
50
+ | ⬇️ Sharply Declining | > -50% | Significant drop-off |
51
+
52
+ *Note: The current (incomplete) week is excluded from trend calculation and marked "(in progress)".*
53
+
54
+ ## Options
55
+
56
+ ```bash
57
+ npx cc-momentum # Last 12 weeks
58
+ npx cc-momentum --weeks=24 # Last 24 weeks
59
+ npx cc-momentum --json # JSON output for dashboards
60
+ ```
61
+
62
+ ## Part of cc-toolkit
63
+
64
+ cc-momentum is tool #48 in [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 49 free tools for Claude Code users.
65
+
66
+ Related tools:
67
+ - [cc-streak](https://github.com/yurukusa/cc-streak) — Consecutive days of usage
68
+ - [cc-gap](https://github.com/yurukusa/cc-gap) — Time between sessions
69
+ - [cc-session-length](https://github.com/yurukusa/cc-session-length) — How long do sessions last?
70
+
71
+ ---
72
+
73
+ **GitHub**: [yurukusa/cc-momentum](https://github.com/yurukusa/cc-momentum)
74
+ **Try it**: `npx cc-momentum`
package/cli.mjs ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import { readdir, open } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+
6
+ const VERSION = '1.0.0';
7
+ const MAX_CHUNK = 4096;
8
+
9
+ function parseArgs(argv) {
10
+ const args = { weeks: 12, json: false };
11
+ for (const a of argv.slice(2)) {
12
+ if (a.startsWith('--weeks=')) args.weeks = parseInt(a.slice(8)) || 12;
13
+ else if (a === '--json') args.json = true;
14
+ else if (a === '--help' || a === '-h') {
15
+ console.log([
16
+ `cc-momentum v${VERSION}`,
17
+ '',
18
+ 'Usage: cc-momentum [options]',
19
+ '',
20
+ 'Options:',
21
+ ' --weeks=N Show last N weeks (default: 12)',
22
+ ' --json Output JSON for piping',
23
+ ' --help Show this help',
24
+ '',
25
+ 'Shows your week-by-week Claude Code session count trend.',
26
+ ].join('\n'));
27
+ process.exit(0);
28
+ }
29
+ }
30
+ return args;
31
+ }
32
+
33
+ async function getFirstTimestamp(path) {
34
+ let fh;
35
+ try {
36
+ fh = await open(path, 'r');
37
+ const buf = Buffer.alloc(MAX_CHUNK);
38
+ const { bytesRead } = await fh.read(buf, 0, MAX_CHUNK, 0);
39
+ const text = buf.subarray(0, bytesRead).toString('utf8');
40
+ for (const line of text.split('\n')) {
41
+ if (!line.trim()) continue;
42
+ try {
43
+ const d = JSON.parse(line);
44
+ if (d.timestamp) return new Date(d.timestamp);
45
+ } catch {}
46
+ }
47
+ } catch {}
48
+ finally { await fh?.close(); }
49
+ return null;
50
+ }
51
+
52
+ function getISOWeek(date) {
53
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
54
+ const day = d.getUTCDay() || 7;
55
+ d.setUTCDate(d.getUTCDate() + 4 - day);
56
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
57
+ const week = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
58
+ return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
59
+ }
60
+
61
+ async function main() {
62
+ const args = parseArgs(process.argv);
63
+ const claudeDir = join(homedir(), '.claude', 'projects');
64
+
65
+ const weekCounts = new Map();
66
+ let projectDirs;
67
+ try { projectDirs = await readdir(claudeDir); } catch {
68
+ console.error('~/.claude/projects not found');
69
+ process.exit(1);
70
+ }
71
+
72
+ for (const pd of projectDirs) {
73
+ const pdPath = join(claudeDir, pd);
74
+ let files;
75
+ try { files = await readdir(pdPath); } catch { continue; }
76
+ for (const name of files) {
77
+ if (!name.endsWith('.jsonl')) continue;
78
+ const ts = await getFirstTimestamp(join(pdPath, name));
79
+ if (!ts) continue;
80
+ const week = getISOWeek(ts);
81
+ weekCounts.set(week, (weekCounts.get(week) || 0) + 1);
82
+ }
83
+ }
84
+
85
+ if (!weekCounts.size) {
86
+ console.log('No sessions found.');
87
+ process.exit(0);
88
+ }
89
+
90
+ const allWeeks = [...weekCounts.keys()].sort();
91
+ const firstWeek = allWeeks[0];
92
+ const lastWeek = allWeeks[allWeeks.length - 1];
93
+
94
+ // Fill in gaps (weeks with 0 sessions)
95
+ const weeks = [];
96
+ let cursor = new Date(firstWeek.replace('W', '') + '-1'); // approximate
97
+ // Build the full range of ISO weeks
98
+ const allKeys = new Set(weekCounts.keys());
99
+ // Generate all ISO weeks from first to last
100
+ let [fy, fw] = firstWeek.split('-W').map(Number);
101
+ let [ly, lw] = lastWeek.split('-W').map(Number);
102
+
103
+ function nextWeek(y, w) {
104
+ // Get max weeks for that year
105
+ const dec28 = new Date(Date.UTC(y, 11, 28));
106
+ const maxW = getISOWeek(dec28).endsWith('53') ? 53 : 52;
107
+ if (w >= maxW) return [y + 1, 1];
108
+ return [y, w + 1];
109
+ }
110
+
111
+ let cy = fy, cw = fw;
112
+ while (true) {
113
+ const key = `${cy}-W${String(cw).padStart(2, '0')}`;
114
+ weeks.push({ key, count: weekCounts.get(key) || 0 });
115
+ if (key === lastWeek) break;
116
+ [cy, cw] = nextWeek(cy, cw);
117
+ if (cy > ly + 1) break; // safety
118
+ }
119
+
120
+ // Show last N weeks
121
+ const display = weeks.slice(-args.weeks);
122
+
123
+ // Current week (may be incomplete) — mark it but exclude from trend
124
+ const currentWeekKey = getISOWeek(new Date());
125
+ const isCurrentWeekPresent = weeks.length > 0 && weeks[weeks.length - 1].key === currentWeekKey;
126
+
127
+ // Trend: compare last 3 complete weeks vs previous 3 weeks
128
+ const completeWeeks = isCurrentWeekPresent ? weeks.slice(0, -1) : weeks;
129
+ const recentWeeks = completeWeeks.slice(-3);
130
+ const prevWeeks = completeWeeks.slice(-6, -3);
131
+ const recentAvg = recentWeeks.reduce((s, w) => s + w.count, 0) / Math.max(recentWeeks.length, 1);
132
+ const prevAvg = prevWeeks.reduce((s, w) => s + w.count, 0) / Math.max(prevWeeks.length, 1);
133
+ const trendPct = prevAvg > 0 ? ((recentAvg - prevAvg) / prevAvg * 100) : 0;
134
+ const peakWeek = weeks.reduce((a, b) => a.count >= b.count ? a : b);
135
+ const totalSessions = weeks.reduce((s, w) => s + w.count, 0);
136
+
137
+ let trendLabel, trendColor;
138
+ if (trendPct >= 50) { trendLabel = '🚀 Accelerating'; trendColor = 'green'; }
139
+ else if (trendPct >= 20) { trendLabel = '📈 Growing'; trendColor = 'green'; }
140
+ else if (trendPct >= -20) { trendLabel = '→ Stable'; trendColor = 'yellow'; }
141
+ else if (trendPct >= -50) { trendLabel = '📉 Declining'; trendColor = 'orange'; }
142
+ else { trendLabel = '⬇️ Sharply Declining'; trendColor = 'red'; }
143
+
144
+ if (args.json) {
145
+ console.log(JSON.stringify({
146
+ totalSessions,
147
+ weeks: weeks.map(w => ({ week: w.key, sessions: w.count })),
148
+ peakWeek: peakWeek.key,
149
+ peakCount: peakWeek.count,
150
+ recentAvg: Math.round(recentAvg),
151
+ prevAvg: Math.round(prevAvg),
152
+ trendPct: Math.round(trendPct),
153
+ trend: trendLabel,
154
+ }, null, 2));
155
+ return;
156
+ }
157
+
158
+ const C = {
159
+ reset: '\x1b[0m', bold: '\x1b[1m',
160
+ green: '\x1b[32m', yellow: '\x1b[33m',
161
+ orange: '\x1b[38;5;214m', red: '\x1b[31m',
162
+ purple: '\x1b[35m', cyan: '\x1b[36m',
163
+ muted: '\x1b[90m',
164
+ };
165
+ const tc = trendColor === 'green' ? C.green
166
+ : trendColor === 'yellow' ? C.yellow
167
+ : trendColor === 'orange' ? C.orange
168
+ : C.red;
169
+
170
+ console.log(`\n${C.purple}${C.bold}cc-momentum${C.reset} — Week-by-week Claude Code trend\n`);
171
+
172
+ const maxCount = Math.max(...display.map(w => w.count), 1);
173
+
174
+ for (const w of display) {
175
+ const barWidth = Math.round(w.count / maxCount * 30);
176
+ const isPeak = w.key === peakWeek.key;
177
+ const isCurrent = w.key === currentWeekKey;
178
+ const color = isPeak ? C.purple : w.count > recentAvg * 1.2 ? C.cyan : C.muted;
179
+ const filled = '█'.repeat(barWidth);
180
+ const empty = '░'.repeat(30 - barWidth);
181
+ const suffix = isPeak ? C.purple + ' ◀ peak' : isCurrent ? C.muted + ' (in progress)' : '';
182
+ console.log(
183
+ ` ${C.muted}${w.key}${C.reset} ` +
184
+ `${color}${filled}${C.muted}${empty}${C.reset} ` +
185
+ `${C.muted}${String(w.count).padStart(3)}${suffix}${C.reset}`
186
+ );
187
+ }
188
+
189
+ console.log(`\n${'─'.repeat(55)}`);
190
+ console.log(` ${C.cyan}Trend (4w):${C.reset} ${tc}${C.bold}${trendLabel}${C.reset}`);
191
+ const sign = trendPct >= 0 ? '+' : '';
192
+ console.log(` ${C.muted}Recent avg: ${Math.round(recentAvg)} / week (${sign}${Math.round(trendPct)}% vs prev 3 weeks)${C.reset}`);
193
+ console.log(`\n ${C.muted}Peak week:${C.reset} ${C.purple}${peakWeek.key}${C.reset} — ${C.bold}${peakWeek.count} sessions${C.reset}`);
194
+ console.log(` ${C.muted}Total:${C.reset} ${C.bold}${totalSessions} sessions${C.reset} across ${C.bold}${weeks.length} weeks${C.reset}`);
195
+ console.log();
196
+ }
197
+
198
+ main().catch(e => { console.error(e.message); process.exit(1); });
package/index.html ADDED
@@ -0,0 +1,215 @@
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-momentum — Your Claude Code usage trend</title>
7
+ <meta name="description" content="Are you using Claude Code more or less than last month? Drop your ~/.claude folder to see your week-by-week session trend.">
8
+ <meta property="og:title" content="cc-momentum — Week-by-week Claude Code trend analyzer">
9
+ <meta property="og:description" content="See if your Claude Code usage is accelerating, stable, or declining. Week-by-week bar chart with trend classification.">
10
+ <meta property="og:url" content="https://yurukusa.github.io/cc-momentum/">
11
+ <meta name="twitter:card" content="summary">
12
+ <meta name="twitter:site" content="@yurukusa_dev">
13
+ <style>
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+ :root {
16
+ --bg: #0d1117; --surface: #161b22; --border: #30363d;
17
+ --text: #e6edf3; --muted: #8b949e;
18
+ --purple: #bc8cff; --cyan: #58d8f0; --yellow: #f7c948; --orange: #ffa657;
19
+ --blue: #58a6ff; --green: #56d364; --red: #ff7b72;
20
+ }
21
+ body {
22
+ background: var(--bg); color: var(--text);
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Consolas, monospace;
24
+ min-height: 100vh; display: flex; flex-direction: column;
25
+ align-items: center; padding: 2rem 1rem;
26
+ }
27
+ .header { text-align: center; margin-bottom: 2rem; }
28
+ .header h1 { font-size: 1.8rem; font-weight: 700; color: var(--green); margin-bottom: 0.5rem; }
29
+ .header p { color: var(--muted); font-size: 0.9rem; }
30
+ .drop-zone {
31
+ width: 100%; max-width: 600px; border: 2px dashed var(--border);
32
+ border-radius: 12px; padding: 3rem 2rem; text-align: center;
33
+ cursor: pointer; transition: border-color 0.2s, background 0.2s;
34
+ margin-bottom: 2rem; background: var(--surface);
35
+ }
36
+ .drop-zone:hover, .drop-zone.drag-over { border-color: var(--green); background: #0d1f0d; }
37
+ .drop-zone h2 { font-size: 1.1rem; margin-bottom: 0.75rem; }
38
+ .drop-zone p { color: var(--muted); font-size: 0.85rem; line-height: 1.5; }
39
+ .drop-zone .icon { font-size: 2.5rem; margin-bottom: 1rem; }
40
+ .drop-zone input[type="file"] { display: none; }
41
+ #results { width: 100%; max-width: 700px; display: none; }
42
+ .meta { font-size: 0.8rem; color: var(--muted); margin-bottom: 1.5rem; text-align: center; }
43
+ .card {
44
+ background: var(--surface); border: 1px solid var(--border);
45
+ border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem;
46
+ }
47
+ .card-title { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1rem; }
48
+ .week-row {
49
+ display: flex; align-items: center; gap: 0.5rem;
50
+ margin-bottom: 0.3rem; font-size: 0.78rem;
51
+ }
52
+ .week-label { color: var(--muted); min-width: 6.5rem; font-family: Consolas, monospace; }
53
+ .week-label.peak { color: var(--purple); }
54
+ .bar-wrap { flex: 1; height: 14px; background: #21262d; border-radius: 3px; overflow: hidden; }
55
+ .bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease; }
56
+ .bar-fill.peak { background: var(--purple); }
57
+ .bar-fill.above { background: var(--cyan); }
58
+ .bar-fill.normal { background: var(--muted); opacity: 0.7; }
59
+ .bar-fill.current { background: var(--yellow); opacity: 0.6; }
60
+ .week-count { color: var(--muted); min-width: 3.5rem; text-align: right; font-family: Consolas, monospace; font-size: 0.75rem; }
61
+ .score-card { text-align: center; }
62
+ .trend-label { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; }
63
+ .trend-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; margin-top: 1rem; }
64
+ .t-stat .num { font-size: 1.2rem; font-weight: 700; display: block; }
65
+ .t-stat .lbl { font-size: 0.7rem; color: var(--muted); }
66
+ .footer { margin-top: 2rem; text-align: center; font-size: 0.8rem; color: var(--muted); }
67
+ .footer a { color: var(--blue); text-decoration: none; margin: 0 0.5rem; }
68
+ .btn {
69
+ background: var(--green); color: #0d1117; border: none;
70
+ border-radius: 6px; padding: 0.6rem 1.5rem; font-size: 0.9rem;
71
+ font-weight: 600; cursor: pointer; margin-top: 1rem; font-family: inherit;
72
+ }
73
+ .btn:hover { opacity: 0.85; }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div class="header">
78
+ <h1>cc-momentum</h1>
79
+ <p>Is your Claude Code usage growing or declining?</p>
80
+ </div>
81
+ <div class="drop-zone" id="dropZone" onclick="document.getElementById('fi').click()">
82
+ <div class="icon">📈</div>
83
+ <h2>Drop your .claude folder here</h2>
84
+ <p>Or click to select it.<br>Your data stays local — nothing is uploaded.</p>
85
+ <input type="file" id="fi" webkitdirectory multiple>
86
+ <button class="btn" onclick="event.stopPropagation(); document.getElementById('fi').click()">
87
+ Select ~/.claude folder
88
+ </button>
89
+ </div>
90
+ <div id="results">
91
+ <div class="meta" id="metaLine"></div>
92
+ <div class="card score-card" id="scoreCard"></div>
93
+ <div class="card">
94
+ <div class="card-title">Sessions per week</div>
95
+ <div id="chart"></div>
96
+ </div>
97
+ <div class="footer">
98
+ <a href="https://github.com/yurukusa/cc-momentum" target="_blank">GitHub</a> ·
99
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit (106 free tools)</a> ·
100
+ <span>Also: <code>npx cc-momentum</code></span>
101
+ </div>
102
+ </div>
103
+ <div class="footer" id="footerMain">
104
+ <a href="https://github.com/yurukusa/cc-momentum" target="_blank">GitHub</a> ·
105
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> ·
106
+ <span>Part of 106 free tools for Claude Code</span>
107
+ </div>
108
+ <script>
109
+ const dropZone = document.getElementById('dropZone');
110
+ const fi = document.getElementById('fi');
111
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
112
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
113
+ dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(Array.from(e.dataTransfer.files)); });
114
+ fi.addEventListener('change', e => processFiles(Array.from(e.target.files)));
115
+
116
+ function getISOWeek(date) {
117
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
118
+ const day = d.getUTCDay() || 7;
119
+ d.setUTCDate(d.getUTCDate() + 4 - day);
120
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
121
+ const week = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
122
+ return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
123
+ }
124
+
125
+ function classify(trendPct) {
126
+ if (trendPct >= 50) return { label: '🚀 Accelerating', color: 'var(--green)' };
127
+ if (trendPct >= 20) return { label: '📈 Growing', color: 'var(--green)' };
128
+ if (trendPct >= -20) return { label: '→ Stable', color: 'var(--yellow)' };
129
+ if (trendPct >= -50) return { label: '📉 Declining', color: 'var(--orange)' };
130
+ return { label: '⬇️ Sharply Declining', color: 'var(--red)' };
131
+ }
132
+
133
+ async function processFiles(files) {
134
+ const sessionFiles = files.filter(f => {
135
+ const p = f.webkitRelativePath;
136
+ return p.endsWith('.jsonl') && p.includes('projects') && !p.includes('/subagents/');
137
+ });
138
+ if (!sessionFiles.length) {
139
+ alert('No session files found.\nSelect your .claude folder (not a subfolder).');
140
+ return;
141
+ }
142
+
143
+ const weekCounts = new Map();
144
+ for (const f of sessionFiles) {
145
+ try {
146
+ const text = await f.slice(0, 4096).text();
147
+ for (const line of text.split('\n')) {
148
+ if (!line.trim()) continue;
149
+ try {
150
+ const d = JSON.parse(line);
151
+ if (d.timestamp) {
152
+ const w = getISOWeek(new Date(d.timestamp));
153
+ weekCounts.set(w, (weekCounts.get(w) || 0) + 1);
154
+ break;
155
+ }
156
+ } catch {}
157
+ }
158
+ } catch {}
159
+ }
160
+
161
+ if (weekCounts.size < 2) {
162
+ alert('Not enough data to compute weekly trend.');
163
+ return;
164
+ }
165
+
166
+ const allWeeks = [...weekCounts.keys()].sort();
167
+ const currentWeek = getISOWeek(new Date());
168
+ const weeks = allWeeks.map(k => ({ key: k, count: weekCounts.get(k) || 0 }));
169
+ const completeWeeks = weeks[weeks.length - 1]?.key === currentWeek ? weeks.slice(0, -1) : weeks;
170
+
171
+ const recent3 = completeWeeks.slice(-3);
172
+ const prev3 = completeWeeks.slice(-6, -3);
173
+ const recentAvg = recent3.reduce((s, w) => s + w.count, 0) / Math.max(recent3.length, 1);
174
+ const prevAvg = prev3.reduce((s, w) => s + w.count, 0) / Math.max(prev3.length, 1);
175
+ const trendPct = prevAvg > 0 ? ((recentAvg - prevAvg) / prevAvg * 100) : 0;
176
+ const peakWeek = weeks.reduce((a, b) => a.count >= b.count ? a : b);
177
+ const total = weeks.reduce((s, w) => s + w.count, 0);
178
+
179
+ document.getElementById('metaLine').textContent = `${total} sessions · ${weeks.length} weeks`;
180
+ dropZone.style.display = 'none';
181
+ document.getElementById('footerMain').style.display = 'none';
182
+ document.getElementById('results').style.display = 'block';
183
+
184
+ const displayWeeks = weeks.slice(-16); // show last 16 weeks max
185
+ const maxCount = Math.max(...displayWeeks.map(w => w.count), 1);
186
+
187
+ document.getElementById('chart').innerHTML = displayWeeks.map(w => {
188
+ const pct = (w.count / maxCount * 100).toFixed(0);
189
+ const isPeak = w.key === peakWeek.key;
190
+ const isCurrent = w.key === currentWeek;
191
+ const cls = isPeak ? 'peak' : isCurrent ? 'current' : w.count > recentAvg * 1.2 ? 'above' : 'normal';
192
+ const suffix = isPeak ? ' ◀ peak' : isCurrent ? ' (in progress)' : '';
193
+ return `<div class="week-row">
194
+ <span class="week-label ${isPeak ? 'peak' : ''}">${w.key}</span>
195
+ <div class="bar-wrap"><div class="bar-fill ${cls}" style="width:${pct}%"></div></div>
196
+ <span class="week-count">${w.count}${suffix}</span>
197
+ </div>`;
198
+ }).join('');
199
+
200
+ const cls = classify(trendPct);
201
+ const sign = trendPct >= 0 ? '+' : '';
202
+ document.getElementById('scoreCard').innerHTML = `
203
+ <div class="card-title">Usage momentum</div>
204
+ <div class="trend-label" style="color:${cls.color}">${cls.label}</div>
205
+ <div style="color:var(--muted);font-size:0.8rem">${sign}${Math.round(trendPct)}% vs previous 3 weeks</div>
206
+ <div class="trend-stats">
207
+ <div class="t-stat"><span class="num" style="color:${cls.color}">${Math.round(recentAvg)}</span><span class="lbl">recent avg (3w)</span></div>
208
+ <div class="t-stat"><span class="num" style="color:var(--purple)">${peakWeek.count}</span><span class="lbl">peak week</span></div>
209
+ <div class="t-stat"><span class="num">${total}</span><span class="lbl">total sessions</span></div>
210
+ <div class="t-stat"><span class="num">${weeks.length}</span><span class="lbl">weeks tracked</span></div>
211
+ </div>`;
212
+ }
213
+ </script>
214
+ </body>
215
+ </html>
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "cc-momentum",
3
+ "version": "1.0.0",
4
+ "description": "Week-by-week Claude Code session trend — are you accelerating or declining?",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-momentum": "./cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node cli.mjs"
11
+ },
12
+ "keywords": [
13
+ "claude-code",
14
+ "claude",
15
+ "ai",
16
+ "session",
17
+ "productivity",
18
+ "analytics",
19
+ "trend",
20
+ "cli"
21
+ ],
22
+ "author": "yurukusa",
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ }