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:
280
tools/codex-tmux-driver/hook-server.js
Normal file
280
tools/codex-tmux-driver/hook-server.js
Normal file
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env node
|
||||
// hook-server.js
|
||||
// Codexフックからのイベントを受信してClaude連携するサーバー
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const PORT = process.env.HOOK_SERVER_PORT || 8770;
|
||||
const STRIP_ANSI = process.env.HOOK_STRIP_ANSI !== 'false';
|
||||
const AUTO_BRIDGE = process.env.AUTO_BRIDGE === 'true';
|
||||
const AUTO_EXIT = process.env.HOOK_SERVER_AUTO_EXIT === 'true';
|
||||
const IDLE_EXIT_MS = Number(process.env.HOOK_IDLE_EXIT_MS || 2000);
|
||||
|
||||
// WebSocketサーバー
|
||||
const wss = new WebSocket.Server({ port: PORT });
|
||||
|
||||
// 状態管理
|
||||
const state = {
|
||||
lastInput: '',
|
||||
lastOutput: '',
|
||||
waitingCount: 0,
|
||||
questionQueue: [],
|
||||
// 接続クライアント: Map<WebSocket, 'hook' | 'control'>
|
||||
clients: new Map()
|
||||
};
|
||||
|
||||
console.log(`🪝 Codex Hook Server listening on ws://localhost:${PORT}`);
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const clientType = req.url === '/control' ? 'control' : 'hook';
|
||||
|
||||
console.log(`📌 New ${clientType} connection`);
|
||||
state.clients.set(ws, clientType);
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
|
||||
if (clientType === 'hook') {
|
||||
// Codexフックからのメッセージ
|
||||
await handleHookMessage(msg, ws);
|
||||
} else {
|
||||
// 制御クライアントからのメッセージ
|
||||
await handleControlMessage(ws, msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Message error:', e);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
state.clients.delete(ws);
|
||||
maybeAutoExit();
|
||||
});
|
||||
});
|
||||
|
||||
// ANSIエスケープ除去
|
||||
function stripAnsi(s) {
|
||||
if (!STRIP_ANSI) return s;
|
||||
if (typeof s !== 'string') return s;
|
||||
// Robust ANSI/CSI/OSC sequences removal
|
||||
const ansiPattern = /[\u001B\u009B][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nq-uy=><]/g;
|
||||
return s.replace(ansiPattern, '');
|
||||
}
|
||||
|
||||
// フックメッセージ処理
|
||||
async function handleHookMessage(msg, senderWs) {
|
||||
const preview = typeof msg.data === 'string' ? stripAnsi(msg.data).substring(0, 80) : JSON.stringify(msg.data);
|
||||
console.log(`[${msg.type}] ${preview}`);
|
||||
|
||||
// 全制御クライアントに転送
|
||||
broadcast('control', msg);
|
||||
|
||||
switch (msg.type) {
|
||||
case 'input':
|
||||
state.lastInput = msg.data;
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
state.lastOutput = typeof msg.data === 'string' ? stripAnsi(msg.data) : msg.data;
|
||||
break;
|
||||
|
||||
case 'hook-event':
|
||||
await handleHookEvent(msg);
|
||||
break;
|
||||
|
||||
case 'inject-input':
|
||||
// フッククライアントからの入力注入リクエスト
|
||||
console.log('🔄 Relaying inject-input from hook client');
|
||||
|
||||
// 明示的なターゲットがあればそれを最優先(tmuxセッション名を想定)
|
||||
if (msg.target && typeof msg.target === 'string') {
|
||||
const { spawn } = require('child_process');
|
||||
const text = String(msg.data ?? '');
|
||||
const targetSession = msg.target;
|
||||
console.log(`📤 Sending to explicit target via tmux: ${targetSession}`);
|
||||
|
||||
// 文字列を通常の方法で送信
|
||||
const { exec } = require('child_process');
|
||||
const messageEscaped = text.replace(/'/g, "'\\''");
|
||||
await new Promise((resolve) => {
|
||||
exec(`tmux send-keys -t ${targetSession} '${messageEscaped}' Enter`, (error) => {
|
||||
if (error) console.error(`❌ tmux error: ${error.message}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.HOOK_SEND_CTRL_J === 'true') {
|
||||
await new Promise((resolve) => {
|
||||
const p = spawn('tmux', ['send-keys', '-t', targetSession, 'C-j']);
|
||||
p.on('close', () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ Message + Enter sent to ${targetSession}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 互換ルーティング(source から推測)
|
||||
let targetSession = 'claude';
|
||||
if (msg.source === 'codex') {
|
||||
targetSession = 'claude';
|
||||
} else if (msg.source === 'claude') {
|
||||
targetSession = 'codex';
|
||||
}
|
||||
|
||||
console.log(`📤 Forwarding to ${targetSession}`);
|
||||
|
||||
if (targetSession === 'claude') {
|
||||
// Claude想定:WebSocket経由でstdinに直接送信(注意: 全hookに送られる)
|
||||
console.log('🎯 Sending to Claude via WebSocket (stdin)');
|
||||
broadcast('hook', {
|
||||
type: 'inject-input',
|
||||
data: msg.data,
|
||||
target: 'claude'
|
||||
});
|
||||
} else {
|
||||
// Codex想定:tmux send-keys
|
||||
const { exec } = require('child_process');
|
||||
const text = String(msg.data ?? '');
|
||||
const messageEscaped = text.replace(/'/g, "'\\''");
|
||||
console.log(`📤 Sending to ${targetSession} via tmux`);
|
||||
await new Promise((resolve) => {
|
||||
exec(`tmux send-keys -t ${targetSession} '${messageEscaped}' Enter`, (error) => {
|
||||
if (error) console.error(`❌ tmux error: ${error.message}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
if (process.env.HOOK_SEND_CTRL_J === 'true') {
|
||||
await new Promise((resolve) => {
|
||||
const p = spawn('tmux', ['send-keys', '-t', targetSession, 'C-j']);
|
||||
p.on('close', () => resolve());
|
||||
});
|
||||
}
|
||||
console.log(`✅ Message + Enter sent to ${targetSession}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// フックイベント処理
|
||||
async function handleHookEvent(msg) {
|
||||
switch (msg.event) {
|
||||
case 'question-detected':
|
||||
console.log('❓ Question detected:', msg.data);
|
||||
state.questionQueue.push({
|
||||
question: msg.data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
if (AUTO_BRIDGE) {
|
||||
// 自動ブリッジが有効なら応答を生成
|
||||
setTimeout(() => {
|
||||
injectResponse('考えさせてください...');
|
||||
}, 1000);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'waiting-detected':
|
||||
state.waitingCount++;
|
||||
console.log(`⏳ Waiting detected (count: ${state.waitingCount})`);
|
||||
|
||||
// 3回連続で待機状態なら介入
|
||||
if (state.waitingCount >= 3 && AUTO_BRIDGE) {
|
||||
console.log('🚨 Auto-intervention triggered');
|
||||
injectResponse('続けてください');
|
||||
state.waitingCount = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'codex-exit':
|
||||
console.log('🛑 Codex process exited');
|
||||
maybeAutoExit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 制御メッセージ処理
|
||||
async function handleControlMessage(ws, msg) {
|
||||
switch (msg.op) {
|
||||
case 'inject':
|
||||
injectResponse(msg.data);
|
||||
ws.send(JSON.stringify({ type: 'injected', data: msg.data }));
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status',
|
||||
state: {
|
||||
lastInput: state.lastInput,
|
||||
lastOutput: state.lastOutput.substring(0, 100),
|
||||
waitingCount: state.waitingCount,
|
||||
questionCount: state.questionQueue.length,
|
||||
clients: state.clients.size
|
||||
}
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'questions':
|
||||
ws.send(JSON.stringify({
|
||||
type: 'questions',
|
||||
data: state.questionQueue
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Codexに応答を注入
|
||||
function injectResponse(response) {
|
||||
console.log('💉 Injecting response:', response);
|
||||
|
||||
// フッククライアントに注入コマンドを送信
|
||||
broadcast('hook', {
|
||||
type: 'inject-input',
|
||||
data: response
|
||||
});
|
||||
}
|
||||
|
||||
// ブロードキャスト
|
||||
function broadcast(clientType, message, excludeWs = null) {
|
||||
const data = JSON.stringify(message);
|
||||
let sentCount = 0;
|
||||
for (const [clientWs, type] of state.clients.entries()) {
|
||||
if (type === clientType && clientWs.readyState === WebSocket.OPEN) {
|
||||
if (excludeWs && clientWs === excludeWs) continue; // 送信元を除外
|
||||
clientWs.send(data);
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
console.log(`📡 Broadcast to ${sentCount} ${clientType} clients`);
|
||||
}
|
||||
|
||||
// フッククライアントがいなければ自動終了
|
||||
let exitTimer = null;
|
||||
function maybeAutoExit() {
|
||||
if (!AUTO_EXIT) return;
|
||||
const hasHook = Array.from(state.clients.values()).some(t => t === 'hook');
|
||||
if (hasHook) return;
|
||||
if (exitTimer) clearTimeout(exitTimer);
|
||||
exitTimer = setTimeout(() => {
|
||||
const hasHookNow = Array.from(state.clients.values()).some(t => t === 'hook');
|
||||
if (!hasHookNow) {
|
||||
console.log(`\n👋 No hook clients. Auto-exiting hook server (port ${PORT}).`);
|
||||
wss.close();
|
||||
process.exit(0);
|
||||
}
|
||||
}, IDLE_EXIT_MS);
|
||||
}
|
||||
|
||||
// 統計情報の定期出力
|
||||
setInterval(() => {
|
||||
console.log(`📊 Stats: Questions: ${state.questionQueue.length}, Waiting: ${state.waitingCount}, Clients: ${state.clients.size}`);
|
||||
}, 60000);
|
||||
|
||||
// グレースフルシャットダウン
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n👋 Shutting down hook server...');
|
||||
wss.close();
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user