cc-night-owl 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 +67 -0
  2. package/cli.mjs +210 -0
  3. package/index.html +235 -0
  4. package/package.json +18 -0
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # cc-night-owl
2
+
3
+ **When does Claude Code actually work?**
4
+
5
+ Scans your `~/.claude` session logs and shows which hours of the day your Claude Code sessions start. Find out if you (or your AI) are a night owl, morning person, or something in between.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx cc-night-owl
11
+ ```
12
+
13
+ ## Example Output
14
+
15
+ ```
16
+ cc-night-owl โ€” when does your Claude Code session start? (Asia/Tokyo)
17
+
18
+ 3481 sessions (all time)
19
+
20
+ 00:00 ๐ŸŒ™ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 243
21
+ 01:00 ๐ŸŒ™ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 192
22
+ ...
23
+ 22:00 ๐ŸŒ™ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ 324
24
+ 23:00 ๐ŸŒ™ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 344
25
+
26
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
27
+ Night sessions (22:00โ€“05:59): 42.5% ๐ŸŒ™ Night Owl
28
+ Peak hour: 23:00 (344 sessions)
29
+
30
+ Period breakdown:
31
+ Night (22:00โ€“05:59) 1480 sessions (42.5%)
32
+ Morning (06:00โ€“11:59) 420 sessions (12.1%)
33
+ Afternoon (12:00โ€“17:59) 656 sessions (18.8%)
34
+ Evening (18:00โ€“21:59) 925 sessions (26.6%)
35
+ ```
36
+
37
+ ## Options
38
+
39
+ ```bash
40
+ npx cc-night-owl # All-time hourly breakdown
41
+ npx cc-night-owl --days=30 # Last 30 days only
42
+ npx cc-night-owl --utc # Show UTC instead of local time
43
+ npx cc-night-owl --json # JSON output for piping
44
+ ```
45
+
46
+ ## Night Owl Score
47
+
48
+ Based on the % of sessions that start between 22:00 and 05:59 (local time):
49
+
50
+ - **Full Night Owl** โ€” 50%+ of sessions
51
+ - **Night Owl** โ€” 30โ€“50%
52
+ - **Evening Worker** โ€” 15โ€“30%
53
+ - **Day Coder** โ€” under 15%
54
+
55
+ ## Browser Version
56
+
57
+ Drag your `~/.claude` folder into: **[yurukusa.github.io/cc-night-owl](https://yurukusa.github.io/cc-night-owl/)**
58
+
59
+ No install, no upload. Everything runs locally in your browser.
60
+
61
+ ## Part of cc-toolkit
62
+
63
+ cc-night-owl is part of [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) โ€” 48 free tools for Claude Code users.
64
+
65
+ ---
66
+
67
+ MIT License. Zero dependencies. Your data stays local.
package/cli.mjs ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cc-night-owl โ€” When does Claude Code actually work?
4
+ *
5
+ * Shows hourly session distribution across all your Claude Code sessions.
6
+ * Are you (or your AI) most active at 3am? Reveals your true work patterns.
7
+ *
8
+ * Zero dependencies. Node.js 18+. ESM.
9
+ *
10
+ * Usage:
11
+ * npx cc-night-owl # All-time hourly breakdown
12
+ * npx cc-night-owl --days=30 # Last 30 days
13
+ * npx cc-night-owl --utc # Show UTC instead of local time
14
+ * npx cc-night-owl --json # JSON output
15
+ */
16
+
17
+ import { readdir, open } from 'node:fs/promises';
18
+ import { join } from 'node:path';
19
+ import { homedir } from 'node:os';
20
+
21
+ const HOME = homedir();
22
+ const PROJECTS_DIR = join(HOME, '.claude', 'projects');
23
+ const MAX_CHUNK = 4 * 1024; // 4KB โ€” enough for first timestamp
24
+
25
+ // โ”€โ”€ CLI args โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
+ const args = process.argv.slice(2);
27
+ const helpFlag = args.includes('--help') || args.includes('-h');
28
+ const jsonFlag = args.includes('--json');
29
+ const utcFlag = args.includes('--utc');
30
+ const daysArg = parseInt(args.find(a => a.startsWith('--days='))?.slice(7) ?? '0') || 0;
31
+
32
+ if (helpFlag) {
33
+ console.log(`cc-night-owl โ€” When does Claude Code actually work?
34
+
35
+ Usage:
36
+ npx cc-night-owl All-time hourly breakdown
37
+ npx cc-night-owl --days=30 Last 30 days only
38
+ npx cc-night-owl --utc Show UTC instead of local time
39
+ npx cc-night-owl --json JSON output
40
+
41
+ Output:
42
+ 24-hour bar chart with night owl score (% sessions between 22:00-05:59 local)
43
+ `);
44
+ process.exit(0);
45
+ }
46
+
47
+ // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
48
+ async function collectJsonlFiles(dir) {
49
+ const files = [];
50
+ try {
51
+ const entries = await readdir(dir, { withFileTypes: true });
52
+ for (const e of entries) {
53
+ const p = join(dir, e.name);
54
+ if (e.isDirectory()) files.push(...await collectJsonlFiles(p));
55
+ else if (e.name.endsWith('.jsonl')) files.push(p);
56
+ }
57
+ } catch {}
58
+ return files;
59
+ }
60
+
61
+ async function readFirstChunk(path) {
62
+ let fh;
63
+ try {
64
+ fh = await open(path, 'r');
65
+ const buf = Buffer.alloc(MAX_CHUNK);
66
+ const { bytesRead } = await fh.read(buf, 0, MAX_CHUNK, 0);
67
+ return buf.subarray(0, bytesRead).toString('utf8');
68
+ } catch { return ''; }
69
+ finally { await fh?.close(); }
70
+ }
71
+
72
+ function extractFirstTimestamp(text) {
73
+ for (const line of text.split('\n')) {
74
+ if (!line.trim()) continue;
75
+ try {
76
+ const d = JSON.parse(line);
77
+ if (d.timestamp) return new Date(d.timestamp);
78
+ } catch {}
79
+ }
80
+ return null;
81
+ }
82
+
83
+ function getLocalHour(date) {
84
+ // Use local timezone offset
85
+ return date.getHours();
86
+ }
87
+
88
+ function getUtcHour(date) {
89
+ return date.getUTCHours();
90
+ }
91
+
92
+ // โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
93
+ const files = await collectJsonlFiles(PROJECTS_DIR);
94
+ const cutoff = daysArg > 0 ? Date.now() - daysArg * 86400000 : 0;
95
+
96
+ const hourCounts = new Array(24).fill(0);
97
+ let totalSessions = 0;
98
+ let earliest = null, latest = null;
99
+
100
+ for (const f of files) {
101
+ const chunk = await readFirstChunk(f);
102
+ const ts = extractFirstTimestamp(chunk);
103
+ if (!ts) continue;
104
+ if (cutoff && ts.getTime() < cutoff) continue;
105
+
106
+ const hour = utcFlag ? getUtcHour(ts) : getLocalHour(ts);
107
+ hourCounts[hour]++;
108
+ totalSessions++;
109
+ if (!earliest || ts < earliest) earliest = ts;
110
+ if (!latest || ts > latest) latest = ts;
111
+ }
112
+
113
+ if (jsonFlag) {
114
+ const tz = utcFlag ? 'UTC' : Intl.DateTimeFormat().resolvedOptions().timeZone;
115
+ const nightHours = new Set([22, 23, 0, 1, 2, 3, 4, 5]);
116
+ const nightSessions = [...nightHours].reduce((s, h) => s + hourCounts[h], 0);
117
+ const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
118
+ process.stdout.write(JSON.stringify({
119
+ total_sessions: totalSessions,
120
+ timezone: tz,
121
+ days_range: daysArg || null,
122
+ night_owl_score: totalSessions ? (nightSessions / totalSessions * 100).toFixed(1) : '0.0',
123
+ peak_hour: peakHour,
124
+ hours: hourCounts.map((count, h) => ({ hour: h, count }))
125
+ }, null, 2) + '\n');
126
+ process.exit(0);
127
+ }
128
+
129
+ // โ”€โ”€ Terminal output โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
130
+ const BAR_WIDTH = 28;
131
+ const maxVal = Math.max(...hourCounts, 1);
132
+ const tz = utcFlag ? 'UTC' : Intl.DateTimeFormat().resolvedOptions().timeZone;
133
+
134
+ const RESET = '\x1b[0m';
135
+ const BOLD = '\x1b[1m';
136
+ const DIM = '\x1b[2m';
137
+ const CYAN = '\x1b[36m';
138
+ const BLUE = '\x1b[34m';
139
+ const YELLOW = '\x1b[33m';
140
+ const ORANGE = '\x1b[38;5;208m';
141
+ const PURPLE = '\x1b[35m';
142
+ const MUTED = '\x1b[38;5;240m';
143
+
144
+ function colorBar(hour, filledCount) {
145
+ if (22 <= hour || hour < 6) return PURPLE; // night
146
+ if (6 <= hour && hour < 12) return YELLOW; // morning
147
+ if (12 <= hour && hour < 18) return CYAN; // afternoon
148
+ return ORANGE; // evening
149
+ }
150
+
151
+ function period(hour) {
152
+ if (23 === hour || hour < 4) return '๐ŸŒ™';
153
+ if (hour < 6) return '๐ŸŒ•';
154
+ if (hour < 12) return '๐ŸŒ…';
155
+ if (hour < 18) return 'โ˜€๏ธ ';
156
+ if (hour < 22) return '๐ŸŒ†';
157
+ return '๐ŸŒ™';
158
+ }
159
+
160
+ console.log(`\n${BOLD}cc-night-owl${RESET}${MUTED} โ€” when does your Claude Code session start? (${tz})${RESET}\n`);
161
+
162
+ const daysLabel = daysArg ? ` (last ${daysArg} days)` : ' (all time)';
163
+ console.log(`${MUTED}${totalSessions} sessions${daysLabel}${RESET}\n`);
164
+
165
+ for (let h = 0; h < 24; h++) {
166
+ const count = hourCounts[h];
167
+ const filled = Math.round(count / maxVal * BAR_WIDTH);
168
+ const empty = BAR_WIDTH - filled;
169
+ const barColor = colorBar(h, filled);
170
+ const bar = barColor + 'โ–ˆ'.repeat(filled) + RESET + MUTED + 'โ–‘'.repeat(empty) + RESET;
171
+ const isNight = (22 <= h || h < 6);
172
+ const label = `${h.toString().padStart(2, '0')}:00`;
173
+ const countStr = count.toString().padStart(4);
174
+ const highlight = isNight ? PURPLE : DIM;
175
+ console.log(` ${highlight}${label}${RESET} ${period(h)} ${bar} ${MUTED}${countStr}${RESET}`);
176
+ }
177
+
178
+ // โ”€โ”€ Night owl score โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
179
+ const nightHours = [22, 23, 0, 1, 2, 3, 4, 5];
180
+ const nightSessions = nightHours.reduce((s, h) => s + hourCounts[h], 0);
181
+ const nightPct = totalSessions ? (nightSessions / totalSessions * 100) : 0;
182
+ const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
183
+ const peakLabel = `${peakHour.toString().padStart(2, '0')}:00`;
184
+
185
+ let owlLabel, owlColor;
186
+ if (nightPct >= 50) { owlLabel = '๐Ÿฆ‰ Full Night Owl'; owlColor = PURPLE; }
187
+ else if (nightPct >= 30) { owlLabel = '๐ŸŒ™ Night Owl'; owlColor = BLUE; }
188
+ else if (nightPct >= 15) { owlLabel = '๐ŸŒ† Evening Worker'; owlColor = ORANGE; }
189
+ else { owlLabel = 'โ˜€๏ธ Day Coder'; owlColor = YELLOW; }
190
+
191
+ console.log(`\n${MUTED}โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€${RESET}`);
192
+ console.log(` ${BOLD}Night sessions${RESET}${MUTED} (22:00โ€“05:59): ${RESET}${owlColor}${nightPct.toFixed(1)}% ${owlLabel}${RESET}`);
193
+ console.log(` ${BOLD}Peak hour${RESET}${MUTED}: ${RESET}${CYAN}${peakLabel}${RESET}${MUTED} (${hourCounts[peakHour]} sessions)${RESET}`);
194
+
195
+ // Period breakdown
196
+ const periods = [
197
+ { name: 'Night (22:00โ€“05:59)', hours: [22,23,0,1,2,3,4,5], color: PURPLE },
198
+ { name: 'Morning (06:00โ€“11:59)', hours: [6,7,8,9,10,11], color: YELLOW },
199
+ { name: 'Afternoon (12:00โ€“17:59)', hours: [12,13,14,15,16,17], color: CYAN },
200
+ { name: 'Evening (18:00โ€“21:59)', hours: [18,19,20,21], color: ORANGE },
201
+ ];
202
+ console.log(`\n${MUTED} Period breakdown:${RESET}`);
203
+ for (const p of periods) {
204
+ const cnt = p.hours.reduce((s, h) => s + hourCounts[h], 0);
205
+ const pct = totalSessions ? (cnt / totalSessions * 100).toFixed(1) : '0.0';
206
+ const barLen = Math.round(parseFloat(pct) / 5);
207
+ console.log(` ${p.color}${p.name.padEnd(28)}${RESET}${MUTED} ${cnt.toString().padStart(4)} sessions (${pct}%)${RESET}`);
208
+ }
209
+
210
+ console.log(`\n${MUTED} Tip: npx cc-night-owl --days=7 for recent activity${RESET}\n`);
package/index.html ADDED
@@ -0,0 +1,235 @@
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-night-owl โ€” When does Claude Code actually work?</title>
7
+ <meta name="description" content="Find out when your Claude Code sessions happen. Drag in your ~/.claude folder to see your hourly activity distribution and night owl score.">
8
+ <meta property="og:title" content="cc-night-owl โ€” Hourly session heatmap for Claude Code">
9
+ <meta property="og:description" content="Is your AI a night owl? See which hours Claude Code is most active across all your sessions.">
10
+ <meta property="og:url" content="https://yurukusa.github.io/cc-night-owl/">
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
+ --night: #bc8cff; --morning: #f7c948; --afternoon: #58d8f0; --evening: #ffa657;
19
+ --cyan: #58d8f0; --blue: #58a6ff; --red: #ff7b72; --green: #56d364;
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(--night); 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(--night); background: #1a0f2e; }
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
+ .chart-container {
44
+ background: var(--surface); border: 1px solid var(--border);
45
+ border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem;
46
+ }
47
+ .chart-title { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1rem; }
48
+ .hour-row {
49
+ display: flex; align-items: center; gap: 0.5rem;
50
+ margin-bottom: 0.3rem; font-size: 0.78rem;
51
+ }
52
+ .hour-label { color: var(--muted); min-width: 3rem; font-family: Consolas, monospace; }
53
+ .hour-label.night { color: var(--night); }
54
+ .hour-emoji { min-width: 1.5rem; text-align: center; }
55
+ .bar-wrap { flex: 1; height: 14px; background: #21262d; border-radius: 3px; overflow: hidden; }
56
+ .bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s ease; }
57
+ .bar-fill.night { background: var(--night); }
58
+ .bar-fill.morning { background: var(--morning); }
59
+ .bar-fill.afternoon { background: var(--afternoon); }
60
+ .bar-fill.evening { background: var(--evening); }
61
+ .hour-count { color: var(--muted); min-width: 3rem; text-align: right; font-family: Consolas, monospace; }
62
+ .score-card {
63
+ background: var(--surface); border: 1px solid var(--border);
64
+ border-radius: 8px; padding: 1.25rem; text-align: center; margin-bottom: 1.5rem;
65
+ }
66
+ .score-title { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1rem; }
67
+ .owl-label { font-size: 1.5rem; font-weight: 700; color: var(--night); margin-bottom: 0.5rem; }
68
+ .owl-pct { font-size: 2.5rem; font-weight: 700; color: var(--night); }
69
+ .period-grid {
70
+ display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 1rem;
71
+ }
72
+ .period-stat { text-align: center; }
73
+ .period-stat .num { font-size: 1.2rem; font-weight: 700; display: block; }
74
+ .period-stat .lbl { font-size: 0.7rem; color: var(--muted); }
75
+ .summary-stats { display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap; margin-top: 1rem; }
76
+ .s-stat .num { font-size: 1.5rem; font-weight: 700; color: var(--night); display: block; }
77
+ .s-stat .lbl { font-size: 0.75rem; color: var(--muted); }
78
+ .footer { margin-top: 2rem; text-align: center; font-size: 0.8rem; color: var(--muted); }
79
+ .footer a { color: var(--blue); text-decoration: none; margin: 0 0.5rem; }
80
+ .btn {
81
+ background: var(--night); color: #0d1117; border: none;
82
+ border-radius: 6px; padding: 0.6rem 1.5rem; font-size: 0.9rem;
83
+ font-weight: 600; cursor: pointer; margin-top: 1rem; font-family: inherit;
84
+ }
85
+ .btn:hover { opacity: 0.85; }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <div class="header">
90
+ <h1>cc-night-owl</h1>
91
+ <p>When does Claude Code actually work?</p>
92
+ </div>
93
+ <div class="drop-zone" id="dropZone" onclick="document.getElementById('fi').click()">
94
+ <div class="icon">๐Ÿฆ‰</div>
95
+ <h2>Drop your .claude folder here</h2>
96
+ <p>Or click to select it.<br>Your data stays local โ€” nothing is uploaded.</p>
97
+ <input type="file" id="fi" webkitdirectory multiple>
98
+ <button class="btn" onclick="event.stopPropagation(); document.getElementById('fi').click()">
99
+ Select ~/.claude folder
100
+ </button>
101
+ </div>
102
+ <div id="results">
103
+ <div class="meta" id="metaLine"></div>
104
+ <div class="score-card" id="scoreCard"></div>
105
+ <div class="chart-container">
106
+ <div class="chart-title">Sessions by hour of day (local time)</div>
107
+ <div id="chart"></div>
108
+ </div>
109
+ <div class="footer">
110
+ <a href="https://github.com/yurukusa/cc-night-owl" target="_blank">GitHub</a> ยท
111
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit (106 free tools)</a> ยท
112
+ <span>Also: <code>npx cc-night-owl</code></span>
113
+ </div>
114
+ </div>
115
+ <div class="footer" id="footerMain">
116
+ <a href="https://github.com/yurukusa/cc-night-owl" target="_blank">GitHub</a> ยท
117
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> ยท
118
+ <span>Part of 106 free tools for Claude Code</span>
119
+ </div>
120
+ <script>
121
+ const dropZone = document.getElementById('dropZone');
122
+ const fi = document.getElementById('fi');
123
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
124
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
125
+ dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(Array.from(e.dataTransfer.files)); });
126
+ fi.addEventListener('change', e => processFiles(Array.from(e.target.files)));
127
+
128
+ async function processFiles(files) {
129
+ const jsonlFiles = files.filter(f => f.name.endsWith('.jsonl') && f.webkitRelativePath.includes('projects'));
130
+ if (!jsonlFiles.length) { alert('No session files found.\nSelect your .claude folder (not a subfolder).'); return; }
131
+
132
+ const hourCounts = new Array(24).fill(0);
133
+ let total = 0;
134
+
135
+ for (const f of jsonlFiles) {
136
+ try {
137
+ const text = await f.slice(0, 4096).text();
138
+ for (const line of text.split('\n')) {
139
+ if (!line.trim()) continue;
140
+ try {
141
+ const d = JSON.parse(line);
142
+ if (d.timestamp) {
143
+ const dt = new Date(d.timestamp);
144
+ const hour = dt.getHours(); // local time
145
+ hourCounts[hour]++;
146
+ total++;
147
+ break;
148
+ }
149
+ } catch {}
150
+ }
151
+ } catch {}
152
+ }
153
+
154
+ document.getElementById('metaLine').textContent = `${jsonlFiles.length} session files ยท ${total} sessions analyzed`;
155
+ dropZone.style.display = 'none';
156
+ document.getElementById('footerMain').style.display = 'none';
157
+ document.getElementById('results').style.display = 'block';
158
+ renderChart(hourCounts, total);
159
+ }
160
+
161
+ function getBarClass(h) {
162
+ if (h >= 22 || h < 6) return 'night';
163
+ if (h < 12) return 'morning';
164
+ if (h < 18) return 'afternoon';
165
+ return 'evening';
166
+ }
167
+
168
+ function getEmoji(h) {
169
+ if (h === 23 || h < 4) return '๐ŸŒ™';
170
+ if (h < 6) return '๐ŸŒ•';
171
+ if (h < 12) return '๐ŸŒ…';
172
+ if (h < 18) return 'โ˜€๏ธ';
173
+ if (h < 22) return '๐ŸŒ†';
174
+ return '๐ŸŒ™';
175
+ }
176
+
177
+ function renderChart(hourCounts, total) {
178
+ const maxVal = Math.max(...hourCounts, 1);
179
+ const chart = document.getElementById('chart');
180
+ chart.innerHTML = hourCounts.map((cnt, h) => {
181
+ const pct = (cnt / maxVal * 100).toFixed(0);
182
+ const cls = getBarClass(h);
183
+ const isNight = (h >= 22 || h < 6);
184
+ return `<div class="hour-row">
185
+ <span class="hour-label ${isNight ? 'night' : ''}">${h.toString().padStart(2,'0')}:00</span>
186
+ <span class="hour-emoji">${getEmoji(h)}</span>
187
+ <div class="bar-wrap"><div class="bar-fill ${cls}" style="width:${pct}%"></div></div>
188
+ <span class="hour-count">${cnt}</span>
189
+ </div>`;
190
+ }).join('');
191
+
192
+ // Night owl score
193
+ const nightHours = [22,23,0,1,2,3,4,5];
194
+ const nightSessions = nightHours.reduce((s,h) => s + hourCounts[h], 0);
195
+ const nightPct = total ? (nightSessions / total * 100) : 0;
196
+ const peakHour = hourCounts.indexOf(Math.max(...hourCounts));
197
+
198
+ let owlLabel, owlColor;
199
+ if (nightPct >= 50) { owlLabel = '๐Ÿฆ‰ Full Night Owl'; owlColor = 'var(--night)'; }
200
+ else if (nightPct >= 30) { owlLabel = '๐ŸŒ™ Night Owl'; owlColor = 'var(--blue)'; }
201
+ else if (nightPct >= 15) { owlLabel = '๐ŸŒ† Evening Worker'; owlColor = 'var(--evening)'; }
202
+ else { owlLabel = 'โ˜€๏ธ Day Coder'; owlColor = 'var(--morning)'; }
203
+
204
+ // Period counts
205
+ const periods = [
206
+ { name: 'Night (22โ€“06)', hours: [22,23,0,1,2,3,4,5], color: 'var(--night)' },
207
+ { name: 'Morning (06โ€“12)', hours: [6,7,8,9,10,11], color: 'var(--morning)' },
208
+ { name: 'Afternoon (12โ€“18)', hours: [12,13,14,15,16,17], color: 'var(--afternoon)' },
209
+ { name: 'Evening (18โ€“22)', hours: [18,19,20,21], color: 'var(--evening)' },
210
+ ];
211
+
212
+ document.getElementById('scoreCard').innerHTML = `
213
+ <div class="score-title">Night Owl Analysis</div>
214
+ <div class="owl-pct" style="color:${owlColor}">${nightPct.toFixed(1)}%</div>
215
+ <div class="owl-label" style="color:${owlColor}">${owlLabel}</div>
216
+ <div style="color:var(--muted);font-size:0.8rem;margin-top:0.5rem">sessions between 22:00โ€“05:59</div>
217
+ <div class="period-grid" style="margin-top:1rem">
218
+ ${periods.map(p => {
219
+ const cnt = p.hours.reduce((s,h) => s + hourCounts[h], 0);
220
+ const pct = total ? (cnt / total * 100).toFixed(1) : '0.0';
221
+ return `<div class="period-stat">
222
+ <span class="num" style="color:${p.color}">${pct}%</span>
223
+ <span class="lbl">${p.name}</span>
224
+ </div>`;
225
+ }).join('')}
226
+ </div>
227
+ <div class="summary-stats">
228
+ <div class="s-stat"><span class="num" style="color:${owlColor}">${peakHour.toString().padStart(2,'0')}:00</span><span class="lbl">peak hour</span></div>
229
+ <div class="s-stat"><span class="num">${total}</span><span class="lbl">total sessions</span></div>
230
+ <div class="s-stat"><span class="num" style="color:var(--night)">${nightSessions}</span><span class="lbl">night sessions</span></div>
231
+ </div>`;
232
+ }
233
+ </script>
234
+ </body>
235
+ </html>
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "cc-night-owl",
3
+ "version": "1.0.0",
4
+ "description": "When does Claude Code actually work? Hourly session distribution from ~/.claude logs.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-night-owl": "./cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node cli.mjs"
11
+ },
12
+ "keywords": ["claude", "claude-code", "ai", "productivity", "analytics", "cli"],
13
+ "author": "yurukusa",
14
+ "license": "MIT",
15
+ "engines": {
16
+ "node": ">=18"
17
+ }
18
+ }