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

439 lines
10 KiB
JavaScript
Raw Normal View History

// codex-claude-bridge.js
// Codex と Claude を自動的に橋渡しするシステム
// 安全装置と制御機能付き
const WebSocket = require('ws');
const fs = require('fs').promises;
const path = require('path');
// 設定
const CONFIG = {
codexWs: 'ws://localhost:8766',
claudeApiUrl: process.env.CLAUDE_API_URL || 'http://localhost:8080/claude', // 要実装
bridgePort: 8768,
// 安全設定
maxBridgesPerHour: 50,
cooldownMs: 5000,
idleTimeoutMs: 30000,
contextWindowSize: 5, // 最後のN個のメッセージを含める
// ログ設定
logDir: './bridge-logs',
enableLogging: true
};
// 検出ボックス
class DetectionBox {
constructor() {
this.patterns = {
question: /\?$|どうしますか|どう思いますか|教えて|どうすれば|何が/,
waiting: /waiting|待機中|入力待ち|▌/i,
stuck: /エラー|失敗|できません|わかりません|困った/,
needHelp: /助けて|ヘルプ|相談|アドバイス/,
planning: /次は|つぎは|計画|予定/
};
this.lastActivity = Date.now();
this.quietPeriods = [];
}
analyze(output) {
const now = Date.now();
const idleTime = now - this.lastActivity;
// 複数のパターンをチェックしてスコアリング
let score = 0;
let reasons = [];
for (const [type, pattern] of Object.entries(this.patterns)) {
if (pattern.test(output)) {
score += 0.3;
reasons.push(type);
}
}
if (idleTime > CONFIG.idleTimeoutMs) {
score += 0.5;
reasons.push('idle');
}
// 最後のアクティビティを更新
this.lastActivity = now;
return {
shouldBridge: score >= 0.5,
confidence: Math.min(score, 1.0),
reasons,
idleTime
};
}
}
// フィルターボックス
class FilterBox {
constructor() {
this.safetyRules = {
blocked: [
/password|secret|token|key|credential/i,
/rm -rf|delete all|destroy|drop database/i,
/private|confidential|機密|秘密/i
],
requireConfirm: [
/production|本番|live environment/i,
/payment|billing|課金|money/i,
/critical|breaking change|重要な変更/i
],
allowed: [
/実装|implement|設計|design|architecture/,
/error|bug|fix|修正|デバッグ/,
/suggest|proposal|提案|アイデア/,
/explain|説明|なぜ|どうして/
]
};
this.contextPatterns = {
jit: /JIT|cranelift|compile|lower/i,
box: /Box|箱|カプセル|Everything is Box/i,
architecture: /設計|アーキテクチャ|構造|structure/i
};
}
filter(content, context = []) {
// 危険なコンテンツチェック
for (const pattern of this.safetyRules.blocked) {
if (pattern.test(content)) {
return {
allow: false,
reason: 'blocked-content',
action: 'reject'
};
}
}
// 確認が必要なコンテンツ
for (const pattern of this.safetyRules.requireConfirm) {
if (pattern.test(content)) {
return {
allow: false,
reason: 'requires-confirmation',
action: 'queue'
};
}
}
// コンテキストスコアリング
let contextScore = 0;
for (const [type, pattern] of Object.entries(this.contextPatterns)) {
if (pattern.test(content)) {
contextScore += 0.3;
}
}
// 許可されたパターン
for (const pattern of this.safetyRules.allowed) {
if (pattern.test(content)) {
return {
allow: true,
confidence: Math.min(0.5 + contextScore, 1.0),
action: 'forward'
};
}
}
// デフォルトは確認待ち
return {
allow: false,
reason: 'no-pattern-match',
action: 'queue'
};
}
}
// ブリッジボックス
class BridgeBox {
constructor() {
this.detection = new DetectionBox();
this.filter = new FilterBox();
this.state = {
active: false,
bridgeCount: 0,
lastBridge: 0,
queue: [],
history: []
};
this.stats = {
total: 0,
forwarded: 0,
blocked: 0,
queued: 0
};
}
async start() {
console.log('🌉 Starting Codex-Claude Bridge...');
// ログディレクトリ作成
if (CONFIG.enableLogging) {
await fs.mkdir(CONFIG.logDir, { recursive: true });
}
// Codexに接続
this.connectToCodex();
// 管理用WebSocketサーバー
this.startControlServer();
this.state.active = true;
console.log('✅ Bridge is active');
}
connectToCodex() {
this.codexWs = new WebSocket(CONFIG.codexWs);
this.codexWs.on('open', () => {
console.log('📡 Connected to Codex');
});
this.codexWs.on('message', async (data) => {
const msg = JSON.parse(data);
if (msg.type === 'codex-event' || msg.type === 'codex-output') {
await this.handleCodexOutput(msg);
}
});
this.codexWs.on('error', (err) => {
console.error('❌ Codex connection error:', err);
});
}
async handleCodexOutput(msg) {
this.stats.total++;
// 停止検出
const detection = this.detection.analyze(msg.data);
if (!detection.shouldBridge) {
return;
}
// フィルタリング
const filterResult = this.filter.filter(msg.data, this.state.history);
if (!filterResult.allow) {
if (filterResult.action === 'queue') {
this.queueForReview(msg, filterResult);
} else {
this.stats.blocked++;
console.log(`🚫 Blocked: ${filterResult.reason}`);
}
return;
}
// クールダウンチェック
if (!this.canBridge()) {
this.queueForReview(msg, { reason: 'cooldown' });
return;
}
// ブリッジ実行
await this.bridge(msg);
}
canBridge() {
const now = Date.now();
// クールダウン
if (now - this.state.lastBridge < CONFIG.cooldownMs) {
return false;
}
// レート制限
const hourAgo = now - 3600000;
const recentBridges = this.state.history.filter(h => h.timestamp > hourAgo);
if (recentBridges.length >= CONFIG.maxBridgesPerHour) {
return false;
}
return true;
}
async bridge(msg) {
console.log('🌉 Bridging to Claude...');
try {
// コンテキスト構築
const context = this.buildContext(msg);
// Claude API呼び出し要実装
const claudeResponse = await this.callClaudeAPI(context);
// Codexに返信
this.sendToCodex(claudeResponse);
// 記録
this.recordBridge(msg, claudeResponse);
this.stats.forwarded++;
this.state.lastBridge = Date.now();
} catch (err) {
console.error('❌ Bridge error:', err);
}
}
buildContext(currentMsg) {
// 最近の履歴を含める
const recentHistory = this.state.history.slice(-CONFIG.contextWindowSize);
return {
current: currentMsg.data,
history: recentHistory.map(h => ({
from: h.from,
content: h.content,
timestamp: h.timestamp
})),
context: {
project: 'Nyash JIT Development',
focus: 'Phase 10.7 - JIT Branch Wiring',
recentTopics: this.extractTopics(recentHistory)
}
};
}
async callClaudeAPI(context) {
// TODO: 実際のClaude API実装
// ここはプレースホルダー
return {
response: "Claude's response would go here",
confidence: 0.9
};
}
sendToCodex(response) {
this.codexWs.send(JSON.stringify({
op: 'send',
data: response.response
}));
}
queueForReview(msg, reason) {
this.state.queue.push({
message: msg,
reason,
timestamp: Date.now()
});
this.stats.queued++;
console.log(`📋 Queued for review: ${reason.reason}`);
}
recordBridge(input, output) {
const record = {
timestamp: Date.now(),
from: 'codex',
to: 'claude',
input: input.data,
output: output.response,
confidence: output.confidence
};
this.state.history.push(record);
this.state.bridgeCount++;
// ログ保存
if (CONFIG.enableLogging) {
this.saveLog(record);
}
}
async saveLog(record) {
const filename = `bridge-${new Date().toISOString().split('T')[0]}.jsonl`;
const filepath = path.join(CONFIG.logDir, filename);
await fs.appendFile(
filepath,
JSON.stringify(record) + '\n'
);
}
extractTopics(history) {
// 最近の話題を抽出
const topics = new Set();
history.forEach(h => {
if (/JIT|cranelift/i.test(h.content)) topics.add('JIT');
if (/box|箱/i.test(h.content)) topics.add('Box Philosophy');
if (/PHI|branch/i.test(h.content)) topics.add('Control Flow');
});
return Array.from(topics);
}
startControlServer() {
// 管理用WebSocketサーバー
const wss = new WebSocket.Server({ port: CONFIG.bridgePort });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const cmd = JSON.parse(data);
switch (cmd.op) {
case 'status':
ws.send(JSON.stringify({
type: 'status',
state: this.state,
stats: this.stats
}));
break;
case 'queue':
ws.send(JSON.stringify({
type: 'queue',
items: this.state.queue
}));
break;
case 'approve':
// キューから承認して転送
if (cmd.id && this.state.queue[cmd.id]) {
const item = this.state.queue[cmd.id];
this.bridge(item.message);
this.state.queue.splice(cmd.id, 1);
}
break;
case 'toggle':
this.state.active = !this.state.active;
ws.send(JSON.stringify({
type: 'toggled',
active: this.state.active
}));
break;
}
});
});
console.log(`🎮 Control server on ws://localhost:${CONFIG.bridgePort}`);
}
}
// メイン
if (require.main === module) {
const bridge = new BridgeBox();
bridge.start();
// グレースフルシャットダウン
process.on('SIGINT', () => {
console.log('\n👋 Shutting down bridge...');
process.exit(0);
});
}
module.exports = { BridgeBox, DetectionBox, FilterBox };