@aigentics/agent-toolkit
Version:
Comprehensive toolkit for validating and managing Claude Flow agent systems
392 lines (342 loc) • 13.6 kB
JavaScript
/**
* Agent Validator
* Validates agent configurations against the standard schema
*/
import path from 'path';
import fs from 'fs/promises';
import { AgentConfig } from './config.mjs';
import {
extractYamlFrontmatter,
findMarkdownFiles,
getRelativePath,
safeReadFile
} from './utils.mjs';
export class AgentValidator {
constructor(options = {}) {
this.baseDir = options.baseDir || process.cwd();
this.agentsDir = options.agentsDir || path.join(this.baseDir, '.claude/agents');
this.excludeFiles = options.excludeFiles || [
'README.md',
'MIGRATION_SUMMARY.md',
'agent-types.md',
'base-agent.yaml',
'SYSTEM_STATUS.md'
];
this.excludeDirs = options.excludeDirs || ['docs', '_templates'];
this.verbose = options.verbose || false;
}
/**
* Find all JSON files recursively
*/
async findJsonFiles(dir) {
const files = [];
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory() && !this.excludeDirs.includes(entry.name)) {
files.push(...await this.findJsonFiles(fullPath));
} else if (entry.isFile() && entry.name.endsWith('.json')) {
files.push(fullPath);
}
}
} catch (error) {
// Ignore errors
}
return files;
}
/**
* Validate all agents in the directory
*/
async validateAll() {
// Check if directory exists
try {
await fs.stat(this.agentsDir);
} catch (error) {
throw new Error('No agents found');
}
const mdFiles = await findMarkdownFiles(this.agentsDir);
const jsonFiles = await this.findJsonFiles(this.agentsDir);
const allFiles = [...mdFiles, ...jsonFiles];
if (allFiles.length === 0) {
throw new Error('No agents found');
}
// Filter out excluded files and directories
const agentFiles = allFiles.filter(f => {
const fileName = path.basename(f);
const relativePath = getRelativePath(f, this.agentsDir);
// Exclude specific files
if (this.excludeFiles.includes(fileName)) {
return false;
}
// Exclude specific directories
for (const excludeDir of this.excludeDirs) {
if (relativePath.startsWith(excludeDir + path.sep)) {
return false;
}
}
return true;
});
const results = {
total: agentFiles.length,
valid: 0,
warnings: 0,
errors: 0,
details: []
};
for (const filePath of agentFiles) {
const result = await this.validateFile(filePath);
results.details.push(result);
if (result.status === 'valid') results.valid++;
else if (result.status === 'warning') results.warnings++;
else results.errors++;
}
results.typeStats = this.calculateTypeStats(results.details);
return results;
}
/**
* Validate a single agent file
*/
async validateFile(filePath) {
try {
let agentData;
const content = await safeReadFile(filePath);
if (filePath.endsWith('.json')) {
// Handle JSON files
try {
agentData = JSON.parse(content);
} catch (error) {
return {
file: filePath,
relativePath: getRelativePath(filePath, this.agentsDir),
status: 'error',
errors: [`Failed to parse JSON: ${error.message}`],
warnings: [],
agent_name: 'unknown',
agent_type: 'unknown'
};
}
} else {
// Handle Markdown files
const [yamlData, remainingContent] = extractYamlFrontmatter(content);
agentData = yamlData;
if (Object.keys(agentData).length === 0) {
return {
file: filePath,
relativePath: getRelativePath(filePath, this.agentsDir),
status: 'error',
errors: ['No YAML frontmatter found'],
warnings: [],
agent_name: 'unknown',
agent_type: 'unknown'
};
}
}
// Validate configuration
const configErrors = this.validateConfig(agentData);
const directoryErrors = this.validateDirectory(filePath, agentData);
const allErrors = [...configErrors, ...directoryErrors];
const warnings = this.checkWarnings(agentData);
const status = allErrors.length > 0 ? 'error' : (warnings.length > 0 ? 'warning' : 'valid');
return {
file: filePath,
relativePath: getRelativePath(filePath, this.agentsDir),
status,
errors: allErrors,
warnings,
agent_name: agentData.name || 'unknown',
agent_type: agentData.type || 'unknown'
};
} catch (error) {
return {
file: filePath,
relativePath: getRelativePath(filePath, this.agentsDir),
status: 'error',
errors: [`Failed to process file: ${error.message}`],
warnings: [],
agent_name: 'unknown',
agent_type: 'unknown'
};
}
}
/**
* Validate agent configuration
*/
validateConfig(agentData) {
const errors = [];
// Check required fields
for (const field of AgentConfig.REQUIRED_FIELDS) {
if (!(field in agentData)) {
errors.push(`Missing required field: ${field}`);
}
}
// Validate specific fields
if ('name' in agentData) {
const name = agentData.name;
if (typeof name !== 'string' || !name) {
errors.push("'name' must be a non-empty string");
} else if (!/^[a-z][a-z0-9-]*$/.test(name)) {
errors.push("'name' must be kebab-case (lowercase, hyphens only)");
}
}
if ('type' in agentData) {
const agentType = agentData.type;
if (!AgentConfig.VALID_TYPES.includes(agentType)) {
errors.push(`Invalid type '${agentType}'. Must be one of: ${AgentConfig.VALID_TYPES.join(', ')}`);
}
}
if ('priority' in agentData) {
const priority = agentData.priority;
if (!AgentConfig.VALID_PRIORITIES.includes(priority)) {
errors.push(`Invalid priority '${priority}'. Must be one of: ${AgentConfig.VALID_PRIORITIES.join(', ')}`);
}
}
if ('color' in agentData) {
const color = agentData.color;
if (typeof color !== 'string' || !/^#[0-9A-Fa-f]{6}$/.test(color)) {
errors.push("'color' must be a valid hex color (e.g., '#FF6B35')");
}
}
if ('version' in agentData) {
const version = agentData.version;
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) {
errors.push("'version' must follow semantic versioning (e.g., '1.0.0')");
}
}
// Validate tools structure
if ('tools' in agentData) {
const tools = agentData.tools;
if (Array.isArray(tools)) {
errors.push("'tools' must be an object with 'allowed', 'restricted', and 'conditional' properties, not an array");
} else if (typeof tools !== 'object') {
errors.push("'tools' must be an object");
} else {
const requiredToolFields = ['allowed', 'restricted'];
for (const field of requiredToolFields) {
if (!(field in tools)) {
errors.push(`'tools.${field}' is required`);
} else if (!Array.isArray(tools[field])) {
errors.push(`'tools.${field}' must be an array`);
}
}
}
}
return errors;
}
/**
* Validate directory placement
*/
validateDirectory(filePath, agentData) {
const errors = [];
if (agentData.type) {
const result = AgentConfig.validateDirectoryPlacement(filePath, agentData.type);
if (!result.valid) {
errors.push(result.error);
}
}
return errors;
}
/**
* Check for warnings
*/
checkWarnings(agentData) {
const warnings = [];
if ('capabilities' in agentData && (!agentData.capabilities || agentData.capabilities.length === 0)) {
warnings.push("No capabilities defined");
}
if ('triggers' in agentData) {
const triggers = agentData.triggers;
const hasAnyTriggers = ['keywords', 'patterns', 'file_patterns', 'context_patterns']
.some(key => triggers[key] && triggers[key].length > 0);
if (!hasAnyTriggers) {
warnings.push("No trigger patterns defined");
}
}
return warnings;
}
/**
* Calculate statistics by agent type
*/
calculateTypeStats(results) {
const typeStats = {};
for (const result of results) {
const type = result.agent_type;
if (!typeStats[type]) {
typeStats[type] = { total: 0, valid: 0, warnings: 0, errors: 0 };
}
typeStats[type].total++;
if (result.status === 'valid') typeStats[type].valid++;
else if (result.status === 'warning') typeStats[type].warnings++;
else typeStats[type].errors++;
}
return typeStats;
}
/**
* Generate validation report
*/
generateReport(results, format = 'text') {
if (format === 'json') {
return JSON.stringify({
valid: results.valid,
warnings: results.warnings,
errors: results.errors,
total: results.total,
results: results.details,
typeStats: results.typeStats
}, null, 2);
}
let report = `Agent Validation Report\n`;
report += `=======================\n\n`;
report += `Total Agents: ${results.total}\n`;
const validPercent = results.total > 0 ? Math.round(results.valid / results.total * 100) : 0;
report += `✓ Valid: ${results.valid} (${validPercent}%)\n`;
report += `⚠ Warnings: ${results.warnings}\n`;
report += `✗ Errors: ${results.errors}\n\n`;
// Show valid agents
if (results.valid > 0) {
report += `Valid Agents:\n`;
report += `-------------\n`;
for (const result of results.details) {
if (result.status === 'valid') {
report += `✓ ${result.agent_name}\n`;
}
}
report += '\n';
}
// Add success message for single agent validation
if (results.total === 1 && results.valid === 1) {
report += 'All agents are valid!\n\n';
}
if (results.errors > 0) {
report += `Errors Found:\n`;
report += `-------------\n`;
for (const result of results.details) {
if (result.status === 'error') {
report += `\n${result.relativePath}:\n`;
for (const error of result.errors) {
report += ` • ${error}\n`;
}
}
}
}
if (results.warnings > 0) {
report += `\nWarnings Found:\n`;
report += `---------------\n`;
for (const result of results.details) {
if (result.status === 'warning') {
report += `\n${result.relativePath}:\n`;
for (const warning of result.warnings) {
report += ` • ${warning}\n`;
}
}
}
}
report += `\nStatistics by Type:\n`;
report += `------------------\n`;
for (const [type, stats] of Object.entries(results.typeStats).sort()) {
const validPercent = Math.round((stats.valid / stats.total) * 100);
report += `${type}: ${stats.total} total, ${stats.valid} valid (${validPercent}%)\n`;
}
return report;
}
}