claude-code-templates 1.5.1 → 1.5.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/analytics.js +307 -175
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/analytics.js CHANGED
@@ -70,12 +70,33 @@ class ClaudeAnalytics {
70
70
  const conversations = [];
71
71
 
72
72
  try {
73
- const files = await fs.readdir(this.claudeDir);
74
- const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
73
+ // Search for .jsonl files recursively in all subdirectories
74
+ const findJsonlFiles = async (dir) => {
75
+ const files = [];
76
+ const items = await fs.readdir(dir);
77
+
78
+ for (const item of items) {
79
+ const itemPath = path.join(dir, item);
80
+ const stats = await fs.stat(itemPath);
81
+
82
+ if (stats.isDirectory()) {
83
+ // Recursively search subdirectories
84
+ const subFiles = await findJsonlFiles(itemPath);
85
+ files.push(...subFiles);
86
+ } else if (item.endsWith('.jsonl')) {
87
+ files.push(itemPath);
88
+ }
89
+ }
90
+
91
+ return files;
92
+ };
75
93
 
76
- for (const file of jsonlFiles) {
77
- const filePath = path.join(this.claudeDir, file);
94
+ const jsonlFiles = await findJsonlFiles(this.claudeDir);
95
+ console.log(chalk.blue(`Found ${jsonlFiles.length} conversation files`));
96
+
97
+ for (const filePath of jsonlFiles) {
78
98
  const stats = await fs.stat(filePath);
99
+ const filename = path.basename(filePath);
79
100
 
80
101
  try {
81
102
  const content = await fs.readFile(filePath, 'utf8');
@@ -88,21 +109,25 @@ class ClaudeAnalytics {
88
109
  }
89
110
  }).filter(Boolean);
90
111
 
112
+ // Extract project name from path
113
+ const projectFromPath = this.extractProjectFromPath(filePath);
114
+
91
115
  const conversation = {
92
- id: file.replace('.jsonl', ''),
93
- filename: file,
116
+ id: filename.replace('.jsonl', ''),
117
+ filename: filename,
118
+ filePath: filePath,
94
119
  messageCount: messages.length,
95
120
  fileSize: stats.size,
96
121
  lastModified: stats.mtime,
97
122
  created: stats.birthtime,
98
123
  tokens: this.estimateTokens(content),
99
- project: this.extractProjectFromConversation(messages),
124
+ project: projectFromPath || this.extractProjectFromConversation(messages),
100
125
  status: this.determineConversationStatus(messages, stats.mtime)
101
126
  };
102
127
 
103
128
  conversations.push(conversation);
104
129
  } catch (error) {
105
- console.warn(chalk.yellow(`Warning: Could not parse ${file}:`, error.message));
130
+ console.warn(chalk.yellow(`Warning: Could not parse ${filename}:`, error.message));
106
131
  }
107
132
  }
108
133
 
@@ -160,6 +185,27 @@ class ClaudeAnalytics {
160
185
  return Math.ceil(text.length / 4);
161
186
  }
162
187
 
188
+ extractProjectFromPath(filePath) {
189
+ // Extract project name from file path like:
190
+ // /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
191
+ const pathParts = filePath.split('/');
192
+ const projectIndex = pathParts.findIndex(part => part === 'projects');
193
+
194
+ if (projectIndex !== -1 && projectIndex + 1 < pathParts.length) {
195
+ const projectDir = pathParts[projectIndex + 1];
196
+ // Clean up the project directory name
197
+ const cleanName = projectDir
198
+ .replace(/^-/, '')
199
+ .replace(/-/g, '/')
200
+ .split('/')
201
+ .pop() || 'Unknown';
202
+
203
+ return cleanName;
204
+ }
205
+
206
+ return null;
207
+ }
208
+
163
209
  extractProjectFromConversation(messages) {
164
210
  // Try to extract project information from conversation
165
211
  for (const message of messages.slice(0, 5)) {
@@ -233,8 +279,10 @@ class ClaudeAnalytics {
233
279
  setupFileWatchers() {
234
280
  console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
235
281
 
236
- // Watch conversation files
237
- const conversationWatcher = chokidar.watch(path.join(this.claudeDir, '*.jsonl'), {
282
+ // Watch conversation files recursively in all subdirectories
283
+ const conversationWatcher = chokidar.watch([
284
+ path.join(this.claudeDir, '**/*.jsonl')
285
+ ], {
238
286
  persistent: true,
239
287
  ignoreInitial: true
240
288
  });
@@ -257,7 +305,7 @@ class ClaudeAnalytics {
257
305
  const projectWatcher = chokidar.watch(this.claudeDir, {
258
306
  persistent: true,
259
307
  ignoreInitial: true,
260
- depth: 1
308
+ depth: 2 // Increased depth to catch subdirectories
261
309
  });
262
310
 
263
311
  projectWatcher.on('addDir', async () => {
@@ -397,7 +445,7 @@ async function createWebDashboard() {
397
445
  <head>
398
446
  <meta charset="UTF-8">
399
447
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
400
- <title>Claude Code Analytics Dashboard</title>
448
+ <title>Claude Code Analytics - Terminal</title>
401
449
  <style>
402
450
  * {
403
451
  margin: 0;
@@ -406,234 +454,292 @@ async function createWebDashboard() {
406
454
  }
407
455
 
408
456
  body {
409
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
410
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
411
- color: #333;
457
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
458
+ background: #0d1117;
459
+ color: #c9d1d9;
412
460
  min-height: 100vh;
461
+ line-height: 1.4;
413
462
  }
414
463
 
415
- .container {
416
- max-width: 1200px;
464
+ .terminal {
465
+ max-width: 1400px;
417
466
  margin: 0 auto;
418
467
  padding: 20px;
419
468
  }
420
469
 
421
- .header {
422
- text-align: center;
423
- color: white;
424
- margin-bottom: 30px;
470
+ .terminal-header {
471
+ border-bottom: 1px solid #30363d;
472
+ padding-bottom: 20px;
473
+ margin-bottom: 20px;
425
474
  }
426
475
 
427
- .header h1 {
428
- font-size: 2.5rem;
429
- margin-bottom: 10px;
476
+ .terminal-title {
477
+ color: #58a6ff;
478
+ font-size: 1.25rem;
479
+ font-weight: normal;
480
+ display: flex;
481
+ align-items: center;
482
+ gap: 8px;
430
483
  }
431
484
 
432
- .status-indicator {
433
- display: inline-block;
434
- width: 12px;
435
- height: 12px;
485
+ .status-dot {
486
+ width: 8px;
487
+ height: 8px;
436
488
  border-radius: 50%;
437
- background: #4ade80;
489
+ background: #3fb950;
438
490
  animation: pulse 2s infinite;
439
- margin-right: 8px;
440
491
  }
441
492
 
442
493
  @keyframes pulse {
443
494
  0%, 100% { opacity: 1; }
444
- 50% { opacity: 0.5; }
495
+ 50% { opacity: 0.6; }
445
496
  }
446
497
 
447
- .stats-grid {
448
- display: grid;
449
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
450
- gap: 20px;
451
- margin-bottom: 30px;
498
+ .terminal-subtitle {
499
+ color: #7d8590;
500
+ font-size: 0.875rem;
501
+ margin-top: 4px;
452
502
  }
453
503
 
454
- .stat-card {
455
- background: white;
456
- border-radius: 12px;
457
- padding: 24px;
458
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
459
- transition: transform 0.2s ease;
504
+ .stats-bar {
505
+ display: flex;
506
+ gap: 40px;
507
+ margin: 20px 0;
508
+ flex-wrap: wrap;
460
509
  }
461
510
 
462
- .stat-card:hover {
463
- transform: translateY(-2px);
511
+ .stat {
512
+ display: flex;
513
+ align-items: center;
514
+ gap: 8px;
464
515
  }
465
516
 
466
- .stat-card h3 {
467
- color: #6b7280;
517
+ .stat-label {
518
+ color: #7d8590;
468
519
  font-size: 0.875rem;
469
- text-transform: uppercase;
470
- letter-spacing: 0.05em;
471
- margin-bottom: 8px;
472
520
  }
473
521
 
474
- .stat-card .value {
475
- font-size: 2rem;
522
+ .stat-value {
523
+ color: #58a6ff;
476
524
  font-weight: bold;
477
- color: #1f2937;
478
- margin-bottom: 4px;
479
525
  }
480
526
 
481
- .stat-card .label {
482
- color: #9ca3af;
527
+ .filter-bar {
528
+ display: flex;
529
+ align-items: center;
530
+ gap: 16px;
531
+ margin: 20px 0;
532
+ padding: 12px 0;
533
+ border-top: 1px solid #21262d;
534
+ border-bottom: 1px solid #21262d;
535
+ }
536
+
537
+ .filter-label {
538
+ color: #7d8590;
483
539
  font-size: 0.875rem;
484
540
  }
485
541
 
486
- .content-grid {
487
- display: grid;
488
- grid-template-columns: 1fr 1fr;
489
- gap: 20px;
542
+ .filter-buttons {
543
+ display: flex;
544
+ gap: 8px;
490
545
  }
491
546
 
492
- .panel {
493
- background: white;
494
- border-radius: 12px;
495
- padding: 24px;
496
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
547
+ .filter-btn {
548
+ background: none;
549
+ border: 1px solid #30363d;
550
+ color: #7d8590;
551
+ padding: 4px 12px;
552
+ border-radius: 4px;
553
+ cursor: pointer;
554
+ font-family: inherit;
555
+ font-size: 0.875rem;
556
+ transition: all 0.2s ease;
497
557
  }
498
558
 
499
- .panel h2 {
500
- color: #1f2937;
501
- margin-bottom: 20px;
502
- font-size: 1.25rem;
559
+ .filter-btn:hover {
560
+ border-color: #58a6ff;
561
+ color: #58a6ff;
503
562
  }
504
563
 
505
- .conversation-item, .project-item {
506
- display: flex;
507
- justify-content: space-between;
508
- align-items: center;
509
- padding: 12px 0;
510
- border-bottom: 1px solid #f3f4f6;
564
+ .filter-btn.active {
565
+ background: #58a6ff;
566
+ border-color: #58a6ff;
567
+ color: #0d1117;
511
568
  }
512
569
 
513
- .conversation-item:last-child, .project-item:last-child {
514
- border-bottom: none;
570
+ .sessions-table {
571
+ width: 100%;
572
+ border-collapse: collapse;
515
573
  }
516
574
 
517
- .item-info h4 {
518
- color: #1f2937;
575
+ .sessions-table th {
576
+ text-align: left;
577
+ padding: 8px 12px;
578
+ color: #7d8590;
519
579
  font-size: 0.875rem;
520
- margin-bottom: 4px;
580
+ font-weight: normal;
581
+ border-bottom: 1px solid #30363d;
521
582
  }
522
583
 
523
- .item-info p {
524
- color: #6b7280;
525
- font-size: 0.75rem;
584
+ .sessions-table td {
585
+ padding: 8px 12px;
586
+ font-size: 0.875rem;
587
+ border-bottom: 1px solid #21262d;
526
588
  }
527
589
 
528
- .status-badge {
529
- padding: 4px 8px;
530
- border-radius: 12px;
531
- font-size: 0.75rem;
532
- font-weight: 500;
590
+ .sessions-table tr:hover {
591
+ background: #161b22;
592
+ }
593
+
594
+ .session-id {
595
+ color: #58a6ff;
596
+ font-family: monospace;
597
+ }
598
+
599
+ .session-project {
600
+ color: #c9d1d9;
601
+ }
602
+
603
+ .session-messages {
604
+ color: #7d8590;
605
+ }
606
+
607
+ .session-tokens {
608
+ color: #f85149;
609
+ }
610
+
611
+ .session-time {
612
+ color: #7d8590;
613
+ font-size: 0.8rem;
533
614
  }
534
615
 
535
616
  .status-active {
536
- background: #d1fae5;
537
- color: #065f46;
617
+ color: #3fb950;
618
+ font-weight: bold;
538
619
  }
539
620
 
540
621
  .status-recent {
541
- background: #fef3c7;
542
- color: #92400e;
622
+ color: #d29922;
543
623
  }
544
624
 
545
625
  .status-inactive {
546
- background: #f3f4f6;
547
- color: #6b7280;
626
+ color: #7d8590;
548
627
  }
549
628
 
550
- .loading {
629
+ .loading, .error {
551
630
  text-align: center;
552
- color: white;
553
631
  padding: 40px;
632
+ color: #7d8590;
554
633
  }
555
634
 
556
635
  .error {
557
- background: #fef2f2;
558
- color: #dc2626;
559
- padding: 16px;
560
- border-radius: 8px;
561
- margin: 20px 0;
636
+ color: #f85149;
637
+ }
638
+
639
+ .no-sessions {
640
+ text-align: center;
641
+ padding: 40px;
642
+ color: #7d8590;
643
+ font-style: italic;
562
644
  }
563
645
 
564
646
  @media (max-width: 768px) {
565
- .content-grid {
566
- grid-template-columns: 1fr;
647
+ .stats-bar {
648
+ gap: 20px;
649
+ }
650
+
651
+ .filter-bar {
652
+ flex-direction: column;
653
+ align-items: flex-start;
654
+ gap: 8px;
655
+ }
656
+
657
+ .sessions-table {
658
+ font-size: 0.8rem;
567
659
  }
568
660
 
569
- .header h1 {
570
- font-size: 2rem;
661
+ .sessions-table th,
662
+ .sessions-table td {
663
+ padding: 6px 8px;
571
664
  }
572
665
  }
573
666
  </style>
574
667
  </head>
575
668
  <body>
576
- <div class="container">
577
- <div class="header">
578
- <h1>
579
- <span class="status-indicator"></span>
580
- Claude Code Analytics
581
- </h1>
582
- <p>Real-time monitoring of your Claude Code usage</p>
583
- <p id="lastUpdate" style="font-size: 0.75rem; opacity: 0.8;"></p>
669
+ <div class="terminal">
670
+ <div class="terminal-header">
671
+ <div class="terminal-title">
672
+ <span class="status-dot"></span>
673
+ claude-code-analytics
674
+ </div>
675
+ <div class="terminal-subtitle">real-time monitoring dashboard</div>
676
+ <div class="terminal-subtitle" id="lastUpdate"></div>
584
677
  </div>
585
678
 
586
679
  <div id="loading" class="loading">
587
- <p>Loading analytics data...</p>
680
+ loading claude code data...
588
681
  </div>
589
682
 
590
683
  <div id="error" class="error" style="display: none;">
591
- <p>Failed to load analytics data. Please check if Claude Code is installed.</p>
684
+ error: failed to load claude code data
592
685
  </div>
593
686
 
594
687
  <div id="dashboard" style="display: none;">
595
- <div class="stats-grid">
596
- <div class="stat-card">
597
- <h3>Total Sessions</h3>
598
- <div class="value" id="totalSessions">0</div>
599
- <div class="label">Conversations</div>
688
+ <div class="stats-bar">
689
+ <div class="stat">
690
+ <span class="stat-label">sessions:</span>
691
+ <span class="stat-value" id="totalSessions">0</span>
600
692
  </div>
601
- <div class="stat-card">
602
- <h3>Total Tokens</h3>
603
- <div class="value" id="totalTokens">0</div>
604
- <div class="label">Estimated</div>
693
+ <div class="stat">
694
+ <span class="stat-label">tokens:</span>
695
+ <span class="stat-value" id="totalTokens">0</span>
605
696
  </div>
606
- <div class="stat-card">
607
- <h3>Active Projects</h3>
608
- <div class="value" id="activeProjects">0</div>
609
- <div class="label">Currently</div>
697
+ <div class="stat">
698
+ <span class="stat-label">projects:</span>
699
+ <span class="stat-value" id="activeProjects">0</span>
610
700
  </div>
611
- <div class="stat-card">
612
- <h3>Data Size</h3>
613
- <div class="value" id="dataSize">0</div>
614
- <div class="label">Total</div>
701
+ <div class="stat">
702
+ <span class="stat-label">storage:</span>
703
+ <span class="stat-value" id="dataSize">0</span>
615
704
  </div>
616
705
  </div>
617
706
 
618
- <div class="content-grid">
619
- <div class="panel">
620
- <h2>Recent Conversations</h2>
621
- <div id="conversations">
622
- <!-- Conversations will be loaded here -->
623
- </div>
624
- </div>
625
-
626
- <div class="panel">
627
- <h2>Active Projects</h2>
628
- <div id="projects">
629
- <!-- Projects will be loaded here -->
630
- </div>
707
+ <div class="filter-bar">
708
+ <span class="filter-label">filter sessions:</span>
709
+ <div class="filter-buttons">
710
+ <button class="filter-btn active" data-filter="active">active</button>
711
+ <button class="filter-btn" data-filter="recent">recent</button>
712
+ <button class="filter-btn" data-filter="inactive">inactive</button>
713
+ <button class="filter-btn" data-filter="all">all</button>
631
714
  </div>
632
715
  </div>
716
+
717
+ <table class="sessions-table">
718
+ <thead>
719
+ <tr>
720
+ <th>session id</th>
721
+ <th>project</th>
722
+ <th>messages</th>
723
+ <th>tokens</th>
724
+ <th>last activity</th>
725
+ <th>status</th>
726
+ </tr>
727
+ </thead>
728
+ <tbody id="sessionsTable">
729
+ <!-- Sessions will be loaded here -->
730
+ </tbody>
731
+ </table>
732
+
733
+ <div id="noSessions" class="no-sessions" style="display: none;">
734
+ no sessions found for current filter
735
+ </div>
633
736
  </div>
634
737
  </div>
635
738
 
636
739
  <script>
740
+ let allConversations = [];
741
+ let currentFilter = 'active';
742
+
637
743
  async function loadData() {
638
744
  try {
639
745
  const response = await fetch('/api/data');
@@ -646,11 +752,11 @@ async function createWebDashboard() {
646
752
 
647
753
  // Update timestamp
648
754
  document.getElementById('lastUpdate').textContent =
649
- \`Last updated: \${data.lastUpdate}\`;
755
+ \`last update: \${data.lastUpdate}\`;
650
756
 
651
757
  updateStats(data.summary);
652
- updateConversations(data.conversations);
653
- updateProjects(data.activeProjects);
758
+ allConversations = data.conversations;
759
+ updateSessionsTable();
654
760
 
655
761
  } catch (error) {
656
762
  document.getElementById('loading').style.display = 'none';
@@ -666,44 +772,70 @@ async function createWebDashboard() {
666
772
  document.getElementById('dataSize').textContent = summary.totalFileSize;
667
773
  }
668
774
 
669
- function updateConversations(conversations) {
670
- const container = document.getElementById('conversations');
775
+ function updateSessionsTable() {
776
+ const tableBody = document.getElementById('sessionsTable');
777
+ const noSessionsDiv = document.getElementById('noSessions');
671
778
 
672
- if (conversations.length === 0) {
673
- container.innerHTML = '<p style="color: #6b7280; text-align: center; padding: 20px;">No conversations found</p>';
779
+ // Filter conversations based on current filter
780
+ let filteredConversations = allConversations;
781
+ if (currentFilter !== 'all') {
782
+ filteredConversations = allConversations.filter(conv => conv.status === currentFilter);
783
+ }
784
+
785
+ if (filteredConversations.length === 0) {
786
+ tableBody.innerHTML = '';
787
+ noSessionsDiv.style.display = 'block';
674
788
  return;
675
789
  }
676
790
 
677
- container.innerHTML = conversations.slice(0, 10).map(conv => \`
678
- <div class="conversation-item">
679
- <div class="item-info">
680
- <h4>\${conv.project}</h4>
681
- <p>\${conv.messageCount} messages • \${conv.tokens.toLocaleString()} tokens</p>
682
- </div>
683
- <span class="status-badge status-\${conv.status}">\${conv.status}</span>
684
- </div>
791
+ noSessionsDiv.style.display = 'none';
792
+
793
+ tableBody.innerHTML = filteredConversations.map(conv => \`
794
+ <tr>
795
+ <td class="session-id">\${conv.id.substring(0, 8)}...</td>
796
+ <td class="session-project">\${conv.project}</td>
797
+ <td class="session-messages">\${conv.messageCount}</td>
798
+ <td class="session-tokens">\${conv.tokens.toLocaleString()}</td>
799
+ <td class="session-time">\${formatTime(conv.lastModified)}</td>
800
+ <td class="status-\${conv.status}">\${conv.status}</td>
801
+ </tr>
685
802
  \`).join('');
686
803
  }
687
804
 
688
- function updateProjects(projects) {
689
- const container = document.getElementById('projects');
805
+ function formatTime(date) {
806
+ const now = new Date();
807
+ const diff = now - new Date(date);
808
+ const minutes = Math.floor(diff / (1000 * 60));
809
+ const hours = Math.floor(minutes / 60);
810
+ const days = Math.floor(hours / 24);
690
811
 
691
- if (projects.length === 0) {
692
- container.innerHTML = '<p style="color: #6b7280; text-align: center; padding: 20px;">No projects found</p>';
693
- return;
694
- }
695
-
696
- container.innerHTML = projects.slice(0, 10).map(project => \`
697
- <div class="project-item">
698
- <div class="item-info">
699
- <h4>\${project.name}</h4>
700
- <p>\${project.todoFiles} todo files</p>
701
- </div>
702
- <span class="status-badge status-\${project.status}">\${project.status}</span>
703
- </div>
704
- \`).join('');
812
+ if (minutes < 1) return 'now';
813
+ if (minutes < 60) return \`\${minutes}m ago\`;
814
+ if (hours < 24) return \`\${hours}h ago\`;
815
+ return \`\${days}d ago\`;
705
816
  }
706
817
 
818
+ // Filter button handlers
819
+ document.addEventListener('DOMContentLoaded', function() {
820
+ const filterButtons = document.querySelectorAll('.filter-btn');
821
+
822
+ filterButtons.forEach(button => {
823
+ button.addEventListener('click', function() {
824
+ // Remove active class from all buttons
825
+ filterButtons.forEach(btn => btn.classList.remove('active'));
826
+
827
+ // Add active class to clicked button
828
+ this.classList.add('active');
829
+
830
+ // Update current filter
831
+ currentFilter = this.dataset.filter;
832
+
833
+ // Update table
834
+ updateSessionsTable();
835
+ });
836
+ });
837
+ });
838
+
707
839
  // Manual refresh function
708
840
  async function forceRefresh() {
709
841
  try {