orchestrix
Version:
Orchestrix - Universal AI Agent Framework for Coordinated AI-Driven Development
1,197 lines (1,005 loc) • 220 kB
JavaScript
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)';