claude-code-templates 1.20.1 → 1.20.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.
package/src/analytics.js CHANGED
@@ -1191,6 +1191,27 @@ class ClaudeAnalytics {
1191
1191
  }
1192
1192
  });
1193
1193
 
1194
+ // Activity heatmap data endpoint - needs full conversation history
1195
+ this.app.get('/api/activity', async (req, res) => {
1196
+ try {
1197
+ console.log(`🔥 /api/activity called - loading all conversations...`);
1198
+ // Get all conversations without the 150 limit for accurate heatmap data
1199
+ const allConversations = await this.conversationAnalyzer.loadConversations(this.stateCalculator);
1200
+ console.log(`🔥 Loaded ${allConversations.length} conversations from server`);
1201
+
1202
+ // Generate activity data using complete dataset
1203
+ const activityData = this.generateActivityDataFromConversations(allConversations);
1204
+ res.json({
1205
+ conversations: allConversations, // Also include conversations for the heatmap component
1206
+ ...activityData,
1207
+ timestamp: new Date().toISOString()
1208
+ });
1209
+ } catch (error) {
1210
+ console.error('Error generating activity data:', error);
1211
+ res.status(500).json({ error: 'Failed to generate activity data' });
1212
+ }
1213
+ });
1214
+
1194
1215
  // Main dashboard route
