158 lines
4.4 KiB
JavaScript
158 lines
4.4 KiB
JavaScript
/**
|
|
* Spike: Convert subagent Markdown (frontmatter + sections) into BMAD *.agent.yaml
|
|
*
|
|
* Usage:
|
|
* node tools/cli/spikes/convert-subagent-md.js subagentic/claude-subagents/agents/master.md \
|
|
* src/modules/subagentic/agents/master.agent.yaml
|
|
*/
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const yaml = require('js-yaml');
|
|
|
|
function readFile(p) {
|
|
return fs.readFileSync(p, 'utf8');
|
|
}
|
|
|
|
function writeFile(p, content) {
|
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
fs.writeFileSync(p, content, 'utf8');
|
|
}
|
|
|
|
function parseFrontmatter(md) {
|
|
const fmMatch = md.match(/^---\n([\s\S]*?)\n---\n/);
|
|
if (!fmMatch) return {};
|
|
try {
|
|
return yaml.load(fmMatch[1]) || {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function extractSection(md, headingRegex) {
|
|
const lines = md.split(/\r?\n/);
|
|
let start = -1;
|
|
for (const [i, line] of lines.entries()) {
|
|
if (headingRegex.test(line)) {
|
|
start = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
if (start === -1) return [];
|
|
const out = [];
|
|
for (let i = start; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line.startsWith('#')) break; // next heading
|
|
out.push(line);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function extractPrinciples(md) {
|
|
// Section: "# Core Operating Principles" — numbered list lines
|
|
const body = extractSection(md, /^#\s+Core Operating Principles/i);
|
|
const principles = [];
|
|
for (const line of body) {
|
|
const m = line.match(/^\s*\d+\.\s*(.*\S)\s*$/);
|
|
if (m) principles.push(m[1]);
|
|
}
|
|
return principles;
|
|
}
|
|
|
|
function kebabize(s) {
|
|
return String(s)
|
|
.trim()
|
|
.replace(/^\*/, '') // strip leading asterisk from commands
|
|
.replaceAll(/[^a-zA-Z0-9]+/g, '-')
|
|
.replaceAll(/-+/g, '-')
|
|
.toLowerCase()
|
|
.replaceAll(/^-|-$/g, '');
|
|
}
|
|
|
|
function extractCommands(md) {
|
|
// Section: "# Commands" — list lines like: - **\*help** - Description
|
|
const body = extractSection(md, /^#\s+Commands/i);
|
|
const commands = [];
|
|
for (const raw of body) {
|
|
const line = raw.trim();
|
|
const m = line.match(/^[-*]\s+\*\*(.+?)\*\*\s*-\s*(.+)$/); // **bold** - desc
|
|
if (m) {
|
|
const bold = m[1].trim(); // e.g. *help or *create-doc {template}
|
|
const desc = m[2].trim();
|
|
const trigger = kebabize(bold.split(/\s|\{/)[0]); // take first token before space or {
|
|
if (trigger) {
|
|
commands.push({ trigger, desc, raw: bold });
|
|
}
|
|
}
|
|
}
|
|
return commands;
|
|
}
|
|
|
|
function buildAgentYaml({ moduleSlug, id, name, title, icon, role, identity, commStyle, principles, commands }) {
|
|
// Map commands to BMAD agent.menu with 'exec' target as a placeholder executor
|
|
const menu = commands.map((c) => ({
|
|
trigger: c.trigger,
|
|
description: c.desc,
|
|
exec: `subagentic-command:${c.raw}`,
|
|
}));
|
|
|
|
// Ensure at least one menu entry
|
|
if (menu.length === 0) {
|
|
menu.push({ trigger: 'help', description: 'Show available commands', exec: 'subagentic-command:*help' });
|
|
}
|
|
|
|
const agentDoc = {
|
|
agent: {
|
|
metadata: {
|
|
id,
|
|
name,
|
|
title,
|
|
icon,
|
|
module: moduleSlug,
|
|
},
|
|
persona: {
|
|
role,
|
|
identity,
|
|
communication_style: commStyle,
|
|
principles:
|
|
principles.length > 0
|
|
? principles
|
|
: ['Runtime resource loading', 'Direct execution', 'Command processing with * prefix', 'Numbered lists for choices'],
|
|
},
|
|
menu,
|
|
},
|
|
};
|
|
|
|
return yaml.dump(agentDoc, { noRefs: true, lineWidth: 120 });
|
|
}
|
|
|
|
function main() {
|
|
const [, , inPath, outPath] = process.argv;
|
|
if (!inPath || !outPath) {
|
|
console.error('Usage: node tools/cli/spikes/convert-subagent-md.js <input.md> <output.agent.yaml>');
|
|
process.exit(2);
|
|
}
|
|
const md = readFile(inPath);
|
|
const fm = parseFrontmatter(md);
|
|
const principles = extractPrinciples(md);
|
|
const commands = extractCommands(md);
|
|
|
|
const baseName = (fm.name || path.parse(inPath).name).trim();
|
|
const moduleSlug = 'subagentic';
|
|
const id = `subagentic/${baseName}`;
|
|
const name = baseName;
|
|
const title = (fm.description || baseName).trim();
|
|
const icon = fm.color ? String(fm.color) : '🧰';
|
|
const role = `${baseName} agent`;
|
|
const identity = fm.description || `${baseName} converted from subagent markdown`;
|
|
const commStyle = 'precise and concise';
|
|
|
|
const outYaml = buildAgentYaml({ moduleSlug, id, name, title, icon, role, identity, commStyle, principles, commands });
|
|
writeFile(outPath, outYaml);
|
|
console.log(`Wrote ${outPath} with ${commands.length} menu item(s).`);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|