UNPKG

agent-collaboration-mcp

Version:

Self-contained MCP server for multi-agent collaboration with automatic authentication delegation and complete tmux session management - no external dependencies required

215 lines (180 loc) 9.54 kB
const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); const path = require('path'); const fs = require('fs'); const { parseTarget, formatOutput } = require('../utils/tmuxUtils'); class AgentManager { constructor(config = {}) { // Use provided config or defaults this.projectDir = config.projectDir || process.cwd(); const scriptDir = config.scriptDir || 'scripts/agent_tools'; // Build script paths - prioritize internal scripts if available const mcpDir = path.dirname(path.dirname(__dirname)); // Get MCP root directory const internalScriptPath = path.join(mcpDir, 'scripts', 'agent_tools', 'agent_manager.sh'); const internalPaneControllerPath = path.join(mcpDir, 'scripts', 'agent_tools', 'pane_controller.sh'); const internalAuthHelperPath = path.join(mcpDir, 'scripts', 'agent_tools', 'auth_helper.sh'); // Use internal scripts if they exist, otherwise fallback to project scripts if (fs.existsSync(internalScriptPath)) { this.scriptPath = internalScriptPath; this.paneControllerPath = internalPaneControllerPath; this.authHelperPath = internalAuthHelperPath; // Suppress console output for MCP protocol compatibility // console.error(`Using internal MCP scripts: ${internalScriptPath}`); } else { this.scriptPath = path.join(this.projectDir, scriptDir, 'agent_manager.sh'); this.paneControllerPath = path.join(this.projectDir, scriptDir, 'pane_controller.sh'); this.authHelperPath = path.join(this.projectDir, scriptDir, 'auth_helper.sh'); // console.error(`Using external project scripts: ${this.scriptPath}`); } // Debug output suppressed for MCP protocol compatibility // console.error(`[DEBUG] AgentManager initialized:...`); } async startAgent(target, agentType = 'claude', additionalArgs = '') { try { const { paneNumber } = parseTarget(target); const cmd = `${this.scriptPath} start ${paneNumber} ${agentType} ${additionalArgs}`.trim(); const { stdout, stderr } = await execAsync(cmd, { cwd: this.projectDir, timeout: 350000 // 350秒(約6分)- agent_manager.shの300秒待機 + 余裕 }); return formatOutput(stdout, stderr); } catch (error) { throw new Error(`Failed to start agent: ${error.message}`); } } // 🔍 改善されたgetStatus - tmux target対応 async getStatus(target = '') { try { let result = ''; if (target && !target.includes('*')) { // 特定ペインの詳細状態 const { sessionName, windowNumber, paneNumber, fullTarget } = parseTarget(target); // ペイン画面内容をキャプチャ(pane_controller.sh経由) let screenContent = ''; try { const { stdout } = await execAsync(`${this.paneControllerPath} capture ${paneNumber} -3000`, { cwd: this.projectDir, timeout: 5000 }); screenContent = stdout; } catch (captureError) { screenContent = `キャプチャエラー: ${captureError.message}`; } // auth_helper.shを使って状態を取得 console.error(`[DEBUG] Getting state for pane ${paneNumber}`); let analysis; try { const { stdout: stateResult } = await execAsync(`${this.authHelperPath} state ${paneNumber}`, { cwd: this.projectDir, timeout: 3000 }); // 結果をパース: "state|agent|details" const [state, agent, details] = stateResult.trim().split('|'); analysis = { state, agent, details }; } catch (stateError) { analysis = { state: 'unknown', agent: 'none', details: `状態取得エラー: ${stateError.message}` }; } console.error(`[DEBUG] Analysis result for ${fullTarget}:`, analysis); result = `🔍 Target ${fullTarget} の詳細状態: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📊 状態: ${this.getStateIcon(analysis.state)} ${analysis.state} 🤖 エージェント: ${analysis.agent} 📝 詳細: ${analysis.details} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📺 画面内容 (最新20行): ${screenContent.split('\n').slice(-20).join('\n')}`; } else { // セッション内全ペイン状態一覧 let sessionName = 'multiagent'; // デフォルト if (target && target.includes('*')) { sessionName = target.split(':')[0]; } result = `🌐 セッション "${sessionName}" 全ペイン状態一覧:\n`; result += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; const stateSummary = { running_claude: 0, running_gemini: 0, auth_claude: 0, auth_gemini: 0, executing_claude: 0, stopped: 0 }; // ペイン一覧を取得(共通ライブラリの関数を使用) try { // get_all_panes関数を呼び出し(MCPディレクトリを考慮) const mcpDir = path.dirname(path.dirname(__dirname)); // MCP root directory const utilsPath = fs.existsSync(path.join(mcpDir, 'scripts', 'common', 'utils.sh')) ? path.join(mcpDir, 'scripts', 'common', 'utils.sh') : path.join(this.projectDir, 'scripts', 'common', 'utils.sh'); const { stdout: paneList } = await execAsync(`bash -c 'source "${utilsPath}" && setup_directories "." >&2 && get_all_panes "${sessionName}"'`, { cwd: this.projectDir, timeout: 5000 }); const paneNumbers = paneList.trim().split('\n').filter(p => p.trim()); for (const paneNum of paneNumbers) { const currentTarget = `${sessionName}:0.${paneNum}`; try { // auth_helper.shを使って状態を取得 const { stdout: stateResult } = await execAsync(`${this.authHelperPath} state ${paneNum}`, { cwd: this.projectDir, timeout: 3000 }); // 結果をパース: "state|agent|details" const [state, agent, details] = stateResult.trim().split('|'); const analysis = { state, agent, details }; stateSummary[analysis.state] = (stateSummary[analysis.state] || 0) + 1; // ペイン名を取得(共通ライブラリの関数を使用) let paneName = ''; try { const { stdout: nameResult } = await execAsync(`bash -c 'source "${utilsPath}" && get_pane_name ${paneNum}'`, { cwd: this.projectDir, timeout: 1000 }); paneName = nameResult.trim(); } catch (nameErr) { // エラーの場合は空文字列のまま } const targetDisplay = paneName ? `${currentTarget} (${paneName})` : currentTarget; result += `${this.getStateIcon(analysis.state)} ${targetDisplay.padEnd(40)} | ${analysis.agent.padEnd(8)} | ${analysis.state.padEnd(12)}\n`; } catch (error) { stateSummary.stopped++; result += `❌ ${currentTarget.padEnd(40)} | error | capture_fail\n`; } } } catch (sessionError) { return `❌ エラー: セッション "${sessionName}" が見つかりません: ${sessionError.message}`; } result += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; result += '📊 状態サマリー:\n'; result += ` ✅ Claude起動完了: ${stateSummary.running_claude}個\n`; result += ` 💎 Gemini起動完了: ${stateSummary.running_gemini}個\n`; result += ` 🔐 Claude認証中: ${stateSummary.auth_claude}個\n`; result += ` 🔑 Gemini認証中: ${stateSummary.auth_gemini}個\n`; result += ` ⚡ Claude実行中: ${stateSummary.executing_claude}個\n`; result += ` ⚫ 停止中: ${stateSummary.stopped}個\n`; } return result; } catch (error) { throw new Error(`Failed to get status: ${error.message}`); } } // ペイン番号→名前変換 async getPaneName(sessionName, windowNumber, paneNumber) { try { // pane_controller.shのstatus機能を使用してペイン名を取得 // 現在はペインタイトルを使用していないため、空文字列を返す // 将来的にペイン名機能を追加する場合はここで実装 return ''; } catch (error) { // エラーの場合は空文字列を返す return ''; } } // 状態アイコン取得 getStateIcon(state) { const icons = { 'running_claude': '✅', 'running_gemini': '💎', 'auth_claude': '🔐', 'auth_gemini': '🔑', 'executing_claude': '⚡', 'stopped': '⚫' }; return icons[state] || '❓'; } } module.exports = { AgentManager };