claude-code-templates 1.4.2 → 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,675 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const chokidar = require('chokidar');
6
+ const open = require('open');
7
+ const os = require('os');
8
+
9
+ class ClaudeAnalytics {
10
+ constructor() {
11
+ this.app = express();
12
+ this.port = 3333;
13
+ this.data = {
14
+ conversations: [],
15
+ summary: {},
16
+ activeProjects: [],
17
+ realtimeStats: {
18
+ totalSessions: 0,
19
+ totalTokens: 0,
20
+ activeProjects: 0,
21
+ lastActivity: null
22
+ }
23
+ };
24
+ this.watchers = [];
25
+ }
26
+
27
+ async initialize() {
28
+ const homeDir = os.homedir();
29
+ this.claudeDir = path.join(homeDir, '.claude');
30
+ this.claudeDesktopDir = path.join(homeDir, 'Library', 'Application Support', 'Claude');
31
+
32
+ // Check if Claude directories exist
33
+ if (!await fs.pathExists(this.claudeDir)) {
34
+ throw new Error(`Claude Code directory not found at ${this.claudeDir}`);
35
+ }
36
+
37
+ await this.loadInitialData();
38
+ this.setupFileWatchers();
39
+ this.setupWebServer();
40
+ }
41
+
42
+ async loadInitialData() {
43
+ console.log(chalk.yellow('📊 Analyzing Claude Code data...'));
44
+
45
+ try {
46
+ // Load conversation files
47
+ const conversations = await this.loadConversations();
48
+ this.data.conversations = conversations;
49
+
50
+ // Load active projects
51
+ const projects = await this.loadActiveProjects();
52
+ this.data.activeProjects = projects;
53
+
54
+ // Calculate summary statistics
55
+ this.data.summary = this.calculateSummary(conversations, projects);
56
+
57
+ // Update realtime stats
58
+ this.updateRealtimeStats();
59
+
60
+ console.log(chalk.green('✅ Data analysis complete'));
61
+ console.log(chalk.gray(`Found ${conversations.length} conversations across ${projects.length} projects`));
62
+
63
+ } catch (error) {
64
+ console.error(chalk.red('Error loading Claude data:'), error.message);
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ async loadConversations() {
70
+ const conversations = [];
71
+
72
+ try {
73
+ const files = await fs.readdir(this.claudeDir);
74
+ const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
75
+
76
+ for (const file of jsonlFiles) {
77
+ const filePath = path.join(this.claudeDir, file);
78
+ const stats = await fs.stat(filePath);
79
+
80
+ try {
81
+ const content = await fs.readFile(filePath, 'utf8');
82
+ const lines = content.trim().split('\n').filter(line => line.trim());
83
+ const messages = lines.map(line => {
84
+ try {
85
+ return JSON.parse(line);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }).filter(Boolean);
90
+
91
+ const conversation = {
92
+ id: file.replace('.jsonl', ''),
93
+ filename: file,
94
+ messageCount: messages.length,
95
+ fileSize: stats.size,
96
+ lastModified: stats.mtime,
97
+ created: stats.birthtime,
98
+ tokens: this.estimateTokens(content),
99
+ project: this.extractProjectFromConversation(messages),
100
+ status: this.determineConversationStatus(messages, stats.mtime)
101
+ };
102
+
103
+ conversations.push(conversation);
104
+ } catch (error) {
105
+ console.warn(chalk.yellow(`Warning: Could not parse ${file}:`, error.message));
106
+ }
107
+ }
108
+
109
+ return conversations.sort((a, b) => b.lastModified - a.lastModified);
110
+ } catch (error) {
111
+ console.error(chalk.red('Error loading conversations:'), error.message);
112
+ return [];
113
+ }
114
+ }
115
+
116
+ async loadActiveProjects() {
117
+ const projects = [];
118
+
119
+ try {
120
+ const files = await fs.readdir(this.claudeDir);
121
+
122
+ for (const file of files) {
123
+ const filePath = path.join(this.claudeDir, file);
124
+ const stats = await fs.stat(filePath);
125
+
126
+ if (stats.isDirectory() && !file.startsWith('.')) {
127
+ const projectPath = filePath;
128
+ const todoFiles = await this.findTodoFiles(projectPath);
129
+
130
+ const project = {
131
+ name: file,
132
+ path: projectPath,
133
+ lastActivity: stats.mtime,
134
+ todoFiles: todoFiles.length,
135
+ status: this.determineProjectStatus(stats.mtime)
136
+ };
137
+
138
+ projects.push(project);
139
+ }
140
+ }
141
+
142
+ return projects.sort((a, b) => b.lastActivity - a.lastActivity);
143
+ } catch (error) {
144
+ console.error(chalk.red('Error loading projects:'), error.message);
145
+ return [];
146
+ }
147
+ }
148
+
149
+ async findTodoFiles(projectPath) {
150
+ try {
151
+ const files = await fs.readdir(projectPath);
152
+ return files.filter(file => file.includes('todo') || file.includes('TODO'));
153
+ } catch {
154
+ return [];
155
+ }
156
+ }
157
+
158
+ estimateTokens(text) {
159
+ // Simple token estimation (roughly 4 characters per token)
160
+ return Math.ceil(text.length / 4);
161
+ }
162
+
163
+ extractProjectFromConversation(messages) {
164
+ // Try to extract project information from conversation
165
+ for (const message of messages.slice(0, 5)) {
166
+ if (message.content && typeof message.content === 'string') {
167
+ const pathMatch = message.content.match(/\/([^\/\s]+)$/);
168
+ if (pathMatch) {
169
+ return pathMatch[1];
170
+ }
171
+ }
172
+ }
173
+ return 'Unknown';
174
+ }
175
+
176
+ determineConversationStatus(messages, lastModified) {
177
+ const now = new Date();
178
+ const timeDiff = now - lastModified;
179
+ const minutesAgo = timeDiff / (1000 * 60);
180
+
181
+ if (minutesAgo < 5) return 'active';
182
+ if (minutesAgo < 60) return 'recent';
183
+ return 'inactive';
184
+ }
185
+
186
+ determineProjectStatus(lastActivity) {
187
+ const now = new Date();
188
+ const timeDiff = now - lastActivity;
189
+ const hoursAgo = timeDiff / (1000 * 60 * 60);
190
+
191
+ if (hoursAgo < 1) return 'active';
192
+ if (hoursAgo < 24) return 'recent';
193
+ return 'inactive';
194
+ }
195
+
196
+ calculateSummary(conversations, projects) {
197
+ const totalTokens = conversations.reduce((sum, conv) => sum + conv.tokens, 0);
198
+ const totalSessions = conversations.length;
199
+ const activeConversations = conversations.filter(c => c.status === 'active').length;
200
+ const activeProjects = projects.filter(p => p.status === 'active').length;
201
+
202
+ const avgTokensPerSession = totalSessions > 0 ? Math.round(totalTokens / totalSessions) : 0;
203
+ const totalFileSize = conversations.reduce((sum, conv) => sum + conv.fileSize, 0);
204
+
205
+ return {
206
+ totalSessions,
207
+ totalTokens,
208
+ activeConversations,
209
+ activeProjects,
210
+ avgTokensPerSession,
211
+ totalFileSize: this.formatBytes(totalFileSize),
212
+ lastActivity: conversations.length > 0 ? conversations[0].lastModified : null
213
+ };
214
+ }
215
+
216
+ updateRealtimeStats() {
217
+ this.data.realtimeStats = {
218
+ totalSessions: this.data.conversations.length,
219
+ totalTokens: this.data.conversations.reduce((sum, conv) => sum + conv.tokens, 0),
220
+ activeProjects: this.data.activeProjects.filter(p => p.status === 'active').length,
221
+ lastActivity: this.data.summary.lastActivity
222
+ };
223
+ }
224
+
225
+ formatBytes(bytes) {
226
+ if (bytes === 0) return '0 Bytes';
227
+ const k = 1024;
228
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
229
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
230
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
231
+ }
232
+
233
+ setupFileWatchers() {
234
+ console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
235
+
236
+ // Watch conversation files
237
+ const conversationWatcher = chokidar.watch(path.join(this.claudeDir, '*.jsonl'), {
238
+ persistent: true,
239
+ ignoreInitial: true
240
+ });
241
+
242
+ conversationWatcher.on('change', async () => {
243
+ await this.loadInitialData();
244
+ console.log(chalk.green('🔄 Conversation data updated'));
245
+ });
246
+
247
+ conversationWatcher.on('add', async () => {
248
+ await this.loadInitialData();
249
+ console.log(chalk.green('📝 New conversation detected'));
250
+ });
251
+
252
+ this.watchers.push(conversationWatcher);
253
+
254
+ // Watch project directories
255
+ const projectWatcher = chokidar.watch(this.claudeDir, {
256
+ persistent: true,
257
+ ignoreInitial: true,
258
+ depth: 1
259
+ });
260
+
261
+ projectWatcher.on('addDir', async () => {
262
+ await this.loadInitialData();
263
+ console.log(chalk.green('📁 New project detected'));
264
+ });
265
+
266
+ this.watchers.push(projectWatcher);
267
+ }
268
+
269
+ setupWebServer() {
270
+ // Serve static files (we'll create the dashboard HTML)
271
+ this.app.use(express.static(path.join(__dirname, 'analytics-web')));
272
+
273
+ // API endpoints
274
+ this.app.get('/api/data', (req, res) => {
275
+ res.json(this.data);
276
+ });
277
+
278
+ this.app.get('/api/realtime', (req, res) => {
279
+ res.json(this.data.realtimeStats);
280
+ });
281
+
282
+ // Main dashboard route
283
+ this.app.get('/', (req, res) => {
284
+ res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
285
+ });
286
+ }
287
+
288
+ async startServer() {
289
+ return new Promise((resolve) => {
290
+ this.server = this.app.listen(this.port, () => {
291
+ console.log(chalk.green(`🚀 Analytics dashboard started at http://localhost:${this.port}`));
292
+ resolve();
293
+ });
294
+ });
295
+ }
296
+
297
+ async openBrowser() {
298
+ try {
299
+ await open(`http://localhost:${this.port}`);
300
+ console.log(chalk.blue('🌐 Opening browser...'));
301
+ } catch (error) {
302
+ console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
303
+ console.log(chalk.cyan(`http://localhost:${this.port}`));
304
+ }
305
+ }
306
+
307
+ stop() {
308
+ // Clean up watchers
309
+ this.watchers.forEach(watcher => watcher.close());
310
+
311
+ // Stop server
312
+ if (this.server) {
313
+ this.server.close();
314
+ }
315
+
316
+ console.log(chalk.yellow('Analytics dashboard stopped'));
317
+ }
318
+ }
319
+
320
+ async function runAnalytics(options = {}) {
321
+ console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
322
+
323
+ const analytics = new ClaudeAnalytics();
324
+
325
+ try {
326
+ await analytics.initialize();
327
+
328
+ // Create web dashboard files
329
+ await createWebDashboard();
330
+
331
+ await analytics.startServer();
332
+ await analytics.openBrowser();
333
+
334
+ console.log(chalk.green('✅ Analytics dashboard is running!'));
335
+ console.log(chalk.gray('Press Ctrl+C to stop the server'));
336
+
337
+ // Handle graceful shutdown
338
+ process.on('SIGINT', () => {
339
+ console.log(chalk.yellow('\n🛑 Shutting down analytics dashboard...'));
340
+ analytics.stop();
341
+ process.exit(0);
342
+ });
343
+
344
+ // Keep the process running
345
+ await new Promise(() => {});
346
+
347
+ } catch (error) {
348
+ console.error(chalk.red('❌ Failed to start analytics dashboard:'), error.message);
349
+ process.exit(1);
350
+ }
351
+ }
352
+
353
+ async function createWebDashboard() {
354
+ const webDir = path.join(__dirname, 'analytics-web');
355
+ await fs.ensureDir(webDir);
356
+
357
+ // Create the HTML dashboard
358
+ const htmlContent = `<!DOCTYPE html>
359
+ <html lang="en">
360
+ <head>
361
+ <meta charset="UTF-8">
362
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
363
+ <title>Claude Code Analytics Dashboard</title>
364
+ <style>
365
+ * {
366
+ margin: 0;
367
+ padding: 0;
368
+ box-sizing: border-box;
369
+ }
370
+
371
+ body {
372
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
373
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
374
+ color: #333;
375
+ min-height: 100vh;
376
+ }
377
+
378
+ .container {
379
+ max-width: 1200px;
380
+ margin: 0 auto;
381
+ padding: 20px;
382
+ }
383
+
384
+ .header {
385
+ text-align: center;
386
+ color: white;
387
+ margin-bottom: 30px;
388
+ }
389
+
390
+ .header h1 {
391
+ font-size: 2.5rem;
392
+ margin-bottom: 10px;
393
+ }
394
+
395
+ .status-indicator {
396
+ display: inline-block;
397
+ width: 12px;
398
+ height: 12px;
399
+ border-radius: 50%;
400
+ background: #4ade80;
401
+ animation: pulse 2s infinite;
402
+ margin-right: 8px;
403
+ }
404
+
405
+ @keyframes pulse {
406
+ 0%, 100% { opacity: 1; }
407
+ 50% { opacity: 0.5; }
408
+ }
409
+
410
+ .stats-grid {
411
+ display: grid;
412
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
413
+ gap: 20px;
414
+ margin-bottom: 30px;
415
+ }
416
+
417
+ .stat-card {
418
+ background: white;
419
+ border-radius: 12px;
420
+ padding: 24px;
421
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
422
+ transition: transform 0.2s ease;
423
+ }
424
+
425
+ .stat-card:hover {
426
+ transform: translateY(-2px);
427
+ }
428
+
429
+ .stat-card h3 {
430
+ color: #6b7280;
431
+ font-size: 0.875rem;
432
+ text-transform: uppercase;
433
+ letter-spacing: 0.05em;
434
+ margin-bottom: 8px;
435
+ }
436
+
437
+ .stat-card .value {
438
+ font-size: 2rem;
439
+ font-weight: bold;
440
+ color: #1f2937;
441
+ margin-bottom: 4px;
442
+ }
443
+
444
+ .stat-card .label {
445
+ color: #9ca3af;
446
+ font-size: 0.875rem;
447
+ }
448
+
449
+ .content-grid {
450
+ display: grid;
451
+ grid-template-columns: 1fr 1fr;
452
+ gap: 20px;
453
+ }
454
+
455
+ .panel {
456
+ background: white;
457
+ border-radius: 12px;
458
+ padding: 24px;
459
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
460
+ }
461
+
462
+ .panel h2 {
463
+ color: #1f2937;
464
+ margin-bottom: 20px;
465
+ font-size: 1.25rem;
466
+ }
467
+
468
+ .conversation-item, .project-item {
469
+ display: flex;
470
+ justify-content: space-between;
471
+ align-items: center;
472
+ padding: 12px 0;
473
+ border-bottom: 1px solid #f3f4f6;
474
+ }
475
+
476
+ .conversation-item:last-child, .project-item:last-child {
477
+ border-bottom: none;
478
+ }
479
+
480
+ .item-info h4 {
481
+ color: #1f2937;
482
+ font-size: 0.875rem;
483
+ margin-bottom: 4px;
484
+ }
485
+
486
+ .item-info p {
487
+ color: #6b7280;
488
+ font-size: 0.75rem;
489
+ }
490
+
491
+ .status-badge {
492
+ padding: 4px 8px;
493
+ border-radius: 12px;
494
+ font-size: 0.75rem;
495
+ font-weight: 500;
496
+ }
497
+
498
+ .status-active {
499
+ background: #d1fae5;
500
+ color: #065f46;
501
+ }
502
+
503
+ .status-recent {
504
+ background: #fef3c7;
505
+ color: #92400e;
506
+ }
507
+
508
+ .status-inactive {
509
+ background: #f3f4f6;
510
+ color: #6b7280;
511
+ }
512
+
513
+ .loading {
514
+ text-align: center;
515
+ color: white;
516
+ padding: 40px;
517
+ }
518
+
519
+ .error {
520
+ background: #fef2f2;
521
+ color: #dc2626;
522
+ padding: 16px;
523
+ border-radius: 8px;
524
+ margin: 20px 0;
525
+ }
526
+
527
+ @media (max-width: 768px) {
528
+ .content-grid {
529
+ grid-template-columns: 1fr;
530
+ }
531
+
532
+ .header h1 {
533
+ font-size: 2rem;
534
+ }
535
+ }
536
+ </style>
537
+ </head>
538
+ <body>
539
+ <div class="container">
540
+ <div class="header">
541
+ <h1>
542
+ <span class="status-indicator"></span>
543
+ Claude Code Analytics
544
+ </h1>
545
+ <p>Real-time monitoring of your Claude Code usage</p>
546
+ </div>
547
+
548
+ <div id="loading" class="loading">
549
+ <p>Loading analytics data...</p>
550
+ </div>
551
+
552
+ <div id="error" class="error" style="display: none;">
553
+ <p>Failed to load analytics data. Please check if Claude Code is installed.</p>
554
+ </div>
555
+
556
+ <div id="dashboard" style="display: none;">
557
+ <div class="stats-grid">
558
+ <div class="stat-card">
559
+ <h3>Total Sessions</h3>
560
+ <div class="value" id="totalSessions">0</div>
561
+ <div class="label">Conversations</div>
562
+ </div>
563
+ <div class="stat-card">
564
+ <h3>Total Tokens</h3>
565
+ <div class="value" id="totalTokens">0</div>
566
+ <div class="label">Estimated</div>
567
+ </div>
568
+ <div class="stat-card">
569
+ <h3>Active Projects</h3>
570
+ <div class="value" id="activeProjects">0</div>
571
+ <div class="label">Currently</div>
572
+ </div>
573
+ <div class="stat-card">
574
+ <h3>Data Size</h3>
575
+ <div class="value" id="dataSize">0</div>
576
+ <div class="label">Total</div>
577
+ </div>
578
+ </div>
579
+
580
+ <div class="content-grid">
581
+ <div class="panel">
582
+ <h2>Recent Conversations</h2>
583
+ <div id="conversations">
584
+ <!-- Conversations will be loaded here -->
585
+ </div>
586
+ </div>
587
+
588
+ <div class="panel">
589
+ <h2>Active Projects</h2>
590
+ <div id="projects">
591
+ <!-- Projects will be loaded here -->
592
+ </div>
593
+ </div>
594
+ </div>
595
+ </div>
596
+ </div>
597
+
598
+ <script>
599
+ async function loadData() {
600
+ try {
601
+ const response = await fetch('/api/data');
602
+ const data = await response.json();
603
+
604
+ document.getElementById('loading').style.display = 'none';
605
+ document.getElementById('dashboard').style.display = 'block';
606
+
607
+ updateStats(data.summary);
608
+ updateConversations(data.conversations);
609
+ updateProjects(data.activeProjects);
610
+
611
+ } catch (error) {
612
+ document.getElementById('loading').style.display = 'none';
613
+ document.getElementById('error').style.display = 'block';
614
+ console.error('Failed to load data:', error);
615
+ }
616
+ }
617
+
618
+ function updateStats(summary) {
619
+ document.getElementById('totalSessions').textContent = summary.totalSessions.toLocaleString();
620
+ document.getElementById('totalTokens').textContent = summary.totalTokens.toLocaleString();
621
+ document.getElementById('activeProjects').textContent = summary.activeProjects;
622
+ document.getElementById('dataSize').textContent = summary.totalFileSize;
623
+ }
624
+
625
+ function updateConversations(conversations) {
626
+ const container = document.getElementById('conversations');
627
+
628
+ if (conversations.length === 0) {
629
+ container.innerHTML = '<p style="color: #6b7280; text-align: center; padding: 20px;">No conversations found</p>';
630
+ return;
631
+ }
632
+
633
+ container.innerHTML = conversations.slice(0, 10).map(conv => \`
634
+ <div class="conversation-item">
635
+ <div class="item-info">
636
+ <h4>\${conv.project}</h4>
637
+ <p>\${conv.messageCount} messages • \${conv.tokens.toLocaleString()} tokens</p>
638
+ </div>
639
+ <span class="status-badge status-\${conv.status}">\${conv.status}</span>
640
+ </div>
641
+ \`).join('');
642
+ }
643
+
644
+ function updateProjects(projects) {
645
+ const container = document.getElementById('projects');
646
+
647
+ if (projects.length === 0) {
648
+ container.innerHTML = '<p style="color: #6b7280; text-align: center; padding: 20px;">No projects found</p>';
649
+ return;
650
+ }
651
+
652
+ container.innerHTML = projects.slice(0, 10).map(project => \`
653
+ <div class="project-item">
654
+ <div class="item-info">
655
+ <h4>\${project.name}</h4>
656
+ <p>\${project.todoFiles} todo files</p>
657
+ </div>
658
+ <span class="status-badge status-\${project.status}">\${project.status}</span>
659
+ </div>
660
+ \`).join('');
661
+ }
662
+
663
+ // Load initial data
664
+ loadData();
665
+
666
+ // Refresh data every 10 seconds
667
+ setInterval(loadData, 10000);
668
+ </script>
669
+ </body>
670
+ </html>`;
671
+
672
+ await fs.writeFile(path.join(webDir, 'index.html'), htmlContent);
673
+ }
674
+
675
+ module.exports = { runAnalytics };