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:
148
tools/codex-tmux-driver/CODEX_BIDIRECTIONAL_GUIDE.md
Normal file
148
tools/codex-tmux-driver/CODEX_BIDIRECTIONAL_GUIDE.md
Normal 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が完全に双方向で協調作業できます!🎉
|
||||
81
tools/codex-tmux-driver/QUICK_START.md
Normal file
81
tools/codex-tmux-driver/QUICK_START.md
Normal 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 Code(c1)起動
|
||||
```bash
|
||||
./tools/codex-tmux-driver/start-ai-tmux.sh c1 /home/tomoaki/.volta/bin/codex
|
||||
```
|
||||
|
||||
### 3. 本物のCodex(c2)起動
|
||||
```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
|
||||
```
|
||||
97
tools/codex-tmux-driver/README-AUTO-BRIDGE.md
Normal file
97
tools/codex-tmux-driver/README-AUTO-BRIDGE.md
Normal 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: セッション名が正しいか確認してください
|
||||
41
tools/codex-tmux-driver/README-FINAL.md
Normal file
41
tools/codex-tmux-driver/README-FINAL.md
Normal 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ブリッジが作れるにゃ!🐱
|
||||
34
tools/codex-tmux-driver/README-SUCCESS.md
Normal file
34
tools/codex-tmux-driver/README-SUCCESS.md
Normal 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ブリッジの実装
|
||||
- 自動応答システムの構築
|
||||
- フィルタリング機能の追加
|
||||
|
||||
やったにゃー!🐱🎉
|
||||
328
tools/codex-tmux-driver/README.md
Normal file
328
tools/codex-tmux-driver/README.md
Normal 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からも返事ができるように!🔄
|
||||
39
tools/codex-tmux-driver/SIMPLE_TEST_STEPS.md
Normal file
39
tools/codex-tmux-driver/SIMPLE_TEST_STEPS.md
Normal 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'に変えるだけ!
|
||||
122
tools/codex-tmux-driver/bridge-control.js
Normal file
122
tools/codex-tmux-driver/bridge-control.js
Normal 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();
|
||||
});
|
||||
189
tools/codex-tmux-driver/claude-codex-unified-bridge.js
Normal file
189
tools/codex-tmux-driver/claude-codex-unified-bridge.js
Normal 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;
|
||||
178
tools/codex-tmux-driver/claude-hook-wrapper.js
Normal file
178
tools/codex-tmux-driver/claude-hook-wrapper.js
Normal 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}`);
|
||||
67
tools/codex-tmux-driver/claude-tmux-controller.js
Normal file
67
tools/codex-tmux-driver/claude-tmux-controller.js
Normal 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;
|
||||
143
tools/codex-tmux-driver/claude-tmux-setup.md
Normal file
143
tools/codex-tmux-driver/claude-tmux-setup.md
Normal 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セッション名が正しいか確認
|
||||
82
tools/codex-tmux-driver/claude-to-claude-test.js
Normal file
82
tools/codex-tmux-driver/claude-to-claude-test.js
Normal 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 # 受信モード
|
||||
`);
|
||||
}
|
||||
146
tools/codex-tmux-driver/codex-claude-auto-bridge.js
Normal file
146
tools/codex-tmux-driver/codex-claude-auto-bridge.js
Normal 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;
|
||||
438
tools/codex-tmux-driver/codex-claude-bridge.js
Normal file
438
tools/codex-tmux-driver/codex-claude-bridge.js
Normal 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 };
|
||||
199
tools/codex-tmux-driver/codex-filter-box.js
Normal file
199
tools/codex-tmux-driver/codex-filter-box.js
Normal 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());
|
||||
}
|
||||
291
tools/codex-tmux-driver/codex-hook-wrapper.js
Normal file
291
tools/codex-tmux-driver/codex-hook-wrapper.js
Normal 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}`);
|
||||
}
|
||||
150
tools/codex-tmux-driver/codex-output-watcher.js
Normal file
150
tools/codex-tmux-driver/codex-output-watcher.js
Normal 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;
|
||||
47
tools/codex-tmux-driver/codex-reply-template.js
Normal file
47
tools/codex-tmux-driver/codex-reply-template.js
Normal 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 が成功');
|
||||
366
tools/codex-tmux-driver/codex-tmux-driver.js
Normal file
366
tools/codex-tmux-driver/codex-tmux-driver.js
Normal file
@ -0,0 +1,366 @@
|
||||
// codex-tmux-driver.js
|
||||
// tmux経由でCodexを管理し、イベントをWebSocket配信するドライバ
|
||||
// 使い方:
|
||||
// 1) npm install
|
||||
// 2) node codex-tmux-driver.js [options]
|
||||
// --session=codex-session (tmuxセッション名)
|
||||
// --port=8766 (WebSocketポート)
|
||||
// --log=/tmp/codex.log (ログファイルパス)
|
||||
// 3) WebSocketで接続して操作
|
||||
// {"op":"send","data":"質問内容"}
|
||||
// {"op":"capture"}
|
||||
// {"op":"status"}
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const { WebSocketServer } = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// --- 設定 ---
|
||||
const argv = process.argv.slice(2).reduce((a, kv) => {
|
||||
const [k, ...rest] = kv.split('=');
|
||||
const v = rest.join('=');
|
||||
a[k.replace(/^--/, '')] = v ?? true;
|
||||
return a;
|
||||
}, {});
|
||||
|
||||
const SESSION_NAME = argv.session || 'codex-session';
|
||||
const PORT = Number(argv.port || 8766);
|
||||
const LOG_FILE = argv.log || `/tmp/codex-${Date.now()}.log`;
|
||||
const CODEX_CMD = argv.cmd || 'codex exec';
|
||||
|
||||
// --- 状態管理 ---
|
||||
let clients = new Set();
|
||||
let tailProcess = null;
|
||||
let sessionActive = false;
|
||||
let codexEventBuffer = [];
|
||||
const MAX_BUFFER_SIZE = 100;
|
||||
|
||||
// --- ユーティリティ関数 ---
|
||||
function broadcast(msg) {
|
||||
const data = JSON.stringify(msg);
|
||||
for (const ws of clients) {
|
||||
try { ws.send(data); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function executeCommand(cmd, args = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(cmd, args);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
proc.stdout.on('data', (data) => { stdout += data; });
|
||||
proc.stderr.on('data', (data) => { stderr += data; });
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(`Command failed: ${stderr}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Codex出力パターン認識 ---
|
||||
function parseCodexOutput(line) {
|
||||
const patterns = {
|
||||
// Codexの応答パターン
|
||||
response: /^(Codex:|回答:|Answer:|Response:)/i,
|
||||
thinking: /(考え中|Processing|Thinking|分析中)/i,
|
||||
error: /(エラー|Error|失敗|Failed)/i,
|
||||
complete: /(完了|Complete|Done|終了)/i,
|
||||
question: /(質問:|Question:|相談:|Help:)/i,
|
||||
};
|
||||
|
||||
for (const [event, pattern] of Object.entries(patterns)) {
|
||||
if (pattern.test(line)) {
|
||||
return {
|
||||
type: 'codex-event',
|
||||
event: event,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: line.trim()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// パターンに一致しない場合は生データとして返す
|
||||
return {
|
||||
type: 'codex-output',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: line
|
||||
};
|
||||
}
|
||||
|
||||
// --- tmuxセッション管理 ---
|
||||
async function createTmuxSession() {
|
||||
try {
|
||||
// 既存セッションをチェック
|
||||
try {
|
||||
await executeCommand('tmux', ['has-session', '-t', SESSION_NAME]);
|
||||
console.log(`[INFO] Session ${SESSION_NAME} already exists`);
|
||||
sessionActive = true;
|
||||
// 既存セッションでもパイプと監視を確実に有効化する
|
||||
try {
|
||||
await executeCommand('tmux', [
|
||||
'pipe-pane', '-t', SESSION_NAME,
|
||||
'-o', `cat >> ${LOG_FILE}`
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn('[WARN] Failed to ensure pipe-pane on existing session:', e.message || e);
|
||||
}
|
||||
if (!tailProcess) {
|
||||
startLogMonitoring();
|
||||
}
|
||||
return;
|
||||
} catch {
|
||||
// セッションが存在しない場合、作成
|
||||
}
|
||||
|
||||
// 新規セッション作成
|
||||
await executeCommand('tmux', [
|
||||
'new-session', '-d', '-s', SESSION_NAME,
|
||||
CODEX_CMD
|
||||
]);
|
||||
|
||||
// pipe-paneでログ出力を設定
|
||||
await executeCommand('tmux', [
|
||||
'pipe-pane', '-t', SESSION_NAME,
|
||||
'-o', `cat >> ${LOG_FILE}`
|
||||
]);
|
||||
|
||||
sessionActive = true;
|
||||
console.log(`[INFO] Created tmux session: ${SESSION_NAME}`);
|
||||
|
||||
// ログファイル監視開始
|
||||
startLogMonitoring();
|
||||
|
||||
} catch (err) {
|
||||
console.error('[ERROR] Failed to create tmux session:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// --- ログ監視 ---
|
||||
function startLogMonitoring() {
|
||||
// ログファイルが存在しない場合は作成
|
||||
if (!fs.existsSync(LOG_FILE)) {
|
||||
fs.writeFileSync(LOG_FILE, '');
|
||||
}
|
||||
|
||||
// tail -fで監視
|
||||
tailProcess = spawn('tail', ['-f', '-n', '0', LOG_FILE]);
|
||||
|
||||
tailProcess.stdout.on('data', (data) => {
|
||||
const lines = data.toString('utf8').split('\n').filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
const event = parseCodexOutput(line);
|
||||
|
||||
// イベントバッファに追加
|
||||
codexEventBuffer.push(event);
|
||||
if (codexEventBuffer.length > MAX_BUFFER_SIZE) {
|
||||
codexEventBuffer.shift();
|
||||
}
|
||||
|
||||
// クライアントに配信
|
||||
broadcast(event);
|
||||
}
|
||||
});
|
||||
|
||||
tailProcess.on('error', (err) => {
|
||||
console.error('[ERROR] Tail process error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// --- WebSocketサーバ ---
|
||||
const wss = new WebSocketServer({ port: PORT });
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
clients.add(ws);
|
||||
|
||||
// 接続時にステータスと最近のイベントを送信
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
session: SESSION_NAME,
|
||||
active: sessionActive,
|
||||
logFile: LOG_FILE,
|
||||
recentEvents: codexEventBuffer.slice(-10)
|
||||
}
|
||||
}));
|
||||
|
||||
ws.on('message', async (raw) => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
ws.send(JSON.stringify({ type: 'error', data: 'Invalid JSON' }));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.op) {
|
||||
case 'send': {
|
||||
// Codexに入力を送信
|
||||
if (!sessionActive) {
|
||||
ws.send(JSON.stringify({ type: 'error', data: 'Session not active' }));
|
||||
break;
|
||||
}
|
||||
|
||||
const input = String(msg.data || '').trim();
|
||||
if (!input) break;
|
||||
|
||||
try {
|
||||
await executeCommand('tmux', [
|
||||
'send-keys', '-t', SESSION_NAME,
|
||||
input, 'Enter'
|
||||
]);
|
||||
|
||||
broadcast({
|
||||
type: 'input-sent',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: input
|
||||
});
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Failed to send input'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'capture': {
|
||||
// 現在の画面をキャプチャ
|
||||
if (!sessionActive) {
|
||||
ws.send(JSON.stringify({ type: 'error', data: 'Session not active' }));
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await executeCommand('tmux', [
|
||||
'capture-pane', '-t', SESSION_NAME, '-p'
|
||||
]);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'screen-capture',
|
||||
timestamp: new Date().toISOString(),
|
||||
data: output
|
||||
}));
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Failed to capture screen'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'status': {
|
||||
// ステータス確認
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
session: SESSION_NAME,
|
||||
active: sessionActive,
|
||||
logFile: LOG_FILE,
|
||||
clientCount: clients.size,
|
||||
bufferSize: codexEventBuffer.length
|
||||
}
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'history': {
|
||||
// イベント履歴取得
|
||||
const count = Number(msg.count || 20);
|
||||
const history = codexEventBuffer.slice(-count);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'history',
|
||||
data: history
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'filter': {
|
||||
// 特定のイベントタイプのみフィルタ
|
||||
const eventType = msg.event || 'all';
|
||||
const filtered = eventType === 'all'
|
||||
? codexEventBuffer
|
||||
: codexEventBuffer.filter(e => e.event === eventType);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'filtered-events',
|
||||
filter: eventType,
|
||||
data: filtered
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'kill': {
|
||||
// セッション終了
|
||||
if (!sessionActive) break;
|
||||
|
||||
try {
|
||||
await executeCommand('tmux', ['kill-session', '-t', SESSION_NAME]);
|
||||
sessionActive = false;
|
||||
if (tailProcess) {
|
||||
try { tailProcess.kill(); } catch {}
|
||||
tailProcess = null;
|
||||
}
|
||||
broadcast({ type: 'session-killed' });
|
||||
} catch (err) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: 'Failed to kill session'
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: `Unknown operation: ${msg.op}`
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws);
|
||||
});
|
||||
});
|
||||
|
||||
// --- 起動処理 ---
|
||||
async function start() {
|
||||
console.log('=== Codex tmux Driver ===');
|
||||
console.log(`WebSocket: ws://localhost:${PORT}`);
|
||||
console.log(`Session: ${SESSION_NAME}`);
|
||||
console.log(`Log file: ${LOG_FILE}`);
|
||||
|
||||
try {
|
||||
await createTmuxSession();
|
||||
|
||||
wss.on('listening', () => {
|
||||
console.log(`[INFO] WebSocket server listening on port ${PORT}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[FATAL] Failed to start:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// --- クリーンアップ ---
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n[INFO] Shutting down...');
|
||||
if (tailProcess) {
|
||||
tailProcess.kill();
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 起動
|
||||
start();
|
||||
184
tools/codex-tmux-driver/common-message-file.js
Normal file
184
tools/codex-tmux-driver/common-message-file.js
Normal 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;
|
||||
53
tools/codex-tmux-driver/greet-codex-8770.js
Normal file
53
tools/codex-tmux-driver/greet-codex-8770.js
Normal 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);
|
||||
35
tools/codex-tmux-driver/greet-codex-direct.js
Normal file
35
tools/codex-tmux-driver/greet-codex-direct.js
Normal 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);
|
||||
280
tools/codex-tmux-driver/hook-server.js
Normal file
280
tools/codex-tmux-driver/hook-server.js
Normal file
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env node
|
||||
// hook-server.js
|
||||
// Codexフックからのイベントを受信してClaude連携するサーバー
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const PORT = process.env.HOOK_SERVER_PORT || 8770;
|
||||
const STRIP_ANSI = process.env.HOOK_STRIP_ANSI !== 'false';
|
||||
const AUTO_BRIDGE = process.env.AUTO_BRIDGE === 'true';
|
||||
const AUTO_EXIT = process.env.HOOK_SERVER_AUTO_EXIT === 'true';
|
||||
const IDLE_EXIT_MS = Number(process.env.HOOK_IDLE_EXIT_MS || 2000);
|
||||
|
||||
// WebSocketサーバー
|
||||
const wss = new WebSocket.Server({ port: PORT });
|
||||
|
||||
// 状態管理
|
||||
const state = {
|
||||
lastInput: '',
|
||||
lastOutput: '',
|
||||
waitingCount: 0,
|
||||
questionQueue: [],
|
||||
// 接続クライアント: Map<WebSocket, 'hook' | 'control'>
|
||||
clients: new Map()
|
||||
};
|
||||
|
||||
console.log(`🪝 Codex Hook Server listening on ws://localhost:${PORT}`);
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const clientType = req.url === '/control' ? 'control' : 'hook';
|
||||
|
||||
console.log(`📌 New ${clientType} connection`);
|
||||
state.clients.set(ws, clientType);
|
||||
|
||||
ws.on('message', async (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString());
|
||||
|
||||
if (clientType === 'hook') {
|
||||
// Codexフックからのメッセージ
|
||||
await handleHookMessage(msg, ws);
|
||||
} else {
|
||||
// 制御クライアントからのメッセージ
|
||||
await handleControlMessage(ws, msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Message error:', e);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
state.clients.delete(ws);
|
||||
maybeAutoExit();
|
||||
});
|
||||
});
|
||||
|
||||
// ANSIエスケープ除去
|
||||
function stripAnsi(s) {
|
||||
if (!STRIP_ANSI) return s;
|
||||
if (typeof s !== 'string') return s;
|
||||
// Robust ANSI/CSI/OSC sequences removal
|
||||
const ansiPattern = /[\u001B\u009B][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nq-uy=><]/g;
|
||||
return s.replace(ansiPattern, '');
|
||||
}
|
||||
|
||||
// フックメッセージ処理
|
||||
async function handleHookMessage(msg, senderWs) {
|
||||
const preview = typeof msg.data === 'string' ? stripAnsi(msg.data).substring(0, 80) : JSON.stringify(msg.data);
|
||||
console.log(`[${msg.type}] ${preview}`);
|
||||
|
||||
// 全制御クライアントに転送
|
||||
broadcast('control', msg);
|
||||
|
||||
switch (msg.type) {
|
||||
case 'input':
|
||||
state.lastInput = msg.data;
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
state.lastOutput = typeof msg.data === 'string' ? stripAnsi(msg.data) : msg.data;
|
||||
break;
|
||||
|
||||
case 'hook-event':
|
||||
await handleHookEvent(msg);
|
||||
break;
|
||||
|
||||
case 'inject-input':
|
||||
// フッククライアントからの入力注入リクエスト
|
||||
console.log('🔄 Relaying inject-input from hook client');
|
||||
|
||||
// 明示的なターゲットがあればそれを最優先(tmuxセッション名を想定)
|
||||
if (msg.target && typeof msg.target === 'string') {
|
||||
const { spawn } = require('child_process');
|
||||
const text = String(msg.data ?? '');
|
||||
const targetSession = msg.target;
|
||||
console.log(`📤 Sending to explicit target via tmux: ${targetSession}`);
|
||||
|
||||
// 文字列を通常の方法で送信
|
||||
const { exec } = require('child_process');
|
||||
const messageEscaped = text.replace(/'/g, "'\\''");
|
||||
await new Promise((resolve) => {
|
||||
exec(`tmux send-keys -t ${targetSession} '${messageEscaped}' Enter`, (error) => {
|
||||
if (error) console.error(`❌ tmux error: ${error.message}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
if (process.env.HOOK_SEND_CTRL_J === 'true') {
|
||||
await new Promise((resolve) => {
|
||||
const p = spawn('tmux', ['send-keys', '-t', targetSession, 'C-j']);
|
||||
p.on('close', () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ Message + Enter sent to ${targetSession}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 互換ルーティング(source から推測)
|
||||
let targetSession = 'claude';
|
||||
if (msg.source === 'codex') {
|
||||
targetSession = 'claude';
|
||||
} else if (msg.source === 'claude') {
|
||||
targetSession = 'codex';
|
||||
}
|
||||
|
||||
console.log(`📤 Forwarding to ${targetSession}`);
|
||||
|
||||
if (targetSession === 'claude') {
|
||||
// Claude想定:WebSocket経由でstdinに直接送信(注意: 全hookに送られる)
|
||||
console.log('🎯 Sending to Claude via WebSocket (stdin)');
|
||||
broadcast('hook', {
|
||||
type: 'inject-input',
|
||||
data: msg.data,
|
||||
target: 'claude'
|
||||
});
|
||||
} else {
|
||||
// Codex想定:tmux send-keys
|
||||
const { exec } = require('child_process');
|
||||
const text = String(msg.data ?? '');
|
||||
const messageEscaped = text.replace(/'/g, "'\\''");
|
||||
console.log(`📤 Sending to ${targetSession} via tmux`);
|
||||
await new Promise((resolve) => {
|
||||
exec(`tmux send-keys -t ${targetSession} '${messageEscaped}' Enter`, (error) => {
|
||||
if (error) console.error(`❌ tmux error: ${error.message}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
if (process.env.HOOK_SEND_CTRL_J === 'true') {
|
||||
await new Promise((resolve) => {
|
||||
const p = spawn('tmux', ['send-keys', '-t', targetSession, 'C-j']);
|
||||
p.on('close', () => resolve());
|
||||
});
|
||||
}
|
||||
console.log(`✅ Message + Enter sent to ${targetSession}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// フックイベント処理
|
||||
async function handleHookEvent(msg) {
|
||||
switch (msg.event) {
|
||||
case 'question-detected':
|
||||
console.log('❓ Question detected:', msg.data);
|
||||
state.questionQueue.push({
|
||||
question: msg.data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
if (AUTO_BRIDGE) {
|
||||
// 自動ブリッジが有効なら応答を生成
|
||||
setTimeout(() => {
|
||||
injectResponse('考えさせてください...');
|
||||
}, 1000);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'waiting-detected':
|
||||
state.waitingCount++;
|
||||
console.log(`⏳ Waiting detected (count: ${state.waitingCount})`);
|
||||
|
||||
// 3回連続で待機状態なら介入
|
||||
if (state.waitingCount >= 3 && AUTO_BRIDGE) {
|
||||
console.log('🚨 Auto-intervention triggered');
|
||||
injectResponse('続けてください');
|
||||
state.waitingCount = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'codex-exit':
|
||||
console.log('🛑 Codex process exited');
|
||||
maybeAutoExit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 制御メッセージ処理
|
||||
async function handleControlMessage(ws, msg) {
|
||||
switch (msg.op) {
|
||||
case 'inject':
|
||||
injectResponse(msg.data);
|
||||
ws.send(JSON.stringify({ type: 'injected', data: msg.data }));
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
ws.send(JSON.stringify({
|
||||
type: 'status',
|
||||
state: {
|
||||
lastInput: state.lastInput,
|
||||
lastOutput: state.lastOutput.substring(0, 100),
|
||||
waitingCount: state.waitingCount,
|
||||
questionCount: state.questionQueue.length,
|
||||
clients: state.clients.size
|
||||
}
|
||||
}));
|
||||
break;
|
||||
|
||||
case 'questions':
|
||||
ws.send(JSON.stringify({
|
||||
type: 'questions',
|
||||
data: state.questionQueue
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Codexに応答を注入
|
||||
function injectResponse(response) {
|
||||
console.log('💉 Injecting response:', response);
|
||||
|
||||
// フッククライアントに注入コマンドを送信
|
||||
broadcast('hook', {
|
||||
type: 'inject-input',
|
||||
data: response
|
||||
});
|
||||
}
|
||||
|
||||
// ブロードキャスト
|
||||
function broadcast(clientType, message, excludeWs = null) {
|
||||
const data = JSON.stringify(message);
|
||||
let sentCount = 0;
|
||||
for (const [clientWs, type] of state.clients.entries()) {
|
||||
if (type === clientType && clientWs.readyState === WebSocket.OPEN) {
|
||||
if (excludeWs && clientWs === excludeWs) continue; // 送信元を除外
|
||||
clientWs.send(data);
|
||||
sentCount++;
|
||||
}
|
||||
}
|
||||
console.log(`📡 Broadcast to ${sentCount} ${clientType} clients`);
|
||||
}
|
||||
|
||||
// フッククライアントがいなければ自動終了
|
||||
let exitTimer = null;
|
||||
function maybeAutoExit() {
|
||||
if (!AUTO_EXIT) return;
|
||||
const hasHook = Array.from(state.clients.values()).some(t => t === 'hook');
|
||||
if (hasHook) return;
|
||||
if (exitTimer) clearTimeout(exitTimer);
|
||||
exitTimer = setTimeout(() => {
|
||||
const hasHookNow = Array.from(state.clients.values()).some(t => t === 'hook');
|
||||
if (!hasHookNow) {
|
||||
console.log(`\n👋 No hook clients. Auto-exiting hook server (port ${PORT}).`);
|
||||
wss.close();
|
||||
process.exit(0);
|
||||
}
|
||||
}, IDLE_EXIT_MS);
|
||||
}
|
||||
|
||||
// 統計情報の定期出力
|
||||
setInterval(() => {
|
||||
console.log(`📊 Stats: Questions: ${state.questionQueue.length}, Waiting: ${state.waitingCount}, Clients: ${state.clients.size}`);
|
||||
}, 60000);
|
||||
|
||||
// グレースフルシャットダウン
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n👋 Shutting down hook server...');
|
||||
wss.close();
|
||||
process.exit(0);
|
||||
});
|
||||
52
tools/codex-tmux-driver/install-hook.sh
Normal file
52
tools/codex-tmux-driver/install-hook.sh
Normal 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
|
||||
153
tools/codex-tmux-driver/manage-ai-sessions.sh
Normal file
153
tools/codex-tmux-driver/manage-ai-sessions.sh
Normal 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
|
||||
129
tools/codex-tmux-driver/manage-instances.sh
Normal file
129
tools/codex-tmux-driver/manage-instances.sh
Normal 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
|
||||
29
tools/codex-tmux-driver/node_modules/.package-lock.json
generated
vendored
Normal file
29
tools/codex-tmux-driver/node_modules/.package-lock.json
generated
vendored
Normal 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
20
tools/codex-tmux-driver/node_modules/ws/LICENSE
generated
vendored
Normal 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
548
tools/codex-tmux-driver/node_modules/ws/README.md
generated
vendored
Normal file
@ -0,0 +1,548 @@
|
||||
# ws: a Node.js WebSocket library
|
||||
|
||||
[](https://www.npmjs.com/package/ws)
|
||||
[](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
|
||||
[](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
8
tools/codex-tmux-driver/node_modules/ws/browser.js
generated
vendored
Normal 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
13
tools/codex-tmux-driver/node_modules/ws/index.js
generated
vendored
Normal 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;
|
||||
131
tools/codex-tmux-driver/node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
131
tools/codex-tmux-driver/node_modules/ws/lib/buffer-util.js
generated
vendored
Normal 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.
|
||||
}
|
||||
}
|
||||
18
tools/codex-tmux-driver/node_modules/ws/lib/constants.js
generated
vendored
Normal file
18
tools/codex-tmux-driver/node_modules/ws/lib/constants.js
generated
vendored
Normal 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: () => {}
|
||||
};
|
||||
292
tools/codex-tmux-driver/node_modules/ws/lib/event-target.js
generated
vendored
Normal file
292
tools/codex-tmux-driver/node_modules/ws/lib/event-target.js
generated
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
203
tools/codex-tmux-driver/node_modules/ws/lib/extension.js
generated
vendored
Normal file
203
tools/codex-tmux-driver/node_modules/ws/lib/extension.js
generated
vendored
Normal 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
55
tools/codex-tmux-driver/node_modules/ws/lib/limiter.js
generated
vendored
Normal 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;
|
||||
528
tools/codex-tmux-driver/node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
528
tools/codex-tmux-driver/node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal 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
706
tools/codex-tmux-driver/node_modules/ws/lib/receiver.js
generated
vendored
Normal 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
602
tools/codex-tmux-driver/node_modules/ws/lib/sender.js
generated
vendored
Normal 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
161
tools/codex-tmux-driver/node_modules/ws/lib/stream.js
generated
vendored
Normal 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;
|
||||
62
tools/codex-tmux-driver/node_modules/ws/lib/subprotocol.js
generated
vendored
Normal file
62
tools/codex-tmux-driver/node_modules/ws/lib/subprotocol.js
generated
vendored
Normal 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 };
|
||||
152
tools/codex-tmux-driver/node_modules/ws/lib/validation.js
generated
vendored
Normal file
152
tools/codex-tmux-driver/node_modules/ws/lib/validation.js
generated
vendored
Normal 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.
|
||||
}
|
||||
}
|
||||
550
tools/codex-tmux-driver/node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
550
tools/codex-tmux-driver/node_modules/ws/lib/websocket-server.js
generated
vendored
Normal 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
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
69
tools/codex-tmux-driver/node_modules/ws/package.json
generated
vendored
Normal 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
8
tools/codex-tmux-driver/node_modules/ws/wrapper.mjs
generated
vendored
Normal 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;
|
||||
38
tools/codex-tmux-driver/package-lock.json
generated
Normal file
38
tools/codex-tmux-driver/package-lock.json
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tools/codex-tmux-driver/package.json
Normal file
16
tools/codex-tmux-driver/package.json
Normal 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"
|
||||
}
|
||||
67
tools/codex-tmux-driver/quick-start-ai-trinity.sh
Normal file
67
tools/codex-tmux-driver/quick-start-ai-trinity.sh
Normal 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"
|
||||
14
tools/codex-tmux-driver/run-codex-clean.sh
Normal file
14
tools/codex-tmux-driver/run-codex-clean.sh
Normal 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
|
||||
30
tools/codex-tmux-driver/send-clear-and-type.js
Normal file
30
tools/codex-tmux-driver/send-clear-and-type.js
Normal 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);
|
||||
});
|
||||
30
tools/codex-tmux-driver/send-enter-first.js
Normal file
30
tools/codex-tmux-driver/send-enter-first.js
Normal 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);
|
||||
});
|
||||
21
tools/codex-tmux-driver/send-greeting-clean.js
Normal file
21
tools/codex-tmux-driver/send-greeting-clean.js
Normal 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);
|
||||
});
|
||||
21
tools/codex-tmux-driver/send-simple.js
Normal file
21
tools/codex-tmux-driver/send-simple.js
Normal 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);
|
||||
});
|
||||
39
tools/codex-tmux-driver/send-special-keys.js
Normal file
39
tools/codex-tmux-driver/send-special-keys.js
Normal 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);
|
||||
});
|
||||
38
tools/codex-tmux-driver/send-test-modes.js
Normal file
38
tools/codex-tmux-driver/send-test-modes.js
Normal 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);
|
||||
});
|
||||
35
tools/codex-tmux-driver/send-to-other.js
Normal file
35
tools/codex-tmux-driver/send-to-other.js
Normal 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);
|
||||
});
|
||||
24
tools/codex-tmux-driver/send-via-tmux.js
Normal file
24
tools/codex-tmux-driver/send-via-tmux.js
Normal 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);
|
||||
22
tools/codex-tmux-driver/send-with-ctrl-j.js
Normal file
22
tools/codex-tmux-driver/send-with-ctrl-j.js
Normal 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);
|
||||
});
|
||||
22
tools/codex-tmux-driver/send-with-newline.js
Normal file
22
tools/codex-tmux-driver/send-with-newline.js
Normal 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);
|
||||
});
|
||||
71
tools/codex-tmux-driver/start-ai-tmux.sh
Normal file
71
tools/codex-tmux-driver/start-ai-tmux.sh
Normal 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
|
||||
40
tools/codex-tmux-driver/start-all.sh
Normal file
40
tools/codex-tmux-driver/start-all.sh
Normal 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"
|
||||
51
tools/codex-tmux-driver/start-claude-tmux.sh
Normal file
51
tools/codex-tmux-driver/start-claude-tmux.sh
Normal 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"
|
||||
13
tools/codex-tmux-driver/start-codex-clean.sh
Normal file
13
tools/codex-tmux-driver/start-codex-clean.sh
Normal 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
|
||||
19
tools/codex-tmux-driver/start-codex-no-update.sh
Normal file
19
tools/codex-tmux-driver/start-codex-no-update.sh
Normal 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
|
||||
10
tools/codex-tmux-driver/start-codex-simple.sh
Normal file
10
tools/codex-tmux-driver/start-codex-simple.sh
Normal 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 "$@"
|
||||
21
tools/codex-tmux-driver/start-codex-tmux.sh
Normal file
21
tools/codex-tmux-driver/start-codex-tmux.sh
Normal 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"
|
||||
16
tools/codex-tmux-driver/start-codex-with-pty.sh
Normal file
16
tools/codex-tmux-driver/start-codex-with-pty.sh
Normal 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 "$@"
|
||||
65
tools/codex-tmux-driver/start-instance.sh
Normal file
65
tools/codex-tmux-driver/start-instance.sh
Normal 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
|
||||
39
tools/codex-tmux-driver/start.sh
Normal file
39
tools/codex-tmux-driver/start.sh
Normal 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" \
|
||||
"$@"
|
||||
32
tools/codex-tmux-driver/test-ai-communication.js
Normal file
32
tools/codex-tmux-driver/test-ai-communication.js
Normal 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);
|
||||
});
|
||||
35
tools/codex-tmux-driver/test-bidirectional-claude-codex.js
Normal file
35
tools/codex-tmux-driver/test-bidirectional-claude-codex.js
Normal 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接続終了');
|
||||
});
|
||||
35
tools/codex-tmux-driver/test-bidirectional-codex-claude.js
Normal file
35
tools/codex-tmux-driver/test-bidirectional-codex-claude.js
Normal 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接続終了');
|
||||
});
|
||||
69
tools/codex-tmux-driver/test-bidirectional.sh
Normal file
69
tools/codex-tmux-driver/test-bidirectional.sh
Normal 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のターミナルを確認してください!"
|
||||
108
tools/codex-tmux-driver/test-client.js
Normal file
108
tools/codex-tmux-driver/test-client.js
Normal 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('');
|
||||
38
tools/codex-tmux-driver/test-direct-stdin.js
Normal file
38
tools/codex-tmux-driver/test-direct-stdin.js
Normal 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);
|
||||
});
|
||||
47
tools/codex-tmux-driver/test-hook-debug.js
Normal file
47
tools/codex-tmux-driver/test-hook-debug.js
Normal 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);
|
||||
29
tools/codex-tmux-driver/test-send-greeting.js
Normal file
29
tools/codex-tmux-driver/test-send-greeting.js
Normal 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.');
|
||||
});
|
||||
|
||||
39
tools/codex-tmux-driver/test-wrapper-safe.sh
Normal file
39
tools/codex-tmux-driver/test-wrapper-safe.sh
Normal 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"
|
||||
127
tools/codex-tmux-driver/tmux-codex-controller.js
Normal file
127
tools/codex-tmux-driver/tmux-codex-controller.js
Normal 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();
|
||||
}
|
||||
36
tools/codex-tmux-driver/tmux-inject-helper.js
Normal file
36
tools/codex-tmux-driver/tmux-inject-helper.js
Normal 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!');
|
||||
}
|
||||
});
|
||||
});
|
||||
35
tools/codex-tmux-driver/tmux-launch-only.sh
Normal file
35
tools/codex-tmux-driver/tmux-launch-only.sh
Normal 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 " 自動的な操作は行いません"
|
||||
217
tools/codex-tmux-driver/tmux-perfect-bridge.js
Normal file
217
tools/codex-tmux-driver/tmux-perfect-bridge.js
Normal 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();
|
||||
}
|
||||
56
tools/codex-tmux-driver/使い方説明書.txt
Normal file
56
tools/codex-tmux-driver/使い方説明書.txt
Normal 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してください
|
||||
|
||||
==================================
|
||||
Reference in New Issue
Block a user