Files
br-os/scripts/spawn-agent.js
2025-11-25 13:54:05 -06:00

378 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* spawn-agent CLI Tool
*
* Creates a new agent with full spec, prompt, workflow, and docs.
*
* Usage: node scripts/spawn-agent.js <agent-id>
* Example: pnpm spawn-agent scribe-support
*/
const fs = require('fs');
const path = require('path');
// Configuration
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const WORKFLOWS_DIR = path.join(__dirname, '..', '.github', 'workflows');
const DOCS_DIR = path.join(__dirname, '..', 'docs', 'agents');
/**
* Convert agent ID to human-readable name
* e.g., "scribe-support" -> "Scribe Support"
*/
function idToName(id) {
return id
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Generate role based on agent naming conventions
*/
function inferRole(id) {
const parts = id.toLowerCase().split('-');
// Common role patterns
const roleMap = {
'scribe': 'Documentation and note-taking specialist',
'support': 'User assistance and support handler',
'review': 'Code review and quality assurance agent',
'reviewer': 'Code review and quality assurance agent',
'deploy': 'Deployment and release automation agent',
'monitor': 'System monitoring and alerting agent',
'test': 'Testing and validation agent',
'security': 'Security scanning and vulnerability assessment agent',
'data': 'Data processing and analysis agent',
'notify': 'Notification and communication agent',
'sync': 'Synchronization and integration agent',
'build': 'Build and compilation agent',
'clean': 'Cleanup and maintenance agent'
};
for (const part of parts) {
if (roleMap[part]) {
return roleMap[part];
}
}
return `Specialized agent for ${idToName(id)} tasks`;
}
/**
* Generate traits based on agent ID
*/
function inferTraits(id) {
const baseTraits = ['autonomous', 'reliable'];
const parts = id.toLowerCase().split('-');
const traitMap = {
'scribe': ['detailed', 'organized'],
'support': ['helpful', 'responsive'],
'review': ['thorough', 'analytical'],
'reviewer': ['thorough', 'analytical'],
'deploy': ['cautious', 'systematic'],
'monitor': ['vigilant', 'proactive'],
'test': ['meticulous', 'comprehensive'],
'security': ['vigilant', 'strict'],
'data': ['analytical', 'efficient'],
'notify': ['timely', 'clear'],
'sync': ['coordinated', 'accurate'],
'build': ['efficient', 'robust'],
'clean': ['systematic', 'thorough']
};
for (const part of parts) {
if (traitMap[part]) {
return [...baseTraits, ...traitMap[part]];
}
}
return baseTraits;
}
/**
* Generate tags based on agent ID
*/
function inferTags(id) {
const baseTags = ['agent', 'blackroad-os'];
const parts = id.toLowerCase().split('-');
return [...baseTags, ...parts];
}
/**
* Read template file
*/
function readTemplate(templateName) {
const templatePath = path.join(TEMPLATES_DIR, templateName);
return fs.readFileSync(templatePath, 'utf8');
}
/**
* Replace all placeholders in template
*/
function processTemplate(template, replacements) {
let result = template;
for (const [key, value] of Object.entries(replacements)) {
const regex = new RegExp(`{{${key}}}`, 'g');
result = result.replace(regex, value);
}
return result;
}
/**
* Ensure directory exists
*/
function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
/**
* Main spawn agent function
*/
function spawnAgent(agentId, options = {}) {
const {
skipDocs = false,
verbose = false
} = options;
// Validate agent ID
if (!agentId || typeof agentId !== 'string') {
throw new Error('Agent ID is required');
}
// Normalize agent ID
const normalizedId = agentId.toLowerCase().replace(/[^a-z0-9-]/g, '-');
// Generate agent metadata
const agentName = idToName(normalizedId);
const agentRole = inferRole(normalizedId);
const agentTraits = inferTraits(normalizedId);
const agentTags = inferTags(normalizedId);
const createdAt = new Date().toISOString();
const description = `${agentName} agent for the BlackRoad-OS ecosystem`;
// Prepare replacements
const replacements = {
'AGENT_ID': normalizedId,
'AGENT_NAME': agentName,
'AGENT_ROLE': agentRole,
'AGENT_DESCRIPTION': description,
'AGENT_TRAITS': JSON.stringify(agentTraits),
'AGENT_TRAITS_LIST': agentTraits.map(t => `- ${t}`).join('\n'),
'AGENT_TRAITS_MDX': agentTraits.map(t => `- **${t}**`).join('\n'),
'AGENT_TAGS': JSON.stringify(agentTags),
'CREATED_AT': createdAt
};
// Ensure output directories exist
ensureDir(AGENTS_DIR);
ensureDir(WORKFLOWS_DIR);
if (!skipDocs) {
ensureDir(DOCS_DIR);
}
// Check if agent already exists
const agentJsonPath = path.join(AGENTS_DIR, `${normalizedId}.agent.json`);
if (fs.existsSync(agentJsonPath)) {
throw new Error(`Agent '${normalizedId}' already exists at ${agentJsonPath}`);
}
// Process and write templates
const outputs = [];
// 1. Agent JSON spec
const agentJson = processTemplate(readTemplate('base-agent.template.json'), replacements);
fs.writeFileSync(agentJsonPath, agentJson);
outputs.push(`agents/${normalizedId}.agent.json`);
// 2. Agent prompt
const promptPath = path.join(AGENTS_DIR, `${normalizedId}.prompt.txt`);
const agentPrompt = processTemplate(readTemplate('base-agent.prompt.template.txt'), replacements);
fs.writeFileSync(promptPath, agentPrompt);
outputs.push(`agents/${normalizedId}.prompt.txt`);
// 3. Workflow YAML
const workflowPath = path.join(WORKFLOWS_DIR, `${normalizedId}.workflow.yml`);
const agentWorkflow = processTemplate(readTemplate('base-agent.workflow.template.yml'), replacements);
fs.writeFileSync(workflowPath, agentWorkflow);
outputs.push(`.github/workflows/${normalizedId}.workflow.yml`);
// 4. MDX docs (optional)
if (!skipDocs) {
const mdxPath = path.join(DOCS_DIR, `${normalizedId}.mdx`);
const agentMdx = processTemplate(readTemplate('base-agent.mdx.template'), replacements);
fs.writeFileSync(mdxPath, agentMdx);
outputs.push(`docs/agents/${normalizedId}.mdx`);
}
return {
agentId: normalizedId,
agentName,
outputs,
metadata: {
role: agentRole,
traits: agentTraits,
tags: agentTags,
createdAt
}
};
}
/**
* CLI entry point
*/
function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
console.log(`
🛠️ spawn-agent BlackRoad-OS Agent Generator
Usage:
node scripts/spawn-agent.js <agent-id> [options]
pnpm spawn-agent <agent-id> [options]
Arguments:
agent-id Unique identifier for the agent (e.g., scribe-support)
Options:
--skip-docs Skip generating MDX documentation
--verbose Show detailed output
--help, -h Show this help message
Examples:
pnpm spawn-agent scribe-support
pnpm spawn-agent code-reviewer --skip-docs
pnpm spawn-agent deploy-bot --verbose
`);
process.exit(0);
}
const agentId = args[0];
const skipDocs = args.includes('--skip-docs');
const verbose = args.includes('--verbose');
try {
console.log(`\n🛠️ Spawning agent: ${agentId}\n`);
const result = spawnAgent(agentId, { skipDocs, verbose });
console.log(`✔ Created agent: ${result.agentName}`);
result.outputs.forEach((output, index) => {
const prefix = index === result.outputs.length - 1 ? '└─' : '├─';
console.log(`${prefix} ${output}`);
});
if (verbose) {
console.log('\n📋 Metadata:');
console.log(` Role: ${result.metadata.role}`);
console.log(` Traits: ${result.metadata.traits.join(', ')}`);
console.log(` Tags: ${result.metadata.tags.join(', ')}`);
console.log(` Created: ${result.metadata.createdAt}`);
}
console.log('\n💚 Agent spawned successfully!\n');
} catch (error) {
console.error(`\n❌ Error: ${error.message}\n`);
process.exit(1);
}
}
// Export for testing
module.exports = {
spawnAgent,
idToName,
inferRole,
inferTraits,
inferTags,
processTemplate
};
// Run CLI if executed directly
if (require.main === module) {
main();
}
const fs = require("fs");
const path = require("path");
const agentName = process.argv[2];
if (!agentName) {
console.error("❌ Please provide an agent name: `npm run spawn-agent <agent-name>`");
process.exit(1);
}
const toTitleCase = (str) => str.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase());
const agentId = agentName.toLowerCase().replace(/\s+/g, "-");
const displayName = toTitleCase(agentId);
const output = {
id: agentId,
name: displayName,
role: "worker",
traits: ["emoji-native"],
inputs: [],
outputs: [],
description: `This is the ${displayName} agent.`,
triggers: [],
inherits_from: "base-agent"
};
// Paths
const jsonPath = `agents/${agentId}.agent.json`;
const promptPath = `agents/${agentId}.prompt.txt`;
const workflowPath = `.github/workflows/${agentId}.workflow.yml`;
const docPath = `docs/agents/${agentId}.mdx`;
// Files
fs.mkdirSync("agents", { recursive: true });
fs.writeFileSync(jsonPath, JSON.stringify(output, null, 2));
fs.writeFileSync(promptPath, `SYSTEM:\nYou are the ${displayName} agent. Your job is to...`);
fs.mkdirSync(".github/workflows", { recursive: true });
fs.writeFileSync(workflowPath, `name: ${displayName} Workflow\non:\n workflow_dispatch:\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - run: echo "${displayName} agent triggered!"`);
fs.mkdirSync("docs/agents", { recursive: true });
fs.writeFileSync(docPath, `# ${displayName} Agent\n\nAuto-generated.\n\n## Purpose\nTBD`);
console.log(`✅ Created agent: ${agentId}`);
console.log(`├─ ${jsonPath}`);
console.log(`├─ ${promptPath}`);
console.log(`├─ ${workflowPath}`);
console.log(`└─ ${docPath}`);
const fs = require("fs");
const path = require("path");
const agentId = process.argv[2];
if (!agentId) {
console.error("❌ No agent ID provided.");
process.exit(1);
}
console.log(`🧬 Scaffolding agent: ${agentId}`);
const agentDir = path.join(__dirname, "..", "agents", agentId);
if (!fs.existsSync(agentDir)) {
fs.mkdirSync(agentDir, { recursive: true });
}
const indexFile = path.join(agentDir, "index.js");
if (!fs.existsSync(indexFile)) {
fs.writeFileSync(
indexFile,
`// Agent: ${agentId}\nconsole.log("🤖 Agent ${agentId} initialized");\n`
);
console.log(`✅ Agent scaffolded at: ${agentDir}`);
} else {
console.log(`⚠️ Agent ${agentId} already exists.`);
}