claude-code-templates 1.12.2 → 1.13.1

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
@@ -10,6 +10,7 @@ const ProcessDetector = require('./analytics/core/ProcessDetector');
10
10
  const ConversationAnalyzer = require('./analytics/core/ConversationAnalyzer');
11
11
  const FileWatcher = require('./analytics/core/FileWatcher');
12
12
  const SessionAnalyzer = require('./analytics/core/SessionAnalyzer');
13
+ const AgentAnalyzer = require('./analytics/core/AgentAnalyzer');
13
14
  const DataCache = require('./analytics/data/DataCache');
14
15
  const WebSocketServer = require('./analytics/notifications/WebSocketServer');
15
16
  const NotificationManager = require('./analytics/notifications/NotificationManager');
@@ -24,6 +25,7 @@ class ClaudeAnalytics {
24
25
  this.processDetector = new ProcessDetector();
25
26
  this.fileWatcher = new FileWatcher();
26
27
  this.sessionAnalyzer = new SessionAnalyzer();
28
+ this.agentAnalyzer = new AgentAnalyzer();
27
29
  this.dataCache = new DataCache();
28
30
  this.performanceMonitor = new PerformanceMonitor({
29
31
  enabled: true,
@@ -667,6 +669,28 @@ class ClaudeAnalytics {
667
669
  }
668
670
  });
669
671
 
672
+ // Agent usage analytics endpoint
673
+ this.app.get('/api/agents', async (req, res) => {
674
+ try {
675
+ const startDate = req.query.startDate;
676
+ const endDate = req.query.endDate;
677
+ const dateRange = (startDate || endDate) ? { startDate, endDate } : null;
678
+
679
+ const agentAnalysis = await this.agentAnalyzer.analyzeAgentUsage(this.data.conversations, dateRange);
680
+ const agentSummary = this.agentAnalyzer.generateSummary(agentAnalysis);
681
+
682
+ res.json({
683
+ ...agentAnalysis,
684
+ summary: agentSummary,
685
+ dateRange,
686
+ timestamp: new Date().toISOString()
687
+ });
688
+ } catch (error) {
689
+ console.error('Error getting agent analytics:', error);
690
+ res.status(500).json({ error: 'Failed to get agent analytics' });
691
+ }
692
+ });
693
+
670
694
  this.app.get('/api/realtime', async (req, res) => {
671
695
  const realtimeWithTimestamp = {
672
696
  ...this.data.realtimeStats,
@@ -1661,12 +1685,88 @@ class ClaudeAnalytics {
1661
1685
  const timeSinceLastUpdate = now - lastUpdate;
1662
1686
  const timeSinceLastUpdateMinutes = Math.floor(timeSinceLastUpdate / (1000 * 60));
1663
1687
 
1664
- // Based on observed pattern: ~2 hours and 21 minutes session limit
1665
- const sessionLimitMs = 2 * 60 * 60 * 1000 + 21 * 60 * 1000; // 2h 21m
1666
- const timeRemaining = sessionLimitMs - sessionDuration;
1667
- const timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
1668
- const timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
1669
- const remainingMinutesDisplay = timeRemainingMinutes % 60;
1688
+ // CORRECTED: Calculate next reset time based on scheduled reset hours
1689
+ // Claude sessions reset at specific times, not fixed durations
1690
+ const resetHours = [1, 7, 13, 19]; // 1am, 7am, 1pm, 7pm local time
1691
+ const sessionStartDate = new Date(startTime);
1692
+
1693
+ // Find next reset time after session start
1694
+ let nextResetTime = new Date(sessionStartDate);
1695
+ nextResetTime.setMinutes(0, 0, 0); // Set to exact hour
1696
+
1697
+ // Find the next reset hour
1698
+ let foundReset = false;
1699
+ for (let i = 0; i < resetHours.length * 2; i++) { // Check up to 2 full days
1700
+ const currentDay = Math.floor(i / resetHours.length);
1701
+ const hourIndex = i % resetHours.length;
1702
+ const resetHour = resetHours[hourIndex];
1703
+
1704
+ const testResetTime = new Date(sessionStartDate);
1705
+ testResetTime.setDate(testResetTime.getDate() + currentDay);
1706
+ testResetTime.setHours(resetHour, 0, 0, 0);
1707
+
1708
+ if (testResetTime > sessionStartDate) {
1709
+ nextResetTime = testResetTime;
1710
+ foundReset = true;
1711
+ break;
1712
+ }
1713
+ }
1714
+
1715
+ if (!foundReset) {
1716
+ // Fallback: assume next day 7am if no pattern found
1717
+ nextResetTime.setDate(nextResetTime.getDate() + 1);
1718
+ nextResetTime.setHours(7, 0, 0, 0);
1719
+ }
1720
+
1721
+ const sessionLimitMs = nextResetTime.getTime() - startTime;
1722
+ let timeRemaining = nextResetTime.getTime() - now;
1723
+ let timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
1724
+ let timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
1725
+ let remainingMinutesDisplay = timeRemainingMinutes % 60;
1726
+
1727
+ // If session is expired but has recent activity, calculate next reset time
1728
+ if (timeRemaining <= 0) {
1729
+ const RECENT_ACTIVITY_THRESHOLD = 5 * 60 * 1000; // 5 minutes
1730
+ const timeSinceLastUpdate = now - lastUpdate;
1731
+
1732
+ if (timeSinceLastUpdate < RECENT_ACTIVITY_THRESHOLD) {
1733
+ // Session was renewed, find the NEXT reset time
1734
+ let nextNextResetTime = new Date(nextResetTime);
1735
+ let foundNextReset = false;
1736
+
1737
+ for (let i = 0; i < resetHours.length; i++) {
1738
+ const resetHour = resetHours[i];
1739
+ const testResetTime = new Date(nextResetTime);
1740
+
1741
+ if (resetHour > nextResetTime.getHours()) {
1742
+ // Same day, later hour
1743
+ testResetTime.setHours(resetHour, 0, 0, 0);
1744
+ } else {
1745
+ // Next day
1746
+ testResetTime.setDate(testResetTime.getDate() + 1);
1747
+ testResetTime.setHours(resetHour, 0, 0, 0);
1748
+ }
1749
+
1750
+ if (testResetTime > nextResetTime) {
1751
+ nextNextResetTime = testResetTime;
1752
+ foundNextReset = true;
1753
+ break;
1754
+ }
1755
+ }
1756
+
1757
+ if (!foundNextReset) {
1758
+ // Default to next day 1am
1759
+ nextNextResetTime.setDate(nextNextResetTime.getDate() + 1);
1760
+ nextNextResetTime.setHours(1, 0, 0, 0);
1761
+ }
1762
+
1763
+ timeRemaining = nextNextResetTime.getTime() - now;
1764
+ timeRemainingMinutes = Math.floor(timeRemaining / (1000 * 60));
1765
+ timeRemainingHours = Math.floor(timeRemainingMinutes / 60);
1766
+ remainingMinutesDisplay = timeRemainingMinutes % 60;
1767
+ nextResetTime = nextNextResetTime;
1768
+ }
1769
+ }
1670
1770
 
1671
1771
  return {
1672
1772
  hasSession: true,
@@ -1691,13 +1791,15 @@ class ClaudeAnalytics {
1691
1791
  hours: timeRemainingHours,
1692
1792
  remainingMinutes: remainingMinutesDisplay,
1693
1793
  formatted: timeRemaining > 0 ? `${timeRemainingHours}h ${remainingMinutesDisplay}m` : 'Session expired',
1694
- isExpired: timeRemaining <= 0
1794
+ isExpired: timeRemaining <= 0 && timeSinceLastUpdate >= (5 * 60 * 1000) // Expired only if past reset time AND no recent activity
1695
1795
  },
1696
1796
  sessionLimit: {
1697
1797
  ms: sessionLimitMs,
1698
- hours: 2,
1699
- minutes: 21,
1700
- formatted: '2h 21m'
1798
+ hours: Math.floor(sessionLimitMs / (1000 * 60 * 60)),
1799
+ minutes: Math.floor((sessionLimitMs % (1000 * 60 * 60)) / (1000 * 60)),
1800
+ formatted: `${Math.floor(sessionLimitMs / (1000 * 60 * 60))}h ${Math.floor((sessionLimitMs % (1000 * 60 * 60)) / (1000 * 60))}m`,
1801
+ nextResetTime: nextResetTime.toISOString(),
1802
+ resetHour: nextResetTime.getHours()
1701
1803
  }
1702
1804
  };
1703
1805
  } catch (error) {
package/src/index.js CHANGED
@@ -80,6 +80,22 @@ async function showMainMenu() {
80
80
  async function createClaudeConfig(options = {}) {
81
81
  const targetDir = options.directory || process.cwd();
82
82
 
83
+ // Handle individual component installation
84
+ if (options.agent) {
85
+ await installIndividualAgent(options.agent, targetDir, options);
86
+ return;
87
+ }
88
+
89
+ if (options.command) {
90
+ await installIndividualCommand(options.command, targetDir, options);
91
+ return;
92
+ }
93
+
94
+ if (options.mcp) {
95
+ await installIndividualMCP(options.mcp, targetDir, options);
96
+ return;
97
+ }
98
+
83
99
  // Handle command stats analysis (both singular and plural)
84
100
  if (options.commandStats || options.commandsStats) {
85
101
  await runCommandStats(options);
@@ -253,4 +269,171 @@ async function createClaudeConfig(options = {}) {
253
269
  }
254
270
  }
255
271
 
272
+ // Individual component installation functions
273
+ async function installIndividualAgent(agentName, targetDir, options) {
274
+ console.log(chalk.blue(`🤖 Installing agent: ${agentName}`));
275
+
276
+ try {
277
+ // Check if components directory exists
278
+ const componentsPath = path.join(__dirname, '..', 'components', 'agents');
279
+ const agentFile = path.join(componentsPath, `${agentName}.md`);
280
+
281
+ if (!await fs.pathExists(agentFile)) {
282
+ console.log(chalk.red(`❌ Agent "${agentName}" not found`));
283
+ console.log(chalk.yellow('Available agents:'));
284
+
285
+ // List available agents
286
+ if (await fs.pathExists(componentsPath)) {
287
+ const agents = await fs.readdir(componentsPath);
288
+ agents.filter(f => f.endsWith('.md')).forEach(agent => {
289
+ console.log(chalk.cyan(` - ${agent.replace('.md', '')}`));
290
+ });
291
+ }
292
+ return;
293
+ }
294
+
295
+ // For agents, they are typically part of templates, so we need to determine
296
+ // the appropriate language/framework and install the complete template
297
+ const agentContent = await fs.readFile(agentFile, 'utf8');
298
+ const language = extractLanguageFromAgent(agentContent, agentName);
299
+ const framework = extractFrameworkFromAgent(agentContent, agentName);
300
+
301
+ console.log(chalk.yellow(`📝 Agent "${agentName}" is part of ${language}/${framework} template`));
302
+ console.log(chalk.blue('🚀 Installing complete template with this agent...'));
303
+
304
+ // Install the template that contains this agent (avoid recursion)
305
+ const setupOptions = {
306
+ ...options,
307
+ language,
308
+ framework,
309
+ yes: true,
310
+ targetDirectory: targetDir,
311
+ agent: null // Remove agent to avoid recursion
312
+ };
313
+ delete setupOptions.agent;
314
+ await createClaudeConfig(setupOptions);
315
+
316
+ console.log(chalk.green(`✅ Agent "${agentName}" installed successfully!`));
317
+
318
+ } catch (error) {
319
+ console.log(chalk.red(`❌ Error installing agent: ${error.message}`));
320
+ }
321
+ }
322
+
323
+ async function installIndividualCommand(commandName, targetDir, options) {
324
+ console.log(chalk.blue(`⚡ Installing command: ${commandName}`));
325
+
326
+ try {
327
+ // Check if components directory exists
328
+ const componentsPath = path.join(__dirname, '..', 'components', 'commands');
329
+ const commandFile = path.join(componentsPath, `${commandName}.md`);
330
+
331
+ if (!await fs.pathExists(commandFile)) {
332
+ console.log(chalk.red(`❌ Command "${commandName}" not found`));
333
+ console.log(chalk.yellow('Available commands:'));
334
+
335
+ // List available commands
336
+ if (await fs.pathExists(componentsPath)) {
337
+ const commands = await fs.readdir(componentsPath);
338
+ commands.filter(f => f.endsWith('.md')).forEach(command => {
339
+ console.log(chalk.cyan(` - ${command.replace('.md', '')}`));
340
+ });
341
+ }
342
+ return;
343
+ }
344
+
345
+ // Create .claude/commands directory if it doesn't exist
346
+ const commandsDir = path.join(targetDir, '.claude', 'commands');
347
+ await fs.ensureDir(commandsDir);
348
+
349
+ // Copy the command file
350
+ const targetFile = path.join(commandsDir, `${commandName}.md`);
351
+ await fs.copy(commandFile, targetFile);
352
+
353
+ console.log(chalk.green(`✅ Command "${commandName}" installed successfully!`));
354
+ console.log(chalk.cyan(`📁 Installed to: ${path.relative(targetDir, targetFile)}`));
355
+
356
+ } catch (error) {
357
+ console.log(chalk.red(`❌ Error installing command: ${error.message}`));
358
+ }
359
+ }
360
+
361
+ async function installIndividualMCP(mcpName, targetDir, options) {
362
+ console.log(chalk.blue(`🔌 Installing MCP: ${mcpName}`));
363
+
364
+ try {
365
+ // Check if components directory exists
366
+ const componentsPath = path.join(__dirname, '..', 'components', 'mcps');
367
+ const mcpFile = path.join(componentsPath, `${mcpName}.json`);
368
+
369
+ if (!await fs.pathExists(mcpFile)) {
370
+ console.log(chalk.red(`❌ MCP "${mcpName}" not found`));
371
+ console.log(chalk.yellow('Available MCPs:'));
372
+
373
+ // List available MCPs
374
+ if (await fs.pathExists(componentsPath)) {
375
+ const mcps = await fs.readdir(componentsPath);
376
+ mcps.filter(f => f.endsWith('.json')).forEach(mcp => {
377
+ console.log(chalk.cyan(` - ${mcp.replace('.json', '')}`));
378
+ });
379
+ }
380
+ return;
381
+ }
382
+
383
+ // Read the MCP configuration
384
+ const mcpConfig = await fs.readJson(mcpFile);
385
+
386
+ // Check if .mcp.json exists in target directory
387
+ const targetMcpFile = path.join(targetDir, '.mcp.json');
388
+ let existingConfig = {};
389
+
390
+ if (await fs.pathExists(targetMcpFile)) {
391
+ existingConfig = await fs.readJson(targetMcpFile);
392
+ console.log(chalk.yellow('📝 Existing .mcp.json found, merging configurations...'));
393
+ }
394
+
395
+ // Merge configurations
396
+ const mergedConfig = {
397
+ ...existingConfig,
398
+ ...mcpConfig
399
+ };
400
+
401
+ // Write the merged configuration
402
+ await fs.writeJson(targetMcpFile, mergedConfig, { spaces: 2 });
403
+
404
+ console.log(chalk.green(`✅ MCP "${mcpName}" installed successfully!`));
405
+ console.log(chalk.cyan(`📁 Configuration merged into: ${path.relative(targetDir, targetMcpFile)}`));
406
+
407
+ } catch (error) {
408
+ console.log(chalk.red(`❌ Error installing MCP: ${error.message}`));
409
+ }
410
+ }
411
+
412
+ // Helper functions to extract language/framework from agent content
413
+ function extractLanguageFromAgent(content, agentName) {
414
+ // Try to determine language from agent content or filename
415
+ if (agentName.includes('react') || content.includes('React')) return 'javascript-typescript';
416
+ if (agentName.includes('django') || content.includes('Django')) return 'python';
417
+ if (agentName.includes('fastapi') || content.includes('FastAPI')) return 'python';
418
+ if (agentName.includes('flask') || content.includes('Flask')) return 'python';
419
+ if (agentName.includes('rails') || content.includes('Rails')) return 'ruby';
420
+ if (agentName.includes('api-security') || content.includes('API security')) return 'javascript-typescript';
421
+ if (agentName.includes('database') || content.includes('database')) return 'javascript-typescript';
422
+
423
+ // Default to javascript-typescript for general agents
424
+ return 'javascript-typescript';
425
+ }
426
+
427
+ function extractFrameworkFromAgent(content, agentName) {
428
+ // Try to determine framework from agent content or filename
429
+ if (agentName.includes('react') || content.includes('React')) return 'react';
430
+ if (agentName.includes('django') || content.includes('Django')) return 'django';
431
+ if (agentName.includes('fastapi') || content.includes('FastAPI')) return 'fastapi';
432
+ if (agentName.includes('flask') || content.includes('Flask')) return 'flask';
433
+ if (agentName.includes('rails') || content.includes('Rails')) return 'rails';
434
+
435
+ // For general agents, return none to install the base template
436
+ return 'none';
437
+ }
438
+
256
439
  module.exports = { createClaudeConfig, showMainMenu };