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/bin/create-claude-config.js +25 -14
- package/package.json +1 -1
- package/src/analytics/core/AgentAnalyzer.js +3 -3
- package/src/analytics/core/ConversationAnalyzer.js +2 -1
- package/src/analytics-web/components/ActivityHeatmap.js +738 -0
- package/src/analytics-web/components/DashboardPage.js +54 -0
- package/src/analytics-web/index.html +471 -1
- package/src/analytics.js +187 -0
- package/src/index.js +63 -1
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
|
-
//
|
|
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}`));
|