claude-code-templates 1.8.2 → 1.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.8.2",
3
+ "version": "1.9.0",
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": {
@@ -31,7 +31,6 @@
31
31
  "dev:link": "npm link",
32
32
  "dev:unlink": "npm unlink -g claude-code-templates",
33
33
  "pretest:commands": "npm run dev:link",
34
- "prepublishOnly": "echo 'Skipping tests for emergency hotfix'",
35
34
  "analytics:start": "node src/analytics.js",
36
35
  "analytics:test": "npm run test:analytics"
37
36
  },
@@ -110,6 +110,9 @@ class ConversationAnalyzer {
110
110
  const tokenUsage = await this.getCachedTokenUsage(filePath, parsedMessages);
111
111
  const modelInfo = await this.getCachedModelInfo(filePath, parsedMessages);
112
112
 
113
+ // Calculate tool usage data with caching
114
+ const toolUsage = await this.getCachedToolUsage(filePath, parsedMessages);
115
+
113
116
  const conversation = {
114
117
  id: filename.replace('.jsonl', ''),
115
118
  filename: filename,
@@ -121,6 +124,7 @@ class ConversationAnalyzer {
121
124
  tokens: tokenUsage.total > 0 ? tokenUsage.total : this.estimateTokens(await this.getFileContent(filePath)),
122
125
  tokenUsage: tokenUsage,
123
126
  modelInfo: modelInfo,
127
+ toolUsage: toolUsage,
124
128
  project: projectFromPath || this.extractProjectFromConversation(parsedMessages),
125
129
  status: stateCalculator.determineConversationStatus(parsedMessages, stats.mtime),
126
130
  conversationState: stateCalculator.determineConversationState(parsedMessages, stats.mtime),
@@ -279,6 +283,21 @@ class ConversationAnalyzer {
279
283
  return this.generateStatusSquares(parsedMessages);
280
284
  }
281
285
 
286
+ /**
287
+ * Get cached tool usage analysis
288
+ * @param {string} filepath - File path
289
+ * @param {Array} parsedMessages - Parsed messages array
290
+ * @returns {Promise<Object>} Tool usage data
291
+ */
292
+ async getCachedToolUsage(filepath, parsedMessages) {
293
+ if (this.dataCache) {
294
+ return await this.dataCache.getCachedToolUsage(filepath, () => {
295
+ return this.extractToolUsage(parsedMessages);
296
+ });
297
+ }
298
+ return this.extractToolUsage(parsedMessages);
299
+ }
300
+
282
301
  /**
283
302
  * Calculate real token usage from message usage data
284
303
  * @param {Array} parsedMessages - Array of parsed message objects
@@ -387,6 +406,65 @@ class ConversationAnalyzer {
387
406
  return 'Unknown';
388
407
  }
389
408
 
409
+ /**
410
+ * Extract tool usage statistics from parsed messages
411
+ * @param {Array} parsedMessages - Array of parsed message objects
412
+ * @returns {Object} Tool usage statistics
413
+ */
414
+ extractToolUsage(parsedMessages) {
415
+ const toolStats = {};
416
+ const toolTimeline = [];
417
+ let totalToolCalls = 0;
418
+
419
+ parsedMessages.forEach(message => {
420
+ if (message.role === 'assistant' && message.content) {
421
+ const content = message.content;
422
+ const timestamp = message.timestamp;
423
+
424
+ // Handle string content with tool indicators
425
+ if (typeof content === 'string') {
426
+ const toolMatches = content.match(/\[Tool:\s*([^\]]+)\]/g);
427
+ if (toolMatches) {
428
+ toolMatches.forEach(match => {
429
+ const toolName = match.replace(/\[Tool:\s*([^\]]+)\]/, '$1').trim();
430
+ toolStats[toolName] = (toolStats[toolName] || 0) + 1;
431
+ totalToolCalls++;
432
+ toolTimeline.push({
433
+ tool: toolName,
434
+ timestamp: timestamp,
435
+ type: 'usage'
436
+ });
437
+ });
438
+ }
439
+ }
440
+
441
+ // Handle array content with tool_use blocks
442
+ if (Array.isArray(content)) {
443
+ content.forEach(block => {
444
+ if (block.type === 'tool_use') {
445
+ const toolName = block.name || 'Unknown Tool';
446
+ toolStats[toolName] = (toolStats[toolName] || 0) + 1;
447
+ totalToolCalls++;
448
+ toolTimeline.push({
449
+ tool: toolName,
450
+ timestamp: timestamp,
451
+ type: 'usage',
452
+ parameters: block.input || {}
453
+ });
454
+ }
455
+ });
456
+ }
457
+ }
458
+ });
459
+
460
+ return {
461
+ toolStats,
462
+ toolTimeline: toolTimeline.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)),
463
+ totalToolCalls,
464
+ uniqueTools: Object.keys(toolStats).length
465
+ };
466
+ }
467
+
390
468
  /**
391
469
  * Generate status indicators for conversation messages
392
470
  * @param {Array} messages - Array of message objects
@@ -19,6 +19,7 @@ class DataCache {
19
19
  tokenUsage: new Map(), // filepath -> { usage, timestamp }
20
20
  modelInfo: new Map(), // filepath -> { info, timestamp }
21
21
  statusSquares: new Map(), // filepath -> { squares, timestamp }
22
+ toolUsage: new Map(), // filepath -> { usage, timestamp }
22
23
 
23
24
  // Expensive computations cache
24
25
  sessions: { data: null, timestamp: 0, dependencies: new Set() },
@@ -319,6 +320,32 @@ class DataCache {
319
320
  return squares;
320
321
  }
321
322
 
323
+ /**
324
+ * Cache tool usage analysis
325
+ * @param {string} filepath - File path
326
+ * @param {Function} extractFn - Tool usage extraction function
327
+ * @returns {Promise<Object>} Tool usage data
328
+ */
329
+ async getCachedToolUsage(filepath, extractFn) {
330
+ const cached = this.caches.toolUsage.get(filepath);
331
+ const fileStats = await this.getFileStats(filepath);
332
+
333
+ if (cached && cached.timestamp >= fileStats.mtime.getTime()) {
334
+ this.metrics.hits++;
335
+ return cached.usage;
336
+ }
337
+
338
+ this.metrics.misses++;
339
+ const usage = await extractFn();
340
+
341
+ this.caches.toolUsage.set(filepath, {
342
+ usage,
343
+ timestamp: fileStats.mtime.getTime()
344
+ });
345
+
346
+ return usage;
347
+ }
348
+
322
349
  /**
323
350
  * Smart invalidation based on file changes
324
351
  * @param {string} filepath - Path of changed file
@@ -332,6 +359,7 @@ class DataCache {
332
359
  this.caches.tokenUsage.delete(filepath);
333
360
  this.caches.modelInfo.delete(filepath);
334
361
  this.caches.statusSquares.delete(filepath);
362
+ this.caches.toolUsage.delete(filepath);
335
363
  this.caches.fileStats.delete(filepath);
336
364
 
337
365
  // Invalidate computations that depend on this file
@@ -450,6 +478,7 @@ class DataCache {
450
478
  ['tokenUsage', this.caches.tokenUsage],
451
479
  ['modelInfo', this.caches.modelInfo],
452
480
  ['statusSquares', this.caches.statusSquares],
481
+ ['toolUsage', this.caches.toolUsage],
453
482
  ['fileStats', this.caches.fileStats],
454
483
  ['projectStats', this.caches.projectStats]
455
484
  ];
@@ -507,6 +536,7 @@ class DataCache {
507
536
  tokenUsage: this.caches.tokenUsage.size,
508
537
  modelInfo: this.caches.modelInfo.size,
509
538
  statusSquares: this.caches.statusSquares.size,
539
+ toolUsage: this.caches.toolUsage.size,
510
540
  fileStats: this.caches.fileStats.size,
511
541
  projectStats: this.caches.projectStats.size,
512
542
  },
@@ -7,6 +7,42 @@
7
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
8
8
  <script src="components/SessionTimer.js"></script>
9
9
  <style>
10
+ :root {
11
+ /* Dark theme colors (default) */
12
+ --bg-primary: #0d1117;
13
+ --bg-secondary: #161b22;
14
+ --bg-tertiary: #21262d;
15
+ --border-primary: #30363d;
16
+ --border-secondary: #21262d;
17
+ --text-primary: #c9d1d9;
18
+ --text-secondary: #7d8590;
19
+ --text-accent: #d57455;
20
+ --text-success: #3fb950;
21
+ --text-warning: #f97316;
22
+ --text-error: #f85149;
23
+ --text-info: #a5d6ff;
24
+ --shadow-primary: rgba(0, 0, 0, 0.4);
25
+ --shadow-secondary: rgba(1, 4, 9, 0.85);
26
+ }
27
+
28
+ [data-theme="light"] {
29
+ /* Light theme colors */
30
+ --bg-primary: #ffffff;
31
+ --bg-secondary: #f6f8fa;
32
+ --bg-tertiary: #f1f3f4;
33
+ --border-primary: #d0d7de;
34
+ --border-secondary: #e5e5e5;
35
+ --text-primary: #24292f;
36
+ --text-secondary: #656d76;
37
+ --text-accent: #d73a49;
38
+ --text-success: #28a745;
39
+ --text-warning: #f97316;
40
+ --text-error: #d73a49;
41
+ --text-info: #0366d6;
42
+ --shadow-primary: rgba(0, 0, 0, 0.1);
43
+ --shadow-secondary: rgba(0, 0, 0, 0.2);
44
+ }
45
+
10
46
  * {
11
47
  margin: 0;
12
48
  padding: 0;
@@ -15,10 +51,11 @@
15
51
 
16
52
  body {
17
53
  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
18
- background: #0d1117;
19
- color: #c9d1d9;
54
+ background: var(--bg-primary);
55
+ color: var(--text-primary);
20
56
  min-height: 100vh;
21
57
  line-height: 1.4;
58
+ transition: background-color 0.3s ease, color 0.3s ease;
22
59
  }
23
60
 
24
61
  .terminal {
@@ -28,14 +65,14 @@
28
65
  }
29
66
 
30
67
  .terminal-header {
31
- border-bottom: 1px solid #30363d;
68
+ border-bottom: 1px solid var(--border-primary);
32
69
  padding-bottom: 20px;
33
70
  margin-bottom: 20px;
34
71
  position: relative;
35
72
  }
36
73
 
37
74
  .terminal-title {
38
- color: #d57455;
75
+ color: var(--text-accent);
39
76
  font-size: 1.25rem;
40
77
  font-weight: normal;
41
78
  display: flex;
@@ -47,7 +84,7 @@
47
84
  width: 8px;
48
85
  height: 8px;
49
86
  border-radius: 50%;
50
- background: #3fb950;
87
+ background: var(--text-success);
51
88
  animation: pulse 2s infinite;
52
89
  }
53
90
 
@@ -57,18 +94,94 @@
57
94
  }
