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.
- package/README.md +67 -0
- package/cli.mjs +210 -0
- package/index.html +235 -0
- 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
|
+
}
|