claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
236 lines (235 loc) • 8.66 kB
JavaScript
/**
* Agent Definition Parser
*
* Parses agent definition files (.md) with YAML frontmatter and markdown content.
* Supports agent definitions in .claude/agents/ directory structure.
*/ import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { glob } from 'glob';
import { execSync } from 'child_process';
// ES Module compatibility
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Detect project root dynamically
* Uses PROJECT_ROOT env var if set, otherwise tries git, falls back to cwd
*/ function getProjectRoot() {
// 1. Check environment variable
if (process.env.PROJECT_ROOT) {
return process.env.PROJECT_ROOT;
}
// 2. Try git rev-parse (most reliable)
try {
const gitRoot = execSync('git rev-parse --show-toplevel', {
encoding: 'utf-8',
cwd: process.cwd(),
stdio: [
'pipe',
'pipe',
'ignore'
]
}).trim();
if (gitRoot) {
return gitRoot;
}
} catch {
// Fall through to next method
}
// 3. Fall back to current working directory
return process.cwd();
}
const projectRoot = getProjectRoot();
/**
* Parse YAML frontmatter from markdown content
* Supports both Unix (LF) and Windows (CRLF) line endings
*/ function parseFrontmatter(content) {
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (!match) {
return {
frontmatter: {},
body: content
};
}
const [, yamlContent, body] = match;
// Simple YAML parser (handles basic key-value pairs, arrays, and objects)
const frontmatter = {};
const lines = yamlContent.split('\n');
let currentKey = '';
let currentArray = [];
let isInArray = false;
let isInObject = false;
let currentObject = {};
let objectKey = '';
for (const line of lines){
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
// Array item
if (trimmed.startsWith('- ')) {
if (!isInArray) {
isInArray = true;
currentArray = [];
}
currentArray.push(trimmed.substring(2).trim());
continue;
}
// End of array
if (isInArray && !trimmed.startsWith('- ')) {
frontmatter[currentKey] = currentArray;
isInArray = false;
currentArray = [];
}
// Object field (indented key-value)
if (trimmed.match(/^\s+\w+:/) && isInObject) {
const [objKey, ...objValueParts] = trimmed.split(':');
const objValue = objValueParts.join(':').trim().replace(/^["']|["']$/g, '');
currentObject[objKey.trim()] = objValue;
continue;
}
// Key-value pair
const colonIndex = trimmed.indexOf(':');
if (colonIndex !== -1) {
const key = trimmed.substring(0, colonIndex).trim();
const value = trimmed.substring(colonIndex + 1).trim();
// Check if this starts an object
if (value === '') {
isInObject = true;
currentObject = {};
objectKey = key;
continue;
}
// End previous object if any
if (isInObject && !trimmed.match(/^\s+/)) {
frontmatter[objectKey] = currentObject;
isInObject = false;
currentObject = {};
}
currentKey = key;
// Multi-line string (starts with |)
if (value === '|') {
continue; // Will be handled by next lines
}
// Inline array (e.g., [item1, item2, item3])
if (value.startsWith('[') && value.endsWith(']')) {
const arrayContent = value.substring(1, value.length - 1);
const items = arrayContent.split(',').map((item)=>item.trim());
frontmatter[key] = items;
continue;
}
// Remove quotes
const cleanValue = value.replace(/^["']|["']$/g, '');
frontmatter[key] = cleanValue;
} else if (currentKey && trimmed && !isInArray && !isInObject) {
// Continuation of multi-line string
const existingValue = frontmatter[currentKey];
frontmatter[currentKey] = existingValue ? `${existingValue}\n${trimmed}` : trimmed;
}
}
// Handle trailing array or object
if (isInArray) {
frontmatter[currentKey] = currentArray;
}
if (isInObject) {
frontmatter[objectKey] = currentObject;
}
return {
frontmatter,
body: body.trim()
};
}
/**
* Find agent definition file by agent type/name
* Searches multiple locations for agent definitions
*/ async function findAgentFile(agentType, baseDir) {
// Normalize agent type (handle both kebab-case and underscores)
const normalizedType = agentType.toLowerCase().replace(/_/g, '-');
// Search locations (in order of priority)
// Use provided baseDir, or construct default path from project root
const defaultBaseDir = path.join(projectRoot, '.claude/agents');
const searchLocations = [
baseDir || defaultBaseDir,
defaultBaseDir,
path.join(__dirname, '../../.claude/agents'),
'/app/.claude/agents'
];
// Search patterns (in order of priority)
const searchPatterns = [
// Exact match in any subdirectory
(loc)=>`${loc}/**/${normalizedType}.md`,
// Match with different casing
(loc)=>`${loc}/**/*${normalizedType}*.md`
];
for (const location of searchLocations){
for (const patternFn of searchPatterns){
const pattern = patternFn(location);
try {
const files = await glob(pattern, {
nodir: true,
absolute: true
});
if (files.length > 0) {
// Prefer exact match over partial match
const exactMatch = files.find((f)=>{
const basename = path.basename(f, '.md').toLowerCase();
return basename === normalizedType;
});
return exactMatch || files[0];
}
} catch (error) {
continue;
}
}
}
return null;
}
/**
* Parse agent definition from file
*/ export async function parseAgentDefinition(agentType) {
// Find agent file
const filePath = await findAgentFile(agentType);
if (!filePath) {
throw new Error(`Agent definition not found: ${agentType}`);
}
// Read file content
const content = await fs.readFile(filePath, 'utf-8');
// Parse frontmatter and body
const { frontmatter, body } = parseFrontmatter(content);
// Extract category from path
const agentsPath = path.join(projectRoot, '.claude/agents');
const relativePath = path.relative(agentsPath, filePath);
const category = relativePath.includes('/') ? relativePath.split('/')[0] : undefined;
// Build agent definition
const definition = {
name: frontmatter.name || agentType,
description: frontmatter.description || '',
tools: Array.isArray(frontmatter.tools) ? frontmatter.tools : [],
model: frontmatter.model || 'haiku',
type: frontmatter.type,
color: frontmatter.color,
acl_level: frontmatter.acl_level ? parseInt(String(frontmatter.acl_level), 10) : undefined,
capabilities: frontmatter.capabilities,
validation_hooks: frontmatter.validation_hooks,
lifecycle: frontmatter.lifecycle,
content: body,
filePath,
category
};
return definition;
}
/**
* List all available agent definitions
*/ export async function listAgentDefinitions(baseDir = '.claude/agents') {
const pattern = `${baseDir}/**/*.md`;
const files = await glob(pattern, {
nodir: true
});
return files.map((f)=>path.basename(f, '.md'));
}
/**
* Check if agent definition includes CFN Loop protocol
*/ export function hasCFNLoopProtocol(definition) {
const content = definition.content.toLowerCase();
return content.includes('cfn loop') && content.includes('redis completion protocol') || content.includes('invoke-waiting-mode.sh');
}
//# sourceMappingURL=agent-definition-parser.js.map