claude-code-templates 1.28.2 → 1.28.4

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
@@ -14,6 +14,7 @@ const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
14
14
  const FileWatcher = require('./analytics/core/FileWatcher');
15
15
  const SessionAnalyzer = require('./analytics/core/SessionAnalyzer');
16
16
  const AgentAnalyzer = require('./analytics/core/AgentAnalyzer');
17
+ const YearInReview2025 = require('./analytics/core/YearInReview2025');
17
18
  const DataCache = require('./analytics/data/DataCache');
18
19
  const WebSocketServer = require('./analytics/notifications/WebSocketServer');
19
20
  const NotificationManager = require('./analytics/notifications/NotificationManager');
@@ -32,6 +33,7 @@ class ClaudeAnalytics {
32
33
  this.fileWatcher = new FileWatcher();
33
34
  this.sessionAnalyzer = new SessionAnalyzer();
34
35
  this.agentAnalyzer = new AgentAnalyzer();
36
+ this.yearInReview2025 = new YearInReview2025();
35
37
  this.dataCache = new DataCache();
36
38
  this.performanceMonitor = new PerformanceMonitor({
37
39
  enabled: true,
@@ -1244,6 +1246,37 @@ class ClaudeAnalytics {
1244
1246
  }
1245
1247
  });
1246
1248
 
1249
+ // Year in Review 2025 API endpoint
1250
+ this.app.get('/api/2025', async (req, res) => {
1251
+ try {
1252
+ console.log('📅 Generating 2025 Year in Review...');
1253
+
1254
+ // Load all conversations for analysis
1255
+ const allConversations = await this.conversationAnalyzer.loadConversations(this.stateCalculator);
1256
+
1257
+ // Generate year in review statistics
1258
+ const yearInReview = await this.yearInReview2025.generateYearInReview(allConversations, this.claudeDir);
1259
+
1260
+ console.log(`✅ 2025 Year in Review generated with ${yearInReview.totalConversations} conversations`);
1261
+
1262
+ res.json({
1263
+ ...yearInReview,
1264
+ timestamp: new Date().toISOString()
1265
+ });
1266
+ } catch (error) {
1267
+ console.error('Error generating 2025 year in review:', error);
1268
+ res.status(500).json({
1269
+ error: 'Failed to generate year in review',
1270
+ message: error.message
1271
+ });
1272
+ }
1273
+ });
1274
+
1275
+ // Year in Review 2025 page route
1276
+ this.app.get('/2025', (req, res) => {
1277
+ res.sendFile(path.join(__dirname, 'analytics-web', '2025.html'));
1278
+ });
1279
+
1247
1280
  // Main dashboard route
1248
1281
  this.app.get('/', (req, res) => {
1249
1282
  res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
@@ -1268,15 +1301,18 @@ class ClaudeAnalytics {
1268
1301
  async openBrowser(openTo = null) {
1269
1302
  const baseUrl = this.publicUrl || `http://localhost:${this.port}`;
1270
1303
  let fullUrl = baseUrl;
1271
-
1272
- // Add fragment/hash for specific page
1304
+
1305
+ // Add fragment/hash or path for specific page
1273
1306
  if (openTo === 'agents') {
1274
1307
  fullUrl = `${baseUrl}/#agents`;
1275
1308
  console.log(chalk.blue('🌐 Opening browser to Claude Code Chats...'));
1309
+ } else if (openTo === '2025') {
1310
+ fullUrl = `${baseUrl}/2025`;
1311
+ console.log(chalk.blue('🎉 Opening browser to 2025 Year in Review...'));
1276
1312
  } else {
1277
1313
  console.log(chalk.blue('🌐 Opening browser to Claude Code Analytics...'));
1278
1314
  }
1279
-
1315
+
1280
1316
  try {
1281
1317
  await open(fullUrl);
1282
1318
  } catch (error) {
package/src/index.js CHANGED
@@ -146,11 +146,12 @@ async function createClaudeConfig(options = {}) {
146
146
  const targetDir = options.directory || process.cwd();
147
147
 
148
148
  // Validate --tunnel usage
149
- if (options.tunnel && !options.analytics && !options.chats && !options.agents && !options.chatsMobile) {
150
- console.log(chalk.red('❌ Error: --tunnel can only be used with --analytics, --chats, or --chats-mobile'));
149
+ if (options.tunnel && !options.analytics && !options.chats && !options.agents && !options.chatsMobile && !options['2025']) {
150
+ console.log(chalk.red('❌ Error: --tunnel can only be used with --analytics, --chats, --2025, or --chats-mobile'));
151
151
  console.log(chalk.yellow('💡 Examples:'));
152
152
  console.log(chalk.gray(' cct --analytics --tunnel'));
153
153
  console.log(chalk.gray(' cct --chats --tunnel'));
154
+ console.log(chalk.gray(' cct --2025 --tunnel'));
154
155
  console.log(chalk.gray(' cct --chats-mobile'));
155
156
  return;
156
157
  }
@@ -242,6 +243,14 @@ async function createClaudeConfig(options = {}) {
242
243
  return;
243
244
  }
244
245
 
246
+ // Handle 2025 Year in Review dashboard
247
+ if (options['2025']) {
248
+ trackingService.trackCommandExecution('2025-year-in-review');
249
+ trackingService.trackAnalyticsDashboard({ page: '2025', source: 'command_line' });
250
+ await runAnalytics({ ...options, openTo: '2025' });
251
+ return;
252
+ }
253
+
245
254
  // Handle plugin dashboard
246
255
  if (options.plugins) {
247
256
  trackingService.trackCommandExecution('plugins');
@@ -151,7 +151,10 @@ class PluginDashboard {
151
151
 
152
152
  if (source.source === 'github') return 'GitHub';
153
153
  if (source.source === 'git') return 'Git';
154
- if (source.source === 'local') return 'Local';
154
+ // Support both 'local' and 'directory' for filesystem-based marketplaces
155
+ // 'directory' is used by Claude Code, 'local' for legacy compatibility
156
+ if (source.source === 'local' || source.source === 'directory') return 'Local';
157
+ if (source.source === 'url') return 'URL';
155
158
  return 'Unknown';
156
159
  }
157
160
 
@@ -249,7 +252,12 @@ class PluginDashboard {
249
252
  mcps: 0
250
253
  };
251
254
 
252
- const pluginSourcePath = pluginDef.source ? path.join(marketplacePath, pluginDef.source) : marketplacePath;
255
+ // Handle both string paths and object-based sources
256
+ // Marketplace aggregators use object format {source: "url", url: "..."} to reference external repos
257
+ // Individual plugins use string paths like "./" or "./plugins/name"
258
+ const pluginSourcePath = typeof pluginDef.source === 'string'
259
+ ? path.join(marketplacePath, pluginDef.source)
260
+ : marketplacePath;
253
261
 
254
262
  // Check if plugin has inline component definitions (claude-code-templates style)
255
263
  if (pluginDef.agents || pluginDef.commands || pluginDef.mcpServers) {
@@ -261,7 +269,7 @@ class PluginDashboard {
261
269
  };
262
270
  }
263
271
  // Otherwise, try to count from source directory (claude-code-plugins style)
264
- else if (pluginDef.source) {
272
+ else if (typeof pluginDef.source === 'string') {
265
273
  if (await fs.pathExists(pluginSourcePath)) {
266
274
  components = await this.countPluginComponents(pluginSourcePath);
267
275
  }
@@ -362,11 +362,11 @@ function updateStats() {
362
362
  document.getElementById('sidebarTotalSkills').textContent = total;
363
363
  document.getElementById('sidebarPersonalSkills').textContent = personal;
364
364
 
365
- // Filter counts
366
- document.getElementById('countAll').textContent = state.filteredSkills.length;
367
- document.getElementById('countPersonal').textContent = state.filteredSkills.filter(s => s.source === 'Personal').length;
368
- document.getElementById('countProject').textContent = state.filteredSkills.filter(s => s.source === 'Project').length;
369
- document.getElementById('countPlugin').textContent = state.filteredSkills.filter(s => s.source === 'Plugin').length;
365
+ // Filter counts - always show totals, not filtered counts
366
+ document.getElementById('countAll').textContent = total;
367
+ document.getElementById('countPersonal').textContent = personal;
368
+ document.getElementById('countProject').textContent = project;
369
+ document.getElementById('countPlugin').textContent = plugin;
370
370
  }
371
371
 
372
372
  // Update Empty State
@@ -26,14 +26,24 @@ class SkillDashboard {
26
26
  async loadSkillsData() {
27
27
  try {
28
28
  // Load personal skills
29
+ console.log(chalk.gray(`📂 Scanning personal skills: ${this.personalSkillsDir}`));
29
30
  this.personalSkills = await this.loadSkillsFromDirectory(this.personalSkillsDir, 'Personal');
31
+ console.log(chalk.gray(`✓ Found ${this.personalSkills.length} personal skill(s)`));
30
32
 
31
33
  // Load project skills (if in a project directory)
32
34
  const projectSkillsDir = path.join(process.cwd(), '.claude', 'skills');
35
+ console.log(chalk.gray(`📂 Scanning project skills: ${projectSkillsDir}`));
33
36
  this.projectSkills = await this.loadSkillsFromDirectory(projectSkillsDir, 'Project');
37
+ console.log(chalk.gray(`✓ Found ${this.projectSkills.length} project skill(s)`));
38
+
39
+ // Load plugin skills from marketplaces
40
+ console.log(chalk.gray(`📂 Scanning plugin skills from marketplaces`));
41
+ this.pluginSkills = await this.loadPluginSkills();
42
+ console.log(chalk.gray(`✓ Found ${this.pluginSkills.length} plugin skill(s)`));
34
43
 
35
44
  // Combine all skills
36
- this.skills = [...this.personalSkills, ...this.projectSkills];
45
+ this.skills = [...this.personalSkills, ...this.projectSkills, ...this.pluginSkills];
46
+ console.log(chalk.green(`✅ Total skills loaded: ${this.skills.length}`));
37
47
 
38
48
  } catch (error) {
39
49
  console.error(chalk.red('Error loading skills data:'), error.message);
@@ -46,38 +56,114 @@ class SkillDashboard {
46
56
 
47
57
  try {
48
58
  if (!(await fs.pathExists(skillsDir))) {
49
- if (this.options.verbose) {
50
- console.log(chalk.yellow(`Skills directory not found: ${skillsDir}`));
51
- }
59
+ console.log(chalk.gray(` ℹ Directory does not exist: ${skillsDir}`));
52
60
  return skills;
53
61
  }
54
62
 
55
63
  const skillDirs = await fs.readdir(skillsDir);
64
+ console.log(chalk.gray(` 📁 Found ${skillDirs.length} item(s) in ${skillsDir}`));
56
65
 
57
66
  for (const skillDir of skillDirs) {
67
+ // Skip hidden files and directories
68
+ if (skillDir.startsWith('.')) continue;
69
+
58
70
  const skillPath = path.join(skillsDir, skillDir);
59
71
 
60
72
  try {
61
73
  const stat = await fs.stat(skillPath);
62
- if (!stat.isDirectory()) continue;
74
+ if (!stat.isDirectory()) {
75
+ console.log(chalk.gray(` ⊘ Skipping non-directory: ${skillDir}`));
76
+ continue;
77
+ }
63
78
 
64
79
  // Look for SKILL.md
65
80
  const skillMdPath = path.join(skillPath, 'SKILL.md');
66
81
 
67
82
  if (await fs.pathExists(skillMdPath)) {
83
+ console.log(chalk.gray(` ✓ Found SKILL.md in ${skillDir}`));
68
84
  const skillData = await this.parseSkill(skillMdPath, skillPath, skillDir, source);
69
85
  if (skillData) {
70
86
  skills.push(skillData);
87
+ console.log(chalk.green(` ✅ Loaded skill: ${skillData.name}`));
71
88
  }
89
+ } else {
90
+ console.log(chalk.gray(` ⊘ No SKILL.md in ${skillDir}`));
72
91
  }
73
92
  } catch (error) {
74
- console.warn(chalk.yellow(`Warning: Error loading skill ${skillDir}`), error.message);
93
+ console.warn(chalk.yellow(` Error loading skill ${skillDir}:`), error.message);
94
+ }
95
+ }
96
+
97
+ return skills;
98
+ } catch (error) {
99
+ console.warn(chalk.yellow(`Warning: Error loading skills from ${skillsDir}:`), error.message);
100
+ return skills;
101
+ }
102
+ }
103
+
104
+ async loadPluginSkills() {
105
+ const skills = [];
106
+ const pluginsDir = path.join(this.claudeDir, 'plugins', 'marketplaces');
107
+
108
+ try {
109
+ if (!(await fs.pathExists(pluginsDir))) {
110
+ console.log(chalk.gray(` ℹ Plugins directory does not exist: ${pluginsDir}`));
111
+ return skills;
112
+ }
113
+
114
+ const marketplaces = await fs.readdir(pluginsDir);
115
+ console.log(chalk.gray(` 📁 Found ${marketplaces.length} marketplace(s)`));
116
+
117
+ for (const marketplace of marketplaces) {
118
+ if (marketplace.startsWith('.')) continue;
119
+
120
+ const marketplacePath = path.join(pluginsDir, marketplace, 'plugins');
121
+
122
+ if (!(await fs.pathExists(marketplacePath))) {
123
+ continue;
124
+ }
125
+
126
+ const plugins = await fs.readdir(marketplacePath);
127
+ console.log(chalk.gray(` 📦 Scanning marketplace: ${marketplace} (${plugins.length} plugin(s))`));
128
+
129
+ for (const plugin of plugins) {
130
+ if (plugin.startsWith('.')) continue;
131
+
132
+ const skillsPath = path.join(marketplacePath, plugin, 'skills');
133
+
134
+ if (!(await fs.pathExists(skillsPath))) {
135
+ continue;
136
+ }
137
+
138
+ const skillDirs = await fs.readdir(skillsPath);
139
+
140
+ for (const skillDir of skillDirs) {
141
+ if (skillDir.startsWith('.')) continue;
142
+
143
+ const skillPath = path.join(skillsPath, skillDir);
144
+ const stat = await fs.stat(skillPath);
145
+
146
+ if (!stat.isDirectory()) {
147
+ continue;
148
+ }
149
+
150
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
151
+
152
+ if (await fs.pathExists(skillMdPath)) {
153
+ console.log(chalk.gray(` ✓ Found plugin skill: ${skillDir} from ${marketplace}`));
154
+ const skillData = await this.parseSkill(skillMdPath, skillPath, skillDir, 'Plugin');
155
+ if (skillData) {
156
+ skills.push(skillData);
157
+ console.log(chalk.green(` ✅ Loaded plugin skill: ${skillData.name}`));
158
+ }
159
+ }
160
+ }
75
161
  }
76
162
  }
77
163
 
78
164
  return skills;
79
165
  } catch (error) {
80
- console.warn(chalk.yellow(`Warning: Error loading skills from ${skillsDir}`), error.message);
166
+ console.warn(chalk.yellow(`Warning: Error loading plugin skills:`), error.message);
81
167
  return skills;
82
168
  }
83
169
  }
@@ -443,7 +443,7 @@ validatorScore = max(0, 100 - (errors * 25) - (warnings * 5))
443
443
 
444
444
  ## Integration with Component Generation
445
445
 
446
- The security validation system integrates with `generate_components_json.py`:
446
+ The security validation system integrates with `scripts/generate_components_json.py`:
447
447
 
448
448
  1. **Automatic Validation** - Runs before generating components.json
449
449
  2. **Metadata Inclusion** - Security scores, hashes, validation status