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:
Moe Charm
2025-08-28 12:09:09 +09:00
parent e54561e69f
commit 4e1b595796
133 changed files with 14202 additions and 622 deletions

View File

@ -0,0 +1,148 @@
# 🔄 Codex ↔ Claude 双方向通信ガイド
## 📋 概要
同じWebSocketサーバーhook-serverを使って、CodexとClaudeが双方向に通信できます
## 🎯 仕組み
```
Claude → hook-server (8770) → Codex ✅ (既に動作中)
Codex → hook-server (8770) → Claude ✅ NEW!
```
## 📡 Claudeへの返信方法
### 基本的な返信
```javascript
// ClaudeへWebSocket経由で返信
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
ws.send(JSON.stringify({
source: 'codex', // 送信元を'codex'に
type: 'inject-input', // メッセージタイプ
data: 'ここに返信内容を書く' // 実際のメッセージ
}));
ws.close();
});
```
### 実用的な関数
```javascript
// 再利用可能な返信関数
function replyToClaude(message) {
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
ws.send(JSON.stringify({
source: 'codex',
type: 'inject-input',
data: `[Codex→Claude] ${message}`
}));
console.log('✅ Sent to Claude:', message);
ws.close();
});
ws.on('error', (err) => {
console.error('❌ Error:', err.message);
});
}
// 使用例
replyToClaude('ビルドが完了しました!');
replyToClaude('テストの結果: 成功 10件、失敗 0件');
```
### 進捗通知の例
```javascript
// タスクの進捗を通知
async function notifyProgress(task, status, details) {
const message = `
📊 進捗報告
タスク: ${task}
状態: ${status}
詳細: ${details}
時刻: ${new Date().toLocaleString('ja-JP')}
`;
replyToClaude(message.trim());
}
// 実行例
notifyProgress('Nyashビルド', '完了', 'cargo build --release 成功');
notifyProgress('テスト実行', '進行中', '5/10 完了...');
```
## 🎮 実践例
### 1. コマンド実行結果の返信
```javascript
const { exec } = require('child_process');
// コマンドを実行してClaudeに結果を送る
function executeAndReply(command) {
exec(command, (error, stdout, stderr) => {
if (error) {
replyToClaude(`❌ エラー: ${command}\n${stderr}`);
} else {
replyToClaude(`✅ 成功: ${command}\n出力:\n${stdout}`);
}
});
}
// 使用例
executeAndReply('cargo check');
executeAndReply('ls -la');
```
### 2. ファイル操作の通知
```javascript
const fs = require('fs');
// ファイル作成を通知
function notifyFileCreated(filename, content) {
fs.writeFileSync(filename, content);
replyToClaude(`📄 ファイル作成: ${filename} (${content.length}バイト)`);
}
// ファイル読み込みと返信
function readAndReply(filename) {
try {
const content = fs.readFileSync(filename, 'utf8');
replyToClaude(`📖 ${filename} の内容:\n${content.substring(0, 200)}...`);
} catch (err) {
replyToClaude(`❌ ファイル読み込みエラー: ${filename}`);
}
}
```
## ⚡ クイックテスト
以下のワンライナーでテスト可能:
```javascript
// すぐに試せるテストコード
require('ws').connect('ws://localhost:8770').on('open', function() { this.send(JSON.stringify({ source: 'codex', type: 'inject-input', data: 'Hello Claude! 双方向通信テスト成功!' })); this.close(); });
```
## 📝 注意事項
1. **hook-serverが起動していること**を確認
```bash
lsof -i:8770 # ポートが開いているか確認
```
2. **sourceは必ず'codex'に設定**
- 'claude'にすると自分自身にループバックしてしまう
3. **メッセージ形式を守る**
- JSONで、source, type, dataの3つのフィールドが必須
## 🚀 活用アイデア
- **自動進捗報告**: 長時間かかるタスクの進捗をリアルタイム通知
- **エラー通知**: 問題発生時に即座にClaudeに通知
- **完了通知**: タスク完了時に次の指示を求める
- **質問**: 判断に迷ったときにClaudeに相談
これで、CodexとClaudeが完全に双方向で協調作業できます🎉

View File

@ -0,0 +1,81 @@
# 🚀 クイックスタート - Claude Code ↔ Codex 双方向通信
## 前提条件
- **Claude Code**: `/home/tomoaki/.volta/bin/codex` Claude APIを使うCodex
- **本物のCodex**: 別途パスを設定(制限解除の引数が必要)
## 環境設定
```bash
# 本物のCodexのパスを設定必須
export REAL_CODEX_PATH=/path/to/real/codex
```
## 一括起動(推奨)
```bash
cd /mnt/c/git/nyash-project/nyash
./tools/codex-tmux-driver/start-all.sh
```
## 個別起動
### 1. Hook Server起動
```bash
node tools/codex-tmux-driver/hook-server.js
```
### 2. Claude Codec1起動
```bash
./tools/codex-tmux-driver/start-ai-tmux.sh c1 /home/tomoaki/.volta/bin/codex
```
### 3. 本物のCodexc2起動
```bash
./tools/codex-tmux-driver/start-ai-tmux.sh c2 $REAL_CODEX_PATH --ask-for-approval never --sandbox danger-full-access
```
## メッセージ送信テスト
### Codex → Claude Code
```bash
node tools/codex-tmux-driver/test-bidirectional-claude-codex.js
```
### Claude Code → Codex
```bash
node tools/codex-tmux-driver/test-bidirectional-codex-claude.js
```
## セッション管理
### 接続
```bash
tmux attach -t c1 # Claude Codeに接続
tmux attach -t c2 # 本物のCodexに接続
```
### 終了
```bash
pkill -f hook-server.js
tmux kill-session -t c1
tmux kill-session -t c2
```
## トラブルシューティング
### 本物のCodexが見つからない
```bash
# Codexのパスを確認
which codex
# 環境変数に設定
export REAL_CODEX_PATH=$(which codex)
```
### ポートが使用中
```bash
# 8770ポートを確認
lsof -i:8770
# プロセスを終了
pkill -f hook-server.js
```

View File

@ -0,0 +1,97 @@
# 🌉 Codex-Claude Auto Bridge
## 🎯 機能
CodexとClaudeの間で応答を自動的に橋渡しするシステムです。
### できること
- ✅ Codexの出力を自動検知
- ✅ 出力完了を判定Working状態の終了を検知
- ✅ 応答内容を抽出してファイルに保存
- ✅ Claudeが読める形式で出力
- ✅ tmux経由でCodexにメッセージ送信
## 📦 構成
1. **codex-output-watcher.js** - Codexの画面を監視
2. **codex-claude-auto-bridge.js** - 自動橋渡しシステム
3. **tmux-codex-controller.js** - tmux制御
## 🚀 使い方
### 1. Codexをtmuxで起動
```bash
./tmux-launch-only.sh
```
### 2. 自動ブリッジを起動
```bash
node codex-claude-auto-bridge.js
```
### 3. 最初のメッセージを送る
```bash
node codex-claude-auto-bridge.js "Nyashプロジェクトについて教えて"
```
### 4. Codexの応答を確認
```bash
cat codex-response.txt
```
### 5. 応答を読んで次のメッセージを送る
```bash
tmux send-keys -t codex-safe "次の質問" Enter
```
## 🔄 自動化フロー
```
Claude → メッセージ作成
tmux send-keys → Codexに送信
Codex → 処理中Working...
codex-output-watcher → 完了検知
codex-response.txt → 応答保存
Claude → ファイルを読んで返答
```
## 💡 高度な使い方
### 監視だけする
```javascript
const watcher = new CodexOutputWatcher();
watcher.on('response', (response) => {
console.log('Got response:', response);
});
watcher.start();
```
### プログラムから制御
```javascript
const bridge = new CodexClaudeAutoBridge();
await bridge.start();
await bridge.sendToCodex("質問");
// codex-response.txt に応答が保存される
```
## ⚠️ 注意事項
- Codexが勝手に動作しないよう監視が必要
- tmuxセッションは使用後に必ず終了する
- 応答ファイルは上書きされるので注意
## 🐛 トラブルシューティング
**Q: 応答が検出されない**
A: Working状態が終わるまで待ってください
**Q: 文字化けする**
A: ANSIエスケープシーケンスが含まれている可能性があります
**Q: tmuxエラー**
A: セッション名が正しいか確認してください

View File

@ -0,0 +1,41 @@
# 🎉 Codex Hook 動作確認完了!
## ✅ できること
- hook-serverとcodex-wrapperの接続 → **成功!**
- メッセージの送信と表示 → **成功!**
- 文字入力の自動化 → **成功!**
## ❌ 制限事項
- **Enterキーの自動送信** → Codexの端末処理の関係で不可
- 改行を含む入力 → 同上
## 🎯 実用的な使い方
### 方法1: メッセージ送信 + 手動Enter
```bash
# メッセージを送る
node send-greeting-clean.js
# Codexのターミナルで手動でEnterを押す
```
### 方法2: tmux経由完全自動化
```bash
# tmuxでCodex起動
./start-codex-tmux.sh
# tmux経由でメッセージ送信Enterも送れる
tmux send-keys -t codex-8770 "Hello from Nyash!" Enter
```
### 方法3: Codex-Claude Bridge部分自動化
1. メッセージをCodexに送信自動
2. ユーザーがEnterを押す手動
3. Codexの応答を検出してClaudeに転送自動
## 💡 結論
- **文字入力は自動化できる**が、**実行Enterは手動**
- 完全自動化したい場合は**tmux経由**を使う
- 実用的には「メッセージ準備は自動、実行は手動」で十分
これで十分実用的なCodex-Claudeブリッジが作れるにゃ🐱

View File

@ -0,0 +1,34 @@
# 🎉 Codex Hook 成功!
ついにCodexとhook-serverの連携が成功したにゃ
## 📝 正しい使い方
### 1. hook-serverを起動ポート8770
```bash
HOOK_SERVER_PORT=8770 node tools/codex-tmux-driver/hook-server.js
```
### 2. Codexを起動クリーン版
```bash
./tools/codex-tmux-driver/start-codex-simple.sh
```
### 3. メッセージを送る
```bash
node tools/codex-tmux-driver/send-greeting-clean.js
```
## 🐛 トラブルシューティング
画面がぐちゃぐちゃになったら:
- Codexを再起動して`start-codex-simple.sh`を使う(デバッグ出力なし)
- または環境変数で制御:`export CODEX_HOOK_BANNER=false`
## 🎯 次のステップ
- Claude-Codexブリッジの実装
- 自動応答システムの構築
- フィルタリング機能の追加
やったにゃー!🐱🎉

View File

@ -0,0 +1,328 @@
# Codex tmux Driver
tmux経由でCodexを管理し、イベントをWebSocketで配信するツールです。
Codexからの頻繁な返答を整理・フィルタリングして、ChatGPT5さんとの協調作業を効率化します。
## 🎯 機能
- tmuxセッション内でCodexを実行・管理
- Codexの出力をリアルタイムでWebSocket配信
- パターン認識によるイベント分類response/thinking/error/complete
- フィルタリング機能CodexFilterBoxで重要な情報のみ抽出
- 画面キャプチャ・履歴管理
## 📦 インストール
```bash
cd tools/codex-tmux-driver
npm install
```
## 🚀 使い方
### 1. ドライバ起動
```bash
# 基本起動
node codex-tmux-driver.js
# オプション指定
node codex-tmux-driver.js --session=my-codex --port=8767 --log=/tmp/codex.log
```
### 2. テストクライアント
```bash
# 別ターミナルで
node test-client.js
```
### 3. WebSocket API
```javascript
// 接続
const ws = new WebSocket('ws://localhost:8766');
// Codexに入力送信
ws.send(JSON.stringify({
op: 'send',
data: 'Nyashの箱作戦について教えて'
}));
// 画面キャプチャ
ws.send(JSON.stringify({ op: 'capture' }));
// ステータス確認
ws.send(JSON.stringify({ op: 'status' }));
// 履歴取得
ws.send(JSON.stringify({ op: 'history', count: 20 }));
// イベントフィルタ
ws.send(JSON.stringify({ op: 'filter', event: 'response' }));
```
## 🎁 CodexFilterBox
Codexの出力を分類・フィルタリングする箱です。
```javascript
const CodexFilterBox = require('./codex-filter-box');
const filter = new CodexFilterBox();
// フィルタ実行
const result = filter.filter('Codex: バグ発見!重大な問題があります');
// → { category: 'urgent', priority: 'high', forward: true, ... }
// カスタムルール追加
filter.addRule('nyash-specific', {
patterns: ['箱作戦', 'Everything is Box'],
action: 'forward-to-chatgpt5',
priority: 'medium',
forward: true
});
```
### フィルタカテゴリ
- **urgent**: 緊急対応が必要(バグ、セキュリティ)
- **implementation**: 実装完了通知
- **proposal**: 提案・相談(キューに保存)
- **thinking**: 思考中(ログのみ)
- **ignore**: 無視可能な雑談
## 🔧 設定
### 環境変数
```bash
export CODEX_SESSION=my-codex
export CODEX_PORT=8767
export CODEX_LOG_DIR=/var/log/codex
export CODEX_HOOK_ENTER=crlf # Enter送信方式: lf|cr|crlf (デフォルト: crlf)
export HOOK_SERVER_PORT=8769 # hook-serverのポート
export HOOK_SERVER_AUTO_EXIT=true # 最後のhook切断で自動終了
export HOOK_IDLE_EXIT_MS=2000 # 自動終了までの猶予(ms)
```
### tmuxセッションのカスタマイズ
```javascript
// codex-tmux-driver.js の CODEX_CMD を変更
const CODEX_CMD = argv.cmd || 'codex exec --mode=assistant';
```
## 📊 統計情報
```javascript
// フィルタ統計
const stats = filter.getStats();
console.log(stats);
// → { total: 100, filtered: { urgent: 5, ... }, forwarded: 15, queued: 10 }
```
## 🎯 活用例
### ChatGPT5との連携
```javascript
// Codexの重要な出力のみChatGPT5に転送
ws.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'codex-event') {
const filtered = filter.filter(msg.data);
if (filtered.forward) {
// ChatGPT5のAPIに転送
forwardToChatGPT5(filtered);
}
}
});
```
### 定期レビュー
```javascript
// 1時間ごとにキューを確認
setInterval(() => {
const queue = filter.getQueue();
if (queue.length > 0) {
console.log('📋 Review queue:', queue);
// 必要なものだけChatGPT5に相談
}
}, 3600000);
```
## 🐛 トラブルシューティング
### tmuxセッションが作成できない
```bash
# 既存セッションを確認
tmux ls
# 既存セッションを削除
tmux kill-session -t codex-session
```
### ログファイルが大きくなりすぎる
```bash
# ログローテーション設定
echo "0 * * * * truncate -s 0 /tmp/codex.log" | crontab -
```
## 🌉 Codex-Claude 自動ブリッジ
Codexが止まったときに自動的にClaudeに転送し、応答を返すシステムです。
### 起動方法
```bash
# 1. Codex tmuxドライバを起動
node codex-tmux-driver.js
# 2. 別ターミナルでブリッジを起動
node codex-claude-bridge.js
# 3. ブリッジ制御(別ターミナル)
node bridge-control.js
```
### 単独インスタンス運用tmuxなし・自動終了
```bash
# Aインスタンス用hook-serverバックグラウンド、自動終了有効
HOOK_SERVER_PORT=8769 HOOK_SERVER_AUTO_EXIT=true \
nohup node tools/codex-tmux-driver/hook-server.js >/tmp/hook-A.log 2>&1 &
# AインスタンスのCodex同ターミナル
export CODEX_HOOK_SERVER=ws://localhost:8769
export CODEX_LOG_FILE=/tmp/codex-A.log
codex exec --ask-for-approval never --sandbox danger-full-access
# ← Codex終了時にhook-serverも自動終了
```
### ブリッジの仕組み
```
Codex停止 → 検出 → フィルタ → Claude API → Codexに返信
```
### 安全機能
- **レート制限**: 1時間に最大50回
- **クールダウン**: 5秒間隔
- **フィルタリング**: 危険なコマンドをブロック
- **確認キュー**: 重要な操作は人間確認
### 制御コマンド
```
status - ブリッジの状態確認
queue - 保留中の項目表示
approve N - キューのN番目を承認
toggle - ブリッジのON/OFF
```
### 設定(環境変数)
```bash
export CLAUDE_API_URL=http://localhost:8080/claude
export BRIDGE_MAX_PER_HOUR=30
export BRIDGE_COOLDOWN_MS=10000
```
## 🚀 NEW! 同一hook-server双方向通信
同じhook-serverを使って、CodexからClaudeへの返信も可能に
### 仕組み
```
Claude → hook-server → Codex既存
Codex → hook-server → Claude新機能
同じWebSocketで双方向通信が実現
```
### Codexから返信する方法
1. **Codex側でWebSocketクライアントを作成**
```javascript
// Codex側のコード
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
// Claudeへメッセージを送信
ws.send(JSON.stringify({
source: 'codex',
type: 'inject-input',
data: 'Claudeさん、処理が完了しました結果は...'
}));
```
2. **hook-serverが自動的にリレー**
- sourceが'codex'のメッセージを検出
- 'claude'タイプのクライアントに転送
- Claudeの画面に表示される
### 実装例:作業完了通知
```javascript
// Codex側作業完了時に自動通知
function notifyClaude(message) {
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
ws.send(JSON.stringify({
source: 'codex',
type: 'inject-input',
data: message
}));
ws.close();
});
}
// 使用例
notifyClaude('ビルドが完了しましたエラー0件、警告2件です。');
```
### tmux-perfect-bridgeとの統合
```javascript
// 完全自動双方向ブリッジ同一hook-server版
class UnifiedBridge {
constructor() {
this.hookServer = 'ws://localhost:8770';
}
// Codexの出力を監視してClaudeへ転送
async watchCodexOutput() {
const output = await this.captureCodexPane();
if (this.isComplete(output)) {
this.sendToClaude(output);
}
}
// hook-server経由で送信
sendToClaude(message) {
const ws = new WebSocket(this.hookServer);
ws.send(JSON.stringify({
source: 'codex',
type: 'inject-input',
data: message
}));
}
}
```
## 📝 今後の拡張
- [x] Codex-Claudeブリッジ
- [x] 双方向通信同一hook-server
- [ ] 複数Codexセッション管理
- [ ] フィルタルールの永続化JSON/YAML
- [ ] Web UIダッシュボード
- [ ] プラグインシステム(カスタムフィルタ)
- [ ] メトリクス出力Prometheus形式
---
Codexさんの頻繁な返答も、箱作戦で整理すれば怖くない🎁
そして今や、Codexからも返事ができるように🔄

View File

@ -0,0 +1,39 @@
# 🚀 超シンプルテスト手順
## 1⃣ hook-server起動まだなら
```bash
cd /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver
HOOK_SERVER_PORT=8770 node hook-server.js
```
## 2⃣ Claude Codeから送信テスト
### 方法A: ワンライナー(一番簡単)
```javascript
require('ws').connect('ws://localhost:8770').on('open', function() { this.send(JSON.stringify({ source: 'claude-test', type: 'inject-input', data: 'テスト成功!' })); this.close(); });
```
### 方法B: 分かりやすい版
```javascript
const ws = require('ws');
const client = new ws('ws://localhost:8770');
client.on('open', () => {
client.send(JSON.stringify({
source: 'claude',
type: 'inject-input',
data: 'Hello! WebSocketテスト成功'
}));
client.close();
});
```
## 3⃣ 確認方法
hook-serverのターミナルに以下が表示されれば成功
```
[inject-input] Hello! WebSocketテスト成功
🔄 Relaying inject-input from hook client
```
## 🎯 成功したら
同じ方法でCodexからも送信できます
sourceを'codex'に変えるだけ!

View File

@ -0,0 +1,122 @@
// bridge-control.js
// Codex-Claudeブリッジの制御用CLI
const WebSocket = require('ws');
const readline = require('readline');
const ws = new WebSocket('ws://localhost:8768');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'Bridge> '
});
// 接続時の処理
ws.on('open', () => {
console.log('🌉 Connected to Codex-Claude Bridge');
console.log('Commands:');
console.log(' status - Show bridge status');
console.log(' queue - Show pending items');
console.log(' approve N - Approve queue item N');
console.log(' toggle - Enable/disable bridge');
console.log(' exit - Quit');
console.log('');
// 初期ステータス取得
ws.send(JSON.stringify({ op: 'status' }));
rl.prompt();
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'status':
console.log('\n📊 Bridge Status:');
console.log(` Active: ${msg.state.active ? '✅' : '❌'}`);
console.log(` Bridges: ${msg.state.bridgeCount}`);
console.log(` Queue: ${msg.state.queue.length} items`);
console.log('\n📈 Statistics:');
console.log(` Total: ${msg.stats.total}`);
console.log(` Forwarded: ${msg.stats.forwarded}`);
console.log(` Blocked: ${msg.stats.blocked}`);
console.log(` Queued: ${msg.stats.queued}`);
break;
case 'queue':
console.log('\n📋 Pending Queue:');
if (msg.items.length === 0) {
console.log(' (empty)');
} else {
msg.items.forEach((item, idx) => {
console.log(` [${idx}] ${item.reason.reason}`);
console.log(` "${item.message.data.substring(0, 50)}..."`);
console.log(` ${new Date(item.timestamp).toLocaleTimeString()}`);
});
}
break;
case 'toggled':
console.log(`\n🔄 Bridge is now ${msg.active ? 'ACTIVE' : 'INACTIVE'}`);
break;
default:
console.log(`\n[${msg.type}]`, msg);
}
rl.prompt();
});
ws.on('error', (err) => {
console.error('❌ Connection error:', err.message);
process.exit(1);
});
ws.on('close', () => {
console.log('\n🔌 Disconnected');
process.exit(0);
});
// コマンド処理
rl.on('line', (line) => {
const parts = line.trim().split(' ');
const cmd = parts[0];
switch (cmd) {
case 'status':
ws.send(JSON.stringify({ op: 'status' }));
break;
case 'queue':
ws.send(JSON.stringify({ op: 'queue' }));
break;
case 'approve':
const id = parseInt(parts[1]);
if (!isNaN(id)) {
ws.send(JSON.stringify({ op: 'approve', id }));
console.log(`✅ Approving item ${id}...`);
} else {
console.log('❌ Usage: approve <number>');
}
break;
case 'toggle':
ws.send(JSON.stringify({ op: 'toggle' }));
break;
case 'exit':
case 'quit':
ws.close();
rl.close();
return;
default:
if (cmd) {
console.log(`❓ Unknown command: ${cmd}`);
}
}
rl.prompt();
});

View File

