cli-wechat-bridge 1.0.5 → 1.0.6
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 +27 -24
- package/dist/bridge/bridge-adapters.claude.js +33 -3
- package/dist/bridge/bridge-adapters.codex.js +216 -6
- package/dist/bridge/bridge-adapters.opencode.js +79 -13
- package/dist/bridge/bridge-adapters.shared.js +215 -1
- package/dist/bridge/bridge-utils.js +49 -2
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +58 -2
- package/dist/daemon/wechat-daemon.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://github.com/UNLINEARITY/CLI-WeChat-Bridge"><img alt="GitHub stars" src="https://img.shields.io/github/stars/UNLINEARITY/CLI-WeChat-Bridge?label=Stars&style=for-the-badge&logo=github&color=0891b2&labelColor=1c1917"></a>
|
|
9
|
-
<a href="https://www.npmjs.com/package/cli-wechat-bridge"><img alt="npm version" src="https://img.shields.io/npm/v/cli-wechat-bridge?label=npm&style=for-the-badge&logo=npm&color=cb3837&labelColor=1c1917"></a>
|
|
10
|
-
<a href="https://www.npmjs.com/package/cli-wechat-bridge"><img alt="npm downloads" src="https://img.shields.io/npm/dm/cli-wechat-bridge?label=Downloads&style=for-the-badge&logo=npm&color=16a34a&labelColor=1c1917"></a>
|
|
11
|
-
<a href="https://
|
|
9
|
+
<a href="https://www.npmjs.com/package/cli-wechat-bridge"><img alt="npm version" src="https://img.shields.io/npm/v/cli-wechat-bridge?label=npm&style=for-the-badge&logo=npm&color=cb3837&labelColor=1c1917"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/cli-wechat-bridge"><img alt="npm downloads" src="https://img.shields.io/npm/dm/cli-wechat-bridge?label=Downloads&style=for-the-badge&logo=npm&color=16a34a&labelColor=1c1917"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@unlinearity/cli-wechat-bridge"><img alt="scoped npm downloads" src="https://img.shields.io/npm/dm/@unlinearity/cli-wechat-bridge?label=Scoped%20downloads&style=for-the-badge&logo=npm&color=15803d&labelColor=1c1917"></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/cli-wechat-bridge">
|
|
13
|
+
<img alt="License" src="https://img.shields.io/npm/l/cli-wechat-bridge?label=License&style=for-the-badge&color=7c3aed&labelColor=1c1917">
|
|
14
|
+
</a>
|
|
12
15
|
</p>
|
|
13
16
|
|
|
14
17
|
**命令行工具的微信桥接**:本项目用于桥接微信消息与本地运行的 [`Codex`](https://github.com/openai/codex)、[`Claude Code`](https://code.claude.com/docs/en/overview)、`OpenCode` 或持久化 `powershell.exe` 会话,并将本地输出、审批请求与运行状态同步回微信。
|
|
@@ -47,13 +50,13 @@
|
|
|
47
50
|
|
|
48
51
|
正式版本可以直接从 npm 安装:
|
|
49
52
|
|
|
50
|
-
```bash
|
|
51
|
-
npm install -g cli-wechat-bridge@latest
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
安装后可在任意项目目录中直接使用 `wechat-daemon`、`wechat-codex-start`、`wechat-claude-start`、`wechat-opencode-start` 等命令。
|
|
55
|
-
|
|
56
|
-
兼容说明:旧包名 `@unlinearity/cli-wechat-bridge` 会继续同步发布,已经安装旧包名的用户可以正常升级;新用户优先使用更短的 `cli-wechat-bridge`。
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g cli-wechat-bridge@latest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
安装后可在任意项目目录中直接使用 `wechat-daemon`、`wechat-codex-start`、`wechat-claude-start`、`wechat-opencode-start` 等命令。
|
|
58
|
+
|
|
59
|
+
兼容说明:旧包名 `@unlinearity/cli-wechat-bridge` 会继续同步发布,已经安装旧包名的用户可以正常升级;新用户优先使用更短的 `cli-wechat-bridge`。
|
|
57
60
|
|
|
58
61
|
如果你希望从源码运行或参与开发,也可以克隆仓库并安装依赖:
|
|
59
62
|
|
|
@@ -152,12 +155,12 @@ wechat-daemon
|
|
|
152
155
|
| 指令 | 行为 |
|
|
153
156
|
| --- | --- |
|
|
154
157
|
| `/codex` | 切换到 Codex |
|
|
155
|
-
| `/claude` | 切换到 Claude Code |
|
|
156
|
-
| `/opencode` | 切换到 OpenCode |
|
|
157
|
-
|
|
158
|
-
daemon 启动后,后续切换都可以直接从微信发起;如果对应 CLI 还没有可见窗口,daemon 会自动打开或复用它,不需要再手动运行 `wechat-codex`、`wechat-claude` 或 `wechat-opencode`。
|
|
159
|
-
|
|
160
|
-
daemon 的 v1 行为是:
|
|
158
|
+
| `/claude` | 切换到 Claude Code |
|
|
159
|
+
| `/opencode` | 切换到 OpenCode |
|
|
160
|
+
|
|
161
|
+
daemon 启动后,后续切换都可以直接从微信发起;如果对应 CLI 还没有可见窗口,daemon 会自动打开或复用它,不需要再手动运行 `wechat-codex`、`wechat-claude` 或 `wechat-opencode`。
|
|
162
|
+
|
|
163
|
+
daemon 的 v1 行为是:
|
|
161
164
|
|
|
162
165
|
- daemon 绑定启动时的工作目录;暂不支持在微信里切换工作目录;
|
|
163
166
|
- 启动时会自动接管并清理旧的单 bridge 进程、失效 lock 和旧 endpoint;
|
|
@@ -485,9 +488,9 @@ wechat-check-update
|
|
|
485
488
|
|
|
486
489
|
如果你使用 npm 全局安装,当前推荐直接执行:
|
|
487
490
|
|
|
488
|
-
```bash
|
|
489
|
-
npm install -g cli-wechat-bridge@latest
|
|
490
|
-
```
|
|
491
|
+
```bash
|
|
492
|
+
npm install -g cli-wechat-bridge@latest
|
|
493
|
+
```
|
|
491
494
|
|
|
492
495
|
升级后请重启正在运行的 bridge 和 companion 终端,确保它们加载同一版本。
|
|
493
496
|
|
|
@@ -516,11 +519,11 @@ npm install -g .
|
|
|
516
519
|
|
|
517
520
|
请确认已经执行以下之一:
|
|
518
521
|
|
|
519
|
-
```bash
|
|
520
|
-
npm install -g cli-wechat-bridge@latest
|
|
521
|
-
npm install -g .
|
|
522
|
-
npm link
|
|
523
|
-
```
|
|
522
|
+
```bash
|
|
523
|
+
npm install -g cli-wechat-bridge@latest
|
|
524
|
+
npm install -g .
|
|
525
|
+
npm link
|
|
526
|
+
```
|
|
524
527
|
|
|
525
528
|
如果命令仍不存在,请检查 npm 全局 bin 目录是否已加入 `PATH`。
|
|
526
529
|
|
|
@@ -3,14 +3,17 @@ import net from "node:net";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
|
|
5
5
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
6
|
-
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
6
|
+
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
7
7
|
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
8
8
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
9
9
|
import * as shared from "./bridge-adapters.shared.js";
|
|
10
|
-
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
10
|
+
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
11
11
|
const CLAUDE_COMPACT_OUTPUT_LINE_RE = /^Compacted(?:\s*\(.*full summary.*\))?$/i;
|
|
12
12
|
const CLAUDE_COMPACT_FAILURE_RE = /Error:\s*Error during compaction:|(?:^|\b)API Error:|\b(?:compact|compaction)\s+failed\b|^Error:/i;
|
|
13
13
|
const CLAUDE_COMPACT_DEDUP_MS = 2_000;
|
|
14
|
+
const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
|
|
15
|
+
const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
|
|
16
|
+
const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
|
|
14
17
|
export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
15
18
|
hookServer = null;
|
|
16
19
|
hookPort = null;
|
|
@@ -99,7 +102,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
99
102
|
this.pendingCliApprovalHints = null;
|
|
100
103
|
this.clearWechatWorkingNotice(true);
|
|
101
104
|
this.setStatus("busy");
|
|
102
|
-
this.writeToPty(
|
|
105
|
+
this.writeToPty(this.buildRemoteInputPayload(text));
|
|
106
|
+
await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
|
|
103
107
|
this.writeToPty("\r");
|
|
104
108
|
this.armWechatWorkingNotice();
|
|
105
109
|
}
|
|
@@ -464,6 +468,13 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
464
468
|
timestamp: nowIso(),
|
|
465
469
|
});
|
|
466
470
|
}
|
|
471
|
+
buildRemoteInputPayload(text) {
|
|
472
|
+
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
473
|
+
if (!normalizedText.includes("\n")) {
|
|
474
|
+
return normalizedText;
|
|
475
|
+
}
|
|
476
|
+
return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
|
|
477
|
+
}
|
|
467
478
|
shouldTreatClaudeOutputAsCompactCompletion(text) {
|
|
468
479
|
if (this.state.status !== "busy" &&
|
|
469
480
|
this.state.status !== "awaiting_approval" &&
|
|
@@ -707,6 +718,25 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
707
718
|
}
|
|
708
719
|
handleClaudePermissionRequest(requestId, payload, socket) {
|
|
709
720
|
this.clearWechatWorkingNotice();
|
|
721
|
+
const denyMessage = getClaudeWechatOutboundAttachmentDenyMessage(payload);
|
|
722
|
+
if (denyMessage) {
|
|
723
|
+
this.pendingApproval = null;
|
|
724
|
+
this.state.pendingApproval = null;
|
|
725
|
+
this.state.pendingApprovalOrigin = undefined;
|
|
726
|
+
if (this.state.status === "awaiting_approval") {
|
|
727
|
+
this.setStatus("busy");
|
|
728
|
+
}
|
|
729
|
+
this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput("deny", denyMessage));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const autoResponse = getClaudePermissionAutoResponse(payload);
|
|
733
|
+
if (autoResponse) {
|
|
734
|
+
if (!this.pendingApproval && !this.state.pendingApproval) {
|
|
735
|
+
this.setStatus("busy", `Claude approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
736
|
+
}
|
|
737
|
+
this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput(autoResponse.action));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
710
740
|
this.flushPendingClaudeHookApprovals();
|
|
711
741
|
this.pendingHookApprovals.set(requestId, {
|
|
712
742
|
requestId,
|
|
@@ -7,7 +7,7 @@ import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
|
7
7
|
import * as shared from "./bridge-adapters.shared.js";
|
|
8
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
9
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
10
|
-
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCliEnvironment, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
|
|
10
|
+
const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCliEnvironment, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
|
|
11
11
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
12
12
|
export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
13
13
|
runtimeKind = "codex_runtime_host";
|
|
@@ -24,6 +24,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
24
24
|
cleanPanelExitInProgress = false;
|
|
25
25
|
rpcRequestCounter = 0;
|
|
26
26
|
pendingRpcRequests = new Map();
|
|
27
|
+
subscribedThreadIds = new Set();
|
|
27
28
|
sharedThreadId = null;
|
|
28
29
|
announcedThreadId = null;
|
|
29
30
|
pendingThreadAnnouncement = null;
|
|
@@ -194,6 +195,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
194
195
|
}
|
|
195
196
|
return await super.resolveApproval(action);
|
|
196
197
|
}
|
|
198
|
+
async submitUserInput(answers) {
|
|
199
|
+
if (!this.pendingUserInputRequest) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
const request = this.pendingUserInputRequest;
|
|
203
|
+
const responseAnswers = {};
|
|
204
|
+
for (const [questionId, values] of Object.entries(answers)) {
|
|
205
|
+
responseAnswers[questionId] = {
|
|
206
|
+
answers: values,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
this.sendRpcMessage({
|
|
210
|
+
id: request.requestId,
|
|
211
|
+
result: {
|
|
212
|
+
answers: responseAnswers,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
this.clearPendingUserInputState();
|
|
216
|
+
this.setStatus("busy", "Codex user input submitted.");
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
197
219
|
async dispose() {
|
|
198
220
|
this.resetTurnTracking({ preserveThread: false });
|
|
199
221
|
if (this.isEmbeddedCliMode()) {
|
|
@@ -752,6 +774,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
752
774
|
if (this.pendingApproval) {
|
|
753
775
|
throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
|
|
754
776
|
}
|
|
777
|
+
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
778
|
+
throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
|
|
779
|
+
}
|
|
755
780
|
if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
|
|
756
781
|
const origin = this.state.activeTurnOrigin;
|
|
757
782
|
if (origin === "local") {
|
|
@@ -766,6 +791,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
766
791
|
this.rememberInjectedInput(text);
|
|
767
792
|
this.clearPendingApprovalState();
|
|
768
793
|
const threadId = await this.ensureThreadStarted();
|
|
794
|
+
const subscribedBeforeTurnStart = await this.tryEnsureSharedThreadSubscribed(threadId);
|
|
769
795
|
this.pendingTurnStart = true;
|
|
770
796
|
this.pendingTurnThreadId = threadId;
|
|
771
797
|
this.interruptPendingTurnStart = false;
|
|
@@ -793,6 +819,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
793
819
|
turnId,
|
|
794
820
|
origin: "wechat",
|
|
795
821
|
});
|
|
822
|
+
if (!subscribedBeforeTurnStart) {
|
|
823
|
+
await this.tryEnsureSharedThreadSubscribed(threadId);
|
|
824
|
+
}
|
|
796
825
|
if (this.interruptPendingTurnStart) {
|
|
797
826
|
await this.requestActiveTurnInterrupt();
|
|
798
827
|
this.armInterruptFallback();
|
|
@@ -813,11 +842,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
813
842
|
if (this.isNativePanelMode() && !this.nativeProcess) {
|
|
814
843
|
return false;
|
|
815
844
|
}
|
|
816
|
-
const turnPending = this.pendingTurnStart ||
|
|
845
|
+
const turnPending = this.pendingTurnStart ||
|
|
846
|
+
this.state.status === "busy" ||
|
|
847
|
+
this.state.status === "awaiting_approval" ||
|
|
848
|
+
this.state.status === "awaiting_input";
|
|
817
849
|
if (!turnPending) {
|
|
818
850
|
return false;
|
|
819
851
|
}
|
|
820
852
|
this.clearPendingApprovalState();
|
|
853
|
+
this.clearPendingUserInputState();
|
|
821
854
|
if (this.pendingTurnStart && !this.activeTurn) {
|
|
822
855
|
this.interruptPendingTurnStart = true;
|
|
823
856
|
this.armInterruptFallback();
|
|
@@ -980,6 +1013,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
980
1013
|
attachRpcSocket(socket) {
|
|
981
1014
|
this.rpcSocket = socket;
|
|
982
1015
|
this.rpcShuttingDown = false;
|
|
1016
|
+
this.subscribedThreadIds.clear();
|
|
983
1017
|
socket.addEventListener("message", (event) => {
|
|
984
1018
|
this.handleRpcMessageData(event.data);
|
|
985
1019
|
});
|
|
@@ -991,6 +1025,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
991
1025
|
const socket = this.rpcSocket;
|
|
992
1026
|
this.rpcSocket = null;
|
|
993
1027
|
this.rpcShuttingDown = true;
|
|
1028
|
+
this.subscribedThreadIds.clear();
|
|
994
1029
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
995
1030
|
if (!socket) {
|
|
996
1031
|
this.rpcShuttingDown = false;
|
|
@@ -1028,6 +1063,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1028
1063
|
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1029
1064
|
});
|
|
1030
1065
|
this.rpcSocket = null;
|
|
1066
|
+
this.subscribedThreadIds.clear();
|
|
1031
1067
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
1032
1068
|
this.rpcShuttingDown = false;
|
|
1033
1069
|
if (expectedShutdown) {
|
|
@@ -1156,9 +1192,44 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1156
1192
|
throw new Error("Codex did not return a thread id for the bridge session.");
|
|
1157
1193
|
}
|
|
1158
1194
|
this.rememberBridgeOwnedThreadSignal(threadId);
|
|
1195
|
+
this.subscribedThreadIds.add(threadId);
|
|
1159
1196
|
this.updateSharedThread(threadId);
|
|
1160
1197
|
return threadId;
|
|
1161
1198
|
}
|
|
1199
|
+
async tryEnsureSharedThreadSubscribed(threadId) {
|
|
1200
|
+
if (this.subscribedThreadIds.has(threadId)) {
|
|
1201
|
+
return true;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
await this.ensureSharedThreadSubscribed(threadId);
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async ensureSharedThreadSubscribed(threadId) {
|
|
1212
|
+
if (this.subscribedThreadIds.has(threadId)) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const response = await this.sendRpcRequest("thread/resume", {
|
|
1216
|
+
threadId,
|
|
1217
|
+
cwd: this.options.cwd,
|
|
1218
|
+
approvalPolicy: "on-request",
|
|
1219
|
+
approvalsReviewer: "user",
|
|
1220
|
+
sandbox: "workspace-write",
|
|
1221
|
+
excludeTurns: true,
|
|
1222
|
+
});
|
|
1223
|
+
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1224
|
+
if (!resumedThreadId) {
|
|
1225
|
+
throw new Error("Codex did not return a thread id while subscribing the bridge client.");
|
|
1226
|
+
}
|
|
1227
|
+
this.rememberBridgeOwnedThreadSignal(resumedThreadId);
|
|
1228
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1229
|
+
if (resumedThreadId !== this.sharedThreadId) {
|
|
1230
|
+
this.updateSharedThread(resumedThreadId);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1162
1233
|
async resumeSharedThread(threadId, options = {}) {
|
|
1163
1234
|
const trimmedThreadId = threadId.trim();
|
|
1164
1235
|
if (!trimmedThreadId) {
|
|
@@ -1180,12 +1251,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1180
1251
|
approvalPolicy: "on-request",
|
|
1181
1252
|
approvalsReviewer: "user",
|
|
1182
1253
|
sandbox: "workspace-write",
|
|
1254
|
+
excludeTurns: true,
|
|
1183
1255
|
});
|
|
1184
1256
|
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1185
1257
|
if (!resumedThreadId) {
|
|
1186
1258
|
throw new Error("Codex did not return a thread id while resuming the saved thread.");
|
|
1187
1259
|
}
|
|
1188
1260
|
this.rememberBridgeOwnedThreadSignal(resumedThreadId);
|
|
1261
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1189
1262
|
this.sessionFilePath = null;
|
|
1190
1263
|
this.sessionReadOffset = 0;
|
|
1191
1264
|
this.sessionPartialLine = "";
|
|
@@ -1238,7 +1311,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1238
1311
|
this.clearInterruptTimer();
|
|
1239
1312
|
this.interruptTimer = setTimeout(() => {
|
|
1240
1313
|
this.interruptTimer = null;
|
|
1241
|
-
if (this.state.status !== "busy" &&
|
|
1314
|
+
if (this.state.status !== "busy" &&
|
|
1315
|
+
this.state.status !== "awaiting_approval" &&
|
|
1316
|
+
this.state.status !== "awaiting_input") {
|
|
1242
1317
|
return;
|
|
1243
1318
|
}
|
|
1244
1319
|
this.resetTurnTracking({ preserveThread: true });
|
|
@@ -1263,6 +1338,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1263
1338
|
pendingTurnStart: this.pendingTurnStart,
|
|
1264
1339
|
hasActiveTurn: Boolean(this.activeTurn),
|
|
1265
1340
|
hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
|
|
1341
|
+
hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
|
|
1266
1342
|
activeTurnId: this.state.activeTurnId,
|
|
1267
1343
|
})) {
|
|
1268
1344
|
return;
|
|
@@ -1301,6 +1377,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1301
1377
|
this.interruptPendingTurnStart = false;
|
|
1302
1378
|
this.pendingThreadFollowId = null;
|
|
1303
1379
|
this.clearPendingApprovalState();
|
|
1380
|
+
this.clearPendingUserInputState();
|
|
1304
1381
|
this.queuedTurnNotifications = [];
|
|
1305
1382
|
this.queuedTurnServerRequests = [];
|
|
1306
1383
|
this.turnFinalMessages.clear();
|
|
@@ -1503,6 +1580,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1503
1580
|
this.state.pendingApproval = null;
|
|
1504
1581
|
this.state.pendingApprovalOrigin = undefined;
|
|
1505
1582
|
}
|
|
1583
|
+
clearPendingUserInputState() {
|
|
1584
|
+
this.pendingUserInputRequest = null;
|
|
1585
|
+
this.state.pendingUserInput = null;
|
|
1586
|
+
this.state.pendingUserInputOrigin = undefined;
|
|
1587
|
+
}
|
|
1506
1588
|
cleanupTurnArtifacts(turnId) {
|
|
1507
1589
|
this.clearFinalReplyCompletionTimerForTurn(turnId);
|
|
1508
1590
|
this.turnFinalMessages.delete(turnId);
|
|
@@ -1570,6 +1652,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1570
1652
|
socket.send(JSON.stringify(payload));
|
|
1571
1653
|
}
|
|
1572
1654
|
async respondToApprovalRequest(request, action) {
|
|
1655
|
+
if (request.method === "item/permissions/requestApproval") {
|
|
1656
|
+
this.sendRpcMessage({
|
|
1657
|
+
id: request.requestId,
|
|
1658
|
+
result: buildCodexPermissionsRequestApprovalResponse(request.params, action),
|
|
1659
|
+
});
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1573
1662
|
const decision = action === "confirm" ? "accept" : "decline";
|
|
1574
1663
|
this.sendRpcMessage({
|
|
1575
1664
|
id: request.requestId,
|
|
@@ -1754,6 +1843,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1754
1843
|
this.setStatus("busy", "Codex approval resolved.");
|
|
1755
1844
|
}
|
|
1756
1845
|
}
|
|
1846
|
+
if (requestId !== null &&
|
|
1847
|
+
this.pendingUserInputRequest &&
|
|
1848
|
+
requestId === this.pendingUserInputRequest.requestId &&
|
|
1849
|
+
trackedTurn.turnId === this.pendingUserInputRequest.turnId) {
|
|
1850
|
+
this.clearPendingUserInputState();
|
|
1851
|
+
if (this.state.status === "awaiting_input") {
|
|
1852
|
+
this.setStatus("busy", "Codex user input resolved.");
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1757
1855
|
return;
|
|
1758
1856
|
}
|
|
1759
1857
|
case "turn/completed": {
|
|
@@ -1764,8 +1862,24 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1764
1862
|
}
|
|
1765
1863
|
}
|
|
1766
1864
|
handleRpcServerRequest(requestId, method, params) {
|
|
1865
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1866
|
+
this.sendRpcMessage({
|
|
1867
|
+
id: requestId,
|
|
1868
|
+
result: buildCodexMcpServerElicitationDeclineResponse(),
|
|
1869
|
+
});
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (method === "item/tool/call") {
|
|
1873
|
+
this.sendRpcMessage({
|
|
1874
|
+
id: requestId,
|
|
1875
|
+
result: buildCodexDynamicToolCallFailureResponse(),
|
|
1876
|
+
});
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1767
1879
|
if (method !== "item/commandExecution/requestApproval" &&
|
|
1768
|
-
method !== "item/fileChange/requestApproval"
|
|
1880
|
+
method !== "item/fileChange/requestApproval" &&
|
|
1881
|
+
method !== "item/permissions/requestApproval" &&
|
|
1882
|
+
method !== "item/tool/requestUserInput") {
|
|
1769
1883
|
this.sendRpcMessage({
|
|
1770
1884
|
id: requestId,
|
|
1771
1885
|
error: {
|
|
@@ -1793,16 +1907,57 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1793
1907
|
});
|
|
1794
1908
|
return;
|
|
1795
1909
|
}
|
|
1796
|
-
const trackedTurn = this.identifyTrackedTurn("server/request", params)
|
|
1910
|
+
const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
|
|
1911
|
+
this.fallbackTrackedTurnForServerRequest(params);
|
|
1797
1912
|
if (!trackedTurn) {
|
|
1913
|
+
this.sendRpcMessage({
|
|
1914
|
+
id: requestId,
|
|
1915
|
+
error: {
|
|
1916
|
+
code: -32602,
|
|
1917
|
+
message: "Invalid Codex server request payload: missing threadId or turnId.",
|
|
1918
|
+
},
|
|
1919
|
+
});
|
|
1798
1920
|
return;
|
|
1799
1921
|
}
|
|
1800
1922
|
this.handleTrackedTurnStarted(trackedTurn);
|
|
1801
1923
|
this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
|
|
1802
1924
|
}
|
|
1803
1925
|
handleTrackedTurnServerRequest(requestId, method, params, trackedTurn) {
|
|
1926
|
+
if (method === "item/tool/requestUserInput") {
|
|
1927
|
+
this.handleTrackedTurnUserInputRequest(requestId, params, trackedTurn);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const denyMessage = getCodexWechatOutboundAttachmentDenyMessage(method, params);
|
|
1931
|
+
if (denyMessage) {
|
|
1932
|
+
this.sendRpcMessage({
|
|
1933
|
+
id: requestId,
|
|
1934
|
+
result: method === "item/permissions/requestApproval"
|
|
1935
|
+
? buildCodexPermissionsRequestApprovalResponse(params, "deny")
|
|
1936
|
+
: { decision: "decline" },
|
|
1937
|
+
});
|
|
1938
|
+
this.state.lastOutputAt = nowIso();
|
|
1939
|
+
this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const autoResponse = getCodexApprovalAutoResponse(method, params);
|
|
1943
|
+
if (autoResponse) {
|
|
1944
|
+
this.sendRpcMessage({
|
|
1945
|
+
id: requestId,
|
|
1946
|
+
result: autoResponse.result,
|
|
1947
|
+
});
|
|
1948
|
+
this.state.lastOutputAt = nowIso();
|
|
1949
|
+
this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1804
1952
|
const request = buildCodexApprovalRequest(method, params);
|
|
1805
1953
|
if (!request) {
|
|
1954
|
+
this.sendRpcMessage({
|
|
1955
|
+
id: requestId,
|
|
1956
|
+
error: {
|
|
1957
|
+
code: -32602,
|
|
1958
|
+
message: "Invalid Codex approval request payload.",
|
|
1959
|
+
},
|
|
1960
|
+
});
|
|
1806
1961
|
return;
|
|
1807
1962
|
}
|
|
1808
1963
|
this.pendingApprovalRequest = {
|
|
@@ -1811,18 +1966,63 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1811
1966
|
threadId: trackedTurn.threadId,
|
|
1812
1967
|
turnId: trackedTurn.turnId,
|
|
1813
1968
|
origin: trackedTurn.origin,
|
|
1969
|
+
params,
|
|
1814
1970
|
};
|
|
1815
1971
|
this.pendingApproval = request;
|
|
1816
1972
|
this.state.pendingApproval = request;
|
|
1817
1973
|
this.state.pendingApprovalOrigin = trackedTurn.origin;
|
|
1818
1974
|
this.state.lastOutputAt = nowIso();
|
|
1819
|
-
this.setStatus("awaiting_approval",
|
|
1975
|
+
this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
|
|
1820
1976
|
this.emit({
|
|
1821
1977
|
type: "approval_required",
|
|
1822
1978
|
request,
|
|
1823
1979
|
timestamp: nowIso(),
|
|
1824
1980
|
});
|
|
1825
1981
|
}
|
|
1982
|
+
fallbackTrackedTurnForServerRequest(params) {
|
|
1983
|
+
if (this.activeTurn) {
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
const threadId = getNotificationThreadId(params);
|
|
1987
|
+
const turnId = getNotificationTurnId(params);
|
|
1988
|
+
if (!threadId || !turnId) {
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
return {
|
|
1992
|
+
threadId,
|
|
1993
|
+
turnId,
|
|
1994
|
+
origin: this.bridgeOwnedTurnIds.has(turnId) ? "wechat" : "local",
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
handleTrackedTurnUserInputRequest(requestId, params, trackedTurn) {
|
|
1998
|
+
const request = buildCodexUserInputRequest(params);
|
|
1999
|
+
if (!request) {
|
|
2000
|
+
this.sendRpcMessage({
|
|
2001
|
+
id: requestId,
|
|
2002
|
+
error: {
|
|
2003
|
+
code: -32602,
|
|
2004
|
+
message: "Invalid Codex user input request payload.",
|
|
2005
|
+
},
|
|
2006
|
+
});
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.pendingUserInputRequest = {
|
|
2010
|
+
requestId,
|
|
2011
|
+
method: "item/tool/requestUserInput",
|
|
2012
|
+
threadId: trackedTurn.threadId,
|
|
2013
|
+
turnId: trackedTurn.turnId,
|
|
2014
|
+
origin: trackedTurn.origin,
|
|
2015
|
+
};
|
|
2016
|
+
this.state.pendingUserInput = request;
|
|
2017
|
+
this.state.pendingUserInputOrigin = trackedTurn.origin;
|
|
2018
|
+
this.state.lastOutputAt = nowIso();
|
|
2019
|
+
this.setStatus("awaiting_input", "Codex is waiting for user input.");
|
|
2020
|
+
this.emit({
|
|
2021
|
+
type: "user_input_required",
|
|
2022
|
+
request,
|
|
2023
|
+
timestamp: nowIso(),
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
1826
2026
|
handleThreadStatusChanged(params) {
|
|
1827
2027
|
const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
|
|
1828
2028
|
if (!threadId) {
|
|
@@ -1935,6 +2135,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1935
2135
|
this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
|
|
1936
2136
|
this.clearPendingApprovalState();
|
|
1937
2137
|
}
|
|
2138
|
+
if (this.pendingUserInputRequest &&
|
|
2139
|
+
this.pendingUserInputRequest.turnId === trackedTurn.turnId) {
|
|
2140
|
+
this.clearPendingUserInputState();
|
|
2141
|
+
}
|
|
1938
2142
|
if (this.activeTurn?.turnId === trackedTurn.turnId) {
|
|
1939
2143
|
this.setActiveTurn(null);
|
|
1940
2144
|
}
|
|
@@ -2023,6 +2227,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2023
2227
|
this.pendingTurnStart ||
|
|
2024
2228
|
this.pendingApproval ||
|
|
2025
2229
|
this.pendingApprovalRequest ||
|
|
2230
|
+
this.pendingUserInputRequest ||
|
|
2231
|
+
this.state.pendingUserInput ||
|
|
2026
2232
|
!this.collectTurnOutput(turnId)) {
|
|
2027
2233
|
return;
|
|
2028
2234
|
}
|
|
@@ -2039,6 +2245,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2039
2245
|
const finalText = this.collectTurnOutput(turnId);
|
|
2040
2246
|
const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
|
|
2041
2247
|
const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
|
|
2248
|
+
const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
|
|
2042
2249
|
const nowMs = Date.now();
|
|
2043
2250
|
if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
|
|
2044
2251
|
candidateTurnId: turnId,
|
|
@@ -2046,6 +2253,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2046
2253
|
activeTurnOrigin: activeTurn?.origin,
|
|
2047
2254
|
pendingTurnStart: this.pendingTurnStart,
|
|
2048
2255
|
hasPendingApproval: pendingApproval,
|
|
2256
|
+
hasPendingUserInput: pendingUserInput,
|
|
2049
2257
|
hasFinalOutput: Boolean(finalText),
|
|
2050
2258
|
hasCompletedTurn: this.hasCompletedTurn(turnId),
|
|
2051
2259
|
lastActivityAtMs,
|
|
@@ -2056,6 +2264,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2056
2264
|
activeTurn.origin === "wechat" &&
|
|
2057
2265
|
!this.pendingTurnStart &&
|
|
2058
2266
|
!pendingApproval &&
|
|
2267
|
+
!pendingUserInput &&
|
|
2059
2268
|
finalText &&
|
|
2060
2269
|
typeof lastActivityAtMs === "number") {
|
|
2061
2270
|
const remainingMs = CODEX_FINAL_REPLY_SETTLE_DELAY_MS - (nowMs - lastActivityAtMs);
|
|
@@ -2073,6 +2282,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2073
2282
|
return;
|
|
2074
2283
|
}
|
|
2075
2284
|
this.clearPendingApprovalState();
|
|
2285
|
+
this.clearPendingUserInputState();
|
|
2076
2286
|
this.setActiveTurn(null);
|
|
2077
2287
|
this.cleanupTurnArtifacts(turnId);
|
|
2078
2288
|
this.state.lastOutputAt = nowIso();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
|
|
3
3
|
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
4
|
-
import { buildOneTimeCode, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
4
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
|
|
5
5
|
const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
|
|
6
6
|
const OPENCODE_DUPLICATE_EVENT_TTL_MS = 150;
|
|
7
7
|
const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
|
|
@@ -801,6 +801,11 @@ export class OpenCodeServerAdapter {
|
|
|
801
801
|
return;
|
|
802
802
|
}
|
|
803
803
|
this.clearWechatWorkingNotice();
|
|
804
|
+
const denyMessage = this.getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission);
|
|
805
|
+
if (denyMessage) {
|
|
806
|
+
this.rejectPermission(pendingPermission, denyMessage);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
804
809
|
const approval = this.toPendingApproval(pendingPermission);
|
|
805
810
|
this.pendingPermission = pendingPermission;
|
|
806
811
|
this.state.pendingApproval = approval;
|
|
@@ -817,6 +822,57 @@ export class OpenCodeServerAdapter {
|
|
|
817
822
|
this.state.pendingApproval = null;
|
|
818
823
|
this.state.pendingApprovalOrigin = undefined;
|
|
819
824
|
}
|
|
825
|
+
getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission) {
|
|
826
|
+
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
827
|
+
const command = this.extractPermissionCommand(properties, metadata);
|
|
828
|
+
if (isWechatOutboundAttachmentWriteCommand(command)) {
|
|
829
|
+
return WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
830
|
+
}
|
|
831
|
+
const targetPath = [
|
|
832
|
+
command,
|
|
833
|
+
typeof metadata.path === "string" ? metadata.path : "",
|
|
834
|
+
typeof metadata.file === "string" ? metadata.file : "",
|
|
835
|
+
typeof metadata.filePath === "string" ? metadata.filePath : "",
|
|
836
|
+
typeof metadata.filepath === "string" ? metadata.filepath : "",
|
|
837
|
+
typeof metadata.parentDir === "string" ? metadata.parentDir : "",
|
|
838
|
+
typeof metadata.target === "string" ? metadata.target : "",
|
|
839
|
+
typeof metadata.detail === "string" ? metadata.detail : "",
|
|
840
|
+
Array.isArray(properties.patterns)
|
|
841
|
+
? properties.patterns.filter((value) => typeof value === "string").join(" ")
|
|
842
|
+
: "",
|
|
843
|
+
typeof properties.pattern === "string" ? properties.pattern : "",
|
|
844
|
+
].join(" ");
|
|
845
|
+
return isWechatOutboundAttachmentMutationTool(pendingPermission.request.toolName, targetPath)
|
|
846
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
847
|
+
: null;
|
|
848
|
+
}
|
|
849
|
+
rejectPermission(pendingPermission, denyMessage) {
|
|
850
|
+
const client = this.client;
|
|
851
|
+
if (!client) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
void (async () => {
|
|
855
|
+
try {
|
|
856
|
+
const result = await client.permission.respond({
|
|
857
|
+
sessionID: pendingPermission.sessionId,
|
|
858
|
+
permissionID: pendingPermission.permissionId,
|
|
859
|
+
directory: this.options.cwd,
|
|
860
|
+
workspace: this.activeWorkspaceId ?? undefined,
|
|
861
|
+
response: "reject",
|
|
862
|
+
});
|
|
863
|
+
if (result.error !== undefined) {
|
|
864
|
+
throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
this.emit({
|
|
869
|
+
type: "stderr",
|
|
870
|
+
text: `${denyMessage}\nFailed to reject permission: ${describeUnknownError(err)}`,
|
|
871
|
+
timestamp: nowIso(),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
})();
|
|
875
|
+
}
|
|
820
876
|
toPendingApproval(pendingPermission) {
|
|
821
877
|
return {
|
|
822
878
|
...pendingPermission.request,
|
|
@@ -834,24 +890,14 @@ export class OpenCodeServerAdapter {
|
|
|
834
890
|
if (!sessionId || !permissionId) {
|
|
835
891
|
return null;
|
|
836
892
|
}
|
|
837
|
-
const toolName =
|
|
838
|
-
? properties.type
|
|
839
|
-
: typeof properties.permission === "string"
|
|
840
|
-
? properties.permission
|
|
841
|
-
: undefined;
|
|
893
|
+
const toolName = this.extractPermissionToolName(properties);
|
|
842
894
|
const title = typeof properties.title === "string"
|
|
843
895
|
? properties.title
|
|
844
896
|
: typeof properties.permission === "string"
|
|
845
897
|
? `Permission request: ${properties.permission}`
|
|
846
898
|
: undefined;
|
|
847
899
|
const metadata = isRecord(properties.metadata) ? properties.metadata : {};
|
|
848
|
-
const command =
|
|
849
|
-
? metadata.command
|
|
850
|
-
: typeof metadata.detail === "string"
|
|
851
|
-
? metadata.detail
|
|
852
|
-
: Array.isArray(properties.patterns)
|
|
853
|
-
? properties.patterns.filter((value) => typeof value === "string").join(", ")
|
|
854
|
-
: undefined;
|
|
900
|
+
const command = this.extractPermissionCommand(properties, metadata);
|
|
855
901
|
return {
|
|
856
902
|
sessionId,
|
|
857
903
|
permissionId,
|
|
@@ -869,6 +915,26 @@ export class OpenCodeServerAdapter {
|
|
|
869
915
|
},
|
|
870
916
|
};
|
|
871
917
|
}
|
|
918
|
+
extractPermissionToolName(properties) {
|
|
919
|
+
return typeof properties.type === "string"
|
|
920
|
+
? properties.type
|
|
921
|
+
: typeof properties.permission === "string"
|
|
922
|
+
? properties.permission
|
|
923
|
+
: undefined;
|
|
924
|
+
}
|
|
925
|
+
extractPermissionCommand(properties, metadata) {
|
|
926
|
+
if (typeof metadata.command === "string") {
|
|
927
|
+
return metadata.command;
|
|
928
|
+
}
|
|
929
|
+
if (typeof metadata.detail === "string") {
|
|
930
|
+
return metadata.detail;
|
|
931
|
+
}
|
|
932
|
+
if (Array.isArray(properties.patterns)) {
|
|
933
|
+
const patterns = properties.patterns.filter((value) => typeof value === "string");
|
|
934
|
+
return patterns.length > 0 ? patterns.join(", ") : undefined;
|
|
935
|
+
}
|
|
936
|
+
return undefined;
|
|
937
|
+
}
|
|
872
938
|
handleSessionCreated(properties) {
|
|
873
939
|
if (!isRecord(properties)) {
|
|
874
940
|
return;
|
|
@@ -7,7 +7,7 @@ import { spawn as spawnPty } from "node-pty";
|
|
|
7
7
|
import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, sendLocalCompanionMessage, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
8
8
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
9
|
import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
10
|
-
import { detectCliApproval, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
10
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, detectCliApproval, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
11
11
|
import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
12
12
|
export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
|
|
13
13
|
export const DEFAULT_COLS = 120;
|
|
@@ -152,8 +152,220 @@ export function buildCodexApprovalRequest(method, params) {
|
|
|
152
152
|
commandPreview: truncatePreview(preview, 180),
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
+
if (method === "item/permissions/requestApproval") {
|
|
156
|
+
const reason = typeof params.reason === "string" ? params.reason : "";
|
|
157
|
+
const preview = summarizeCodexPermissionsRequest(params.permissions);
|
|
158
|
+
return {
|
|
159
|
+
source: "cli",
|
|
160
|
+
summary: reason
|
|
161
|
+
? `Codex needs approval before granting extra permissions: ${truncatePreview(reason, 160)}`
|
|
162
|
+
: "Codex needs approval before granting extra permissions.",
|
|
163
|
+
commandPreview: truncatePreview(preview, 180),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
export function getCodexWechatOutboundAttachmentDenyMessage(method, params) {
|
|
169
|
+
if (!isRecord(params)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
173
|
+
return isWechatOutboundAttachmentWriteCommand(params.command) ||
|
|
174
|
+
containsWechatOutboundAttachmentPathDeep(params.additionalPermissions)
|
|
175
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
176
|
+
: null;
|
|
177
|
+
}
|
|
178
|
+
if (method === "item/fileChange/requestApproval") {
|
|
179
|
+
return containsWechatOutboundAttachmentPath(params.grantRoot)
|
|
180
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
181
|
+
: null;
|
|
182
|
+
}
|
|
183
|
+
if (method === "item/permissions/requestApproval") {
|
|
184
|
+
return containsWechatOutboundAttachmentPathDeep(params.permissions)
|
|
185
|
+
? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
155
188
|
return null;
|
|
156
189
|
}
|
|
190
|
+
function commandApprovalAllowsAccept(availableDecisions) {
|
|
191
|
+
if (!Array.isArray(availableDecisions)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return availableDecisions.some((decision) => decision === "accept");
|
|
195
|
+
}
|
|
196
|
+
function normalizePermissionPath(pathValue) {
|
|
197
|
+
const normalized = pathValue.trim().replace(/\\/g, "/").toLowerCase();
|
|
198
|
+
return normalized === "/" ? normalized : normalized.replace(/\/+$/g, "");
|
|
199
|
+
}
|
|
200
|
+
function isHighRiskPermissionPath(pathValue) {
|
|
201
|
+
const normalized = normalizePermissionPath(pathValue);
|
|
202
|
+
if (!normalized) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (normalized === "/" || /^[a-z]:$/i.test(normalized)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return /^[a-z]:(?:\/windows|\/program files(?: \(x86\))?|\/programdata)(?:\/|$)/i.test(normalized);
|
|
209
|
+
}
|
|
210
|
+
function collectPermissionPaths(value, output) {
|
|
211
|
+
if (typeof value === "string") {
|
|
212
|
+
output.push(value);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (Array.isArray(value)) {
|
|
216
|
+
for (const item of value) {
|
|
217
|
+
collectPermissionPaths(item, output);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!isRecord(value)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (typeof value.path === "string") {
|
|
225
|
+
output.push(value.path);
|
|
226
|
+
}
|
|
227
|
+
if (typeof value.pattern === "string") {
|
|
228
|
+
output.push(value.pattern);
|
|
229
|
+
}
|
|
230
|
+
for (const item of Object.values(value)) {
|
|
231
|
+
collectPermissionPaths(item, output);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function hasRootSpecialPermissionPath(value) {
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
return value.some(hasRootSpecialPermissionPath);
|
|
237
|
+
}
|
|
238
|
+
if (!isRecord(value)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if ((value.kind === "root" || value.kind === "Root") ||
|
|
242
|
+
(value.value === "root" || value.value === "Root")) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return Object.values(value).some(hasRootSpecialPermissionPath);
|
|
246
|
+
}
|
|
247
|
+
function containsHighRiskPermissionTarget(value) {
|
|
248
|
+
if (hasRootSpecialPermissionPath(value)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
const paths = [];
|
|
252
|
+
collectPermissionPaths(value, paths);
|
|
253
|
+
return paths.some(isHighRiskPermissionPath);
|
|
254
|
+
}
|
|
255
|
+
export function getCodexApprovalAutoResponse(method, params) {
|
|
256
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
257
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
258
|
+
if (!commandApprovalAllowsAccept(params.availableDecisions)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (command && isHighRiskShellCommand(command)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (containsHighRiskPermissionTarget(params.additionalPermissions)) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
result: { decision: "accept" },
|
|
269
|
+
reason: command
|
|
270
|
+
? `low-risk command ${truncatePreview(command, 120)}`
|
|
271
|
+
: "low-risk command approval",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (method === "item/fileChange/requestApproval") {
|
|
275
|
+
if (containsHighRiskPermissionTarget(params.grantRoot)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
result: { decision: "accept" },
|
|
280
|
+
reason: "low-risk file change approval",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (method === "item/permissions/requestApproval") {
|
|
284
|
+
if (containsHighRiskPermissionTarget(params.permissions)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
result: buildCodexPermissionsRequestApprovalResponse(params, "confirm", {
|
|
289
|
+
strictAutoReview: true,
|
|
290
|
+
}),
|
|
291
|
+
reason: "low-risk permission grant",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function collectStringValues(value) {
|
|
297
|
+
return Array.isArray(value)
|
|
298
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
299
|
+
: [];
|
|
300
|
+
}
|
|
301
|
+
function summarizeCodexPermissionsRequest(permissions) {
|
|
302
|
+
if (!isRecord(permissions)) {
|
|
303
|
+
return "Additional permissions requested.";
|
|
304
|
+
}
|
|
305
|
+
const parts = [];
|
|
306
|
+
if (isRecord(permissions.network) && permissions.network.enabled === true) {
|
|
307
|
+
parts.push("network access");
|
|
308
|
+
}
|
|
309
|
+
if (isRecord(permissions.fileSystem)) {
|
|
310
|
+
const readPaths = collectStringValues(permissions.fileSystem.read);
|
|
311
|
+
const writePaths = collectStringValues(permissions.fileSystem.write);
|
|
312
|
+
if (readPaths.length > 0) {
|
|
313
|
+
parts.push(`read: ${readPaths.join(", ")}`);
|
|
314
|
+
}
|
|
315
|
+
if (writePaths.length > 0) {
|
|
316
|
+
parts.push(`write: ${writePaths.join(", ")}`);
|
|
317
|
+
}
|
|
318
|
+
if (Array.isArray(permissions.fileSystem.entries) && permissions.fileSystem.entries.length > 0) {
|
|
319
|
+
parts.push(`filesystem entries: ${permissions.fileSystem.entries.length}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return parts.length > 0 ? parts.join("; ") : "Additional permissions requested.";
|
|
323
|
+
}
|
|
324
|
+
function clonePermissionObject(value) {
|
|
325
|
+
if (!isRecord(value)) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return JSON.parse(JSON.stringify(value));
|
|
329
|
+
}
|
|
330
|
+
export function buildCodexPermissionsRequestApprovalResponse(params, action = "deny", options = {}) {
|
|
331
|
+
const permissions = {};
|
|
332
|
+
if (action === "confirm" && isRecord(params) && isRecord(params.permissions)) {
|
|
333
|
+
const network = clonePermissionObject(params.permissions.network);
|
|
334
|
+
const fileSystem = clonePermissionObject(params.permissions.fileSystem);
|
|
335
|
+
if (network) {
|
|
336
|
+
permissions.network = network;
|
|
337
|
+
}
|
|
338
|
+
if (fileSystem) {
|
|
339
|
+
permissions.fileSystem = fileSystem;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const response = {
|
|
343
|
+
permissions,
|
|
344
|
+
scope: "turn",
|
|
345
|
+
};
|
|
346
|
+
if (options.strictAutoReview) {
|
|
347
|
+
response.strictAutoReview = true;
|
|
348
|
+
}
|
|
349
|
+
return response;
|
|
350
|
+
}
|
|
351
|
+
export function buildCodexMcpServerElicitationDeclineResponse() {
|
|
352
|
+
return {
|
|
353
|
+
action: "decline",
|
|
354
|
+
content: null,
|
|
355
|
+
_meta: null,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
export function buildCodexDynamicToolCallFailureResponse() {
|
|
359
|
+
return {
|
|
360
|
+
contentItems: [
|
|
361
|
+
{
|
|
362
|
+
type: "inputText",
|
|
363
|
+
text: "Dynamic tool calls are not supported by the WeChat bridge.",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
success: false,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
157
369
|
export function buildCodexUserInputRequest(params) {
|
|
158
370
|
if (!isRecord(params) || !Array.isArray(params.questions)) {
|
|
159
371
|
return null;
|
|
@@ -287,6 +499,7 @@ export function shouldRecoverCodexStaleBusyState(params) {
|
|
|
287
499
|
!params.pendingTurnStart &&
|
|
288
500
|
!params.hasActiveTurn &&
|
|
289
501
|
!params.hasPendingApproval &&
|
|
502
|
+
!params.hasPendingUserInput &&
|
|
290
503
|
!params.activeTurnId);
|
|
291
504
|
}
|
|
292
505
|
export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
|
|
@@ -295,6 +508,7 @@ export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
|
|
|
295
508
|
params.activeTurnOrigin === "wechat" &&
|
|
296
509
|
!params.pendingTurnStart &&
|
|
297
510
|
!params.hasPendingApproval &&
|
|
511
|
+
!params.hasPendingUserInput &&
|
|
298
512
|
params.hasFinalOutput &&
|
|
299
513
|
!params.hasCompletedTurn &&
|
|
300
514
|
typeof params.lastActivityAtMs === "number" &&
|
|
@@ -16,10 +16,12 @@ const WECHAT_ATTACHMENT_PROMPT_PREFIX = [
|
|
|
16
16
|
"[WeChat bridge note]",
|
|
17
17
|
"Your final reply will be forwarded back to a WeChat chat.",
|
|
18
18
|
"If the user asks you to send a local file or media to WeChat and you know the local path, do not say that you lack a WeChat sending tool.",
|
|
19
|
-
"For a real send request,
|
|
19
|
+
"For a real send request, locate the original local file path and reference that path directly in the attachment block.",
|
|
20
|
+
"Do not create, copy, move, or write files into ~/.claude/channels/wechat, ~/.cli-bridge, or any outbound-attachments directory.",
|
|
21
|
+
"Do not call Write, cp, copy, xcopy, robocopy, mv, move, or shell redirection to stage outbound files for WeChat.",
|
|
20
22
|
"Put any brief visible reply text first, then end the message with exactly one trailing block like:",
|
|
21
23
|
"```wechat-attachments",
|
|
22
|
-
"file C:\\Users\\name\\Desktop\\document.
|
|
24
|
+
"file C:\\Users\\name\\Desktop\\document.docx",
|
|
23
25
|
"```",
|
|
24
26
|
"Valid kinds: image, file, video, voice.",
|
|
25
27
|
"Use `file` for PDFs and ordinary documents. Only include files you truly intend to upload.",
|
|
@@ -27,6 +29,10 @@ const WECHAT_ATTACHMENT_PROMPT_PREFIX = [
|
|
|
27
29
|
"[User request]",
|
|
28
30
|
].join("\n");
|
|
29
31
|
const WECHAT_ATTACHMENT_BLOCK_RE = /\n```wechat-attachments[ \t]*\n([\s\S]*?)\n```[ \t]*$/;
|
|
32
|
+
export const WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = "The WeChat bridge does not use outbound attachment directories. Do not create or copy files under .claude/channels/wechat/outbound-attachments or .cli-bridge/outbound-attachments. To send a file, put the original absolute local file path in the final ```wechat-attachments``` block.";
|
|
33
|
+
const WECHAT_OUTBOUND_ATTACHMENT_PATH_RE = /(?:^|\/)(?:(?:\.claude\/channels\/wechat\/|\.cli-bridge\/)?outbound-attachments)(?:\/|$)/i;
|
|
34
|
+
const WECHAT_OUTBOUND_ATTACHMENT_WRITE_COMMAND_RE = /\b(cp|copy|copy-item|xcopy|robocopy|mv|move|move-item|mkdir|md|new-item|ni|set-content|add-content|out-file|write-output|touch)\b|>\s*["']?[^&|]*outbound-attachments/i;
|
|
35
|
+
const WECHAT_OUTBOUND_ATTACHMENT_MUTATION_TOOL_RE = /^(?:write|edit|multiedit|notebookedit|patch|create|mkdir|move|copy|file[_-]?change|external_directory)$/i;
|
|
30
36
|
const WECHAT_ATTACHMENT_KINDS = ["image", "file", "video", "voice"];
|
|
31
37
|
const INLINE_IMAGE_EXTENSIONS = new Set([
|
|
32
38
|
".png",
|
|
@@ -211,6 +217,43 @@ export function shouldInjectWechatAttachmentPrompt(text) {
|
|
|
211
217
|
mentionsLocalPath ||
|
|
212
218
|
looksLikeShortSendCommand);
|
|
213
219
|
}
|
|
220
|
+
export function containsWechatOutboundAttachmentPath(value) {
|
|
221
|
+
if (typeof value !== "string") {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const normalized = value.trim().replace(/\\/g, "/");
|
|
225
|
+
return WECHAT_OUTBOUND_ATTACHMENT_PATH_RE.test(normalized);
|
|
226
|
+
}
|
|
227
|
+
export function containsWechatOutboundAttachmentPathDeep(value) {
|
|
228
|
+
const seen = new WeakSet();
|
|
229
|
+
const visit = (candidate) => {
|
|
230
|
+
if (containsWechatOutboundAttachmentPath(candidate)) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (!candidate || typeof candidate !== "object") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (seen.has(candidate)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
seen.add(candidate);
|
|
240
|
+
if (Array.isArray(candidate)) {
|
|
241
|
+
return candidate.some((item) => visit(item));
|
|
242
|
+
}
|
|
243
|
+
return Object.values(candidate).some((item) => visit(item));
|
|
244
|
+
};
|
|
245
|
+
return visit(value);
|
|
246
|
+
}
|
|
247
|
+
export function isWechatOutboundAttachmentWriteCommand(command) {
|
|
248
|
+
return (typeof command === "string" &&
|
|
249
|
+
containsWechatOutboundAttachmentPath(command) &&
|
|
250
|
+
WECHAT_OUTBOUND_ATTACHMENT_WRITE_COMMAND_RE.test(command));
|
|
251
|
+
}
|
|
252
|
+
export function isWechatOutboundAttachmentMutationTool(toolName, targetPath) {
|
|
253
|
+
return (typeof toolName === "string" &&
|
|
254
|
+
WECHAT_OUTBOUND_ATTACHMENT_MUTATION_TOOL_RE.test(toolName.trim()) &&
|
|
255
|
+
containsWechatOutboundAttachmentPath(targetPath));
|
|
256
|
+
}
|
|
214
257
|
function formatPromptByteSize(bytes) {
|
|
215
258
|
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
|
|
216
259
|
return null;
|
|
@@ -356,7 +399,11 @@ const HIGH_RISK_PATTERNS = [
|
|
|
356
399
|
/\b(?:invoke-expression|iex)\b/i,
|
|
357
400
|
/\bcurl\b.*\|\s*(?:iex|powershell)\b/i,
|
|
358
401
|
/\binvoke-webrequest\b.*\|\s*(?:iex|powershell)\b/i,
|
|
402
|
+
/\brm\b\s+/i,
|
|
359
403
|
/\brm\b\s+-[A-Za-z-]*r[A-Za-z-]*/i,
|
|
404
|
+
/\bfind\b[^\r\n]*\s-delete\b/i,
|
|
405
|
+
/\bfind\b[^\r\n]*\s-exec\s+(?:rm|rmdir|del|erase|remove-item)\b/i,
|
|
406
|
+
/\bxargs\b[^\r\n]*\b(?:rm|rmdir|del|erase|remove-item)\b/i,
|
|
360
407
|
/\bsudo\b/i,
|
|
361
408
|
/\bmkfs(?:\.\w+)?\b/i,
|
|
362
409
|
/\bdd\b/i,
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { normalizeOutput, truncatePreview } from "./bridge-utils.js";
|
|
1
|
+
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, isHighRiskShellCommand, isWechatOutboundAttachmentWriteCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
|
|
2
|
+
export const CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
3
|
+
const FILE_MUTATION_TOOL_NAMES = new Set([
|
|
4
|
+
"Write",
|
|
5
|
+
"Edit",
|
|
6
|
+
"MultiEdit",
|
|
7
|
+
"NotebookEdit",
|
|
8
|
+
]);
|
|
9
|
+
const LOW_RISK_READ_TOOL_NAMES = new Set([
|
|
10
|
+
"Glob",
|
|
11
|
+
"Grep",
|
|
12
|
+
"LS",
|
|
13
|
+
"NotebookRead",
|
|
14
|
+
"Read",
|
|
15
|
+
"TodoRead",
|
|
16
|
+
]);
|
|
2
17
|
function quoteWindowsCommandArg(value) {
|
|
3
18
|
return `"${value.replace(/"/g, '""')}"`;
|
|
4
19
|
}
|
|
@@ -166,14 +181,60 @@ export function buildClaudePermissionApprovalRequest(payload) {
|
|
|
166
181
|
detailPreview,
|
|
167
182
|
};
|
|
168
183
|
}
|
|
169
|
-
|
|
184
|
+
function getClaudeToolInputString(payload, key) {
|
|
185
|
+
const value = payload.tool_input?.[key];
|
|
186
|
+
return typeof value === "string" ? value : "";
|
|
187
|
+
}
|
|
188
|
+
export function getClaudeWechatOutboundAttachmentDenyMessage(payload) {
|
|
189
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
|
|
190
|
+
if (!toolName) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (FILE_MUTATION_TOOL_NAMES.has(toolName)) {
|
|
194
|
+
const filePath = getClaudeToolInputString(payload, "file_path");
|
|
195
|
+
return containsWechatOutboundAttachmentPath(filePath)
|
|
196
|
+
? CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
|
|
197
|
+
: null;
|
|
198
|
+
}
|
|
199
|
+
if (toolName === "Bash") {
|
|
200
|
+
const command = getClaudeToolInputString(payload, "command");
|
|
201
|
+
if (isWechatOutboundAttachmentWriteCommand(command)) {
|
|
202
|
+
return CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
export function getClaudePermissionAutoResponse(payload) {
|
|
208
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
|
|
209
|
+
if (!toolName) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (LOW_RISK_READ_TOOL_NAMES.has(toolName)) {
|
|
213
|
+
return {
|
|
214
|
+
action: "confirm",
|
|
215
|
+
reason: `low-risk ${toolName} permission`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (toolName !== "Bash") {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const command = getClaudeToolInputString(payload, "command").trim();
|
|
222
|
+
if (!command || isHighRiskShellCommand(command)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
action: "confirm",
|
|
227
|
+
reason: `low-risk command ${truncatePreview(command, 120)}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function buildClaudePermissionDecisionHookOutput(action, denyMessage = "Permission denied from WeChat bridge.") {
|
|
170
231
|
const decision = action === "confirm"
|
|
171
232
|
? {
|
|
172
233
|
behavior: "allow",
|
|
173
234
|
}
|
|
174
235
|
: {
|
|
175
236
|
behavior: "deny",
|
|
176
|
-
message:
|
|
237
|
+
message: denyMessage,
|
|
177
238
|
interrupt: false,
|
|
178
239
|
};
|
|
179
240
|
return JSON.stringify({
|
|
@@ -8,7 +8,7 @@ import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
|
8
8
|
import { BridgeStateStore } from "./bridge-state.js";
|
|
9
9
|
import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
|
|
10
10
|
import { createRuntimeHost } from "../runtime/create-runtime-host.js";
|
|
11
|
-
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
11
|
+
import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
|
|
12
12
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
|
|
13
13
|
import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
|
|
14
14
|
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
@@ -111,6 +111,15 @@ function toPendingApproval(request) {
|
|
|
111
111
|
createdAt: nowIso(),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
function toPendingUserInput(request) {
|
|
115
|
+
if (typeof request.createdAt === "string") {
|
|
116
|
+
return request;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
...request,
|
|
120
|
+
createdAt: nowIso(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
114
123
|
export function shouldDeferCodexInboundMessage(params) {
|
|
115
124
|
return (params.adapter === "codex" &&
|
|
116
125
|
!params.hasPendingConfirmation &&
|
|
@@ -122,11 +131,13 @@ export function canDrainDeferredCodexInboundQueue(params) {
|
|
|
122
131
|
return (params.adapter === "codex" &&
|
|
123
132
|
params.deferredCount > 0 &&
|
|
124
133
|
!params.hasPendingConfirmation &&
|
|
134
|
+
!params.hasPendingUserInput &&
|
|
125
135
|
!params.hasPendingApproval &&
|
|
126
136
|
!params.hasActiveTask &&
|
|
127
137
|
!params.activeTurnId &&
|
|
128
138
|
params.status !== "busy" &&
|
|
129
|
-
params.status !== "awaiting_approval"
|
|
139
|
+
params.status !== "awaiting_approval" &&
|
|
140
|
+
params.status !== "awaiting_input");
|
|
130
141
|
}
|
|
131
142
|
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
132
143
|
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
@@ -394,6 +405,7 @@ async function main() {
|
|
|
394
405
|
status: adapterState.status,
|
|
395
406
|
activeTurnId: adapterState.activeTurnId,
|
|
396
407
|
hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
|
|
408
|
+
hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
|
|
397
409
|
hasPendingApproval: Boolean(adapterState.pendingApproval),
|
|
398
410
|
hasActiveTask: Boolean(activeTask),
|
|
399
411
|
})) {
|
|
@@ -717,6 +729,9 @@ function wireAdapterEvents(params) {
|
|
|
717
729
|
if (bridgeState.pendingConfirmation && !adapterState.pendingApproval) {
|
|
718
730
|
stateStore.clearPendingConfirmation();
|
|
719
731
|
}
|
|
732
|
+
if (bridgeState.pendingUserInput && !adapterState.pendingUserInput) {
|
|
733
|
+
stateStore.clearPendingUserInput();
|
|
734
|
+
}
|
|
720
735
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
721
736
|
switch (event.type) {
|
|
722
737
|
case "stdout":
|
|
@@ -775,6 +790,14 @@ function wireAdapterEvents(params) {
|
|
|
775
790
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
776
791
|
}));
|
|
777
792
|
break;
|
|
793
|
+
case "user_input_required":
|
|
794
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
795
|
+
const pending = toPendingUserInput(event.request);
|
|
796
|
+
stateStore.setPendingUserInput(pending);
|
|
797
|
+
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
798
|
+
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
799
|
+
}));
|
|
800
|
+
break;
|
|
778
801
|
case "mirrored_user_input":
|
|
779
802
|
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
|
|
780
803
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
|
|
@@ -813,6 +836,7 @@ function wireAdapterEvents(params) {
|
|
|
813
836
|
case "task_complete":
|
|
814
837
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
815
838
|
stateStore.clearPendingConfirmation();
|
|
839
|
+
stateStore.clearPendingUserInput();
|
|
816
840
|
if (options.adapter === "shell") {
|
|
817
841
|
const summary = buildCompletionSummary({
|
|
818
842
|
adapter: options.adapter,
|
|
@@ -829,6 +853,7 @@ function wireAdapterEvents(params) {
|
|
|
829
853
|
case "task_failed":
|
|
830
854
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
831
855
|
stateStore.clearPendingConfirmation();
|
|
856
|
+
stateStore.clearPendingUserInput();
|
|
832
857
|
clearActiveTask();
|
|
833
858
|
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
|
|
834
859
|
await maybeDrainDeferredInboundMessages();
|
|
@@ -838,6 +863,7 @@ function wireAdapterEvents(params) {
|
|
|
838
863
|
logError(event.message);
|
|
839
864
|
stateStore.appendLog(`fatal_error: ${event.message}`);
|
|
840
865
|
stateStore.clearPendingConfirmation();
|
|
866
|
+
stateStore.clearPendingUserInput();
|
|
841
867
|
clearActiveTask();
|
|
842
868
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
843
869
|
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
|
|
@@ -914,6 +940,7 @@ async function handleInboundMessage(params) {
|
|
|
914
940
|
await outputBatcher.flushNow();
|
|
915
941
|
outputBatcher.clear();
|
|
916
942
|
stateStore.clearPendingConfirmation();
|
|
943
|
+
stateStore.clearPendingUserInput();
|
|
917
944
|
stateStore.clearSharedSessionId();
|
|
918
945
|
await adapter.createSession();
|
|
919
946
|
stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
|
|
@@ -930,6 +957,7 @@ async function handleInboundMessage(params) {
|
|
|
930
957
|
await outputBatcher.flushNow();
|
|
931
958
|
outputBatcher.clear();
|
|
932
959
|
stateStore.clearPendingConfirmation();
|
|
960
|
+
stateStore.clearPendingUserInput();
|
|
933
961
|
stateStore.clearSharedSessionId();
|
|
934
962
|
await adapter.reset();
|
|
935
963
|
stateStore.appendLog("Worker reset by owner.");
|
|
@@ -974,11 +1002,39 @@ async function handleInboundMessage(params) {
|
|
|
974
1002
|
await queueWechatMessage(message.senderId, "Approval denied.");
|
|
975
1003
|
return null;
|
|
976
1004
|
}
|
|
1005
|
+
case "answer": {
|
|
1006
|
+
const pending = state.pendingUserInput;
|
|
1007
|
+
if (!pending) {
|
|
1008
|
+
await queueWechatMessage(message.senderId, "No pending user input request.");
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
const parsed = parsePendingUserInputAnswerCommand(systemCommand.raw, pending);
|
|
1012
|
+
if ("error" in parsed) {
|
|
1013
|
+
await queueWechatMessage(message.senderId, parsed.error);
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const submitted = await adapter.submitUserInput(parsed.answers);
|
|
1017
|
+
if (!submitted) {
|
|
1018
|
+
await queueWechatMessage(message.senderId, "The worker could not apply this answer.");
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
stateStore.clearPendingUserInput();
|
|
1022
|
+
stateStore.appendLog(`User input answered: ${parsed.preview}`);
|
|
1023
|
+
await queueWechatMessage(message.senderId, "Answer submitted. Continuing...");
|
|
1024
|
+
return {
|
|
1025
|
+
startedAt: Date.now(),
|
|
1026
|
+
inputPreview: parsed.preview,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
977
1029
|
}
|
|
978
1030
|
if (state.pendingConfirmation) {
|
|
979
1031
|
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
|
|
980
1032
|
return null;
|
|
981
1033
|
}
|
|
1034
|
+
if (state.pendingUserInput) {
|
|
1035
|
+
await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
982
1038
|
const adapterState = adapter.getState();
|
|
983
1039
|
if (shouldDeferCodexInboundMessage({
|
|
984
1040
|
adapter: options.adapter,
|
|
@@ -703,7 +703,7 @@ class WechatDaemon {
|
|
|
703
703
|
const pending = toPendingUserInput(event.request);
|
|
704
704
|
slot.pendingUserInput = pending;
|
|
705
705
|
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
706
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "
|
|
706
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
707
707
|
}));
|
|
708
708
|
break;
|
|
709
709
|
case "mirrored_user_input":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "cli-wechat-bridge",
|
|
3
|
-
"version": "1.0.
|
|
2
|
+
"name": "cli-wechat-bridge",
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"lint:fix": "eslint bin src test --fix",
|
|
56
56
|
"typecheck:src": "tsc -p tsconfig.typecheck.src.json --noEmit --pretty false",
|
|
57
57
|
"quality": "npm run lint && npm run typecheck:src && bun test test && npm run build",
|
|
58
|
-
"smoke:global": "node scripts/smoke-global-install.mjs",
|
|
59
|
-
"publish:dual": "node scripts/publish-dual.mjs",
|
|
58
|
+
"smoke:global": "node scripts/smoke-global-install.mjs",
|
|
59
|
+
"publish:dual": "node scripts/publish-dual.mjs",
|
|
60
60
|
"setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
|
|
61
61
|
"start": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts",
|
|
62
62
|
"check": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts --check",
|