poi-plugin-mcp 0.2.16 → 0.2.22

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/README.md CHANGED
@@ -1,201 +1,290 @@
1
- # poi-plugin-mcp
2
-
3
- `poi-plugin-mcp` 是运行在 Poi 内的本地数据桥。它把当前舰队、舰娘、装备、
4
- 资源、任务、陆航和相关主数据通过本机 HTTP 与 MCP 提供给本机工具。
5
-
6
- 默认监听:
7
-
8
- ```none
9
- http://127.0.0.1:17777
10
- ```
11
-
12
- 服务只绑定本机回环地址 `127.0.0.1`,不对局域网或公网监听。本文介绍用于
13
- 读取 Poi 状态的 MCP 工具、MCP 资源和 HTTP 数据端点。
14
-
15
- ## 安装与重启
16
-
17
- 在 Poi 插件管理器中安装 npm 包:
18
-
19
- ```none
20
- poi-plugin-mcp
21
- ```
22
-
23
- 也可以完全退出 Poi 后,在 Poi 插件目录中手动安装:
24
-
25
- ```powershell
26
- cd "$env:APPDATA\poi\plugins"
27
- npm install poi-plugin-mcp
28
- ```
29
-
30
- 安装或更新后完整退出并重新启动 Poi。在插件设置中确认“`MCP 数据桥`”已启用,
31
- 然后进入游戏;需要读取账号数据的端点要等 Poi store 初始化完成。
32
-
33
- 如果 MCP 客户端要使用 stdio 方式,可另外全局安装同一个 npm 包,使
34
- `poi-mcp` 命令进入 `PATH`:
35
-
36
- ```powershell
37
- npm install -g poi-plugin-mcp
38
- Get-Command poi-mcp
39
- ```
40
-
41
- ## 健康检查与端口发现
42
-
43
- 默认端口的检查命令:
44
-
45
- ```powershell
46
- Invoke-RestMethod http://127.0.0.1:17777/health
47
- ```
48
-
49
- 正常响应:
50
-
51
- ```json
52
- {"status":"ok"}
53
- ```
54
-
55
- 插件启动后会把实际端口写入:
56
-
57
- ```none
58
- %USERPROFILE%\.poi-mcp\port
59
- ```
60
-
61
- 修改过端口时,应以该文件为准:
62
-
63
- ```powershell
64
- $port = (Get-Content "$HOME\.poi-mcp\port" -Raw).Trim()
65
- Invoke-RestMethod "http://127.0.0.1:$port/health"
66
- ```
67
-
68
- 根路径 `/` 返回 404 不代表服务故障,请使用 `/health`。
69
-
70
- ## MCP 接入
71
-
72
- 插件提供两种 MCP 连接方式:
73
-
74
- - **HTTP MCP**:客户端直接连接 Poi 内的 `POST /mcp`。
75
- - **stdio MCP**:客户端启动 npm 安装的 `poi-mcp` 命令;该命令读取端口文件,
76
- 再访问 Poi 的本地数据 API。
77
-
78
- 两种方式都要求 Poi 正在运行且插件已加载。
79
-
80
- ### HTTP MCP
81
-
82
- MCP URL:
83
-
84
- ```none
85
- http://127.0.0.1:17777/mcp
86
- ```
87
-
88
- 这是 JSON-RPC over HTTP 端点,请由 MCP 客户端向它发送 `POST` 请求。Codex
89
- 配置示例:
90
-
91
- ```toml
92
- [mcp_servers.poi]
93
- url = "http://127.0.0.1:17777/mcp"
94
- ```
95
-
96
- 使用 JSON 配置的 MCP 客户端可采用:
97
-
98
- ```json
99
- {
100
- "mcpServers": {
101
- "poi": {
102
- "url": "http://127.0.0.1:17777/mcp"
103
- }
104
- }
105
- }
106
- ```
107
-
108
- 若修改过端口,请把 URL 中的 `17777` 替换为端口文件中的值。
109
-
110
- ### stdio CLI
111
-
112
- `poi-mcp` 是 MCP stdio 服务命令,不是交互式终端程序。MCP 客户端应负责启动
113
- 并通过标准输入输出与它通信。
114
-
115
- Codex 配置示例:
116
-
117
- ```toml
118
- [mcp_servers.poi]
119
- command = "poi-mcp"
120
- args = []
121
- ```
122
-
123
- 通用 JSON 配置示例:
124
-
125
- ```json
126
- {
127
- "mcpServers": {
128
- "poi": {
129
- "command": "poi-mcp",
130
- "args": []
131
- }
132
- }
133
- }
134
- ```
135
-
136
- 如果桌面客户端找不到 npm 的全局命令,请先运行 `Get-Command poi-mcp`,再把
137
- 配置中的 `command` 改为返回的完整可执行文件路径。
138
-
139
- ## MCP 工具
140
-
141
- | 工具 | 用途 |
142
- |---|---|
143
- | `get_fleet_status` | 按 `fleetId`(1-4)读取一支舰队及舰娘、装备实例 |
144
- | `search_ships` | 按可选的 `minLevel`、`maxLevel`、`minMorale` 筛选持有舰娘 |
145
- | `search_equipment` | 按可选的 `minLevel` 筛选持有装备 |
146
- | `get_resources` | 读取当前资源数组 |
147
- | `get_all` | 读取基础汇总;`include` 可选 `master`、`event`、`planner` |
148
-
149
- ## MCP 资源
150
-
151
- 可通过 `resources/list` 和 `resources/read` 读取:
152
-
153
- ```none
154
- poi://basic
155
- poi://fleets
156
- poi://ships
157
- poi://equipment
158
- poi://resources
159
- poi://quests
160
- poi://airbase
161
- poi://names
162
- poi://master
163
- poi://event
164
- poi://planner
165
- poi://all
166
- ```
167
-
168
- ## 数据端点
169
-
170
- 主要的本地 HTTP 数据查询端点:
171
-
172
- | 端点 | 内容 |
173
- |---|---|
174
- | `GET /health` | bridge 健康状态 |
175
- | `GET /basic` | 提督基础信息 |
176
- | `GET /fleets` | 舰队数据 |
177
- | `GET /ships` | 持有舰娘实例 |
178
- | `GET /equipment` | 持有装备实例 |
179
- | `GET /resources` | 当前资源 |
180
- | `GET /quests` | 当前任务与任务记录 |
181
- | `GET /airbase` | 基地航空队 |
182
- | `GET /names` | 舰娘、装备和远征名称映射 |
183
- | `GET /master` | 舰娘、装备、类型、远征等主数据 |
184
- | `GET /event` | 活动标签定义与持有舰娘出击标签 |
185
- | `GET /planner` | Ship Info 配装规划数据 |
186
- | `GET /battle` | 已观测战斗数据、结算与简化战斗状态 |
187
- | `GET /all` | 基础运行数据汇总 |
188
- | `POST /mcp` | HTTP MCP JSON-RPC 入口 |
189
-
190
- ## 开发验证
191
-
192
- 在源码目录执行:
193
-
194
- ```powershell
195
- npm install
196
- npm test
197
- npm pack --dry-run --ignore-scripts
198
- ```
199
-
200
- `npm test` 应运行 `node --test test/*.test.js`;打包预览应包含
201
- `index.js`、`lib`、`mcp-server.js` 和本 README。
1
+ # poi-plugin-mcp
2
+
3
+ `poi-plugin-mcp` 是运行在 Poi 内的本地数据桥。它把当前舰队、舰娘、装备、
4
+ 资源、任务、陆航和相关主数据通过本机 HTTP 与 MCP 提供给本机工具。
5
+
6
+ 默认监听:
7
+
8
+ ```none
9
+ http://127.0.0.1:17777
10
+ ```
11
+
12
+ 服务只绑定本机回环地址 `127.0.0.1`,不对局域网或公网监听。本文介绍用于
13
+ 读取 Poi 状态的 MCP 工具、MCP 资源和 HTTP 数据端点。
14
+
15
+ ## 安装与重启
16
+
17
+ 在 Poi 插件管理器中安装 npm 包:
18
+
19
+ ```none
20
+ poi-plugin-mcp
21
+ ```
22
+
23
+ 也可以完全退出 Poi 后,在 Poi 插件目录中手动安装:
24
+
25
+ ```powershell
26
+ cd "$env:APPDATA\poi\plugins"
27
+ npm install poi-plugin-mcp
28
+ ```
29
+
30
+ 安装或更新后完整退出并重新启动 Poi。在插件设置中确认“`MCP 数据桥`”已启用,
31
+ 然后进入游戏;需要读取账号数据的端点要等 Poi store 初始化完成。
32
+
33
+ 如果 MCP 客户端要使用 stdio 方式,可另外全局安装同一个 npm 包,使
34
+ `poi-mcp` 命令进入 `PATH`:
35
+
36
+ ```powershell
37
+ npm install -g poi-plugin-mcp
38
+ Get-Command poi-mcp
39
+ ```
40
+
41
+ ## 健康检查与端口发现
42
+
43
+ 默认端口的检查命令:
44
+
45
+ ```powershell
46
+ Invoke-RestMethod http://127.0.0.1:17777/health
47
+ ```
48
+
49
+ 正常响应:
50
+
51
+ ```json
52
+ {"status":"ok"}
53
+ ```
54
+
55
+ 插件启动后会把实际端口写入:
56
+
57
+ ```none
58
+ %USERPROFILE%\.poi-mcp\port
59
+ ```
60
+
61
+ 修改过端口时,应以该文件为准:
62
+
63
+ ```powershell
64
+ $port = (Get-Content "$HOME\.poi-mcp\port" -Raw).Trim()
65
+ Invoke-RestMethod "http://127.0.0.1:$port/health"
66
+ ```
67
+
68
+ 根路径 `/` 返回 404 不代表服务故障,请使用 `/health`。
69
+
70
+ ## 本地操作示教记录
71
+
72
+ 插件设置中的 `Record play` 默认关闭。打开后进入自动待命:插件只挂接游戏
73
+ WebView 的鼠标观察器,不会立即创建空会话。首次游戏鼠标活动会自动开始录制;
74
+ 连续 5 分钟没有游戏 WebView 鼠标事件时自动结束当前会话,之后的下一次鼠标活动
75
+ 会再开一个会话。关闭开关会立即解除待命并结束正在录制的会话。
76
+
77
+ 观察器不会阻止或回放输入,也不会扫描或包裹游戏运行时对象。Windows 默认写入
78
+ D 盘,不经过用户目录或 OneDrive:
79
+
80
+ ```none
81
+ D:\poi-mcp\recordings
82
+ ```
83
+
84
+ 记录规则:
85
+
86
+ - 没有收到移动事件且位移小于 6 个 WebView 像素:记为 `click`,即使截图导致鼠标
87
+ 抬起事件延迟也不会误记为长按;保存按下时截图并保留原始 `durationMs`。
88
+ - 位移达到 6 像素:记为 `drag`,保存开始、结束截图与按住期间的有界轨迹。
89
+ - 收到移动事件、位移不足 6 像素且按住超过 200 ms:记为 `hold`,保存开始、结束
90
+ 截图。
91
+ - 不记录未按键时的鼠标移动,因此普通悬停不会产生记录。
92
+ - 每次松开后约 0、250、1000 ms 保存装备成员 ID、舰娘槽位顺序和舰队顺序;装备
93
+ 成员 ID 不能代表 UI 顺序。
94
+ - 同步探测 `listMode`、`slotItemFilter`、`slotItemFilterDetail`、`slotItemPage`;
95
+ 会话开始和结束时另存 Poi 当前装备 master 的 `api_type`、`api_sortno` 与名称,
96
+ 供离线重建装备候选顺序。
97
+ - 默认不会扫描游戏对象图、调用未知 getter 或包裹游戏排序函数。只有在确认一个
98
+ 可直接访问且可恢复的运行时目标后,才应另行启用精确 sorter 观测。
99
+ - 同步保存 Poi 的 `game.response` 路径、请求参数和响应 JSON;数组保持原始顺序。
100
+ - 只对 `*.kancolle-server.com` 游戏 frame 的 `localStorage`、`sessionStorage`
101
+ 保存键和变化哈希,排除 DMM 外层页面、广告和追踪 frame。只有四个装备 UI 状态
102
+ 字段会保存明文;普通值使用每个会话独立的 HMAC,敏感键或含敏感字段的 JSON
103
+ 既不保存值,也不保存哈希。
104
+
105
+ 装备顺序需要把不同证据分开看:
106
+
107
+ - `game.response` 中的数组只代表该响应的原始顺序。
108
+ - `equipmentMembershipIds` Poi `info.equips` 的对象键只代表当前装备成员关系。
109
+ - 游戏客户端会自行过滤、排序并分页;当前静态参考显示普通装备顺序主要由
110
+ `equipTypeSp master ID → instance ID` 决定,改修筛选还会加入星级升/降序。
111
+ - KCV 的 `AssistantEquipRules` 会重建自己的候选顺序,不能当作游戏 UI 顺序。
112
+
113
+ 因此一次装备示教应至少包含:打开装备页、改变筛选/排序、翻页、选中装备,并保留
114
+ 相邻截图、筛选/分页状态、Poi 响应和开始/结束 master 参考。之后可以离线对齐这些
115
+ 证据,而不需要在游玩时对游戏 renderer 做全局探测。
116
+
117
+ 会话主要包含:
118
+
119
+ ```none
120
+ manifest.json
121
+ events.jsonl
122
+ frames/
123
+ responses/
124
+ states/
125
+ storage/
126
+ checkpoints/
127
+ ```
128
+
129
+ 包含 `token`、`cookie`、`authorization`、`secret`、`password` 等名称的字段、header
130
+ 或存储键会写为 `[REDACTED]`;`api_sort_key`、`sort_key`、`sortKey` 和
131
+ `shipSortKeyType` 是游戏业务排序字段,会保留原值。URL 查询串会移除,扁平
132
+ `rawHeaders` 也会按键值对脱敏。录制器不会读取 Cookie、CacheStorage 或 IndexedDB。
133
+ `states/session-start.json` 与 `session-stop.json` 只包含选定的 Poi `info` 和
134
+ `sortie` 状态及装备排序所需的精简 master 参考。
135
+
136
+ 单个会话默认最多写入 8 GiB、20,000 条时间线事件、运行 4 小时,单张截图最多
137
+ 16 MiB;4 小时使用真实截止定时器。达到任一限制后会结束采集、解绑会话监听、写完
138
+ 最终 manifest,并在插件设置中显示原因。录制目录默认还限制为最多 200 个会话、
139
+ 合计 64 GiB;达到总量时拒绝开始新会话,不会自动删除旧录制。自动待命观察器本身
140
+ 不写截图或空会话,只有真实游戏鼠标活动才会占用录制空间。
141
+
142
+ ## MCP 接入
143
+
144
+ 插件提供两种 MCP 连接方式:
145
+
146
+ - **HTTP MCP**:客户端直接连接 Poi 内的 `POST /mcp`。
147
+ - **stdio MCP**:客户端启动 npm 安装的 `poi-mcp` 命令;该命令读取端口文件,
148
+ 再访问 Poi 的本地数据 API。
149
+
150
+ 两种方式都要求 Poi 正在运行且插件已加载。
151
+
152
+ ### HTTP MCP
153
+
154
+ MCP URL:
155
+
156
+ ```none
157
+ http://127.0.0.1:17777/mcp
158
+ ```
159
+
160
+ 这是 JSON-RPC over HTTP 端点,请由 MCP 客户端向它发送 `POST` 请求。Codex
161
+ 配置示例:
162
+
163
+ ```toml
164
+ [mcp_servers.poi]
165
+ url = "http://127.0.0.1:17777/mcp"
166
+ ```
167
+
168
+ 使用 JSON 配置的 MCP 客户端可采用:
169
+
170
+ ```json
171
+ {
172
+ "mcpServers": {
173
+ "poi": {
174
+ "url": "http://127.0.0.1:17777/mcp"
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ 若修改过端口,请把 URL 中的 `17777` 替换为端口文件中的值。
181
+
182
+ ### stdio CLI
183
+
184
+ `poi-mcp` MCP stdio 服务命令,不是交互式终端程序。MCP 客户端应负责启动
185
+ 并通过标准输入输出与它通信。
186
+
187
+ Codex 配置示例:
188
+
189
+ ```toml
190
+ [mcp_servers.poi]
191
+ command = "poi-mcp"
192
+ args = []
193
+ ```
194
+
195
+ 通用 JSON 配置示例:
196
+
197
+ ```json
198
+ {
199
+ "mcpServers": {
200
+ "poi": {
201
+ "command": "poi-mcp",
202
+ "args": []
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ 如果桌面客户端找不到 npm 的全局命令,请先运行 `Get-Command poi-mcp`,再把
209
+ 配置中的 `command` 改为返回的完整可执行文件路径。
210
+
211
+ ## MCP 工具
212
+
213
+ | 工具 | 用途 |
214
+ |---|---|
215
+ | `get_fleet_status` | 按 `fleetId`(1-4)读取一支舰队:舰名、装备、补强、速度、士气、33式索敌(Cn 1–4)、制空。看一队时不要用 `get_all` |
216
+ | `search_ships` | 按可选的 `minLevel`、`maxLevel`、`minMorale` 筛选持有舰娘 |
217
+ | `search_equipment` | 按可选的 `minLevel` 筛选持有装备 |
218
+ | `get_resources` | 读取具名资源。Poi 数组下标 4=高速建造材(喷火)、5=高速修复材(桶)。HTTP `/resources` 仍是 8 元数组 |
219
+ | `get_all` | 整包账号转储。优先 `get_fleet_status` / `search_*` / `get_resources`;`include` 可选 `master`、`event`、`planner` |
220
+
221
+ ## MCP 资源
222
+
223
+ 可通过 `resources/list` 和 `resources/read` 读取:
224
+
225
+ ```none
226
+ poi://basic
227
+ poi://fleets
228
+ poi://ships
229
+ poi://equipment
230
+ poi://resources
231
+ poi://quests
232
+ poi://airbase
233
+ poi://names
234
+ poi://master
235
+ poi://event
236
+ poi://planner
237
+ poi://all
238
+ ```
239
+
240
+ ## 数据端点
241
+
242
+ 主要的本地 HTTP 数据查询端点:
243
+
244
+ | 端点 | 内容 |
245
+ |---|---|
246
+ | `GET /health` | bridge 健康状态 |
247
+ | `GET /basic` | 提督基础信息 |
248
+ | `GET /fleets` | 舰队数据 |
249
+ | `GET /ships` | 持有舰娘实例 |
250
+ | `GET /equipment` | 持有装备实例 |
251
+ | `GET /resources` | 当前资源 |
252
+ | `GET /quests` | 当前任务与任务记录 |
253
+ | `GET /airbase` | 基地航空队 |
254
+ | `GET /names` | 舰娘、装备和远征名称映射 |
255
+ | `GET /master` | 舰娘、装备、类型、远征等主数据 |
256
+ | `GET /event` | 活动标签定义与持有舰娘出击标签 |
257
+ | `GET /planner` | Ship Info 配装规划数据 |
258
+ | `GET /battle` | 已观测战斗数据、结算与简化战斗状态 |
259
+ | `GET /action-events` | 已捕获的成功游戏 API 动作事件(支持 `after` / `limit` 游标) |
260
+ | `GET /action-events/wait` | 有新动作时立即返回,否则在 `timeoutMs`(1-60000)内事件驱动等待;超时返回 `timedOut: true` 心跳 |
261
+ | `GET /all` | 基础运行数据汇总 |
262
+ | `POST /query` | 带 Bearer token 的通用只读查询:Poi store、任意已捕获 kcsapi、Poi JSON 缓存、游戏 frame/storage/属性路径 |
263
+ | `GET /debug/status` | 带 Bearer token 读取危险 WebView eval 开关状态 |
264
+ | `POST /debug/evaluate` | 仅在设置中显式开启后运行受大小限制的 WebView JavaScript 调试代码 |
265
+ | `POST /mcp` | HTTP MCP JSON-RPC 入口 |
266
+
267
+ `/query`、`/debug/status` 和 `/debug/evaluate` 不开放 CORS,响应禁止缓存,并要求
268
+ `%USERPROFILE%\.poi-mcp\input-token` 中的 Bearer token。`/query` 是正式数据面:
269
+ 调用方提交 `source + path + filter`,Bridge 保持通用取数、限量和脱敏,新增缓存字段
270
+ 通常不需要修改插件。
271
+
272
+ 插件设置中的 `Debug eval` 默认关闭。打开后,认证调用方可以在经过
273
+ `*.kancolle-server.com` origin 校验且由 `processId:routingId` 明确指定的游戏 frame
274
+ 内执行任意 JavaScript。它能够读取令牌、调用游戏接口、修改页面,也可能因同步死循环
275
+ 卡住游戏 renderer;异步超时不能撤销已经发生的副作用。因此只用于发现未知运行时
276
+ 路径,调试结束后应关闭,正式自动化必须改用 `/query` 或固定快照端点。审计日志只保存
277
+ 脚本 SHA-256、frame、时间和成功/失败,不保存脚本文本。
278
+
279
+ ## 开发验证
280
+
281
+ 在源码目录执行:
282
+
283
+ ```powershell
284
+ npm install
285
+ npm test
286
+ npm pack --dry-run --ignore-scripts
287
+ ```
288
+
289
+ `npm test` 应运行 `node --test test/*.test.js`;打包预览应包含
290
+ `index.js`、`lib`、`mcp-server.js` 和本 README。
package/index.js CHANGED
@@ -7,9 +7,12 @@ const controller = createBridgeController({
7
7
  getQuestList: telemetry.getQuestList,
8
8
  getQuestAction: telemetry.getQuestAction,
9
9
  getEquipmentAction: telemetry.getEquipmentAction,
10
+ getEquipmentSelection: telemetry.getEquipmentSelection,
10
11
  getUnsetSlot: telemetry.getUnsetSlot,
11
12
  getFleetAction: telemetry.getFleetAction,
12
13
  getActionEvents: telemetry.getActionEvents,
14
+ getActionEventsWait: telemetry.getActionEventsWait,
15
+ getApiResponses: telemetry.getApiResponses,
13
16
  getBattleTelemetry: telemetry.getBattleTelemetry,
14
17
  })
15
18
  const settingsClass = createSettingsClass(controller)
@@ -14,6 +14,7 @@ function createBridgeController(options = {}) {
14
14
 
15
15
  let settings = loadSettings(settingsPath)
16
16
  let bridge = null
17
+ let recorder = null
17
18
  let inputToken = options.inputToken || null
18
19
  let pending = Promise.resolve()
19
20
 
@@ -33,11 +34,15 @@ function createBridgeController(options = {}) {
33
34
  getQuestList: options.getQuestList,
34
35
  getQuestAction: options.getQuestAction,
35
36
  getEquipmentAction: options.getEquipmentAction,
37
+ getEquipmentSelection: options.getEquipmentSelection,
36
38
  getUnsetSlot: options.getUnsetSlot,
37
39
  getFleetAction: options.getFleetAction,
38
40
  getActionEvents: options.getActionEvents,
41
+ getActionEventsWait: options.getActionEventsWait,
42
+ getApiResponses: options.getApiResponses,
39
43
  getBattleTelemetry: options.getBattleTelemetry,
40
44
  inputEnabled: settings.inputEnabled,
45
+ debugEvalEnabled: settings.debugEvalEnabled,
41
46
  inputToken,
42
47
  port: settings.port,
43
48
  portFile: options.portFile,
@@ -45,6 +50,29 @@ function createBridgeController(options = {}) {
45
50
  })
46
51
  }
47
52
 
53
+ function createCurrentRecorder() {
54
+ if (options.createRecorder) {
55
+ return options.createRecorder({
56
+ getStore: options.getStore,
57
+ captureScreenshot: options.captureScreenshot,
58
+ logger,
59
+ })
60
+ }
61
+ const {
62
+ createPoiAutoInteractionRecorder,
63
+ } = require('./poi-auto-interaction-recorder')
64
+ const { createPoiInteractionRecorder } = require('./poi-interaction-recorder')
65
+ return createPoiAutoInteractionRecorder({
66
+ getStore: options.getStore,
67
+ logger,
68
+ createSessionRecorder: () => createPoiInteractionRecorder({
69
+ getStore: options.getStore,
70
+ captureScreenshot: options.captureScreenshot,
71
+ logger,
72
+ }),
73
+ })
74
+ }
75
+
48
76
  async function ensureStarted() {
49
77
  if (!bridge) {
50
78
  bridge = createCurrentBridge()
@@ -52,6 +80,16 @@ function createBridgeController(options = {}) {
52
80
  await bridge.start()
53
81
  }
54
82
 
83
+ async function ensureRecorderStarted() {
84
+ if (!recorder) recorder = createCurrentRecorder()
85
+ await recorder.start()
86
+ }
87
+
88
+ async function stopCurrentRecorder() {
89
+ if (!recorder) return
90
+ await recorder.stop()
91
+ }
92
+
55
93
  async function stopCurrentBridge() {
56
94
  if (!bridge) return
57
95
 
@@ -70,11 +108,13 @@ function createBridgeController(options = {}) {
70
108
  return enqueue(async () => {
71
109
  settings = loadSettings(settingsPath)
72
110
  if (settings.enabled) await ensureStarted()
111
+ if (settings.recordingEnabled) await ensureRecorderStarted()
73
112
  })
74
113
  },
75
114
 
76
115
  unload() {
77
116
  return enqueue(async () => {
117
+ await stopCurrentRecorder()
78
118
  await stopCurrentBridge()
79
119
  })
80
120
  },
@@ -99,15 +139,31 @@ function createBridgeController(options = {}) {
99
139
  const portChanged = normalized.port !== settings.port
100
140
  const enabledChanged = normalized.enabled !== settings.enabled
101
141
  const inputEnabledChanged = normalized.inputEnabled !== settings.inputEnabled
142
+ const debugEvalEnabledChanged =
143
+ normalized.debugEvalEnabled !== settings.debugEvalEnabled
144
+ const recordingEnabledChanged =
145
+ normalized.recordingEnabled !== settings.recordingEnabled
102
146
 
103
147
  persist(normalized)
104
148
 
149
+ if (settings.recordingEnabled) {
150
+ await ensureRecorderStarted()
151
+ } else if (recordingEnabledChanged) {
152
+ await stopCurrentRecorder()
153
+ }
154
+
105
155
  if (!settings.enabled) {
106
156
  await stopCurrentBridge()
107
157
  return
108
158
  }
109
159
 
110
- if (portChanged || enabledChanged || inputEnabledChanged || !bridge) {
160
+ if (
161
+ portChanged ||
162
+ enabledChanged ||
163
+ inputEnabledChanged ||
164
+ debugEvalEnabledChanged ||
165
+ !bridge
166
+ ) {
111
167
  await stopCurrentBridge()
112
168
  await ensureStarted()
113
169
  }
@@ -120,12 +176,30 @@ function createBridgeController(options = {}) {
120
176
 
121
177
  getStatus() {
122
178
  const actualPort = bridge ? bridge.getPort() : 0
179
+ const recorderStatus = recorder && typeof recorder.getStatus === 'function'
180
+ ? recorder.getStatus()
181
+ : {}
123
182
  return {
124
183
  enabled: settings.enabled,
125
184
  running: actualPort > 0,
126
185
  port: settings.port,
127
186
  actualPort,
128
187
  inputEnabled: settings.inputEnabled,
188
+ debugEvalEnabled: settings.debugEvalEnabled,
189
+ recordingEnabled: settings.recordingEnabled,
190
+ recordingArmed: recorderStatus.armed === true,
191
+ recordingStarting: recorderStatus.starting === true,
192
+ recording: recorderStatus.running === true,
193
+ recordingAttached: recorderStatus.attached === true,
194
+ recordingSessionId: recorderStatus.sessionId || null,
195
+ recordingSessionDir: recorderStatus.sessionDir || null,
196
+ recordingSessionBytes: Number.isFinite(recorderStatus.sessionBytes)
197
+ ? recorderStatus.sessionBytes
198
+ : 0,
199
+ recordingLimitReached: recorderStatus.limitReached || null,
200
+ recordingIdleSessionMs: Number.isFinite(recorderStatus.idleSessionMs)
201
+ ? recorderStatus.idleSessionMs
202
+ : 0,
129
203
  }
130
204
  },
131
205
  }