1195
1216
  this.app.get('/', (req, res) => {
1196
1217
  res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
@@ -2033,6 +2054,172 @@ class ClaudeAnalytics {
2033
2054
  }
2034
2055
  }
2035
2056
 
2057
+ /**
2058
+ * Generate activity data for the heatmap using complete conversation set
2059
+ * @param {Array} conversations - Complete conversation array (not limited)
2060
+ * @returns {Object} Activity data including daily contributions and stats
2061
+ */
2062
+ generateActivityDataFromConversations(conversations) {
2063
+ const dailyActivity = new Map();
2064
+ const oneYearAgo = new Date();
2065
+ oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
2066
+ oneYearAgo.setHours(0, 0, 0, 0);
2067
+
2068
+ const today = new Date();
2069
+ today.setHours(23, 59, 59, 999);
2070
+
2071
+ console.log(`🔥 Generating activity data for ${conversations.length} conversations (FULL DATASET)...`);
2072
+
2073
+ // Show date range of conversations
2074
+ if (conversations.length > 0) {
2075
+ const dates = conversations.map(c => c.lastModified ? new Date(c.lastModified) : null).filter(Boolean);
2076
+ dates.sort((a, b) => a - b);
2077
+ console.log(`🔥 Server conversation date range: ${dates[0]?.toLocaleDateString()} to ${dates[dates.length - 1]?.toLocaleDateString()}`);
2078
+
2079
+ // Show some sample conversations with dates
2080
+ console.log(`🔥 First 3 conversations by date:`);
2081
+ const sortedConversations = conversations
2082
+ .filter(c => c.lastModified)
2083
+ .sort((a, b) => new Date(a.lastModified) - new Date(b.lastModified))
2084
+ .slice(0, 3);
2085
+ sortedConversations.forEach((conv, i) => {
2086
+ console.log(` ${i+1}: ${conv.filename} - ${new Date(conv.lastModified).toLocaleDateString()}`);
2087
+ });
2088
+ }
2089
+
2090
+ // Process conversations for daily activity
2091
+ conversations.forEach(conversation => {
2092
+ if (!conversation.lastModified) return;
2093
+
2094
+ const date = new Date(conversation.lastModified);
2095
+ if (date < oneYearAgo || date > today) return;
2096
+
2097
+ const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
2098
+
2099
+ const current = dailyActivity.get(dateKey) || {
2100
+ conversations: 0,
2101
+ tokens: 0,
2102
+ messages: 0,
2103
+ tools: 0,
2104
+ date: dateKey
2105
+ };
2106
+
2107
+ current.conversations += 1;
2108
+ current.tokens += conversation.tokens || 0;
2109
+ current.messages += conversation.messageCount || 0;
2110
+ current.tools += (conversation.toolUsage?.totalToolCalls || 0);
2111
+
2112
+ dailyActivity.set(dateKey, current);
2113
+ });
2114
+
2115
+ // Convert to array and sort by date
2116
+ const activityArray = Array.from(dailyActivity.values())
2117
+ .sort((a, b) => a.date.localeCompare(b.date));
2118
+
2119
+ // Calculate stats
2120
+ const totalContributions = activityArray.reduce((sum, day) => sum + day.conversations, 0);
2121
+ const totalTools = activityArray.reduce((sum, day) => sum + day.tools, 0);
2122
+ const { longestStreak, currentStreak } = this.calculateStreaks(activityArray);
2123
+
2124
+ console.log(`🔥 Activity data generated: ${activityArray.length} active days, ${totalContributions} total contributions, ${totalTools} tool calls`);
2125
+
2126
+ return {
2127
+ dailyActivity: activityArray,
2128
+ totalContributions,
2129
+ totalTools,
2130
+ longestStreak,
2131
+ currentStreak,
2132
+ activeDays: activityArray.length,
2133
+ startDate: oneYearAgo.toISOString().split('T')[0],
2134
+ endDate: today.toISOString().split('T')[0]
2135
+ };
2136
+ }
2137
+
2138
+ /**
2139
+ * Generate activity data for the heatmap (legacy method for backward compatibility)
2140
+ * @returns {Object} Activity data including daily contributions and stats
2141
+ */
2142
+ generateActivityData() {
2143
+ if (!this.data || !this.data.conversations) {
2144
+ return {
2145
+ dailyActivity: [],
2146
+ totalContributions: 0,
2147
+ longestStreak: 0,
2148
+ currentStreak: 0
2149
+ };
2150
+ }
2151
+
2152
+ return this.generateActivityDataFromConversations(this.data.conversations);
2153
+ }
2154
+
2155
+ /**
2156
+ * Calculate contribution streaks
2157
+ * @param {Array} activityArray - Sorted array of daily activity
2158
+ * @returns {Object} Longest and current streak counts
2159
+ */
2160
+ calculateStreaks(activityArray) {
2161
+ if (activityArray.length === 0) {
2162
+ return { longestStreak: 0, currentStreak: 0 };
2163
+ }
2164
+
2165
+ let longestStreak = 0;
2166
+ let currentStreak = 0;
2167
+ let tempStreak = 0;
2168
+
2169
+ const today = new Date();
2170
+ today.setHours(0, 0, 0, 0);
2171
+
2172
+ // Create a map of active dates for quick lookup
2173
+ const activeDates = new Set(activityArray.map(day => day.date));
2174
+
2175
+ // Check streak going backwards from today
2176
+ let checkDate = new Date(today);
2177
+ let foundToday = false;
2178
+
2179
+ // First check if today or yesterday has activity
2180
+ for (let i = 0; i < 2; i++) {
2181
+ const dateKey = checkDate.toISOString().split('T')[0];
2182
+ if (activeDates.has(dateKey)) {
2183
+ foundToday = true;
2184
+ break;
2185
+ }
2186
+ checkDate.setDate(checkDate.getDate() - 1);
2187
+ }
2188
+
2189
+ if (foundToday) {
2190
+ // Calculate current streak
2191
+ checkDate = new Date(today);
2192
+ while (true) {
2193
+ const dateKey = checkDate.toISOString().split('T')[0];
2194
+ if (activeDates.has(dateKey)) {
2195
+ currentStreak++;
2196
+ checkDate.setDate(checkDate.getDate() - 1);
2197
+ } else {
2198
+ break;
2199
+ }
2200
+ }
2201
+ }
2202
+
2203
+ // Calculate longest streak by checking all consecutive days
2204
+ const oneYearAgo = new Date(today);
2205
+ oneYearAgo.setFullYear(today.getFullYear() - 1);
2206
+
2207
+ checkDate = new Date(oneYearAgo);
2208
+ while (checkDate <= today) {
2209
+ const dateKey = checkDate.toISOString().split('T')[0];
2210
+
2211
+ if (activeDates.has(dateKey)) {
2212
+ tempStreak++;
2213
+ longestStreak = Math.max(longestStreak, tempStreak);
2214
+ } else {
2215
+ tempStreak = 0;
2216
+ }
2217
+
2218
+ checkDate.setDate(checkDate.getDate() + 1);
2219
+ }
2220
+
2221
+ return { longestStreak, currentStreak };
2222
+ }
2036
2223
 
2037
2224
  stop() {
2038
2225
  // Stop file watchers
package/src/index.js CHANGED
@@ -607,6 +607,7 @@ async function installIndividualSetting(settingName, targetDir, options) {
607
607
  if (response.status === 404) {
608
608
  console.log(chalk.red(`❌ Setting "${settingName}" not found`));
609
609
  console.log(chalk.yellow('Available settings: enable-telemetry, disable-telemetry, allow-npm-commands, deny-sensitive-files, use-sonnet, use-haiku, retention-7-days, retention-90-days'));
610
+ console.log(chalk.yellow('Available statuslines: statusline/context-monitor'));
610
611
  return;
611
612
  }
612
613
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -615,9 +616,39 @@ async function installIndividualSetting(settingName, targetDir, options) {
615
616
  const settingConfigText = await response.text();
616
617
  const settingConfig = JSON.parse(settingConfigText);
617
618
 
618
- // Remove description field before merging
619
+ // Check if there are additional files to download (e.g., Python scripts)
620
+ const additionalFiles = {};
621
+
622
+ // For statusline settings, check if there's a corresponding Python file
623
+ if (settingName.includes('statusline/')) {
624
+ const pythonFileName = settingName.split('/')[1] + '.py';
625
+ const pythonUrl = githubUrl.replace('.json', '.py');
626
+
627
+ try {
628
+ console.log(chalk.gray(`📥 Downloading Python script: ${pythonFileName}...`));
629
+ const pythonResponse = await fetch(pythonUrl);
630
+ if (pythonResponse.ok) {
631
+ const pythonContent = await pythonResponse.text();
632
+ additionalFiles['.claude/scripts/' + pythonFileName] = {
633
+ content: pythonContent,
634
+ executable: true
635
+ };
636
+ }
637
+ } catch (error) {
638
+ console.log(chalk.yellow(`⚠️ Could not download Python script: ${error.message}`));
639
+ }
640
+ }
641
+
642
+ // Extract and handle additional files before removing them from config
643
+ const configFiles = settingConfig.files || {};
644
+
645
+ // Merge downloaded files with config files
646
+ Object.assign(additionalFiles, configFiles);
647
+
648
+ // Remove description and files fields before merging
619
649
  if (settingConfig && typeof settingConfig === 'object') {
620
650
  delete settingConfig.description;
651
+ delete settingConfig.files;
621
652
  }
622
653
 
623
654
  // Use shared locations if provided (batch mode), otherwise ask user
@@ -817,6 +848,37 @@ async function installIndividualSetting(settingName, targetDir, options) {
817
848
  // Write the merged configuration
818
849
  await fs.writeJson(actualTargetFile, mergedConfig, { spaces: 2 });
819
850
 
851
+ // Install additional files if any exist
852
+ if (Object.keys(additionalFiles).length > 0) {
853
+ console.log(chalk.blue(`📄 Installing ${Object.keys(additionalFiles).length} additional file(s)...`));
854
+
855
+ for (const [filePath, fileConfig] of Object.entries(additionalFiles)) {
856
+ try {
857
+ // Resolve tilde (~) to home directory
858
+ const resolvedFilePath = filePath.startsWith('~')
859
+ ? path.join(require('os').homedir(), filePath.slice(1))
860
+ : path.resolve(currentTargetDir, filePath);
861
+
862
+ // Ensure directory exists
863
+ await fs.ensureDir(path.dirname(resolvedFilePath));
864
+
865
+ // Write file content
866
+ await fs.writeFile(resolvedFilePath, fileConfig.content, 'utf8');
867
+
868
+ // Make file executable if specified
869
+ if (fileConfig.executable) {
870
+ await fs.chmod(resolvedFilePath, 0o755);
871
+ console.log(chalk.gray(`🔧 Made executable: ${resolvedFilePath}`));
872
+ }
873
+
874
+ console.log(chalk.green(`✅ File installed: ${resolvedFilePath}`));
875
+
876
+ } catch (fileError) {
877
+ console.log(chalk.red(`❌ Failed to install file ${filePath}: ${fileError.message}`));
878
+ }
879
+ }
880
+ }
881
+
820
882
  if (!options.silent) {
821
883
  console.log(chalk.green(`✅ Setting "${settingName}" installed successfully in ${installLocation}!`));
822
884
  console.log(chalk.cyan(`📁 Configuration merged into: ${actualTargetFile}`));