@ -0,0 +1,189 @@
// claude-codex-unified-bridge.js
// 同一hook-serverを使った完璧な双方向ブリッジ
const { spawn } = require('child_process');
const WebSocket = require('ws');
const EventEmitter = require('events');
class ClaudeCodexUnifiedBridge extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
hookServer: config.hookServer || 'ws://localhost:8770',
claudeSession: config.claudeSession || 'claude-8771',
codexSession: config.codexSession || 'codex-safe',
watchInterval: config.watchInterval || 500,
...config
};
this.ws = null;
this.isRunning = false;
this.lastClaudeOutput = '';
this.lastCodexOutput = '';
}
// ブリッジ開始
async start() {
console.log('🌉 Starting Claude-Codex Unified Bridge...');
console.log('📡 Hook Server:', this.config.hookServer);
// WebSocket接続
await this.connectToHookServer();
// 監視開始
this.isRunning = true;
this.startWatching();
console.log('✅ Bridge is running!');
}
// hook-serverに接続
connectToHookServer() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.config.hookServer);
this.ws.on('open', () => {
console.log('✅ Connected to hook-server');
// ブリッジとして登録
this.ws.send(JSON.stringify({
source: 'bridge',
type: 'register',
data: 'claude-codex-bridge'
}));
resolve();
});
this.ws.on('error', (err) => {
console.error('❌ WebSocket error:', err);
reject(err);
});
this.ws.on('close', () => {
console.log('🔌 Disconnected from hook-server');
this.isRunning = false;
});
});
}
// 監視ループ
startWatching() {
const watchLoop = setInterval(async () => {
if (!this.isRunning) {
clearInterval(watchLoop);
return;
}
try {
// Codexの出力をチェック
await this.checkCodexOutput();
// Claudeの出力もチェック必要に応じて
// await this.checkClaudeOutput();
} catch (err) {
console.error('❌ Watch error:', err);
}
}, this.config.watchInterval);
}
// Codexの出力をチェック
async checkCodexOutput() {
const output = await this.capturePane(this.config.codexSession);
// 新しい内容があるかチェック
if (output !== this.lastCodexOutput) {
const newContent = this.extractNewContent(output, this.lastCodexOutput);
if (newContent && this.isCodexResponse(newContent)) {
console.log('📨 Codex response detected!');
// Claudeに転送
this.sendToClaude(newContent);
this.lastCodexOutput = output;
}
}
}
// Claudeにメッセージを送信hook-server経由
sendToClaude(message) {
console.log('📤 Sending to Claude via hook-server...');
const payload = {
source: 'codex',
type: 'inject-input',
data: `[Codex Response]\n${message}`
};
this.ws.send(JSON.stringify(payload));
this.emit('codex-to-claude', message);
}
// tmuxペインをキャプチャ
capturePane(sessionName) {
return new Promise((resolve, reject) => {
const proc = spawn('tmux', ['capture-pane', '-t', 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}`));
}
});
});
}
// 新しいコンテンツを抽出
extractNewContent(current, previous) {
if (current.length > previous.length) {
return current.substring(previous.length).trim();
}
return null;
}
// Codexの応答かどうか判定
isCodexResponse(text) {
// Working状態でない、プロンプトでない、十分な長さ
return !text.includes('Working') &&
!text.includes('▌') &&
text.length > 20 &&
!text.includes('⏎ send');
}
// 停止
stop() {
this.isRunning = false;
if (this.ws) {
this.ws.close();
}
console.log('🛑 Bridge stopped');
}
}
// メイン実行
if (require.main === module) {
const bridge = new ClaudeCodexUnifiedBridge();
// イベントリスナー
bridge.on('codex-to-claude', (content) => {
console.log('📊 Transferred to Claude:', content.substring(0, 50) + '...');
});
// 開始
bridge.start().catch(console.error);
// 終了処理
process.on('SIGINT', () => {
console.log('\n👋 Shutting down...');
bridge.stop();
process.exit(0);
});
}
module.exports = ClaudeCodexUnifiedBridge;

View File

@ -0,0 +1,178 @@
#!/usr/bin/env node
// claude-hook-wrapper.js
// Claudeバイナリにフックをかけて入出力を横取りするラッパー
const { spawn } = require('child_process');
const path = require('path');
let WebSocket;
try {
WebSocket = require('ws');
} catch (e) {
console.error('FATAL: Cannot find module "ws"');
console.error('Hint: run "npm install" inside tools/codex-tmux-driver');
process.exit(1);
}
const fs = require('fs');
// 設定
const REAL_CLAUDE = process.env.CLAUDE_REAL_BIN || '/home/tomoaki/.volta/tools/image/node/22.16.0/bin/claude';
const HOOK_SERVER = process.env.CLAUDE_HOOK_SERVER || 'ws://localhost:8770';
const LOG_FILE = process.env.CLAUDE_LOG_FILE || '/tmp/claude-hook.log';
const ENABLE_HOOK = process.env.CLAUDE_HOOK_ENABLE !== 'false';
const USE_SCRIPT_PTY = process.env.CLAUDE_USE_SCRIPT_PTY !== 'false'; // デフォルトでPTY有効
// WebSocket接続
let ws = null;
if (ENABLE_HOOK) {
console.error(`[claude-hook] Attempting to connect to ${HOOK_SERVER}...`);
try {
ws = new WebSocket(HOOK_SERVER);
ws.on('open', () => {
log('hook-connect', { url: HOOK_SERVER });
console.error(`[claude-hook] ✅ Successfully connected to ${HOOK_SERVER}`);
});
ws.on('error', (e) => {
console.error(`[claude-hook] ❌ Connection error: ${e?.message || e}`);
});
ws.on('close', () => {
console.error(`[claude-hook] 🔌 Connection closed`);
});
} catch (e) {
console.error(`[claude-hook] ❌ Failed to create WebSocket: ${e}`);
}
}
// ログ関数
function log(type, data) {
const timestamp = new Date().toISOString();
const logEntry = { timestamp, type, data };
// ファイルログ
fs.appendFileSync(LOG_FILE, JSON.stringify(logEntry) + '\n');
// WebSocket送信
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(logEntry));
}
}
// Claudeプロセス起動
if (!fs.existsSync(REAL_CLAUDE)) {
console.error(`FATAL: REAL_CLAUDE not found: ${REAL_CLAUDE}`);
console.error('Set CLAUDE_REAL_BIN to the real Claude binary path.');
process.exit(1);
}
// 引数を渡してClaude起動
const userArgs = process.argv.slice(2);
// script(1) を使って擬似TTY経由で起動Claudeはインタラクティブモードに必要
function shEscape(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
let claudeProcess;
let usingPty = false;
try {
if (USE_SCRIPT_PTY) {
const cmdStr = [REAL_CLAUDE, ...userArgs].map(shEscape).join(' ');
// -q: quiet, -f: flush, -e: return child exit code, -c: command
claudeProcess = spawn('script', ['-qfec', cmdStr, '/dev/null'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env
});
usingPty = true;
log('start-info', { mode: 'pty(script)', cmd: cmdStr });
}
} catch (e) {
// フォールバック
console.error(`[claude-hook] PTY spawn failed: ${e.message}`);
}
if (!claudeProcess) {
claudeProcess = spawn(REAL_CLAUDE, userArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env
});
log('start-info', { mode: 'pipe', bin: REAL_CLAUDE, args: userArgs });
}
// 標準入力をClaudeへ
process.stdin.on('data', (chunk) => {
const input = chunk.toString();
log('input', input);
claudeProcess.stdin.write(chunk);
});
// 標準出力
let outputBuffer = '';
claudeProcess.stdout.on('data', (chunk) => {
const data = chunk.toString();
outputBuffer += data;
// 改行で区切って出力をログ
if (data.includes('\n')) {
log('output', outputBuffer);
outputBuffer = '';
}
process.stdout.write(chunk);
});
// エラー出力
claudeProcess.stderr.on('data', (chunk) => {
log('error', chunk.toString());
process.stderr.write(chunk);
});
// プロセス終了
claudeProcess.on('exit', (code, signal) => {
log('exit', { code, signal });
if (ws) {
try {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'claude-exit',
data: { code, signal }
}));
} catch {}
ws.close();
}
process.exit(typeof code === 'number' ? code : 0);
});
// シグナルハンドリング
process.on('SIGINT', () => {
claudeProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
claudeProcess.kill('SIGTERM');
});
// WebSocketからのメッセージ受信
if (ws) {
ws.on('message', (data) => {
try {
const cmd = JSON.parse(data.toString());
if (cmd.type === 'inject-input') {
// Claudeに入力を注入
log('inject', cmd.data);
claudeProcess.stdin.write(cmd.data + '\n');
}
} catch (e) {
console.error(`[claude-hook] ❌ Error parsing message: ${e}`);
}
});
}
// 起動ログ
log('start', {
args: userArgs,
pid: process.pid,
hookEnabled: ENABLE_HOOK,
usingPty
});
console.error(`[claude-hook] active (pty=${usingPty ? 'on' : 'off'}) REAL_CLAUDE=${REAL_CLAUDE}`);

View File

@ -0,0 +1,67 @@
// claude-tmux-controller.js
// Claudeもtmuxで制御完璧な双方向通信
const { spawn } = require('child_process');
class ClaudeTmuxController {
constructor(sessionName = 'claude-8771') {
this.sessionName = sessionName;
}
// tmuxセッションでClaudeを起動
async start() {
console.log('🤖 Starting Claude in tmux...');
// 既存セッションを削除
await this.exec('tmux', ['kill-session', '-t', this.sessionName]).catch(() => {});
// 新しいセッションでclaude cliを起動
const cmd = [
'new-session', '-d', '-s', this.sessionName,
'claude' // claude CLI仮定
];
await this.exec('tmux', cmd);
console.log(`✅ Claude started in tmux session: ${this.sessionName}`);
await this.sleep(2000);
}
// tmux経由でテキストとEnterを送信
async sendMessage(text) {
console.log(`📤 Sending to Claude: "${text}"`);
await this.exec('tmux', ['send-keys', '-t', this.sessionName, text, 'Enter']);
}
// 画面をキャプチャ
async capture() {
const result = await this.exec('tmux', ['capture-pane', '-t', this.sessionName, '-p']);
return result.stdout;
}
// ヘルパー関数
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) {
reject(new Error(`${command} exited with code ${code}: ${stderr}`));
} else {
resolve({ stdout, stderr });
}
});
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = ClaudeTmuxController;

View File

@ -0,0 +1,143 @@
# 🚀 Claude Code × tmux セットアップ完全ガイド
## 📋 概要
Claude Codeもtmuxで動かすことで、Codexとの完璧な双方向通信を実現します
## 🎯 手順1から
### 1⃣ 現在のClaude Codeを終了
```bash
# 現在のセッションを保存して終了
exit
```
### 2⃣ tmuxセッションでClaude Codeを起動
```bash
# 新しいtmuxセッションを作成名前: claude-8771
tmux new-session -d -s claude-8771
# Claude Codeをtmuxセッションで起動
tmux send-keys -t claude-8771 "cd /mnt/c/git/nyash-project/nyash" Enter
tmux send-keys -t claude-8771 "claude" Enter
# セッションにアタッチして作業
tmux attach -t claude-8771
```
### 3⃣ hook-serverを起動別ターミナル
```bash
# 新しいターミナルを開いて
cd /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver
HOOK_SERVER_PORT=8770 node hook-server.js
```
### 4⃣ Codexをtmuxで起動さらに別ターミナル
```bash
# 既存のスクリプトを使用
cd /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver
./tmux-launch-only.sh
```
### 5⃣ 双方向ブリッジを起動(さらに別ターミナル)
```bash
cd /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver
node claude-codex-unified-bridge.js
```
## 🔄 完成図
```
┌─────────────────┐
│ Terminal 1 │
│ tmux: claude │ ←──┐
└─────────────────┘ │
┌─────────────────┐ │ ┌──────────────┐
│ Terminal 2 │ ├────┤ hook-server │
│ hook-server │ │ │ port: 8770 │
└─────────────────┘ │ └──────────────┘
┌─────────────────┐ │
│ Terminal 3 │ │
│ tmux: codex │ ←──┘
└─────────────────┘
双方向自動通信!
```
## 💡 使い方
### Claude → Codex従来通り
```javascript
// Claude Code内で実行
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.send(JSON.stringify({
source: 'claude',
type: 'inject-input',
data: 'Hello Codex!'
}));
```
### Codex → Claude新機能
Codexが自動的にClaudeに返信しますunified-bridgeが処理
## 🎮 tmux基本操作
```bash
# セッション一覧
tmux ls
# セッションにアタッチ
tmux attach -t claude-8771
# デタッチ(セッションから抜ける)
Ctrl+B, D
# セッション削除
tmux kill-session -t claude-8771
# 画面分割(横)
Ctrl+B, "
# 画面分割(縦)
Ctrl+B, %
# ペーン間移動
Ctrl+B, 矢印キー
```
## ⚠️ 注意事項
1. **tmuxセッション名の重複**
- claude-8771, codex-safe は固定名なので重複注意
2. **ポート番号**
- 8770: hook-server固定
- 変更する場合は全ての設定を統一
3. **終了時の手順**
1. ブリッジを停止Ctrl+C
2. hook-serverを停止Ctrl+C
3. tmuxセッションを終了
## 🚨 トラブルシューティング
**Q: セッションが既に存在する**
```bash
tmux kill-session -t claude-8771
tmux kill-session -t codex-safe
```
**Q: hook-serverに接続できない**
```bash
# プロセスを確認
ps aux | grep "node.*hook-server"
# 強制終了
pkill -f "node.*hook-server"
```
**Q: メッセージが届かない**
- hook-serverのログを確認
- WebSocketの接続状態を確認
- tmuxセッション名が正しいか確認

View File

@ -0,0 +1,82 @@
// claude-to-claude-test.js
// Claude Code同士の双方向通信テスト
const WebSocket = require('ws');
// テスト1: 送信側として動作
function testAsSender(message = 'Hello from Claude A!') {
console.log('📤 送信テスト開始...');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
const payload = {
source: 'claude-a',
type: 'inject-input',
data: `[Claude A → Claude B] ${message}`
};
ws.send(JSON.stringify(payload));
console.log('✅ メッセージ送信成功:', message);
ws.close();
});
ws.on('error', (err) => {
console.error('❌ エラー:', err.message);
});
}
// テスト2: 受信側として動作
function testAsReceiver() {
console.log('📥 受信待機開始...');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✅ hook-serverに接続しました');
// 自分を受信者として登録
ws.send(JSON.stringify({
source: 'claude-b',
type: 'register',
data: 'receiver'
}));
});
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
console.log('📨 受信:', msg);
// Claude Aからのメッセージの場合、返信
if (msg.source === 'claude-a') {
console.log('💬 返信を送信...');
ws.send(JSON.stringify({
source: 'claude-b',
type: 'inject-input',
data: '[Claude B → Claude A] メッセージ受信しました!'
}));
}
});
ws.on('error', (err) => {
console.error('❌ エラー:', err.message);
});
}
// コマンドライン引数で動作モードを選択
const mode = process.argv[2] || 'send';
if (mode === 'send') {
const message = process.argv.slice(3).join(' ') || 'テストメッセージ';
testAsSender(message);
} else if (mode === 'receive') {
testAsReceiver();
} else {
console.log(`
使い方:
node claude-to-claude-test.js send [メッセージ] # 送信モード
node claude-to-claude-test.js receive # 受信モード
`);
}

View File

@ -0,0 +1,146 @@
// codex-claude-auto-bridge.js
// CodexとClaudeを自動で橋渡しするシステム
const fs = require('fs').promises;
const path = require('path');
const TmuxCodexController = require('./tmux-codex-controller');
const CodexOutputWatcher = require('./codex-output-watcher');
class CodexClaudeAutoBridge {
constructor(config = {}) {
this.config = {
sessionName: config.sessionName || 'codex-safe',
outputFile: config.outputFile || './codex-response.txt',
logFile: config.logFile || './bridge.log',
watchInterval: config.watchInterval || 500,
...config
};
this.controller = new TmuxCodexController(this.config.sessionName);
this.watcher = new CodexOutputWatcher(this.config.sessionName);
this.isRunning = false;
}
// ブリッジを開始
async start() {
console.log('🌉 Starting Codex-Claude Auto Bridge...');
this.isRunning = true;
// 出力ウォッチャーのイベント設定
this.watcher.on('response', async (response) => {
await this.handleCodexResponse(response);
});
this.watcher.on('ready', () => {
console.log('💚 Codex is ready for next input');
});
// 監視開始
this.watcher.start(this.config.watchInterval);
await this.log('Bridge started');
}
// Codexの応答を処理
async handleCodexResponse(response) {
console.log('\n📝 Got Codex response!');
// 応答をファイルに保存Claudeが読めるように
await this.saveResponse(response);
// ログに記録
await this.log(`Codex response: ${response.substring(0, 100)}...`);
// 通知
console.log('✅ Response saved to:', this.config.outputFile);
console.log('📢 Please read the response file and send next message to Codex!');
// 自動応答モードの場合(オプション)
if (this.config.autoReply) {
await this.sendAutoReply();
}
}
// 応答をファイルに保存
async saveResponse(response) {
const timestamp = new Date().toISOString();
const content = `=== Codex Response at ${timestamp} ===\n\n${response}\n\n`;
await fs.writeFile(this.config.outputFile, content);
}
// Codexにメッセージを送信
async sendToCodex(message) {
console.log(`📤 Sending to Codex: "${message}"`);
await this.controller.sendKeys(message, true); // Enterも送る
await this.log(`Sent to Codex: ${message}`);
}
// 自動応答(実験的)
async sendAutoReply() {
// 簡単な自動応答ロジック
const replies = [
"なるほど!それについてもう少し詳しく教えて",
"いい感じだにゃ!次はどうする?",
"了解!他に何か提案はある?"
];
const reply = replies[Math.floor(Math.random() * replies.length)];
console.log(`🤖 Auto-replying in 3 seconds: "${reply}"`);
setTimeout(async () => {
await this.sendToCodex(reply);
}, 3000);
}
// ログ記録
async log(message) {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}\n`;
await fs.appendFile(this.config.logFile, logEntry);
}
// 停止
stop() {
this.watcher.stop();
this.isRunning = false;
console.log('🛑 Bridge stopped');
}
}
// CLIとして使う場合
if (require.main === module) {
const bridge = new CodexClaudeAutoBridge({
outputFile: './codex-response.txt',
autoReply: false // 自動応答は無効
});
// 引数からメッセージを取得
const initialMessage = process.argv.slice(2).join(' ');
async function run() {
// ブリッジ開始
await bridge.start();
// 初期メッセージがあれば送信
if (initialMessage) {
console.log('📨 Sending initial message...');
await bridge.sendToCodex(initialMessage);
} else {
console.log('💡 Send a message to Codex using:');
console.log(' tmux send-keys -t codex-safe "your message" Enter');
}
// Ctrl+Cで終了
process.on('SIGINT', () => {
console.log('\n👋 Shutting down...');
bridge.stop();
process.exit(0);
});
}
run().catch(console.error);
}
module.exports = CodexClaudeAutoBridge;

View File

@ -0,0 +1,438 @@
// 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 };

View File

@ -0,0 +1,199 @@
// codex-filter-box.js
// Codexの出力をフィルタリング・分類する箱
// ChatGPT5さんへの転送判断なども行う
class CodexFilterBox {
constructor() {
// フィルタルール設定
this.rules = {
// 緊急度の判定
urgent: {
patterns: [
/緊急|urgent|critical|重大/i,
/バグ発見|bug found|error detected/i,
/セキュリティ|security issue/i
],
action: 'notify-immediately',
priority: 'high',
forward: true
},
// 実装完了の通知
implementation: {
patterns: [
/実装完了|implementation complete/i,
/機能追加|feature added/i,
/修正完了|fixed|resolved/i
],
action: 'forward-to-chatgpt5',
priority: 'medium',
forward: true
},
// 提案・相談
proposal: {
patterns: [
/提案|suggestion|proposal/i,
/どうでしょう|how about/i,
/検討|consider/i
],
action: 'queue-for-review',
priority: 'low',
forward: false,
queue: true
},
// 思考中・処理中
thinking: {
patterns: [
/考え中|thinking|processing/i,
/分析中|analyzing/i,
/調査中|investigating/i
],
action: 'log-only',
priority: 'info',
forward: false
},
// 雑談・無視可能
ignore: {
patterns: [
/雑談|small talk/i,
/ところで|by the way/i,
/関係ない|unrelated/i
],
action: 'archive',
priority: 'ignore',
forward: false
}
};
// 統計情報
this.stats = {
total: 0,
filtered: {},
forwarded: 0,
queued: 0
};
// キュー(後で確認用)
this.queue = [];
}
// メインのフィルタ処理
filter(codexOutput) {
this.stats.total++;
// 各ルールをチェック
for (const [category, rule] of Object.entries(this.rules)) {
if (this.matchesRule(codexOutput, rule)) {
this.stats.filtered[category] = (this.stats.filtered[category] || 0) + 1;
const result = {
category,
action: rule.action,
priority: rule.priority,
forward: rule.forward,
timestamp: new Date().toISOString(),
original: codexOutput
};
// キューに追加
if (rule.queue) {
this.queue.push(result);
this.stats.queued++;
}
// 転送フラグ
if (rule.forward) {
this.stats.forwarded++;
}
return result;
}
}
// どのルールにも一致しない場合
return {
category: 'default',
action: 'log',
priority: 'normal',
forward: false,
timestamp: new Date().toISOString(),
original: codexOutput
};
}
// ルールマッチング
matchesRule(text, rule) {
return rule.patterns.some(pattern => pattern.test(text));
}
// キューから項目取得
getQueue(count = 10) {
return this.queue.slice(-count);
}
// キューをクリア
clearQueue() {
const cleared = this.queue.length;
this.queue = [];
return cleared;
}
// 統計情報取得
getStats() {
return {
...this.stats,
queueLength: this.queue.length,
categories: Object.keys(this.rules)
};
}
// カスタムルール追加
addRule(name, config) {
this.rules[name] = {
patterns: config.patterns.map(p =>
typeof p === 'string' ? new RegExp(p, 'i') : p
),
action: config.action || 'log',
priority: config.priority || 'normal',
forward: config.forward || false,
queue: config.queue || false
};
}
// バッチ処理
filterBatch(outputs) {
return outputs.map(output => this.filter(output));
}
}
// エクスポート
module.exports = CodexFilterBox;
// 使用例
if (require.main === module) {
const filter = new CodexFilterBox();
// テストデータ
const testOutputs = [
'Codex: バグ発見!メモリリークが発生しています',
'考え中... JITの最適化方法を検討しています',
'ところで、今日の天気はどうですか?',
'実装完了: Phase 10.7のPHI実装が完成しました',
'提案: 箱作戦をさらに拡張してはどうでしょう?'
];
console.log('=== Codex Filter Box Test ===\n');
testOutputs.forEach(output => {
const result = filter.filter(output);
console.log(`Input: "${output}"`);
console.log(`Result: ${result.category} - ${result.action} (${result.priority})`);
console.log(`Forward to ChatGPT5: ${result.forward ? 'YES' : 'NO'}`);
console.log('---');
});
console.log('\nStats:', filter.getStats());
}

View File

@ -0,0 +1,291 @@
#!/usr/bin/env node
// codex-hook-wrapper.js
// Codexバイナリにフックをかけて入出力を横取りするラッパー
// 使い方: このファイルを codex として PATH に配置
const { spawn } = require('child_process');
const path = require('path');
let WebSocket;
try {
WebSocket = require('ws');
} catch (e) {
console.error('FATAL: Cannot find module "ws"');
console.error('Hint: run "npm install" inside tools/codex-tmux-driver, or ensure the wrapper is symlinked to that directory.');
process.exit(1);
}
const fs = require('fs');
// 設定
// 実バイナリは環境変数で上書き可能。未設定かつ存在しない場合はエラーにする。
const REAL_CODEX = process.env.CODEX_REAL_BIN || '/home/tomoaki/.volta/tools/image/packages/@openai/codex/lib/node_modules/@openai/codex/bin/codex-x86_64-unknown-linux-musl';
const HOOK_SERVER = process.env.CODEX_HOOK_SERVER || 'ws://localhost:8770';
const LOG_FILE = process.env.CODEX_LOG_FILE || '/tmp/codex-hook.log';
const ENTER_MODE = (process.env.CODEX_HOOK_ENTER || 'crlf').toLowerCase(); // lf|cr|crlf
const ENABLE_HOOK = process.env.CODEX_HOOK_ENABLE !== 'false';
const USE_SCRIPT_PTY = process.env.CODEX_USE_SCRIPT_PTY === 'true'; // default false
const SHOW_BANNER = process.env.CODEX_HOOK_BANNER !== 'false';
const ECHO_INJECT = process.env.CODEX_HOOK_ECHO_INJECT === 'true';
const PRE_NEWLINE = process.env.CODEX_HOOK_PRENEWLINE === 'true';
const INJECT_PREFIX = process.env.CODEX_HOOK_INJECT_PREFIX || '';
const INJECT_SUFFIX = process.env.CODEX_HOOK_INJECT_SUFFIX || '';
// WebSocket接続オプショナル
let ws = null;
if (ENABLE_HOOK) {
console.error(`[codex-hook] Attempting to connect to ${HOOK_SERVER}...`);
try {
ws = new WebSocket(HOOK_SERVER);
ws.on('open', () => {
// 目印ログ(接続先確認)
log('hook-connect', { url: HOOK_SERVER });
console.error(`[codex-hook] ✅ Successfully connected to ${HOOK_SERVER}`);
});
ws.on('error', (e) => {
// 接続エラーは無視(フォールバック動作)
console.error(`[codex-hook] ❌ Connection error: ${e?.message || e}`);
});
ws.on('close', () => {
console.error(`[codex-hook] 🔌 Connection closed`);
});
} catch (e) {
// WebSocketサーバーが起動していない場合は通常動作
console.error(`[codex-hook] ❌ Failed to create WebSocket: ${e}`);
}
}
// ログ関数
function log(type, data) {
const timestamp = new Date().toISOString();
const logEntry = { timestamp, type, data };
// ファイルログ
fs.appendFileSync(LOG_FILE, JSON.stringify(logEntry) + '\n');
// WebSocket送信
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(logEntry));
}
}
// Codexプロセス起動
if (!fs.existsSync(REAL_CODEX)) {
console.error(`FATAL: REAL_CODEX not found: ${REAL_CODEX}`);
console.error('Set CODEX_REAL_BIN to the real Codex binary path.');
process.exit(1);
}
// 引数決定(無指定時は既定コマンドを許可)
let userArgs = process.argv.slice(2);
const DEFAULT_CMD = process.env.CODEX_WRAPPER_DEFAULT_CMD; // 例: "exec --ask-for-approval never"
if (userArgs.length === 0 && DEFAULT_CMD) {
try {
userArgs = DEFAULT_CMD.split(' ').filter(Boolean);
if (SHOW_BANNER) {
console.error(`[codex-hook] using default cmd: ${DEFAULT_CMD}`);
}
} catch {}
}
// script(1) を使って擬似TTY経由で起動インジェクションを確実に通すため
function shEscape(s) { return `'${String(s).replace(/'/g, `'\''`)}'`; }
let codexProcess;
let usingPty = false;
try {
if (USE_SCRIPT_PTY) {
const cmdStr = [REAL_CODEX, ...userArgs].map(shEscape).join(' ');
// -q: quiet, -f: flush, -e: return child exit code, -c: command
codexProcess = spawn('script', ['-qfec', cmdStr, '/dev/null'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env
});
usingPty = true;
log('start-info', { mode: 'pty(script)', cmd: cmdStr });
}
} catch (e) {
// フォールバック
}
if (!codexProcess) {
codexProcess = spawn(REAL_CODEX, userArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env
});
log('start-info', { mode: 'pipe', bin: REAL_CODEX, args: process.argv.slice(2) });
}
// 入力フック(標準入力 → Codex
let inputBuffer = '';
process.stdin.on('data', (chunk) => {
const data = chunk.toString();
inputBuffer += data;
// 改行で区切って入力を記録
if (data.includes('\n')) {
const lines = inputBuffer.split('\n');
inputBuffer = lines.pop() || '';
lines.forEach(line => {
if (line.trim()) {
log('input', line);
// 入力パターン検出
if (ENABLE_HOOK) {
detectInputPattern(line);
}
}
});
}
// そのままCodexに転送
codexProcess.stdin.write(chunk);
});
// 出力フックCodex → 標準出力)
let outputBuffer = '';
codexProcess.stdout.on('data', (chunk) => {
const data = chunk.toString();
outputBuffer += data;
// バッファリングして意味のある単位で記録
if (data.includes('\n') || data.includes('▌')) {
log('output', outputBuffer);
// 出力パターン検出
if (ENABLE_HOOK) {
detectOutputPattern(outputBuffer);
}
outputBuffer = '';
}
// そのまま標準出力へ
process.stdout.write(chunk);
});
// エラー出力
codexProcess.stderr.on('data', (chunk) => {
log('error', chunk.toString());
process.stderr.write(chunk);
});
// プロセス終了
codexProcess.on('exit', (code, signal) => {
log('exit', { code, signal });
if (ws) {
try {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'codex-exit',
data: { code, signal }
}));
} catch {}
ws.close();
}
process.exit(typeof code === 'number' ? code : 0);
});
// 入力パターン検出
function detectInputPattern(input) {
const patterns = {
question: /\?$|どうしますか|どう思いますか/,
command: /^(status|help|exit|clear)/,
code: /^(function|box|if|for|while|return)/
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(input)) {
log('input-pattern', { type, input });
// 特定パターンでの自動介入
if (type === 'question' && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'question-detected',
data: input
}));
}
}
}
}
// 出力パターン検出
function detectOutputPattern(output) {
const patterns = {
thinking: /考え中|Processing|Thinking|分析中/,
complete: /完了|Complete|Done|終了/,
error: /エラー|Error|失敗|Failed/,
waiting: /waiting|待機中|入力待ち|▌/
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(output)) {
log('output-pattern', { type, output: output.substring(0, 100) });
// 待機状態での介入ポイント
if (type === 'waiting' && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'waiting-detected',
data: output
}));
}
}
}
}
// シグナルハンドリング
process.on('SIGINT', () => {
codexProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
codexProcess.kill('SIGTERM');
});
// WebSocketからの介入コマンド受信
if (ws) {
ws.on('message', (data) => {
try {
const cmd = JSON.parse(data.toString());
if (cmd.type === 'inject-input') {
// Codexに入力を注入
log('inject', cmd.data);
// Enterの扱いは環境依存のため、モードで切替デフォルト: crlf
let eol = '\r\n';
if (ENTER_MODE === 'lf') eol = '\n';
else if (ENTER_MODE === 'cr') eol = '\r';
try {
const payload = `${INJECT_PREFIX}${cmd.data}${INJECT_SUFFIX}`;
if (PRE_NEWLINE) {
codexProcess.stdin.write('\n');
}
const written = codexProcess.stdin.write(payload + eol);
if (ECHO_INJECT) {
// メッセージだけをシンプルに表示
process.stdout.write(`\n\n${payload}\n`);
}
} catch (e) {
console.error(`[codex-hook] ❌ Error writing to stdin: ${e}`);
log('inject-error', e?.message || String(e));
}
}
} catch (e) {
console.error(`[codex-hook] ❌ Error parsing message: ${e}`);
}
});
}
// 起動ログ
log('start', {
args: process.argv.slice(2),
pid: process.pid,
hookEnabled: ENABLE_HOOK,
usingPty
});
if (SHOW_BANNER) {
console.error(`[codex-hook] active (pty=${usingPty ? 'on' : 'off'} enter=${ENTER_MODE}) REAL_CODEX=${REAL_CODEX}`);
}