58
95
 
59
96
  .terminal-subtitle {
60
- color: #7d8590;
97
+ color: var(--text-secondary);
61
98
  font-size: 0.875rem;
62
99
  margin-top: 4px;
63
100
  }
64
101
 
65
- .github-star-btn {
102
+ .header-actions {
66
103
  position: absolute;
67
104
  top: 0;
68
105
  right: 0;
69
- background: #21262d;
70
- border: 1px solid #30363d;
71
- color: #c9d1d9;
106
+ display: flex;
107
+ gap: 8px;
108
+ align-items: center;
109
+ }
110
+
111
+ .theme-switch-container {
112
+ display: flex;
113
+ align-items: center;
114
+ font-family: inherit;
115
+ }
116
+
117
+ .theme-switch {
118
+ position: relative;
119
+ cursor: pointer;
120
+ }
121
+
122
+ .theme-switch-track {
123
+ position: relative;
124
+ width: 48px;
125
+ height: 24px;
126
+ background: var(--bg-tertiary);
127
+ border: 1px solid var(--border-primary);
128
+ border-radius: 3px;
129
+ transition: all 0.3s ease;
130
+ }
131
+
132
+ .theme-switch-track:hover {
133
+ border-color: var(--text-accent);
134
+ background: var(--border-primary);
135
+ }
136
+
137
+ .theme-switch-thumb {
138
+ position: absolute;
139
+ top: 2px;
140
+ left: 2px;
141
+ width: 18px;
142
+ height: 18px;
143
+ background: var(--text-accent);
144
+ border-radius: 2px;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
149
+ transform: translateX(0);
150
+ }
151
+
152
+ .theme-switch-thumb.light {
153
+ transform: translateX(22px);
154
+ }
155
+
156
+ .theme-switch-icon {
157
+ font-size: 0.7rem;
158
+ color: var(--bg-primary);
159
+ transition: all 0.3s ease;
160
+ }
161
+
162
+
163
+ .theme-switch-track::before {
164
+ content: '';
165
+ position: absolute;
166
+ top: -1px;
167
+ left: -1px;
168
+ right: -1px;
169
+ bottom: -1px;
170
+ background: linear-gradient(45deg, transparent, var(--text-accent));
171
+ border-radius: 3px;
172
+ opacity: 0;
173
+ transition: opacity 0.3s ease;
174
+ z-index: -1;
175
+ }
176
+
177
+ .theme-switch-track:hover::before {
178
+ opacity: 0.1;
179
+ }
180
+
181
+ .github-star-btn {
182
+ background: var(--bg-tertiary);
183
+ border: 1px solid var(--border-primary);
184
+ color: var(--text-primary);
72
185
  padding: 8px 12px;
73
186
  border-radius: 6px;
74
187
  text-decoration: none;
@@ -82,9 +195,9 @@
82
195
  }
83
196
 
84
197
  .github-star-btn:hover {
85
- border-color: #d57455;
86
- background: #30363d;
87
- color: #d57455;
198
+ border-color: var(--text-accent);
199
+ background: var(--border-primary);
200
+ color: var(--text-accent);
88
201
  text-decoration: none;
89
202
  }
90
203
 
@@ -710,7 +823,7 @@
710
823
 
711
824
  .charts-container {
712
825
  display: grid;
713
- grid-template-columns: 2fr 1fr;
826
+ grid-template-columns: 1fr 1fr;
714
827
  gap: 30px;
715
828
  margin: 20px 0 30px 0;
716
829
  }
@@ -738,6 +851,85 @@
738
851
  height: 200px !important;
739
852
  }
