UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

236 lines (235 loc) 8.66 kB
/** * 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