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/package.json +1 -2
- package/src/analytics/core/ConversationAnalyzer.js +65 -23
- package/src/analytics-web/components/AgentsPage.js +2365 -156
- package/src/analytics-web/components/App.js +11 -0
- package/src/analytics-web/components/DashboardPage.js +4 -0
- package/src/analytics-web/components/ToolDisplay.js +17 -2
- package/src/analytics-web/index.html +3005 -1059
- package/src/analytics.js +342 -3
package/src/analytics.js
CHANGED
|
@@ -238,8 +238,35 @@ class ClaudeAnalytics {
|
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
extractProjectFromPath(filePath) {
|
|
242
|
-
//
|
|
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
|
|
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
|
*/
|