UNPKG

orchestrix

Version:

Orchestrix - Universal AI Agent Framework for Coordinated AI-Driven Development

1,197 lines (1,005 loc) 220 kB
const path = require("path"); const fs = require("fs-extra"); const yaml = require("js-yaml"); const glob = require("glob"); const fileManager = require("./file-manager"); const configLoader = require("./config-loader"); const { extractYamlFromAgent, extractAgentDependencies, loadAgentYaml, findAgentPath, getAgentMetadata } = require("../../lib/yaml-utils"); // Dynamic import for ES module let chalk; let inquirer; // Initialize ES modules async function initializeModules() { if (!chalk) { chalk = (await import("chalk")).default; } if (!inquirer) { inquirer = (await import("inquirer")).default; } } class IdeSetup { constructor() { this.ideAgentConfig = null; } async loadIdeAgentConfig() { if (this.ideAgentConfig) return this.ideAgentConfig; try { const configPath = path.join(__dirname, '..', 'config', 'ide-agent-config.yaml'); const configContent = await fs.readFile(configPath, 'utf8'); this.ideAgentConfig = yaml.load(configContent); return this.ideAgentConfig; } catch (error) { console.warn('Failed to load IDE agent configuration, using defaults'); return { 'roo-permissions': {}, 'cline-order': {} }; } } async setup(ide, installDir, selectedAgent = null, spinner = null, preConfiguredSettings = null) { await initializeModules(); const ideConfig = await configLoader.getIdeConfiguration(ide); if (!ideConfig) { console.log(chalk.yellow(`\nNo configuration available for ${ide}`)); return false; } switch (ide) { case "cursor": return this.setupCursor(installDir, selectedAgent); case "claude-code": return this.setupClaudeCode(installDir, selectedAgent); case "windsurf": return this.setupWindsurf(installDir, selectedAgent); case "trae": return this.setupTrae(installDir, selectedAgent); case "roo": return this.setupRoo(installDir, selectedAgent); case "cline": return this.setupCline(installDir, selectedAgent); case "gemini": return this.setupGeminiCli(installDir, selectedAgent); case "github-copilot": return this.setupGitHubCopilot(installDir, selectedAgent, spinner, preConfiguredSettings); default: console.log(chalk.yellow(`\nIDE ${ide} not yet supported`)); return false; } } async setupCursor(installDir, selectedAgent) { const cursorRulesDir = path.join(installDir, ".cursor", "rules"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(cursorRulesDir); for (const agentId of agents) { // Find the agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); const mdcPath = path.join(cursorRulesDir, `${agentId}.mdc`); // Get agent metadata for description const metadata = this.extractAgentMetadata(agentContent); const agentTitle = await this.getAgentTitle(agentId, installDir); const description = metadata.agent?.whenToUse || `Activate ${agentTitle} agent for specialized tasks`; // Create MDC content with proper format and meaningful description let mdcContent = "---\n"; mdcContent += `description: "${description}"\n`; mdcContent += "globs: []\n"; mdcContent += "alwaysApply: false\n"; mdcContent += "---\n\n"; mdcContent += `# ${agentId.toUpperCase()} Agent Rule\n\n`; mdcContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle( agentId, installDir )} agent persona.\n\n`; mdcContent += "## Agent Activation\n\n"; mdcContent += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; mdcContent += "```yaml\n"; // Extract the complete YAML content from the agent file const yamlContent = extractYamlFromAgent(agentContent); if (yamlContent) { // Clean the YAML content to remove redundant sections let cleanedYaml = this.cleanYamlContent(yamlContent); // Replace {root} variables for Cursor rules // {root} should resolve to .orchestrix-core let processedYamlContent = cleanedYaml.replace(/\{root\}/g, '.orchestrix-core'); mdcContent += processedYamlContent; } else { // If YAML extraction completely fails, provide meaningful error message mdcContent += `# YAML configuration not found for ${agentId}\n`; mdcContent += `# Please check the agent file format in:\n`; mdcContent += `# ${path.relative(installDir, agentPath)}\n`; mdcContent += `# Expected format: \`\`\`yaml ... \`\`\``; } mdcContent += "\n```\n\n"; mdcContent += "## File Reference\n\n"; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); mdcContent += `The complete agent definition is available in [${relativePath}](mdc:${relativePath}).\n\n`; mdcContent += "## Usage\n\n"; mdcContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle( agentId, installDir )} persona and follow all instructions defined in the YAML configuration above.\n`; await fileManager.writeFile(mdcPath, mdcContent); } } console.log(chalk.green(`\n✓ 已为 Cursor 创建 ${agents.length} 个代理规则`)); return true; } // 修改 setupClaudeCode 方法,添加测试选项 async setupClaudeCode(installDir, selectedAgent, runTests = false) { console.log(chalk.blue("\n🔧 设置 Claude Code 双模式集成...")); const subagentsCount = await this.setupClaudeCodeSubagents(installDir, selectedAgent); console.log(chalk.green(`✔ 已创建 ${subagentsCount} 个优化的 Claude Code 子代理`)); // 运行测试(如果启用) if (runTests) { await this.testSubagentGeneration(installDir); } // 继续设置传统的Commands模式 const coreSlashPrefix = await this.getCoreSlashPrefix(installDir); const coreAgents = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir); const coreTasks = await this.getCoreTaskIds(installDir); await this.setupClaudeCodeForPackage(installDir, "core", coreSlashPrefix, coreAgents, coreTasks, ".orchestrix-core"); // Setup expansion pack commands const expansionPacks = await this.getInstalledExpansionPacks(installDir); for (const packInfo of expansionPacks) { const packSlashPrefix = await this.getExpansionPackSlashPrefix(packInfo.path); const packAgents = await this.getExpansionPackAgents(packInfo.path); const packTasks = await this.getExpansionPackTasks(packInfo.path); if (packAgents.length > 0 || packTasks.length > 0) { const rootPath = path.relative(installDir, packInfo.path); await this.setupClaudeCodeForPackage(installDir, packInfo.name, packSlashPrefix, packAgents, packTasks, rootPath); } } // Summary console.log(chalk.green(`\n✅ Claude Code 双模式集成完成:`)); console.log(chalk.dim(` • Sub Agents: .claude/agents/ (${subagentsCount} 个优化代理)`)); console.log(chalk.dim(` • Commands: .claude/commands/ (${coreAgents.length} 个命令 + ${coreTasks.length} 个任务)`)); console.log(chalk.dim(` • 使用方式: 在 Claude Code 中直接选择 Sub Agent 或使用 /命令`)); return true; } // 统一的路径解析器类 createPathResolver(installDir, packageName, rootPath) { return { resolveAgentPath: (agentId) => { if (packageName !== "core") { // Check for both YAML and MD files in expansion pack const expansionYamlPath = path.join(installDir, rootPath, "agents", `${agentId}.yaml`); const expansionMdPath = path.join(installDir, rootPath, "agents", `${agentId}.md`); if (require('fs').existsSync(expansionYamlPath)) { return expansionYamlPath; } if (require('fs').existsSync(expansionMdPath)) { return expansionMdPath; } } // Check for both YAML and MD files in core const coreYamlPath = path.join(installDir, ".orchestrix-core", "agents", `${agentId}.yaml`); const coreMdPath = path.join(installDir, ".orchestrix-core", "agents", `${agentId}.md`); if (require('fs').existsSync(coreYamlPath)) { return coreYamlPath; } return coreMdPath; }, resolveTaskPath: (taskId) => { if (packageName !== "core") { const expansionTaskPath = path.join(installDir, rootPath, "tasks", `${taskId}.md`); if (require('fs').existsSync(expansionTaskPath)) { return expansionTaskPath; } } return path.join(installDir, ".orchestrix-core", "tasks", `${taskId}.md`); }, resolveDependencyPath: (dependencyPath) => { // 智能依赖路径解析 if (dependencyPath.startsWith('.orchestrix-core/')) { return dependencyPath; // 保持核心路径不变 } return path.join(rootPath, dependencyPath); } }; } // Smart path replacement function for handling root placeholder smartPathReplacement(content, packageName, rootPath) { if (!content) return content; let result = content; // Handle complex nested placeholders like "{root} resolves to {root}/" // First pass: replace descriptive text patterns result = result.replace(/{root}\s+resolves\s+to\s+{root}\//g, 'files are resolved relative to .orchestrix-core/'); result = result.replace(/where\s+{root}\s+resolves\s+to\s+\.orchestrix-core\//g, 'where files are resolved relative to project root'); if (packageName === "core") { // For core package, simple replacement return result.replace(/{root}/g, rootPath); } else { // For expansion packs, preserve core dependencies while replacing package-specific paths return result // Keep core system dependencies unchanged .replace(/{root}\/(?=(?:tasks|templates|checklists|data|utils)\/)/g, '.orchestrix-core/') // Replace package-specific root references .replace(/{root}/g, rootPath); } } async setupClaudeCodeForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) { const commandsBaseDir = path.join(installDir, ".claude", "commands", slashPrefix); const agentsDir = path.join(commandsBaseDir, "agents"); const tasksDir = path.join(commandsBaseDir, "tasks"); // Ensure directories exist await fileManager.ensureDirectory(agentsDir); await fileManager.ensureDirectory(tasksDir); // Setup agents for (const agentId of agentIds) { // Find the agent file - for expansion packs, prefer the expansion pack version let agentPath; if (packageName !== "core") { // For expansion packs, first try to find the agent in the expansion pack directory const expansionYamlPath = path.join(installDir, rootPath, "agents", `${agentId}.yaml`); const expansionMdPath = path.join(installDir, rootPath, "agents", `${agentId}.md`); if (await fileManager.pathExists(expansionYamlPath)) { agentPath = expansionYamlPath; } else if (await fileManager.pathExists(expansionMdPath)) { agentPath = expansionMdPath; } else { // Fall back to core if not found in expansion pack agentPath = await this.findAgentPath(agentId, installDir); } } else { // For core, use the normal search agentPath = await this.findAgentPath(agentId, installDir); } const commandPath = path.join(agentsDir, `${agentId}.md`); if (agentPath) { // Create command file with agent content let agentContent = await fileManager.readFile(agentPath); // Replace {root} placeholder with smart path logic agentContent = this.smartPathReplacement(agentContent, packageName, rootPath); // Add command header let commandContent = `# /${agentId} Command\n\n`; commandContent += `When this command is used, adopt the following agent persona:\n\n`; commandContent += agentContent; await fileManager.writeFile(commandPath, commandContent); // Removed individual command creation messages for cleaner output } } // Setup tasks for (const taskId of taskIds) { // Find the task file - for expansion packs, prefer the expansion pack version let taskPath; if (packageName !== "core") { // For expansion packs, first try to find the task in the expansion pack directory const expansionPackPath = path.join(installDir, rootPath, "tasks", `${taskId}.md`); if (await fileManager.pathExists(expansionPackPath)) { taskPath = expansionPackPath; } else { // Fall back to core if not found in expansion pack taskPath = await this.findTaskPath(taskId, installDir); } } else { // For core, use the normal search taskPath = await this.findTaskPath(taskId, installDir); } const commandPath = path.join(tasksDir, `${taskId}.md`); if (taskPath) { // Create command file with task content let taskContent = await fileManager.readFile(taskPath); // Replace {root} placeholder with smart path logic taskContent = this.smartPathReplacement(taskContent, packageName, rootPath); // Add command header let commandContent = `# /${taskId} Task\n\n`; commandContent += `When this command is used, execute the following task:\n\n`; commandContent += taskContent; await fileManager.writeFile(commandPath, commandContent); // Removed individual task creation messages for cleaner output } } const displayName = packageName === "core" ? "Orchestrix 核心系统" : packageName; console.log(chalk.green(`\n✓ 已为 ${displayName} 创建 Claude Code 命令 (代理: ${agentIds.length}个, 任务: ${taskIds.length}个)`)); } async setupWindsurf(installDir, selectedAgent) { const windsurfRulesDir = path.join(installDir, ".windsurf", "rules"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(windsurfRulesDir); for (const agentId of agents) { // Find the agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); const mdPath = path.join(windsurfRulesDir, `${agentId}.md`); // Create MD content (similar to Cursor but without frontmatter) let mdContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`; mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle( agentId, installDir )} agent persona.\n\n`; mdContent += "## Agent Activation\n\n"; mdContent += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; mdContent += "```yaml\n"; // Extract and clean the YAML content from the agent file const processedYamlContent = this.getCleanedYamlForIDE(agentContent, '.orchestrix-core'); if (processedYamlContent) { mdContent += processedYamlContent; } else { // If no YAML found, include the whole content minus the header mdContent += agentContent.replace(/^#.*$/m, "").trim(); } mdContent += "\n```\n\n"; mdContent += "## File Reference\n\n"; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`; mdContent += "## Usage\n\n"; mdContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle( agentId, installDir )} persona and follow all instructions defined in the YAML configuration above.\n`; await fileManager.writeFile(mdPath, mdContent); console.log(chalk.green(`✓ Created rule: ${agentId}.md`)); } } console.log(chalk.green(`\n✓ Created Windsurf rules in ${windsurfRulesDir}`)); return true; } async setupTrae(installDir, selectedAgent) { const traeRulesDir = path.join(installDir, ".trae", "rules"); const agents = selectedAgent? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(traeRulesDir); for (const agentId of agents) { // Find the agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); const mdPath = path.join(traeRulesDir, `${agentId}.md`); // Create MD content (similar to Cursor but without frontmatter) let mdContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`; mdContent += `This rule is triggered when the user types \`@${agentId}\` and activates the ${await this.getAgentTitle( agentId, installDir )} agent persona.\n\n`; mdContent += "## Agent Activation\n\n"; mdContent += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; mdContent += "```yaml\n"; // Extract and clean the YAML content from the agent file const processedYamlContent = this.getCleanedYamlForIDE(agentContent, '.orchestrix-core'); if (processedYamlContent) { mdContent += processedYamlContent; } else { // If no YAML found, include the whole content minus the header mdContent += agentContent.replace(/^#.*$/m, "").trim(); } mdContent += "\n```\n\n"; mdContent += "## File Reference\n\n"; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); mdContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`; mdContent += "## Usage\n\n"; mdContent += `When the user types \`@${agentId}\`, activate this ${await this.getAgentTitle( agentId, installDir )} persona and follow all instructions defined in the YAML configuration above.\n`; await fileManager.writeFile(mdPath, mdContent); console.log(chalk.green(`✓ Created rule: ${agentId}.md`)); } } } async findAgentPath(agentId, installDir) { // Try to find the agent file in various locations, prioritizing YAML files const possiblePaths = [ // YAML files (preferred) path.join(installDir, ".orchestrix-core", "agents", `${agentId}.yaml`), path.join(installDir, "orchestrix-core", "agents", `${agentId}.yaml`), // Source directory path.join(installDir, "agents", `${agentId}.yaml`), // MD files (backward compatibility) path.join(installDir, ".orchestrix-core", "agents", `${agentId}.md`), path.join(installDir, "orchestrix-core", "agents", `${agentId}.md`), // Source directory path.join(installDir, "agents", `${agentId}.md`) ]; // Also check expansion pack directories // Exclude IDE configuration directories const expansionDirs = glob.sync(".*/agents", { cwd: installDir }).filter(dir => { const dirName = path.basename(path.dirname(dir)); const ideConfigDirs = ['.claude', '.cursor', '.windsurf', '.trae', '.cline', '.clinerules', '.vscode', '.idea', '.roomodes']; return !ideConfigDirs.includes(dirName); }); for (const expDir of expansionDirs) { possiblePaths.push(path.join(installDir, expDir, `${agentId}.yaml`)); possiblePaths.push(path.join(installDir, expDir, `${agentId}.md`)); } for (const agentPath of possiblePaths) { if (await fileManager.pathExists(agentPath)) { return agentPath; } } return null; } async getAllAgentIds(installDir) { const allAgentIds = []; // Check core agents in .orchestrix-core or root let agentsDir = path.join(installDir, ".orchestrix-core", "agents"); if (!(await fileManager.pathExists(agentsDir))) { agentsDir = path.join(installDir, "agents"); } if (await fileManager.pathExists(agentsDir)) { // Support both YAML and MD files, prioritize YAML // Filter out .src.yaml files as they contain $include directives and need to be compiled first // Also filter out README files const yamlFiles = glob.sync("*.yaml", { cwd: agentsDir }).filter(file => !file.endsWith('.src.yaml') && !file.toUpperCase().startsWith('README')); const mdFiles = glob.sync("*.md", { cwd: agentsDir }).filter(file => !file.toUpperCase().startsWith('README')); // Extract IDs from YAML files first const yamlIds = yamlFiles.map((file) => path.basename(file, ".yaml")); // Extract IDs from MD files, but exclude those already found in YAML const mdIds = mdFiles.map((file) => path.basename(file, ".md")).filter(id => !yamlIds.includes(id)); allAgentIds.push(...yamlIds, ...mdIds); } // Also check for expansion pack agents in dot folders // Exclude IDE configuration directories (.claude, .cursor, .windsurf, .trae, etc.) const expansionDirs = glob.sync(".*/agents", { cwd: installDir }).filter(dir => { const dirName = path.basename(path.dirname(dir)); const ideConfigDirs = ['.claude', '.cursor', '.windsurf', '.trae', '.cline', '.clinerules', '.vscode', '.idea', '.roomodes']; return !ideConfigDirs.includes(dirName); }); for (const expDir of expansionDirs) { const fullExpDir = path.join(installDir, expDir); // Support both YAML and MD files for expansion packs too // Filter out .src.yaml files and README files const expYamlFiles = glob.sync("*.yaml", { cwd: fullExpDir }).filter(file => !file.endsWith('.src.yaml') && !file.toUpperCase().startsWith('README')); const expMdFiles = glob.sync("*.md", { cwd: fullExpDir }).filter(file => !file.toUpperCase().startsWith('README')); // Extract IDs from YAML files first const expYamlIds = expYamlFiles.map((file) => path.basename(file, ".yaml")); // Extract IDs from MD files, but exclude those already found in YAML const expMdIds = expMdFiles.map((file) => path.basename(file, ".md")).filter(id => !expYamlIds.includes(id)); allAgentIds.push(...expYamlIds, ...expMdIds); } // Remove duplicates return [...new Set(allAgentIds)]; } async getCoreAgentIds(installDir) { const allAgentIds = []; // Check core agents in .orchestrix-core or root only let agentsDir = path.join(installDir, ".orchestrix-core", "agents"); if (!(await fileManager.pathExists(agentsDir))) { agentsDir = path.join(installDir, "orchestrix-core", "agents"); } if (await fileManager.pathExists(agentsDir)) { // Support both YAML and MD files, prioritize YAML // Filter out .src.yaml files and README files const yamlFiles = glob.sync("*.yaml", { cwd: agentsDir }).filter(file => !file.endsWith('.src.yaml') && !file.toUpperCase().startsWith('README')); const mdFiles = glob.sync("*.md", { cwd: agentsDir }).filter(file => !file.toUpperCase().startsWith('README')); // Extract IDs from YAML files first const yamlIds = yamlFiles.map((file) => path.basename(file, ".yaml")); // Extract IDs from MD files, but exclude those already found in YAML const mdIds = mdFiles.map((file) => path.basename(file, ".md")).filter(id => !yamlIds.includes(id)); allAgentIds.push(...yamlIds, ...mdIds); } return [...new Set(allAgentIds)]; } async getCoreTaskIds(installDir) { const allTaskIds = []; // Check core tasks in .orchestrix-core or root only let tasksDir = path.join(installDir, ".orchestrix-core", "tasks"); if (!(await fileManager.pathExists(tasksDir))) { tasksDir = path.join(installDir, "orchestrix-core", "tasks"); } if (await fileManager.pathExists(tasksDir)) { const taskFiles = glob.sync("*.md", { cwd: tasksDir }); allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md"))); } // Check common tasks const commonTasksDir = path.join(installDir, "common", "tasks"); if (await fileManager.pathExists(commonTasksDir)) { const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir }); allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, ".md"))); } return [...new Set(allTaskIds)]; } async getAgentTitle(agentId, installDir) { // Use the updated findAgentPath method const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { try { if (agentPath.endsWith('.yaml')) { // Direct YAML file loading const agentConfig = await loadAgentYaml(agentPath); if (agentConfig) { const metadata = getAgentMetadata(agentConfig); return metadata.title; } } else { // Legacy MD file support const agentContent = await fileManager.readFile(agentPath); const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); if (yamlMatch) { const yamlContent = yamlMatch[1]; const titleMatch = yamlContent.match(/title:\s*(.+)/); if (titleMatch) { return titleMatch[1].trim(); } } } } catch (error) { console.warn(`Failed to read agent title for ${agentId}: ${error.message}`); } } // Fallback to formatted agent ID return agentId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); } async getAllTaskIds(installDir) { const allTaskIds = []; // Check core tasks in .orchestrix-core or root let tasksDir = path.join(installDir, ".orchestrix-core", "tasks"); if (!(await fileManager.pathExists(tasksDir))) { tasksDir = path.join(installDir, "orchestrix-core", "tasks"); } if (await fileManager.pathExists(tasksDir)) { const taskFiles = glob.sync("*.md", { cwd: tasksDir }); allTaskIds.push(...taskFiles.map((file) => path.basename(file, ".md"))); } // Check common tasks const commonTasksDir = path.join(installDir, "common", "tasks"); if (await fileManager.pathExists(commonTasksDir)) { const commonTaskFiles = glob.sync("*.md", { cwd: commonTasksDir }); allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, ".md"))); } // Also check for expansion pack tasks in dot folders // Exclude IDE configuration directories const expansionDirs = glob.sync(".*/tasks", { cwd: installDir }).filter(dir => { const dirName = path.basename(path.dirname(dir)); const ideConfigDirs = ['.claude', '.cursor', '.windsurf', '.trae', '.cline', '.clinerules', '.vscode', '.idea', '.roomodes']; return !ideConfigDirs.includes(dirName); }); for (const expDir of expansionDirs) { const fullExpDir = path.join(installDir, expDir); const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir }); allTaskIds.push(...expTaskFiles.map((file) => path.basename(file, ".md"))); } // Check expansion-packs folder tasks const expansionPacksDir = path.join(installDir, "expansion-packs"); if (await fileManager.pathExists(expansionPacksDir)) { const expPackDirs = glob.sync("*/tasks", { cwd: expansionPacksDir }); for (const expDir of expPackDirs) { const fullExpDir = path.join(expansionPacksDir, expDir); const expTaskFiles = glob.sync("*.md", { cwd: fullExpDir }); allTaskIds.push(...expTaskFiles.map((file) => path.basename(file, ".md"))); } } // Remove duplicates return [...new Set(allTaskIds)]; } async findTaskPath(taskId, installDir) { // Try to find the task file in various locations const possiblePaths = [ path.join(installDir, ".orchestrix-core", "tasks", `${taskId}.md`), path.join(installDir, "orchestrix-core", "tasks", `${taskId}.md`), path.join(installDir, "common", "tasks", `${taskId}.md`) ]; // Also check expansion pack directories // Check dot folder expansion packs // Exclude IDE configuration directories const expansionDirs = glob.sync(".*/tasks", { cwd: installDir }).filter(dir => { const dirName = path.basename(path.dirname(dir)); const ideConfigDirs = ['.claude', '.cursor', '.windsurf', '.trae', '.cline', '.clinerules', '.vscode', '.idea', '.roomodes']; return !ideConfigDirs.includes(dirName); }); for (const expDir of expansionDirs) { possiblePaths.push(path.join(installDir, expDir, `${taskId}.md`)); } // Check expansion-packs folder const expansionPacksDir = path.join(installDir, "expansion-packs"); if (await fileManager.pathExists(expansionPacksDir)) { const expPackDirs = glob.sync("*/tasks", { cwd: expansionPacksDir }); for (const expDir of expPackDirs) { possiblePaths.push(path.join(expansionPacksDir, expDir, `${taskId}.md`)); } } for (const taskPath of possiblePaths) { if (await fileManager.pathExists(taskPath)) { return taskPath; } } return null; } async getCoreSlashPrefix(installDir) { try { const coreConfigPath = path.join(installDir, ".orchestrix-core", "core-config.yaml"); if (!(await fileManager.pathExists(coreConfigPath))) { // Try orchestrix-core directory const altConfigPath = path.join(installDir, "orchestrix-core", "core-config.yaml"); if (await fileManager.pathExists(altConfigPath)) { const configContent = await fileManager.readFile(altConfigPath); const config = yaml.load(configContent); return config.slashPrefix || "orchestrix"; } return "orchestrix"; // fallback } const configContent = await fileManager.readFile(coreConfigPath); const config = yaml.load(configContent); return config.slashPrefix || "orchestrix"; } catch (error) { console.warn(`Failed to read core slashPrefix, using default 'orchestrix': ${error.message}`); return "orchestrix"; } } async getInstalledExpansionPacks(installDir) { const expansionPacks = []; // Check for dot-prefixed expansion packs in install directory const dotExpansions = glob.sync(".orchestrix-*", { cwd: installDir }); for (const dotExpansion of dotExpansions) { if (dotExpansion !== ".orchestrix-core") { const packPath = path.join(installDir, dotExpansion); const packName = dotExpansion.substring(1); // remove the dot expansionPacks.push({ name: packName, path: packPath }); } } // Check for expansion-packs directory style const expansionPacksDir = path.join(installDir, "expansion-packs"); if (await fileManager.pathExists(expansionPacksDir)) { const packDirs = glob.sync("*", { cwd: expansionPacksDir }); for (const packDir of packDirs) { const packPath = path.join(expansionPacksDir, packDir); if ((await fileManager.pathExists(packPath)) && (await fileManager.pathExists(path.join(packPath, "config.yaml")))) { expansionPacks.push({ name: packDir, path: packPath }); } } } return expansionPacks; } async getExpansionPackSlashPrefix(packPath) { try { const configPath = path.join(packPath, "config.yaml"); if (await fileManager.pathExists(configPath)) { const configContent = await fileManager.readFile(configPath); const config = yaml.load(configContent); return config.slashPrefix || path.basename(packPath); } } catch (error) { console.warn(`Failed to read expansion pack slashPrefix from ${packPath}: ${error.message}`); } return path.basename(packPath); // fallback to directory name } async getExpansionPackAgents(packPath) { const agentsDir = path.join(packPath, "agents"); if (!(await fileManager.pathExists(agentsDir))) { return []; } try { // Support both YAML and MD files, prioritize YAML const yamlFiles = glob.sync("*.yaml", { cwd: agentsDir }); const mdFiles = glob.sync("*.md", { cwd: agentsDir }); // Extract IDs from YAML files first const yamlIds = yamlFiles.map((file) => path.basename(file, ".yaml")); // Extract IDs from MD files, but exclude those already found in YAML const mdIds = mdFiles.map((file) => path.basename(file, ".md")).filter(id => !yamlIds.includes(id)); return [...yamlIds, ...mdIds]; } catch (error) { console.warn(`Failed to read expansion pack agents from ${packPath}: ${error.message}`); return []; } } async getExpansionPackTasks(packPath) { const tasksDir = path.join(packPath, "tasks"); if (!(await fileManager.pathExists(tasksDir))) { return []; } try { const taskFiles = glob.sync("*.md", { cwd: tasksDir }); return taskFiles.map(file => path.basename(file, ".md")); } catch (error) { console.warn(`Failed to read expansion pack tasks from ${packPath}: ${error.message}`); return []; } } async setupRoo(installDir, selectedAgent) { const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); // Check for existing .roomodes file in project root const roomodesPath = path.join(installDir, ".roomodes"); let existingModes = []; let existingContent = ""; if (await fileManager.pathExists(roomodesPath)) { existingContent = await fileManager.readFile(roomodesPath); // Parse existing modes to avoid duplicates const modeMatches = existingContent.matchAll(/- slug: ([\w-]+)/g); for (const match of modeMatches) { existingModes.push(match[1]); } console.log(chalk.yellow(`Found existing .roomodes file with ${existingModes.length} modes`)); } // Create new modes content let newModesContent = ""; // Load dynamic agent permissions from configuration const config = await this.loadIdeAgentConfig(); const agentPermissions = config['roo-permissions'] || {}; for (const agentId of agents) { // Skip if already exists if (existingModes.includes(`orchestrix-${agentId}`)) { console.log(chalk.dim(`Skipping ${agentId} - already exists in .roomodes`)); continue; } // Read agent file to extract all information const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); // Extract YAML content const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); if (yamlMatch) { const yaml = yamlMatch[1]; // Extract agent info from YAML const titleMatch = yaml.match(/title:\s*(.+)/); const iconMatch = yaml.match(/icon:\s*(.+)/); const whenToUseMatch = yaml.match(/whenToUse:\s*(?:"([^"]+)"|'([^']+)'|([^\n\r]+))/); const roleDefinitionMatch = yaml.match(/roleDefinition:\s*"(.+)"/); const title = titleMatch ? titleMatch[1].trim() : await this.getAgentTitle(agentId, installDir); const icon = iconMatch ? iconMatch[1].trim() : "🤖"; let whenToUse = `Use for ${title} tasks`; if (whenToUseMatch) { // Handle quoted strings (with " or ') const whenToUseValue = whenToUseMatch[1] || whenToUseMatch[2]; // Handle unquoted strings (but trim trailing whitespace) if (whenToUseValue) { whenToUse = whenToUseValue.trim(); } else if (whenToUseMatch[3]) { whenToUse = whenToUseMatch[3].trim(); } } const roleDefinition = roleDefinitionMatch ? roleDefinitionMatch[1].trim() : `You are a ${title} specializing in ${title.toLowerCase()} tasks and responsibilities.`; // Build mode entry with proper formatting (matching exact indentation) newModesContent += ` - slug: orchestrix-${agentId}\n`; newModesContent += ` name: '${icon} ${title}'\n`; newModesContent += ` roleDefinition: ${roleDefinition}\n`; newModesContent += ` whenToUse: ${whenToUse}\n`; // Get relative path from installDir to agent file const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); newModesContent += ` customInstructions: CRITICAL Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`; newModesContent += ` groups:\n`; newModesContent += ` - read\n`; // Add permissions based on agent type const permissions = agentPermissions[agentId]; if (permissions) { newModesContent += ` - - edit\n`; newModesContent += ` - fileRegex: ${permissions.fileRegex}\n`; newModesContent += ` description: ${permissions.description}\n`; } else { newModesContent += ` - edit\n`; } console.log(chalk.green(`✓ Added mode: orchestrix-${agentId} (${icon} ${title})`)); } } } // Build final roomodes content let roomodesContent = ""; if (existingContent) { // If there's existing content, append new modes to it roomodesContent = existingContent.trim() + "\n" + newModesContent; } else { // Create new .roomodes file with proper YAML structure roomodesContent = "customModes:\n" + newModesContent; } // Write .roomodes file await fileManager.writeFile(roomodesPath, roomodesContent); console.log(chalk.green("✓ Created .roomodes file in project root")); console.log(chalk.green(`\n✓ Roo Code setup complete!`)); console.log(chalk.dim("Custom modes will be available when you open this project in Roo Code")); return true; } async setupCline(installDir, selectedAgent) { const clineRulesDir = path.join(installDir, ".clinerules"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(clineRulesDir); // Load dynamic agent ordering from configuration const config = await this.loadIdeAgentConfig(); const agentOrder = config['cline-order'] || {}; for (const agentId of agents) { // Find the agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); // Get numeric prefix for ordering const order = agentOrder[agentId] || 99; const prefix = order.toString().padStart(2, '0'); const mdPath = path.join(clineRulesDir, `${prefix}-${agentId}.md`); // Create MD content for Cline (focused on project standards and role) let mdContent = `# ${await this.getAgentTitle(agentId, installDir)} Agent\n\n`; mdContent += `This rule defines the ${await this.getAgentTitle(agentId, installDir)} persona and project standards.\n\n`; mdContent += "## Role Definition\n\n"; mdContent += "When the user types `@" + agentId + "`, adopt this persona and follow these guidelines:\n\n"; mdContent += "```yaml\n"; // Extract and clean the YAML content from the agent file const processedYamlContent = this.getCleanedYamlForIDE(agentContent, '.orchestrix-core'); if (processedYamlContent) { mdContent += processedYamlContent; } else { // If no YAML found, include the whole content minus the header mdContent += agentContent.replace(/^#.*$/m, "").trim(); } mdContent += "\n```\n\n"; mdContent += "## Project Standards\n\n"; mdContent += `- Always maintain consistency with project documentation in .orchestrix-core/\n`; mdContent += `- Follow the agent's specific guidelines and constraints\n`; mdContent += `- Update relevant project files when making changes\n`; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); mdContent += `- Reference the complete agent definition in [${relativePath}](${relativePath})\n\n`; mdContent += "## Usage\n\n"; mdContent += `Type \`@${agentId}\` to activate this ${await this.getAgentTitle(agentId, installDir)} persona.\n`; await fileManager.writeFile(mdPath, mdContent); console.log(chalk.green(`✓ Created rule: ${prefix}-${agentId}.md`)); } } console.log(chalk.green(`\n✓ Created Cline rules in ${clineRulesDir}`)); return true; } async setupGeminiCli(installDir) { await initializeModules(); const geminiDir = path.join(installDir, ".gemini"); const orchestrixMethodDir = path.join(geminiDir, "Orchestrix"); await fileManager.ensureDirectory(orchestrixMethodDir); // Update logic for existing settings.json const settingsPath = path.join(geminiDir, "settings.json"); if (await fileManager.pathExists(settingsPath)) { try { const settingsContent = await fileManager.readFile(settingsPath); const settings = JSON.parse(settingsContent); let updated = false; // Handle contextFileName property if (settings.contextFileName && Array.isArray(settings.contextFileName)) { const originalLength = settings.contextFileName.length; settings.contextFileName = settings.contextFileName.filter( (fileName) => !fileName.startsWith("agents/") ); if (settings.contextFileName.length !== originalLength) { updated = true; } } if (updated) { await fileManager.writeFile( settingsPath, JSON.stringify(settings, null, 2) ); console.log(chalk.green("✓ Updated .gemini/settings.json - removed agent file references")); } } catch (error) { console.warn( chalk.yellow("Could not update .gemini/settings.json"), error ); } } // Remove old agents directory const agentsDir = path.join(geminiDir, "agents"); if (await fileManager.pathExists(agentsDir)) { await fileManager.removeDirectory(agentsDir); console.log(chalk.green("✓ Removed old .gemini/agents directory")); } // Get all available agents const agents = await this.getAllAgentIds(installDir); let concatenatedContent = ""; for (const agentId of agents) { // Find the source agent file const agentPath = await this.findAgentPath(agentId, installDir); if (agentPath) { const agentContent = await fileManager.readFile(agentPath); // Create properly formatted agent rule content (similar to trae) let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`; agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle( agentId, installDir )} agent persona.\n\n`; agentRuleContent += "## Agent Activation\n\n"; agentRuleContent += "CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n"; agentRuleContent += "```yaml\n"; // Extract and clean the YAML content from the agent file const processedYamlContent = this.getCleanedYamlForIDE(agentContent, '.orchestrix-core'); if (processedYamlContent) { agentRuleContent += processedYamlContent; } else { // If no YAML found, include the whole content minus the header agentRuleContent += agentContent.replace(/^#.*$/m, "").trim(); } agentRuleContent += "\n```\n\n"; agentRuleContent += "## File Reference\n\n"; const relativePath = path.relative(installDir, agentPath).replace(/\\/g, '/'); agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`; agentRuleContent += "## Usage\n\n"; agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle( agentId, installDir )} persona and follow all instructions defined in the YAML configuration above.\n`; // Add to concatenated content with separator concatenatedContent += agentRuleContent + "\n\n---\n\n"; console.log(chalk.green(`✓ Added context for @${agentId}`)); } } // Write the concatenated content to GEMINI.md const geminiMdPath = path.join(orchestrixMethodDir, "GEMINI.md"); await fileManager.writeFile(geminiMdPath, concatenatedContent); console.log(chalk.green(`\n✓ Created GEMINI.md in ${orchestrixMethodDir}`)); return true; } async setupGitHubCopilot(installDir, selectedAgent, spinner = null, preConfiguredSettings = null) { await initializeModules(); // Configure VS Code workspace settings first to avoid UI conflicts with loading spinners await this.configureVsCodeSettings(installDir, spinner, preConfiguredSettings); const chatmodesDir = path.join(installDir, ".github", "chatmodes"); const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir); await fileManager.ensureDirectory(chatmodesDir); for (const agentId of agents) { // Find the agent file const agentPath = await this.findAgentPath(agentId, installDir); const chatmodePath = path.join(chatmodesDir, `${agentId}.chatmode.md`); if (agentPath) { // Create chat mode file with agent content const agentContent = await fileManager.readFile(agentPath); const agentTitle = await this.getAgentTitle(agentId, installDir); // Extract whenToUse for the description const yamlMatch = agentContent.match(/```ya?ml\r?\n([\s\S]*?)```/); let description = `Activates the ${agentTitle} agent persona.`; if (yamlMatch) { const whenToUseMatch = yamlMatch[1].match(/whenToUse:\s*(?:"([^"]+)"|'([^']+)'|([^\n\r]+))/); if (whenToUseMatch) { // Handle quoted strings (with " or ') const whenToUseValue = whenToUseMatch[1] || whenToUseMatch[2]; // Handle unquoted strings (but trim trailing whitespace) if (whenToUseValue) { description = whenToUseValue.trim(); } else if (whenToUseMatch[3]) { description = whenToUseMatch[3].trim(); } } } let chatmodeContent = `--- description: "${description.replace(/"/g, '\\"')}" tools: ['changes', 'codebase', 'fetch', 'findTestFiles', 'githubRepo', 'problems', 'usages'] --- `; chatmodeContent += agentContent; await fileManager.writeFile(chatmodePath, chatmodeContent); console.log(chalk.green(`✓ Created chat mode: ${agentId}.chatmode.md`)); } } console.log(chalk.green(`\n✓ Github Copilot setup complete!`)); console.log(chalk.dim(`You can now find the orchestrix agents in the Chat view's mode selector.`)); return true; } generateDescription(metadata) { const title = metadata.agent?.title || metadata.agent?.name || 'AI Assistant'; const role = metadata.persona?.role || 'Assistant'; return `${title} - ${role} specialized in Orchestrix workflows`; } generateWhenToUse(metadata) { const whenToUse = metadata.agent?.whenToUse || 'general assistance and task execution'; // Clean up the description to avoid redundancy return whenToUse.replace(/^use for /i, '').replace(/^Use for /i, ''); } generateCorePrinciples(metadata) { const principles = metadata.core_principles || []; if (principles.length === 0) { return '- Follow Orchestrix workflows and maintain quality standards\n- Execute commands with precision and provide clear feedback'; } // Take top 3-4 most important principles and format them return principles.slice(0, 4).map(p => `- ${p}`).join('\n'); } generateCommands(metadata, agentContent) { const commands = metadata.commands || []; if (commands.length === 0) { return '- `*help`: Show available commands\n- `*exit`: Exit agent mode'; } return commands.map(cmd => { const description = this.getCommandDescription(cmd, agentContent); return `- \`*${cmd.name}\`: ${description}`; }).join('\n'); } generateDependencies(metadata) { const deps = metadata.dependencies || {}; const depTypes = Object.keys(deps).filter(key => deps[key] && deps[key].length > 0); if (depTypes.length === 0) { return 'Standard Orchestrix resources (tasks, templates, checklists)';