vibemon 1.5.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Shared configuration for Vibe Monitor (CommonJS)
3
+ * App settings from constants.json
4
+ * Rendering data is in vibemon-engine-standalone.js
5
+ *
6
+ * Constants are in constants.cjs - re-exported here for convenience
7
+ */
8
+
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ // Re-export all constants for backward compatibility
13
+ const constants = require('./constants.cjs');
14
+
15
+ // =============================================================================
16
+ // WebSocket Configuration (from environment variables)
17
+ // =============================================================================
18
+ const WS_URL = process.env.VIBEMON_WS_URL || 'wss://ws.vibemon.io';
19
+ const WS_TOKEN = process.env.VIBEMON_WS_TOKEN || null;
20
+
21
+ // =============================================================================
22
+ // Paths
23
+ // =============================================================================
24
+ const STATS_CACHE_PATH = path.join(os.homedir(), '.claude', 'stats-cache.json');
25
+
26
+ // =============================================================================
27
+ // State & Character Data (from constants.json)
28
+ // =============================================================================
29
+
30
+ // Directly from constants.json
31
+ const {
32
+ VALID_STATES,
33
+ STATE_COLORS,
34
+ CHARACTER_NAMES,
35
+ CHARACTER_COLORS
36
+ } = constants;
37
+
38
+ // Derived CHARACTER_CONFIG for backward compatibility
39
+ const CHARACTER_CONFIG = Object.fromEntries(
40
+ CHARACTER_NAMES.map(name => [name, {
41
+ name,
42
+ color: CHARACTER_COLORS[name]
43
+ }])
44
+ );
45
+
46
+ module.exports = {
47
+ // Re-export all constants
48
+ ...constants,
49
+
50
+ // Paths
51
+ STATS_CACHE_PATH,
52
+
53
+ // State data (from constants.json)
54
+ VALID_STATES,
55
+ STATE_COLORS,
56
+
57
+ // Character data (from constants.json)
58
+ CHARACTER_CONFIG,
59
+ CHARACTER_NAMES,
60
+
61
+ // WebSocket
62
+ WS_URL,
63
+ WS_TOKEN
64
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared constants for Vibe Monitor (CommonJS)
3
+ * Loads from single source of truth: data/constants.json
4
+ */
5
+
6
+ const constants = require('./data/constants.json');
7
+
8
+ module.exports = constants;
@@ -0,0 +1,86 @@
1
+ {
2
+ "HTTP_PORT": 19280,
3
+ "MAX_PAYLOAD_SIZE": 10240,
4
+
5
+ "WINDOW_WIDTH": 172,
6
+ "WINDOW_HEIGHT": 348,
7
+ "SNAP_THRESHOLD": 30,
8
+ "SNAP_DEBOUNCE": 150,
9
+ "WINDOW_GAP": 10,
10
+ "MAX_WINDOWS": 5,
11
+ "MAX_PROJECT_LIST": 10,
12
+
13
+ "IDLE_TIMEOUT": 60000,
14
+ "SLEEP_TIMEOUT": 300000,
15
+ "WINDOW_CLOSE_TIMEOUT": 600000,
16
+
17
+ "TRAY_ICON_SIZE": 22,
18
+
19
+ "DEFAULT_CHARACTER": "clawd",
20
+ "CHAR_SIZE": 128,
21
+ "SCALE": 2,
22
+
23
+ "COLOR_EYE": "#000000",
24
+ "COLOR_WHITE": "#FFFFFF",
25
+
26
+ "FLOAT_AMPLITUDE_X": 3,
27
+ "FLOAT_AMPLITUDE_Y": 5,
28
+ "CHAR_X_BASE": 22,
29
+ "CHAR_Y_BASE": 20,
30
+
31
+ "FRAME_INTERVAL": 100,
32
+ "FLOAT_CYCLE_FRAMES": 32,
33
+ "LOADING_DOT_COUNT": 4,
34
+ "THINKING_ANIMATION_SLOWDOWN": 3,
35
+ "BLINK_START_FRAME": 30,
36
+ "BLINK_END_FRAME": 31,
37
+
38
+ "PROJECT_NAME_MAX_LENGTH": 20,
39
+ "PROJECT_NAME_TRUNCATE_AT": 17,
40
+ "MODEL_NAME_MAX_LENGTH": 14,
41
+ "MODEL_NAME_TRUNCATE_AT": 11,
42
+
43
+ "MATRIX_STREAM_DENSITY": 0.7,
44
+ "MATRIX_SPEED_MIN": 1,
45
+ "MATRIX_SPEED_MAX": 6,
46
+ "MATRIX_COLUMN_WIDTH": 4,
47
+ "MATRIX_FLICKER_PERIOD": 3,
48
+ "MATRIX_TAIL_LENGTH_FAST": 8,
49
+ "MATRIX_TAIL_LENGTH_SLOW": 6,
50
+
51
+ "LOCK_MODES": {
52
+ "first-project": "First Project",
53
+ "on-thinking": "On Thinking"
54
+ },
55
+
56
+ "ALWAYS_ON_TOP_MODES": {
57
+ "active-only": "Active Only",
58
+ "all": "All Windows",
59
+ "disabled": "Disabled"
60
+ },
61
+
62
+ "ACTIVE_STATES": ["thinking", "planning", "working", "notification", "packing"],
63
+
64
+ "VALID_STATES": ["start", "idle", "thinking", "planning", "working", "packing", "notification", "sleep", "done"],
65
+
66
+ "CHARACTER_NAMES": ["apto", "clawd", "kiro", "claw"],
67
+
68
+ "CHARACTER_COLORS": {
69
+ "apto": "#797C98",
70
+ "clawd": "#D97757",
71
+ "kiro": "#FFFFFF",
72
+ "claw": "#DD4444"
73
+ },
74
+
75
+ "STATE_COLORS": {
76
+ "start": "#00CCCC",
77
+ "idle": "#00AA00",
78
+ "thinking": "#9933FF",
79
+ "planning": "#008888",
80
+ "working": "#0066CC",
81
+ "packing": "#AAAAAA",
82
+ "notification": "#FFCC00",
83
+ "sleep": "#111144",
84
+ "done": "#00AA00"
85
+ }
86
+ }
package/stats.html ADDED
@@ -0,0 +1,521 @@
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>Claude Code Stats</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ html, body {
15
+ background: transparent;
16
+ width: 640px;
17
+ height: 475px;
18
+ overflow: hidden;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Söhne', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
23
+ color: #1C1917;
24
+ }
25
+
26
+ .window {
27
+ width: 640px;
28
+ height: 475px;
29
+ background: #FAF9F6;
30
+ border-radius: 12px;
31
+ overflow: hidden;
32
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
33
+ }
34
+
35
+ .title-bar {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ padding: 10px 16px;
40
+ background: #D97706;
41
+ -webkit-app-region: drag;
42
+ cursor: grab;
43
+ }
44
+
45
+ .title-bar h1 {
46
+ font-size: 0.9rem;
47
+ font-weight: 600;
48
+ color: #FFFFFF;
49
+ margin: 0;
50
+ }
51
+
52
+ .close-btn {
53
+ -webkit-app-region: no-drag;
54
+ width: 20px;
55
+ height: 20px;
56
+ border: none;
57
+ border-radius: 50%;
58
+ background: rgba(255, 255, 255, 0.3);
59
+ color: #FFFFFF;
60
+ font-size: 14px;
61
+ cursor: pointer;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ transition: background 0.2s;
66
+ }
67
+
68
+ .close-btn:hover {
69
+ background: rgba(255, 255, 255, 0.5);
70
+ }
71
+
72
+ .content {
73
+ padding: 12px 16px;
74
+ height: calc(475px - 40px);
75
+ overflow: hidden;
76
+ }
77
+
78
+ /* Summary Cards */
79
+ .summary-grid {
80
+ display: grid;
81
+ grid-template-columns: repeat(6, 1fr);
82
+ gap: 8px;
83
+ margin-bottom: 12px;
84
+ }
85
+
86
+ .summary-card {
87
+ background: #FFFFFF;
88
+ border: 1px solid #E7E5E4;
89
+ border-radius: 8px;
90
+ padding: 8px 6px;
91
+ text-align: center;
92
+ }
93
+
94
+ .summary-card .value {
95
+ font-size: 1.1rem;
96
+ font-weight: 700;
97
+ margin-bottom: 2px;
98
+ }
99
+
100
+ .summary-card .label {
101
+ font-size: 0.6rem;
102
+ color: #78716C;
103
+ text-transform: uppercase;
104
+ letter-spacing: 0.5px;
105
+ }
106
+
107
+ .summary-card.sessions .value { color: #D97706; }
108
+ .summary-card.messages .value { color: #9333EA; }
109
+ .summary-card.tools .value { color: #059669; }
110
+ .summary-card.tokens .value { color: #DC2626; }
111
+ .summary-card.days .value { color: #2563EB; }
112
+ .summary-card.longest .value { color: #EA580C; }
113
+
114
+ /* Contributions Graph */
115
+ .contributions-section {
116
+ background: #FFFFFF;
117
+ border: 1px solid #E7E5E4;
118
+ border-radius: 8px;
119
+ padding: 12px;
120
+ margin-bottom: 12px;
121
+ }
122
+
123
+ .contributions-section h2 {
124
+ font-size: 0.75rem;
125
+ margin-bottom: 8px;
126
+ color: #44403C;
127
+ font-weight: 600;
128
+ }
129
+
130
+ .contributions-wrapper {
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 6px;
134
+ }
135
+
136
+ .contributions-graph {
137
+ display: flex;
138
+ gap: 2px;
139
+ overflow: hidden;
140
+ }
141
+
142
+ .contrib-week {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 2px;
146
+ }
147
+
148
+ .contrib-day {
149
+ width: 9px;
150
+ height: 9px;
151
+ border-radius: 2px;
152
+ background: #EBEDF0;
153
+ }
154
+
155
+ .contrib-day.level-1 { background: #FED7AA; }
156
+ .contrib-day.level-2 { background: #FDBA74; }
157
+ .contrib-day.level-3 { background: #F97316; }
158
+ .contrib-day.level-4 { background: #C2410C; }
159
+
160
+ .contrib-legend {
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: flex-end;
164
+ gap: 4px;
165
+ font-size: 0.65rem;
166
+ color: #78716C;
167
+ }
168
+
169
+ .contrib-legend span {
170
+ margin: 0 2px;
171
+ }
172
+
173
+ .legend-box {
174
+ width: 9px;
175
+ height: 9px;
176
+ border-radius: 2px;
177
+ }
178
+
179
+ /* Bottom Row */
180
+ .bottom-row {
181
+ display: grid;
182
+ grid-template-columns: 1fr 1fr;
183
+ gap: 12px;
184
+ height: 180px;
185
+ }
186
+
187
+ .chart-section {
188
+ background: #FFFFFF;
189
+ border: 1px solid #E7E5E4;
190
+ border-radius: 8px;
191
+ padding: 12px;
192
+ display: flex;
193
+ flex-direction: column;
194
+ }
195
+
196
+ .chart-section h2 {
197
+ font-size: 0.75rem;
198
+ margin-bottom: 8px;
199
+ color: #44403C;
200
+ font-weight: 600;
201
+ }
202
+
203
+ .chart-container {
204
+ flex: 1;
205
+ position: relative;
206
+ min-height: 0;
207
+ }
208
+
209
+ /* Model List */
210
+ .model-list {
211
+ font-size: 0.7rem;
212
+ }
213
+
214
+ .model-item {
215
+ display: flex;
216
+ justify-content: space-between;
217
+ padding: 6px 0;
218
+ border-bottom: 1px solid #F5F5F4;
219
+ }
220
+
221
+ .model-item:last-child {
222
+ border-bottom: none;
223
+ }
224
+
225
+ .model-name {
226
+ color: #D97706;
227
+ font-weight: 600;
228
+ }
229
+
230
+ .model-tokens {
231
+ color: #78716C;
232
+ }
233
+
234
+ /* Hour Line Chart */
235
+ .hour-chart {
236
+ width: 100%;
237
+ height: 100%;
238
+ }
239
+
240
+ .hour-chart .line {
241
+ fill: none;
242
+ stroke: #D97706;
243
+ stroke-width: 1;
244
+ stroke-linecap: round;
245
+ stroke-linejoin: round;
246
+ }
247
+
248
+ .hour-chart .area {
249
+ fill: url(#areaGradient);
250
+ }
251
+
252
+ .hour-chart .axis {
253
+ font-size: 9px;
254
+ fill: #A8A29E;
255
+ }
256
+
257
+ .hour-chart .grid {
258
+ stroke: #E7E5E4;
259
+ stroke-width: 1;
260
+ }
261
+
262
+ /* Loading & Error */
263
+ .loading, .error {
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: center;
267
+ height: 100%;
268
+ color: #78716C;
269
+ }
270
+
271
+ .error { color: #DC2626; }
272
+ </style>
273
+ </head>
274
+ <body>
275
+ <div class="window">
276
+ <div class="title-bar">
277
+ <h1>Claude Stats</h1>
278
+ <button class="close-btn" onclick="window.close()">×</button>
279
+ </div>
280
+ <div class="content">
281
+ <div id="content">
282
+ <div class="loading">Loading...</div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+
287
+ <script>
288
+ const COLORS = {
289
+ coral: '#D97706',
290
+ purple: '#9333EA',
291
+ green: '#059669',
292
+ red: '#DC2626',
293
+ blue: '#2563EB'
294
+ };
295
+
296
+ const formatNumber = (num) => {
297
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
298
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
299
+ return num.toLocaleString();
300
+ };
301
+
302
+ const formatDuration = (ms) => {
303
+ const hours = Math.floor(ms / 3600000);
304
+ const minutes = Math.floor((ms % 3600000) / 60000);
305
+ if (hours > 0) return `${hours}h ${minutes}m`;
306
+ return `${minutes}m`;
307
+ };
308
+
309
+ const getModelShortName = (model) => {
310
+ if (model.includes('opus')) return 'Opus 4.5';
311
+ if (model.includes('sonnet')) return 'Sonnet 4.5';
312
+ if (model.includes('haiku')) return 'Haiku';
313
+ return model.split('-').slice(1, 3).join(' ');
314
+ };
315
+
316
+ async function loadStats() {
317
+ try {
318
+ const response = await fetch('/stats/data');
319
+ if (!response.ok) throw new Error('Failed to load');
320
+ const data = await response.json();
321
+ renderStats(data);
322
+ } catch (error) {
323
+ document.getElementById('content').innerHTML = `<div class="error">Failed to load stats</div>`;
324
+ }
325
+ }
326
+
327
+ function renderStats(data) {
328
+ const { dailyActivity, modelUsage, totalSessions, totalMessages, longestSession, firstSessionDate, hourCounts } = data;
329
+
330
+ const totalToolCalls = dailyActivity.reduce((sum, d) => sum + d.toolCallCount, 0);
331
+ const totalTokens = Object.values(modelUsage || {}).reduce((sum, m) => sum + (m.inputTokens || 0) + (m.outputTokens || 0), 0);
332
+ const daysSinceStart = firstSessionDate ? Math.ceil((new Date() - new Date(firstSessionDate)) / 86400000) : 0;
333
+ const activeDays = dailyActivity?.length || 0;
334
+
335
+ document.getElementById('content').innerHTML = `
336
+ <div class="summary-grid">
337
+ <div class="summary-card sessions">
338
+ <div class="value">${formatNumber(totalSessions || 0)}</div>
339
+ <div class="label">Sessions</div>
340
+ </div>
341
+ <div class="summary-card messages">
342
+ <div class="value">${formatNumber(totalMessages || 0)}</div>
343
+ <div class="label">Messages</div>
344
+ </div>
345
+ <div class="summary-card tools">
346
+ <div class="value">${formatNumber(totalToolCalls)}</div>
347
+ <div class="label">Tools</div>
348
+ </div>
349
+ <div class="summary-card tokens">
350
+ <div class="value">${formatNumber(totalTokens)}</div>
351
+ <div class="label">Tokens</div>
352
+ </div>
353
+ <div class="summary-card days">
354
+ <div class="value">${activeDays}</div>
355
+ <div class="label">Days</div>
356
+ </div>
357
+ <div class="summary-card longest">
358
+ <div class="value">${longestSession ? formatDuration(longestSession.duration) : '-'}</div>
359
+ <div class="label">Longest</div>
360
+ </div>
361
+ </div>
362
+
363
+ <div class="contributions-section">
364
+ <h2>Activity</h2>
365
+ <div class="contributions-wrapper">
366
+ <div class="contributions-graph" id="contribGraph"></div>
367
+ <div class="contrib-legend">
368
+ <span>Less</span>
369
+ <div class="legend-box" style="background:#EBEDF0"></div>
370
+ <div class="legend-box" style="background:#FED7AA"></div>
371
+ <div class="legend-box" style="background:#FDBA74"></div>
372
+ <div class="legend-box" style="background:#F97316"></div>
373
+ <div class="legend-box" style="background:#C2410C"></div>
374
+ <span>More</span>
375
+ </div>
376
+ </div>
377
+ </div>
378
+
379
+ <div class="bottom-row">
380
+ <div class="chart-section">
381
+ <h2>By Hour</h2>
382
+ <div class="chart-container" id="hourChart"></div>
383
+ </div>
384
+ <div class="chart-section">
385
+ <h2>Models</h2>
386
+ <div class="model-list" id="modelList"></div>
387
+ </div>
388
+ </div>
389
+ `;
390
+
391
+ renderContributions(dailyActivity || []);
392
+ renderHourBars(hourCounts || {});
393
+ renderModelList(modelUsage || {});
394
+ }
395
+
396
+ function renderContributions(dailyActivity) {
397
+ const container = document.getElementById('contribGraph');
398
+
399
+ // Create activity map
400
+ const activityMap = {};
401
+ let maxActivity = 0;
402
+ dailyActivity.forEach(d => {
403
+ activityMap[d.date] = d.messageCount;
404
+ if (d.messageCount > maxActivity) maxActivity = d.messageCount;
405
+ });
406
+
407
+ // Generate last 52 weeks (364 days)
408
+ const today = new Date();
409
+ const weeks = [];
410
+
411
+ // Find the start of the week grid (going back ~52 weeks, starting from Sunday)
412
+ const startDate = new Date(today);
413
+ startDate.setDate(startDate.getDate() - 363);
414
+ // Adjust to previous Sunday
415
+ startDate.setDate(startDate.getDate() - startDate.getDay());
416
+
417
+ let currentDate = new Date(startDate);
418
+
419
+ for (let w = 0; w < 52; w++) {
420
+ const week = [];
421
+ for (let d = 0; d < 7; d++) {
422
+ const dateStr = currentDate.toISOString().split('T')[0];
423
+ const activity = activityMap[dateStr] || 0;
424
+ let level = 0;
425
+ if (activity > 0) {
426
+ const ratio = activity / maxActivity;
427
+ if (ratio > 0.75) level = 4;
428
+ else if (ratio > 0.5) level = 3;
429
+ else if (ratio > 0.25) level = 2;
430
+ else level = 1;
431
+ }
432
+ week.push({ date: dateStr, level, activity });
433
+ currentDate.setDate(currentDate.getDate() + 1);
434
+ }
435
+ weeks.push(week);
436
+ }
437
+
438
+ container.innerHTML = weeks.map(week => `
439
+ <div class="contrib-week">
440
+ ${week.map(day => `
441
+ <div class="contrib-day level-${day.level}" title="${day.date}: ${day.activity} messages"></div>
442
+ `).join('')}
443
+ </div>
444
+ `).join('');
445
+ }
446
+
447
+ function renderHourBars(hourCounts) {
448
+ const container = document.getElementById('hourChart');
449
+ const hours = Array.from({ length: 24 }, (_, i) => i);
450
+ const counts = hours.map(h => hourCounts[h] || 0);
451
+ const maxCount = Math.max(...counts, 1);
452
+
453
+ const width = 280;
454
+ const height = 140;
455
+ const padding = { top: 10, right: 10, bottom: 20, left: 25 };
456
+ const chartW = width - padding.left - padding.right;
457
+ const chartH = height - padding.top - padding.bottom;
458
+
459
+ // Generate points
460
+ const points = hours.map((h, i) => {
461
+ const x = padding.left + (i / 23) * chartW;
462
+ const y = padding.top + chartH - (counts[i] / maxCount) * chartH;
463
+ return { x, y, h, count: counts[i] };
464
+ });
465
+
466
+ // Create smooth curve path using cubic bezier
467
+ const tension = 0.15;
468
+ const maxY = padding.top + chartH; // bottom limit (y increases downward)
469
+ const clampY = (y) => Math.min(y, maxY);
470
+ let linePath = `M ${points[0].x} ${points[0].y}`;
471
+ for (let i = 1; i < points.length; i++) {
472
+ const p0 = points[i - 2] || points[0];
473
+ const p1 = points[i - 1];
474
+ const p2 = points[i];
475
+ const p3 = points[i + 1] || points[points.length - 1];
476
+ const cp1x = p1.x + (p2.x - p0.x) * tension;
477
+ const cp1y = clampY(p1.y + (p2.y - p0.y) * tension);
478
+ const cp2x = p2.x - (p3.x - p1.x) * tension;
479
+ const cp2y = clampY(p2.y - (p3.y - p1.y) * tension);
480
+ linePath += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`;
481
+ }
482
+ const areaPath = linePath + ` L ${points[23].x} ${padding.top + chartH} L ${padding.left} ${padding.top + chartH} Z`;
483
+
484
+ container.innerHTML = `
485
+ <svg class="hour-chart" viewBox="0 0 ${width} ${height}">
486
+ <defs>
487
+ <linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
488
+ <stop offset="0%" stop-color="#D97706" stop-opacity="0.3"/>
489
+ <stop offset="100%" stop-color="#D97706" stop-opacity="0.05"/>
490
+ </linearGradient>
491
+ </defs>
492
+ <path class="area" d="${areaPath}"/>
493
+ <path class="line" d="${linePath}"/>
494
+ ${[0, 6, 12, 18].map(h => {
495
+ const x = padding.left + (h / 23) * chartW;
496
+ return `<text class="axis" x="${x}" y="${height - 5}" text-anchor="middle">${h}</text>`;
497
+ }).join('')}
498
+ <text class="axis" x="${width - padding.right}" y="${height - 5}" text-anchor="end">23</text>
499
+ </svg>
500
+ `;
501
+ }
502
+
503
+ function renderModelList(modelUsage) {
504
+ const container = document.getElementById('modelList');
505
+ const entries = Object.entries(modelUsage);
506
+
507
+ container.innerHTML = entries.map(([model, usage]) => {
508
+ const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
509
+ return `
510
+ <div class="model-item">
511
+ <span class="model-name">${getModelShortName(model)}</span>
512
+ <span class="model-tokens">${formatNumber(total)} tokens</span>
513
+ </div>
514
+ `;
515
+ }).join('');
516
+ }
517
+
518
+ loadStats();
519
+ </script>
520
+ </body>
521
+ </html>