740
853
 
854
+ .tool-summary {
855
+ padding: 20px;
856
+ height: 200px;
857
+ display: flex;
858
+ flex-direction: column;
859
+ gap: 12px;
860
+ overflow-y: auto;
861
+ }
862
+
863
+ .tool-stat {
864
+ display: flex;
865
+ justify-content: space-between;
866
+ align-items: center;
867
+ padding: 8px 12px;
868
+ background: var(--bg-tertiary);
869
+ border: 1px solid var(--border-secondary);
870
+ border-radius: 4px;
871
+ transition: all 0.2s ease;
872
+ font-size: 0.8rem;
873
+ }
874
+
875
+ .tool-stat:hover {
876
+ background: var(--bg-secondary);
877
+ border-color: var(--border-primary);
878
+ }
879
+
880
+ .tool-stat-label {
881
+ color: var(--text-secondary);
882
+ font-weight: 500;
883
+ }
884
+
885
+ .tool-stat-value {
886
+ color: var(--text-primary);
887
+ font-weight: 600;
888
+ }
889
+
890
+ .tool-stat-accent {
891
+ color: var(--text-accent);
892
+ }
893
+
894
+ .tool-top-tool {
895
+ display: flex;
896
+ align-items: center;
897
+ gap: 8px;
898
+ padding: 12px;
899
+ background: var(--bg-tertiary);
900
+ border: 1px solid var(--border-secondary);
901
+ border-radius: 4px;
902
+ margin-top: auto;
903
+ }
904
+
905
+ .tool-icon {
906
+ width: 24px;
907
+ height: 24px;
908
+ background: var(--text-accent);
909
+ border-radius: 4px;
910
+ display: flex;
911
+ align-items: center;
912
+ justify-content: center;
913
+ font-size: 12px;
914
+ color: white;
915
+ }
916
+
917
+ .tool-info {
918
+ flex: 1;
919
+ }
920
+
921
+ .tool-name {
922
+ color: var(--text-primary);
923
+ font-size: 0.8rem;
924
+ font-weight: 600;
925
+ margin-bottom: 2px;
926
+ }
927
+
928
+ .tool-usage {
929
+ color: var(--text-secondary);
930
+ font-size: 0.7rem;
931
+ }
932
+
741
933
  .filter-bar {
742
934
  display: flex;
743
935
  align-items: center;
@@ -1317,12 +1509,20 @@
1317
1509
  padding: 6px 8px;
1318
1510
  }
