Files
hakorune/tools/codex-tmux-driver/tmux-perfect-bridge.js

217 lines
6.2 KiB
JavaScript
Raw Normal View History

// tmux-perfect-bridge.js
// tmux × tmux = 完璧な双方向自動ブリッジ!
const { spawn } = require('child_process');
const EventEmitter = require('events');
class TmuxPerfectBridge extends EventEmitter {
constructor() {
super();
this.codexSession = 'codex-bridge';
this.claudeSession = 'claude-bridge';
this.isRunning = false;
this.lastCodexOutput = '';
this.lastClaudeOutput = '';
}
// 両方のAIをtmuxで起動
async start() {
console.log('🚀 Starting Perfect Bridge...');
// Codexを起動
await this.startSession(this.codexSession, '/home/tomoaki/.volta/bin/codex');
// Claudeを起動仮のコマンド
// await this.startSession(this.claudeSession, 'claude');
console.log('✅ Both AIs are ready in tmux!');
this.isRunning = true;
}
// tmuxセッションを起動
async startSession(sessionName, command) {
// 既存セッションを削除
await this.exec('tmux', ['kill-session', '-t', sessionName]).catch(() => {});
// 新規セッション作成
await this.exec('tmux', ['new-session', '-d', '-s', sessionName, command]);
console.log(`📺 Started ${sessionName}`);
// 起動待ち
await this.sleep(2000);
}
// Codex → Claude 転送
async forwardCodexToClaude() {
const codexOutput = await this.capturePane(this.codexSession);
const newContent = this.extractNewContent(codexOutput, this.lastCodexOutput);
if (newContent && this.isCodexResponse(newContent)) {
console.log('📨 Codex → Claude:', newContent.substring(0, 50) + '...');
// tmux send-keysで直接送信Enterも完璧
await this.sendToSession(this.claudeSession, newContent);
this.lastCodexOutput = codexOutput;
this.emit('codex-to-claude', newContent);
}
}
// Claude → Codex 転送
async forwardClaudeToCodex() {
const claudeOutput = await this.capturePane(this.claudeSession);
const newContent = this.extractNewContent(claudeOutput, this.lastClaudeOutput);
if (newContent && this.isClaudeResponse(newContent)) {
console.log('📨 Claude → Codex:', newContent.substring(0, 50) + '...');
// tmux send-keysで直接送信Enterも完璧
await this.sendToSession(this.codexSession, newContent);
this.lastClaudeOutput = claudeOutput;
this.emit('claude-to-codex', newContent);
}
}
// 双方向監視ループ
async startWatching(intervalMs = 1000) {
console.log('👁️ Starting bidirectional watch...');
const watchLoop = setInterval(async () => {
if (!this.isRunning) {
clearInterval(watchLoop);
return;
}
try {
// 両方向をチェック
await this.forwardCodexToClaude();
await this.forwardClaudeToCodex();
} catch (err) {
console.error('❌ Watch error:', err);
}
}, intervalMs);
}
// tmuxペインをキャプチャ
async capturePane(sessionName) {
const result = await this.exec('tmux', ['capture-pane', '-t', sessionName, '-p']);
return result.stdout;
}
// tmuxセッションに送信Enterも
async sendToSession(sessionName, text) {
await this.exec('tmux', ['send-keys', '-t', sessionName, text, 'Enter']);
}
// 新しいコンテンツを抽出
extractNewContent(current, previous) {
// 簡単な差分検出(実際はもっと高度にする)
if (current.length > previous.length) {
return current.substring(previous.length).trim();
}
return null;
}
// Codexの応答かどうか判定
isCodexResponse(text) {
return !text.includes('Working') &&
!text.includes('▌') &&
text.length > 10;
}
// Claudeの応答かどうか判定
isClaudeResponse(text) {
// Claudeの出力パターンに応じて調整
return text.length > 10;
}
// 初期メッセージを送信
async sendInitialMessage(message) {
console.log('🎯 Sending initial message to Codex...');
await this.sendToSession(this.codexSession, message);
}
// 両セッションを表示(デバッグ用)
showSessions() {
console.log('\n📺 Showing both sessions side by side...');
spawn('tmux', [
'new-window', '-n', 'AI-Bridge',
`tmux select-pane -t 0 \\; \
attach-session -t ${this.codexSession} \\; \
split-window -h \\; \
attach-session -t ${this.claudeSession}`
], { stdio: 'inherit' });
}
// 停止
async stop() {
this.isRunning = false;
await this.exec('tmux', ['kill-session', '-t', this.codexSession]).catch(() => {});
await this.exec('tmux', ['kill-session', '-t', this.claudeSession]).catch(() => {});
console.log('👋 Bridge stopped');
}
// ヘルパー関数
exec(command, args) {
return new Promise((resolve, reject) => {
const proc = spawn(command, args);
let stdout = '';
let stderr = '';
proc.stdout.on('data', (data) => stdout += data);
proc.stderr.on('data', (data) => stderr += data);
proc.on('close', (code) => {
if (code !== 0 && !stderr.includes('no server running')) {
reject(new Error(`${command} exited with code ${code}: ${stderr}`));
} else {
resolve({ stdout, stderr });
}
});
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// デモ実行
async function demo() {
const bridge = new TmuxPerfectBridge();
// イベントリスナー
bridge.on('codex-to-claude', (content) => {
console.log('🔄 Transferred from Codex to Claude');
});
bridge.on('claude-to-codex', (content) => {
console.log('🔄 Transferred from Claude to Codex');
});
try {
// ブリッジ開始
await bridge.start();
// 初期メッセージ
await bridge.sendInitialMessage('Nyashプロジェクトについて、お互いに意見を交換してください');
// 監視開始
await bridge.startWatching(500);
// デバッグ用に画面表示
// bridge.showSessions();
} catch (err) {
console.error('❌ Error:', err);
}
}
// エクスポート
module.exports = TmuxPerfectBridge;
// 直接実行
if (require.main === module) {
demo();
}