const path = require('node:path'); const fs = require('fs-extra'); const AgentPartyGenerator = { /** * Generate agent-party.xml content * @param {Array} agentDetails - Array of agent details * @param {Object} options - Generation options * @returns {string} XML content */ generateAgentParty(agentDetails, options = {}) { const { forWeb = false } = options; // Group agents by module const agentsByModule = { bmm: [], cis: [], core: [], custom: [], }; for (const agent of agentDetails) { const moduleKey = agentsByModule[agent.module] ? agent.module : 'custom'; agentsByModule[moduleKey].push(agent); } // Build XML content let xmlContent = ` Complete roster of ${forWeb ? 'bundled' : 'installed'} BMAD agents with summarized personas for efficient multi-agent orchestration. Used by party-mode and other multi-agent coordination features. `; // Add agents by module for (const [module, agents] of Object.entries(agentsByModule)) { if (agents.length === 0) continue; const moduleTitle = module === 'bmm' ? 'BMM Module' : module === 'cis' ? 'CIS Module' : module === 'core' ? 'Core Module' : 'Custom Module'; xmlContent += `\n \n`; for (const agent of agents) { xmlContent += ` ${this.escapeXml(agent.role || '')} ${this.escapeXml(agent.identity || '')} ${this.escapeXml(agent.communicationStyle || '')} ${agent.principles || ''} \n`; } } // Add statistics const totalAgents = agentDetails.length; const moduleList = Object.keys(agentsByModule) .filter((m) => agentsByModule[m].length > 0) .join(', '); xmlContent += `\n ${totalAgents} ${moduleList} ${new Date().toISOString()} `; return xmlContent; }, /** * Extract agent details from XML content * @param {string} content - Full agent file content (markdown with XML) * @param {string} moduleName - Module name * @param {string} agentName - Agent name * @returns {Object} Agent details */ extractAgentDetails(content, moduleName, agentName) { try { // Extract agent XML block const agentMatch = content.match(/]*>([\s\S]*?)<\/agent>/); if (!agentMatch) return null; const agentXml = agentMatch[0]; // Extract attributes from opening tag const nameMatch = agentXml.match(/name="([^"]*)"/); const titleMatch = agentXml.match(/title="([^"]*)"/); const iconMatch = agentXml.match(/icon="([^"]*)"/); // Extract persona elements - now we just copy them as-is const roleMatch = agentXml.match(/([\s\S]*?)<\/role>/); const identityMatch = agentXml.match(/([\s\S]*?)<\/identity>/); const styleMatch = agentXml.match(/([\s\S]*?)<\/communication_style>/); const principlesMatch = agentXml.match(/([\s\S]*?)<\/principles>/); return { id: `bmad/${moduleName}/agents/${agentName}.md`, name: nameMatch ? nameMatch[1] : agentName, title: titleMatch ? titleMatch[1] : 'Agent', icon: iconMatch ? iconMatch[1] : '🤖', module: moduleName, role: roleMatch ? roleMatch[1].trim() : '', identity: identityMatch ? identityMatch[1].trim() : '', communicationStyle: styleMatch ? styleMatch[1].trim() : '', principles: principlesMatch ? principlesMatch[1].trim() : '', }; } catch (error) { console.error(`Error extracting details for agent ${agentName}:`, error); return null; } }, /** * Extract attribute from XML tag */ extractAttribute(xml, tagName, attrName) { const regex = new RegExp(`<${tagName}[^>]*\\s${attrName}="([^"]*)"`, 'i'); const match = xml.match(regex); return match ? match[1] : ''; }, /** * Escape XML special characters */ escapeXml(text) { if (!text) return ''; return text .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); }, /** * Apply config overrides to agent details * @param {Object} details - Original agent details * @param {string} configContent - Config file content * @returns {Object} Agent details with overrides applied */ applyConfigOverrides(details, configContent) { try { // Extract agent-config XML block const configMatch = configContent.match(/([\s\S]*?)<\/agent-config>/); if (!configMatch) return details; const configXml = configMatch[0]; // Extract override values const nameMatch = configXml.match(/([\s\S]*?)<\/name>/); const titleMatch = configXml.match(/([\s\S]*?)<\/title>/); const roleMatch = configXml.match(/<role>([\s\S]*?)<\/role>/); const identityMatch = configXml.match(/<identity>([\s\S]*?)<\/identity>/); const styleMatch = configXml.match(/<communication_style>([\s\S]*?)<\/communication_style>/); const principlesMatch = configXml.match(/<principles>([\s\S]*?)<\/principles>/); // Apply overrides only if values are non-empty if (nameMatch && nameMatch[1].trim()) { details.name = nameMatch[1].trim(); } if (titleMatch && titleMatch[1].trim()) { details.title = titleMatch[1].trim(); } if (roleMatch && roleMatch[1].trim()) { details.role = roleMatch[1].trim(); } if (identityMatch && identityMatch[1].trim()) { details.identity = identityMatch[1].trim(); } if (styleMatch && styleMatch[1].trim()) { details.communicationStyle = styleMatch[1].trim(); } if (principlesMatch && principlesMatch[1].trim()) { // Principles are now just copied as-is (narrative paragraph) details.principles = principlesMatch[1].trim(); } return details; } catch (error) { console.error(`Error applying config overrides:`, error); return details; } }, /** * Write agent-party.xml to file */ async writeAgentParty(filePath, agentDetails, options = {}) { const content = this.generateAgentParty(agentDetails, options); await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, content, 'utf8'); return content; }, }; module.exports = { AgentPartyGenerator };