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.
- package/README.md +74 -0
- package/cli.mjs +198 -0
- package/index.html +215 -0
- 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
|
+
}
|