feat: implement recursive agent discovery and compilation
- Module agents now discovered recursively at any depth in agents folder - .agent.yaml files are compiled to .md format during module installation - Custom agents also support subdirectory structure - Agents maintain their directory structure when installed - YAML files are skipped during file copying as they're compiled separately - Added compileModuleAgents method to handle YAML-to-MD compilation - Updated discoverAgents to recursively search for .agent.yaml files - Agents in subdirectories are properly placed in _cfg/agents with relative paths This fixes issue where agents like cbt-coach were not being compiled and were only copied as YAML files.
This commit is contained in:
parent
0d83799ecf
commit
1bd01e1ce6
|
|
@ -2532,8 +2532,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
agentType = parts.slice(-2).join('-'); // Take last 2 parts as type
|
agentType = parts.slice(-2).join('-'); // Take last 2 parts as type
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create target directory
|
// Create target directory - use relative path if agent is in a subdirectory
|
||||||
const agentTargetDir = path.join(customAgentsDir, finalAgentName);
|
const agentTargetDir = agent.relativePath
|
||||||
|
? path.join(customAgentsDir, agent.relativePath)
|
||||||
|
: path.join(customAgentsDir, finalAgentName);
|
||||||
await fs.ensureDir(agentTargetDir);
|
await fs.ensureDir(agentTargetDir);
|
||||||
|
|
||||||
// Calculate paths
|
// Calculate paths
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,9 @@ class ModuleManager {
|
||||||
// Copy module files with filtering
|
// Copy module files with filtering
|
||||||
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
|
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
|
||||||
|
|
||||||
|
// Compile any .agent.yaml files to .md format
|
||||||
|
await this.compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir);
|
||||||
|
|
||||||
// Process agent files to inject activation block
|
// Process agent files to inject activation block
|
||||||
await this.processAgentFiles(targetPath, moduleName);
|
await this.processAgentFiles(targetPath, moduleName);
|
||||||
|
|
||||||
|
|
@ -491,6 +494,11 @@ class ModuleManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip .agent.yaml files - they will be compiled separately
|
||||||
|
if (file.endsWith('.agent.yaml')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip user documentation if install_user_docs is false
|
// Skip user documentation if install_user_docs is false
|
||||||
if (moduleConfig.install_user_docs === false && (file.startsWith('docs/') || file.startsWith('docs\\'))) {
|
if (moduleConfig.install_user_docs === false && (file.startsWith('docs/') || file.startsWith('docs\\'))) {
|
||||||
console.log(chalk.dim(` Skipping user documentation: ${file}`));
|
console.log(chalk.dim(` Skipping user documentation: ${file}`));
|
||||||
|
|
@ -633,6 +641,91 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile .agent.yaml files to .md format in modules
|
||||||
|
* @param {string} sourcePath - Source module path
|
||||||
|
* @param {string} targetPath - Target module path
|
||||||
|
* @param {string} moduleName - Module name
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
*/
|
||||||
|
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir) {
|
||||||
|
const sourceAgentsPath = path.join(sourcePath, 'agents');
|
||||||
|
const targetAgentsPath = path.join(targetPath, 'agents');
|
||||||
|
const cfgAgentsDir = path.join(bmadDir, '_cfg', 'agents');
|
||||||
|
|
||||||
|
// Check if agents directory exists in source
|
||||||
|
if (!(await fs.pathExists(sourceAgentsPath))) {
|
||||||
|
return; // No agents to compile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all agent YAML files recursively
|
||||||
|
const agentFiles = await this.findAgentFiles(sourceAgentsPath);
|
||||||
|
|
||||||
|
for (const agentFile of agentFiles) {
|
||||||
|
if (!agentFile.endsWith('.agent.yaml')) continue;
|
||||||
|
|
||||||
|
const relativePath = path.relative(sourceAgentsPath, agentFile);
|
||||||
|
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
|
||||||
|
|
||||||
|
await fs.ensureDir(targetDir);
|
||||||
|
|
||||||
|
const agentName = path.basename(agentFile, '.agent.yaml');
|
||||||
|
const sourceYamlPath = agentFile;
|
||||||
|
const targetMdPath = path.join(targetDir, `${agentName}.md`);
|
||||||
|
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
|
||||||
|
|
||||||
|
// Read and compile the YAML
|
||||||
|
try {
|
||||||
|
const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
|
||||||
|
const { compileAgent } = require('../../../lib/agent/compiler');
|
||||||
|
|
||||||
|
// Check for customizations
|
||||||
|
let customizedFields = [];
|
||||||
|
if (await fs.pathExists(customizePath)) {
|
||||||
|
const customizeContent = await fs.readFile(customizePath, 'utf8');
|
||||||
|
const customizeData = yaml.load(customizeContent);
|
||||||
|
customizedFields = customizeData.customized_fields || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile with customizations if any
|
||||||
|
const { xml } = compileAgent(yamlContent, customizedFields, agentName, relativePath);
|
||||||
|
|
||||||
|
// Write the compiled MD file
|
||||||
|
await fs.writeFile(targetMdPath, xml, 'utf8');
|
||||||
|
|
||||||
|
console.log(chalk.dim(` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}`));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all .agent.yaml files recursively in a directory
|
||||||
|
* @param {string} dir - Directory to search
|
||||||
|
* @returns {Array} List of .agent.yaml file paths
|
||||||
|
*/
|
||||||
|
async findAgentFiles(dir) {
|
||||||
|
const agentFiles = [];
|
||||||
|
|
||||||
|
async function searchDirectory(searchDir) {
|
||||||
|
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(searchDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
|
||||||
|
agentFiles.push(fullPath);
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
await searchDirectory(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await searchDirectory(dir);
|
||||||
|
return agentFiles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process agent files to inject activation block
|
* Process agent files to inject activation block
|
||||||
* @param {string} modulePath - Path to installed module
|
* @param {string} modulePath - Path to installed module
|
||||||
|
|
@ -646,24 +739,49 @@ class ModuleManager {
|
||||||
return; // No agents to process
|
return; // No agents to process
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all agent files
|
// Get all agent MD files recursively
|
||||||
const agentFiles = await fs.readdir(agentsPath);
|
const agentFiles = await this.findAgentMdFiles(agentsPath);
|
||||||
|
|
||||||
for (const agentFile of agentFiles) {
|
for (const agentFile of agentFiles) {
|
||||||
if (!agentFile.endsWith('.md')) continue;
|
if (!agentFile.endsWith('.md')) continue;
|
||||||
|
|
||||||
const agentPath = path.join(agentsPath, agentFile);
|
let content = await fs.readFile(agentFile, 'utf8');
|
||||||
let content = await fs.readFile(agentPath, 'utf8');
|
|
||||||
|
|
||||||
// Check if content has agent XML and no activation block
|
// Check if content has agent XML and no activation block
|
||||||
if (content.includes('<agent') && !content.includes('<activation')) {
|
if (content.includes('<agent') && !content.includes('<activation')) {
|
||||||
// Inject the activation block using XML handler
|
// Inject the activation block using XML handler
|
||||||
content = this.xmlHandler.injectActivationSimple(content);
|
content = this.xmlHandler.injectActivationSimple(content);
|
||||||
await fs.writeFile(agentPath, content, 'utf8');
|
await fs.writeFile(agentFile, content, 'utf8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all .md agent files recursively in a directory
|
||||||
|
* @param {string} dir - Directory to search
|
||||||
|
* @returns {Array} List of .md agent file paths
|
||||||
|
*/
|
||||||
|
async findAgentMdFiles(dir) {
|
||||||
|
const agentFiles = [];
|
||||||
|
|
||||||
|
async function searchDirectory(searchDir) {
|
||||||
|
const entries = await fs.readdir(searchDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(searchDir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||||
|
agentFiles.push(fullPath);
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
await searchDirectory(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await searchDirectory(dir);
|
||||||
|
return agentFiles;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vendor cross-module workflows referenced in agent files
|
* Vendor cross-module workflows referenced in agent files
|
||||||
* Scans SOURCE agent.yaml files for workflow-install and copies workflows to destination
|
* Scans SOURCE agent.yaml files for workflow-install and copies workflows to destination
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ function resolvePath(pathStr, context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover available agents in the custom agent location
|
* Discover available agents in the custom agent location recursively
|
||||||
* @param {string} searchPath - Path to search for agents
|
* @param {string} searchPath - Path to search for agents
|
||||||
* @returns {Array} List of agent info objects
|
* @returns {Array} List of agent info objects
|
||||||
*/
|
*/
|
||||||
|
|
@ -56,35 +56,59 @@ function discoverAgents(searchPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const agents = [];
|
const agents = [];
|
||||||
const entries = fs.readdirSync(searchPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
// Helper function to recursively search
|
||||||
const fullPath = path.join(searchPath, entry.name);
|
function searchDirectory(dir, relativePath = '') {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
|
for (const entry of entries) {
|
||||||
// Simple agent (single file)
|
const fullPath = path.join(dir, entry.name);
|
||||||
agents.push({
|
const agentRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
||||||
type: 'simple',
|
|
||||||
name: entry.name.replace('.agent.yaml', ''),
|
if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
|
||||||
path: fullPath,
|
// Simple agent (single file)
|
||||||
yamlFile: fullPath,
|
// The agent name is based on the filename
|
||||||
});
|
const agentName = entry.name.replace('.agent.yaml', '');
|
||||||
} else if (entry.isDirectory()) {
|
|
||||||
// Check for agent with sidecar (folder containing .agent.yaml)
|
|
||||||
const yamlFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.agent.yaml'));
|
|
||||||
if (yamlFiles.length === 1) {
|
|
||||||
const agentYamlPath = path.join(fullPath, yamlFiles[0]);
|
|
||||||
agents.push({
|
agents.push({
|
||||||
type: 'expert',
|
type: 'simple',
|
||||||
name: entry.name,
|
name: agentName,
|
||||||
path: fullPath,
|
path: fullPath,
|
||||||
yamlFile: agentYamlPath,
|
yamlFile: fullPath,
|
||||||
hasSidecar: true,
|
relativePath: agentRelativePath.replace('.agent.yaml', ''),
|
||||||
});
|
});
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
// Check if this directory contains an .agent.yaml file
|
||||||
|
try {
|
||||||
|
const dirContents = fs.readdirSync(fullPath);
|
||||||
|
const yamlFiles = dirContents.filter((f) => f.endsWith('.agent.yaml'));
|
||||||
|
|
||||||
|
if (yamlFiles.length > 0) {
|
||||||
|
// Found .agent.yaml files in this directory
|
||||||
|
for (const yamlFile of yamlFiles) {
|
||||||
|
const agentYamlPath = path.join(fullPath, yamlFile);
|
||||||
|
const agentName = path.basename(yamlFile, '.agent.yaml');
|
||||||
|
|
||||||
|
agents.push({
|
||||||
|
type: 'expert',
|
||||||
|
name: agentName,
|
||||||
|
path: fullPath,
|
||||||
|
yamlFile: agentYamlPath,
|
||||||
|
hasSidecar: true,
|
||||||
|
relativePath: agentRelativePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No .agent.yaml in this directory, recurse deeper
|
||||||
|
searchDirectory(fullPath, agentRelativePath);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip directories we can't read
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchDirectory(searchPath);
|
||||||
return agents;
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue