AI協調開発研究ドキュメントの完成と Phase 10.9-β 進捗
【AI協調開発研究】 - AI二重化モデルの学術論文draft完成(workshop_paper_draft.md) - 「隠れた危機」分析とbirthの原則哲学化 - TyEnv「唯一の真実」協調会話を保存・研究資料に統合 - papers管理構造の整備(wip/under-review/published分離) 【Phase 10.9-β HostCall進捗】 - JitConfigBox: relax_numeric フラグ追加(i64→f64コアーション制御) - HostcallRegistryBox: 署名検証・白黒リスト・コアーション対応 - JitHostcallRegistryBox: Nyash側レジストリ操作API - Lower統合: env直読 → jit::config::current() 参照に統一 - 数値緩和設定: NYASH_JIT_HOSTCALL_RELAX_NUMERIC/Config.set_flag 【検証サンプル拡充】 - math.sin/cos/abs/min/max 関数スタイル(examples/jit_math_function_style_*.nyash) - 境界ケース: 署名不一致・コアーション許可・mutating拒否サンプル - E2E実証: String.length→allow, Array.push→fallback, math関数の署名一致観測 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
366
tools/codex-tmux-driver/codex-tmux-driver.js
Normal file
366
tools/codex-tmux-driver/codex-tmux-driver.js
Normal file
@ -0,0 +1,366 @@
|
||||
// codex-tmux-driver.js
|
||||
// tmux経由でCodexを管理し、イベントをWebSocket配信するドライバ
|
||||
// 使い方:
|
||||
// 1) npm install
|
||||
// 2) node codex-tmux-driver.js [options]
|
||||
// --session=codex-session (tmuxセッション名)
|
||||
// --port=8766 (WebSocketポート)
|
||||
// --log=/tmp/codex.log (ログファイルパス)
|
||||
// 3) WebSocketで接続して操作
|
||||
// {"op":"send","data":"質問内容"}
|
||||
// {"op":"capture"}
|
||||
// {"op":"status"}
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const { WebSocketServer } = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// --- 設定 ---
|
||||
const argv = process.argv.slice(2).reduce((a, kv) => {
|
||||
const [k, ...rest] = kv.split('=');
|
||||
const v = rest.join('=');
|
||||
a[k.replace(/^--/, '')] = v ?? true;
|
||||
return a;
|
||||
}, {});
|
||||
|
||||
const SESSION_NAME = argv.session || 'codex-session';
|
||||
const PORT = Number(argv.port || 8766);
|
||||
const LOG_FILE = argv.log || `/tmp/codex-${Date.now()}.log`;
|
||||
const CODEX_CMD = argv.cmd || 'codex exec';
|
||||
|
||||
// --- 状態管理 ---
|
||||
let clients = new Set();
|
||||
let tailProcess = null;
|
||||
let sessionActive = false;
|
||||
let codexEventBuffer = [];
|
||||
const MAX_BUFFER_SIZE = 100;
|
||||
|
||||
// --- ユーティリティ関数 ---
|
||||
function broadcast(msg) {
|
||||
const data = JSON.stringify(msg);
|
||||
for (const ws of clients) {
|
||||
try { ws.send(data); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function executeCommand(cmd, args = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(cmd, 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) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(`Command failed: ${stderr}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Codex出力パターン認識 ---
|
||||
function parseCodexOutput(line) {
|
||||
const patterns = {
|
||||
// Codexの応答パターン
|
||||
response: /^(Codex:|回答:|Answer:|Response:)/i,
|
||||
thinking: /(考え中|Processing|Thinking|分析中)/i,
|
||||
error: /(エラー|Error|失敗|Failed)/i,
|
||||
complete: /(完了|Complete|Done|終了)/i,
|
||||
question: /(質問:|Question:|相談:|Help:)/i,
|
||||
};
|
||||
|
||||
for (const [event, pattern] of Object.entries(patterns)) {
|
||||
if (pattern.test(line)) {
|
||||
return {
|
||||
type: 'codex-event',
|
||||
event: event,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: line.trim()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// パターンに一致しない場合は生データとして返す
|
||||
return {
|
||||
type: 'codex-output',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: line
|
||||
};
|
||||
}
|
||||
|
||||
// --- tmuxセッション管理 ---
|
||||
async function createTmuxSession() {
|
||||
try {
|
||||
// 既存セッションをチェック
|
||||
try {
|
||||
await executeCommand('tmux', ['has-session', '-t', SESSION_NAME]);
|
||||
console.log(`[INFO] Session ${SESSION_NAME} already exists`);
|
||||
sessionActive = true;
|
||||
// 既存セッションでもパイプと監視を確実に有効化する
|
||||
try {
|
||||
await executeCommand('tmux', [
|
||||
'pipe-pane', '-t', SESSION_NAME,
|
||||
'-o', `cat >> ${LOG_FILE}`
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn('[WARN] Failed to ensure pipe-pane on existing session:', e.message || e);
|
||||
}
|
||||
if (!tailProcess) {
|
||||
startLogMonitoring();
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
// セッションが存在しない場合、作成
|
||||
}
|
||||
|
||||
// 新規セッション作成
|
||||
await executeCommand('tmux', [
|
||||
'new-session', '-d', '-s', SESSION_NAME,
|
||||
CODEX_CMD
|
||||
]);
|
||||
|
||||
// pipe-paneでログ出力を設定
|
||||
await executeCommand('tmux', [
|
||||
'pipe-pane', '-t', SESSION_NAME,
|
||||
'-o', `cat >> ${LOG_FILE}`
|
||||
]);
|
||||
|
||||
sessionActive = true;
|
||||
console.log(`[INFO] Created tmux session: ${SESSION_NAME}`);
|
||||
|
||||
// ログファイル監視開始
|
||||
startLogMonitoring();
|
||||
|
||||
} catch (err) {
|
||||
console.error('[ERROR] Failed to create tmux session:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// --- ログ監視 ---
|
||||
function startLogMonitoring() {
|
||||
// ログファイルが存在しない場合は作成
|
||||
if (!fs.existsSync(LOG_FILE)) {
|
||||
fs.writeFileSync(LOG_FILE, '');
|
||||
}
|
||||
|
||||
// tail -fで監視
|
||||
tailProcess = spawn('tail', ['-f', '-n', '0', LOG_FILE]);
|
||||
|
||||
tailProcess.stdout.on('data', (data) => {
|
||||
const lines = data.toString('utf8').split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
const event = parseCodexOutput(line);
|
||||
|
||||
// イベントバッファに追加
|
||||
codexEventBuffer.push(event);
|
||||
if (codexEventBuffer.length > MAX_BUFFER_SIZE) {
|
||||
codexEventBuffer.shift();
|
||||
}
|
||||
|
||||
// クライアントに配信
|
||||
broadcast(event);
|
||||
}
|
||||
});
|
||||
|
||||
tailProcess.on('error', (err) => {
|
||||
console.error('[ERROR] Tail process error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// --- WebSocketサーバ ---
|
||||
const wss = new WebSocketServer({ port: PORT });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
clients.add(ws);
|
||||
|
||||
// 接続時にステータスと最近のイベントを送信
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
session: SESSION_NAME,
|
||||
active: sessionActive,
|
||||
logFile: LOG_FILE,
|
||||
recentEvents: codexEventBuffer.slice(-10)
|
||||
}
|
||||
}));
|
||||
|
||||
ws.on('message', async (raw) => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
ws.send(JSON.stringify({ type: 'error', data: 'Invalid JSON' }));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.op) {
|
||||
case 'send': {
|
||||
// Codexに入力を送信
|
||||
if (!sessionActive) {
|
||||
ws.send(JSON.stringify({ type: 'error', data: 'Session not active' }));
|
||||
break;
|
||||
}
|
||||
|
||||
const input = String(msg.data || '').trim();
|
||||
if (!input) break;
|
||||
|
||||
try {
|
||||
await executeCommand('tmux', [
|
||||
'send-keys', '-t', SESSION_NAME,
|
||||
input, 'Enter'
|
||||
]);
|
||||
|
||||
broadcast({
|
||||
type: 'input-sent',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: input
|
||||
});
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Failed to send input'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'capture': {
|
||||
// 現在の画面をキャプチャ
|
||||
if (!sessionActive) {
|
||||
ws.send(JSON.stringify({ type: 'error', data: 'Session not active' }));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await executeCommand('tmux', [
|
||||
'capture-pane', '-t', SESSION_NAME, '-p'
|
||||
]);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'screen-capture',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: output
|
||||
}));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Failed to capture screen'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'status': {
|
||||
// ステータス確認
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
session: SESSION_NAME,
|
||||
active: sessionActive,
|
||||
logFile: LOG_FILE,
|
||||
clientCount: clients.size,
|
||||
bufferSize: codexEventBuffer.length
|
||||
}
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'history': {
|
||||
// イベント履歴取得
|
||||
const count = Number(msg.count || 20);
|
||||
const history = codexEventBuffer.slice(-count);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'history',
|
||||
data: history
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'filter': {
|
||||
// 特定のイベントタイプのみフィルタ
|
||||
const eventType = msg.event || 'all';
|
||||
const filtered = eventType === 'all'
|
||||
? codexEventBuffer
|
||||
: codexEventBuffer.filter(e => e.event === eventType);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'filtered-events',
|
||||
filter: eventType,
|
||||
data: filtered
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'kill': {
|
||||
// セッション終了
|
||||
if (!sessionActive) break;
|
||||
|
||||
try {
|
||||
await executeCommand('tmux', ['kill-session', '-t', SESSION_NAME]);
|
||||
sessionActive = false;
|
||||
if (tailProcess) {
|
||||
try { tailProcess.kill(); } catch {}
|
||||
tailProcess = null;
|
||||
}
|
||||
broadcast({ type: 'session-killed' });
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Failed to kill session'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: `Unknown operation: ${msg.op}`
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws);
|
||||
});
|
||||
});
|
||||
|
||||
// --- 起動処理 ---
|
||||
async function start() {
|
||||
console.log('=== Codex tmux Driver ===');
|
||||
console.log(`WebSocket: ws://localhost:${PORT}`);
|
||||
console.log(`Session: ${SESSION_NAME}`);
|
||||
console.log(`Log file: ${LOG_FILE}`);
|
||||
|
||||
try {
|
||||
await createTmuxSession();
|
||||
|
||||
wss.on('listening', () => {
|
||||
console.log(`[INFO] WebSocket server listening on port ${PORT}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[FATAL] Failed to start:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- クリーンアップ ---
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n[INFO] Shutting down...');
|
||||
if (tailProcess) {
|
||||
tailProcess.kill();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 起動
|
||||
start();
|
||||
Reference in New Issue
Block a user