1319
1511
 
1320
- .github-star-btn {
1512
+ .header-actions {
1321
1513
  position: relative;
1322
1514
  margin-top: 12px;
1323
1515
  align-self: flex-start;
1516
+ gap: 12px;
1324
1517
  }
1325
1518
 
1519
+ .github-star-btn {
1520
+ position: relative;
1521
+ margin-top: 0;
1522
+ align-self: flex-start;
1523
+ }
1524
+
1525
+
1326
1526
  .terminal-header {
1327
1527
  display: flex;
1328
1528
  flex-direction: column;
@@ -1340,10 +1540,21 @@
1340
1540
  <div class="terminal-subtitle">real-time monitoring dashboard</div>
1341
1541
  <div class="terminal-subtitle" id="lastUpdate"></div>
1342
1542
 
1343
- <a href="https://github.com/davila7/claude-code-templates" target="_blank" class="github-star-btn" title="Give us a star on GitHub to support the project!">
1344
- <span class="star-icon">⭐</span>
1345
- <span>Star on GitHub</span>
1346
- </a>
1543
+ <div class="header-actions">
1544
+ <div class="theme-switch-container" title="Toggle light/dark theme">
1545
+ <div class="theme-switch" id="themeSwitch">
1546
+ <div class="theme-switch-track">
1547
+ <div class="theme-switch-thumb" id="themeSwitchThumb">
1548
+ <span class="theme-switch-icon">🌙</span>
1549
+ </div>
1550
+ </div>
1551
+ </div>
1552
+ </div>
1553
+ <a href="https://github.com/davila7/claude-code-templates" target="_blank" class="github-star-btn" title="Give us a star on GitHub to support the project!">
1554
+ <span class="star-icon">⭐</span>
1555
+ <span>Star on GitHub</span>
1556
+ </a>
1557
+ </div>
1347
1558
  </div>
1348
1559
 
1349
1560
  <div id="loading" class="loading">
@@ -1453,6 +1664,22 @@
1453
1664
  </div>
1454
1665
  <canvas id="projectChart" class="chart-canvas"></canvas>
1455
1666
  </div>
1667
+
1668
+ <div class="chart-card">
1669
+ <div class="chart-title">
1670
+ 🛠️ tool usage trends
1671
+ </div>
1672
+ <canvas id="toolChart" class="chart-canvas"></canvas>
1673
+ </div>
1674
+
1675
+ <div class="chart-card">
1676
+ <div class="chart-title">
1677
+ ⚡ tool activity summary
1678
+ </div>
1679
+ <div id="toolSummary" class="tool-summary">
1680
+ <!-- Tool summary will be loaded here -->
1681
+ </div>
1682
+ </div>
1456
1683
  </div>
1457
1684
 
1458
1685
  <div class="filter-bar">
@@ -1530,6 +1757,7 @@
1530
1757
  let currentSession = null;
1531
1758
  let tokenChart = null;
1532
1759
  let projectChart = null;
1760
+ let toolChart = null;
1533
1761
  let allData = null;
1534
1762
  let notificationsEnabled = false;
1535
1763
  let previousConversationStates = new Map();
@@ -1899,6 +2127,12 @@
1899
2127
 
1900
2128
  // Update Project Activity Distribution Chart
1901
2129
  updateProjectChart(data.conversations);
2130
+
2131
+ // Update Tool Usage Trends Chart
2132
+ updateToolChart(data.conversations);
2133
+
2134
+ // Update Tool Activity Summary
2135
+ updateToolSummary(data.conversations);
1902
2136
  }
1903
2137
 
1904
2138
  async function refreshCharts() {
@@ -1962,11 +2196,11 @@
1962
2196
  datasets: [{
1963
2197
  label: 'Tokens',
1964
2198
  data: dateRange.map(day => day.tokens),
1965
- borderColor: '#d57455',
1966
- backgroundColor: 'rgba(213, 116, 85, 0.1)',
2199
+ borderColor: getComputedStyle(document.documentElement).getPropertyValue('--text-accent').trim(),
2200
+ backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--text-accent').trim() + '20',
1967
2201
  borderWidth: 2,
1968
- pointBackgroundColor: '#d57455',
1969
- pointBorderColor: '#d57455',
2202
+ pointBackgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--text-accent').trim(),
2203
+ pointBorderColor: getComputedStyle(document.documentElement).getPropertyValue('--text-accent').trim(),
1970
2204
  pointRadius: 4,
1971
2205
  pointHoverRadius: 6,
1972
2206
  fill: true,
@@ -2076,14 +2310,14 @@
2076
2310
  sortedProjects.push(['others', othersTotal]);
2077
2311
  }
2078
2312
 
2079
- // Terminal-style colors
2313
+ // Terminal-style colors using CSS variables
2080
2314
  const colors = [
2081
- '#d57455', // Orange
2082
- '#3fb950', // Green
2083
- '#a5d6ff', // Blue
2084
- '#f97316', // Orange variant
2085
- '#c9d1d9', // Light gray
2086
- '#7d8590' // Gray
2315
+ getComputedStyle(document.documentElement).getPropertyValue('--text-accent').trim(),
2316
+ getComputedStyle(document.documentElement).getPropertyValue('--text-success').trim(),
2317
+ getComputedStyle(document.documentElement).getPropertyValue('--text-info').trim(),
2318
+ getComputedStyle(document.documentElement).getPropertyValue('--text-warning').trim(),
2319
+ getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim(),
2320
+ getComputedStyle(document.documentElement).getPropertyValue('--text-secondary').trim()
2087
2321
  ];
2088
2322
 
2089
2323
  const ctx = document.getElementById('projectChart').getContext('2d');
@@ -2099,7 +2333,7 @@
2099
2333
  datasets: [{
2100
2334
  data: sortedProjects.map(([,tokens]) => tokens),
2101
2335
  backgroundColor: colors.slice(0, sortedProjects.length),
2102
- borderColor: '#161b22',
2336
+ borderColor: getComputedStyle(document.documentElement).getPropertyValue('--bg-secondary').trim(),
2103
2337
  borderWidth: 2,
2104
2338
  hoverBorderWidth: 3
2105
2339
  }]
@@ -2147,6 +2381,329 @@
2147
2381
  });
2148
2382
  }