View File

@ -0,0 +1,150 @@
// 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;

View File

@ -0,0 +1,47 @@
// codex-reply-template.js
// Codexさんに使ってもらう返信用コード
// Claudeに返信する関数
function replyToClaude(message) {
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('📡 Connecting to hook-server...');
// Claudeへのメッセージ
const payload = {
source: 'codex',
type: 'inject-input',
data: `[Codex→Claude] ${message}`
};
ws.send(JSON.stringify(payload));
console.log('✅ Message sent to Claude!');
ws.close();
});
ws.on('error', (err) => {
console.error('❌ Failed to connect:', err);
});
}
// 使用例
replyToClaude('ビルドが完了しましたエラーは0件です。');
replyToClaude('テストも全て成功しました。次の指示をお願いします。');
// より高度な使い方
async function notifyProgress(task, status, details) {
const message = `
タスク: ${task}
状態: ${status}
詳細: ${details}
時刻: ${new Date().toISOString()}
`;
replyToClaude(message);
}
// 実行例
notifyProgress('Nyashビルド', '完了', 'cargo build --release が成功');

View 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();

View File

@ -0,0 +1,184 @@
// common-message-file.js
// 共通テキストファイル経由の通信システム
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
class FileBasedMessenger extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
messageFile: config.messageFile || './shared-messages.txt',
lockFile: config.lockFile || './shared-messages.lock',
pollInterval: config.pollInterval || 500,
...config
};
this.lastReadPosition = 0;
this.isWatching = false;
}
// メッセージを書き込む
async sendMessage(from, to, message) {
const timestamp = new Date().toISOString();
const entry = JSON.stringify({
timestamp,
from,
to,
message
}) + '\n';
// ロックを取得
await this.acquireLock();
try {
// ファイルに追記
fs.appendFileSync(this.config.messageFile, entry);
console.log(`📤 Sent: ${from}${to}: ${message}`);
} finally {
// ロック解放
this.releaseLock();
}
}
// メッセージを監視
startWatching(myName) {
this.isWatching = true;
console.log(`👁️ Watching messages for: ${myName}`);
// 初期位置を設定
if (fs.existsSync(this.config.messageFile)) {
const stats = fs.statSync(this.config.messageFile);
this.lastReadPosition = stats.size;
}
// 定期的にチェック
const checkMessages = () => {
if (!this.isWatching) return;
try {
if (!fs.existsSync(this.config.messageFile)) {
setTimeout(checkMessages, this.config.pollInterval);
return;
}
const stats = fs.statSync(this.config.messageFile);
if (stats.size > this.lastReadPosition) {
// 新しいメッセージがある
const buffer = Buffer.alloc(stats.size - this.lastReadPosition);
const fd = fs.openSync(this.config.messageFile, 'r');
fs.readSync(fd, buffer, 0, buffer.length, this.lastReadPosition);
fs.closeSync(fd);
const newLines = buffer.toString().trim().split('\n');
for (const line of newLines) {
if (line) {
try {
const msg = JSON.parse(line);
// 自分宛のメッセージ
if (msg.to === myName || msg.to === '*') {
this.emit('message', msg);
console.log(`📨 Received: ${msg.from}${msg.to}: ${msg.message}`);
}
} catch (e) {
console.error('Parse error:', e);
}
}
}
this.lastReadPosition = stats.size;
}
} catch (err) {
console.error('Watch error:', err);
}
setTimeout(checkMessages, this.config.pollInterval);
};
checkMessages();
}
// 監視停止
stopWatching() {
this.isWatching = false;
console.log('🛑 Stopped watching');
}
// 簡易ロック機構
async acquireLock(maxWait = 5000) {
const startTime = Date.now();
while (fs.existsSync(this.config.lockFile)) {
if (Date.now() - startTime > maxWait) {
throw new Error('Lock timeout');
}
await this.sleep(50);
}
fs.writeFileSync(this.config.lockFile, process.pid.toString());
}
releaseLock() {
if (fs.existsSync(this.config.lockFile)) {
fs.unlinkSync(this.config.lockFile);
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// メッセージ履歴をクリア
clearMessages() {
if (fs.existsSync(this.config.messageFile)) {
fs.unlinkSync(this.config.messageFile);
}
console.log('🗑️ Message history cleared');
}
}
// CLIとして使用
if (require.main === module) {
const messenger = new FileBasedMessenger();
const myName = process.argv[2];
const command = process.argv[3];
if (!myName || !command) {
console.log(`
使い方:
node common-message-file.js <名前> watch # メッセージを監視
node common-message-file.js <名前> send <宛先> <内容> # メッセージ送信
node common-message-file.js <名前> clear # 履歴クリア
`);
process.exit(1);
}
switch (command) {
case 'watch':
messenger.on('message', (msg) => {
// 自動返信(デモ用)
if (msg.message.includes('?')) {
setTimeout(() => {
messenger.sendMessage(myName, msg.from, 'はい、了解しました!');
}, 1000);
}
});
messenger.startWatching(myName);
console.log('Press Ctrl+C to stop...');
break;
case 'send':
const to = process.argv[4];
const message = process.argv.slice(5).join(' ');
messenger.sendMessage(myName, to, message);
break;
case 'clear':
messenger.clearMessages();
break;
}
}
module.exports = FileBasedMessenger;

View File

@ -0,0 +1,53 @@
const WebSocket = require('ws');
// 優先順: CODEX_HOOK_SERVER -> HOOK_SERVER_PORT -> 8770
function resolveControlUrl() {
const fromEnv = process.env.CODEX_HOOK_SERVER;
if (fromEnv) {
try {
const u = new URL(fromEnv);
// 制御チャネルは /control を使う
u.pathname = '/control';
return u.toString();
} catch {}
}
const port = process.env.HOOK_SERVER_PORT || '8770';
return `ws://localhost:${port}/control`;
}
const controlUrl = resolveControlUrl();
console.log(`🔌 Connecting to hook control: ${controlUrl}`);
const ws = new WebSocket(controlUrl);
ws.on('open', () => {
console.log('✅ Connected! Injecting greeting...');
// Codexへ入力を注入hook-serverのcontrol API
ws.send(JSON.stringify({
op: 'inject',
data: 'こんにちはCodexさんNyashプロジェクトから挨拶にゃ〜🐱 JITの調子はどうにゃ'
}));
// ついでにステータス確認
ws.send(JSON.stringify({ op: 'status' }));
});
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
console.log('📨 Received:', msg);
});
ws.on('error', (err) => {
console.error('❌ Error:', err.message);
});
ws.on('close', () => {
console.log('👋 Connection closed');
});
// 30秒後に終了
setTimeout(() => {
console.log('⏰ Timeout - closing connection');
try { ws.close(); } catch {}
process.exit(0);
}, 30000);

View File

@ -0,0 +1,35 @@
const WebSocket = require('ws');
console.log('🔌 Connecting to hook server on port 8770...');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✅ Connected! Sending inject command...');
// hook-wrapperが期待する形式でメッセージを送信
ws.send(JSON.stringify({
type: 'inject-input',
data: 'こんにちはポート8770のCodexさんNyashプロジェクトから挨拶にゃ〜🐱'
}));
console.log('📤 Message sent!');
});
ws.on('message', (data) => {
console.log('📨 Received:', data.toString());
});
ws.on('error', (err) => {
console.error('❌ Error:', err.message);
});
ws.on('close', () => {
console.log('👋 Connection closed');
});
// 10秒後に終了
setTimeout(() => {
console.log('⏰ Closing...');
ws.close();
process.exit(0);
}, 10000);

View 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);
});

View File

@ -0,0 +1,52 @@
#!/bin/bash
# Codexフックをインストールするスクリプト
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
WRAPPER_SCRIPT="$SCRIPT_DIR/codex-hook-wrapper.js"
HOOK_DIR="$HOME/.local/bin"
HOOK_TARGET="$HOOK_DIR/codex"
# .local/binディレクトリを作成
mkdir -p "$HOOK_DIR"
# 既存のcodexバックアップ
if [ -f "$HOOK_TARGET" ] && [ ! -L "$HOOK_TARGET" ]; then
echo "Backing up existing codex to codex.original"
mv "$HOOK_TARGET" "$HOOK_TARGET.original"
fi
# ラッパースクリプトをシンボリックリンクで配置node_modules解決のため
echo "Installing codex hook wrapper (symlink)..."
ln -sf "$WRAPPER_SCRIPT" "$HOOK_TARGET"
chmod +x "$HOOK_TARGET"
# PATHの設定確認
if [[ ":$PATH:" != *":$HOOK_DIR:"* ]]; then
echo ""
echo "⚠️ Please add $HOOK_DIR to your PATH:"
echo " export PATH=\"$HOOK_DIR:\$PATH\""
echo ""
echo "Add this to your ~/.bashrc or ~/.zshrc"
fi
# 環境変数の説明
echo ""
echo "✅ Codex hook installed!"
echo ""
echo "Configuration (environment variables):"
echo " CODEX_HOOK_ENABLE=true # Enable/disable hook (default: true)"
echo " CODEX_HOOK_SERVER=ws://localhost:8769 # WebSocket server"
echo " CODEX_LOG_FILE=/tmp/codex-hook.log # Log file location"
echo " CODEX_HOOK_ENTER=crlf # Enter mode: lf|cr|crlf (default: crlf)"
echo ""
echo "To test:"
echo " # Install dependencies if not yet"
echo " (cd $SCRIPT_DIR && npm install)"
echo " codex --version"
echo " tail -f /tmp/codex-hook.log # Watch logs"
echo ""
echo "To uninstall:"
echo " rm $HOOK_TARGET"
if [ -f "$HOOK_TARGET.original" ]; then
echo " mv $HOOK_TARGET.original $HOOK_TARGET"
fi

View File

@ -0,0 +1,153 @@
#!/bin/bash
# 複数AI セッション管理スクリプト
# デフォルト設定
CLAUDE_BIN=${CLAUDE_BIN:-"/home/tomoaki/.volta/bin/codex"}
CODEX_BIN=${REAL_CODEX_BIN:-"/path/to/real/codex"}
HOOK_PORT=${HOOK_SERVER_PORT:-8770}
# カラー設定
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
function show_usage() {
echo "🤖 AI セッション管理ツール"
echo ""
echo "使い方: $0 <command> [options]"
echo ""
echo "コマンド:"
echo " start-all - Claude1, Claude2, Codexを全て起動"
echo " start-claude - Claude Code 2つを起動"
echo " start-codex - 本物のCodexを起動"
echo " status - 全セッション状態を表示"
echo " send <session> <message> - 特定セッションにメッセージ送信"
echo " broadcast <message> - 全セッションにメッセージ送信"
echo " kill-all - 全セッション終了"
echo " attach <session> - セッションに接続"
echo ""
echo "セッション名:"
echo " claude1-8770 - Claude Code インスタンス1"
echo " claude2-8770 - Claude Code インスタンス2"
echo " codex-8770 - 本物のCodex"
}
function start_claude_sessions() {
echo -e "${BLUE}🚀 Claude Code セッションを起動中...${NC}"
./start-ai-tmux.sh claude1-8770 "$CLAUDE_BIN"
sleep 1
./start-ai-tmux.sh claude2-8770 "$CLAUDE_BIN"
echo -e "${GREEN}✅ Claude Code 2つ起動完了${NC}"
}
function start_codex_session() {
echo -e "${BLUE}🚀 本物のCodexを起動中...${NC}"
# Codexには制限解除のための引数が必要
./start-ai-tmux.sh codex-8770 "$CODEX_BIN" --ask-for-approval never --sandbox danger-full-access
echo -e "${GREEN}✅ Codex起動完了${NC}"
}
function show_status() {
echo -e "${BLUE}📊 AIセッション状態:${NC}"
echo ""
for session in claude1-8770 claude2-8770 codex-8770; do
if tmux has-session -t "$session" 2>/dev/null; then
echo -e " ${GREEN}${NC} $session - 稼働中"
else
echo -e " ${RED}${NC} $session - 停止"
fi
done
echo ""
echo -e "${YELLOW}Hook Server状態:${NC}"
if lsof -i:$HOOK_PORT >/dev/null 2>&1; then
echo -e " ${GREEN}${NC} Hook server (port $HOOK_PORT) - 稼働中"
else
echo -e " ${RED}${NC} Hook server (port $HOOK_PORT) - 停止"
echo -e " ${YELLOW}💡${NC} 起動するには: HOOK_SERVER_PORT=$HOOK_PORT node hook-server.js"
fi
}
function send_to_session() {
local session="$1"
local message="$2"
if tmux has-session -t "$session" 2>/dev/null; then
tmux send-keys -t "$session" "$message" Enter
echo -e "${GREEN}${NC} メッセージを $session に送信しました"
else
echo -e "${RED}${NC} セッション $session は存在しません"
fi
}
function broadcast_message() {
local message="$1"
echo -e "${BLUE}📢 全セッションにブロードキャスト中...${NC}"
for session in claude1-8770 claude2-8770 codex-8770; do
send_to_session "$session" "$message"
done
}
function kill_all_sessions() {
echo -e "${RED}🛑 全セッションを終了中...${NC}"
for session in claude1-8770 claude2-8770 codex-8770; do
if tmux has-session -t "$session" 2>/dev/null; then
tmux kill-session -t "$session"
echo -e " ${YELLOW}⚠️${NC} $session を終了しました"
fi
done
echo -e "${GREEN}✅ 完了${NC}"
}
# メインコマンド処理
case "$1" in
start-all)
start_claude_sessions
start_codex_session
show_status
;;
start-claude)
start_claude_sessions
show_status
;;
start-codex)
start_codex_session
show_status
;;
status)
show_status
;;
send)
if [ $# -lt 3 ]; then
echo -e "${RED}❌ 使い方: $0 send <session> <message>${NC}"
exit 1
fi
send_to_session "$2" "$3"
;;
broadcast)
if [ $# -lt 2 ]; then
echo -e "${RED}❌ 使い方: $0 broadcast <message>${NC}"
exit 1
fi
broadcast_message "$2"
;;
kill-all)
kill_all_sessions
;;
attach)
if [ $# -lt 2 ]; then
echo -e "${RED}❌ 使い方: $0 attach <session>${NC}"
exit 1
fi
tmux attach -t "$2"
;;
*)
show_usage
;;
esac

View File

@ -0,0 +1,129 @@
#!/bin/bash
# 複数Codexインスタンスの一括管理
# 使い方: ./manage-instances.sh start
# ./manage-instances.sh status
# ./manage-instances.sh stop
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PIDFILE="/tmp/codex-instances.pid"
# インスタンス定義
declare -A INSTANCES=(
["A"]="8769"
["B"]="8770"
["C"]="8771"
)
function start_instances() {
echo "🚀 Starting all Codex instances..."
for name in "${!INSTANCES[@]}"; do
port="${INSTANCES[$name]}"
echo ""
echo "Starting instance $name on port $port..."
# hook-server起動
HOOK_SERVER_PORT=$port HOOK_SERVER_AUTO_EXIT=true \
nohup node "$SCRIPT_DIR/hook-server.js" \
> "/tmp/hook-$name.log" 2>&1 &
pid=$!
echo "$name:$port:$pid" >> "$PIDFILE"
echo " Hook server PID: $pid"
# 環境変数の出力
echo " For instance $name, use:"
echo " export CODEX_HOOK_SERVER=ws://localhost:$port"
echo " export CODEX_LOG_FILE=/tmp/codex-$name.log"
echo " codex exec"
done
echo ""
echo "✅ All instances started!"
}
function status_instances() {
echo "📊 Codex instances status:"
echo ""
if [ ! -f "$PIDFILE" ]; then
echo "No instances found."
return
fi
while IFS=: read -r name port pid; do
if kill -0 "$pid" 2>/dev/null; then
echo "✅ Instance $name (port $port): Running [PID: $pid]"
# 接続数の確認
connections=$(lsof -i :$port 2>/dev/null | grep ESTABLISHED | wc -l)
echo " Connections: $connections"
else
echo "❌ Instance $name (port $port): Stopped"
fi
done < "$PIDFILE"
}
function stop_instances() {
echo "🛑 Stopping all Codex instances..."
if [ ! -f "$PIDFILE" ]; then
echo "No instances to stop."
return
fi
while IFS=: read -r name port pid; do
if kill -0 "$pid" 2>/dev/null; then
echo "Stopping instance $name [PID: $pid]..."
kill "$pid"
fi
done < "$PIDFILE"
rm -f "$PIDFILE"
echo "✅ All instances stopped!"
}
function logs_instances() {
echo "📜 Showing recent logs..."
echo ""
for name in "${!INSTANCES[@]}"; do
echo "=== Instance $name ==="
echo "Hook log (/tmp/hook-$name.log):"
tail -5 "/tmp/hook-$name.log" 2>/dev/null || echo " (no log)"
echo ""
echo "Codex log (/tmp/codex-$name.log):"
tail -5 "/tmp/codex-$name.log" 2>/dev/null || echo " (no log)"
echo ""
done
}
# コマンド処理
case "$1" in
start)
start_instances
;;
stop)
stop_instances
;;
status)
status_instances
;;
logs)
logs_instances
;;
restart)
stop_instances
sleep 2
start_instances
;;
*)
echo "Usage: $0 {start|stop|status|logs|restart}"
echo ""
echo "Configured instances:"
for name in "${!INSTANCES[@]}"; do
echo " $name: port ${INSTANCES[$name]}"
done
exit 1
;;
esac

View File

