openinputbridge-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +211 -0
- package/bin/oib_bridge.exe +0 -0
- package/dist/bridge.js +224 -0
- package/dist/bridge.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/keycodes.js +325 -0
- package/dist/keycodes.js.map +1 -0
- package/dist/safety.js +60 -0
- package/dist/safety.js.map +1 -0
- package/dist/tools.js +465 -0
- package/dist/tools.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenInputBridge-MCP Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
NOTE: This project communicates with the OpenInputBridge driver by directly
|
|
26
|
+
implementing the IOCTL protocol documented in OpenInputBridge's
|
|
27
|
+
docs/PROTOCOL.md. It does not use, link against, or redistribute any code
|
|
28
|
+
from oblitum/Interception (which is separately dual-licensed under LGPL /
|
|
29
|
+
commercial terms). See NOTICE.md for details.
|
package/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# OpenInputBridge-MCP
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Applet-LLC/OpenInputBridge-MCP/actions/workflows/build.yml)
|
|
4
|
+
|
|
5
|
+
**[OpenInputBridge](https://github.com/Applet-LLC/OpenInputBridge)(Interception互換のカーネルレベル キーボード/マウス入力ドライバ)を、MCP (Model Context Protocol) 経由のツールとして公開するサーバーです。**
|
|
6
|
+
|
|
7
|
+
GUI/ネイティブアプリのテスト自動化における `SendInput()` / UI Automation / 座標ベース自動化ツールの代替・上位互換として、AIエージェント(Claude Codeなど)やテストコードから、カーネルレベルの合成キーボード/マウス入力を送信できます。
|
|
8
|
+
|
|
9
|
+
> ⚠️ 本プロジェクトは [oblitum/Interception](https://github.com/oblitum/Interception)(LGPL/商用デュアルライセンス)のコードには一切依存していません。ヘルパー実行ファイル(`helper/oib_bridge.c`)は、OpenInputBridge本体の [`docs/PROTOCOL.md`](https://github.com/Applet-LLC/OpenInputBridge/blob/main/docs/PROTOCOL.md) に文書化されたワイヤプロトコルのみを根拠に、独自にIOCTLを実装しています。
|
|
10
|
+
|
|
11
|
+
## これは何のためのツールか
|
|
12
|
+
|
|
13
|
+
`SendInput()` / UI Automation / PyAutoGUI・Selenium等の座標ベース自動化には、テスト自動化の現場でよく遭遇する構造的な限界があります。本ツールはそれらを、ドライバレベルで合成入力を注入することで回避します。
|
|
14
|
+
|
|
15
|
+
| よくある失敗パターン | 原因 | 本ツールでの解決 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 管理者権限で起動したアプリに入力が届かない | UIPI (User Interface Privilege Isolation) により、非管理者プロセスからの合成入力が上位integrity levelのウィンドウにブロックされる | カーネルドライバ層でHIDスタックに直接介在するため、送信元プロセスのintegrity levelに依存しない |
|
|
18
|
+
| RDP/仮想マシン/CI専用機で不安定 | 仮想ディスプレイやリモートセッションでは、`SendInput` が前提とするフォアグラウンドウィンドウ/デスクトップの扱いが環境依存になりやすい | ドライバはセッションが物理/仮想いずれであってもHIDスタック側で動作する |
|
|
19
|
+
| UI Automation/PyAutoGUIが解像度・DPI変更で壊れる | 画面座標やUI要素のプロパティに依存する | キーのメイクコード/マウスの相対移動量ベースで送信するため、解像度非依存 |
|
|
20
|
+
| 一部アプリが合成入力(`SendInput`由来)を区別・無視する | アプリによっては `SendInput` のフラグやRAW_INPUTの出自を見て弾く実装がある | 物理デバイスと同じ経路(`KEYBOARD_INPUT_DATA`/`MOUSE_INPUT_DATA`)でHIDスタックに入るため、アプリ側から区別しにくい |
|
|
21
|
+
|
|
22
|
+
**注意**: 上記はあくまで技術的な限界の回避策であり、「検知されない」ことを保証するものではありません。カーネルレベルのフィルタドライバ自体が検知され得ることは [SECURITY.md](SECURITY.md) に記載しています。**自分が権限を持つ/管理しているテスト環境以外(他社のゲーム・アプリのアンチチート回避目的など)での利用は想定しておらず、対象ソフトウェアの利用規約に違反する可能性がある用途には使用しないでください。**
|
|
23
|
+
|
|
24
|
+
## アーキテクチャ
|
|
25
|
+
|
|
26
|
+
```mermaid
|
|
27
|
+
flowchart TB
|
|
28
|
+
Client["MCPクライアント<br/>(Claude Desktop / Claude Code など)"]
|
|
29
|
+
|
|
30
|
+
subgraph Server["openinputbridge-mcp (Node.js/TypeScript)"]
|
|
31
|
+
direction TB
|
|
32
|
+
McpServer["MCP Server<br/>(stdio transport, ネットワーク非公開)"]
|
|
33
|
+
Safety["Safety Gate<br/>arm必須化 + レート制限"]
|
|
34
|
+
Bridge["OibBridge<br/>JSON Linesクライアント"]
|
|
35
|
+
McpServer --> Safety --> Bridge
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
subgraph Helper["oib_bridge.exe (自作Cヘルパー, MIT)"]
|
|
39
|
+
direction TB
|
|
40
|
+
StdioLoop["stdin/stdout<br/>JSON Lines プロトコル"]
|
|
41
|
+
Watchdog["排他モード<br/>ウォッチドッグスレッド"]
|
|
42
|
+
Ioctl["DeviceIoControl呼び出し"]
|
|
43
|
+
StdioLoop --> Ioctl
|
|
44
|
+
Watchdog -.監視.-> Ioctl
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
subgraph Driver["OpenInputBridgeドライバ"]
|
|
48
|
+
direction TB
|
|
49
|
+
Devices["\\.\interception00-19<br/>(コントロールデバイス)"]
|
|
50
|
+
Filter["oib_kbd.sys / oib_mou.sys<br/>(キーボード/マウス フィルタドライバ)"]
|
|
51
|
+
Devices --> Filter
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Target["対象アプリケーション<br/>(実際のキーボード/マウス入力として着弾)"]
|
|
55
|
+
|
|
56
|
+
Client -- "MCPプロトコル (stdio, JSON-RPC)" --> McpServer
|
|
57
|
+
Bridge -- "子プロセスspawn<br/>stdin/stdout (JSON Lines)" --> StdioLoop
|
|
58
|
+
Ioctl -- "IOCTL_WRITE / IOCTL_SET_FILTER 等" --> Devices
|
|
59
|
+
Filter -- "合成入力として注入<br/>(実HIDスタックと同じ経路)" --> Target
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **stdioトランスポートのみ**。ネットワークリスナーは一切持ちません。MCPクライアントがローカルでサブプロセス起動する通常の使い方のみを想定しています。
|
|
63
|
+
- ヘルパー(`oib_bridge.exe`)とドライバの間は `docs/PROTOCOL.md` を単一の仕様源とし、`third_party/interception`(LGPL)には一切依存しません。
|
|
64
|
+
- MCPサーバー(Node.js)とヘルパー(C)の間は、1行1JSONオブジェクトの単純なリクエスト/レスポンスプロトコルです。
|
|
65
|
+
|
|
66
|
+
## できること(v1ツール一覧)
|
|
67
|
+
|
|
68
|
+
**送信専用**です。物理入力の内容を読み取る/監視するツールは意図的に含んでいません(詳細は [SECURITY.md](SECURITY.md))。
|
|
69
|
+
|
|
70
|
+
| ツール | できること |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `enable_input_control` | このセッションで送信系ツールを有効化する(**最初に必ず1回呼ぶ必要がある**) |
|
|
73
|
+
| `disable_input_control` | 送信系ツールを無効化する |
|
|
74
|
+
| `get_driver_status` | ドライバのインストール状況・バージョン・キーボード/マウスのスロット構成を確認する(診断用、armなしで呼べる) |
|
|
75
|
+
| `press_key` | 1キーをタップ(押して離す)。Ctrl+A等の修飾キー同時押しにも対応 |
|
|
76
|
+
| `key_down` / `key_up` | キーを押しっぱなしにする/離す(複合ジェスチャ用) |
|
|
77
|
+
| `type_text` | 文字列をキーストローク列として送信する(US/JIS/独/仏/露配列は自動判定対応、韓国語字母・台湾注音符号は明示指定) |
|
|
78
|
+
| `mouse_move` | マウスを相対/絶対移動する(絶対移動は`virtualDesktop:true`でマルチモニタ全体を対象にできる) |
|
|
79
|
+
| `mouse_click` | マウスボタン(左/右/中/X1/X2)のクリック・押下・解放 |
|
|
80
|
+
| `mouse_wheel` | 垂直/水平ホイールのスクロール |
|
|
81
|
+
| `enable_exclusive_input_mode` | **排他モード**: 物理キーボード/マウスの入力を全スロットで捕捉・破棄し、このセッションからの合成入力だけを対象アプリに届ける(CI/専用テスト機向け、要armかつ強い注意が必要) |
|
|
82
|
+
| `disable_exclusive_input_mode` | 排他モードを解除する(**armなしでも常に呼び出せるエスケープハッチ**) |
|
|
83
|
+
| `get_exclusive_mode_status` | 排他モードが現在有効かどうかを確認する |
|
|
84
|
+
|
|
85
|
+
## AIエージェントが知っておくべき仕様
|
|
86
|
+
|
|
87
|
+
このMCPサーバーを操作するAIエージェント(あるいはそれを実装する開発者)は、以下を理解しておく必要があります。
|
|
88
|
+
|
|
89
|
+
### 1. 送信前に必ず `enable_input_control` を呼ぶ
|
|
90
|
+
|
|
91
|
+
サーバー起動直後は全ての送信系ツール(`press_key`等)が `NotArmedError` で拒否されます。MCPクライアント自体のツール許可UIとは別に、このドライバ固有の強力さに見合ったもう一段の明示的な同意ステップです。セッション中に1回呼べば、以降はそのプロセスが生きている間は有効です。
|
|
92
|
+
|
|
93
|
+
### 2. キー名はDOM `KeyboardEvent.code` 語彙
|
|
94
|
+
|
|
95
|
+
`press_key`/`key_down`/`key_up` の `key` パラメータは、Playwright/Seleniumのテスト自動化エンジニアに馴染みのある [DOM `KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values) 命名(`KeyA`〜`KeyZ`, `Digit0`〜`Digit9`, `Enter`, `ArrowUp`, `ShiftLeft`, `F1`〜`F12` 等、JIS配列専用の`IntlRo`/`IntlYen`/`Convert`/`NonConvert`/`KanaMode`も含む)を使います。完全な一覧は [`src/keycodes.ts`](src/keycodes.ts) の `KEY_TABLE` を参照してください。これらは物理キー位置ベースなのでレイアウトに依存せず動作します。
|
|
96
|
+
|
|
97
|
+
`type_text` は入力された**文字**からキー+Shift状態を逆算する必要があり、これはOS側のアクティブなキーボードレイアウトに依存します。既定(`layout: "auto"`)では、フォーカス中のウィンドウの入力ロケールを呼び出しごとに検出し、`us`/`jis`(日本語)/`de`(ドイツ語QWERTZ)/`fr`(フランス語AZERTY)/`ru`(ロシア語ЙЦУКЕН)を自動選択します(`layout`パラメータで明示指定も可能)。**US/JISのみ実機で検証済み**です([test/REALWORLD_TESTING.md](test/REALWORLD_TESTING.md)参照)。独/仏/露は標準的なレイアウト仕様を基にした未検証の実装です。
|
|
98
|
+
|
|
99
|
+
`ko`(韓国語2ベルシク)・`tw`(台湾注音符号)も`layout`パラメータで明示指定できますが、**IMEによる合成(ハングル音節・漢字への変換)は行わず、字母/注音記号を1文字ずつそのまま送信します**(例: `layout:"ko"`で"r"+"k"を送ると、合成された音節「가」ではなく字母「ㄱ」「ㅏ」が個別に送信される)。この2つは自動判定の対象外です — 同じ韓国語/繁体中国語キーボードレイアウトのままIMEのON/OFF状態だけが切り替わることがあり、レイアウトのLANGIDだけからは判別できないため、明示的に`layout`を指定した場合のみ有効になります。
|
|
100
|
+
|
|
101
|
+
**中国語(拼音入力)を扱う場合は `layout: "us"`(または `"auto"`)を使ってください。** 中国語入力で主に使われる拼音(ピンイン)方式は、物理キーボード自体はUS配列(QWERTY)そのままで、ローマ字を打鍵した内容をIMEが漢字に変換する仕組みのため、専用のキー配列テーブルは不要です。ただしこれも他の言語と同様、実際のIME変換(拼音→漢字)はスコープ外です。`type_text` はローマ字までしか送れず、続く候補選択・確定はこのMCPサーバーの対象外です。注音符号(ボポモフォ)方式で入力したい場合のみ、上記の `layout: "tw"` で未合成の注音記号を送信できます。
|
|
102
|
+
|
|
103
|
+
上記いずれのレイアウトも、実際のIME変換(ひらがな/漢字、ハングル音節合成、拼音→漢字等)を経由した入力はスコープ外です。
|
|
104
|
+
|
|
105
|
+
### 3. `type_text` は全体を検証してから送信する(部分的な副作用なし)
|
|
106
|
+
|
|
107
|
+
未対応文字(非ASCII等)が1文字でも含まれる場合、何も送信せずエラーを返します。途中まで入力されて残りが失敗する、という状態にはなりません。
|
|
108
|
+
|
|
109
|
+
### 4. デバイススロットの境界は可変
|
|
110
|
+
|
|
111
|
+
`\\.\interception00`〜`19` の20スロットのうち、どこまでがキーボードでどこからがマウスかは、ドライバのインストール時設定(`KeyboardSlotCount`)次第で変わります(デフォルトは10/10)。ツール側のデフォルト値(キーボード系は`device=0`、マウス系は`device=10`)は既定構成を前提にしているため、複数デバイス/非既定構成を扱う場合は `get_driver_status` の `keyboardSlotCount`/`mouseSlotCount` を先に確認してください。
|
|
112
|
+
|
|
113
|
+
### 5. レート制限がある
|
|
114
|
+
|
|
115
|
+
既定では10秒間に最大500入力イベントまで(環境変数 `OIB_MCP_RATE_LIMIT_MAX` / `OIB_MCP_RATE_LIMIT_WINDOW_MS` で変更可能)。暴走したエージェント(プロンプトインジェクション含む)が入力を連射し続けることを防ぐためのものです。超過すると `RateLimitError` が返ります。
|
|
116
|
+
|
|
117
|
+
### 6. 排他モードは強力・危険。CI/専用テスト機以外では使わない
|
|
118
|
+
|
|
119
|
+
`enable_exclusive_input_mode` を有効化すると、**オペレーターが物理キーボード/マウスを操作しても対象アプリには一切反映されなくなります**。日常利用中のPCで有効化すると物理入力が使えなくなるため、無人のテスト実行環境(CI・専用テスト機)での利用のみを想定しています。
|
|
120
|
+
|
|
121
|
+
- ハートビートが一定時間(既定5秒、`watchdogTimeoutMs`で設定可)途絶えると自動的に解除されます
|
|
122
|
+
- `disable_exclusive_input_mode` は arm状態やレート制限に関係なく**常に呼び出せます**
|
|
123
|
+
- MCPサーバーやAIエージェント自体が応答不能になった場合の最終手段として、**`oib_bridge.exe` プロセスを終了させると、ドライバ側の仕組みにより即座に物理入力が復元されます**(Interceptionプロトコルのハンドルクローズ時クリーンアップによるもので、他のいかなるプロセスもこれを代替できません)。詳細は [SECURITY.md](SECURITY.md) を参照してください。
|
|
124
|
+
|
|
125
|
+
### 7. v1には「読み取り・監視系」ツールがない
|
|
126
|
+
|
|
127
|
+
物理キーボード/マウスの入力内容をAIエージェントに渡すツール(`IOCTL_READ`/`interception_receive`相当)は意図的に実装していません。これは「MCP経由でAIがシステム全体のキー入力を盗聴できる」という最も深刻な悪用シナリオを設計上排除するためです。
|
|
128
|
+
|
|
129
|
+
## 前提条件
|
|
130
|
+
|
|
131
|
+
- **Windows専用**(OpenInputBridge自体がWindows専用のため)
|
|
132
|
+
- [OpenInputBridge](https://github.com/Applet-LLC/OpenInputBridge) ドライバがインストール済み・起動していること(`sc.exe query OpenInputBridgeKeyboard` / `OpenInputBridgeMouse` が `RUNNING`)
|
|
133
|
+
- Node.js 18以上
|
|
134
|
+
- ヘルパー実行ファイルのビルドに Visual Studio 2022 (C++ ビルドツール) — 事前ビルド済みバイナリの配布は今後の予定です(下記「既知の制限」参照)
|
|
135
|
+
|
|
136
|
+
## クイックスタート
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
git clone https://github.com/Applet-LLC/OpenInputBridge-MCP.git
|
|
140
|
+
cd OpenInputBridge-MCP
|
|
141
|
+
npm install
|
|
142
|
+
npm run build
|
|
143
|
+
|
|
144
|
+
# C ヘルパーのビルド (Visual Studio Developer PowerShell/コマンドプロンプトで)
|
|
145
|
+
cl.exe /nologo /W4 /utf-8 /Fe:helper\oib_bridge.exe helper\oib_bridge.c
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
npm公開後は `npx openinputbridge-mcp` でのビルド不要インストールを予定していますが、現時点では未公開のため上記のソースビルドが必要です(詳細は「既知の制限」参照)。
|
|
149
|
+
|
|
150
|
+
MCPクライアント(例: Claude Code の `.mcp.json`)に登録します。
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"mcpServers": {
|
|
155
|
+
"openinputbridge": {
|
|
156
|
+
"command": "node",
|
|
157
|
+
"args": ["C:\\path\\to\\OpenInputBridge-MCP\\dist\\index.js"]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
接続後、まず `get_driver_status` でドライバが認識されているか確認し、`enable_input_control` を呼んでから各ツールを使用してください。
|
|
164
|
+
|
|
165
|
+
## 既知の制限
|
|
166
|
+
|
|
167
|
+
実機(OpenInputBridgeインストール環境)での検証を実施済みです。詳細は [test/REALWORLD_TESTING.md](test/REALWORLD_TESTING.md) を参照してください。
|
|
168
|
+
|
|
169
|
+
- **US/JIS/独/仏/露配列に対応**(`type_text`が呼び出しごとにフォーカス中ウィンドウのレイアウトを自動検出、明示指定も可)。**US/JISのみ実機検証済み**、独/仏/露は標準仕様ベースの未検証実装です。韓国語字母・台湾注音符号は明示指定のみ(自動判定対象外)、いずれもIME合成なしの生記号送信です。**中国語(拼音入力)は物理配列がUS配列と同一のため専用レイアウトは無く、`layout:"us"`(既定の`"auto"`でも可)を使用**します。IME経由のひらがな/漢字/ハングル音節/拼音変換等はスコープ外
|
|
170
|
+
- JIS配列の「¥」キーは(Windowsの既知の仕様により)実際にはASCIIバックスラッシュを送出し、真のyen記号文字(U+00A5)を`type_text`で入力する手段はありません(物理キーそのものは`press_key({key:"IntlYen"})`で押せます)
|
|
171
|
+
- `type_text`でShift状態を1文字ごとに切り替える極端なパターン(例: `"MiXeD"`)は、タイミング対策後も一部の文字でShiftが反映されないことがあります。通常の英文・識別子等では問題にならないことを確認済みです
|
|
172
|
+
- マウスの相対移動(`mouse_move`, `absolute:false`)はOSのポインタ加速の影響を受けるため、指定した移動量とカーソルの実際の移動量は一致しません(物理マウスと同じ経路のため、想定通りの挙動)
|
|
173
|
+
- マウスの絶対移動(`absolute:true`)の0-65535正規化座標は、既定では**プライマリモニタの物理ピクセル範囲**にマッピングされます(DPIスケーリング設定とは無関係。標準Win32 `SendInput`の`MOUSEEVENTF_ABSOLUTE`と同じ仕様)。**セカンダリモニタなど、仮想デスクトップ全体を対象にしたい場合は`virtualDesktop:true`を指定してください**(標準`SendInput`の`MOUSEEVENTF_VIRTUALDESK`相当)。マルチモニタ環境では相対移動(`absolute:false`)でモニタ境界をまたぐことも可能です。実機検証・`mouse_click`によるクリック精度確認済みです(詳細は [test/REALWORLD_TESTING.md](test/REALWORLD_TESTING.md) の項目6)
|
|
174
|
+
- **Bluetoothキーボード/マウス等、切断され得るデバイスでは、非接続時に送信が失敗します**: OpenInputBridgeデバイスドライバは、その時点で実際に接続されているキーボード/マウスのみを対象とします。Bluetooth接続の入力デバイスが省電力モードなどで切断状態になっている間は、MCPサーバー経由の入力送信も失敗します。常時接続の有線デバイス、またはCI/専用テスト機での利用を推奨します
|
|
175
|
+
- **Windows専用**
|
|
176
|
+
- **読み取り・監視系ツールなし**(意図的、上記参照)
|
|
177
|
+
- **事前ビルド済みバイナリ未配布**: 現状 `helper/oib_bridge.c` を利用者自身がビルドする必要があります。npm公開の準備(`.github/workflows/release.yml`、`scripts/check-native-binary.mjs`等)は整いましたが、実際の公開はまだ行っていません。長期有効なトークンをリポジトリに置かない[npm Trusted Publishing(OIDC)](https://docs.npmjs.com/trusted-publishers/)方式を採用する予定で、Trusted Publisherの設定自体は「既存パッケージの設定ページ」から行う仕様のため、最初の1回だけ保守側が手元から`npm publish`する必要があります(それ以降のリリースは`release.yml`がタグpushで自動公開)。公開後もビルド成果物(`oib_bridge.exe`)はコード署名されない見込みで、初回実行時にWindows SmartScreen等の警告が出る可能性があります(署名は将来検討)
|
|
178
|
+
|
|
179
|
+
## セキュリティ
|
|
180
|
+
|
|
181
|
+
このツールが持つ能力(無昇格プロセスからのシステム全体入力の注入)のリスクと、実装済みの安全機構については [SECURITY.md](SECURITY.md) を必ず読んでください。
|
|
182
|
+
|
|
183
|
+
## ロードマップ
|
|
184
|
+
|
|
185
|
+
| マイルストーン | 内容 | 状態 |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| M1 | プロトタイプ: Cヘルパー(`oib_bridge.exe`) + TypeScript製MCPサーバーのスケルトン | ✅ 完了 |
|
|
188
|
+
| M2 | v1ツール一式(送信専用)+ セーフティ機構(arm/レート制限)の実装 | ✅ 完了 |
|
|
189
|
+
| M3 | 排他モードの実装(物理入力の捕捉・破棄、ウォッチドッグによる自動解除) | ✅ 完了 |
|
|
190
|
+
| M4 | 実機検証(実際のOpenInputBridgeインストール環境での動作確認・バグ修正、US/JIS配列対応) | ✅ 完了(詳細は [test/REALWORLD_TESTING.md](test/REALWORLD_TESTING.md)) |
|
|
191
|
+
| M5 | GitHubでの公開(MITライセンス、パブリックリポジトリ) | ✅ 完了 |
|
|
192
|
+
| M6 | GitHub Actionsによるビルド検証(push/PRごとにCヘルパー+TypeScript双方をビルド、`oib_bridge.exe`のスモークテスト) | ✅ 完了 |
|
|
193
|
+
| M6b | ビルド成果物の署名検討、npmパッケージ公開(`npx openinputbridge-mcp`) | 🚧 公開準備完了(タグpush時の`release.yml`、Trusted Publishing対応)、実公開は初回手動publish + Trusted Publisher設定待ち |
|
|
194
|
+
| M7 | クローズドベータ: 複数環境(非既定`KeyboardSlotCount`構成、複数物理キーボードの個別指定送信、他レイアウト等)での動作確認 | 🔲 未着手 |
|
|
195
|
+
| M8 | MCPサーバーディレクトリへの掲載検討(安定運用の確認後) | 🔲 未着手 |
|
|
196
|
+
|
|
197
|
+
今後の検証・改善候補(優先度未確定、詳細は [test/REALWORLD_TESTING.md](test/REALWORLD_TESTING.md) の「未実施の検証」参照):
|
|
198
|
+
|
|
199
|
+
- 非既定の`KeyboardSlotCount`構成での動作確認
|
|
200
|
+
- 複数物理キーボードを`device`パラメータで個別指定して送信する動作の確認
|
|
201
|
+
- 独/仏/露/韓国語字母/台湾注音符号レイアウトの実機検証(現状は標準仕様ベースの未検証実装)
|
|
202
|
+
- 中国語(拼音等)・韓国語・台湾のIME合成対応(現状はIME非対応が既定方針。将来検討)
|
|
203
|
+
|
|
204
|
+
## ライセンス
|
|
205
|
+
|
|
206
|
+
[MIT](LICENSE)。`third_party/interception`(LGPL)のコードには一切依存していません。
|
|
207
|
+
|
|
208
|
+
## Contributors
|
|
209
|
+
|
|
210
|
+
- **[Applet-LLC](https://github.com/Applet-LLC)** — プロジェクトオーナー
|
|
211
|
+
- **Claude**([Anthropic](https://www.anthropic.com/)、[Claude Code](https://claude.com/claude-code) 経由)— 実装・実機検証・ドキュメント作成に貢献
|
|
Binary file
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the oib_bridge.exe child process and speaks its JSON-Lines
|
|
3
|
+
* request/response protocol over stdin/stdout (one JSON object per line,
|
|
4
|
+
* matched by numeric "id"; lines with no "id" are unsolicited events -
|
|
5
|
+
* currently only the watchdog's exclusive-mode auto-disable notice). See
|
|
6
|
+
* helper/oib_bridge.c for the protocol.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import { EventEmitter } from "node:events";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
export class OibBridgeError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "OibBridgeError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Locates the compiled helper executable: packaged (bin/) first, then a local source build (helper/). */
|
|
21
|
+
export function resolveHelperExePath() {
|
|
22
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const candidates = [
|
|
24
|
+
join(moduleDir, "..", "bin", "oib_bridge.exe"),
|
|
25
|
+
join(moduleDir, "..", "helper", "oib_bridge.exe"),
|
|
26
|
+
];
|
|
27
|
+
for (const candidate of candidates) {
|
|
28
|
+
if (existsSync(candidate))
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
throw new OibBridgeError(`oib_bridge.exe not found. Looked in:\n ${candidates.join("\n ")}\n` +
|
|
32
|
+
"Build it from helper/oib_bridge.c (see README.md) or install a packaged release.");
|
|
33
|
+
}
|
|
34
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 3000;
|
|
35
|
+
/** Heartbeats are sent at roughly a third of the watchdog timeout, never below this. */
|
|
36
|
+
const MIN_HEARTBEAT_INTERVAL_MS = 500;
|
|
37
|
+
export class OibBridge extends EventEmitter {
|
|
38
|
+
exePath;
|
|
39
|
+
child = null;
|
|
40
|
+
rl = null;
|
|
41
|
+
nextId = 1;
|
|
42
|
+
pending = new Map();
|
|
43
|
+
heartbeatTimer = null;
|
|
44
|
+
constructor(exePath = resolveHelperExePath()) {
|
|
45
|
+
super();
|
|
46
|
+
this.exePath = exePath;
|
|
47
|
+
}
|
|
48
|
+
ensureStarted() {
|
|
49
|
+
if (this.child)
|
|
50
|
+
return this.child;
|
|
51
|
+
const child = spawn(this.exePath, [], { stdio: ["pipe", "pipe", "pipe"] });
|
|
52
|
+
this.child = child;
|
|
53
|
+
const rl = createInterface({ input: child.stdout });
|
|
54
|
+
rl.on("line", (line) => this.handleLine(line));
|
|
55
|
+
this.rl = rl;
|
|
56
|
+
child.on("exit", (code, signal) => this.handleTermination(code, signal));
|
|
57
|
+
child.on("error", (err) => this.handleTermination(null, null, err));
|
|
58
|
+
return child;
|
|
59
|
+
}
|
|
60
|
+
handleLine(line) {
|
|
61
|
+
if (!line)
|
|
62
|
+
return;
|
|
63
|
+
let msg;
|
|
64
|
+
try {
|
|
65
|
+
msg = JSON.parse(line);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return; // malformed line from the helper; ignore rather than crash the server
|
|
69
|
+
}
|
|
70
|
+
if (typeof msg.event === "string") {
|
|
71
|
+
this.handleEvent(msg);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const response = msg;
|
|
75
|
+
const pending = this.pending.get(response.id);
|
|
76
|
+
if (!pending)
|
|
77
|
+
return;
|
|
78
|
+
this.pending.delete(response.id);
|
|
79
|
+
clearTimeout(pending.timer);
|
|
80
|
+
pending.resolve(response);
|
|
81
|
+
}
|
|
82
|
+
handleEvent(evt) {
|
|
83
|
+
if (evt.event === "exclusive_mode_auto_disabled") {
|
|
84
|
+
this.stopHeartbeat();
|
|
85
|
+
this.emit("exclusiveModeAutoDisabled", {
|
|
86
|
+
reason: String(evt.reason ?? "unknown"),
|
|
87
|
+
failedDeviceCount: Number(evt.failedDeviceCount ?? 0),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
handleTermination(code, signal, spawnError) {
|
|
92
|
+
this.stopHeartbeat();
|
|
93
|
+
const reason = spawnError
|
|
94
|
+
? `failed to launch oib_bridge.exe: ${spawnError.message}`
|
|
95
|
+
: `oib_bridge.exe exited unexpectedly (code=${code ?? "null"}, signal=${signal ?? "null"})`;
|
|
96
|
+
const err = new OibBridgeError(reason);
|
|
97
|
+
for (const req of this.pending.values()) {
|
|
98
|
+
clearTimeout(req.timer);
|
|
99
|
+
req.reject(err);
|
|
100
|
+
}
|
|
101
|
+
this.pending.clear();
|
|
102
|
+
this.rl?.close();
|
|
103
|
+
this.rl = null;
|
|
104
|
+
this.child = null;
|
|
105
|
+
}
|
|
106
|
+
request(cmd, fields = {}, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
107
|
+
const child = this.ensureStarted();
|
|
108
|
+
const id = this.nextId++;
|
|
109
|
+
const line = JSON.stringify({ id, cmd, ...fields }) + "\n";
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
this.pending.delete(id);
|
|
113
|
+
reject(new OibBridgeError(`bridge request timed out: ${cmd}`));
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
116
|
+
child.stdin.write(line, (err) => {
|
|
117
|
+
if (err) {
|
|
118
|
+
this.pending.delete(id);
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
reject(new OibBridgeError(`failed to write to oib_bridge.exe: ${err.message}`));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}).then((msg) => {
|
|
124
|
+
if (!msg.ok) {
|
|
125
|
+
throw new OibBridgeError(msg.error ?? `${cmd} failed with no error message`);
|
|
126
|
+
}
|
|
127
|
+
return msg;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async status() {
|
|
131
|
+
const msg = await this.request("status");
|
|
132
|
+
return {
|
|
133
|
+
installed: true,
|
|
134
|
+
versionMajor: Number(msg.versionMajor),
|
|
135
|
+
versionMinor: Number(msg.versionMinor),
|
|
136
|
+
keyboardSlotCount: msg.keyboardSlotCount !== undefined ? Number(msg.keyboardSlotCount) : undefined,
|
|
137
|
+
mouseSlotCount: msg.mouseSlotCount !== undefined ? Number(msg.mouseSlotCount) : undefined,
|
|
138
|
+
exclusiveModeActive: Boolean(msg.exclusiveModeActive),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Returns the Windows LANGID (low word of the HKL) of whatever window currently has focus. */
|
|
142
|
+
async getActiveKeyboardLayout() {
|
|
143
|
+
const msg = await this.request("get_active_keyboard_layout");
|
|
144
|
+
return Number(msg.languageId);
|
|
145
|
+
}
|
|
146
|
+
async writeKey(device, makeCode, down, extended) {
|
|
147
|
+
await this.request("write_key", {
|
|
148
|
+
device,
|
|
149
|
+
makeCode,
|
|
150
|
+
down: down ? 1 : 0,
|
|
151
|
+
extended: extended ? 1 : 0,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async writeMouseButton(device, buttonFlags) {
|
|
155
|
+
await this.request("write_mouse_button", { device, buttonFlags });
|
|
156
|
+
}
|
|
157
|
+
async writeMouseMove(device, x, y, absolute, virtualDesktop = false) {
|
|
158
|
+
await this.request("write_mouse_move", {
|
|
159
|
+
device,
|
|
160
|
+
x,
|
|
161
|
+
y,
|
|
162
|
+
absolute: absolute ? 1 : 0,
|
|
163
|
+
virtualDesktop: virtualDesktop ? 1 : 0,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async writeMouseWheel(device, rolling, horizontal) {
|
|
167
|
+
await this.request("write_mouse_wheel", { device, rolling, horizontal: horizontal ? 1 : 0 });
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Arms exclusive input mode: every physical keyboard/mouse slot captures
|
|
171
|
+
* and discards real input, while this bridge's own write* calls still
|
|
172
|
+
* reach the real input stream. Starts sending periodic heartbeats so the
|
|
173
|
+
* helper's watchdog doesn't auto-disable it. See helper/oib_bridge.c.
|
|
174
|
+
*/
|
|
175
|
+
async enableExclusiveMode(watchdogTimeoutMs) {
|
|
176
|
+
const msg = await this.request("enable_exclusive_input_mode", { watchdogTimeoutMs }, 5000);
|
|
177
|
+
const info = {
|
|
178
|
+
keyboardSlotCount: Number(msg.keyboardSlotCount),
|
|
179
|
+
mouseSlotCount: Number(msg.mouseSlotCount),
|
|
180
|
+
watchdogTimeoutMs: Number(msg.watchdogTimeoutMs),
|
|
181
|
+
};
|
|
182
|
+
this.startHeartbeat(info.watchdogTimeoutMs);
|
|
183
|
+
return info;
|
|
184
|
+
}
|
|
185
|
+
async disableExclusiveMode() {
|
|
186
|
+
this.stopHeartbeat();
|
|
187
|
+
const msg = await this.request("disable_exclusive_input_mode", {}, 5000);
|
|
188
|
+
return {
|
|
189
|
+
wasActive: Boolean(msg.wasActive),
|
|
190
|
+
failedDeviceCount: Number(msg.failedDeviceCount ?? 0),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Sends one heartbeat and returns the helper's authoritative exclusive-mode state. */
|
|
194
|
+
async heartbeat() {
|
|
195
|
+
const msg = await this.request("heartbeat");
|
|
196
|
+
return Boolean(msg.exclusiveModeActive);
|
|
197
|
+
}
|
|
198
|
+
startHeartbeat(watchdogTimeoutMs) {
|
|
199
|
+
this.stopHeartbeat();
|
|
200
|
+
const intervalMs = Math.max(MIN_HEARTBEAT_INTERVAL_MS, Math.floor(watchdogTimeoutMs / 3));
|
|
201
|
+
this.heartbeatTimer = setInterval(() => {
|
|
202
|
+
this.request("heartbeat").catch(() => {
|
|
203
|
+
// Best-effort; if the bridge is gone, handleTermination() already
|
|
204
|
+
// stopped this timer and rejected everything else.
|
|
205
|
+
});
|
|
206
|
+
}, intervalMs);
|
|
207
|
+
this.heartbeatTimer.unref?.();
|
|
208
|
+
}
|
|
209
|
+
stopHeartbeat() {
|
|
210
|
+
if (this.heartbeatTimer) {
|
|
211
|
+
clearInterval(this.heartbeatTimer);
|
|
212
|
+
this.heartbeatTimer = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Terminates the helper process, if running. Safe to call multiple times. */
|
|
216
|
+
dispose() {
|
|
217
|
+
this.stopHeartbeat();
|
|
218
|
+
if (!this.child)
|
|
219
|
+
return;
|
|
220
|
+
this.child.stdin?.end();
|
|
221
|
+
this.child.kill();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* openinputbridge-mcp entry point. Starts an MCP server over stdio that
|
|
4
|
+
* exposes OpenInputBridge (kernel-level, Interception-compatible key/mouse
|
|
5
|
+
* injection) as SendInput()-alternative tools for GUI/native-app test
|
|
6
|
+
* automation. See README.md for setup and SECURITY.md for the safety model.
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { OibBridge } from "./bridge.js";
|
|
11
|
+
import { SafetyGate, DEFAULT_SAFETY_OPTIONS } from "./safety.js";
|
|
12
|
+
import { registerTools } from "./tools.js";
|
|
13
|
+
function readSafetyOptionsFromEnv() {
|
|
14
|
+
const maxEventsPerWindow = Number(process.env.OIB_MCP_RATE_LIMIT_MAX ?? DEFAULT_SAFETY_OPTIONS.maxEventsPerWindow);
|
|
15
|
+
const windowMs = Number(process.env.OIB_MCP_RATE_LIMIT_WINDOW_MS ?? DEFAULT_SAFETY_OPTIONS.windowMs);
|
|
16
|
+
return {
|
|
17
|
+
maxEventsPerWindow: Number.isFinite(maxEventsPerWindow) && maxEventsPerWindow > 0 ? maxEventsPerWindow : DEFAULT_SAFETY_OPTIONS.maxEventsPerWindow,
|
|
18
|
+
windowMs: Number.isFinite(windowMs) && windowMs > 0 ? windowMs : DEFAULT_SAFETY_OPTIONS.windowMs,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
if (process.platform !== "win32") {
|
|
23
|
+
process.stderr.write("openinputbridge-mcp: OpenInputBridge is Windows-only; this process will start but every tool call will fail.\n");
|
|
24
|
+
}
|
|
25
|
+
const bridge = new OibBridge();
|
|
26
|
+
const safety = new SafetyGate(readSafetyOptionsFromEnv());
|
|
27
|
+
bridge.on("exclusiveModeAutoDisabled", (info) => {
|
|
28
|
+
process.stderr.write(`openinputbridge-mcp: exclusive input mode was auto-disabled by the watchdog ` +
|
|
29
|
+
`(reason=${info.reason}, failedDeviceCount=${info.failedDeviceCount}); physical input restored.\n`);
|
|
30
|
+
});
|
|
31
|
+
const server = new McpServer({
|
|
32
|
+
name: "openinputbridge-mcp",
|
|
33
|
+
version: "0.1.0",
|
|
34
|
+
});
|
|
35
|
+
registerTools(server, bridge, safety);
|
|
36
|
+
const shutdown = () => {
|
|
37
|
+
bridge.dispose();
|
|
38
|
+
process.exit(0);
|
|
39
|
+
};
|
|
40
|
+
process.on("SIGINT", shutdown);
|
|
41
|
+
process.on("SIGTERM", shutdown);
|
|
42
|
+
const transport = new StdioServerTransport();
|
|
43
|
+
await server.connect(transport);
|
|
44
|
+
}
|
|
45
|
+
main().catch((err) => {
|
|
46
|
+
process.stderr.write(`openinputbridge-mcp: fatal error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,SAAS,wBAAwB;IAC/B,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IACnH,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IACrG,OAAO;QACL,kBAAkB,EAAE,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,kBAAkB,GAAG,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,sBAAsB,CAAC,kBAAkB;QAClJ,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,sBAAsB,CAAC,QAAQ;KACjG,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gHAAgH,CACjH,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,wBAAwB,EAAE,CAAC,CAAC;IAE1D,MAAM,CAAC,EAAE,CAAC,2BAA2B,EAAE,CAAC,IAAI,EAAE,EAAE;QAC9C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,8EAA8E;YAC5E,WAAW,IAAI,CAAC,MAAM,uBAAuB,IAAI,CAAC,iBAAiB,+BAA+B,CACrG,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAEtC,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,MAAM,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7H,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|