UNPKG

jay-code

Version:

Streamlined AI CLI orchestration engine with mathematical rigor and enterprise-grade reliability

275 lines (237 loc) 8.44 kB
/** * Dynamic Agent Loader - Reads agent definitions from .claude/agents/ directory * This is the single source of truth for all agent types in the system */ import { readFileSync, existsSync } from 'node:fs'; import { glob } from 'glob'; import { resolve, dirname } from 'node:path'; import { parse as parseYaml } from 'yaml'; // Legacy agent type mapping for backward compatibility const LEGACY_AGENT_MAPPING = { analyst: 'code-analyzer', coordinator: 'task-orchestrator', optimizer: 'perf-analyzer', documenter: 'api-docs', monitor: 'performance-benchmarker', specialist: 'system-architect', architect: 'system-architect', } as const; /** * Resolve legacy agent types to current equivalents */ function resolveLegacyAgentType(legacyType: string): string { return LEGACY_AGENT_MAPPING[legacyType as keyof typeof LEGACY_AGENT_MAPPING] || legacyType; } export interface AgentDefinition { name: string; type?: string; color?: string; description: string; capabilities?: string[]; priority?: 'low' | 'medium' | 'high' | 'critical'; hooks?: { pre?: string; post?: string; }; content?: string; // The markdown content after frontmatter } export interface AgentCategory { name: string; agents: AgentDefinition[]; } class AgentLoader { private agentCache: Map<string, AgentDefinition> = new Map(); private categoriesCache: AgentCategory[] = []; private lastLoadTime = 0; private cacheExpiry = 60000; // 1 minute cache /** * Get the .claude/agents directory path */ private getAgentsDirectory(): string { // Start from current working directory and walk up to find .claude/agents let currentDir = process.cwd(); while (currentDir !== '/') { const claudeAgentsPath = resolve(currentDir, '.claude', 'agents'); if (existsSync(claudeAgentsPath)) { return claudeAgentsPath; } currentDir = dirname(currentDir); } // Fallback to relative path return resolve(process.cwd(), '.claude', 'agents'); } /** * Parse agent definition from markdown file */ private parseAgentFile(filePath: string): AgentDefinition | null { try { const content = readFileSync(filePath, 'utf-8'); // Split frontmatter and content const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!frontmatterMatch) { console.warn(`No frontmatter found in ${filePath}`); return null; } const [, yamlContent, markdownContent] = frontmatterMatch; const frontmatter = parseYaml(yamlContent); if (!frontmatter.name || !frontmatter.metadata?.description) { console.warn(`Missing required fields (name, metadata.description) in ${filePath}`); return null; } return { name: frontmatter.name, type: frontmatter.type, color: frontmatter.color, description: frontmatter.metadata.description, capabilities: frontmatter.metadata.capabilities || frontmatter.capabilities || [], priority: frontmatter.priority || 'medium', hooks: frontmatter.hooks, content: markdownContent.trim(), }; } catch (error) { console.error(`Error parsing agent file ${filePath}:`, error); return null; } } /** * Load all agent definitions from .claude/agents directory */ private async loadAgents(): Promise<void> { const agentsDir = this.getAgentsDirectory(); if (!existsSync(agentsDir)) { console.warn(`Agents directory not found: ${agentsDir}`); return; } // Find all .md files in the agents directory const agentFiles = await glob('**/*.md', { cwd: agentsDir, ignore: ['**/README.md', '**/MIGRATION_SUMMARY.md'], absolute: true, }); // Clear cache this.agentCache.clear(); this.categoriesCache = []; // Track categories const categoryMap = new Map<string, AgentDefinition[]>(); // Parse each agent file for (const filePath of agentFiles) { const agent = this.parseAgentFile(filePath); if (agent) { this.agentCache.set(agent.name, agent); // Determine category from file path const relativePath = filePath.replace(agentsDir, ''); const pathParts = relativePath.split('/'); const category = pathParts[1] || 'uncategorized'; // First directory after agents/ if (!categoryMap.has(category)) { categoryMap.set(category, []); } categoryMap.get(category)!.push(agent); } } // Build categories array this.categoriesCache = Array.from(categoryMap.entries()).map(([name, agents]) => ({ name, agents: agents.sort((a, b) => a.name.localeCompare(b.name)), })); this.lastLoadTime = Date.now(); } /** * Check if cache needs refresh */ private needsRefresh(): boolean { return Date.now() - this.lastLoadTime > this.cacheExpiry; } /** * Ensure agents are loaded and cache is fresh */ private async ensureLoaded(): Promise<void> { if (this.agentCache.size === 0 || this.needsRefresh()) { await this.loadAgents(); } } /** * Get all available agent types */ async getAvailableAgentTypes(): Promise<string[]> { await this.ensureLoaded(); const currentTypes = Array.from(this.agentCache.keys()); const legacyTypes = Object.keys(LEGACY_AGENT_MAPPING); // Return both current types and legacy types, removing duplicates const combined = [...currentTypes, ...legacyTypes]; const uniqueTypes = Array.from(new Set(combined)); return uniqueTypes.sort(); } /** * Get agent definition by name */ async getAgent(name: string): Promise<AgentDefinition | null> { await this.ensureLoaded(); // First try the original name, then try the legacy mapping return this.agentCache.get(name) || this.agentCache.get(resolveLegacyAgentType(name)) || null; } /** * Get all agent definitions */ async getAllAgents(): Promise<AgentDefinition[]> { await this.ensureLoaded(); return Array.from(this.agentCache.values()).sort((a, b) => a.name.localeCompare(b.name)); } /** * Get agents organized by category */ async getAgentCategories(): Promise<AgentCategory[]> { await this.ensureLoaded(); return this.categoriesCache; } /** * Search agents by capabilities, description, or name */ async searchAgents(query: string): Promise<AgentDefinition[]> { await this.ensureLoaded(); const lowerQuery = query.toLowerCase(); return Array.from(this.agentCache.values()).filter(agent => { return ( agent.name.toLowerCase().includes(lowerQuery) || agent.description.toLowerCase().includes(lowerQuery) || agent.capabilities?.some(cap => cap.toLowerCase().includes(lowerQuery)) || false ); }); } /** * Check if an agent type is valid */ async isValidAgentType(name: string): Promise<boolean> { await this.ensureLoaded(); // First try the original name, then try the legacy mapping return this.agentCache.has(name) || this.agentCache.has(resolveLegacyAgentType(name)); } /** * Get agents by category name */ async getAgentsByCategory(category: string): Promise<AgentDefinition[]> { const categories = await this.getAgentCategories(); const found = categories.find(cat => cat.name === category); return found?.agents || []; } /** * Force refresh the agent cache */ async refresh(): Promise<void> { this.lastLoadTime = 0; // Force reload await this.loadAgents(); } } // Singleton instance export const agentLoader = new AgentLoader(); // Convenience functions export const getAvailableAgentTypes = () => agentLoader.getAvailableAgentTypes(); export const getAgent = (name: string) => agentLoader.getAgent(name); export const getAllAgents = () => agentLoader.getAllAgents(); export const getAgentCategories = () => agentLoader.getAgentCategories(); export const searchAgents = (query: string) => agentLoader.searchAgents(query); export const isValidAgentType = (name: string) => agentLoader.isValidAgentType(name); export const getAgentsByCategory = (category: string) => agentLoader.getAgentsByCategory(category); export const refreshAgents = () => agentLoader.refresh(); // Export legacy mapping utilities export { resolveLegacyAgentType, LEGACY_AGENT_MAPPING };