claude-code-templates 1.11.0 → 1.12.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/src/analytics.js CHANGED
@@ -238,8 +238,35 @@ class ClaudeAnalytics {
238
238
  };
239
239
  }
240
240
 
241
- extractProjectFromPath(filePath) {
242
- // Extract project name from file path like:
241
+ async extractProjectFromPath(filePath) {
242
+ // First try to read cwd from the conversation file itself
243
+ try {
244
+ const content = await fs.readFile(filePath, 'utf8');
245
+ const lines = content.trim().split('\n').filter(line => line.trim());
246
+
247
+ for (const line of lines.slice(0, 10)) { // Check first 10 lines
248
+ try {
249
+ const item = JSON.parse(line);
250
+
251
+ // Look for cwd field in the message
252
+ if (item.cwd) {
253
+ return path.basename(item.cwd);
254
+ }
255
+
256
+ // Also check if it's in nested objects
257
+ if (item.message && item.message.cwd) {
258
+ return path.basename(item.message.cwd);
259
+ }
260
+ } catch (parseError) {
261
+ // Skip invalid JSON lines
262
+ continue;
263
+ }
264
+ }
265
+ } catch (error) {
266
+ console.warn(chalk.yellow(`Warning: Could not extract project from conversation ${filePath}:`, error.message));
267
+ }
268
+
269
+ // Fallback: Extract project name from file path like:
243
270
  // /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
244
271
  const pathParts = filePath.split('/');
245
272
  const projectIndex = pathParts.findIndex(part => part === 'projects');
@@ -256,7 +283,7 @@ class ClaudeAnalytics {
256
283
  return cleanName;
257
284
  }
258
285
 
259
- return null;
286
+ return 'Unknown';
260
287
  }
261
288
 
262
289
 
@@ -1097,6 +1124,17 @@ class ClaudeAnalytics {
1097
1124
  }
1098
1125
  });
1099
1126
 
1127
+ // Agents API endpoint
1128
+ this.app.get('/api/agents', async (req, res) => {
1129
+ try {
1130
+ const agents = await this.loadAgents();
1131
+ res.json({ agents });
1132
+ } catch (error) {
1133
+ console.error('Error loading agents:', error);
1134
+ res.status(500).json({ error: 'Failed to load agents data' });
1135
+ }
1136
+ });
1137
+
1100
1138
  // Main dashboard route
1101
1139
  this.app.get('/', (req, res) => {
1102
1140
  res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
@@ -1282,6 +1320,307 @@ class ClaudeAnalytics {
1282
1320
  });
1283
1321
  }
1284
1322
 