2149
2383
 
2384
+ function updateToolChart(conversations) {
2385
+ // Check if Chart.js is available
2386
+ if (typeof Chart === 'undefined') {
2387
+ console.warn('Chart.js not available for updateToolChart');
2388
+ return;
2389
+ }
2390
+
2391
+ // Apply date filtering to conversations first
2392
+ const { fromDate, toDate } = getDateRange();
2393
+ const filteredConversations = conversations.filter(conv => {
2394
+ const convDate = new Date(conv.lastModified);
2395
+ return convDate >= fromDate && convDate <= toDate;
2396
+ });
2397
+
2398
+ // Aggregate tool usage across filtered conversations
2399
+ const toolTimeline = [];
2400
+ const toolStats = {};
2401
+
2402
+ filteredConversations.forEach(conv => {
2403
+ if (conv.toolUsage && conv.toolUsage.toolTimeline) {
2404
+ conv.toolUsage.toolTimeline.forEach(entry => {
2405
+ const entryDate = new Date(entry.timestamp);
2406
+ // Only include tool usage within date range
2407
+ if (entryDate >= fromDate && entryDate <= toDate) {
2408
+ toolTimeline.push({
2409
+ tool: entry.tool,
2410
+ timestamp: entryDate,
2411
+ date: entryDate.toDateString()
2412
+ });
2413
+
2414
+ toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1;
2415
+ }
2416
+ });
2417
+ }
2418
+ });
2419
+
2420
+ const ctx = document.getElementById('toolChart').getContext('2d');
2421
+
2422
+ if (toolChart) {
2423
+ toolChart.destroy();
2424
+ }
2425
+
2426
+ if (Object.keys(toolStats).length === 0) {
2427
+ // No tool usage data available
2428
+ toolChart = new Chart(ctx, {
2429
+ type: 'bar',
2430
+ data: {
2431
+ labels: ['No Data'],
2432
+ datasets: [{
2433
+ label: 'Tool Usage',
2434
+ data: [0],
2435
+ backgroundColor: 'rgba(109, 40, 217, 0.6)',
2436
+ borderColor: 'rgba(109, 40, 217, 1)',
2437
+ borderWidth: 1
2438
+ }]
2439
+ },
2440
+ options: {
2441
+ responsive: true,
2442
+ maintainAspectRatio: false,
2443
+ interaction: {
2444
+ intersect: false,
2445
+ mode: 'nearest'
2446
+ },
2447
+ plugins: {
2448
+ legend: {
2449
+ display: false
2450
+ },
2451
+ tooltip: {
2452
+ enabled: true,
2453
+ position: 'nearest',
2454
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
2455
+ titleColor: '#fff',
2456
+ bodyColor: '#fff',
2457
+ borderColor: 'rgba(255, 255, 255, 0.1)',
2458
+ borderWidth: 1
2459
+ }
2460
+ },
2461
+ scales: {
2462
+ y: {
2463
+ beginAtZero: true,
2464
+ ticks: {
2465
+ color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary').trim()
2466
+ },
2467
+ grid: {
2468
+ color: getComputedStyle(document.documentElement).getPropertyValue('--border-primary').trim()
2469
+ }
2470
+ },
2471
+ x: {
2472
+ ticks: {
2473
+ color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary').trim()
2474
+ },
2475
+ grid: {
2476
+ color: getComputedStyle(document.documentElement).getPropertyValue('--border-primary').trim()
2477
+ }
2478
+ }
2479
+ }
2480
+ }
2481
+ });
2482
+ return;
2483
+ }
2484
+
2485
+ // Get top 10 most used tools
2486
+ const sortedTools = Object.entries(toolStats)
2487
+ .sort((a, b) => b[1] - a[1])
2488
+ .slice(0, 10);
2489
+
2490
+ // Generate colors for different tools
2491
+ const colors = [
2492
+ 'rgba(75, 192, 192, 0.6)',
2493
+ 'rgba(255, 99, 132, 0.6)',
2494
+ 'rgba(54, 162, 235, 0.6)',
2495
+ 'rgba(255, 206, 86, 0.6)',
2496
+ 'rgba(153, 102, 255, 0.6)',
2497
+ 'rgba(255, 159, 64, 0.6)',
2498
+ 'rgba(199, 199, 199, 0.6)',
2499
+ 'rgba(83, 102, 255, 0.6)',
2500
+ 'rgba(255, 99, 255, 0.6)',
2501
+ 'rgba(99, 255, 132, 0.6)'
2502
+ ];
2503
+
2504
+ const borderColors = [
2505
+ 'rgba(75, 192, 192, 1)',
2506
+ 'rgba(255, 99, 132, 1)',
2507
+ 'rgba(54, 162, 235, 1)',
2508
+ 'rgba(255, 206, 86, 1)',
2509
+ 'rgba(153, 102, 255, 1)',
2510
+ 'rgba(255, 159, 64, 1)',
2511
+ 'rgba(199, 199, 199, 1)',
2512
+ 'rgba(83, 102, 255, 1)',
2513
+ 'rgba(255, 99, 255, 1)',
2514
+ 'rgba(99, 255, 132, 1)'
2515
+ ];
2516
+
2517
+ // Store sorted tools data for reliable tooltip access
2518
+ window.currentToolData = sortedTools;
2519
+
2520
+ toolChart = new Chart(ctx, {
2521
+ type: 'bar',
2522
+ data: {
2523
+ labels: sortedTools.map(([tool]) => {
2524
+ // Truncate long tool names for display
2525
+ return tool.length > 15 ? tool.substring(0, 15) + '...' : tool;
2526
+ }),
2527
+ datasets: [{
2528
+ label: 'Usage Count',
2529
+ data: sortedTools.map(([, count]) => count),
2530
+ backgroundColor: colors.slice(0, sortedTools.length),
2531
+ borderColor: borderColors.slice(0, sortedTools.length),
2532
+ borderWidth: 1
2533
+ }]
2534
+ },
2535
+ options: {
2536
+ responsive: true,
2537
+ maintainAspectRatio: false,
2538
+ interaction: {
2539
+ intersect: false,
2540
+ mode: 'nearest'
2541
+ },
2542
+ plugins: {
2543
+ legend: {
2544
+ display: false
2545
+ },
2546
+ tooltip: {
2547
+ enabled: true,
2548
+ position: 'nearest',
2549
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
2550
+ titleColor: '#fff',
2551
+ bodyColor: '#fff',
2552
+ borderColor: 'rgba(255, 255, 255, 0.1)',
2553
+ borderWidth: 1,
2554
+ callbacks: {
2555
+ title: function(tooltipItems) {
2556
+ const index = tooltipItems[0].dataIndex;
2557
+ // Use the stored tool data for reliable access
2558
+ return window.currentToolData && window.currentToolData[index]
2559
+ ? window.currentToolData[index][0]
2560
+ : 'Tool';
2561
+ },
2562
+ label: function(context) {
2563
+ const value = context.parsed.y;
2564
+ const total = window.currentToolData ?
2565
+ window.currentToolData.reduce((sum, [, count]) => sum + count, 0) :
2566
+ context.dataset.data.reduce((sum, val) => sum + val, 0);
2567
+ const percentage = ((value / total) * 100).toFixed(1);
2568
+ return `Used ${value} times (${percentage}%)`;
2569
+ }
2570
+ }
2571
+ }
2572
+ },
2573
+ scales: {
2574
+ y: {
2575
+ beginAtZero: true,
2576
+ ticks: {
2577
+ color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary').trim(),
2578
+ stepSize: 1
2579
+ },
2580
+ grid: {
2581
+ color: getComputedStyle(document.documentElement).getPropertyValue('--border-primary').trim()
2582
+ }
2583
+ },
2584
+ x: {
2585
+ ticks: {
2586
+ color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary').trim(),
2587
+ maxRotation: 45,
2588
+ minRotation: 0
2589
+ },
2590
+ grid: {
2591
+ color: getComputedStyle(document.documentElement).getPropertyValue('--border-primary').trim()
2592
+ }
2593
+ }
2594
+ }
2595
+ }
2596
+ });
2597
+ }
2598
+
2599
+ function updateToolSummary(conversations) {
2600
+ // Apply date filtering to conversations first
2601
+ const { fromDate, toDate } = getDateRange();
2602
+ const filteredConversations = conversations.filter(conv => {
2603
+ const convDate = new Date(conv.lastModified);
2604
+ return convDate >= fromDate && convDate <= toDate;
2605
+ });
2606
+
2607
+ // Aggregate tool usage across filtered conversations
2608
+ const toolStats = {};
2609
+ let totalToolCalls = 0;
2610
+ let conversationsWithTools = 0;
2611
+ let totalUniqueTools = new Set();
2612
+
2613
+ filteredConversations.forEach(conv => {
2614
+ if (conv.toolUsage && conv.toolUsage.toolTimeline) {
2615
+ let convHasTools = false;
2616
+ conv.toolUsage.toolTimeline.forEach(entry => {
2617
+ const entryDate = new Date(entry.timestamp);
2618
+ // Only include tool usage within date range
2619
+ if (entryDate >= fromDate && entryDate <= toDate) {
2620
+ toolStats[entry.tool] = (toolStats[entry.tool] || 0) + 1;
2621
+ totalToolCalls++;
2622
+ totalUniqueTools.add(entry.tool);
2623
+ convHasTools = true;
2624
+ }
2625
+ });
2626
+ if (convHasTools) {
2627
+ conversationsWithTools++;
2628
+ }
2629
+ }
2630
+ });
2631
+
2632
+ const summaryElement = document.getElementById('toolSummary');
2633
+
2634
+ if (totalToolCalls === 0) {
2635
+ summaryElement.innerHTML = `
2636
+ <div class="tool-stat">
2637
+ <span class="tool-stat-label">No tool usage data available</span>
2638
+ <span class="tool-stat-value">📊</span>
2639
+ </div>
2640
+ `;
2641
+ return;
2642
+ }
2643
+
2644
+ // Find most used tool
2645
+ const sortedTools = Object.entries(toolStats).sort((a, b) => b[1] - a[1]);
2646
+ const topTool = sortedTools[0];
2647
+ const avgToolsPerConv = conversationsWithTools > 0 ? (totalToolCalls / conversationsWithTools).toFixed(1) : 0;
2648
+
2649
+ // Calculate tool diversity using Shannon Diversity Index (more accurate)
2650
+ const toolCount = sortedTools.length;
2651
+ let shannonIndex = 0;
2652
+ let diversityScore = 0;
2653
+
2654
+ if (toolCount > 1) {
2655
+ // Calculate Shannon diversity index
2656
+ sortedTools.forEach(([, count]) => {
2657
+ const proportion = count / totalToolCalls;
2658
+ if (proportion > 0) {
2659
+ shannonIndex -= proportion * Math.log2(proportion);
2660
+ }
2661
+ });
2662
+
2663
+ // Normalize to 0-100% scale
2664
+ const maxDiversity = Math.log2(toolCount); // Maximum possible diversity
2665
+ diversityScore = (shannonIndex / maxDiversity) * 100;
2666
+ }
2667
+ // If only one tool used, diversityScore remains 0
2668
+
2669
+ summaryElement.innerHTML = `
2670
+ <div class="tool-stat">
2671
+ <span class="tool-stat-label">Total Tool Calls</span>
2672
+ <span class="tool-stat-value tool-stat-accent">${totalToolCalls}</span>
2673
+ </div>
2674
+
2675
+ <div class="tool-stat">
2676
+ <span class="tool-stat-label">Unique Tools Used</span>
2677
+ <span class="tool-stat-value">${totalUniqueTools.size}</span>
2678
+ </div>
2679
+
2680
+ <div class="tool-stat">
2681
+ <span class="tool-stat-label">Conversations with Tools</span>
2682
+ <span class="tool-stat-value">${conversationsWithTools}/${filteredConversations.length}</span>
2683
+ </div>
2684
+
2685
+ <div class="tool-stat">
2686
+ <span class="tool-stat-label">Avg Tools per Conversation</span>
2687
+ <span class="tool-stat-value">${avgToolsPerConv}</span>
2688
+ </div>
2689
+
2690
+ <div class="tool-stat" title="How evenly distributed tool usage is. 100% = perfectly balanced, 0% = concentrated in few tools">
2691
+ <span class="tool-stat-label">Tool Balance Score</span>
2692
+ <span class="tool-stat-value">${diversityScore.toFixed(0)}%</span>
2693
+ </div>
2694
+
2695
+ ${topTool ? `
2696
+ <div class="tool-top-tool">
2697
+ <div class="tool-icon">🏆</div>
2698
+ <div class="tool-info">
2699
+ <div class="tool-name">Most Used: ${topTool[0]}</div>
2700
+ <div class="tool-usage">${topTool[1]} calls (${((topTool[1] / totalToolCalls) * 100).toFixed(1)}%)</div>
2701
+ </div>
2702
+ </div>
2703
+ ` : ''}
2704
+ `;
2705
+ }
2706
+
2150
2707
  function updateSessionsTable() {
2151
2708
  const tableBody = document.getElementById('sessionsTable');
2152
2709
  const noSessionsDiv = document.getElementById('noSessions');
@@ -2646,6 +3203,9 @@
2646
3203
  document.getElementById('dateFrom').addEventListener('change', refreshCharts);
2647
3204
  document.getElementById('dateTo').addEventListener('change', refreshCharts);
2648
3205
 
3206
+ // Initialize theme toggle
3207
+ initializeTheme();
3208
+
2649
3209
  // Initialize notification button state
2650
3210
  updateNotificationButtonState();
2651
3211
  });
