cli-wechat-bridge 1.1.3 → 1.1.4
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 +98 -135
- package/bin/_run-entry.mjs +12 -0
- package/bin/wechat-claude-start.mjs +7 -2
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex-start.mjs +7 -2
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode-start.mjs +7 -2
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi-start.mjs +7 -2
- package/bin/wechat-pi.mjs +1 -1
- package/dist/bridge/bridge-adapters.claude.js +21 -1
- package/dist/bridge/bridge-adapters.codex.js +27 -4
- package/dist/bridge/bridge-adapters.core.js +76 -4
- package/dist/bridge/bridge-adapters.js +0 -3
- package/dist/bridge/bridge-adapters.opencode.js +130 -21
- package/dist/bridge/bridge-adapters.pi.js +12 -2
- package/dist/bridge/bridge-adapters.shared.js +13 -100
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-state.js +41 -6
- package/dist/bridge/bridge-utils.js +37 -224
- package/dist/bridge/wechat-bridge.js +75 -130
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +20 -6
- package/dist/companion/local-companion-link.js +7 -2
- package/dist/companion/local-companion-start.js +45 -13
- package/dist/companion/local-companion.js +6 -3
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +148 -54
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +2 -3
- package/dist/i18n/messages-zh.js +2 -3
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +23 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -11
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +2 -9
- package/bin/wechat-bridge-claude.mjs +0 -5
- package/bin/wechat-bridge-codex.mjs +0 -5
- package/bin/wechat-bridge-opencode.mjs +0 -5
- package/bin/wechat-bridge-pi.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
npm install -g cli-wechat-bridge@latest
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
安装后,可以在任意项目目录中直接运行 `wechat-codex`、`wechat-claude`、`wechat-opencode`、`wechat-pi`,或使用 `wechat-daemon` 保持长期连接。
|
|
66
66
|
|
|
67
67
|
兼容性说明:旧包名 `@unlinearity/cli-wechat-bridge` 会继续同步发布,已经安装旧包名的用户可以正常升级;新用户优先使用更短的 `cli-wechat-bridge`。
|
|
68
68
|
|
|
@@ -114,7 +114,7 @@ wechat-setup
|
|
|
114
114
|
|
|
115
115
|
登录成功后,程序会清理旧的同步游标和上下文 token,避免旧会话状态污染新的登录状态。数据目录、状态文件和旧版本迁移说明见 [问题排查](docs/troubleshooting.md#数据目录与状态文件)。
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
首次安装或微信登录过期时,四个直接启动命令也会在前台提示扫码登录。
|
|
118
118
|
|
|
119
119
|
### 4. 先从微信发一条同步消息(重要)
|
|
120
120
|
|
|
@@ -122,7 +122,7 @@ wechat-setup
|
|
|
122
122
|
|
|
123
123
|
如果冷启动或长时间闲置后直接从本地终端先发消息,bridge 通常仍会捕获这条本地输入并交给 Codex / Claude Code / OpenCode / Pi 处理,但回发到微信时可能因为旧的 `context_token` 失效而失败。表现是:本地已经有回复,微信暂时收不到;等你先从微信发来一条消息后,后续双向同步就能恢复正常。
|
|
124
124
|
|
|
125
|
-
### 5.
|
|
125
|
+
### 5. 直接启动本地 CLI
|
|
126
126
|
|
|
127
127
|
先进入需要操作的项目目录:
|
|
128
128
|
|
|
@@ -134,28 +134,30 @@ cd D:\work\your-project
|
|
|
134
134
|
|
|
135
135
|
| 使用的本地 CLI | 启动命令 |
|
|
136
136
|
| --- | --- |
|
|
137
|
-
| Codex | `wechat-codex
|
|
138
|
-
| Claude Code | `wechat-claude
|
|
139
|
-
| OpenCode | `wechat-opencode
|
|
140
|
-
| Pi | `wechat-pi
|
|
137
|
+
| Codex | `wechat-codex` |
|
|
138
|
+
| Claude Code | `wechat-claude` |
|
|
139
|
+
| OpenCode | `wechat-opencode` |
|
|
140
|
+
| Pi | `wechat-pi` |
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
这些命令会自动完成以下动作:
|
|
143
143
|
|
|
144
144
|
1. 校验或刷新微信登录凭据;
|
|
145
|
-
2. 如果当前目录已有 `wechat-daemon`,则委托 daemon 切换到对应 CLI;
|
|
146
|
-
3. 如果没有 daemon
|
|
147
|
-
4.
|
|
148
|
-
5. 等待当前目录对应的本地
|
|
149
|
-
6. 打开可见的本地 CLI 会话。
|
|
150
|
-
|
|
151
|
-
没有 daemon
|
|
145
|
+
2. 如果当前目录已有 `wechat-daemon`,则委托 daemon 切换到对应 CLI;
|
|
146
|
+
3. 如果没有 daemon,则创建或复用内部 transient bridge runtime;
|
|
147
|
+
4. 如果旧 runtime 正在服务其他目录,则安全停止旧 runtime 并切换到当前目录;
|
|
148
|
+
5. 等待当前目录对应的本地 endpoint 就绪;
|
|
149
|
+
6. 打开可见的本地 CLI 会话。
|
|
150
|
+
|
|
151
|
+
没有 daemon 时,四个直接命令按**单活工作区切换器**工作:
|
|
152
152
|
|
|
153
153
|
- 同一时间只有一个项目与微信对话;
|
|
154
154
|
- Codex 在当前目录重复执行时会复用已有会话;Claude Code、OpenCode 和 Pi 启动器默认新建会话;
|
|
155
|
-
- Pi 如需显式恢复上一次会话,可使用 `wechat-pi
|
|
155
|
+
- Pi 如需显式恢复上一次会话,可使用 `wechat-pi --session-start-mode restore`;
|
|
156
156
|
- 如果当前目录已经有可见 companion / panel 在运行,则不会重复打开第二个窗口;
|
|
157
157
|
- 如果检测到可见端仍在运行但 worker 状态异常(如 `stopped` / `error`),会自动重启 bridge 再重新打开可见端;
|
|
158
|
-
- 在其他目录执行会显式切换活动工作区。
|
|
158
|
+
- 在其他目录执行会显式切换活动工作区。
|
|
159
|
+
|
|
160
|
+
`wechat-codex-start`、`wechat-claude-start`、`wechat-opencode-start`、`wechat-pi-start` 暂时保留为过渡别名,功能与直接命令一致,但会输出弃用提示,并将在下一版本移除。
|
|
159
161
|
|
|
160
162
|
### 6. 常驻 daemon 模式(支持多 CLI 切换)
|
|
161
163
|
|
|
@@ -197,20 +199,9 @@ wechat-daemon --adapter codex
|
|
|
197
199
|
wechat-daemon --adapter claude --profile work
|
|
198
200
|
```
|
|
199
201
|
|
|
200
|
-
当同一工作目录已有 `wechat-daemon`
|
|
202
|
+
当同一工作目录已有 `wechat-daemon` 在运行时,四个直接启动命令会自动委托给 daemon:请求 daemon 切到对应 CLI,并在需要时打开可见终端,不会停止 daemon 或关闭其他 CLI。
|
|
201
203
|
|
|
202
|
-
其中 `wechat-pi
|
|
203
|
-
|
|
204
|
-
### 7. 手动双终端模式(高级调试)
|
|
205
|
-
|
|
206
|
-
如果你希望明确观察 bridge 与本地 CLI companion,也可以用两个终端分别启动。
|
|
207
|
-
|
|
208
|
-
| 适配器 | 终端 A:bridge | 终端 B:本地 companion |
|
|
209
|
-
| --- | --- | --- |
|
|
210
|
-
| Codex | `wechat-bridge-codex` | `wechat-codex` |
|
|
211
|
-
| Claude Code | `wechat-bridge-claude` | `wechat-claude` |
|
|
212
|
-
| OpenCode | `wechat-bridge-opencode` | `wechat-opencode` |
|
|
213
|
-
| Pi | `wechat-bridge-pi` | `wechat-pi` |
|
|
204
|
+
其中 `wechat-pi` 默认表示启动一个新的 Pi session;如果 Pi TUI 已经可见,daemon 会在现有窗口中创建新 session。微信中的 `/pi` 仅用于切换适配器,仍会复用已经连接的 Pi TUI。
|
|
214
205
|
|
|
215
206
|
## 适配器支持情况
|
|
216
207
|
|
|
@@ -224,12 +215,12 @@ wechat-daemon --adapter claude --profile work
|
|
|
224
215
|
- 当前不会自动 OCR 图片,也不会自动抽取 PDF / DOCX 正文;如需解析,由本地 CLI 根据路径完成。
|
|
225
216
|
- 具体保存位置见 [问题排查](docs/troubleshooting.md#数据目录与状态文件)。
|
|
226
217
|
|
|
227
|
-
| 适配器 | 当前状态 | 说明 |
|
|
228
|
-
| --- | --- | --- |
|
|
229
|
-
| `codex` | 已接入 |
|
|
230
|
-
| `claude` | 已接入 | `wechat-
|
|
231
|
-
| `opencode` | 已接入 |
|
|
232
|
-
| `pi` | 已接入 | `wechat-
|
|
218
|
+
| 适配器 | 当前状态 | 说明 |
|
|
219
|
+
| --- | --- | --- |
|
|
220
|
+
| `codex` | 已接入 | `wechat-codex` 自动确保内部 runtime 并打开可见 Codex;微信跟随本地 thread |
|
|
221
|
+
| `claude` | 已接入 | `wechat-claude` 自动启动或复用 Claude Code;会话、最终回复与审批按 Claude session 语义同步 |
|
|
222
|
+
| `opencode` | 已接入 | `wechat-opencode` 自动启动或复用 OpenCode;支持本地 session 跟随及微信 `/new` / `/new-session` |
|
|
223
|
+
| `pi` | 已接入 | `wechat-pi` 启动用户原生 Pi TUI,并通过本地 extension 让微信接管同一 session,支持最终回复、停止、新建和恢复 session |
|
|
233
224
|
|
|
234
225
|
Pi 按全权限本地代理运行:bridge 不增加工具审批层,并传入 `--approve` 信任当前项目;读写文件和执行命令均使用启动 `wechat-pi` 的本地用户权限。原生 TUI 的主题、快捷键、模型选择和 extension UI 都会保留。`wechat-pi` 本身就是被微信接管的 Pi TUI;不要再启动第二个 Pi 进程同时写入同一个 session 文件。
|
|
235
226
|
|
|
@@ -249,97 +240,69 @@ Pi 按全权限本地代理运行:bridge 不增加工具审批层,并传入
|
|
|
249
240
|
|
|
250
241
|
OpenCode 模式下,微信侧支持 `/new` 或 `/new-session` 创建新 session;如果在本地 OpenCode CLI 中创建新 session,微信消息也会跟随新的 session。
|
|
251
242
|
|
|
252
|
-
## 命令说明
|
|
253
|
-
|
|
254
|
-
### 常用全局命令
|
|
255
|
-
|
|
256
|
-
| 类型 | 命令 |
|
|
257
|
-
| --- | --- |
|
|
258
|
-
| 登录与更新 | `wechat-setup`、`wechat-check-update` |
|
|
259
|
-
| 常驻 daemon | `wechat-daemon` |
|
|
260
|
-
| Codex | `wechat-
|
|
261
|
-
| Claude Code | `wechat-
|
|
262
|
-
| OpenCode | `wechat-
|
|
263
|
-
| Pi | `wechat-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
wechat-daemon --
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
- `--cwd <path>`:指定 daemon 绑定的工作目录;
|
|
243
|
+
## 命令说明
|
|
244
|
+
|
|
245
|
+
### 常用全局命令
|
|
246
|
+
|
|
247
|
+
| 类型 | 命令 |
|
|
248
|
+
| --- | --- |
|
|
249
|
+
| 登录与更新 | `wechat-setup`、`wechat-check-update` |
|
|
250
|
+
| 常驻 daemon | `wechat-daemon` |
|
|
251
|
+
| Codex | `wechat-codex` |
|
|
252
|
+
| Claude Code | `wechat-claude` |
|
|
253
|
+
| OpenCode | `wechat-opencode` |
|
|
254
|
+
| Pi | `wechat-pi` |
|
|
255
|
+
|
|
256
|
+
`wechat-*-start` 暂时作为弃用别名保留一个版本。公开的 `wechat-bridge*` 命令和 Shell adapter 已移除;内部 bridge runtime 仍由直接命令和 daemon 自动管理。
|
|
257
|
+
|
|
258
|
+
### Daemon CLI 参数
|
|
259
|
+
|
|
260
|
+
示例:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
wechat-daemon --cwd D:\work\my-project
|
|
264
|
+
wechat-daemon --adapter codex
|
|
265
|
+
wechat-daemon --adapter claude --profile work
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
支持参数:
|
|
269
|
+
|
|
270
|
+
- `--cwd <path>`:指定 daemon 绑定的工作目录;
|
|
282
271
|
- `--adapter <codex|claude|opencode|pi>`:启动 daemon 后立即切换到指定 CLI;
|
|
283
|
-
- `--profile <name-or-path>`:传给 daemon 创建的对应适配器;
|
|
284
|
-
- `--no-open`:只创建
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
```bash
|
|
319
|
-
wechat-codex-start --cwd D:\work\my-project
|
|
320
|
-
wechat-claude-start --profile work
|
|
321
|
-
wechat-opencode-start --cwd D:\work\my-project
|
|
322
|
-
wechat-pi-start --cwd D:\work\my-project --model openai/gpt-5.6-sol
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
支持参数:
|
|
326
|
-
|
|
327
|
-
- `--cwd <path>`:显式指定 bridge / companion 对应的工作目录;
|
|
328
|
-
- `--profile <name-or-path>`:转发给后台启动的 `wechat-bridge-codex` / `wechat-bridge-claude` / `wechat-bridge-opencode` / `wechat-bridge-pi`;
|
|
329
|
-
- `--timeout-ms <ms>`:等待当前目录 endpoint 的最长时间,默认 `15000`。
|
|
330
|
-
- `--session-start-mode <restore|new>`:显式选择恢复或新建会话;Pi 启动器默认 `new`。
|
|
331
|
-
|
|
332
|
-
高级用法:除上述启动器参数外,未知参数会继续透传给可见的底层 CLI。这样既能保留微信登录、工作区切换与 bridge 生命周期管理,也能启用 Codex / Claude Code / OpenCode / Pi 自己的高级启动模式。
|
|
333
|
-
|
|
334
|
-
```bash
|
|
335
|
-
wechat-codex-start --yolo
|
|
336
|
-
wechat-codex-start --model gpt-5.2 --yolo
|
|
337
|
-
wechat-claude-start --dangerously-skip-permissions
|
|
338
|
-
wechat-claude-start --model sonnet --dangerously-skip-permissions
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
其中,`--yolo` 会传给 Codex;`--dangerously-skip-permissions` 会传给 Claude Code。它们只影响本地可见 CLI,不会覆盖 bridge 托管的 Codex remote 连接参数、Claude settings、微信登录状态或当前工作区锁。由于这两类参数会降低审批保护,建议只在可信工作区中使用。
|
|
342
|
-
|
|
272
|
+
- `--profile <name-or-path>`:传给 daemon 创建的对应适配器;
|
|
273
|
+
- `--no-open`:只创建 runtime slot,不自动打开可见 CLI;
|
|
274
|
+
- `--doctor`:检查环境、CLI、锁、endpoint 和 daemon 状态。
|
|
275
|
+
|
|
276
|
+
### 直接启动命令参数
|
|
277
|
+
|
|
278
|
+
适用于 `wechat-codex`、`wechat-claude`、`wechat-opencode`、`wechat-pi`。
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
wechat-codex --cwd D:\work\my-project
|
|
282
|
+
wechat-claude --profile work
|
|
283
|
+
wechat-opencode --cwd D:\work\my-project
|
|
284
|
+
wechat-pi --cwd D:\work\my-project --model openai/gpt-5.6-sol
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
支持参数:
|
|
288
|
+
|
|
289
|
+
- `--cwd <path>`:指定 runtime 与可见 CLI 对应的工作目录;
|
|
290
|
+
- `--profile <name-or-path>`:传给内部 runtime;
|
|
291
|
+
- `--timeout-ms <ms>`:等待当前目录 endpoint 的最长时间,默认 `15000`;
|
|
292
|
+
- `--session-start-mode <restore|new>`:显式选择恢复或新建会话;
|
|
293
|
+
- 其他未知参数会继续透传给可见的底层 CLI。
|
|
294
|
+
|
|
295
|
+
默认会话策略:Codex 恢复当前会话;Claude Code、OpenCode 和 Pi 新建会话。
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
wechat-codex --yolo
|
|
299
|
+
wechat-codex --model gpt-5.2 --yolo
|
|
300
|
+
wechat-claude --dangerously-skip-permissions
|
|
301
|
+
wechat-claude --model sonnet --dangerously-skip-permissions
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
其中,`--yolo` 会传给 Codex;`--dangerously-skip-permissions` 会传给 Claude Code。它们只影响本地可见 CLI,不会覆盖内部 runtime 的连接参数、微信登录状态或工作区锁。由于这两类参数会降低审批保护,建议只在可信工作区中使用。
|
|
305
|
+
|
|
343
306
|
## 微信侧支持的指令
|
|
344
307
|
|
|
345
308
|
| 指令 | 说明 |
|
|
@@ -392,18 +355,18 @@ Daemon 模式支持将微信表情映射为命令,在微信中发送表情即
|
|
|
392
355
|
|
|
393
356
|
## 工作区模型
|
|
394
357
|
|
|
395
|
-
本项目采用“当前目录即当前工作区”的模型:
|
|
396
|
-
|
|
397
|
-
- 从哪个目录启动 `wechat-
|
|
398
|
-
-
|
|
399
|
-
- 不同工作区的运行状态相互隔离。
|
|
358
|
+
本项目采用“当前目录即当前工作区”的模型:
|
|
359
|
+
|
|
360
|
+
- 从哪个目录启动 `wechat-codex` / `wechat-claude` / `wechat-opencode` / `wechat-pi`,哪个目录就是当前工作区;
|
|
361
|
+
- 直接命令会自动管理同一工作区的内部 runtime 与可见 CLI;
|
|
362
|
+
- 不同工作区的运行状态相互隔离。
|
|
400
363
|
|
|
401
364
|
目前支持两种运行模型:
|
|
402
365
|
|
|
403
|
-
-
|
|
366
|
+
- 直接启动模式:单 owner、内部 transient runtime、单活动工作区;
|
|
404
367
|
- `wechat-daemon`:单 owner、单 daemon、绑定一个启动工作区,但可在这个工作区内同时保留 Codex / Claude Code / OpenCode / Pi 四个 CLI slot。
|
|
405
|
-
|
|
406
|
-
|
|
368
|
+
|
|
369
|
+
对四个直接启动命令来说,这意味着:
|
|
407
370
|
|
|
408
371
|
- 如果同一工作区已有 daemon,它们会委托 daemon 切换 CLI;
|
|
409
372
|
- 如果没有 daemon,它们会作为**单活工作区切换器**工作;
|
|
@@ -426,7 +389,7 @@ wechat-check-update
|
|
|
426
389
|
- 远程仓库的最新版本;
|
|
427
390
|
- 如果有更新,会提供详细的更新指引。
|
|
428
391
|
|
|
429
|
-
|
|
392
|
+
没有 daemon 时,四个直接命令启动的内部 runtime 会按现有缓存策略检查更新。自动检查在后台异步执行,不影响启动速度。
|
|
430
393
|
|
|
431
394
|
### 获取最新版本
|
|
432
395
|
|
package/bin/_run-entry.mjs
CHANGED
|
@@ -57,3 +57,15 @@ export function runJsEntry(relativeEntryPath, extraArgs = []) {
|
|
|
57
57
|
process.exit(code ?? 0);
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
export function runDeprecatedJsEntry(
|
|
62
|
+
commandName,
|
|
63
|
+
replacementCommand,
|
|
64
|
+
relativeEntryPath,
|
|
65
|
+
extraArgs = [],
|
|
66
|
+
) {
|
|
67
|
+
process.stderr.write(
|
|
68
|
+
`[cli-wechat-bridge] ${commandName} is deprecated; use ${replacementCommand}. This alias will be removed in the next release.\n`,
|
|
69
|
+
);
|
|
70
|
+
runJsEntry(relativeEntryPath, extraArgs);
|
|
71
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { runDeprecatedJsEntry } from "./_run-entry.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
runDeprecatedJsEntry(
|
|
6
|
+
"wechat-claude-start",
|
|
7
|
+
"wechat-claude",
|
|
8
|
+
"dist/companion/local-companion-start.js",
|
|
9
|
+
["--adapter", "claude"],
|
|
10
|
+
);
|
package/bin/wechat-claude.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { runDeprecatedJsEntry } from "./_run-entry.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
runDeprecatedJsEntry(
|
|
6
|
+
"wechat-codex-start",
|
|
7
|
+
"wechat-codex",
|
|
8
|
+
"dist/companion/local-companion-start.js",
|
|
9
|
+
["--adapter", "codex"],
|
|
10
|
+
);
|
package/bin/wechat-codex.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { runDeprecatedJsEntry } from "./_run-entry.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
runDeprecatedJsEntry(
|
|
6
|
+
"wechat-opencode-start",
|
|
7
|
+
"wechat-opencode",
|
|
8
|
+
"dist/companion/local-companion-start.js",
|
|
9
|
+
["--adapter", "opencode"],
|
|
10
|
+
);
|
package/bin/wechat-opencode.mjs
CHANGED
package/bin/wechat-pi-start.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { runDeprecatedJsEntry } from "./_run-entry.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
runDeprecatedJsEntry(
|
|
6
|
+
"wechat-pi-start",
|
|
7
|
+
"wechat-pi",
|
|
8
|
+
"dist/companion/local-companion-start.js",
|
|
9
|
+
["--adapter", "pi"],
|
|
10
|
+
);
|
package/bin/wechat-pi.mjs
CHANGED
|
@@ -208,6 +208,21 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
208
208
|
this.clearWechatWorkingNotice(true);
|
|
209
209
|
this.pendingCliApprovalHints = null;
|
|
210
210
|
this.flushPendingClaudeHookApprovals();
|
|
211
|
+
if (this.pendingApproval) {
|
|
212
|
+
// The flush above answered the pending hook approval with an empty
|
|
213
|
+
// response, so the mirrored request is dead. Drop it — otherwise a
|
|
214
|
+
// later /confirm targets a deleted requestId and every new message is
|
|
215
|
+
// rejected by a stale "approval pending" state. Mirrors what the
|
|
216
|
+
// socket-close path does via handleClosedClaudeHookApproval.
|
|
217
|
+
this.pendingApproval = null;
|
|
218
|
+
this.state.pendingApproval = null;
|
|
219
|
+
this.state.pendingApprovalOrigin = undefined;
|
|
220
|
+
}
|
|
221
|
+
if (this.state.status === "awaiting_approval") {
|
|
222
|
+
// Return to busy so the interrupt settle path (guarded on busy) can
|
|
223
|
+
// complete the turn normally.
|
|
224
|
+
this.setStatus("busy");
|
|
225
|
+
}
|
|
211
226
|
this.writeToPty("\u0003");
|
|
212
227
|
this.scheduleTaskComplete(shared.INTERRUPT_SETTLE_DELAY_MS);
|
|
213
228
|
return true;
|
|
@@ -480,7 +495,12 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
480
495
|
throw new Error("Claude hook server is not ready.");
|
|
481
496
|
}
|
|
482
497
|
const { workspaceDir } = ensureWorkspaceChannelDir(this.options.cwd);
|
|
483
|
-
|
|
498
|
+
// Scope the runtime directory to this adapter instance: two bridges in the
|
|
499
|
+
// same cwd would otherwise overwrite each other's hook script, settings,
|
|
500
|
+
// and error log, routing approvals and final replies to the wrong chat.
|
|
501
|
+
// The hook token is unique per hook server (per instance), and the same
|
|
502
|
+
// token regenerates the same directory across hook restarts.
|
|
503
|
+
const runtimeDir = path.join(workspaceDir, `claude-runtime-${this.hookToken.replace(/[^a-zA-Z0-9]/g, "").slice(0, 12)}`);
|
|
484
504
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
485
505
|
const hookScriptPath = path.join(runtimeDir, process.platform === "win32" ? "hook.cmd" : "hook.sh");
|
|
486
506
|
const settingsFilePath = path.join(runtimeDir, "settings.json");
|
|
@@ -1186,6 +1186,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1186
1186
|
Date.now() < reconnectDeadline) {
|
|
1187
1187
|
try {
|
|
1188
1188
|
await this.connectRpcClient();
|
|
1189
|
+
// The new connection starts with an empty subscription set.
|
|
1190
|
+
// Resubscribe the shared thread so turn notifications and approval
|
|
1191
|
+
// requests keep flowing: without this, a mid-turn reconnect
|
|
1192
|
+
// silently stops delivery, and approvals answered over the new
|
|
1193
|
+
// connection (with the old request id) are ignored by the server.
|
|
1194
|
+
if (this.state.sharedThreadId) {
|
|
1195
|
+
await this.tryEnsureSharedThreadSubscribed(this.state.sharedThreadId);
|
|
1196
|
+
}
|
|
1189
1197
|
return true;
|
|
1190
1198
|
}
|
|
1191
1199
|
catch (error) {
|
|
@@ -1394,7 +1402,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1394
1402
|
this.state.status !== "awaiting_input") {
|
|
1395
1403
|
return;
|
|
1396
1404
|
}
|
|
1397
|
-
|
|
1405
|
+
// Remember the interrupted turn as completed before resetting: the
|
|
1406
|
+
// server may still deliver a late turn/completed for it, and without
|
|
1407
|
+
// this entry it would be tracked as a brand-new local turn — emitting a
|
|
1408
|
+
// duplicate task_complete (or a ghost reply) for work we already
|
|
1409
|
+
// reported as interrupted.
|
|
1410
|
+
const interruptedTurnId = this.activeTurn?.turnId;
|
|
1411
|
+
this.resetTurnTracking({
|
|
1412
|
+
preserveThread: true,
|
|
1413
|
+
preserveCompletedTurns: true,
|
|
1414
|
+
});
|
|
1415
|
+
if (interruptedTurnId) {
|
|
1416
|
+
this.rememberCompletedTurn(interruptedTurnId);
|
|
1417
|
+
this.bridgeOwnedTurnIds.add(interruptedTurnId);
|
|
1418
|
+
}
|
|
1398
1419
|
this.setStatus("idle", "Codex task interrupted.");
|
|
1399
1420
|
this.emit({
|
|
1400
1421
|
type: "task_complete",
|
|
@@ -1463,9 +1484,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1463
1484
|
this.turnErrorById.clear();
|
|
1464
1485
|
this.turnLastActivityAtMs.clear();
|
|
1465
1486
|
this.mirroredUserInputTurnIds.clear();
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1487
|
+
if (!options.preserveCompletedTurns) {
|
|
1488
|
+
this.bridgeOwnedTurnIds.clear();
|
|
1489
|
+
this.completedTurnIds.clear();
|
|
1490
|
+
this.completedTurnOrder = [];
|
|
1491
|
+
}
|
|
1469
1492
|
this.pendingInjectedInputs = [];
|
|
1470
1493
|
this.recentBridgeThreadSignalAtById.clear();
|
|
1471
1494
|
this.sessionFinalText = null;
|
|
@@ -45,6 +45,8 @@ export class LocalCompanionProxyAdapter {
|
|
|
45
45
|
eventSink = () => undefined;
|
|
46
46
|
server = null;
|
|
47
47
|
socket = null;
|
|
48
|
+
// Pending senders waiting for the companion's first state frame.
|
|
49
|
+
readyWaiters = new Set();
|
|
48
50
|
detachMessageListener = null;
|
|
49
51
|
requestCounter = 0;
|
|
50
52
|
endpoint = null;
|
|
@@ -99,11 +101,22 @@ export class LocalCompanionProxyAdapter {
|
|
|
99
101
|
});
|
|
100
102
|
this.server = server;
|
|
101
103
|
server.on("error", (error) => {
|
|
104
|
+
// Roll back the half-initialized server: start() early-returns while
|
|
105
|
+
// this.server is set, so a leftover handle would make every retry
|
|
106
|
+
// silently "succeed" without ever listening.
|
|
107
|
+
if (this.server === server) {
|
|
108
|
+
this.server = null;
|
|
109
|
+
}
|
|
110
|
+
server.close();
|
|
102
111
|
reject(error);
|
|
103
112
|
});
|
|
104
113
|
server.listen(0, CODEX_APP_SERVER_HOST, () => {
|
|
105
114
|
const address = server.address();
|
|
106
115
|
if (!address || typeof address === "string") {
|
|
116
|
+
if (this.server === server) {
|
|
117
|
+
this.server = null;
|
|
118
|
+
}
|
|
119
|
+
server.close();
|
|
107
120
|
reject(new Error(`Failed to allocate a local ${this.options.kind} companion port.`));
|
|
108
121
|
return;
|
|
109
122
|
}
|
|
@@ -174,8 +187,17 @@ export class LocalCompanionProxyAdapter {
|
|
|
174
187
|
const ok = await this.resolveApproval(action);
|
|
175
188
|
return ok ? 1 : 0;
|
|
176
189
|
}
|
|
177
|
-
async submitUserInput(
|
|
178
|
-
|
|
190
|
+
async submitUserInput(answers) {
|
|
191
|
+
try {
|
|
192
|
+
const result = await this.sendRequest({
|
|
193
|
+
command: "submit_user_input",
|
|
194
|
+
answers,
|
|
195
|
+
});
|
|
196
|
+
return result === true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
179
201
|
}
|
|
180
202
|
async dispose() {
|
|
181
203
|
this.shuttingDown = true;
|
|
@@ -310,6 +332,9 @@ export class LocalCompanionProxyAdapter {
|
|
|
310
332
|
this.state.activeTurnOrigin = undefined;
|
|
311
333
|
this.state.pendingApprovalOrigin = undefined;
|
|
312
334
|
Object.assign(this.state, message.state);
|
|
335
|
+
if (this.state.pid) {
|
|
336
|
+
this.settleReadyWaiters();
|
|
337
|
+
}
|
|
313
338
|
this.eventSink({
|
|
314
339
|
type: "status",
|
|
315
340
|
status: this.state.status,
|
|
@@ -332,6 +357,10 @@ export class LocalCompanionProxyAdapter {
|
|
|
332
357
|
}
|
|
333
358
|
}
|
|
334
359
|
detachPanelSocket() {
|
|
360
|
+
this.settleReadyWaiters(new Error(formatCompanionNotConnectedMessage({
|
|
361
|
+
kind: this.options.kind,
|
|
362
|
+
launchMode: this.options.companionLaunchMode,
|
|
363
|
+
})));
|
|
335
364
|
this.rejectPendingRequests("Companion socket disconnected unexpectedly.");
|
|
336
365
|
this.detachMessageListener?.();
|
|
337
366
|
this.detachMessageListener = null;
|
|
@@ -420,8 +449,44 @@ export class LocalCompanionProxyAdapter {
|
|
|
420
449
|
}
|
|
421
450
|
this.pendingRequests.clear();
|
|
422
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Wait for the companion's first state frame (which reports the worker
|
|
454
|
+
* pid). The companion sends it only after its adapter finished starting —
|
|
455
|
+
* for opencode that spans spawning `opencode serve`, connecting, and the
|
|
456
|
+
* initial session scan — so a WeChat message arriving inside that window
|
|
457
|
+
* must wait instead of failing with "connected but not ready yet".
|
|
458
|
+
*/
|
|
459
|
+
waitForCompanionReady() {
|
|
460
|
+
if (this.state.pid) {
|
|
461
|
+
return Promise.resolve();
|
|
462
|
+
}
|
|
463
|
+
return new Promise((resolve, reject) => {
|
|
464
|
+
const entry = {
|
|
465
|
+
timer: setTimeout(() => {
|
|
466
|
+
this.readyWaiters.delete(entry);
|
|
467
|
+
reject(new Error(`${this.options.kind} companion is connected but not ready yet. Wait for it to finish starting.`));
|
|
468
|
+
}, 30_000),
|
|
469
|
+
settle: (error) => {
|
|
470
|
+
clearTimeout(entry.timer);
|
|
471
|
+
this.readyWaiters.delete(entry);
|
|
472
|
+
if (error) {
|
|
473
|
+
reject(error);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
resolve();
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
this.readyWaiters.add(entry);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
settleReadyWaiters(error) {
|
|
484
|
+
for (const entry of [...this.readyWaiters]) {
|
|
485
|
+
entry.settle(error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
423
488
|
async sendRequest(payload) {
|
|
424
|
-
|
|
489
|
+
let socket = this.socket;
|
|
425
490
|
if (!socket) {
|
|
426
491
|
throw new Error(formatCompanionNotConnectedMessage({
|
|
427
492
|
kind: this.options.kind,
|
|
@@ -429,7 +494,14 @@ export class LocalCompanionProxyAdapter {
|
|
|
429
494
|
}));
|
|
430
495
|
}
|
|
431
496
|
if (!this.state.pid && payload.command !== "dispose") {
|
|
432
|
-
|
|
497
|
+
await this.waitForCompanionReady();
|
|
498
|
+
socket = this.socket;
|
|
499
|
+
if (!socket) {
|
|
500
|
+
throw new Error(formatCompanionNotConnectedMessage({
|
|
501
|
+
kind: this.options.kind,
|
|
502
|
+
launchMode: this.options.companionLaunchMode,
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
433
505
|
}
|
|
434
506
|
const id = `${++this.requestCounter}`;
|
|
435
507
|
const response = new Promise((resolve, reject) => {
|
|
@@ -3,7 +3,6 @@ import { LocalCompanionProxyAdapter } from "./bridge-adapters.core.js";
|
|
|
3
3
|
import { CodexPtyAdapter } from "./bridge-adapters.codex.js";
|
|
4
4
|
import { OpenCodeServerAdapter } from "./bridge-adapters.opencode.js";
|
|
5
5
|
import { PiTuiAdapter } from "./bridge-adapters.pi.js";
|
|
6
|
-
import { ShellAdapter } from "./bridge-adapters.shell.js";
|
|
7
6
|
export * from "./bridge-adapters.shared.js";
|
|
8
7
|
export function createBridgeAdapter(options) {
|
|
9
8
|
switch (options.kind) {
|
|
@@ -23,8 +22,6 @@ export function createBridgeAdapter(options) {
|
|
|
23
22
|
return options.renderMode === "companion"
|
|
24
23
|
? new PiTuiAdapter(options)
|
|
25
24
|
: new LocalCompanionProxyAdapter(options);
|
|
26
|
-
case "shell":
|
|
27
|
-
return new ShellAdapter(options);
|
|
28
25
|
default:
|
|
29
26
|
throw new Error(`Unsupported adapter: ${options.kind}`);
|
|
30
27
|
}
|