BMAD-METHOD/bmad-claude-integration/core/bmad-loader.js

382 lines
9.9 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
class BMADLoader {
constructor(options = {}) {
// Try to find bmad-core in parent directories if not specified
this.bmadRoot = options.bmadRoot || this.findBmadRoot();
this.cache = new Map();
this.agentPaths = {
core: path.join(this.bmadRoot, 'agents'),
tasks: path.join(this.bmadRoot, 'tasks'),
checklists: path.join(this.bmadRoot, 'checklists'),
data: path.join(this.bmadRoot, 'data'),
templates: path.join(this.bmadRoot, 'templates')
};
}
findBmadRoot() {
let currentDir = __dirname;
for (let i = 0; i < 5; i++) {
const candidate = path.join(currentDir, 'bmad-core');
if (require('fs').existsSync(candidate)) {
return candidate;
}
currentDir = path.dirname(currentDir);
}
return path.join(process.cwd(), 'bmad-core');
}
async loadAgent(agentName) {
const cacheKey = `agent:${agentName}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const agentPath = path.join(this.agentPaths.core, `${agentName}.md`);
const content = await fs.readFile(agentPath, 'utf8');
const parsed = this.parseAgentFile(content);
this.cache.set(cacheKey, parsed);
return parsed;
}
parseAgentFile(content) {
const lines = content.split('\n');
const result = {
title: '',
yaml: null,
rawContent: content,
sections: {},
dependencies: {}
};
// Extract title
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
result.title = titleMatch[1].trim();
}
// Extract YAML block
const yamlMatch = content.match(/```yaml\n([\s\S]+?)\n```/);
if (yamlMatch) {
try {
result.yaml = yaml.load(yamlMatch[1]);
// Extract key information from YAML
if (result.yaml.agent) {
result.agent = result.yaml.agent;
}
if (result.yaml.persona) {
result.persona = result.yaml.persona;
}
if (result.yaml.commands) {
result.commands = result.yaml.commands;
}
if (result.yaml.dependencies) {
result.dependencies = result.yaml.dependencies;
}
if (result.yaml['activation-instructions']) {
result.activationInstructions = result.yaml['activation-instructions'];
}
} catch (e) {
console.error('Failed to parse YAML:', e);
}
}
// Extract sections
let currentSection = null;
let sectionContent = [];
for (const line of lines) {
if (line.match(/^##\s+(.+)$/)) {
if (currentSection) {
result.sections[currentSection] = sectionContent.join('\n').trim();
}
currentSection = line.match(/^##\s+(.+)$/)[1];
sectionContent = [];
} else if (currentSection) {
sectionContent.push(line);
}
}
if (currentSection) {
result.sections[currentSection] = sectionContent.join('\n').trim();
}
return result;
}
async loadTask(taskName) {
const cacheKey = `task:${taskName}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const taskPath = path.join(this.agentPaths.tasks, taskName);
const content = await fs.readFile(taskPath, 'utf8');
const parsed = this.parseTaskFile(content);
this.cache.set(cacheKey, parsed);
return parsed;
}
parseTaskFile(content) {
const result = {
title: '',
purpose: '',
instructions: [],
scenarios: [],
elicitation: false,
rawContent: content
};
// Extract title
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
result.title = titleMatch[1].trim();
}
// Check if this is an elicitation task
if (content.includes('elicitation') || content.includes('Elicitation')) {
result.elicitation = true;
}
// Extract purpose
const purposeMatch = content.match(/##\s+Purpose\s*\n([\s\S]+?)(?=\n##|$)/);
if (purposeMatch) {
result.purpose = purposeMatch[1].trim();
}
// Extract instructions
const instructionsMatch = content.match(/##\s+(?:Task\s+)?Instructions?\s*\n([\s\S]+?)(?=\n##|$)/);
if (instructionsMatch) {
result.instructions = this.parseInstructions(instructionsMatch[1]);
}
// Extract scenarios
const scenarioMatches = content.matchAll(/###\s+Scenario\s+\d+:\s*(.+?)\n([\s\S]+?)(?=\n###|\n##|$)/g);
for (const match of scenarioMatches) {
result.scenarios.push({
title: match[1].trim(),
content: match[2].trim()
});
}
return result;
}
parseInstructions(instructionsText) {
const instructions = [];
const lines = instructionsText.split('\n');
let currentInstruction = null;
let subItems = [];
for (const line of lines) {
const mainMatch = line.match(/^\d+\.\s+\*\*(.+?)\*\*:?\s*(.*)/);
const subMatch = line.match(/^\s*[-•]\s+(.+)/);
if (mainMatch) {
if (currentInstruction) {
instructions.push({
...currentInstruction,
subItems
});
}
currentInstruction = {
title: mainMatch[1],
description: mainMatch[2] || ''
};
subItems = [];
} else if (subMatch && currentInstruction) {
subItems.push(subMatch[1]);
} else if (currentInstruction && line.trim()) {
currentInstruction.description += ' ' + line.trim();
}
}
if (currentInstruction) {
instructions.push({
...currentInstruction,
subItems
});
}
return instructions;
}
async loadChecklist(checklistName) {
const cacheKey = `checklist:${checklistName}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const checklistPath = path.join(this.agentPaths.checklists, checklistName);
const content = await fs.readFile(checklistPath, 'utf8');
const parsed = this.parseChecklistFile(content);
this.cache.set(cacheKey, parsed);
return parsed;
}
parseChecklistFile(content) {
const result = {
title: '',
items: [],
rawContent: content
};
// Extract title
const titleMatch = content.match(/^#\s+(.+)$/m);
if (titleMatch) {
result.title = titleMatch[1].trim();
}
// Extract checklist items
const itemMatches = content.matchAll(/^\s*[-□]\s+(.+)$/gm);
for (const match of itemMatches) {
result.items.push({
text: match[1].trim(),
checked: false
});
}
return result;
}
async loadTemplate(templateName) {
const cacheKey = `template:${templateName}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const templatePath = path.join(this.agentPaths.templates, templateName);
const content = await fs.readFile(templatePath, 'utf8');
const parsed = this.parseTemplateFile(content, templateName);
this.cache.set(cacheKey, parsed);
return parsed;
}
parseTemplateFile(content, filename) {
const ext = path.extname(filename);
if (ext === '.yaml' || ext === '.yml') {
return {
type: 'yaml',
content: yaml.load(content),
rawContent: content
};
} else {
return {
type: 'markdown',
content: content,
sections: this.parseMarkdownSections(content)
};
}
}
parseMarkdownSections(content) {
const sections = {};
const lines = content.split('\n');
let currentSection = 'header';
let sectionContent = [];
for (const line of lines) {
const sectionMatch = line.match(/^#+\s+(.+)$/);
if (sectionMatch) {
if (sectionContent.length > 0) {
sections[currentSection] = sectionContent.join('\n').trim();
}
currentSection = sectionMatch[1].toLowerCase().replace(/\s+/g, '-');
sectionContent = [];
} else {
sectionContent.push(line);
}
}
if (sectionContent.length > 0) {
sections[currentSection] = sectionContent.join('\n').trim();
}
return sections;
}
async listAgents() {
try {
const files = await fs.readdir(this.agentPaths.core);
return files
.filter(f => f.endsWith('.md'))
.map(f => f.replace('.md', ''));
} catch (error) {
return [];
}
}
async getAgentMetadata(agentName) {
const agent = await this.loadAgent(agentName);
return {
name: agent.agent?.name || agentName,
id: agent.agent?.id || agentName,
title: agent.agent?.title || agent.title,
icon: agent.agent?.icon || '🤖',
whenToUse: agent.agent?.whenToUse || '',
commands: agent.commands || [],
dependencies: agent.dependencies || {}
};
}
clearCache() {
this.cache.clear();
}
}
// CLI interface for testing
if (require.main === module) {
const loader = new BMADLoader();
const commands = {
async list() {
const agents = await loader.listAgents();
console.log('Available agents:', agents.join(', '));
},
async load(agentName) {
const agent = await loader.loadAgent(agentName);
console.log(JSON.stringify(agent, null, 2));
},
async metadata(agentName) {
const metadata = await loader.getAgentMetadata(agentName);
console.log(JSON.stringify(metadata, null, 2));
},
async task(taskName) {
const task = await loader.loadTask(taskName);
console.log(JSON.stringify(task, null, 2));
},
async checklist(checklistName) {
const checklist = await loader.loadChecklist(checklistName);
console.log(JSON.stringify(checklist, null, 2));
}
};
const [,, command, ...args] = process.argv;
if (commands[command]) {
commands[command](...args).catch(console.error);
} else {
console.log('Usage: bmad-loader.js <command> [args]');
console.log('Commands:', Object.keys(commands).join(', '));
}
}
module.exports = BMADLoader;