Files
hakorune/tools/codex-tmux-driver/codex-output-watcher.js

150 lines
3.9 KiB
JavaScript
Raw Normal View History

// codex-output-watcher.js
// Codexの出力を監視してClaudeに転送するウォッチャー
const { spawn } = require('child_process');
const EventEmitter = require('events');
class CodexOutputWatcher extends EventEmitter {
constructor(sessionName = 'codex-safe') {
super();
this.sessionName = sessionName;
this.lastOutput = '';
this.isWorking = false;
this.watchInterval = null;
}
// 監視開始
start(intervalMs = 1000) {
console.log(`👁️ Starting to watch Codex output in ${this.sessionName}...`);
this.watchInterval = setInterval(() => {
this.checkOutput();
}, intervalMs);
}
// 監視停止
stop() {
if (this.watchInterval) {
clearInterval(this.watchInterval);
this.watchInterval = null;
console.log('👁️ Stopped watching');
}
}
// 画面をキャプチャして状態を確認
async checkOutput() {
try {
const output = await this.capturePane();
// 状態を解析
const wasWorking = this.isWorking;
this.isWorking = this.detectWorking(output);
// Working → 完了に変化した場合
if (wasWorking && !this.isWorking) {
console.log('✅ Codex finished working!');
const response = this.extractCodexResponse(output);
if (response) {
this.emit('response', response);
}
}
// プロンプトが表示されている = 入力待ち
if (this.detectPrompt(output) && !this.isWorking) {
this.emit('ready');
}
this.lastOutput = output;
} catch (err) {
console.error('❌ Watch error:', err);
}
}
// tmuxペインをキャプチャ
capturePane() {
return new Promise((resolve, reject) => {
const proc = spawn('tmux', ['capture-pane', '-t', this.sessionName, '-p']);
let output = '';
proc.stdout.on('data', (data) => output += data);
proc.on('close', (code) => {
if (code === 0) {
resolve(output);
} else {
reject(new Error(`tmux capture failed with code ${code}`));
}
});
});
}
// "Working" 状態を検出
detectWorking(output) {
return output.includes('Working (') || output.includes('⏳');
}
// プロンプト(入力待ち)を検出
detectPrompt(output) {
return output.includes('▌') && output.includes('⏎ send');
}
// Codexの応答を抽出
extractCodexResponse(output) {
const lines = output.split('\n');
let inCodexResponse = false;
let response = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// "codex" ラベルを見つけたら応答開始
if (line.trim() === 'codex') {
inCodexResponse = true;
continue;
}
// 次のプロンプトや"user"が来たら終了
if (inCodexResponse && (line.includes('▌') || line.trim() === 'user')) {
break;
}
// 応答を収集
if (inCodexResponse && line.trim()) {
// Working行やメタ情報を除外
if (!line.includes('Working') && !line.includes('⏎ send')) {
response.push(line);
}
}
}
return response.join('\n').trim();
}
}
// 使用例とテスト
if (require.main === module) {
const watcher = new CodexOutputWatcher();
watcher.on('response', (response) => {
console.log('\n📝 Codex Response:');
console.log('-------------------');
console.log(response);
console.log('-------------------\n');
// ここでClaudeに転送する処理を追加
console.log('🚀 TODO: Send this to Claude!');
});
watcher.on('ready', () => {
console.log('💚 Codex is ready for input');
});
watcher.start(500); // 500msごとにチェック
// 30秒後に停止
setTimeout(() => {
watcher.stop();
process.exit(0);
}, 30000);
}
module.exports = CodexOutputWatcher;