claude-code-company
Version:
Multi-agent tmux coordination system for Claude Code with perfect Unicode support
254 lines (223 loc) • 9.11 kB
JavaScript
const fs = require('fs');
const { colors, agents, rateLimitConfig, getWindowConfig } = require('./config');
const { tmuxCommand, getTmuxTarget, tmuxGetPaneContent, tmuxSendKeys, tmuxSendKeysRaw, getCallerWindow, getAgentWindow, getCachedWindowId } = require('./tmux');
// Get pane number for agent
async function getAgentPane(agent) {
// Null/undefined チェック
if (agent === null || agent === undefined) {
throw new Error('Agent parameter is null or undefined');
}
// エージェントが実際にいるwindow IDを取得
let windowId;
try {
// エージェント名からそのエージェントがいるウィンドウを特定
windowId = await getAgentWindow(agent);
} catch (error) {
// window ID取得失敗時はデフォルト設定を使用
const { agents: currentAgents, panes: currentPanes } = require('./config');
const agentIndex = currentAgents.indexOf(agent);
if (agentIndex !== -1 && agentIndex < currentPanes.length) {
return currentPanes[agentIndex];
}
// 数値でアクセスする場合の対応
const paneNum = parseInt(agent);
if (!isNaN(paneNum)) {
return paneNum;
}
throw new Error(`Invalid agent: ${agent}`);
}
// window IDに対応する設定を取得
const windowConfig = getWindowConfig(windowId);
const agentIndex = windowConfig.agents.indexOf(agent);
if (agentIndex !== -1 && agentIndex < windowConfig.panes.length) {
const pane = windowConfig.panes[agentIndex];
if (pane !== null && pane !== undefined) {
return pane;
}
throw new Error(`Pane not found for agent ${agent} at index ${agentIndex}`);
}
// 数値でアクセスする場合の対応
const paneNum = parseInt(agent);
if (!isNaN(paneNum)) {
return paneNum;
}
throw new Error(`Invalid agent: ${agent}`);
}
// Ping an agent to check if it's responsive
async function pingAgent(pane, agent) {
try {
// まずpaneが存在するか確認
const target = await getTmuxTarget(pane, agent);
await tmuxCommand('list-panes', '-t', target);
// paneの内容を取得
const content = await tmuxGetPaneContent(pane, agent);
// Claude Codeが起動しているかチェック
if (content.includes('Welcome to Claude Code') || content.includes('?')) {
return { status: 'ready', responsive: true };
} else if (content.includes('claude') && content.includes('$')) {
// claudeコマンドが実行されたが、まだ起動していない
return { status: 'starting', responsive: false };
} else if (content.includes('===') && content.includes('pane')) {
// エージェントが設定されているが、Claude Codeが起動していない
return { status: 'configured', responsive: false };
} else {
return { status: 'unknown', responsive: false };
}
} catch (e) {
return { status: 'offline', responsive: false };
}
}
// Restart an agent if it's not responsive
async function restartAgent(pane, agentName) {
console.log(`${colors.yellow}🔄 Attempting to restart ${agentName} (pane ${pane})...${colors.nc}`);
try {
// まずプロセスを停止
await tmuxSendKeysRaw(pane, 'C-c', agentName);
await new Promise(resolve => setTimeout(resolve, 500));
// paneをクリア
await tmuxSendKeys(pane, 'clear', agentName);
await new Promise(resolve => setTimeout(resolve, 200));
// エージェント識別子を表示
await tmuxSendKeys(pane, `echo '=== ${agentName} (pane ${pane}) ==='`, agentName);
await new Promise(resolve => setTimeout(resolve, 300));
// Claude Codeを再起動
await tmuxSendKeys(pane, 'claude', agentName);
await new Promise(resolve => setTimeout(resolve, rateLimitConfig.setupDelay));
// 再度pingして確認
const pingResult = await pingAgent(pane, agentName);
if (pingResult.status === 'ready' || pingResult.status === 'starting') {
console.log(`${colors.green}✅ ${agentName} restarted successfully!${colors.nc}`);
return true;
} else {
console.log(`${colors.red}❌ Failed to restart ${agentName}${colors.nc}`);
return false;
}
} catch (e) {
console.log(`${colors.red}❌ Error restarting ${agentName}: ${e.message}${colors.nc}`);
return false;
}
}
// Show system status
async function showSystemStatus() {
console.log(`${colors.yellow}📊 System Status${colors.nc}\n`);
// tmuxセッション確認
try {
await tmuxCommand('list-sessions');
console.log(`${colors.green}✅ tmux session: Active${colors.nc}`);
} catch (e) {
console.log(`${colors.red}❌ tmux session: Inactive${colors.nc}`);
return;
}
console.log(`\n${colors.yellow}Agent Status:${colors.nc}`);
// 各paneの状態確認
const totalAgents = agents.length;
for (let i = 0; i < totalAgents; i++) {
const agent = agents[i];
const pane = await getAgentPane(agent);
if (!agent) continue;
// エージェントをpingして状態を確認
const pingResult = await pingAgent(pane, agent);
let statusIcon, statusText, statusColor;
switch (pingResult.status) {
case 'ready':
statusIcon = '✅';
statusText = 'Ready';
statusColor = colors.green;
break;
case 'starting':
statusIcon = '⚡';
statusText = 'Starting';
statusColor = colors.yellow;
break;
case 'configured':
statusIcon = '⚙️';
statusText = 'Configured (Claude not running)';
statusColor = colors.yellow;
break;
case 'offline':
statusIcon = '❌';
statusText = 'Offline';
statusColor = colors.red;
break;
default:
statusIcon = '❓';
statusText = 'Unknown';
statusColor = colors.red;
}
console.log(` ${agent.padEnd(12)} ${statusColor}${statusIcon} ${statusText}${colors.nc}`);
}
// 一時ファイル確認
console.log(`\n${colors.yellow}📁 Temporary files:${colors.nc}`);
try {
const files = fs.readdirSync('./tmp');
if (files.length === 0) {
console.log(`${colors.green}✅ No temporary files${colors.nc}`);
} else {
files.forEach(file => {
console.log(`${colors.blue}📄 ${file}${colors.nc}`);
});
}
} catch (e) {
console.log(`${colors.red}❌ Cannot read tmp directory${colors.nc}`);
}
}
// アクティブなWorkerを取得する関数(現在のウィンドウコンテキスト対応)
async function getActiveWorkers() {
// 現在のウィンドウIDを取得
let currentWindowId;
let windowConfig;
try {
// キャッシュされたwindow IDがあればそれを使用
currentWindowId = getCachedWindowId();
if (!currentWindowId) {
currentWindowId = await getCurrentWindow();
}
console.log(`${colors.blue}🔍 現在のウィンドウ: ${currentWindowId} でWorker検出中${colors.nc}`);
windowConfig = getWindowConfig(currentWindowId);
} catch (error) {
console.log(`${colors.yellow}⚠️ ウィンドウ検出失敗、デフォルトWorkerを使用: ${error.message}${colors.nc}`);
const { agents: currentAgents } = require('./config');
return currentAgents.filter(agent =>
agent && agent.startsWith('worker') && !agent.startsWith('president')
);
}
const workerAgents = windowConfig.agents.filter(agent =>
agent && agent.startsWith('worker') && !agent.startsWith('president')
);
// tmuxセッション外の場合は設定されたWorkerをそのまま返す
try {
const { getCurrentRole } = require('./tmux');
const currentRole = await getCurrentRole();
if (currentRole === 'outside_session' || currentRole === 'no_tmux') {
console.log(`${colors.yellow}⚠️ tmuxセッション外からWorker検出をスキップし、設定されたWorkerを使用${colors.nc}`);
return workerAgents;
}
} catch (error) {
// tmux関連エラーの場合も設定されたWorkerを使用
console.log(`${colors.yellow}⚠️ Worker検出エラーのため、設定されたWorkerを使用: ${error.message}${colors.nc}`);
return workerAgents;
}
const activeWorkers = [];
for (const worker of workerAgents) {
try {
const pane = await getAgentPane(worker);
const pingResult = await pingAgent(pane, worker);
if (pingResult.responsive || pingResult.status === 'ready' || pingResult.status === 'starting') {
activeWorkers.push(worker);
}
} catch (error) {
// エラーの場合でもWorkerを含める(メッセージ送信を試行)
console.log(`${colors.yellow}⚠️ ${worker}のping失敗、メッセージ送信を試行: ${error.message}${colors.nc}`);
activeWorkers.push(worker);
}
}
return activeWorkers.length > 0 ? activeWorkers : workerAgents;
}
// 同期版(バックアップ用)
function getActiveWorkersSync() {
return agents.filter(agent => agent.startsWith('worker'));
}
module.exports = {
getAgentPane,
getActiveWorkers
};