1323
+ /**
1324
+ * Load available agents from .claude/agents directories (project and user level)
1325
+ * @returns {Promise<Array>} Array of agent objects
1326
+ */
1327
+ async loadAgents() {
1328
+ const agents = [];
1329
+ const homeDir = os.homedir();
1330
+
1331
+ // Define agent paths (user level and project level)
1332
+ const userAgentsDir = path.join(homeDir, '.claude', 'agents');
1333
+ const projectAgentsDirs = [];
1334
+
1335
+ try {
1336
+ // 1. Check current working directory for .claude/agents
1337
+ const currentProjectAgentsDir = path.join(process.cwd(), '.claude', 'agents');
1338
+ if (await fs.pathExists(currentProjectAgentsDir)) {
1339
+ const currentProjectName = path.basename(process.cwd());
1340
+ projectAgentsDirs.push({
1341
+ path: currentProjectAgentsDir,
1342
+ projectName: currentProjectName
1343
+ });
1344
+ }
1345
+
1346
+ // 2. Check parent directories for .claude/agents (for monorepo/nested projects)
1347
+ let currentDir = process.cwd();
1348
+ let parentDir = path.dirname(currentDir);
1349
+
1350
+ // Search up to 3 levels up for .claude/agents
1351
+ for (let i = 0; i < 3 && parentDir !== currentDir; i++) {
1352
+ const parentProjectAgentsDir = path.join(parentDir, '.claude', 'agents');
1353
+
1354
+ if (await fs.pathExists(parentProjectAgentsDir)) {
1355
+ const parentProjectName = path.basename(parentDir);
1356
+
1357
+ // Avoid duplicates
1358
+ const exists = projectAgentsDirs.some(p => p.path === parentProjectAgentsDir);
1359
+ if (!exists) {
1360
+ projectAgentsDirs.push({
1361
+ path: parentProjectAgentsDir,
1362
+ projectName: parentProjectName
1363
+ });
1364
+ }
1365
+ break; // Found one, no need to go further up
1366
+ }
1367
+ currentDir = parentDir;
1368
+ parentDir = path.dirname(currentDir);
1369
+ }
1370
+
1371
+ // 3. Find all project directories that might have agents (in ~/.claude/projects)
1372
+ const projectsDir = path.join(this.claudeDir, 'projects');
1373
+ if (await fs.pathExists(projectsDir)) {
1374
+ const projectDirs = await fs.readdir(projectsDir);
1375
+ for (const projectDir of projectDirs) {
1376
+ const projectAgentsDir = path.join(projectsDir, projectDir, '.claude', 'agents');
1377
+ if (await fs.pathExists(projectAgentsDir)) {
1378
+ projectAgentsDirs.push({
1379
+ path: projectAgentsDir,
1380
+ projectName: this.cleanProjectName(projectDir)
1381
+ });
1382
+ }
1383
+ }
1384
+ }
1385
+
1386
+ // Load user-level agents
1387
+ if (await fs.pathExists(userAgentsDir)) {
1388
+ const userAgents = await this.loadAgentsFromDirectory(userAgentsDir, 'user');
1389
+ agents.push(...userAgents);
1390
+ }
1391
+
1392
+ // Load project-level agents
1393
+ for (const projectInfo of projectAgentsDirs) {
1394
+ const projectAgents = await this.loadAgentsFromDirectory(
1395
+ projectInfo.path,
1396
+ 'project',
1397
+ projectInfo.projectName
1398
+ );
1399
+ agents.push(...projectAgents);
1400
+ }
1401
+
1402
+ // Log agents summary
1403
+ console.log(chalk.blue('🤖 Agents loaded:'), agents.length);
1404
+ if (agents.length > 0) {
1405
+ const projectAgents = agents.filter(a => a.level === 'project').length;
1406
+ const userAgents = agents.filter(a => a.level === 'user').length;
1407
+ console.log(chalk.gray(` 📦 Project agents: ${projectAgents}, 👤 User agents: ${userAgents}`));
1408
+ }
1409
+
1410
+ // Sort agents by name and prioritize project agents over user agents
1411
+ return agents.sort((a, b) => {
1412
+ if (a.level !== b.level) {
1413
+ return a.level === 'project' ? -1 : 1;
1414
+ }
1415
+ return a.name.localeCompare(b.name);
1416
+ });
1417
+
1418
+ } catch (error) {
1419
+ console.error(chalk.red('Error loading agents:'), error);
1420
+ return [];
1421
+ }
1422
+ }
1423
+
1424
+ /**
1425
+ * Load agents from a specific directory
1426
+ * @param {string} agentsDir - Directory containing agent files
1427
+ * @param {string} level - 'user' or 'project'
1428
+ * @param {string} projectName - Name of project (if project level)
1429
+ * @returns {Promise<Array>} Array of agent objects
1430
+ */
1431
+ async loadAgentsFromDirectory(agentsDir, level, projectName = null) {
1432
+ const agents = [];
1433
+
1434
+ try {
1435
+ const files = await fs.readdir(agentsDir);
1436
+
1437
+ for (const file of files) {
1438
+ if (file.endsWith('.md')) {
1439
+ const filePath = path.join(agentsDir, file);
1440
+ const agentData = await this.parseAgentFile(filePath, level, projectName);
1441
+ if (agentData) {
1442
+ agents.push(agentData);
1443
+ }
1444
+ }
1445
+ }
1446
+ } catch (error) {
1447
+ console.warn(chalk.yellow(`Warning: Could not read agents directory ${agentsDir}:`, error.message));
1448
+ }
1449
+
1450
+ return agents;
1451
+ }
1452
+
1453
+ /**
1454
+ * Parse agent markdown file
1455
+ * @param {string} filePath - Path to agent file
1456
+ * @param {string} level - 'user' or 'project'
1457
+ * @param {string} projectName - Name of project (if project level)
1458
+ * @returns {Promise<Object|null>} Agent object or null if parsing failed
1459
+ */
1460
+ async parseAgentFile(filePath, level, projectName = null) {
1461
+ try {
1462
+ const content = await fs.readFile(filePath, 'utf8');
1463
+ const stats = await fs.stat(filePath);
1464
+
1465
+ // Parse YAML frontmatter
1466
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1467
+ if (!frontmatterMatch) {
1468
+ console.warn(chalk.yellow(`Agent file ${path.basename(filePath)} missing frontmatter`));
1469
+ return null;
1470
+ }
1471
+
1472
+ const frontmatter = {};
1473
+ const yamlContent = frontmatterMatch[1];
1474
+
1475
+ // Simple YAML parser for the fields we need
1476
+ const yamlLines = yamlContent.split('\n');
1477
+ for (const line of yamlLines) {
1478
+ const match = line.match(/^(\w+):\s*(.*)$/);
1479
+ if (match) {
1480
+ const [, key, value] = match;
1481
+ frontmatter[key] = value.trim();
1482
+ }
1483
+ }
1484
+
1485
+ // Log parsed frontmatter for debugging
1486
+ console.log(chalk.blue(`📋 Parsed agent frontmatter for ${path.basename(filePath)}:`), frontmatter);
1487
+
1488
+ if (!frontmatter.name || !frontmatter.description) {
1489
+ console.warn(chalk.yellow(`Agent file ${path.basename(filePath)} missing required fields`));
1490
+ return null;
1491
+ }
1492
+
1493
+ // Extract system prompt (content after frontmatter)
1494
+ const systemPrompt = content.substring(frontmatterMatch[0].length).trim();
1495
+
1496
+ // Parse tools if specified
1497
+ let tools = [];
1498
+ if (frontmatter.tools) {
1499
+ tools = frontmatter.tools.split(',').map(tool => tool.trim()).filter(Boolean);
1500
+ }
1501
+
1502
+ // Use color from frontmatter if available, otherwise generate one
1503
+ const color = frontmatter.color ? this.convertColorToHex(frontmatter.color) : this.generateAgentColor(frontmatter.name);
1504
+
1505
+ return {
1506
+ name: frontmatter.name,
1507
+ description: frontmatter.description,
1508
+ systemPrompt,
1509
+ tools,
1510
+ level,
1511
+ projectName,
1512
+ filePath,
1513
+ lastModified: stats.mtime,
1514
+ color,
1515
+ isActive: true // All loaded agents are considered active
1516
+ };
1517
+
1518
+ } catch (error) {
1519
+ console.warn(chalk.yellow(`Warning: Could not parse agent file ${filePath}:`, error.message));
1520
+ return null;
1521
+ }
1522
+ }
1523
+
1524
+ /**
1525
+ * Generate consistent color for agent based on name
1526
+ * @param {string} agentName - Name of the agent
1527
+ * @returns {string} Hex color code
1528
+ */
1529
+ generateAgentColor(agentName) {
1530
+ // Simple hash function to generate consistent colors
1531
+ let hash = 0;
1532
+ for (let i = 0; i < agentName.length; i++) {
1533
+ const char = agentName.charCodeAt(i);
1534
+ hash = ((hash << 5) - hash) + char;
1535
+ hash = hash & hash; // Convert to 32-bit integer
1536
+ }
1537
+
1538
+ // Generate RGB values with good contrast and visibility
1539
+ const hue = Math.abs(hash) % 360;
1540
+ const saturation = 70 + (Math.abs(hash) % 30); // 70-100%
1541
+ const lightness = 45 + (Math.abs(hash) % 20); // 45-65%
1542
+
1543
+ // Convert HSL to RGB
1544
+ const hslToRgb = (h, s, l) => {
1545
+ h /= 360;
1546
+ s /= 100;
1547
+ l /= 100;
1548
+
1549
+ const hue2rgb = (p, q, t) => {
1550
+ if (t < 0) t += 1;
1551
+ if (t > 1) t -= 1;
1552
+ if (t < 1/6) return p + (q - p) * 6 * t;
1553
+ if (t < 1/2) return q;
1554
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
1555
+ return p;
1556
+ };
1557
+
1558
+ let r, g, b;
1559
+ if (s === 0) {
1560
+ r = g = b = l;
1561
+ } else {
1562
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1563
+ const p = 2 * l - q;
1564
+ r = hue2rgb(p, q, h + 1/3);
1565
+ g = hue2rgb(p, q, h);
1566
+ b = hue2rgb(p, q, h - 1/3);
1567
+ }
1568
+
1569
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
1570
+ };
1571
+
1572
+ const [r, g, b] = hslToRgb(hue, saturation, lightness);
1573
+ return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
1574
+ }
1575
+
1576
+ /**
1577
+ * Convert color names to hex values
1578
+ * @param {string} color - Color name or hex value
1579
+ * @returns {string} Hex color code
1580
+ */
1581
+ convertColorToHex(color) {
1582
+ if (!color) return '#007acc';
1583
+
1584
+ // If already hex, return as-is
1585
+ if (color.startsWith('#')) return color;
1586
+
1587
+ // Convert common color names to hex
1588
+ const colorMap = {
1589
+ 'red': '#ff4444',
1590
+ 'blue': '#4444ff',
1591
+ 'green': '#44ff44',
1592
+ 'yellow': '#ffff44',
1593
+ 'orange': '#ff8844',
1594
+ 'purple': '#8844ff',
1595
+ 'pink': '#ff44ff',
1596
+ 'cyan': '#44ffff',
1597
+ 'brown': '#8b4513',
1598
+ 'gray': '#888888',
1599
+ 'grey': '#888888',
1600
+ 'black': '#333333',
1601
+ 'white': '#ffffff',
1602
+ 'teal': '#008080',
1603
+ 'navy': '#000080',
1604
+ 'lime': '#00ff00',
1605
+ 'maroon': '#800000',
1606
+ 'olive': '#808000',
1607
+ 'silver': '#c0c0c0'
1608
+ };
1609
+
1610
+ return colorMap[color.toLowerCase()] || '#007acc';
1611
+ }
1612
+
1613
+ /**
1614
+ * Clean project name for display
1615
+ * @param {string} projectDir - Raw project directory name
1616
+ * @returns {string} Cleaned project name
1617
+ */
1618
+ cleanProjectName(projectDir) {
1619
+ // Convert encoded project paths like "-Users-user-Projects-MyProject" to "MyProject"
1620
+ const parts = projectDir.split('-').filter(Boolean);
1621
+ return parts[parts.length - 1] || projectDir;
1622
+ }
1623
+
1285
1624
  /**
1286
1625
  * Get Claude session information from statsig files
1287
1626
  */