UNPKG

@lsendel/claude-agents

Version:

Supercharge Claude Code with specialized AI sub-agents for code review, testing, debugging, documentation & more. Now with process & standards management! Easy CLI tool to install, manage & create custom AI agents for enhanced development workflow

328 lines (276 loc) 9.72 kB
import chalk from 'chalk'; import ora from 'ora'; import { readdirSync, readFileSync, existsSync, writeFileSync, mkdirSync, } from 'fs'; import { join, basename, dirname } from 'path'; import { fileURLToPath } from 'url'; import { extractFrontmatter } from '../utils/yaml-parser.js'; import { getAgentsDir, ensureDirectories } from '../utils/paths.js'; import { getInstalledAgents, addInstalledAgent } from '../utils/config.js'; import { logger } from '../utils/logger.js'; import { ensureLatestAgents, isDeprecated } from '../utils/agent-updater.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** * Get the project root directory (where package.json is) */ function getProjectRoot() { return join(__dirname, '..', '..'); } /** * Create metadata object for an agent */ function createAgentMetadata(agent) { return { name: agent.name, version: '1.0.0', description: agent.frontmatter.description || `${agent.name} agent`, author: 'External', tags: agent.frontmatter.tags || [], requirements: { tools: agent.frontmatter.tools ? agent.frontmatter.tools.split(',').map((t) => t.trim()) : [], }, compatible_with: ['claude-code@>=1.0.0'], }; } /** * Copy agent files to project directory */ async function copyAgentToProject(agent) { const projectRoot = getProjectRoot(); const projectAgentsDir = join(projectRoot, 'agents', agent.name); try { // Create agent directory mkdirSync(projectAgentsDir, { recursive: true }); // Copy agent.md file const agentTargetPath = join(projectAgentsDir, 'agent.md'); writeFileSync( agentTargetPath, agent.fullContent || readFileSync(agent.path, 'utf-8'), ); // Create metadata.json const metadata = createAgentMetadata(agent); const metadataPath = join(projectAgentsDir, 'metadata.json'); writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); return true; } catch (error) { logger.error( `Failed to copy agent ${agent.name} to project: ${error.message}`, ); return false; } } /** * Sync command to detect and register agents installed through Claude Code * or other external methods */ export async function syncCommand(options) { const spinner = ora(); try { ensureDirectories(); // Ensure only latest agents are present spinner.start('Removing deprecated agents and ensuring latest versions...'); const cleanupSuccess = ensureLatestAgents(); if (cleanupSuccess) { spinner.succeed('Ensured only latest agents are present'); } else { spinner.warn('Some deprecated agents could not be removed'); } // Force copy mode - copy all registered agents to project if (options.forceCopy) { spinner.start('Copying all agents to project directory...'); const registeredAgents = getInstalledAgents(); let copied = 0; let failed = 0; for (const [agentName, agentData] of Object.entries(registeredAgents)) { try { // Read agent file from user directory const userAgentPath = join(getAgentsDir(false), `${agentName}.md`); if (existsSync(userAgentPath)) { const fullContent = readFileSync(userAgentPath, 'utf-8'); const agent = { name: agentName, path: userAgentPath, frontmatter: agentData.frontmatter || { name: agentName, description: agentData.description, tools: agentData.requirements?.tools?.join(', ') || '', }, fullContent, }; const success = await copyAgentToProject(agent); if (success) { copied++; } else { failed++; } } else { logger.debug(`Agent file not found: ${userAgentPath}`); failed++; } } catch (error) { logger.error(`Failed to copy ${agentName}: ${error.message}`); failed++; } } spinner.succeed( `Copied ${copied} agent(s) to project directory${failed > 0 ? ` (${failed} failed)` : ''}`, ); return; } spinner.start('Scanning for unregistered agents...'); // Get paths for both user and project scopes const userAgentsDir = getAgentsDir(false); const projectAgentsDir = getAgentsDir(true); // Get currently registered agents const registeredAgents = getInstalledAgents(); const registeredNames = Object.keys(registeredAgents); // Scan both directories for agent files const unregisteredAgents = []; const scopes = [ { dir: userAgentsDir, scope: 'user' }, { dir: projectAgentsDir, scope: 'project' }, ]; for (const { dir, scope } of scopes) { if (!existsSync(dir)) continue; const files = readdirSync(dir, { withFileTypes: true }) .filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md')) .map((dirent) => dirent.name); for (const file of files) { const agentName = basename(file, '.md'); // Skip if this is a deprecated agent if (isDeprecated(agentName)) { logger.debug(`Skipping ${agentName} - deprecated agent`); continue; } // Skip if already registered if (registeredNames.includes(agentName)) { logger.debug(`Skipping ${agentName} - already registered`); continue; } try { const agentPath = join(dir, file); const fullContent = readFileSync(agentPath, 'utf-8'); // Use custom parser that supports Claude Code format const { frontmatter, content } = extractFrontmatter(fullContent); if (!frontmatter) { logger.debug(`No frontmatter found in ${agentName}`); continue; } // Extract agent details const agentInfo = { name: agentName, path: agentPath, scope, frontmatter, content: content, fullContent: fullContent, // Keep the full content including frontmatter }; unregisteredAgents.push(agentInfo); } catch (error) { logger.debug(`Error parsing agent ${agentName}: ${error.message}`); } } } spinner.stop(); if (unregisteredAgents.length === 0) { console.log(chalk.green('✓ All agents are properly registered')); return; } // Display found agents console.log( chalk.yellow( `\nFound ${unregisteredAgents.length} unregistered agent(s):\n`, ), ); for (const agent of unregisteredAgents) { console.log(chalk.bold(` • ${agent.name}`)); const desc = agent.frontmatter.description || 'No description available'; const shortDesc = desc.split('\\n')[0].substring(0, 60); console.log( chalk.gray(` ${shortDesc}${desc.length > 60 ? '...' : ''}`), ); } console.log(''); // In auto mode, register all. Otherwise, let user select let agentsToRegister = unregisteredAgents; if (!options.auto) { const { default: inquirer } = await import('inquirer'); // Let user select which agents to register const { selectedAgents } = await inquirer.prompt([ { type: 'checkbox', name: 'selectedAgents', message: 'Select agents to register:', choices: unregisteredAgents.map((agent) => ({ name: `${agent.name} - ${agent.frontmatter.description || 'No description'}`.substring( 0, 80, ), value: agent.name, checked: true, })), }, ]); if (selectedAgents.length === 0) { console.log(chalk.yellow('No agents selected')); return; } // Filter to only selected agents agentsToRegister = unregisteredAgents.filter((a) => selectedAgents.includes(a.name), ); } // Register each selected agent spinner.start('Registering agents and copying to project...'); let registered = 0; let copied = 0; for (const agent of agentsToRegister) { try { // Ensure we have the full content if (!agent.fullContent) { agent.fullContent = readFileSync(agent.path, 'utf-8'); } // Copy agent files to project directory const copySuccess = await copyAgentToProject(agent); if (copySuccess) { copied++; logger.debug(`Copied agent ${agent.name} to project directory`); } // Create metadata structure const metadata = createAgentMetadata(agent); // Add to configuration const agentData = { ...metadata, frontmatter: agent.frontmatter, content: agent.content, fullContent: agent.fullContent, }; addInstalledAgent(agent.name, agentData, agent.scope === 'project'); registered++; logger.debug(`Registered agent: ${agent.name}`); } catch (error) { logger.error(`Failed to register ${agent.name}: ${error.message}`); } } spinner.succeed( `Registered ${registered} agent(s), copied ${copied} to project`, ); // Commands feature removed - agents use description-based delegation console.log(''); console.log(chalk.green('✓ Sync complete!')); console.log(chalk.gray('Use "claude-agents list" to see all agents')); } catch (error) { spinner.fail('Sync failed'); logger.error(error.message); logger.debug(error.stack); throw error; } }