@aigentics/agent-toolkit
Version:
Comprehensive toolkit for validating and managing Claude Flow agent systems
612 lines (539 loc) • 22.5 kB
JavaScript
/**
* Agent Analyzer
* Analyzes agent system for insights and optimization opportunities
*/
import path from 'path';
import fs from 'fs/promises';
import {
extractYamlFrontmatter,
findMarkdownFiles,
getRelativePath,
safeReadFile,
formatFileSize
} from './utils.mjs';
import { AgentValidator } from './validator.mjs';
export class AgentAnalyzer {
constructor(options = {}) {
this.baseDir = options.baseDir || process.cwd();
this.agentsDir = options.agentsDir || path.join(this.baseDir, '.claude/agents');
}
/**
* Analyze entire agent system
*/
async analyze(targetDir) {
const baseDirectory = targetDir || this.baseDir;
const agentsDirectory = path.join(baseDirectory, '.claude/agents');
// Check if agents directory exists
try {
await fs.stat(agentsDirectory);
} catch (error) {
return {
summary: {
totalAgents: 0,
validAgents: 0,
invalidAgents: 0,
totalCapabilities: 0,
uniqueCapabilities: 0,
totalTools: 0,
uniqueTools: 0
},
agents: [],
capabilities: {},
tools: {},
dependencies: {},
issues: [{
type: 'info',
message: 'No agents directory found at .claude/agents'
}]
};
}
// Find all agent files (both .md and .json)
const mdFiles = await findMarkdownFiles(agentsDirectory);
const jsonFiles = await this.findJsonFiles(agentsDirectory);
const allFiles = [...mdFiles, ...jsonFiles];
const analysis = {
summary: {
totalAgents: 0,
validAgents: 0,
invalidAgents: 0,
totalCapabilities: 0,
uniqueCapabilities: 0,
totalTools: 0,
uniqueTools: 0,
totalSize: 0,
avgSize: 0,
types: {},
priorities: {},
capabilities: new Set(),
tools: new Set(),
dependencies: []
},
agents: [],
capabilities: {},
tools: {},
dependencies: {},
typeAnalysis: {},
toolUsage: {},
dependencyGraph: {},
securityAnalysis: {
sandboxed: 0,
unsandboxed: 0,
restrictedPaths: new Set(),
networkAccess: {}
},
performanceAnalysis: {
parallelEnabled: 0,
batchingEnabled: 0,
resourceLimits: {}
},
recommendations: [],
issues: []
};
// Process all files
const agentNames = new Map();
for (const filePath of allFiles) {
const agentResult = await this.analyzeFile(filePath, analysis, agentNames);
if (agentResult) {
analysis.agents.push(agentResult);
}
}
// Calculate averages and generate recommendations
this.finalizeAnalysis(analysis);
return analysis;
}
/**
* Find JSON files recursively
*/
async findJsonFiles(dir) {
const results = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...await this.findJsonFiles(fullPath));
} else if (entry.name.endsWith('.json')) {
results.push(fullPath);
}
}
} catch (error) {
// Ignore errors
}
return results;
}
/**
* Analyze individual agent file
*/
async analyzeFile(filePath, analysis, agentNames) {
try {
let agentData;
let content;
const stats = await fs.stat(filePath);
if (filePath.endsWith('.json')) {
// Handle JSON files
content = await safeReadFile(filePath);
try {
agentData = JSON.parse(content);
} catch (error) {
analysis.summary.invalidAgents++;
return {
name: path.basename(filePath, '.json'),
valid: false,
errors: [`Failed to parse JSON: ${error.message}`],
path: filePath
};
}
} else {
// Handle Markdown files
content = await safeReadFile(filePath);
const [yamlData, _] = extractYamlFrontmatter(content);
agentData = yamlData;
if (Object.keys(agentData).length === 0) {
analysis.summary.invalidAgents++;
return {
name: path.basename(filePath, '.md'),
valid: false,
errors: ['No YAML frontmatter found'],
path: filePath
};
}
}
// Validate agent
const validator = new AgentValidator();
const errors = validator.validateConfig(agentData);
const agentResult = {
name: agentData.name || path.basename(filePath).replace(/\.(md|json)$/, ''),
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
path: filePath,
complexity: this.calculateComplexity(agentData)
};
analysis.summary.totalAgents++;
analysis.summary.totalSize += stats.size;
if (agentResult.valid) {
analysis.summary.validAgents++;
} else {
analysis.summary.invalidAgents++;
}
// Check for duplicate names
if (agentNames.has(agentResult.name)) {
analysis.issues.push({
type: 'warning',
message: `Duplicate agent name '${agentResult.name}' found in ${filePath} and ${agentNames.get(agentResult.name)}`
});
} else {
agentNames.set(agentResult.name, filePath);
}
// Type analysis
if (agentData.type) {
analysis.summary.types[agentData.type] = (analysis.summary.types[agentData.type] || 0) + 1;
if (!analysis.typeAnalysis[agentData.type]) {
analysis.typeAnalysis[agentData.type] = {
count: 0,
agents: [],
commonCapabilities: {},
commonTools: {}
};
}
const typeInfo = analysis.typeAnalysis[agentData.type];
typeInfo.count++;
typeInfo.agents.push(agentData.name);
}
// Priority analysis
if (agentData.priority) {
analysis.summary.priorities[agentData.priority] = (analysis.summary.priorities[agentData.priority] || 0) + 1;
}
// Capabilities analysis
if (agentData.capabilities && Array.isArray(agentData.capabilities)) {
agentData.capabilities.forEach(cap => {
analysis.summary.capabilities.add(cap);
analysis.capabilities[cap] = (analysis.capabilities[cap] || 0) + 1;
analysis.summary.totalCapabilities++;
});
}
// Tools analysis
if (agentData.tools) {
let tools = [];
if (Array.isArray(agentData.tools)) {
tools = agentData.tools;
} else if (agentData.tools.allowed) {
tools = agentData.tools.allowed;
}
tools.forEach(tool => {
analysis.summary.tools.add(tool);
analysis.tools[tool] = (analysis.tools[tool] || 0) + 1;
analysis.toolUsage[tool] = (analysis.toolUsage[tool] || 0) + 1;
analysis.summary.totalTools++;
});
}
// Dependencies analysis
if (agentData.dependencies) {
let deps = [];
if (Array.isArray(agentData.dependencies)) {
deps = agentData.dependencies;
} else if (agentData.dependencies.requires) {
deps = agentData.dependencies.requires.map(d => d.name || d);
}
if (deps.length > 0) {
analysis.dependencies[agentResult.name] = deps;
deps.forEach(dep => {
analysis.summary.dependencies.push({
from: agentResult.name,
to: dep
});
});
}
}
// Security analysis
if (agentData.security) {
if (agentData.security.sandboxing && agentData.security.sandboxing.enabled) {
analysis.securityAnalysis.sandboxed++;
} else {
analysis.securityAnalysis.unsandboxed++;
}
}
// Performance analysis
if (agentData.execution) {
if (agentData.execution.parallelization && agentData.execution.parallelization.enabled) {
analysis.performanceAnalysis.parallelEnabled++;
}
if (agentData.execution.batching && agentData.execution.batching.enabled) {
analysis.performanceAnalysis.batchingEnabled++;
}
}
return agentResult;
} catch (error) {
analysis.summary.invalidAgents++;
return {
name: path.basename(filePath).replace(/\.(md|json)$/, ''),
valid: false,
errors: [`Error analyzing file: ${error.message}`],
path: filePath
};
}
}
/**
* Calculate agent complexity score
*/
calculateComplexity(agentData) {
let score = 0;
// Capabilities complexity
if (agentData.capabilities && Array.isArray(agentData.capabilities)) {
score += agentData.capabilities.length;
}
// Tools complexity
if (agentData.tools) {
const toolCount = Array.isArray(agentData.tools) ?
agentData.tools.length :
(agentData.tools.allowed || []).length;
score += toolCount;
}
// Dependencies complexity
if (agentData.dependencies) {
const depCount = Array.isArray(agentData.dependencies) ?
agentData.dependencies.length :
(agentData.dependencies.requires || []).length;
score += depCount;
}
// Prompts complexity
if (agentData.prompts) {
score += Object.keys(agentData.prompts).length;
}
return score;
}
/**
* Finalize analysis and generate recommendations
*/
finalizeAnalysis(analysis) {
const summary = analysis.summary;
// Calculate averages and unique counts
summary.avgSize = summary.totalAgents > 0 ? summary.totalSize / summary.totalAgents : 0;
summary.uniqueCapabilities = summary.capabilities.size;
summary.uniqueTools = summary.tools.size;
// Keep as Set for tests that expect it, but also provide array versions
const capabilitiesArray = Array.from(summary.capabilities);
const toolsArray = Array.from(summary.tools);
// Check for circular dependencies
const circularDeps = this.findCircularDependencies(analysis.dependencies);
if (circularDeps.length > 0) {
circularDeps.forEach(circle => {
analysis.issues.push({
type: 'error',
message: `Circular dependency detected: ${circle}`
});
});
}
// Generate recommendations
const recommendations = [];
// Security recommendations
const unsandboxedPercent = (analysis.securityAnalysis.unsandboxed / summary.totalAgents) * 100;
if (unsandboxedPercent > 20) {
recommendations.push({
type: 'security',
severity: 'high',
message: `${unsandboxedPercent.toFixed(1)}% of agents are not sandboxed. Consider enabling sandboxing for better security.`
});
}
// Performance recommendations
const parallelPercent = (analysis.performanceAnalysis.parallelEnabled / summary.totalAgents) * 100;
if (parallelPercent < 50) {
recommendations.push({
type: 'performance',
severity: 'medium',
message: `Only ${parallelPercent.toFixed(1)}% of agents have parallel execution enabled. Consider enabling for better performance.`
});
}
// Dependency recommendations are already handled above via circularDeps
// Tool usage recommendations
const unusedTools = this.findUnusedTools(analysis.toolUsage, summary.tools);
if (unusedTools.length > 0) {
recommendations.push({
type: 'optimization',
severity: 'low',
message: `Some tools are rarely used: ${unusedTools.join(', ')}. Consider removing or consolidating.`
});
}
analysis.recommendations = recommendations;
}
/**
* Find circular dependencies
*/
findCircularDependencies(dependencies) {
const circles = [];
const visited = new Set();
const recursionStack = new Set();
const hasCycle = (node, path = []) => {
if (recursionStack.has(node)) {
const cycleStart = path.indexOf(node);
const cycle = [...path.slice(cycleStart), node].join(' -> ');
circles.push(cycle);
return true;
}
if (visited.has(node)) {
return false;
}
visited.add(node);
recursionStack.add(node);
path.push(node);
const neighbors = dependencies[node] || [];
for (const neighbor of neighbors) {
if (hasCycle(neighbor, [...path])) {
// Continue checking for more cycles
}
}
recursionStack.delete(node);
return false;
};
// Check all nodes for cycles
for (const node in dependencies) {
if (!visited.has(node)) {
hasCycle(node);
}
}
return circles;
}
/**
* Find rarely used tools
*/
findUnusedTools(toolUsage, allTools) {
const threshold = Math.max(1, allTools.length * 0.05); // 5% usage threshold
return Object.entries(toolUsage)
.filter(([tool, count]) => count <= threshold)
.map(([tool]) => tool);
}
/**
* Generate analysis report
*/
generateReport(analysis, format = 'text') {
if (format === 'json') {
return JSON.stringify(analysis, null, 2);
}
if (format === 'markdown') {
return this.generateMarkdownReport(analysis);
}
let report = `Claude Flow Agent System Analysis\n`;
report += `=================================\n\n`;
// Summary
report += `Summary:\n`;
report += `--------\n`;
report += `Total Agents: ${analysis.summary.totalAgents}\n`;
report += `Valid Agents: ${analysis.summary.validAgents}\n`;
report += `Invalid Agents: ${analysis.summary.invalidAgents}\n`;
if (analysis.summary.totalSize > 0) {
report += `Total Size: ${formatFileSize(analysis.summary.totalSize)}\n`;
report += `Average Size: ${formatFileSize(analysis.summary.avgSize)}\n`;
}
report += `\n`;
// Capability distribution
if (Object.keys(analysis.capabilities).length > 0) {
report += `Capability Distribution:\n`;
report += `------------------------\n`;
for (const [cap, count] of Object.entries(analysis.capabilities).sort((a, b) => b[1] - a[1])) {
report += `${cap}: ${count}\n`;
}
report += '\n';
}
// Tool usage
if (Object.keys(analysis.tools).length > 0) {
report += `Tool Usage:\n`;
report += `-----------\n`;
for (const [tool, count] of Object.entries(analysis.tools).sort((a, b) => b[1] - a[1])) {
report += `${tool}: ${count}\n`;
}
report += '\n';
}
// Issues
if (analysis.issues && analysis.issues.length > 0) {
report += `Issues:\n`;
report += `-------\n`;
for (const issue of analysis.issues) {
const icon = issue.type === 'error' ? '❌' : issue.type === 'warning' ? '⚠️' : 'ℹ️';
report += `${icon} ${issue.message}\n`;
}
report += '\n';
}
// Type distribution (if available)
if (analysis.summary.types && Object.keys(analysis.summary.types).length > 0) {
report += `Agent Types:\n`;
report += `-----------\n`;
for (const [type, count] of Object.entries(analysis.summary.types).sort((a, b) => b[1] - a[1])) {
const percent = ((count / analysis.summary.totalAgents) * 100).toFixed(1);
report += `${type}: ${count} (${percent}%)\n`;
}
report += '\n';
}
// Security analysis (if available)
if (analysis.securityAnalysis && (analysis.securityAnalysis.sandboxed > 0 || analysis.securityAnalysis.unsandboxed > 0)) {
report += `Security Analysis:\n`;
report += `-----------------\n`;
report += `Sandboxed: ${analysis.securityAnalysis.sandboxed}\n`;
report += `Not Sandboxed: ${analysis.securityAnalysis.unsandboxed}\n\n`;
}
// Performance analysis (if available)
if (analysis.performanceAnalysis && (analysis.performanceAnalysis.parallelEnabled > 0 || analysis.performanceAnalysis.batchingEnabled > 0)) {
report += `Performance Analysis:\n`;
report += `--------------------\n`;
report += `Parallel Execution Enabled: ${analysis.performanceAnalysis.parallelEnabled}\n`;
report += `Batching Enabled: ${analysis.performanceAnalysis.batchingEnabled}\n\n`;
}
// Recommendations
if (analysis.recommendations && analysis.recommendations.length > 0) {
report += `Recommendations:\n`;
report += `---------------\n`;
for (const rec of analysis.recommendations) {
report += `[${rec.severity.toUpperCase()}] ${rec.type}: ${rec.message}\n`;
}
}
return report;
}
/**
* Generate markdown report
*/
generateMarkdownReport(analysis) {
let report = `# Claude Flow Agent System Analysis\n\n`;
report += `## Summary\n\n`;
report += `| Metric | Value |\n`;
report += `|--------|-------|\n`;
report += `| Total Agents | ${analysis.summary.totalAgents} |\n`;
report += `| Valid Agents | ${analysis.summary.validAgents} |\n`;
report += `| Invalid Agents | ${analysis.summary.invalidAgents} |\n`;
if (analysis.summary.totalCapabilities) {
report += `| Total Capabilities | ${analysis.summary.totalCapabilities} |\n`;
report += `| Unique Capabilities | ${analysis.summary.uniqueCapabilities} |\n`;
}
if (analysis.summary.totalTools) {
report += `| Total Tools | ${analysis.summary.totalTools} |\n`;
report += `| Unique Tools | ${analysis.summary.uniqueTools} |\n`;
}
report += `\n`;
// Issues
if (analysis.issues && analysis.issues.length > 0) {
report += `## Issues\n\n`;
for (const issue of analysis.issues) {
const icon = issue.type === 'error' ? '❌' : issue.type === 'warning' ? '⚠️' : 'ℹ️';
report += `${icon} ${issue.message}\n\n`;
}
}
// Capabilities
if (Object.keys(analysis.capabilities).length > 0) {
report += `## Capability Distribution\n\n`;
report += `| Capability | Count |\n`;
report += `|------------|-------|\n`;
for (const [cap, count] of Object.entries(analysis.capabilities).sort((a, b) => b[1] - a[1])) {
report += `| ${cap} | ${count} |\n`;
}
report += `\n`;
}
// Tools
if (Object.keys(analysis.tools).length > 0) {
report += `## Tool Usage\n\n`;
report += `| Tool | Count |\n`;
report += `|------|-------|\n`;
for (const [tool, count] of Object.entries(analysis.tools).sort((a, b) => b[1] - a[1])) {
report += `| ${tool} | ${count} |\n`;
}
report += `\n`;
}
return report;
}
}