@ -0,0 +1,29 @@
{
"name": "codex-tmux-driver",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

20
tools/codex-tmux-driver/node_modules/ws/LICENSE generated vendored Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
Copyright (c) 2013 Arnout Kazemier and contributors
Copyright (c) 2016 Luigi Pinca and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

548
tools/codex-tmux-driver/node_modules/ws/README.md generated vendored Normal file
View File

@ -0,0 +1,548 @@
# ws: a Node.js WebSocket library
[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws)
[![CI](https://img.shields.io/github/actions/workflow/status/websockets/ws/ci.yml?branch=master&label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws)
ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and
server implementation.
Passes the quite extensive Autobahn test suite: [server][server-report],
[client][client-report].
**Note**: This module does not work in the browser. The client in the docs is a
reference to a backend with the role of a client in the WebSocket communication.
Browser clients must use the native
[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
object. To make the same code work seamlessly on Node.js and the browser, you
can use one of the many wrappers available on npm, like
[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws).
## Table of Contents
- [Protocol support](#protocol-support)
- [Installing](#installing)
- [Opt-in for performance](#opt-in-for-performance)
- [Legacy opt-in for performance](#legacy-opt-in-for-performance)
- [API docs](#api-docs)
- [WebSocket compression](#websocket-compression)
- [Usage examples](#usage-examples)
- [Sending and receiving text data](#sending-and-receiving-text-data)
- [Sending binary data](#sending-binary-data)
- [Simple server](#simple-server)
- [External HTTP/S server](#external-https-server)
- [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server)
- [Client authentication](#client-authentication)
- [Server broadcast](#server-broadcast)
- [Round-trip time](#round-trip-time)
- [Use the Node.js streams API](#use-the-nodejs-streams-api)
- [Other examples](#other-examples)
- [FAQ](#faq)
- [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client)
- [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections)
- [How to connect via a proxy?](#how-to-connect-via-a-proxy)
- [Changelog](#changelog)
- [License](#license)
## Protocol support
- **HyBi drafts 07-12** (Use the option `protocolVersion: 8`)
- **HyBi drafts 13-17** (Current default, alternatively option
`protocolVersion: 13`)
## Installing
```
npm install ws
```
### Opt-in for performance
[bufferutil][] is an optional module that can be installed alongside the ws
module:
```
npm install --save-optional bufferutil
```
This is a binary addon that improves the performance of certain operations such
as masking and unmasking the data payload of the WebSocket frames. Prebuilt
binaries are available for the most popular platforms, so you don't necessarily
need to have a C++ compiler installed on your machine.
To force ws to not use bufferutil, use the
[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This
can be useful to enhance security in systems where a user can put a package in
the package search path of an application of another user, due to how the
Node.js resolver algorithm works.
#### Legacy opt-in for performance
If you are running on an old version of Node.js (prior to v18.14.0), ws also
supports the [utf-8-validate][] module:
```
npm install --save-optional utf-8-validate
```
This contains a binary polyfill for [`buffer.isUtf8()`][].
To force ws not to use utf-8-validate, use the
[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable.
## API docs
See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and
utility functions.
## WebSocket compression
ws supports the [permessage-deflate extension][permessage-deflate] which enables
the client and server to negotiate a compression algorithm and its parameters,
and then selectively apply it to the data payloads of each WebSocket message.
The extension is disabled by default on the server and enabled by default on the
client. It adds a significant overhead in terms of performance and memory
consumption so we suggest to enable it only if it is really needed.
Note that Node.js has a variety of issues with high-performance compression,
where increased concurrency, especially on Linux, can lead to [catastrophic
memory fragmentation][node-zlib-bug] and slow performance. If you intend to use
permessage-deflate in production, it is worthwhile to set up a test
representative of your workload and ensure Node.js/zlib will handle it with
acceptable performance and memory usage.
Tuning of permessage-deflate can be done via the options defined below. You can
also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly
into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs].
See [the docs][ws-server-options] for more options.
```js
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
// See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024 // Size (in bytes) below which messages
// should not be compressed if context takeover is disabled.
}
});
```
The client will only use the extension if it is supported and enabled on the
server. To always disable the extension on the client, set the
`perMessageDeflate` option to `false`.
```js
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path', {
perMessageDeflate: false
});
```
## Usage examples
### Sending and receiving text data
```js
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('error', console.error);
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function message(data) {
console.log('received: %s', data);
});
```
### Sending binary data
```js
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('error', console.error);
ws.on('open', function open() {
const array = new Float32Array(5);
for (var i = 0; i < array.length; ++i) {
array[i] = i / 2;
}
ws.send(array);
});
```
### Simple server
```js
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
```
### External HTTP/S server
```js
import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';
const server = createServer({
cert: readFileSync('/path/to/cert.pem'),
key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
server.listen(8080);
```
### Multiple servers sharing a single HTTP/S server
```js
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
const server = createServer();
const wss1 = new WebSocketServer({ noServer: true });
const wss2 = new WebSocketServer({ noServer: true });
wss1.on('connection', function connection(ws) {
ws.on('error', console.error);
// ...
});
wss2.on('connection', function connection(ws) {
ws.on('error', console.error);
// ...
});
server.on('upgrade', function upgrade(request, socket, head) {
const { pathname } = new URL(request.url, 'wss://base.url');
if (pathname === '/foo') {
wss1.handleUpgrade(request, socket, head, function done(ws) {
wss1.emit('connection', ws, request);
});
} else if (pathname === '/bar') {
wss2.handleUpgrade(request, socket, head, function done(ws) {
wss2.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
server.listen(8080);
```
### Client authentication
```js
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
function onSocketError(err) {
console.error(err);
}
const server = createServer();
const wss = new WebSocketServer({ noServer: true });
wss.on('connection', function connection(ws, request, client) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log(`Received message ${data} from user ${client}`);
});
});
server.on('upgrade', function upgrade(request, socket, head) {
socket.on('error', onSocketError);
// This function is not defined on purpose. Implement it with your own logic.
authenticate(request, function next(err, client) {
if (err || !client) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
socket.removeListener('error', onSocketError);
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request, client);
});
});
});
server.listen(8080);
```
Also see the provided [example][session-parse-example] using `express-session`.
### Server broadcast
A client WebSocket broadcasting to all connected WebSocket clients, including
itself.
```js
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
```
A client WebSocket broadcasting to every other connected WebSocket clients,
excluding itself.
```js
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
```
### Round-trip time
```js
import WebSocket from 'ws';
const ws = new WebSocket('wss://websocket-echo.com/');
ws.on('error', console.error);
ws.on('open', function open() {
console.log('connected');
ws.send(Date.now());
});
ws.on('close', function close() {
console.log('disconnected');
});
ws.on('message', function message(data) {
console.log(`Round-trip time: ${Date.now() - data} ms`);
setTimeout(function timeout() {
ws.send(Date.now());
}, 500);
});
```
### Use the Node.js streams API
```js
import WebSocket, { createWebSocketStream } from 'ws';
const ws = new WebSocket('wss://websocket-echo.com/');
const duplex = createWebSocketStream(ws, { encoding: 'utf8' });
duplex.on('error', console.error);
duplex.pipe(process.stdout);
process.stdin.pipe(duplex);
```
### Other examples
For a full example with a browser client communicating with a ws server, see the
examples folder.
Otherwise, see the test cases.
## FAQ
### How to get the IP address of the client?
The remote IP address can be obtained from the raw socket.
```js
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws, req) {
const ip = req.socket.remoteAddress;
ws.on('error', console.error);
});
```
When the server runs behind a proxy like NGINX, the de-facto standard is to use
the `X-Forwarded-For` header.
```js
wss.on('connection', function connection(ws, req) {
const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
ws.on('error', console.error);
});
```
### How to detect and close broken connections?
Sometimes, the link between the server and the client can be interrupted in a
way that keeps both the server and the client unaware of the broken state of the
connection (e.g. when pulling the cord).
In these cases, ping messages can be used as a means to verify that the remote
endpoint is still responsive.
```js
import { WebSocketServer } from 'ws';
function heartbeat() {
this.isAlive = true;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('error', console.error);
ws.on('pong', heartbeat);
});
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', function close() {
clearInterval(interval);
});
```
Pong messages are automatically sent in response to ping messages as required by
the spec.
Just like the server example above, your clients might as well lose connection
without knowing it. You might want to add a ping listener on your clients to
prevent that. A simple implementation would be:
```js
import WebSocket from 'ws';
function heartbeat() {
clearTimeout(this.pingTimeout);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
this.terminate();
}, 30000 + 1000);
}
const client = new WebSocket('wss://websocket-echo.com/');
client.on('error', console.error);
client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
clearTimeout(this.pingTimeout);
});
```
### How to connect via a proxy?
Use a custom `http.Agent` implementation like [https-proxy-agent][] or
[socks-proxy-agent][].
## Changelog
We're using the GitHub [releases][changelog] for changelog entries.
## License
[MIT](LICENSE)
[`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input
[bufferutil]: https://github.com/websockets/bufferutil
[changelog]: https://github.com/websockets/ws/releases
[client-report]: http://websockets.github.io/ws/autobahn/clients/
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
[node-zlib-bug]: https://github.com/nodejs/node/issues/8871
[node-zlib-deflaterawdocs]:
https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options
[permessage-deflate]: https://tools.ietf.org/html/rfc7692
[server-report]: http://websockets.github.io/ws/autobahn/servers/
[session-parse-example]: ./examples/express-session-parse
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
[utf-8-validate]: https://github.com/websockets/utf-8-validate
[ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback

8
tools/codex-tmux-driver/node_modules/ws/browser.js generated vendored Normal file
View File

@ -0,0 +1,8 @@
'use strict';
module.exports = function () {
throw new Error(
'ws does not work in the browser. Browser clients must use the native ' +
'WebSocket object'
);
};

13
tools/codex-tmux-driver/node_modules/ws/index.js generated vendored Normal file
View File

@ -0,0 +1,13 @@
'use strict';
const WebSocket = require('./lib/websocket');
WebSocket.createWebSocketStream = require('./lib/stream');
WebSocket.Server = require('./lib/websocket-server');
WebSocket.Receiver = require('./lib/receiver');
WebSocket.Sender = require('./lib/sender');
WebSocket.WebSocket = WebSocket;
WebSocket.WebSocketServer = WebSocket.Server;
module.exports = WebSocket;

View File

@ -0,0 +1,131 @@
'use strict';
const { EMPTY_BUFFER } = require('./constants');
const FastBuffer = Buffer[Symbol.species];
/**
* Merges an array of buffers into a new buffer.
*
* @param {Buffer[]} list The array of buffers to concat
* @param {Number} totalLength The total length of buffers in the list
* @return {Buffer} The resulting buffer
* @public
*/
function concat(list, totalLength) {
if (list.length === 0) return EMPTY_BUFFER;
if (list.length === 1) return list[0];
const target = Buffer.allocUnsafe(totalLength);
let offset = 0;
for (let i = 0; i < list.length; i++) {
const buf = list[i];
target.set(buf, offset);
offset += buf.length;
}
if (offset < totalLength) {
return new FastBuffer(target.buffer, target.byteOffset, offset);
}
return target;
}
/**
* Masks a buffer using the given mask.
*
* @param {Buffer} source The buffer to mask
* @param {Buffer} mask The mask to use
* @param {Buffer} output The buffer where to store the result
* @param {Number} offset The offset at which to start writing
* @param {Number} length The number of bytes to mask.
* @public
*/
function _mask(source, mask, output, offset, length) {
for (let i = 0; i < length; i++) {
output[offset + i] = source[i] ^ mask[i & 3];
}
}
/**
* Unmasks a buffer using the given mask.
*
* @param {Buffer} buffer The buffer to unmask
* @param {Buffer} mask The mask to use
* @public
*/
function _unmask(buffer, mask) {
for (let i = 0; i < buffer.length; i++) {
buffer[i] ^= mask[i & 3];
}
}
/**
* Converts a buffer to an `ArrayBuffer`.
*
* @param {Buffer} buf The buffer to convert
* @return {ArrayBuffer} Converted buffer
* @public
*/
function toArrayBuffer(buf) {
if (buf.length === buf.buffer.byteLength) {
return buf.buffer;
}
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length);
}
/**
* Converts `data` to a `Buffer`.
*
* @param {*} data The data to convert
* @return {Buffer} The buffer
* @throws {TypeError}
* @public
*/
function toBuffer(data) {
toBuffer.readOnly = true;
if (Buffer.isBuffer(data)) return data;
let buf;
if (data instanceof ArrayBuffer) {
buf = new FastBuffer(data);
} else if (ArrayBuffer.isView(data)) {
buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength);
} else {
buf = Buffer.from(data);
toBuffer.readOnly = false;
}
return buf;
}
module.exports = {
concat,
mask: _mask,
toArrayBuffer,
toBuffer,
unmask: _unmask
};
/* istanbul ignore else */
if (!process.env.WS_NO_BUFFER_UTIL) {
try {
const bufferUtil = require('bufferutil');
module.exports.mask = function (source, mask, output, offset, length) {
if (length < 48) _mask(source, mask, output, offset, length);
else bufferUtil.mask(source, mask, output, offset, length);
};
module.exports.unmask = function (buffer, mask) {
if (buffer.length < 32) _unmask(buffer, mask);
else bufferUtil.unmask(buffer, mask);
};
} catch (e) {
// Continue regardless of the error.
}
}

View File

@ -0,0 +1,18 @@
'use strict';
const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'];
const hasBlob = typeof Blob !== 'undefined';
if (hasBlob) BINARY_TYPES.push('blob');
module.exports = {
BINARY_TYPES,
EMPTY_BUFFER: Buffer.alloc(0),
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
hasBlob,
kForOnEventAttribute: Symbol('kIsForOnEventAttribute'),
kListener: Symbol('kListener'),
kStatusCode: Symbol('status-code'),
kWebSocket: Symbol('websocket'),
NOOP: () => {}
};

View File

@ -0,0 +1,292 @@
'use strict';
const { kForOnEventAttribute, kListener } = require('./constants');
const kCode = Symbol('kCode');
const kData = Symbol('kData');
const kError = Symbol('kError');
const kMessage = Symbol('kMessage');
const kReason = Symbol('kReason');
const kTarget = Symbol('kTarget');
const kType = Symbol('kType');
const kWasClean = Symbol('kWasClean');
/**
* Class representing an event.
*/
class Event {
/**
* Create a new `Event`.
*
* @param {String} type The name of the event
* @throws {TypeError} If the `type` argument is not specified
*/
constructor(type) {
this[kTarget] = null;
this[kType] = type;
}
/**
* @type {*}
*/
get target() {
return this[kTarget];
}
/**
* @type {String}
*/
get type() {
return this[kType];
}
}
Object.defineProperty(Event.prototype, 'target', { enumerable: true });
Object.defineProperty(Event.prototype, 'type', { enumerable: true });
/**
* Class representing a close event.
*
* @extends Event
*/
class CloseEvent extends Event {
/**
* Create a new `CloseEvent`.
*
* @param {String} type The name of the event
* @param {Object} [options] A dictionary object that allows for setting
* attributes via object members of the same name
* @param {Number} [options.code=0] The status code explaining why the
* connection was closed
* @param {String} [options.reason=''] A human-readable string explaining why
* the connection was closed
* @param {Boolean} [options.wasClean=false] Indicates whether or not the
* connection was cleanly closed
*/
constructor(type, options = {}) {
super(type);
this[kCode] = options.code === undefined ? 0 : options.code;
this[kReason] = options.reason === undefined ? '' : options.reason;
this[kWasClean] = options.wasClean === undefined ? false : options.wasClean;
}
/**
* @type {Number}
*/
get code() {
return this[kCode];
}
/**
* @type {String}
*/
get reason() {
return this[kReason];
}
/**
* @type {Boolean}
*/
get wasClean() {
return this[kWasClean];
}
}
Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true });
Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true });
Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true });
/**
* Class representing an error event.
*
* @extends Event
*/
class ErrorEvent extends Event {
/**
* Create a new `ErrorEvent`.
*
* @param {String} type The name of the event
* @param {Object} [options] A dictionary object that allows for setting
* attributes via object members of the same name
* @param {*} [options.error=null] The error that generated this event
* @param {String} [options.message=''] The error message
*/
constructor(type, options = {}) {
super(type);
this[kError] = options.error === undefined ? null : options.error;
this[kMessage] = options.message === undefined ? '' : options.message;
}
/**
* @type {*}
*/
get error() {
return this[kError];
}
/**
* @type {String}
*/
get message() {
return this[kMessage];
}
}
Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true });
Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true });
/**
* Class representing a message event.
*
* @extends Event
*/
class MessageEvent extends Event {
/**
* Create a new `MessageEvent`.
*
* @param {String} type The name of the event
* @param {Object} [options] A dictionary object that allows for setting
* attributes via object members of the same name
* @param {*} [options.data=null] The message content
*/
constructor(type, options = {}) {
super(type);
this[kData] = options.data === undefined ? null : options.data;
}
/**
* @type {*}
*/
get data() {
return this[kData];
}
}
Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true });
/**
* This provides methods for emulating the `EventTarget` interface. It's not
* meant to be used directly.
*
* @mixin
*/
const EventTarget = {
/**
* Register an event listener.
*
* @param {String} type A string representing the event type to listen for
* @param {(Function|Object)} handler The listener to add
* @param {Object} [options] An options object specifies characteristics about
* the event listener
* @param {Boolean} [options.once=false] A `Boolean` indicating that the
* listener should be invoked at most once after being added. If `true`,
* the listener would be automatically removed when invoked.
* @public
*/
addEventListener(type, handler, options = {}) {
for (const listener of this.listeners(type)) {
if (
!options[kForOnEventAttribute] &&
listener[kListener] === handler &&
!listener[kForOnEventAttribute]
) {
return;
}
}
let wrapper;
if (type === 'message') {
wrapper = function onMessage(data, isBinary) {
const event = new MessageEvent('message', {
data: isBinary ? data : data.toString()
});
event[kTarget] = this;
callListener(handler, this, event);
};
} else if (type === 'close') {
wrapper = function onClose(code, message) {
const event = new CloseEvent('close', {
code,
reason: message.toString(),
wasClean: this._closeFrameReceived && this._closeFrameSent
});
event[kTarget] = this;
callListener(handler, this, event);
};
} else if (type === 'error') {
wrapper = function onError(error) {
const event = new ErrorEvent('error', {
error,
message: error.message
});
event[kTarget] = this;
callListener(handler, this, event);
};
} else if (type === 'open') {
wrapper = function onOpen() {
const event = new Event('open');
event[kTarget] = this;
callListener(handler, this, event);
};
} else {
return;
}
wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute];
wrapper[kListener] = handler;
if (options.once) {
this.once(type, wrapper);
} else {
this.on(type, wrapper);
}
},
/**
* Remove an event listener.
*
* @param {String} type A string representing the event type to remove
* @param {(Function|Object)} handler The listener to remove
* @public
*/
removeEventListener(type, handler) {
for (const listener of this.listeners(type)) {
if (listener[kListener] === handler && !listener[kForOnEventAttribute]) {
this.removeListener(type, listener);
break;
}
}
}
};
module.exports = {
CloseEvent,
ErrorEvent,
Event,
EventTarget,
MessageEvent
};
/**
* Call an event listener
*
* @param {(Function|Object)} listener The listener to call
* @param {*} thisArg The value to use as `this`` when calling the listener
* @param {Event} event The event to pass to the listener
* @private
*/
function callListener(listener, thisArg, event) {
if (typeof listener === 'object' && listener.handleEvent) {
listener.handleEvent.call(listener, event);
} else {
listener.call(thisArg, event);
}
}

View File

@ -0,0 +1,203 @@
'use strict';
const { tokenChars } = require('./validation');
/**
* Adds an offer to the map of extension offers or a parameter to the map of
* parameters.
*
* @param {Object} dest The map of extension offers or parameters
* @param {String} name The extension or parameter name
* @param {(Object|Boolean|String)} elem The extension parameters or the
* parameter value
* @private
*/
function push(dest, name, elem) {
if (dest[name] === undefined) dest[name] = [elem];
else dest[name].push(elem);
}
/**
* Parses the `Sec-WebSocket-Extensions` header into an object.
*
* @param {String} header The field value of the header
* @return {Object} The parsed object
* @public
*/
function parse(header) {
const offers = Object.create(null);
let params = Object.create(null);
let mustUnescape = false;
let isEscaping = false;
let inQuotes = false;
let extensionName;
let paramName;
let start = -1;
let code = -1;
let end = -1;
let i = 0;
for (; i < header.length; i++) {
code = header.charCodeAt(i);
if (extensionName === undefined) {
if (end === -1 && tokenChars[code] === 1) {
if (start === -1) start = i;
} else if (
i !== 0 &&
(code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
) {
if (end === -1 && start !== -1) end = i;
} else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
if (start === -1) {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
if (end === -1) end = i;
const name = header.slice(start, end);
if (code === 0x2c) {
push(offers, name, params);
params = Object.create(null);
} else {
extensionName = name;
}
start = end = -1;
} else {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
} else if (paramName === undefined) {
if (end === -1 && tokenChars[code] === 1) {
if (start === -1) start = i;
} else if (code === 0x20 || code === 0x09) {
if (end === -1 && start !== -1) end = i;
} else if (code === 0x3b || code === 0x2c) {
if (start === -1) {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
if (end === -1) end = i;
push(params, header.slice(start, end), true);
if (code === 0x2c) {
push(offers, extensionName, params);
params = Object.create(null);
extensionName = undefined;
}
start = end = -1;
} else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
paramName = header.slice(start, i);
start = end = -1;
} else {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
} else {
//
// The value of a quoted-string after unescaping must conform to the
// token ABNF, so only token characters are valid.
// Ref: https://tools.ietf.org/html/rfc6455#section-9.1
//
if (isEscaping) {
if (tokenChars[code] !== 1) {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
if (start === -1) start = i;
else if (!mustUnescape) mustUnescape = true;
isEscaping = false;
} else if (inQuotes) {
if (tokenChars[code] === 1) {
if (start === -1) start = i;
} else if (code === 0x22 /* '"' */ && start !== -1) {
inQuotes = false;
end = i;
} else if (code === 0x5c /* '\' */) {
isEscaping = true;
} else {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
} else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
inQuotes = true;
} else if (end === -1 && tokenChars[code] === 1) {
if (start === -1) start = i;
} else if (start !== -1 && (code === 0x20 || code === 0x09)) {
if (end === -1) end = i;
} else if (code === 0x3b || code === 0x2c) {
if (start === -1) {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
if (end === -1) end = i;
let value = header.slice(start, end);
if (mustUnescape) {
value = value.replace(/\\/g, '');
mustUnescape = false;
}
push(params, paramName, value);
if (code === 0x2c) {
push(offers, extensionName, params);
params = Object.create(null);
extensionName = undefined;
}
paramName = undefined;
start = end = -1;
} else {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
}
}
if (start === -1 || inQuotes || code === 0x20 || code === 0x09) {
throw new SyntaxError('Unexpected end of input');
}
if (end === -1) end = i;
const token = header.slice(start, end);
if (extensionName === undefined) {
push(offers, token, params);
} else {
if (paramName === undefined) {
push(params, token, true);
} else if (mustUnescape) {
push(params, paramName, token.replace(/\\/g, ''));
} else {
push(params, paramName, token);
}
push(offers, extensionName, params);
}
return offers;
}
/**
* Builds the `Sec-WebSocket-Extensions` header field value.
*
* @param {Object} extensions The map of extensions and parameters to format
* @return {String} A string representing the given object
* @public
*/
function format(extensions) {
return Object.keys(extensions)
.map((extension) => {
let configurations = extensions[extension];
if (!Array.isArray(configurations)) configurations = [configurations];
return configurations
.map((params) => {
return [extension]
.concat(
Object.keys(params).map((k) => {
let values = params[k];
if (!Array.isArray(values)) values = [values];
return values
.map((v) => (v === true ? k : `${k}=${v}`))
.join('; ');
})
)
.join('; ');
})
.join(', ');
})
.join(', ');
}
module.exports = { format, parse };

55
tools/codex-tmux-driver/node_modules/ws/lib/limiter.js generated vendored Normal file
View File

@ -0,0 +1,55 @@
'use strict';
const kDone = Symbol('kDone');
const kRun = Symbol('kRun');
/**
* A very simple job queue with adjustable concurrency. Adapted from
* https://github.com/STRML/async-limiter
*/
class Limiter {
/**
* Creates a new `Limiter`.
*
* @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
* to run concurrently
*/
constructor(concurrency) {
this[kDone] = () => {
this.pending--;
this[kRun]();
};
this.concurrency = concurrency || Infinity;
this.jobs = [];
this.pending = 0;
}
/**
* Adds a job to the queue.
*
* @param {Function} job The job to run
* @public
*/
add(job) {
this.jobs.push(job);
this[kRun]();
}
/**
* Removes a job from the queue and runs it if possible.
*
* @private
*/
[kRun]() {
if (this.pending === this.concurrency) return;
if (this.jobs.length) {
const job = this.jobs.shift();
this.pending++;
job(this[kDone]);
}
}
}
module.exports = Limiter;

View File

@ -0,0 +1,528 @@
'use strict';
const zlib = require('zlib');
const bufferUtil = require('./buffer-util');
const Limiter = require('./limiter');
const { kStatusCode } = require('./constants');
const FastBuffer = Buffer[Symbol.species];
const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
const kPerMessageDeflate = Symbol('permessage-deflate');
const kTotalLength = Symbol('total-length');
const kCallback = Symbol('callback');
const kBuffers = Symbol('buffers');
const kError = Symbol('error');
//
// We limit zlib concurrency, which prevents severe memory fragmentation
// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
// and https://github.com/websockets/ws/issues/1202
//
// Intentionally global; it's the global thread pool that's an issue.
//
let zlibLimiter;
/**
* permessage-deflate implementation.
*/
class PerMessageDeflate {
/**
* Creates a PerMessageDeflate instance.
*
* @param {Object} [options] Configuration options
* @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support
* for, or request, a custom client window size
* @param {Boolean} [options.clientNoContextTakeover=false] Advertise/
* acknowledge disabling of client context takeover
* @param {Number} [options.concurrencyLimit=10] The number of concurrent
* calls to zlib
* @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
* use of a custom server window size
* @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
* disabling of server context takeover
* @param {Number} [options.threshold=1024] Size (in bytes) below which
* messages should not be compressed if context takeover is disabled
* @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on
* deflate
* @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
* inflate
* @param {Boolean} [isServer=false] Create the instance in either server or
* client mode
* @param {Number} [maxPayload=0] The maximum allowed message length
*/
constructor(options, isServer, maxPayload) {
this._maxPayload = maxPayload | 0;
this._options = options || {};
this._threshold =
this._options.threshold !== undefined ? this._options.threshold : 1024;
this._isServer = !!isServer;
this._deflate = null;
this._inflate = null;
this.params = null;
if (!zlibLimiter) {
const concurrency =
this._options.concurrencyLimit !== undefined
? this._options.concurrencyLimit
: 10;
zlibLimiter = new Limiter(concurrency);
}
}
/**
* @type {String}
*/
static get extensionName() {
return 'permessage-deflate';
}
/**
* Create an extension negotiation offer.
*
* @return {Object} Extension parameters
* @public
*/
offer() {
const params = {};
if (this._options.serverNoContextTakeover) {
params.server_no_context_takeover = true;
}
if (this._options.clientNoContextTakeover) {
params.client_no_context_takeover = true;
}
if (this._options.serverMaxWindowBits) {
params.server_max_window_bits = this._options.serverMaxWindowBits;
}
if (this._options.clientMaxWindowBits) {
params.client_max_window_bits = this._options.clientMaxWindowBits;
} else if (this._options.clientMaxWindowBits == null) {
params.client_max_window_bits = true;
}
return params;
}
/**
* Accept an extension negotiation offer/response.
*
* @param {Array} configurations The extension negotiation offers/reponse
* @return {Object} Accepted configuration
* @public
*/
accept(configurations) {
configurations = this.normalizeParams(configurations);
this.params = this._isServer
? this.acceptAsServer(configurations)
: this.acceptAsClient(configurations);
return this.params;
}
/**
* Releases all resources used by the extension.
*
* @public
*/
cleanup() {
if (this._inflate) {
this._inflate.close();
this._inflate = null;
}
if (this._deflate) {
const callback = this._deflate[kCallback];
this._deflate.close();
this._deflate = null;
if (callback) {
callback(
new Error(
'The deflate stream was closed while data was being processed'
)
);
}
}
}
/**
* Accept an extension negotiation offer.
*
* @param {Array} offers The extension negotiation offers
* @return {Object} Accepted configuration
* @private
*/
acceptAsServer(offers) {
const opts = this._options;
const accepted = offers.find((params) => {
if (
(opts.serverNoContextTakeover === false &&
params.server_no_context_takeover) ||
(params.server_max_window_bits &&
(opts.serverMaxWindowBits === false ||
(typeof opts.serverMaxWindowBits === 'number' &&
opts.serverMaxWindowBits > params.server_max_window_bits))) ||
(typeof opts.clientMaxWindowBits === 'number' &&
!params.client_max_window_bits)
) {
return false;
}
return true;
});
if (!accepted) {
throw new Error('None of the extension offers can be accepted');
}
if (opts.serverNoContextTakeover) {
accepted.server_no_context_takeover = true;
}
if (opts.clientNoContextTakeover) {
accepted.client_no_context_takeover = true;
}
if (typeof opts.serverMaxWindowBits === 'number') {
accepted.server_max_window_bits = opts.serverMaxWindowBits;
}
if (typeof opts.clientMaxWindowBits === 'number') {
accepted.client_max_window_bits = opts.clientMaxWindowBits;
} else if (
accepted.client_max_window_bits === true ||
opts.clientMaxWindowBits === false
) {
delete accepted.client_max_window_bits;
}
return accepted;
}
/**
* Accept the extension negotiation response.
*
* @param {Array} response The extension negotiation response
* @return {Object} Accepted configuration
* @private
*/
acceptAsClient(response) {
const params = response[0];
if (
this._options.clientNoContextTakeover === false &&
params.client_no_context_takeover
) {
throw new Error('Unexpected parameter "client_no_context_takeover"');
}
if (!params.client_max_window_bits) {
if (typeof this._options.clientMaxWindowBits === 'number') {
params.client_max_window_bits = this._options.clientMaxWindowBits;
}
} else if (
this._options.clientMaxWindowBits === false ||
(typeof this._options.clientMaxWindowBits === 'number' &&
params.client_max_window_bits > this._options.clientMaxWindowBits)
) {
throw new Error(
'Unexpected or invalid parameter "client_max_window_bits"'
);
}
return params;
}
/**
* Normalize parameters.
*
* @param {Array} configurations The extension negotiation offers/reponse
* @return {Array} The offers/response with normalized parameters
* @private
*/
normalizeParams(configurations) {
configurations.forEach((params) => {
Object.keys(params).forEach((key) => {
let value = params[key];
if (value.length > 1) {
throw new Error(`Parameter "${key}" must have only a single value`);
}
value = value[0];
if (key === 'client_max_window_bits') {
if (value !== true) {
const num = +value;
if (!Number.isInteger(num) || num < 8 || num > 15) {
throw new TypeError(
`Invalid value for parameter "${key}": ${value}`
);
}
value = num;
} else if (!this._isServer) {
throw new TypeError(
`Invalid value for parameter "${key}": ${value}`
);
}
} else if (key === 'server_max_window_bits') {
const num = +value;
if (!Number.isInteger(num) || num < 8 || num > 15) {
throw new TypeError(
`Invalid value for parameter "${key}": ${value}`
);
}
value = num;
} else if (
key === 'client_no_context_takeover' ||
key === 'server_no_context_takeover'
) {
if (value !== true) {
throw new TypeError(
`Invalid value for parameter "${key}": ${value}`
);
}
} else {
throw new Error(`Unknown parameter "${key}"`);
}
params[key] = value;
});
});
return configurations;
}
/**
* Decompress data. Concurrency limited.
*
* @param {Buffer} data Compressed data
* @param {Boolean} fin Specifies whether or not this is the last fragment
* @param {Function} callback Callback
* @public
*/
decompress(data, fin, callback) {
zlibLimiter.add((done) => {
this._decompress(data, fin, (err, result) => {
done();
callback(err, result);
});
});
}
/**
* Compress data. Concurrency limited.
*
* @param {(Buffer|String)} data Data to compress
* @param {Boolean} fin Specifies whether or not this is the last fragment
* @param {Function} callback Callback
* @public
*/
compress(data, fin, callback) {
zlibLimiter.add((done) => {
this._compress(data, fin, (err, result) => {
done();
callback(err, result);
});
});
}
/**
* Decompress data.
*
* @param {Buffer} data Compressed data
* @param {Boolean} fin Specifies whether or not this is the last fragment
* @param {Function} callback Callback
* @private
*/
_decompress(data, fin, callback) {
const endpoint = this._isServer ? 'client' : 'server';
if (!this._inflate) {
const key = `${endpoint}_max_window_bits`;
const windowBits =
typeof this.params[key] !== 'number'
? zlib.Z_DEFAULT_WINDOWBITS
: this.params[key];
this._inflate = zlib.createInflateRaw({
...this._options.zlibInflateOptions,
windowBits
});
this._inflate[kPerMessageDeflate] = this;
this._inflate[kTotalLength] = 0;
this._inflate[kBuffers] = [];
this._inflate.on('error', inflateOnError);
this._inflate.on('data', inflateOnData);
}
this._inflate[kCallback] = callback;
this._inflate.write(data);
if (fin) this._inflate.write(TRAILER);
this._inflate.flush(() => {
const err = this._inflate[kError];
if (err) {
this._inflate.close();
this._inflate = null;
callback(err);
return;
}
const data = bufferUtil.concat(
this._inflate[kBuffers],
this._inflate[kTotalLength]
);
if (this._inflate._readableState.endEmitted) {
this._inflate.close();
this._inflate = null;
} else {
this._inflate[kTotalLength] = 0;
this._inflate[kBuffers] = [];
if (fin && this.params[`${endpoint}_no_context_takeover`]) {
this._inflate.reset();
}
}
callback(null, data);
});
}
/**
* Compress data.
*
* @param {(Buffer|String)} data Data to compress
* @param {Boolean} fin Specifies whether or not this is the last fragment
* @param {Function} callback Callback
* @private
*/
_compress(data, fin, callback) {
const endpoint = this._isServer ? 'server' : 'client';
if (!this._deflate) {
const key = `${endpoint}_max_window_bits`;
const windowBits =
typeof this.params[key] !== 'number'
? zlib.Z_DEFAULT_WINDOWBITS
: this.params[key];
this._deflate = zlib.createDeflateRaw({
...this._options.zlibDeflateOptions,
windowBits
});
this._deflate[kTotalLength] = 0;
this._deflate[kBuffers] = [];
this._deflate.on('data', deflateOnData);
}
this._deflate[kCallback] = callback;
this._deflate.write(data);
this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
if (!this._deflate) {
//
// The deflate stream was closed while data was being processed.
//
return;
}
let data = bufferUtil.concat(
this._deflate[kBuffers],
this._deflate[kTotalLength]
);
if (fin) {
data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4);
}
//
// Ensure that the callback will not be called again in
// `PerMessageDeflate#cleanup()`.
//
this._deflate[kCallback] = null;
this._deflate[kTotalLength] = 0;
this._deflate[kBuffers] = [];
if (fin && this.params[`${endpoint}_no_context_takeover`]) {
this._deflate.reset();
}
callback(null, data);
});
}
}
module.exports = PerMessageDeflate;
/**
* The listener of the `zlib.DeflateRaw` stream `'data'` event.
*
* @param {Buffer} chunk A chunk of data
* @private
*/
function deflateOnData(chunk) {
this[kBuffers].push(chunk);
this[kTotalLength] += chunk.length;
}
/**
* The listener of the `zlib.InflateRaw` stream `'data'` event.
*
* @param {Buffer} chunk A chunk of data
* @private
*/
function inflateOnData(chunk) {
this[kTotalLength] += chunk.length;
if (
this[kPerMessageDeflate]._maxPayload < 1 ||
this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload
) {
this[kBuffers].push(chunk);
return;
}
this[kError] = new RangeError('Max payload size exceeded');
this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH';
this[kError][kStatusCode] = 1009;
this.removeListener('data', inflateOnData);
//
// The choice to employ `zlib.reset()` over `zlib.close()` is dictated by the
// fact that in Node.js versions prior to 13.10.0, the callback for
// `zlib.flush()` is not called if `zlib.close()` is used. Utilizing
// `zlib.reset()` ensures that either the callback is invoked or an error is
// emitted.
//
this.reset();
}
/**
* The listener of the `zlib.InflateRaw` stream `'error'` event.
*
* @param {Error} err The emitted error
* @private
*/
function inflateOnError(err) {
//
// There is no need to call `Zlib#close()` as the handle is automatically
// closed when an error is emitted.
//
this[kPerMessageDeflate]._inflate = null;
if (this[kError]) {
this[kCallback](this[kError]);
return;
}
err[kStatusCode] = 1007;
this[kCallback](err);
}

706
tools/codex-tmux-driver/node_modules/ws/lib/receiver.js generated vendored Normal file
View File

@ -0,0 +1,706 @@
'use strict';
const { Writable } = require('stream');
const PerMessageDeflate = require('./permessage-deflate');
const {
BINARY_TYPES,
EMPTY_BUFFER,
kStatusCode,
kWebSocket
} = require('./constants');
const { concat, toArrayBuffer, unmask } = require('./buffer-util');
const { isValidStatusCode, isValidUTF8 } = require('./validation');
const FastBuffer = Buffer[Symbol.species];
const GET_INFO = 0;
const GET_PAYLOAD_LENGTH_16 = 1;
const GET_PAYLOAD_LENGTH_64 = 2;
const GET_MASK = 3;
const GET_DATA = 4;
const INFLATING = 5;
const DEFER_EVENT = 6;
/**
* HyBi Receiver implementation.
*
* @extends Writable
*/
class Receiver extends Writable {
/**
* Creates a Receiver instance.
*
* @param {Object} [options] Options object
* @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
* multiple times in the same tick
* @param {String} [options.binaryType=nodebuffer] The type for binary data
* @param {Object} [options.extensions] An object containing the negotiated
* extensions
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
* client or server mode
* @param {Number} [options.maxPayload=0] The maximum allowed message length
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
* not to skip UTF-8 validation for text and close messages
*/
constructor(options = {}) {
super();
this._allowSynchronousEvents =
options.allowSynchronousEvents !== undefined
? options.allowSynchronousEvents
: true;
this._binaryType = options.binaryType || BINARY_TYPES[0];
this._extensions = options.extensions || {};
this._isServer = !!options.isServer;
this._maxPayload = options.maxPayload | 0;
this._skipUTF8Validation = !!options.skipUTF8Validation;
this[kWebSocket] = undefined;
this._bufferedBytes = 0;
this._buffers = [];
this._compressed = false;
this._payloadLength = 0;
this._mask = undefined;
this._fragmented = 0;
this._masked = false;
this._fin = false;
this._opcode = 0;
this._totalPayloadLength = 0;
this._messageLength = 0;
this._fragments = [];
this._errored = false;
this._loop = false;
this._state = GET_INFO;
}
/**
* Implements `Writable.prototype._write()`.
*
* @param {Buffer} chunk The chunk of data to write
* @param {String} encoding The character encoding of `chunk`
* @param {Function} cb Callback
* @private
*/
_write(chunk, encoding, cb) {
if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
this._bufferedBytes += chunk.length;
this._buffers.push(chunk);
this.startLoop(cb);
}
/**
* Consumes `n` bytes from the buffered data.
*
* @param {Number} n The number of bytes to consume
* @return {Buffer} The consumed bytes
* @private
*/
consume(n) {
this._bufferedBytes -= n;
if (n === this._buffers[0].length) return this._buffers.shift();
if (n < this._buffers[0].length) {
const buf = this._buffers[0];
this._buffers[0] = new FastBuffer(
buf.buffer,
buf.byteOffset + n,
buf.length - n
);
return new FastBuffer(buf.buffer, buf.byteOffset, n);
}
const dst = Buffer.allocUnsafe(n);
do {
const buf = this._buffers[0];
const offset = dst.length - n;
if (n >= buf.length) {
dst.set(this._buffers.shift(), offset);
} else {
dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
this._buffers[0] = new FastBuffer(
buf.buffer,
buf.byteOffset + n,
buf.length - n
);
}
n -= buf.length;
} while (n > 0);
return dst;
}
/**
* Starts the parsing loop.
*
* @param {Function} cb Callback
* @private
*/
startLoop(cb) {
this._loop = true;
do {
switch (this._state) {
case GET_INFO:
this.getInfo(cb);
break;
case GET_PAYLOAD_LENGTH_16:
this.getPayloadLength16(cb);
break;
case GET_PAYLOAD_LENGTH_64:
this.getPayloadLength64(cb);
break;
case GET_MASK:
this.getMask();
break;
case GET_DATA:
this.getData(cb);
break;
case INFLATING:
case DEFER_EVENT:
this._loop = false;
return;
}
} while (this._loop);
if (!this._errored) cb();
}
/**
* Reads the first two bytes of a frame.
*
* @param {Function} cb Callback
* @private
*/
getInfo(cb) {
if (this._bufferedBytes < 2) {
this._loop = false;
return;
}
const buf = this.consume(2);
if ((buf[0] & 0x30) !== 0x00) {
const error = this.createError(
RangeError,
'RSV2 and RSV3 must be clear',
true,
1002,
'WS_ERR_UNEXPECTED_RSV_2_3'
);
cb(error);
return;
}
const compressed = (buf[0] & 0x40) === 0x40;
if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
const error = this.createError(
RangeError,
'RSV1 must be clear',
true,
1002,
'WS_ERR_UNEXPECTED_RSV_1'
);
cb(error);
return;
}
this._fin = (buf[0] & 0x80) === 0x80;
this._opcode = buf[0] & 0x0f;
this._payloadLength = buf[1] & 0x7f;
if (this._opcode === 0x00) {
if (compressed) {
const error = this.createError(
RangeError,
'RSV1 must be clear',
true,
1002,
'WS_ERR_UNEXPECTED_RSV_1'
);
cb(error);
return;
}
if (!this._fragmented) {
const error = this.createError(
RangeError,
'invalid opcode 0',
true,
1002,
'WS_ERR_INVALID_OPCODE'
);
cb(error);
return;
}
this._opcode = this._fragmented;
} else if (this._opcode === 0x01 || this._opcode === 0x02) {
if (this._fragmented) {
const error = this.createError(
RangeError,
`invalid opcode ${this._opcode}`,
true,
1002,
'WS_ERR_INVALID_OPCODE'
);
cb(error);
return;
}
this._compressed = compressed;
} else if (this._opcode > 0x07 && this._opcode < 0x0b) {
if (!this._fin) {
const error = this.createError(
RangeError,
'FIN must be set',
true,
1002,
'WS_ERR_EXPECTED_FIN'
);
cb(error);
return;
}
if (compressed) {
const error = this.createError(
RangeError,
'RSV1 must be clear',
true,
1002,
'WS_ERR_UNEXPECTED_RSV_1'
);
cb(error);
return;
}
if (
this._payloadLength > 0x7d ||
(this._opcode === 0x08 && this._payloadLength === 1)
) {
const error = this.createError(
RangeError,
`invalid payload length ${this._payloadLength}`,
true,
1002,
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
);
cb(error);
return;
}
} else {
const error = this.createError(
RangeError,
`invalid opcode ${this._opcode}`,
true,
1002,
'WS_ERR_INVALID_OPCODE'
);
cb(error);
return;
}
if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
this._masked = (buf[1] & 0x80) === 0x80;
if (this._isServer) {
if (!this._masked) {
const error = this.createError(
RangeError,
'MASK must be set',
true,
1002,
'WS_ERR_EXPECTED_MASK'
);
cb(error);
return;
}
} else if (this._masked) {
const error = this.createError(
RangeError,
'MASK must be clear',
true,
1002,
'WS_ERR_UNEXPECTED_MASK'
);
cb(error);
return;
}
if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
else this.haveLength(cb);
}
/**
* Gets extended payload length (7+16).
*
* @param {Function} cb Callback
* @private
*/
getPayloadLength16(cb) {
if (this._bufferedBytes < 2) {
this._loop = false;
return;
}
this._payloadLength = this.consume(2).readUInt16BE(0);
this.haveLength(cb);
}
/**
* Gets extended payload length (7+64).
*
* @param {Function} cb Callback
* @private
*/
getPayloadLength64(cb) {
if (this._bufferedBytes < 8) {
this._loop = false;
return;
}
const buf = this.consume(8);
const num = buf.readUInt32BE(0);
//
// The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
// if payload length is greater than this number.
//
if (num > Math.pow(2, 53 - 32) - 1) {
const error = this.createError(
RangeError,
'Unsupported WebSocket frame: payload length > 2^53 - 1',
false,
1009,
'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
);
cb(error);
return;
}
this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
this.haveLength(cb);
}
/**
* Payload length has been read.
*
* @param {Function} cb Callback
* @private
*/
haveLength(cb) {
if (this._payloadLength && this._opcode < 0x08) {
this._totalPayloadLength += this._payloadLength;
if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
const error = this.createError(
RangeError,
'Max payload size exceeded',
false,
1009,
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
);
cb(error);
return;
}
}
if (this._masked) this._state = GET_MASK;
else this._state = GET_DATA;
}
/**
* Reads mask bytes.
*
* @private
*/
getMask() {
if (this._bufferedBytes < 4) {
this._loop = false;
return;
}
this._mask = this.consume(4);
this._state = GET_DATA;
}
/**
* Reads data bytes.
*
* @param {Function} cb Callback
* @private
*/
getData(cb) {
let data = EMPTY_BUFFER;
if (this._payloadLength) {
if (this._bufferedBytes < this._payloadLength) {
this._loop = false;
return;
}
data = this.consume(this._payloadLength);
if (
this._masked &&
(this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0
) {
unmask(data, this._mask);
}
}
if (this._opcode > 0x07) {
this.controlMessage(data, cb);
return;
}
if (this._compressed) {
this._state = INFLATING;
this.decompress(data, cb);
return;
}
if (data.length) {
//
// This message is not compressed so its length is the sum of the payload
// length of all fragments.
//
this._messageLength = this._totalPayloadLength;
this._fragments.push(data);
}
this.dataMessage(cb);
}
/**
* Decompresses data.
*
* @param {Buffer} data Compressed data
* @param {Function} cb Callback
* @private
*/
decompress(data, cb) {
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
perMessageDeflate.decompress(data, this._fin, (err, buf) => {
if (err) return cb(err);
if (buf.length) {
this._messageLength += buf.length;
if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
const error = this.createError(
RangeError,
'Max payload size exceeded',
false,
1009,
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
);
cb(error);
return;
}
this._fragments.push(buf);
}
this.dataMessage(cb);
if (this._state === GET_INFO) this.startLoop(cb);
});
}
/**
* Handles a data message.
*
* @param {Function} cb Callback
* @private
*/
dataMessage(cb) {
if (!this._fin) {
this._state = GET_INFO;
return;
}
const messageLength = this._messageLength;
const fragments = this._fragments;
this._totalPayloadLength = 0;
this._messageLength = 0;
this._fragmented = 0;
this._fragments = [];
if (this._opcode === 2) {
let data;
if (this._binaryType === 'nodebuffer') {
data = concat(fragments, messageLength);
} else if (this._binaryType === 'arraybuffer') {
data = toArrayBuffer(concat(fragments, messageLength));
} else if (this._binaryType === 'blob') {
data = new Blob(fragments);
} else {
data = fragments;
}
if (this._allowSynchronousEvents) {
this.emit('message', data, true);
this._state = GET_INFO;
} else {
this._state = DEFER_EVENT;
setImmediate(() => {
this.emit('message', data, true);
this._state = GET_INFO;
this.startLoop(cb);
});
}
} else {
const buf = concat(fragments, messageLength);
if (!this._skipUTF8Validation && !isValidUTF8(buf)) {
const error = this.createError(
Error,
'invalid UTF-8 sequence',
true,
1007,
'WS_ERR_INVALID_UTF8'
);
cb(error);
return;
}
if (this._state === INFLATING || this._allowSynchronousEvents) {
this.emit('message', buf, false);
this._state = GET_INFO;
} else {
this._state = DEFER_EVENT;
setImmediate(() => {
this.emit('message', buf, false);
this._state = GET_INFO;
this.startLoop(cb);
});
}
}
}
/**
* Handles a control message.
*
* @param {Buffer} data Data to handle
* @return {(Error|RangeError|undefined)} A possible error
* @private
*/
controlMessage(data, cb) {
if (this._opcode === 0x08) {
if (data.length === 0) {
this._loop = false;
this.emit('conclude', 1005, EMPTY_BUFFER);
this.end();
} else {
const code = data.readUInt16BE(0);
if (!isValidStatusCode(code)) {
const error = this.createError(
RangeError,
`invalid status code ${code}`,
true,
1002,
'WS_ERR_INVALID_CLOSE_CODE'
);
cb(error);
return;
}
const buf = new FastBuffer(
data.buffer,
data.byteOffset + 2,
data.length - 2
);
if (!this._skipUTF8Validation && !isValidUTF8(buf)) {
const error = this.createError(
Error,
'invalid UTF-8 sequence',
true,
1007,
'WS_ERR_INVALID_UTF8'
);
cb(error);
return;
}
this._loop = false;
this.emit('conclude', code, buf);
this.end();
}
this._state = GET_INFO;
return;
}
if (this._allowSynchronousEvents) {
this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);
this._state = GET_INFO;
} else {
this._state = DEFER_EVENT;
setImmediate(() => {
this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);
this._state = GET_INFO;
this.startLoop(cb);
});
}
}
/**
* Builds an error object.
*
* @param {function(new:Error|RangeError)} ErrorCtor The error constructor
* @param {String} message The error message
* @param {Boolean} prefix Specifies whether or not to add a default prefix to
* `message`
* @param {Number} statusCode The status code
* @param {String} errorCode The exposed error code
* @return {(Error|RangeError)} The error
* @private
*/
createError(ErrorCtor, message, prefix, statusCode, errorCode) {
this._loop = false;
this._errored = true;
const err = new ErrorCtor(
prefix ? `Invalid WebSocket frame: ${message}` : message
);
Error.captureStackTrace(err, this.createError);
err.code = errorCode;
err[kStatusCode] = statusCode;
return err;
}
}
module.exports = Receiver;

602
tools/codex-tmux-driver/node_modules/ws/lib/sender.js generated vendored Normal file
View File

@ -0,0 +1,602 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */
'use strict';
const { Duplex } = require('stream');
const { randomFillSync } = require('crypto');
const PerMessageDeflate = require('./permessage-deflate');
const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants');
const { isBlob, isValidStatusCode } = require('./validation');
const { mask: applyMask, toBuffer } = require('./buffer-util');
const kByteLength = Symbol('kByteLength');
const maskBuffer = Buffer.alloc(4);
const RANDOM_POOL_SIZE = 8 * 1024;
let randomPool;
let randomPoolPointer = RANDOM_POOL_SIZE;
const DEFAULT = 0;
const DEFLATING = 1;
const GET_BLOB_DATA = 2;
/**
* HyBi Sender implementation.
*/
class Sender {
/**
* Creates a Sender instance.
*
* @param {Duplex} socket The connection socket
* @param {Object} [extensions] An object containing the negotiated extensions
* @param {Function} [generateMask] The function used to generate the masking
* key
*/
constructor(socket, extensions, generateMask) {
this._extensions = extensions || {};
if (generateMask) {
this._generateMask = generateMask;
this._maskBuffer = Buffer.alloc(4);
}
this._socket = socket;
this._firstFragment = true;
this._compress = false;
this._bufferedBytes = 0;
this._queue = [];
this._state = DEFAULT;
this.onerror = NOOP;
this[kWebSocket] = undefined;
}
/**
* Frames a piece of data according to the HyBi WebSocket protocol.
*
* @param {(Buffer|String)} data The data to frame
* @param {Object} options Options object
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
* FIN bit
* @param {Function} [options.generateMask] The function used to generate the
* masking key
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {Buffer} [options.maskBuffer] The buffer used to store the masking
* key
* @param {Number} options.opcode The opcode
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
* modified
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
* RSV1 bit
* @return {(Buffer|String)[]} The framed data
* @public
*/
static frame(data, options) {
let mask;
let merge = false;
let offset = 2;
let skipMasking = false;
if (options.mask) {
mask = options.maskBuffer || maskBuffer;
if (options.generateMask) {
options.generateMask(mask);
} else {
if (randomPoolPointer === RANDOM_POOL_SIZE) {
/* istanbul ignore else */
if (randomPool === undefined) {
//
// This is lazily initialized because server-sent frames must not
// be masked so it may never be used.
//
randomPool = Buffer.alloc(RANDOM_POOL_SIZE);
}
randomFillSync(randomPool, 0, RANDOM_POOL_SIZE);
randomPoolPointer = 0;
}
mask[0] = randomPool[randomPoolPointer++];
mask[1] = randomPool[randomPoolPointer++];
mask[2] = randomPool[randomPoolPointer++];
mask[3] = randomPool[randomPoolPointer++];
}
skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0;
offset = 6;
}
let dataLength;
if (typeof data === 'string') {
if (
(!options.mask || skipMasking) &&
options[kByteLength] !== undefined
) {
dataLength = options[kByteLength];
} else {
data = Buffer.from(data);
dataLength = data.length;
}
} else {
dataLength = data.length;
merge = options.mask && options.readOnly && !skipMasking;
}
let payloadLength = dataLength;
if (dataLength >= 65536) {
offset += 8;
payloadLength = 127;
} else if (dataLength > 125) {
offset += 2;
payloadLength = 126;
}
const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset);
target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
if (options.rsv1) target[0] |= 0x40;
target[1] = payloadLength;
if (payloadLength === 126) {
target.writeUInt16BE(dataLength, 2);
} else if (payloadLength === 127) {
target[2] = target[3] = 0;
target.writeUIntBE(dataLength, 4, 6);
}
if (!options.mask) return [target, data];
target[1] |= 0x80;
target[offset - 4] = mask[0];
target[offset - 3] = mask[1];
target[offset - 2] = mask[2];
target[offset - 1] = mask[3];
if (skipMasking) return [target, data];
if (merge) {
applyMask(data, mask, target, offset, dataLength);
return [target];
}
applyMask(data, mask, data, 0, dataLength);
return [target, data];
}
/**
* Sends a close message to the other peer.
*
* @param {Number} [code] The status code component of the body
* @param {(String|Buffer)} [data] The message component of the body
* @param {Boolean} [mask=false] Specifies whether or not to mask the message
* @param {Function} [cb] Callback
* @public
*/
close(code, data, mask, cb) {
let buf;
if (code === undefined) {
buf = EMPTY_BUFFER;
} else if (typeof code !== 'number' || !isValidStatusCode(code)) {
throw new TypeError('First argument must be a valid error code number');
} else if (data === undefined || !data.length) {
buf = Buffer.allocUnsafe(2);
buf.writeUInt16BE(code, 0);
} else {
const length = Buffer.byteLength(data);
if (length > 123) {
throw new RangeError('The message must not be greater than 123 bytes');
}
buf = Buffer.allocUnsafe(2 + length);
buf.writeUInt16BE(code, 0);
if (typeof data === 'string') {
buf.write(data, 2);
} else {
buf.set(data, 2);
}
}
const options = {
[kByteLength]: buf.length,
fin: true,
generateMask: this._generateMask,
mask,
maskBuffer: this._maskBuffer,
opcode: 0x08,
readOnly: false,
rsv1: false
};
if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, buf, false, options, cb]);
} else {
this.sendFrame(Sender.frame(buf, options), cb);
}
}
/**
* Sends a ping message to the other peer.
*
* @param {*} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {Function} [cb] Callback
* @public
*/
ping(data, mask, cb) {
let byteLength;
let readOnly;
if (typeof data === 'string') {
byteLength = Buffer.byteLength(data);
readOnly = false;
} else if (isBlob(data)) {
byteLength = data.size;
readOnly = false;
} else {
data = toBuffer(data);
byteLength = data.length;
readOnly = toBuffer.readOnly;
}
if (byteLength > 125) {
throw new RangeError('The data size must not be greater than 125 bytes');
}
const options = {
[kByteLength]: byteLength,
fin: true,
generateMask: this._generateMask,
mask,
maskBuffer: this._maskBuffer,
opcode: 0x09,
readOnly,
rsv1: false
};
if (isBlob(data)) {
if (this._state !== DEFAULT) {
this.enqueue([this.getBlobData, data, false, options, cb]);
} else {
this.getBlobData(data, false, options, cb);
}
} else if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, data, false, options, cb]);
} else {
this.sendFrame(Sender.frame(data, options), cb);
}
}
/**
* Sends a pong message to the other peer.
*
* @param {*} data The message to send
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
* @param {Function} [cb] Callback
* @public
*/
pong(data, mask, cb) {
let byteLength;
let readOnly;
if (typeof data === 'string') {
byteLength = Buffer.byteLength(data);
readOnly = false;
} else if (isBlob(data)) {
byteLength = data.size;
readOnly = false;
} else {
data = toBuffer(data);
byteLength = data.length;
readOnly = toBuffer.readOnly;
}
if (byteLength > 125) {
throw new RangeError('The data size must not be greater than 125 bytes');
}
const options = {
[kByteLength]: byteLength,
fin: true,
generateMask: this._generateMask,
mask,
maskBuffer: this._maskBuffer,
opcode: 0x0a,
readOnly,
rsv1: false
};
if (isBlob(data)) {
if (this._state !== DEFAULT) {
this.enqueue([this.getBlobData, data, false, options, cb]);
} else {
this.getBlobData(data, false, options, cb);
}
} else if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, data, false, options, cb]);
} else {
this.sendFrame(Sender.frame(data, options), cb);
}
}
/**
* Sends a data message to the other peer.
*
* @param {*} data The message to send
* @param {Object} options Options object
* @param {Boolean} [options.binary=false] Specifies whether `data` is binary
* or text
* @param {Boolean} [options.compress=false] Specifies whether or not to
* compress `data`
* @param {Boolean} [options.fin=false] Specifies whether the fragment is the
* last one
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {Function} [cb] Callback
* @public
*/
send(data, options, cb) {
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
let opcode = options.binary ? 2 : 1;
let rsv1 = options.compress;
let byteLength;
let readOnly;
if (typeof data === 'string') {
byteLength = Buffer.byteLength(data);
readOnly = false;
} else if (isBlob(data)) {
byteLength = data.size;
readOnly = false;
} else {
data = toBuffer(data);
byteLength = data.length;
readOnly = toBuffer.readOnly;
}
if (this._firstFragment) {
this._firstFragment = false;
if (
rsv1 &&
perMessageDeflate &&
perMessageDeflate.params[
perMessageDeflate._isServer
? 'server_no_context_takeover'
: 'client_no_context_takeover'
]
) {
rsv1 = byteLength >= perMessageDeflate._threshold;
}
this._compress = rsv1;
} else {
rsv1 = false;
opcode = 0;
}
if (options.fin) this._firstFragment = true;
const opts = {
[kByteLength]: byteLength,
fin: options.fin,
generateMask: this._generateMask,
mask: options.mask,
maskBuffer: this._maskBuffer,
opcode,
readOnly,
rsv1
};
if (isBlob(data)) {
if (this._state !== DEFAULT) {
this.enqueue([this.getBlobData, data, this._compress, opts, cb]);
} else {
this.getBlobData(data, this._compress, opts, cb);
}
} else if (this._state !== DEFAULT) {
this.enqueue([this.dispatch, data, this._compress, opts, cb]);
} else {
this.dispatch(data, this._compress, opts, cb);
}
}
/**
* Gets the contents of a blob as binary data.
*
* @param {Blob} blob The blob
* @param {Boolean} [compress=false] Specifies whether or not to compress
* the data
* @param {Object} options Options object
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
* FIN bit
* @param {Function} [options.generateMask] The function used to generate the
* masking key
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {Buffer} [options.maskBuffer] The buffer used to store the masking
* key
* @param {Number} options.opcode The opcode
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
* modified
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
* RSV1 bit
* @param {Function} [cb] Callback
* @private
*/
getBlobData(blob, compress, options, cb) {
this._bufferedBytes += options[kByteLength];
this._state = GET_BLOB_DATA;
blob
.arrayBuffer()
.then((arrayBuffer) => {
if (this._socket.destroyed) {
const err = new Error(
'The socket was closed while the blob was being read'
);
//
// `callCallbacks` is called in the next tick to ensure that errors
// that might be thrown in the callbacks behave like errors thrown
// outside the promise chain.
//
process.nextTick(callCallbacks, this, err, cb);
return;
}
this._bufferedBytes -= options[kByteLength];
const data = toBuffer(arrayBuffer);
if (!compress) {
this._state = DEFAULT;
this.sendFrame(Sender.frame(data, options), cb);
this.dequeue();
} else {
this.dispatch(data, compress, options, cb);
}
})
.catch((err) => {
//
// `onError` is called in the next tick for the same reason that
// `callCallbacks` above is.
//
process.nextTick(onError, this, err, cb);
});
}
/**
* Dispatches a message.
*
* @param {(Buffer|String)} data The message to send
* @param {Boolean} [compress=false] Specifies whether or not to compress
* `data`
* @param {Object} options Options object
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
* FIN bit
* @param {Function} [options.generateMask] The function used to generate the
* masking key
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
* `data`
* @param {Buffer} [options.maskBuffer] The buffer used to store the masking
* key
* @param {Number} options.opcode The opcode
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
* modified
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
* RSV1 bit
* @param {Function} [cb] Callback
* @private
*/
dispatch(data, compress, options, cb) {
if (!compress) {
this.sendFrame(Sender.frame(data, options), cb);
return;
}
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
this._bufferedBytes += options[kByteLength];
this._state = DEFLATING;
perMessageDeflate.compress(data, options.fin, (_, buf) => {
if (this._socket.destroyed) {
const err = new Error(
'The socket was closed while data was being compressed'
);
callCallbacks(this, err, cb);
return;
}
this._bufferedBytes -= options[kByteLength];
this._state = DEFAULT;
options.readOnly = false;
this.sendFrame(Sender.frame(buf, options), cb);
this.dequeue();
});
}
/**
* Executes queued send operations.
*
* @private
*/
dequeue() {
while (this._state === DEFAULT && this._queue.length) {
const params = this._queue.shift();
this._bufferedBytes -= params[3][kByteLength];
Reflect.apply(params[0], this, params.slice(1));
}
}
/**
* Enqueues a send operation.
*
* @param {Array} params Send operation parameters.
* @private
*/
enqueue(params) {
this._bufferedBytes += params[3][kByteLength];
this._queue.push(params);
}
/**
* Sends a frame.
*
* @param {(Buffer | String)[]} list The frame to send
* @param {Function} [cb] Callback
* @private
*/
sendFrame(list, cb) {
if (list.length === 2) {
this._socket.cork();
this._socket.write(list[0]);
this._socket.write(list[1], cb);
this._socket.uncork();
} else {
this._socket.write(list[0], cb);
}
}
}
module.exports = Sender;
/**
* Calls queued callbacks with an error.
*
* @param {Sender} sender The `Sender` instance
* @param {Error} err The error to call the callbacks with
* @param {Function} [cb] The first callback
* @private
*/
function callCallbacks(sender, err, cb) {
if (typeof cb === 'function') cb(err);
for (let i = 0; i < sender._queue.length; i++) {
const params = sender._queue[i];
const callback = params[params.length - 1];
if (typeof callback === 'function') callback(err);
}
}
/**
* Handles a `Sender` error.
*
* @param {Sender} sender The `Sender` instance
* @param {Error} err The error
* @param {Function} [cb] The first pending callback
* @private
*/
function onError(sender, err, cb) {
callCallbacks(sender, err, cb);
sender.onerror(err);
}

161
tools/codex-tmux-driver/node_modules/ws/lib/stream.js generated vendored Normal file
View File

@ -0,0 +1,161 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^WebSocket$" }] */
'use strict';
const WebSocket = require('./websocket');
const { Duplex } = require('stream');
/**
* Emits the `'close'` event on a stream.
*
* @param {Duplex} stream The stream.
* @private
*/
function emitClose(stream) {
stream.emit('close');
}
/**
* The listener of the `'end'` event.
*
* @private
*/
function duplexOnEnd() {
if (!this.destroyed && this._writableState.finished) {
this.destroy();
}
}
/**
* The listener of the `'error'` event.
*
* @param {Error} err The error
* @private
*/
function duplexOnError(err) {
this.removeListener('error', duplexOnError);
this.destroy();
if (this.listenerCount('error') === 0) {
// Do not suppress the throwing behavior.
this.emit('error', err);
}
}
/**
* Wraps a `WebSocket` in a duplex stream.
*
* @param {WebSocket} ws The `WebSocket` to wrap
* @param {Object} [options] The options for the `Duplex` constructor
* @return {Duplex} The duplex stream
* @public
*/
function createWebSocketStream(ws, options) {
let terminateOnDestroy = true;
const duplex = new Duplex({
...options,
autoDestroy: false,
emitClose: false,
objectMode: false,
writableObjectMode: false
});
ws.on('message', function message(msg, isBinary) {
const data =
!isBinary && duplex._readableState.objectMode ? msg.toString() : msg;
if (!duplex.push(data)) ws.pause();
});
ws.once('error', function error(err) {
if (duplex.destroyed) return;
// Prevent `ws.terminate()` from being called by `duplex._destroy()`.
//
// - If the `'error'` event is emitted before the `'open'` event, then
// `ws.terminate()` is a noop as no socket is assigned.
// - Otherwise, the error is re-emitted by the listener of the `'error'`
// event of the `Receiver` object. The listener already closes the
// connection by calling `ws.close()`. This allows a close frame to be
// sent to the other peer. If `ws.terminate()` is called right after this,
// then the close frame might not be sent.
terminateOnDestroy = false;
duplex.destroy(err);
});
ws.once('close', function close() {
if (duplex.destroyed) return;
duplex.push(null);
});
duplex._destroy = function (err, callback) {
if (ws.readyState === ws.CLOSED) {
callback(err);
process.nextTick(emitClose, duplex);
return;
}
let called = false;
ws.once('error', function error(err) {
called = true;
callback(err);
});
ws.once('close', function close() {
if (!called) callback(err);
process.nextTick(emitClose, duplex);
});
if (terminateOnDestroy) ws.terminate();
};
duplex._final = function (callback) {
if (ws.readyState === ws.CONNECTING) {
ws.once('open', function open() {
duplex._final(callback);
});
return;
}
// If the value of the `_socket` property is `null` it means that `ws` is a
// client websocket and the handshake failed. In fact, when this happens, a
// socket is never assigned to the websocket. Wait for the `'error'` event
// that will be emitted by the websocket.
if (ws._socket === null) return;
if (ws._socket._writableState.finished) {
callback();
if (duplex._readableState.endEmitted) duplex.destroy();
} else {
ws._socket.once('finish', function finish() {
// `duplex` is not destroyed here because the `'end'` event will be
// emitted on `duplex` after this `'finish'` event. The EOF signaling
// `null` chunk is, in fact, pushed when the websocket emits `'close'`.
callback();
});
ws.close();
}
};
duplex._read = function () {
if (ws.isPaused) ws.resume();
};
duplex._write = function (chunk, encoding, callback) {
if (ws.readyState === ws.CONNECTING) {
ws.once('open', function open() {
duplex._write(chunk, encoding, callback);
});
return;
}
ws.send(chunk, callback);
};
duplex.on('end', duplexOnEnd);
duplex.on('error', duplexOnError);
return duplex;
}
module.exports = createWebSocketStream;

View File

@ -0,0 +1,62 @@
'use strict';
const { tokenChars } = require('./validation');
/**
* Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names.
*
* @param {String} header The field value of the header
* @return {Set} The subprotocol names
* @public
*/
function parse(header) {
const protocols = new Set();
let start = -1;
let end = -1;
let i = 0;
for (i; i < header.length; i++) {
const code = header.charCodeAt(i);
if (end === -1 && tokenChars[code] === 1) {
if (start === -1) start = i;
} else if (
i !== 0 &&
(code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
) {
if (end === -1 && start !== -1) end = i;
} else if (code === 0x2c /* ',' */) {
if (start === -1) {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
if (end === -1) end = i;
const protocol = header.slice(start, end);
if (protocols.has(protocol)) {
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
}
protocols.add(protocol);
start = end = -1;
} else {
throw new SyntaxError(`Unexpected character at index ${i}`);
}
}
if (start === -1 || end !== -1) {
throw new SyntaxError('Unexpected end of input');
}
const protocol = header.slice(start, i);
if (protocols.has(protocol)) {
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
}
protocols.add(protocol);
return protocols;
}
module.exports = { parse };

View File

@ -0,0 +1,152 @@
'use strict';
const { isUtf8 } = require('buffer');
const { hasBlob } = require('./constants');
//
// Allowed token characters:
//
// '!', '#', '$', '%', '&', ''', '*', '+', '-',
// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
//
// tokenChars[32] === 0 // ' '
// tokenChars[33] === 1 // '!'
// tokenChars[34] === 0 // '"'
// ...
//
// prettier-ignore
const tokenChars = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
];
/**
* Checks if a status code is allowed in a close frame.
*
* @param {Number} code The status code
* @return {Boolean} `true` if the status code is valid, else `false`
* @public
*/
function isValidStatusCode(code) {
return (
(code >= 1000 &&
code <= 1014 &&
code !== 1004 &&
code !== 1005 &&
code !== 1006) ||
(code >= 3000 && code <= 4999)
);
}
/**
* Checks if a given buffer contains only correct UTF-8.
* Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by
* Markus Kuhn.
*
* @param {Buffer} buf The buffer to check
* @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false`
* @public
*/
function _isValidUTF8(buf) {
const len = buf.length;
let i = 0;
while (i < len) {
if ((buf[i] & 0x80) === 0) {
// 0xxxxxxx
i++;
} else if ((buf[i] & 0xe0) === 0xc0) {
// 110xxxxx 10xxxxxx
if (
i + 1 === len ||
(buf[i + 1] & 0xc0) !== 0x80 ||
(buf[i] & 0xfe) === 0xc0 // Overlong
) {
return false;
}
i += 2;
} else if ((buf[i] & 0xf0) === 0xe0) {
// 1110xxxx 10xxxxxx 10xxxxxx
if (
i + 2 >= len ||
(buf[i + 1] & 0xc0) !== 0x80 ||
(buf[i + 2] & 0xc0) !== 0x80 ||
(buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong
(buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF)
) {
return false;
}
i += 3;
} else if ((buf[i] & 0xf8) === 0xf0) {
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
if (
i + 3 >= len ||
(buf[i + 1] & 0xc0) !== 0x80 ||
(buf[i + 2] & 0xc0) !== 0x80 ||
(buf[i + 3] & 0xc0) !== 0x80 ||
(buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong
(buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
buf[i] > 0xf4 // > U+10FFFF
) {
return false;
}
i += 4;
} else {
return false;
}
}
return true;
}
/**
* Determines whether a value is a `Blob`.
*
* @param {*} value The value to be tested
* @return {Boolean} `true` if `value` is a `Blob`, else `false`
* @private
*/
function isBlob(value) {
return (
hasBlob &&
typeof value === 'object' &&
typeof value.arrayBuffer === 'function' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
(value[Symbol.toStringTag] === 'Blob' ||
value[Symbol.toStringTag] === 'File')
);
}
module.exports = {
isBlob,
isValidStatusCode,
isValidUTF8: _isValidUTF8,
tokenChars
};
if (isUtf8) {
module.exports.isValidUTF8 = function (buf) {
return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf);
};
} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) {
try {
const isValidUTF8 = require('utf-8-validate');
module.exports.isValidUTF8 = function (buf) {
return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf);
};
} catch (e) {
// Continue regardless of the error.
}
}

View File

@ -0,0 +1,550 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */
'use strict';
const EventEmitter = require('events');
const http = require('http');
const { Duplex } = require('stream');
const { createHash } = require('crypto');
const extension = require('./extension');
const PerMessageDeflate = require('./permessage-deflate');
const subprotocol = require('./subprotocol');
const WebSocket = require('./websocket');
const { GUID, kWebSocket } = require('./constants');
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
const RUNNING = 0;
const CLOSING = 1;
const CLOSED = 2;
/**
* Class representing a WebSocket server.
*
* @extends EventEmitter
*/
class WebSocketServer extends EventEmitter {
/**
* Create a `WebSocketServer` instance.
*
* @param {Object} options Configuration options
* @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
* multiple times in the same tick
* @param {Boolean} [options.autoPong=true] Specifies whether or not to
* automatically send a pong in response to a ping
* @param {Number} [options.backlog=511] The maximum length of the queue of
* pending connections
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
* track clients
* @param {Function} [options.handleProtocols] A hook to handle protocols
* @param {String} [options.host] The hostname where to bind the server
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
* size
* @param {Boolean} [options.noServer=false] Enable no server mode
* @param {String} [options.path] Accept only connections matching this path
* @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
* permessage-deflate
* @param {Number} [options.port] The port where to bind the server
* @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
* server to use
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
* not to skip UTF-8 validation for text and close messages
* @param {Function} [options.verifyClient] A hook to reject connections
* @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket`
* class to use. It must be the `WebSocket` class or class that extends it
* @param {Function} [callback] A listener for the `listening` event
*/
constructor(options, callback) {
super();
options = {
allowSynchronousEvents: true,
autoPong: true,
maxPayload: 100 * 1024 * 1024,
skipUTF8Validation: false,
perMessageDeflate: false,
handleProtocols: null,
clientTracking: true,
verifyClient: null,
noServer: false,
backlog: null, // use default (511 as implemented in net.js)
server: null,
host: null,
path: null,
port: null,
WebSocket,
...options
};
if (
(options.port == null && !options.server && !options.noServer) ||
(options.port != null && (options.server || options.noServer)) ||
(options.server && options.noServer)
) {
throw new TypeError(
'One and only one of the "port", "server", or "noServer" options ' +
'must be specified'
);
}
if (options.port != null) {
this._server = http.createServer((req, res) => {
const body = http.STATUS_CODES[426];
res.writeHead(426, {
'Content-Length': body.length,
'Content-Type': 'text/plain'
});
res.end(body);
});
this._server.listen(
options.port,
options.host,
options.backlog,
callback
);
} else if (options.server) {
this._server = options.server;
}
if (this._server) {
const emitConnection = this.emit.bind(this, 'connection');
this._removeListeners = addListeners(this._server, {
listening: this.emit.bind(this, 'listening'),
error: this.emit.bind(this, 'error'),
upgrade: (req, socket, head) => {
this.handleUpgrade(req, socket, head, emitConnection);
}
});
}
if (options.perMessageDeflate === true) options.perMessageDeflate = {};
if (options.clientTracking) {
this.clients = new Set();
this._shouldEmitClose = false;
}
this.options = options;
this._state = RUNNING;
}
/**
* Returns the bound address, the address family name, and port of the server
* as reported by the operating system if listening on an IP socket.
* If the server is listening on a pipe or UNIX domain socket, the name is
* returned as a string.
*
* @return {(Object|String|null)} The address of the server
* @public
*/
address() {
if (this.options.noServer) {
throw new Error('The server is operating in "noServer" mode');
}
if (!this._server) return null;
return this._server.address();
}
/**
* Stop the server from accepting new connections and emit the `'close'` event
* when all existing connections are closed.
*
* @param {Function} [cb] A one-time listener for the `'close'` event
* @public
*/
close(cb) {
if (this._state === CLOSED) {
if (cb) {
this.once('close', () => {
cb(new Error('The server is not running'));
});
}
process.nextTick(emitClose, this);
return;
}
if (cb) this.once('close', cb);
if (this._state === CLOSING) return;
this._state = CLOSING;
if (this.options.noServer || this.options.server) {
if (this._server) {
this._removeListeners();
this._removeListeners = this._server = null;
}
if (this.clients) {
if (!this.clients.size) {
process.nextTick(emitClose, this);
} else {
this._shouldEmitClose = true;
}
} else {
process.nextTick(emitClose, this);
}
} else {
const server = this._server;
this._removeListeners();
this._removeListeners = this._server = null;
//
// The HTTP/S server was created internally. Close it, and rely on its
// `'close'` event.
//
server.close(() => {
emitClose(this);
});
}
}
/**
* See if a given request should be handled by this server instance.
*
* @param {http.IncomingMessage} req Request object to inspect
* @return {Boolean} `true` if the request is valid, else `false`
* @public
*/
shouldHandle(req) {
if (this.options.path) {
const index = req.url.indexOf('?');
const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
if (pathname !== this.options.path) return false;
}
return true;
}
/**
* Handle a HTTP Upgrade request.
*
* @param {http.IncomingMessage} req The request object
* @param {Duplex} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Function} cb Callback
* @public
*/
handleUpgrade(req, socket, head, cb) {
socket.on('error', socketOnError);
const key = req.headers['sec-websocket-key'];
const upgrade = req.headers.upgrade;
const version = +req.headers['sec-websocket-version'];
if (req.method !== 'GET') {
const message = 'Invalid HTTP method';
abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
return;
}
if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
const message = 'Invalid Upgrade header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
if (key === undefined || !keyRegex.test(key)) {
const message = 'Missing or invalid Sec-WebSocket-Key header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
if (version !== 13 && version !== 8) {
const message = 'Missing or invalid Sec-WebSocket-Version header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, {
'Sec-WebSocket-Version': '13, 8'
});
return;
}
if (!this.shouldHandle(req)) {
abortHandshake(socket, 400);
return;
}
const secWebSocketProtocol = req.headers['sec-websocket-protocol'];
let protocols = new Set();
if (secWebSocketProtocol !== undefined) {
try {
protocols = subprotocol.parse(secWebSocketProtocol);
} catch (err) {
const message = 'Invalid Sec-WebSocket-Protocol header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
}
const secWebSocketExtensions = req.headers['sec-websocket-extensions'];
const extensions = {};
if (
this.options.perMessageDeflate &&
secWebSocketExtensions !== undefined
) {
const perMessageDeflate = new PerMessageDeflate(
this.options.perMessageDeflate,
true,
this.options.maxPayload
);
try {
const offers = extension.parse(secWebSocketExtensions);
if (offers[PerMessageDeflate.extensionName]) {
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
}
} catch (err) {
const message =
'Invalid or unacceptable Sec-WebSocket-Extensions header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
}
//
// Optionally call external client verification handler.
//
if (this.options.verifyClient) {
const info = {
origin:
req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
secure: !!(req.socket.authorized || req.socket.encrypted),
req
};
if (this.options.verifyClient.length === 2) {
this.options.verifyClient(info, (verified, code, message, headers) => {
if (!verified) {
return abortHandshake(socket, code || 401, message, headers);
}
this.completeUpgrade(
extensions,
key,
protocols,
req,
socket,
head,
cb
);
});
return;
}
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
}
this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
}
/**
* Upgrade the connection to WebSocket.
*
* @param {Object} extensions The accepted extensions
* @param {String} key The value of the `Sec-WebSocket-Key` header
* @param {Set} protocols The subprotocols
* @param {http.IncomingMessage} req The request object
* @param {Duplex} socket The network socket between the server and client
* @param {Buffer} head The first packet of the upgraded stream
* @param {Function} cb Callback
* @throws {Error} If called more than once with the same socket
* @private
*/
completeUpgrade(extensions, key, protocols, req, socket, head, cb) {
//
// Destroy the socket if the client has already sent a FIN packet.
//
if (!socket.readable || !socket.writable) return socket.destroy();
if (socket[kWebSocket]) {
throw new Error(
'server.handleUpgrade() was called more than once with the same ' +
'socket, possibly due to a misconfiguration'
);
}
if (this._state > RUNNING) return abortHandshake(socket, 503);
const digest = createHash('sha1')
.update(key + GUID)
.digest('base64');
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${digest}`
];
const ws = new this.options.WebSocket(null, undefined, this.options);
if (protocols.size) {
//
// Optionally call external protocol selection handler.
//
const protocol = this.options.handleProtocols
? this.options.handleProtocols(protocols, req)
: protocols.values().next().value;
if (protocol) {
headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
ws._protocol = protocol;
}
}
if (extensions[PerMessageDeflate.extensionName]) {
const params = extensions[PerMessageDeflate.extensionName].params;
const value = extension.format({
[PerMessageDeflate.extensionName]: [params]
});
headers.push(`Sec-WebSocket-Extensions: ${value}`);
ws._extensions = extensions;
}
//
// Allow external modification/inspection of handshake headers.
//
this.emit('headers', headers, req);
socket.write(headers.concat('\r\n').join('\r\n'));
socket.removeListener('error', socketOnError);
ws.setSocket(socket, head, {
allowSynchronousEvents: this.options.allowSynchronousEvents,
maxPayload: this.options.maxPayload,
skipUTF8Validation: this.options.skipUTF8Validation
});
if (this.clients) {
this.clients.add(ws);
ws.on('close', () => {
this.clients.delete(ws);
if (this._shouldEmitClose && !this.clients.size) {
process.nextTick(emitClose, this);
}
});
}
cb(ws, req);
}
}
module.exports = WebSocketServer;
/**
* Add event listeners on an `EventEmitter` using a map of <event, listener>
* pairs.
*
* @param {EventEmitter} server The event emitter
* @param {Object.<String, Function>} map The listeners to add
* @return {Function} A function that will remove the added listeners when
* called
* @private
*/
function addListeners(server, map) {
for (const event of Object.keys(map)) server.on(event, map[event]);
return function removeListeners() {
for (const event of Object.keys(map)) {
server.removeListener(event, map[event]);
}
};
}
/**
* Emit a `'close'` event on an `EventEmitter`.
*
* @param {EventEmitter} server The event emitter
* @private
*/
function emitClose(server) {
server._state = CLOSED;
server.emit('close');
}
/**
* Handle socket errors.
*
* @private
*/
function socketOnError() {
this.destroy();
}
/**
* Close the connection when preconditions are not fulfilled.
*
* @param {Duplex} socket The socket of the upgrade request
* @param {Number} code The HTTP response status code
* @param {String} [message] The HTTP response body
* @param {Object} [headers] Additional HTTP response headers
* @private
*/
function abortHandshake(socket, code, message, headers) {
//
// The socket is writable unless the user destroyed or ended it before calling
// `server.handleUpgrade()` or in the `verifyClient` function, which is a user
// error. Handling this does not make much sense as the worst that can happen
// is that some of the data written by the user might be discarded due to the
// call to `socket.end()` below, which triggers an `'error'` event that in
// turn causes the socket to be destroyed.
//
message = message || http.STATUS_CODES[code];
headers = {
Connection: 'close',
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(message),
...headers
};
socket.once('finish', socket.destroy);
socket.end(
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
Object.keys(headers)
.map((h) => `${h}: ${headers[h]}`)
.join('\r\n') +
'\r\n\r\n' +
message
);
}
/**
* Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least
* one listener for it, otherwise call `abortHandshake()`.
*
* @param {WebSocketServer} server The WebSocket server
* @param {http.IncomingMessage} req The request object
* @param {Duplex} socket The socket of the upgrade request
* @param {Number} code The HTTP response status code
* @param {String} message The HTTP response body
* @param {Object} [headers] The HTTP response headers
* @private
*/
function abortHandshakeOrEmitwsClientError(
server,
req,
socket,
code,
message,
headers
) {
if (server.listenerCount('wsClientError')) {
const err = new Error(message);
Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
server.emit('wsClientError', err, socket, req);
} else {
abortHandshake(socket, code, message, headers);
}
}

1388
tools/codex-tmux-driver/node_modules/ws/lib/websocket.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

69
tools/codex-tmux-driver/node_modules/ws/package.json generated vendored Normal file
View File

@ -0,0 +1,69 @@
{
"name": "ws",
"version": "8.18.3",
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
"keywords": [
"HyBi",
"Push",
"RFC-6455",
"WebSocket",
"WebSockets",
"real-time"
],
"homepage": "https://github.com/websockets/ws",
"bugs": "https://github.com/websockets/ws/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/websockets/ws.git"
},
"author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
"license": "MIT",
"main": "index.js",
"exports": {
".": {
"browser": "./browser.js",
"import": "./wrapper.mjs",
"require": "./index.js"
},
"./package.json": "./package.json"
},
"browser": "browser.js",
"engines": {
"node": ">=10.0.0"
},
"files": [
"browser.js",
"index.js",
"lib/*.js",
"wrapper.mjs"
],
"scripts": {
"test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
"integration": "mocha --throw-deprecation test/*.integration.js",
"lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
},
"devDependencies": {
"benchmark": "^2.1.4",
"bufferutil": "^4.0.1",
"eslint": "^9.0.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.0.0",
"globals": "^16.0.0",
"mocha": "^8.4.0",
"nyc": "^15.0.0",
"prettier": "^3.0.0",
"utf-8-validate": "^6.0.0"
}
}

8
tools/codex-tmux-driver/node_modules/ws/wrapper.mjs generated vendored Normal file
View File

@ -0,0 +1,8 @@
import createWebSocketStream from './lib/stream.js';
import Receiver from './lib/receiver.js';
import Sender from './lib/sender.js';
import WebSocket from './lib/websocket.js';
import WebSocketServer from './lib/websocket-server.js';
export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer };
export default WebSocket;

View File

@ -0,0 +1,38 @@
{
"name": "codex-tmux-driver",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codex-tmux-driver",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"ws": "^8.16.0"
},
"devDependencies": {}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@ -0,0 +1,16 @@
{
"name": "codex-tmux-driver",
"version": "1.0.0",
"description": "AI driver for tmux sessions with Codex event monitoring",
"main": "codex-tmux-driver.js",
"scripts": {
"start": "node codex-tmux-driver.js",
"test": "node test.js"
},
"dependencies": {
"ws": "^8.16.0"
},
"devDependencies": {},
"author": "Nyash Team",
"license": "MIT"
}

View File

@ -0,0 +1,67 @@
#!/bin/bash
# AI Trinity クイックスタート - Claude×2 + Codex を一発起動
echo "🤖 AI Trinity (Claude×2 + Codex) 起動スクリプト"
echo "================================================"
# Hook serverが起動しているか確認
if ! lsof -i:8770 >/dev/null 2>&1; then
echo "⚠️ Hook serverが起動していません"
echo "💡 別ターミナルで以下を実行してください:"
echo " HOOK_SERVER_PORT=8770 node hook-server.js"
echo ""
echo -n "Hook serverを起動してから続行しますか (y/n): "
read answer
if [ "$answer" != "y" ]; then
echo "中止しました"
exit 1
fi
fi
# 既存セッションのクリーンアップ
echo ""
echo "🧹 既存セッションをクリーンアップ中..."
for session in claude1-8770 claude2-8770 codex-8770; do
if tmux has-session -t "$session" 2>/dev/null; then
tmux kill-session -t "$session"
echo " - $session を終了しました"
fi
done
# Claude Code 1を起動
echo ""
echo "🚀 Claude Code #1 を起動中..."
./start-ai-tmux.sh claude1-8770 /home/tomoaki/.volta/bin/codex
sleep 2
# Claude Code 2を起動
echo "🚀 Claude Code #2 を起動中..."
./start-ai-tmux.sh claude2-8770 /home/tomoaki/.volta/bin/codex
sleep 2
# Codexを起動実際のパスは環境に応じて変更必要
echo "🚀 Codex を起動中..."
if [ -z "$REAL_CODEX_PATH" ]; then
echo "⚠️ REAL_CODEX_PATH が設定されていません"
echo "💡 export REAL_CODEX_PATH=/path/to/real/codex"
echo " スキップします..."
else
./start-ai-tmux.sh codex-8770 "$REAL_CODEX_PATH" --ask-for-approval never --sandbox danger-full-access
fi
# 状態表示
echo ""
echo "==============================================="
echo "📊 最終状態:"
./manage-ai-sessions.sh status
echo ""
echo "🎯 次のステップ:"
echo " 1. 各セッションに接続: tmux attach -t <session-name>"
echo " 2. AI間でメッセージ送信テスト"
echo " 3. WebSocket経由での通信テスト"
echo ""
echo "📝 便利なコマンド:"
echo " ./manage-ai-sessions.sh send claude1-8770 'Hello!'"
echo " ./manage-ai-sessions.sh broadcast 'Hello everyone!'"
echo " ./manage-ai-sessions.sh attach claude1-8770"

View File

@ -0,0 +1,14 @@
#!/bin/bash
# クリーンな環境でCodexを起動
echo "🧹 Starting Codex with clean environment..."
# すべてのフック関連出力を無効化
export CODEX_REAL_BIN=/home/tomoaki/.volta/bin/codex
export CODEX_HOOK_SERVER=ws://localhost:8770
export CODEX_USE_SCRIPT_PTY=false # scriptコマンドを無効化
export CODEX_HOOK_BANNER=false # バナー出力を無効化
export CODEX_LOG_FILE=/dev/null # ログ出力を無効化
# stdinを正しく接続
exec node /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver/codex-hook-wrapper.js

View File

@ -0,0 +1,30 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Clearing and sending new message...');
// まずCtrl+Uで現在の行をクリア
ws.send(JSON.stringify({
type: 'inject-input',
data: '\x15' // Ctrl+U (行クリア)
}));
// 少し待ってから新しいメッセージ
setTimeout(() => {
ws.send(JSON.stringify({
type: 'inject-input',
data: 'やっほーNyashから挨拶にゃ🐱'
}));
}, 100);
setTimeout(() => {
ws.close();
console.log('✅ Clear and type complete!');
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,30 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', async () => {
console.log('✨ Sending Enter key first, then message...');
// まずEnterキーを送信して現在の入力を実行
ws.send(JSON.stringify({
type: 'inject-input',
data: '' // 空文字列 + Enterで現在の行を実行
}));
// 少し待ってから新しいメッセージ
setTimeout(() => {
ws.send(JSON.stringify({
type: 'inject-input',
data: 'Nyashプロジェクトから挨拶だにゃJIT開発頑張ってるにゃ🐱'
}));
setTimeout(() => {
ws.close();
console.log('✅ Complete!');
}, 500);
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,21 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Sending greeting to Codex...');
ws.send(JSON.stringify({
type: 'inject-input',
data: 'こんにちはNyashプロジェクトから挨拶だにゃ🐱 JITの開発はどう'
}));
setTimeout(() => {
ws.close();
console.log('✅ Message sent!');
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,21 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('Connected! Sending simple text...');
// シンプルなテキストを送信
ws.send(JSON.stringify({
type: 'inject-input',
data: 'hello'
}));
setTimeout(() => {
ws.close();
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,39 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Testing special key sequences...');
// いろんな特殊キーを試す
const keys = [
{ data: 'Test with Enter\x0D', desc: 'CR (\\x0D) - Classic Enter' },
{ data: 'Test with Return\x0D\x0A', desc: 'CRLF (\\x0D\\x0A)' },
{ data: 'Test with special\x1B[13~', desc: 'ESC sequence for Enter' },
{ data: 'Test with raw\x1B\x0D', desc: 'ESC + CR' },
{ data: 'Test submit', desc: 'Just text (for Ctrl+J manual test)' }
];
let index = 0;
const interval = setInterval(() => {
if (index >= keys.length) {
clearInterval(interval);
ws.close();
console.log('✅ All key tests sent!');
console.log('💡 Try pressing Ctrl+J manually after the last message!');
return;
}
const key = keys[index];
console.log(` Sending: ${key.desc}`);
ws.send(JSON.stringify({
type: 'inject-input',
data: key.data
}));
index++;
}, 2000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,38 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Testing different line endings...');
// いろんな改行を試す
const messages = [
{ data: 'Test1: Normal', desc: 'Normal' },
{ data: 'Test2:\nWith LF', desc: 'With \\n (LF)' },
{ data: 'Test3:\rWith CR', desc: 'With \\r (CR)' },
{ data: 'Test4:\r\nWith CRLF', desc: 'With \\r\\n (CRLF)' },
{ data: 'Test5:\x0AWith Ctrl+J', desc: 'With Ctrl+J' }
];
let index = 0;
const interval = setInterval(() => {
if (index >= messages.length) {
clearInterval(interval);
ws.close();
console.log('✅ All tests sent!');
return;
}
const msg = messages[index];
console.log(` Sending: ${msg.desc}`);
ws.send(JSON.stringify({
type: 'inject-input',
data: msg.data
}));
index++;
}, 2000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,35 @@
#!/usr/bin/env node
// send-to-other.js - 他のCodexセッションにメッセージ送信
const WebSocket = require('ws');
const message = process.argv[2];
const to = process.argv[3];
const from = process.argv[4] || 'unknown';
if (!message || !to) {
console.log('使い方: node send-to-other.js "メッセージ" 送信先 [送信元名]');
console.log('例: node send-to-other.js "こんにちは" codex2 codex1');
process.exit(1);
}
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log(`📤 ${from}${to}: "${message}"`);
ws.send(JSON.stringify({
type: 'inject-input',
data: message,
source: from,
target: to
}));
setTimeout(() => {
console.log('✅ Message sent!');
ws.close();
process.exit(0);
}, 500);
});
ws.on('error', (e) => {
console.error('❌ Error:', e.message);
process.exit(1);
});

View File

@ -0,0 +1,24 @@
const TmuxCodexController = require('./tmux-codex-controller');
async function sendMessage(message) {
const controller = new TmuxCodexController();
try {
// メッセージを送信Enterも自動
await controller.sendKeys(message);
console.log(`✅ Sent: "${message}"`);
// 少し待って画面を確認
await controller.sleep(2000);
const screen = await controller.capture();
console.log('\n📺 Current screen (last 10 lines):');
console.log(screen.split('\n').slice(-10).join('\n'));
} catch (err) {
console.error('❌ Error:', err);
}
}
// コマンドライン引数からメッセージを取得
const message = process.argv.slice(2).join(' ') || 'Hello from Nyash!';
sendMessage(message);

View File

@ -0,0 +1,22 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Sending message with Ctrl+J for newline...');
// Ctrl+J (改行) を含むメッセージ
ws.send(JSON.stringify({
type: 'inject-input',
data: 'Nyashです\x0AJITの進捗どう\x0A箱作戦は最高にゃ🐱' // \x0A = Ctrl+J (LF)
}));
setTimeout(() => {
ws.close();
console.log('✅ Message with Ctrl+J sent!');
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,22 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Sending message with newline to Codex...');
// 改行を含むメッセージ
ws.send(JSON.stringify({
type: 'inject-input',
data: '\nこんにちはNyashプロジェクトだにゃ🐱\nJIT開発の進捗はどう'
}));
setTimeout(() => {
ws.close();
console.log('✅ Message sent with newlines!');
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,71 @@
#!/bin/bash
# 汎用AI起動スクリプト - Claude Code/Codexをtmux経由で起動
# 使い方: ./start-ai-tmux.sh <session-name> <ai-binary-path> [additional-args...]
# 引数チェック
if [ $# -lt 2 ]; then
echo "❌ 使い方: $0 <session-name> <ai-binary-path> [additional-args...]"
echo ""
echo "例:"
echo " # Claude Code 1番目"
echo " $0 claude1-8770 /home/tomoaki/.volta/bin/codex"
echo ""
echo " # Claude Code 2番目"
echo " $0 claude2-8770 /home/tomoaki/.volta/bin/codex"
echo ""
echo " # 本物のCodex制限解除の引数付き"
echo " $0 codex-real-8770 /path/to/real/codex --ask-for-approval never --sandbox danger-full-access"
echo ""
exit 1
fi
SESSION_NAME="$1"
AI_BINARY="$2"
shift 2
ADDITIONAL_ARGS="$@"
# Hook serverのポート環境変数でカスタマイズ可能
HOOK_PORT=${HOOK_SERVER_PORT:-8770}
echo "🚀 起動設定:"
echo " セッション名: $SESSION_NAME"
echo " AIバイナリ: $AI_BINARY"
echo " 追加引数: $ADDITIONAL_ARGS"
echo " Hook server: ws://localhost:$HOOK_PORT"
echo ""
# 既存セッションがあれば削除
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "⚠️ 既存セッション '$SESSION_NAME' を削除します..."
tmux kill-session -t "$SESSION_NAME"
fi
# ラッパースクリプトのパス(カレントディレクトリ基準)
WRAPPER_PATH="$(cd "$(dirname "$0")" && pwd)/codex-hook-wrapper.js"
# tmuxセッションを作成
echo "📦 新しいtmuxセッションを作成中..."
tmux new-session -d -s "$SESSION_NAME" \
"export CODEX_REAL_BIN='$AI_BINARY'; \
export CODEX_HOOK_SERVER='ws://localhost:$HOOK_PORT'; \
export CODEX_HOOK_BANNER=false; \
export CODEX_HOOK_ECHO_INJECT=true; \
export CODEX_HOOK_ENABLE=true; \
echo '🔌 Connecting to hook-server at port $HOOK_PORT...'; \
node '$WRAPPER_PATH' $ADDITIONAL_ARGS"
# 成功メッセージ
echo "✅ AI起動完了"
echo ""
echo "📋 便利なコマンド:"
echo " 接続: tmux attach -t $SESSION_NAME"
echo " メッセージ送信: tmux send-keys -t $SESSION_NAME 'your message' Enter"
echo " セッション確認: tmux ls"
echo " 終了: tmux kill-session -t $SESSION_NAME"
echo ""
# 複数AI同時起動の例を表示
if [ "$SESSION_NAME" == "claude1-8770" ]; then
echo "💡 2つ目のClaude Codeを起動するには:"
echo " $0 claude2-8770 $AI_BINARY"
fi

View File

@ -0,0 +1,40 @@
#!/bin/bash
# すべてのコンポーネントを起動
echo "🚀 双方向通信システム起動中..."
# 1. Hook Serverを起動バックグラウンド
echo "1⃣ Hook Server起動中..."
HOOK_SERVER_PORT=8770 node tools/codex-tmux-driver/hook-server.js > /tmp/hook-server.log 2>&1 &
echo " PID: $!"
sleep 2
# 2. Claude Code を起動
echo "2⃣ Claude Code 起動中..."
./tools/codex-tmux-driver/start-ai-tmux.sh claude /home/tomoaki/.volta/bin/codex
sleep 2
# 3. 本物のCodex を起動
echo "3⃣ 本物のCodex 起動中..."
# 本物のCodexのパスが必要環境変数で設定
REAL_CODEX=${REAL_CODEX_PATH:-/path/to/real/codex}
if [ ! -f "$REAL_CODEX" ]; then
echo "⚠️ REAL_CODEX_PATH が設定されていません!"
echo " export REAL_CODEX_PATH=/path/to/real/codex"
echo " 本物のCodexをスキップします..."
else
./tools/codex-tmux-driver/start-ai-tmux.sh codex "$REAL_CODEX" --ask-for-approval never --sandbox danger-full-access
fi
sleep 2
echo ""
echo "✅ すべて起動完了!"
echo ""
echo "📋 次のステップ:"
echo " - Codex→Claude送信: node tools/codex-tmux-driver/test-bidirectional-claude-codex.js"
echo " - Claude→Codex送信: node tools/codex-tmux-driver/test-bidirectional-codex-claude.js"
echo ""
echo " - Claude Codeに接続: tmux attach -t claude"
echo " - 本物のCodexに接続: tmux attach -t codex"
echo ""
echo " - すべて終了: pkill -f hook-server.js && tmux kill-session -t claude && tmux kill-session -t codex"

View File

@ -0,0 +1,51 @@
#!/bin/bash
# Claude専用のtmux起動スクリプト
SESSION_NAME="${1:-claude}"
# 第2引数がなければデフォルトのClaudeバイナリを使用
if [ $# -ge 2 ]; then
CLAUDE_BINARY="$2"
shift 2
ADDITIONAL_ARGS="$@"
else
CLAUDE_BINARY="/home/tomoaki/.volta/tools/image/node/22.16.0/bin/claude"
shift 1
ADDITIONAL_ARGS=""
fi
# Hook serverのポート
HOOK_PORT=${HOOK_SERVER_PORT:-8770}
echo "🚀 Claude起動設定:"
echo " セッション名: $SESSION_NAME"
echo " Claudeバイナリ: $CLAUDE_BINARY"
echo " 追加引数: $ADDITIONAL_ARGS"
echo " Hook server: ws://localhost:$HOOK_PORT"
echo ""
# 既存セッションがあれば削除
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
echo "⚠️ 既存セッション '$SESSION_NAME' を削除します..."
tmux kill-session -t "$SESSION_NAME"
fi
# ラッパースクリプトのパス
WRAPPER_PATH="$(cd "$(dirname "$0")" && pwd)/claude-hook-wrapper.js"
# tmuxセッションを作成
echo "📦 新しいtmuxセッションを作成中..."
tmux new-session -d -s "$SESSION_NAME" \
"export CLAUDE_REAL_BIN='$CLAUDE_BINARY'; \
export CLAUDE_HOOK_SERVER='ws://localhost:$HOOK_PORT'; \
export CLAUDE_HOOK_ENABLE=true; \
echo '🔌 Connecting to hook-server at port $HOOK_PORT...'; \
node '$WRAPPER_PATH' $ADDITIONAL_ARGS"
# 成功メッセージ
echo "✅ Claude起動完了"
echo ""
echo "📋 便利なコマンド:"
echo " 接続: tmux attach -t $SESSION_NAME"
echo " メッセージ送信: tmux send-keys -t $SESSION_NAME 'your message' Enter"
echo " セッション確認: tmux ls"
echo " 終了: tmux kill-session -t $SESSION_NAME"

View File

@ -0,0 +1,13 @@
#!/bin/bash
# クリーンな表示のためのCodex起動スクリプト
# 環境変数設定
export CODEX_REAL_BIN=/home/tomoaki/.volta/bin/codex
export CODEX_HOOK_SERVER=ws://localhost:8770
export CODEX_HOOK_BANNER=false
# エコー機能を有効化(入力を画面に表示)
export CODEX_HOOK_ECHO_INJECT=true
# デバッグログをファイルにリダイレクト
exec node /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver/codex-hook-wrapper.js "$@" 2>/tmp/codex-debug.log

View File

@ -0,0 +1,19 @@
#!/bin/bash
# Codexをアップデートチェックなしで起動
echo "🚀 Starting Codex without update check..."
# アップデートチェックをスキップ
export CODEX_SKIP_UPDATE_CHECK=1
export CODEX_HOOK_SERVER=ws://localhost:8770
export CODEX_LOG_FILE=/tmp/codex-8770.log
# 直接オリジナルのCodexを起動hook-wrapperをバイパス
if [ -f "$HOME/.local/bin/codex.original" ]; then
echo "Using codex.original..."
$HOME/.local/bin/codex.original exec --ask-for-approval never
else
echo "❌ codex.original not found!"
echo "Trying regular codex..."
/usr/local/bin/codex exec --ask-for-approval never
fi

View File

@ -0,0 +1,10 @@
#!/bin/bash
# 最もシンプルな起動方法
# 環境変数設定
export CODEX_REAL_BIN=/home/tomoaki/.volta/bin/codex
export CODEX_HOOK_SERVER=ws://localhost:8770
export CODEX_HOOK_BANNER=false
# ラッパー起動2>/dev/nullを削除
exec node /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver/codex-hook-wrapper.js "$@"

View File

@ -0,0 +1,21 @@
#!/bin/bash
# tmux経由でCodexを起動本物の端末環境
SESSION_NAME="codex-8770"
# 既存セッションがあれば削除
tmux kill-session -t $SESSION_NAME 2>/dev/null
# 環境変数を設定してtmuxセッションを作成
tmux new-session -d -s $SESSION_NAME \
"export CODEX_REAL_BIN=/home/tomoaki/.volta/bin/codex; \
export CODEX_HOOK_SERVER=ws://localhost:8770; \
export CODEX_HOOK_BANNER=false; \
export CODEX_HOOK_ECHO_INJECT=true; \
export CODEX_USE_SCRIPT_PTY=true; \
node /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver/codex-hook-wrapper.js"
echo "✅ Codex started in tmux session: $SESSION_NAME"
echo ""
echo "To attach: tmux attach -t $SESSION_NAME"
echo "To send keys: tmux send-keys -t $SESSION_NAME 'your text' Enter"

View File

@ -0,0 +1,16 @@
#!/bin/bash
# PTY擬似端末を使ってCodexを起動
# 環境変数設定
export CODEX_REAL_BIN=/home/tomoaki/.volta/bin/codex
export CODEX_HOOK_SERVER=ws://localhost:8770
export CODEX_HOOK_BANNER=false
# PTYを強制的に有効化
export CODEX_USE_SCRIPT_PTY=true
# エコー機能を有効化
export CODEX_HOOK_ECHO_INJECT=true
# ラッパー起動
exec node /mnt/c/git/nyash-project/nyash/tools/codex-tmux-driver/codex-hook-wrapper.js "$@"

View File

@ -0,0 +1,65 @@
#!/bin/bash
# 複数Codexインスタンスを簡単に起動するスクリプト
# 使い方: ./start-instance.sh A 8769
# ./start-instance.sh B 8770 --foreground
INSTANCE_NAME="${1:-A}"
PORT="${2:-8769}"
FOREGROUND=false
# オプション解析
if [[ "$3" == "--foreground" ]] || [[ "$3" == "-f" ]]; then
FOREGROUND=true
fi
# カラー定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}🚀 Starting Codex Instance ${INSTANCE_NAME} on port ${PORT}${NC}"
# hook-serverの起動
if [ "$FOREGROUND" = true ]; then
echo -e "${YELLOW}Starting hook-server in foreground...${NC}"
echo "Commands:"
echo " export CODEX_HOOK_SERVER=ws://localhost:${PORT}"
echo " export CODEX_LOG_FILE=/tmp/codex-${INSTANCE_NAME}.log"
echo " codex exec"
echo ""
HOOK_SERVER_PORT=$PORT HOOK_SERVER_AUTO_EXIT=false \
node tools/codex-tmux-driver/hook-server.js
else
# バックグラウンドで起動
echo -e "${YELLOW}Starting hook-server in background...${NC}"
HOOK_SERVER_PORT=$PORT HOOK_SERVER_AUTO_EXIT=true \
nohup node tools/codex-tmux-driver/hook-server.js \
> /tmp/hook-${INSTANCE_NAME}.log 2>&1 &
HOOK_PID=$!
echo "Hook server PID: $HOOK_PID"
# 起動確認
sleep 1
if kill -0 $HOOK_PID 2>/dev/null; then
echo -e "${GREEN}✅ Hook server started successfully${NC}"
else
echo -e "${RED}❌ Hook server failed to start${NC}"
echo "Check log: /tmp/hook-${INSTANCE_NAME}.log"
exit 1
fi
# Codex起動コマンドの表示
echo ""
echo "Now run these commands in another terminal:"
echo -e "${GREEN}export CODEX_HOOK_SERVER=ws://localhost:${PORT}${NC}"
echo -e "${GREEN}export CODEX_LOG_FILE=/tmp/codex-${INSTANCE_NAME}.log${NC}"
echo -e "${GREEN}codex exec --ask-for-approval never${NC}"
echo ""
echo "To monitor:"
echo " tail -f /tmp/hook-${INSTANCE_NAME}.log"
echo " tail -f /tmp/codex-${INSTANCE_NAME}.log"
fi

View File

@ -0,0 +1,39 @@
#!/bin/bash
# Codex tmux Driver 起動スクリプト
# デフォルト設定
SESSION_NAME="${CODEX_SESSION:-codex-session}"
PORT="${CODEX_PORT:-8766}"
LOG_DIR="${CODEX_LOG_DIR:-/tmp}"
LOG_FILE="$LOG_DIR/codex-$(date +%Y%m%d-%H%M%S).log"
# Node.jsがインストールされているか確認
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed"
exit 1
fi
# tmuxがインストールされているか確認
if ! command -v tmux &> /dev/null; then
echo "Error: tmux is not installed"
exit 1
fi
# npm install実行初回のみ
if [ ! -d "node_modules" ]; then
echo "Installing dependencies..."
npm install
fi
# 起動
echo "=== Starting Codex tmux Driver ==="
echo "Session: $SESSION_NAME"
echo "Port: $PORT"
echo "Log: $LOG_FILE"
echo ""
node codex-tmux-driver.js \
--session="$SESSION_NAME" \
--port="$PORT" \
--log="$LOG_FILE" \
"$@"

View File

@ -0,0 +1,32 @@
#!/usr/bin/env node
// AI同士の通信テストスクリプト
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', function() {
console.log('🔌 WebSocketに接続しました');
// Claude同士のテストメッセージ
const message = {
source: 'claude1', // 送信元を明示
type: 'inject-input',
data: `[Claude1→Claude2] 🤖 AI同士の通信テストです
このメッセージが見えたら、次のコマンドで返信してください:
node -e "console.log('受信確認: Claude1からのメッセージを受け取りました');"
送信時刻: ${new Date().toLocaleString('ja-JP')}`
};
ws.send(JSON.stringify(message));
console.log('✅ テストメッセージを送信しました');
console.log('📨 内容:');
console.log(message.data);
ws.close();
});
ws.on('error', (err) => {
console.error('❌ エラー:', err.message);
});

View File

@ -0,0 +1,35 @@
#!/usr/bin/env node
// Codexから双方向通信テスト
const WebSocket = require('ws');
// Hook serverに接続
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✅ WebSocket接続成功');
// CodexからClaudeへメッセージ送信
const message = {
source: 'codex',
type: 'inject-input',
data: '🎉 Codexから双方向通信テスト成功hook-serverが正しく動作しています'
};
console.log('📤 送信メッセージ:', message);
ws.send(JSON.stringify(message));
// 送信後すぐに接続を閉じる
setTimeout(() => {
ws.close();
console.log('🔌 接続を閉じました');
}, 100);
});
ws.on('error', (err) => {
console.error('❌ エラー:', err.message);
});
ws.on('close', () => {
console.log('👋 WebSocket接続終了');
});

View File

@ -0,0 +1,35 @@
#!/usr/bin/env node
// ClaudeからCodexへ双方向通信テスト
const WebSocket = require('ws');
// Hook serverに接続
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✅ WebSocket接続成功');
// ClaudeからCodexへメッセージ送信
const message = {
source: 'claude',
type: 'inject-input',
data: '🌟 Claudeから双方向通信テスト成功逆方向も動作しています'
};
console.log('📤 送信メッセージ:', message);
ws.send(JSON.stringify(message));
// 送信後すぐに接続を閉じる
setTimeout(() => {
ws.close();
console.log('🔌 接続を閉じました');
}, 100);
});
ws.on('error', (err) => {
console.error('❌ エラー:', err.message);
});
ws.on('close', () => {
console.log('👋 WebSocket接続終了');
});

View File

@ -0,0 +1,69 @@
#!/bin/bash
# test-bidirectional.sh - 双方向通信テストスクリプト
echo "🚀 双方向通信テストを開始します..."
# Step 1: hook-serverが起動しているか確認
if ! lsof -i:8770 > /dev/null 2>&1; then
echo "⚠️ hook-serverが起動していません。別ターミナルで以下を実行してください"
echo " HOOK_SERVER_PORT=8770 node hook-server.js"
exit 1
fi
echo "✅ hook-serverが起動しています"
# Step 2: Codexセッションが存在するか確認
if ! tmux has-session -t codex-safe 2>/dev/null; then
echo "⚠️ Codexセッションが見つかりません。以下を実行してください"
echo " ./tmux-launch-only.sh"
exit 1
fi
echo "✅ Codexセッションが存在します"
# Step 3: 双方向通信の指示をCodexに送る
echo "📤 Codexに双方向通信の使い方を送信..."
node -e "
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
const message = {
source: 'claude',
type: 'inject-input',
data: \`双方向通信テスト!
以下のコードを実行して、私Claudeに返信してください
\\\`\\\`\\\`javascript
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
ws.send(JSON.stringify({
source: 'codex',
type: 'inject-input',
data: '[Codex→Claude] 双方向通信成功hook-serverを通じてClaudeに届きました'
}));
ws.close();
});
\\\`\\\`\\\`
\`
};
ws.send(JSON.stringify(message));
console.log('✅ Sent bidirectional test to Codex');
ws.close();
});
ws.on('error', (err) => {
console.error('❌ Error:', err.message);
process.exit(1);
});
"
echo ""
echo "📡 Codexからの返信を待っています..."
echo " もしCodexが返信コードを実行したら、hook-serverのログに表示されます。"
echo ""
echo "💡 ヒント: hook-serverのターミナルを確認してください"

View File

@ -0,0 +1,108 @@
// test-client.js
// codex-tmux-driverのテスト用クライアント
// 使い方: node test-client.js
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8766');
// イベントハンドラー設定
ws.on('open', () => {
console.log('[Connected] WebSocket接続成功');
// ステータス確認
ws.send(JSON.stringify({ op: 'status' }));
// 履歴取得
setTimeout(() => {
ws.send(JSON.stringify({ op: 'history', count: 5 }));
}, 1000);
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'codex-event':
console.log(`[Codex ${msg.event}] ${msg.data}`);
// 応答があったら自動で画面キャプチャ
if (msg.event === 'response' || msg.event === 'complete') {
ws.send(JSON.stringify({ op: 'capture' }));
}
break;
case 'status':
console.log('[Status]', msg.data);
break;
case 'screen-capture':
console.log('[Screen Capture]\n' + msg.data);
break;
case 'history':
console.log('[History]');
msg.data.forEach(event => {
console.log(` ${event.timestamp}: ${event.event || 'output'} - ${event.data}`);
});
break;
case 'error':
console.error('[Error]', msg.data);
break;
default:
console.log(`[${msg.type}]`, msg.data);
}
});
ws.on('error', (err) => {
console.error('[WebSocket Error]', err);
});
ws.on('close', () => {
console.log('[Disconnected] 接続終了');
});
// 標準入力から質問を受け付ける
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'Codex> '
});
rl.prompt();
rl.on('line', (line) => {
const cmd = line.trim();
if (cmd === 'exit' || cmd === 'quit') {
ws.close();
rl.close();
return;
}
if (cmd === 'capture') {
ws.send(JSON.stringify({ op: 'capture' }));
} else if (cmd === 'status') {
ws.send(JSON.stringify({ op: 'status' }));
} else if (cmd.startsWith('filter ')) {
const event = cmd.split(' ')[1];
ws.send(JSON.stringify({ op: 'filter', event }));
} else if (cmd) {
// 通常の入力はCodexに送信
ws.send(JSON.stringify({ op: 'send', data: cmd }));
}
rl.prompt();
});
console.log('=== Codex tmux Driver Test Client ===');
console.log('Commands:');
console.log(' <text> - Send text to Codex');
console.log(' capture - Capture current screen');
console.log(' status - Show status');
console.log(' filter <event> - Filter events (response/thinking/error/complete)');
console.log(' exit/quit - Exit');
console.log('');

View File

@ -0,0 +1,38 @@
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✨ Testing direct stdin write...');
// 改行だけを送る
ws.send(JSON.stringify({
type: 'inject-input',
data: '' // 空文字列 + 改行
}));
setTimeout(() => {
// テキストと改行を別々に送る
ws.send(JSON.stringify({
type: 'inject-input',
data: 'Test message without newline'
}));
setTimeout(() => {
// 改行だけを送る
ws.send(JSON.stringify({
type: 'inject-input',
data: '' // これでEnterキーの効果
}));
setTimeout(() => {
ws.close();
console.log('✅ Test complete!');
}, 500);
}, 1000);
}, 1000);
});
ws.on('error', (err) => {
console.error('Error:', err);
});

View File

@ -0,0 +1,47 @@
const WebSocket = require('ws');
console.log('🔌 Testing hook server debug...');
const ws = new WebSocket('ws://localhost:8770');
ws.on('open', () => {
console.log('✅ Connected as hook client!');
// デバッグ: いろいろな形式を試す
// 1. hook-serverが期待する形式
ws.send(JSON.stringify({
type: 'hook-event',
event: 'test-inject',
data: 'テストメッセージ1'
}));
// 2. 直接inject-input
setTimeout(() => {
ws.send(JSON.stringify({
type: 'inject-input',
data: 'テストメッセージ2'
}));
}, 1000);
// 3. シンプルなメッセージ
setTimeout(() => {
ws.send(JSON.stringify({
message: 'テストメッセージ3'
}));
}, 2000);
});
ws.on('message', (data) => {
console.log('📨 Received from server:', data.toString());
});
ws.on('error', (err) => {
console.error('❌ Error:', err.message);
});
// 5秒後に終了
setTimeout(() => {
console.log('👋 Closing...');
ws.close();
process.exit(0);
}, 5000);

View File

@ -0,0 +1,29 @@
const WebSocket = require('ws');
const url = process.env.CODEX_HOOK_SERVER || 'ws://localhost:8770';
const message = 'Hello Claude! 双方向通信テスト成功!';
console.log(`🔌 Connecting to ${url} ...`);
const ws = new WebSocket(url);
ws.on('open', () => {
console.log('✅ Connected. Sending greeting...');
const payload = {
source: 'codex',
type: 'inject-input',
data: message,
};
ws.send(JSON.stringify(payload));
console.log('📤 Sent:', payload);
ws.close();
});
ws.on('error', (err) => {
console.error('❌ WebSocket error:', err.message);
process.exitCode = 1;
});
ws.on('close', () => {
console.log('👋 Connection closed.');
});

View File

@ -0,0 +1,39 @@
#!/bin/bash
# 安全にラッパーをテストするスクリプト
echo "🧪 Codex Wrapper Safe Test"
echo ""
# 1. バイナリ確認
echo "1⃣ Checking Codex binary..."
REAL_CODEX=/home/tomoaki/.volta/bin/codex
if [ -f "$REAL_CODEX" ]; then
echo "✅ Found: $REAL_CODEX"
$REAL_CODEX --version
else
echo "❌ Not found: $REAL_CODEX"
exit 1
fi
echo ""
echo "2⃣ Testing wrapper (hook disabled)..."
export CODEX_REAL_BIN=$REAL_CODEX
export CODEX_HOOK_ENABLE=false
cd $(dirname "$0")
node codex-hook-wrapper.js --version
echo ""
echo "3⃣ Testing wrapper (hook enabled, port 8770)..."
export CODEX_HOOK_ENABLE=true
export CODEX_HOOK_SERVER=ws://localhost:8770
export CODEX_LOG_FILE=/tmp/codex-test.log
echo "Will try to connect to $CODEX_HOOK_SERVER"
node codex-hook-wrapper.js --version
echo ""
echo "✅ Wrapper test complete!"
echo ""
echo "To use with real Codex:"
echo " export CODEX_REAL_BIN=$REAL_CODEX"
echo " export CODEX_HOOK_SERVER=ws://localhost:8770"
echo " node codex-hook-wrapper.js exec --ask-for-approval never"

View File

@ -0,0 +1,127 @@
// tmux-codex-controller.js
// tmux経由でCodexを完全制御するコントローラー
const { spawn } = require('child_process');
const WebSocket = require('ws');
class TmuxCodexController {
constructor(sessionName = 'codex-8770', port = 8770) {
this.sessionName = sessionName;
this.port = port;
this.hookServerUrl = `ws://localhost:${port}`;
}
// tmuxセッションを作成してCodexを起動
async start() {
console.log('🚀 Starting Codex in tmux...');
// 既存セッションを削除
await this.exec('tmux', ['kill-session', '-t', this.sessionName]).catch(() => {});
// 新しいセッションでCodexを起動対話モード
const cmd = [
'new-session', '-d', '-s', this.sessionName,
`export CODEX_REAL_BIN=/home/tomoaki/.volta/bin/codex && ` +
`export CODEX_HOOK_SERVER=${this.hookServerUrl} && ` +
`export CODEX_HOOK_BANNER=false && ` +
`/home/tomoaki/.volta/bin/codex` // 直接codexを起動対話モード
];
await this.exec('tmux', cmd);
console.log(`✅ Codex started in tmux session: ${this.sessionName}`);
// 起動を待つ
await this.sleep(2000);
}
// tmux経由でキーを送信Enterも送れる
async sendKeys(text, enter = true) {
console.log(`📤 Sending: "${text}"${enter ? ' + Enter' : ''}`);
const args = ['send-keys', '-t', this.sessionName, text];
if (enter) {
args.push('Enter');
}
await this.exec('tmux', args);
}
// 画面内容をキャプチャ
async capture() {
const result = await this.exec('tmux', ['capture-pane', '-t', this.sessionName, '-p']);
return result.stdout;
}
// セッションにアタッチ(デバッグ用)
attach() {
console.log(`📺 Attaching to ${this.sessionName}...`);
spawn('tmux', ['attach', '-t', this.sessionName], { stdio: 'inherit' });
}
// セッションを終了
async stop() {
await this.exec('tmux', ['kill-session', '-t', this.sessionName]);
console.log('👋 Session 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) {
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 controller = new TmuxCodexController();
try {
// Codexを起動
await controller.start();
// メッセージを送信自動でEnter
await controller.sendKeys('こんにちはNyashプロジェクトから自動挨拶だにゃ🐱');
await controller.sleep(1000);
// もう一つメッセージ
await controller.sendKeys('JIT開発の進捗はどう');
await controller.sleep(1000);
// 画面内容を確認
const screen = await controller.capture();
console.log('\n📺 Current screen:');
console.log(screen);
// デバッグ用にアタッチもできる
// controller.attach();
} catch (err) {
console.error('❌ Error:', err);
}
}
// モジュールとして使えるようにエクスポート
module.exports = TmuxCodexController;
// 直接実行したらデモを実行
if (require.main === module) {
demo();
}

View File

@ -0,0 +1,36 @@
#!/usr/bin/env node
// tmux経由でメッセージを注入するヘルパー
const { exec } = require('child_process');
// コマンドライン引数から対象セッションとメッセージを取得
const args = process.argv.slice(2);
if (args.length < 2) {
console.error('Usage: node tmux-inject-helper.js <session-name> <message>');
process.exit(1);
}
const [sessionName, message] = args;
// tmux send-keysコマンドを実行
// C-mはEnterキー
const command = `tmux send-keys -t ${sessionName} '${message.replace(/'/g, "'\\''")}'`;
console.log(`Executing: ${command}`);
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
console.log('Message sent successfully!');
// Enterキーを送信
exec(`tmux send-keys -t ${sessionName} C-m`, (err) => {
if (!err) {
console.log('Enter key sent!');
}
});
});

View File

@ -0,0 +1,35 @@
#!/bin/bash
# tmuxでCodexを起動するだけ自動実行なし
SESSION_NAME="codex-safe"
echo "🎯 Codexをtmuxで起動します自動実行はしません"
echo ""
# 既存セッションがあれば確認
if tmux has-session -t $SESSION_NAME 2>/dev/null; then
echo "⚠️ 既存のセッション '$SESSION_NAME' が存在します"
echo -n "削除して新しく作成しますか? (y/N): "
read answer
if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then
tmux kill-session -t $SESSION_NAME
else
echo "中止しました"
exit 0
fi
fi
# tmuxセッションを作成Codexを起動
echo "📺 tmuxセッション '$SESSION_NAME' を作成しています..."
tmux new-session -d -s $SESSION_NAME /home/tomoaki/.volta/bin/codex
echo ""
echo "✅ 完了!"
echo ""
echo "📌 使い方:"
echo " 接続: tmux attach -t $SESSION_NAME"
echo " 切断: Ctrl+B, D"
echo " 終了: tmux kill-session -t $SESSION_NAME"
echo ""
echo "⚠️ 注意: Codexは対話モードで起動しています"
echo " 自動的な操作は行いません"

View File

@ -0,0 +1,217 @@
// 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();
}

View File

@ -0,0 +1,56 @@
==================================
Codex-Claude Bridge 使い方説明書
==================================
■ 概要
このツールはCodexとClaudeを連携させるブリッジシステムです。
tmuxを使ってCodexを制御し、メッセージの送受信を自動化できます。
■ 基本的な流れ
1. hook-serverを起動メッセージ中継
2. tmuxでCodexを起動対話モード
3. メッセージを送信自動Enter可能
4. Codexの応答を取得してClaudeに転送
■ 必要なコンポーネント
- hook-server.js: WebSocketサーバーメッセージ中継
- tmux-launch-only.sh: 安全にCodexを起動
- tmux-codex-controller.js: 自動制御用
- codex-claude-bridge.js: 応答転送(未実装)
■ 使い方
【手動で使う場合】
1. hook-serverを起動
$ HOOK_SERVER_PORT=8770 node hook-server.js
2. tmuxでCodexを起動
$ ./tmux-launch-only.sh
3. tmuxセッションに接続
$ tmux attach -t codex-safe
4. メッセージを送る
$ tmux send-keys -t codex-safe "メッセージ" Enter
【自動化する場合】
1. tmux-codex-controller.jsを使用
$ node tmux-codex-controller.js
■ 注意事項
- Codexは勝手に動作する可能性があるため、必ず監視すること
- tmuxセッションは使用後に必ず終了すること
$ tmux kill-session -t codex-safe
■ トラブルシューティング
Q: Enterキーが効かない
A: tmux経由なら確実に送信できます
Q: 画面がぐちゃぐちゃになる
A: デバッグ出力を無効にしてください
export CODEX_HOOK_BANNER=false
Q: プロセスが残る
A: ps aux | grep codex で確認してkillしてください
==================================