@@ -2668,7 +3228,11 @@
2668
3228
  getSessionData: async () => {
2669
3229
  try {
2670
3230
  const response = await fetch('/api/session/data');
2671
- return await response.json();
3231
+ if (!response.ok) {
3232
+ throw new Error(`HTTP error! status: ${response.status}`);
3233
+ }
3234
+ const data = await response.json();
3235
+ return data;
2672
3236
  } catch (error) {
2673
3237
  console.error('Error fetching session data:', error);
2674
3238
  return null;
@@ -2707,6 +3271,49 @@
2707
3271
  }
2708
3272
  }
2709
3273
 
3274
+ // Theme toggle functionality
3275
+ function initializeTheme() {
3276
+ const savedTheme = localStorage.getItem('claude-analytics-theme') || 'dark';
3277
+ const body = document.body;
3278
+ const themeSwitch = document.getElementById('themeSwitch');
3279
+ const themeSwitchThumb = document.getElementById('themeSwitchThumb');
3280
+ const themeIcon = themeSwitchThumb.querySelector('.theme-switch-icon');
3281
+
3282
+ function setTheme(theme) {
3283
+ if (theme === 'light') {
3284
+ body.setAttribute('data-theme', 'light');
3285
+ themeSwitchThumb.classList.add('light');
3286
+ themeIcon.textContent = '☀️';
3287
+ } else {
3288
+ body.removeAttribute('data-theme');
3289
+ themeSwitchThumb.classList.remove('light');
3290
+ themeIcon.textContent = '🌙';
3291
+ }
3292
+ localStorage.setItem('claude-analytics-theme', theme);
3293
+
3294
+ // Update charts if they exist to use new theme colors
3295
+ if (tokenChart || projectChart || toolChart) {
3296
+ setTimeout(() => {
3297
+ if (allData) {
3298
+ updateCharts(allData);
3299
+ }
3300
+ }, 100); // Small delay to let CSS variables update
3301
+ }
3302
+ }
3303
+
3304
+ function toggleTheme() {
3305
+ const currentTheme = body.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
3306
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
3307
+ setTheme(newTheme);
3308
+ }
3309
+
3310
+ // Set initial theme
3311
+ setTheme(savedTheme);
3312
+
3313
+ // Add click event listener
3314
+ themeSwitch.addEventListener('click', toggleTheme);
3315
+ }
3316
+
2710
3317
  // Add keyboard shortcut for refresh (F5 or Ctrl+R)
2711
3318
  document.addEventListener('keydown', function(e) {
2712
3319
  if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {