@unlinearity/cli-wechat-bridge 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/bridge-adapters.claude.js +186 -5
- package/dist/bridge/bridge-adapters.codex.js +58 -24
- package/dist/bridge/bridge-adapters.core.js +39 -5
- package/dist/bridge/bridge-adapters.opencode.js +6 -2
- package/dist/bridge/bridge-adapters.shared.js +101 -1
- package/dist/bridge/bridge-adapters.shell.js +1 -1
- package/dist/bridge/bridge-utils.js +36 -12
- package/dist/bridge/claude-hooks.js +9 -16
- package/dist/bridge/wechat-bridge.js +59 -10
- package/dist/companion/local-companion.js +1 -0
- package/dist/daemon/emoji-bindings.js +31 -0
- package/dist/daemon/wechat-daemon.js +56 -75
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/messages-en.js +48 -0
- package/dist/i18n/messages-zh.js +48 -0
- package/dist/runtime/legacy-adapter-runtime.js +3 -0
- package/dist/wechat/channel-config.js +2 -0
- package/package.json +1 -1
|
@@ -3,9 +3,10 @@ import net from "node:net";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
6
7
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
7
8
|
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
8
|
-
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
9
|
+
import { detectCliApproval, isThinkingForwardEnabled, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
9
10
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
10
11
|
import * as shared from "./bridge-adapters.shared.js";
|
|
11
12
|
const { CLAUDE_HOOK_LISTEN_HOST, CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS, DEFAULT_COLS, DEFAULT_ROWS, MODULE_DIR, buildClaudeCliArgs, delay, isClaudeInvalidResumeError, quotePosixCommandArg, quoteWindowsCommandArg, shouldIncludeClaudeNoAltScreen, } = shared;
|
|
@@ -90,6 +91,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
90
91
|
localTerminalInputListener = null;
|
|
91
92
|
resizeListener = null;
|
|
92
93
|
settingsFilePath = null;
|
|
94
|
+
hookErrorLogPath = null;
|
|
95
|
+
hookReceivedCount = 0;
|
|
96
|
+
hookHealthCheckTimer = null;
|
|
93
97
|
pendingHookApprovals = new Map();
|
|
94
98
|
recoveringInvalidResume = false;
|
|
95
99
|
workingNoticeTimer = null;
|
|
@@ -98,6 +102,10 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
98
102
|
lastCompactCompletionAtMs = 0;
|
|
99
103
|
startupOutputBuffer = "";
|
|
100
104
|
hasAutoConfirmedWorkspaceTrustPrompt = false;
|
|
105
|
+
lastAutoApprovedPayload = null;
|
|
106
|
+
transcriptPollTimer = null;
|
|
107
|
+
transcriptTailOffset = 0;
|
|
108
|
+
polledTranscriptSessionId = null;
|
|
101
109
|
constructor(options) {
|
|
102
110
|
super(options);
|
|
103
111
|
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
@@ -163,7 +171,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
163
171
|
throw new Error("claude is still working. Wait for the current reply or use /stop.");
|
|
164
172
|
}
|
|
165
173
|
if (this.pendingApproval) {
|
|
166
|
-
throw new Error("A Claude approval request is pending. Reply with /confirm
|
|
174
|
+
throw new Error("A Claude approval request is pending. Reply with /confirm or /deny.");
|
|
167
175
|
}
|
|
168
176
|
const normalizedText = normalizeOutput(text).trim();
|
|
169
177
|
this.pendingInjectedInputs.push({
|
|
@@ -178,6 +186,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
178
186
|
this.pendingCliApprovalHints = null;
|
|
179
187
|
this.clearWechatWorkingNotice(true);
|
|
180
188
|
this.setStatus("busy");
|
|
189
|
+
this.startTranscriptThinkingWatch();
|
|
181
190
|
this.writeToPty(this.buildRemoteInputPayload(text));
|
|
182
191
|
await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
|
|
183
192
|
this.writeToPty("\r");
|
|
@@ -248,11 +257,34 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
248
257
|
this.writeToPty(input);
|
|
249
258
|
return true;
|
|
250
259
|
}
|
|
260
|
+
async resolveAllApprovals(action) {
|
|
261
|
+
let count = 0;
|
|
262
|
+
for (const requestId of Array.from(this.pendingHookApprovals.keys())) {
|
|
263
|
+
const pending = this.pendingHookApprovals.get(requestId);
|
|
264
|
+
if (pending) {
|
|
265
|
+
this.pendingHookApprovals.delete(requestId);
|
|
266
|
+
this.respondToClaudeHook(pending.socket, requestId, buildClaudePermissionDecisionHookOutput(action));
|
|
267
|
+
count++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (count > 0) {
|
|
271
|
+
this.clearWechatWorkingNotice();
|
|
272
|
+
this.pendingCliApprovalHints = null;
|
|
273
|
+
this.pendingApproval = null;
|
|
274
|
+
this.state.pendingApproval = null;
|
|
275
|
+
this.state.pendingApprovalOrigin = undefined;
|
|
276
|
+
this.setStatus("busy");
|
|
277
|
+
return count;
|
|
278
|
+
}
|
|
279
|
+
const ok = await this.resolveApproval(action);
|
|
280
|
+
return ok ? 1 : 0;
|
|
281
|
+
}
|
|
251
282
|
async dispose() {
|
|
252
283
|
this.detachLocalTerminal();
|
|
253
284
|
this.clearWechatWorkingNotice(true);
|
|
254
285
|
this.pendingCliApprovalHints = null;
|
|
255
286
|
this.flushPendingClaudeHookApprovals();
|
|
287
|
+
this.stopTranscriptThinkingWatch();
|
|
256
288
|
await super.dispose();
|
|
257
289
|
await this.stopHookServer();
|
|
258
290
|
}
|
|
@@ -271,6 +303,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
271
303
|
afterStart() {
|
|
272
304
|
this.attachLocalTerminal();
|
|
273
305
|
this.resizePtyToTerminal();
|
|
306
|
+
this.startHookHealthCheck();
|
|
274
307
|
}
|
|
275
308
|
handleData(rawText) {
|
|
276
309
|
this.renderLocalOutput(rawText);
|
|
@@ -447,12 +480,15 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
447
480
|
const hookEntryPath = fs.existsSync(sourceHookEntryPath)
|
|
448
481
|
? sourceHookEntryPath
|
|
449
482
|
: path.join(MODULE_DIR, "claude-hook.js");
|
|
483
|
+
const hookErrorLogPath = path.join(runtimeDir, "hook-error.log");
|
|
484
|
+
this.hookErrorLogPath = hookErrorLogPath;
|
|
450
485
|
fs.writeFileSync(hookScriptPath, buildClaudeHookScript({
|
|
451
486
|
platform: process.platform,
|
|
452
487
|
runtimeExecPath: process.execPath,
|
|
453
488
|
hookEntryPath,
|
|
454
489
|
hookPort: this.hookPort,
|
|
455
490
|
hookToken: this.hookToken,
|
|
491
|
+
hookErrorLogPath,
|
|
456
492
|
}), "utf8");
|
|
457
493
|
if (process.platform !== "win32") {
|
|
458
494
|
fs.chmodSync(hookScriptPath, 0o755);
|
|
@@ -501,7 +537,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
501
537
|
return;
|
|
502
538
|
}
|
|
503
539
|
try {
|
|
504
|
-
this.pty.resize(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
|
|
540
|
+
this.pty.resize?.(process.stdout.columns || DEFAULT_COLS, process.stdout.rows || DEFAULT_ROWS);
|
|
505
541
|
}
|
|
506
542
|
catch {
|
|
507
543
|
// Best effort resize sync.
|
|
@@ -547,6 +583,34 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
547
583
|
this.workingNoticeSent = false;
|
|
548
584
|
}
|
|
549
585
|
}
|
|
586
|
+
startHookHealthCheck() {
|
|
587
|
+
this.hookReceivedCount = 0;
|
|
588
|
+
this.clearHookHealthCheck();
|
|
589
|
+
this.hookHealthCheckTimer = setTimeout(() => {
|
|
590
|
+
this.hookHealthCheckTimer = null;
|
|
591
|
+
if (this.hookReceivedCount === 0 && !this.shuttingDown) {
|
|
592
|
+
const logHint = this.hookErrorLogPath
|
|
593
|
+
? t("hook.healthCheck.logHint", { logPath: this.hookErrorLogPath })
|
|
594
|
+
: "";
|
|
595
|
+
this.emit({
|
|
596
|
+
type: "stdout",
|
|
597
|
+
text: [
|
|
598
|
+
t("hook.healthCheck.warning"),
|
|
599
|
+
logHint,
|
|
600
|
+
t("hook.healthCheck.fixes"),
|
|
601
|
+
].filter(Boolean).join("\n"),
|
|
602
|
+
timestamp: nowIso(),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}, 15_000);
|
|
606
|
+
this.hookHealthCheckTimer.unref?.();
|
|
607
|
+
}
|
|
608
|
+
clearHookHealthCheck() {
|
|
609
|
+
if (this.hookHealthCheckTimer) {
|
|
610
|
+
clearTimeout(this.hookHealthCheckTimer);
|
|
611
|
+
this.hookHealthCheckTimer = null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
550
614
|
emitClaudeNotice(text, level = "info") {
|
|
551
615
|
const normalized = normalizeOutput(text).trim();
|
|
552
616
|
if (!normalized) {
|
|
@@ -668,6 +732,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
668
732
|
this.currentPreview = "(idle)";
|
|
669
733
|
}
|
|
670
734
|
handleClaudeHookEnvelope(params) {
|
|
735
|
+
this.hookReceivedCount++;
|
|
736
|
+
this.clearHookHealthCheck();
|
|
671
737
|
const payload = parseClaudeHookPayload(params.rawPayload);
|
|
672
738
|
if (!payload?.hook_event_name) {
|
|
673
739
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
@@ -686,8 +752,32 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
686
752
|
this.handleClaudePermissionRequest(params.requestId, payload, params.socket);
|
|
687
753
|
return;
|
|
688
754
|
case "Notification":
|
|
689
|
-
if (payload.notification_type === "permission_prompt"
|
|
690
|
-
this.
|
|
755
|
+
if (payload.notification_type === "permission_prompt") {
|
|
756
|
+
if (this.pendingApproval) {
|
|
757
|
+
this.setStatus("awaiting_approval", "Claude approval is required.");
|
|
758
|
+
}
|
|
759
|
+
else if (this.lastAutoApprovedPayload) {
|
|
760
|
+
// Auto-approval response failed to reach Claude Code (socket closed before delivery).
|
|
761
|
+
// Fall back to forwarding the approval request to WeChat.
|
|
762
|
+
const fallbackPayload = this.lastAutoApprovedPayload;
|
|
763
|
+
this.lastAutoApprovedPayload = null;
|
|
764
|
+
const request = buildClaudePermissionApprovalRequest(fallbackPayload);
|
|
765
|
+
this.pendingApproval = {
|
|
766
|
+
...request,
|
|
767
|
+
requestId: undefined,
|
|
768
|
+
confirmInput: this.pendingCliApprovalHints?.confirmInput,
|
|
769
|
+
denyInput: this.pendingCliApprovalHints?.denyInput,
|
|
770
|
+
};
|
|
771
|
+
this.pendingCliApprovalHints = null;
|
|
772
|
+
this.state.pendingApproval = this.pendingApproval;
|
|
773
|
+
this.state.pendingApprovalOrigin = this.state.activeTurnOrigin;
|
|
774
|
+
this.setStatus("awaiting_approval", "Claude auto-approval failed, forwarding to WeChat.");
|
|
775
|
+
this.emit({
|
|
776
|
+
type: "approval_required",
|
|
777
|
+
request: this.pendingApproval,
|
|
778
|
+
timestamp: nowIso(),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
691
781
|
}
|
|
692
782
|
this.respondToClaudeHook(params.socket, params.requestId);
|
|
693
783
|
return;
|
|
@@ -740,6 +830,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
740
830
|
this.state.resumeConversationId = nextResumeConversationId ?? undefined;
|
|
741
831
|
this.transcriptPath = nextTranscriptPath;
|
|
742
832
|
this.state.transcriptPath = nextTranscriptPath ?? undefined;
|
|
833
|
+
this.startTranscriptThinkingWatch();
|
|
743
834
|
if (previousRuntimeSessionId === payload.session_id) {
|
|
744
835
|
return;
|
|
745
836
|
}
|
|
@@ -778,6 +869,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
778
869
|
this.pendingCliApprovalHints = null;
|
|
779
870
|
this.clearWechatWorkingNotice(true);
|
|
780
871
|
this.setStatus("busy");
|
|
872
|
+
this.startTranscriptThinkingWatch();
|
|
781
873
|
this.emit({
|
|
782
874
|
type: "mirrored_user_input",
|
|
783
875
|
text: prompt,
|
|
@@ -817,6 +909,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
817
909
|
}
|
|
818
910
|
handleClaudePermissionRequest(requestId, payload, socket) {
|
|
819
911
|
this.clearWechatWorkingNotice();
|
|
912
|
+
this.lastAutoApprovedPayload = null;
|
|
820
913
|
const denyMessage = getClaudeWechatOutboundAttachmentDenyMessage(payload);
|
|
821
914
|
if (denyMessage) {
|
|
822
915
|
this.pendingApproval = null;
|
|
@@ -833,6 +926,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
833
926
|
if (!this.pendingApproval && !this.state.pendingApproval) {
|
|
834
927
|
this.setStatus("busy", `Claude approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
835
928
|
}
|
|
929
|
+
this.lastAutoApprovedPayload = payload;
|
|
836
930
|
this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput(autoResponse.action));
|
|
837
931
|
return;
|
|
838
932
|
}
|
|
@@ -898,6 +992,7 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
898
992
|
handleClaudeStop(payload) {
|
|
899
993
|
this.clearWechatWorkingNotice(true);
|
|
900
994
|
this.pendingCliApprovalHints = null;
|
|
995
|
+
this.lastAutoApprovedPayload = null;
|
|
901
996
|
this.flushPendingClaudeHookApprovals();
|
|
902
997
|
this.pendingApproval = null;
|
|
903
998
|
this.state.pendingApproval = null;
|
|
@@ -915,11 +1010,97 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
915
1010
|
summary: this.currentPreview,
|
|
916
1011
|
timestamp: nowIso(),
|
|
917
1012
|
});
|
|
1013
|
+
this.stopTranscriptThinkingWatch();
|
|
918
1014
|
this.currentPreview = "(idle)";
|
|
919
1015
|
}
|
|
920
1016
|
handleClaudeStopFailure(payload) {
|
|
1017
|
+
this.lastAutoApprovedPayload = null;
|
|
1018
|
+
this.stopTranscriptThinkingWatch();
|
|
921
1019
|
this.failClaudeTurn(buildClaudeFailureMessage(payload));
|
|
922
1020
|
}
|
|
1021
|
+
startTranscriptThinkingWatch() {
|
|
1022
|
+
this.stopTranscriptThinkingWatch();
|
|
1023
|
+
if (!this.transcriptPath) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (!isThinkingForwardEnabled()) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
this.transcriptTailOffset = 0;
|
|
1030
|
+
try {
|
|
1031
|
+
const stat = fs.statSync(this.transcriptPath);
|
|
1032
|
+
this.transcriptTailOffset = stat.size;
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const watchPath = this.transcriptPath;
|
|
1038
|
+
const sessionId = this.runtimeSessionId;
|
|
1039
|
+
this.polledTranscriptSessionId = sessionId;
|
|
1040
|
+
this.transcriptPollTimer = setInterval(() => {
|
|
1041
|
+
if (this.shuttingDown) {
|
|
1042
|
+
this.stopTranscriptThinkingWatch();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (this.polledTranscriptSessionId !== this.runtimeSessionId) {
|
|
1046
|
+
this.stopTranscriptThinkingWatch();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
const stat = fs.statSync(watchPath);
|
|
1051
|
+
if (stat.size <= this.transcriptTailOffset) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const fd = fs.openSync(watchPath, "r");
|
|
1055
|
+
const buf = Buffer.alloc(stat.size - this.transcriptTailOffset);
|
|
1056
|
+
fs.readSync(fd, buf, 0, buf.length, this.transcriptTailOffset);
|
|
1057
|
+
fs.closeSync(fd);
|
|
1058
|
+
this.transcriptTailOffset = stat.size;
|
|
1059
|
+
const newText = buf.toString("utf8");
|
|
1060
|
+
const lines = newText.split(/\r?\n/).filter(Boolean);
|
|
1061
|
+
for (const line of lines) {
|
|
1062
|
+
let parsed;
|
|
1063
|
+
try {
|
|
1064
|
+
parsed = JSON.parse(line);
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (!parsed ||
|
|
1070
|
+
parsed.type !== "assistant" ||
|
|
1071
|
+
!Array.isArray(parsed.message?.content)) {
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
for (const block of parsed.message.content) {
|
|
1075
|
+
if (block.type === "thinking" &&
|
|
1076
|
+
typeof block.thinking === "string" &&
|
|
1077
|
+
block.thinking.trim()) {
|
|
1078
|
+
const thinking = normalizeOutput(block.thinking).trim();
|
|
1079
|
+
if (thinking) {
|
|
1080
|
+
this.emit({
|
|
1081
|
+
type: "thinking",
|
|
1082
|
+
text: thinking,
|
|
1083
|
+
timestamp: nowIso(),
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// File may be temporarily locked or deleted; silently retry next poll.
|
|
1092
|
+
}
|
|
1093
|
+
}, 800);
|
|
1094
|
+
this.transcriptPollTimer.unref?.();
|
|
1095
|
+
}
|
|
1096
|
+
stopTranscriptThinkingWatch() {
|
|
1097
|
+
if (this.transcriptPollTimer) {
|
|
1098
|
+
clearInterval(this.transcriptPollTimer);
|
|
1099
|
+
this.transcriptPollTimer = null;
|
|
1100
|
+
}
|
|
1101
|
+
this.transcriptTailOffset = 0;
|
|
1102
|
+
this.polledTranscriptSessionId = null;
|
|
1103
|
+
}
|
|
923
1104
|
respondToClaudeHook(socket, requestId, stdout) {
|
|
924
1105
|
try {
|
|
925
1106
|
socket.end(`${JSON.stringify({ requestId, stdout })}\n`);
|
|
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import { spawn as spawnChild } from "node:child_process";
|
|
5
5
|
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
6
6
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
7
|
+
import { killProcessTreeSync } from "./bridge-process-reaper.js";
|
|
7
8
|
import * as shared from "./bridge-adapters.shared.js";
|
|
8
9
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
9
10
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
@@ -35,7 +36,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
35
36
|
pendingTurnThreadId = null;
|
|
36
37
|
interruptPendingTurnStart = false;
|
|
37
38
|
pendingThreadFollowId = null;
|
|
38
|
-
|
|
39
|
+
pendingApprovalRequests = [];
|
|
39
40
|
pendingUserInputRequest = null;
|
|
40
41
|
queuedTurnNotifications = [];
|
|
41
42
|
queuedTurnServerRequests = [];
|
|
@@ -137,7 +138,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
137
138
|
throw new Error("codex is still working. Wait for the current reply or use /stop.");
|
|
138
139
|
}
|
|
139
140
|
if (this.pendingApproval) {
|
|
140
|
-
throw new Error("A Codex approval request is pending. Reply with /confirm
|
|
141
|
+
throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
|
|
141
142
|
}
|
|
142
143
|
if (this.startupBlocker) {
|
|
143
144
|
throw new Error("Codex is waiting for local terminal input before the session can continue.");
|
|
@@ -185,18 +186,35 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
185
186
|
return true;
|
|
186
187
|
}
|
|
187
188
|
async resolveApproval(action) {
|
|
188
|
-
if (!this.pendingApproval) {
|
|
189
|
+
if (!this.pendingApproval && this.pendingApprovalRequests.length === 0) {
|
|
189
190
|
return false;
|
|
190
191
|
}
|
|
191
|
-
if (this.
|
|
192
|
-
const request
|
|
193
|
-
|
|
192
|
+
if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
|
|
193
|
+
for (const request of this.pendingApprovalRequests) {
|
|
194
|
+
await this.respondToApprovalRequest(request, action);
|
|
195
|
+
}
|
|
194
196
|
this.clearPendingApprovalState();
|
|
195
197
|
this.setStatus("busy");
|
|
196
198
|
return true;
|
|
197
199
|
}
|
|
198
200
|
return await super.resolveApproval(action);
|
|
199
201
|
}
|
|
202
|
+
async resolveAllApprovals(action) {
|
|
203
|
+
if (this.pendingApprovalRequests.length === 0 && !this.pendingApproval) {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
if (this.pendingApprovalRequests.length > 0 && this.rpcSocket) {
|
|
207
|
+
const count = this.pendingApprovalRequests.length;
|
|
208
|
+
for (const request of this.pendingApprovalRequests) {
|
|
209
|
+
await this.respondToApprovalRequest(request, action);
|
|
210
|
+
}
|
|
211
|
+
this.clearPendingApprovalState();
|
|
212
|
+
this.setStatus("busy");
|
|
213
|
+
return count;
|
|
214
|
+
}
|
|
215
|
+
const ok = await super.resolveApproval(action);
|
|
216
|
+
return ok ? 1 : 0;
|
|
217
|
+
}
|
|
200
218
|
async submitUserInput(answers) {
|
|
201
219
|
if (!this.pendingUserInputRequest) {
|
|
202
220
|
return false;
|
|
@@ -450,7 +468,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
450
468
|
};
|
|
451
469
|
child.once("exit", () => finish());
|
|
452
470
|
try {
|
|
453
|
-
child.
|
|
471
|
+
if (child.pid) {
|
|
472
|
+
killProcessTreeSync(child.pid);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
child.kill();
|
|
476
|
+
}
|
|
454
477
|
}
|
|
455
478
|
catch {
|
|
456
479
|
finish();
|
|
@@ -783,7 +806,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
783
806
|
this.recoverStaleBusyStateIfNeeded();
|
|
784
807
|
this.recoverStaleActiveTurnStateIfNeeded();
|
|
785
808
|
if (this.pendingApproval) {
|
|
786
|
-
throw new Error("A Codex approval request is pending. Reply with /confirm
|
|
809
|
+
throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
|
|
787
810
|
}
|
|
788
811
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
789
812
|
throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
|
|
@@ -1247,7 +1270,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1247
1270
|
throw new Error("A thread id is required to resume a Codex thread.");
|
|
1248
1271
|
}
|
|
1249
1272
|
if (this.pendingApproval) {
|
|
1250
|
-
throw new Error("A Codex approval request is pending. Reply with /confirm
|
|
1273
|
+
throw new Error("A Codex approval request is pending. Reply with /confirm or /deny.");
|
|
1251
1274
|
}
|
|
1252
1275
|
if (!options.startup &&
|
|
1253
1276
|
(this.pendingTurnStart ||
|
|
@@ -1348,7 +1371,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1348
1371
|
status: this.state.status,
|
|
1349
1372
|
pendingTurnStart: this.pendingTurnStart,
|
|
1350
1373
|
hasActiveTurn: Boolean(this.activeTurn),
|
|
1351
|
-
hasPendingApproval: Boolean(this.pendingApproval || this.
|
|
1374
|
+
hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequests.length),
|
|
1352
1375
|
hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
|
|
1353
1376
|
activeTurnId: this.state.activeTurnId,
|
|
1354
1377
|
})) {
|
|
@@ -1366,7 +1389,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1366
1389
|
if (!this.activeTurn ||
|
|
1367
1390
|
this.pendingTurnStart ||
|
|
1368
1391
|
this.pendingApproval ||
|
|
1369
|
-
this.
|
|
1392
|
+
this.pendingApprovalRequests.length ||
|
|
1370
1393
|
this.state.status === "busy" ||
|
|
1371
1394
|
this.state.status === "awaiting_approval" ||
|
|
1372
1395
|
this.state.activeTurnId) {
|
|
@@ -1586,7 +1609,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1586
1609
|
return true;
|
|
1587
1610
|
}
|
|
1588
1611
|
clearPendingApprovalState() {
|
|
1589
|
-
this.
|
|
1612
|
+
this.pendingApprovalRequests = [];
|
|
1590
1613
|
this.pendingApproval = null;
|
|
1591
1614
|
this.state.pendingApproval = null;
|
|
1592
1615
|
this.state.pendingApprovalOrigin = undefined;
|
|
@@ -1850,10 +1873,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1850
1873
|
case "serverRequest/resolved": {
|
|
1851
1874
|
const requestId = getCodexRpcRequestId(params.requestId);
|
|
1852
1875
|
if (requestId !== null &&
|
|
1853
|
-
this.
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1876
|
+
this.pendingApprovalRequests.some((r) => r.requestId === requestId && r.turnId === trackedTurn.turnId)) {
|
|
1877
|
+
this.pendingApprovalRequests = this.pendingApprovalRequests.filter((r) => r.requestId !== requestId);
|
|
1878
|
+
if (this.pendingApprovalRequests.length === 0) {
|
|
1879
|
+
this.clearPendingApprovalState();
|
|
1880
|
+
}
|
|
1857
1881
|
if (this.state.status === "awaiting_approval") {
|
|
1858
1882
|
this.setStatus("busy", "Codex approval resolved.");
|
|
1859
1883
|
}
|
|
@@ -1975,14 +1999,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1975
1999
|
});
|
|
1976
2000
|
return;
|
|
1977
2001
|
}
|
|
1978
|
-
this.
|
|
2002
|
+
this.pendingApprovalRequests.push({
|
|
1979
2003
|
requestId,
|
|
1980
2004
|
method,
|
|
1981
2005
|
threadId: trackedTurn.threadId,
|
|
1982
2006
|
turnId: trackedTurn.turnId,
|
|
1983
2007
|
origin: trackedTurn.origin,
|
|
1984
2008
|
params,
|
|
1985
|
-
};
|
|
2009
|
+
});
|
|
1986
2010
|
this.pendingApproval = request;
|
|
1987
2011
|
this.state.pendingApproval = request;
|
|
1988
2012
|
this.state.pendingApprovalOrigin = trackedTurn.origin;
|
|
@@ -2146,8 +2170,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2146
2170
|
: completedTrackedTurn.origin === "local"
|
|
2147
2171
|
? "Local terminal turn completed."
|
|
2148
2172
|
: this.currentPreview;
|
|
2149
|
-
if (this.
|
|
2150
|
-
this.
|
|
2173
|
+
if (this.pendingApprovalRequests.length > 0 &&
|
|
2174
|
+
this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
|
|
2151
2175
|
this.clearPendingApprovalState();
|
|
2152
2176
|
}
|
|
2153
2177
|
if (this.pendingUserInputRequest &&
|
|
@@ -2241,7 +2265,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2241
2265
|
this.activeTurn.origin !== "wechat" ||
|
|
2242
2266
|
this.pendingTurnStart ||
|
|
2243
2267
|
this.pendingApproval ||
|
|
2244
|
-
this.
|
|
2268
|
+
this.pendingApprovalRequests.length ||
|
|
2245
2269
|
this.pendingUserInputRequest ||
|
|
2246
2270
|
this.state.pendingUserInput ||
|
|
2247
2271
|
!this.collectTurnOutput(turnId)) {
|
|
@@ -2259,7 +2283,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2259
2283
|
const activeTurn = this.activeTurn;
|
|
2260
2284
|
const finalText = this.collectTurnOutput(turnId);
|
|
2261
2285
|
const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
|
|
2262
|
-
const pendingApproval = Boolean(this.pendingApproval || this.
|
|
2286
|
+
const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequests.length);
|
|
2263
2287
|
const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
|
|
2264
2288
|
const nowMs = Date.now();
|
|
2265
2289
|
if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
|
|
@@ -2339,7 +2363,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2339
2363
|
};
|
|
2340
2364
|
child.once("exit", () => finish());
|
|
2341
2365
|
try {
|
|
2342
|
-
child.
|
|
2366
|
+
if (child.pid) {
|
|
2367
|
+
killProcessTreeSync(child.pid);
|
|
2368
|
+
}
|
|
2369
|
+
else {
|
|
2370
|
+
child.kill();
|
|
2371
|
+
}
|
|
2343
2372
|
}
|
|
2344
2373
|
catch {
|
|
2345
2374
|
finish();
|
|
@@ -2370,7 +2399,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2370
2399
|
}
|
|
2371
2400
|
if (this.nativeProcess) {
|
|
2372
2401
|
try {
|
|
2373
|
-
this.nativeProcess.
|
|
2402
|
+
if (this.nativeProcess.pid) {
|
|
2403
|
+
killProcessTreeSync(this.nativeProcess.pid);
|
|
2404
|
+
}
|
|
2405
|
+
else {
|
|
2406
|
+
this.nativeProcess.kill();
|
|
2407
|
+
}
|
|
2374
2408
|
}
|
|
2375
2409
|
catch {
|
|
2376
2410
|
// Best effort cleanup after panel client failure.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import { spawn as spawnPty } from "node-pty";
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
3
4
|
import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
4
5
|
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
5
6
|
import * as shared from "./bridge-adapters.shared.js";
|
|
6
7
|
import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
|
|
7
|
-
const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, } = shared;
|
|
8
|
+
const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, buildSpawnDiagnostic, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, spawnFallbackProcess, } = shared;
|
|
8
9
|
export class LocalCompanionProxyAdapter {
|
|
9
10
|
options;
|
|
10
11
|
state;
|
|
@@ -136,6 +137,10 @@ export class LocalCompanionProxyAdapter {
|
|
|
136
137
|
});
|
|
137
138
|
return Boolean(result);
|
|
138
139
|
}
|
|
140
|
+
async resolveAllApprovals(action) {
|
|
141
|
+
const ok = await this.resolveApproval(action);
|
|
142
|
+
return ok ? 1 : 0;
|
|
143
|
+
}
|
|
139
144
|
async submitUserInput(_answers) {
|
|
140
145
|
return false;
|
|
141
146
|
}
|
|
@@ -299,6 +304,7 @@ export class LocalCompanionProxyAdapter {
|
|
|
299
304
|
this.detachMessageListener = null;
|
|
300
305
|
if (this.socket) {
|
|
301
306
|
this.socket.removeAllListeners();
|
|
307
|
+
this.socket.on("error", () => { });
|
|
302
308
|
this.socket.destroy();
|
|
303
309
|
this.socket = null;
|
|
304
310
|
}
|
|
@@ -509,10 +515,27 @@ export class AbstractPtyAdapter {
|
|
|
509
515
|
try {
|
|
510
516
|
spawnTarget = resolveSpawnTarget(this.options.command, this.options.kind);
|
|
511
517
|
const env = this.buildEnv();
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
518
|
+
const spawnArgs = [...spawnTarget.args, ...this.buildSpawnArgs()];
|
|
519
|
+
let ptyProcess;
|
|
520
|
+
let usedFallback = false;
|
|
521
|
+
try {
|
|
522
|
+
ptyProcess = spawnPty(spawnTarget.file, spawnArgs, buildPtySpawnOptions({
|
|
523
|
+
cwd: this.options.cwd,
|
|
524
|
+
env,
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
catch (ptyErr) {
|
|
528
|
+
usedFallback = true;
|
|
529
|
+
try {
|
|
530
|
+
ptyProcess = spawnFallbackProcess(spawnTarget.file, spawnArgs, {
|
|
531
|
+
cwd: this.options.cwd,
|
|
532
|
+
env,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
catch (fallbackErr) {
|
|
536
|
+
throw new Error(buildSpawnDiagnostic(ptyErr, spawnTarget), { cause: fallbackErr });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
516
539
|
this.pty = ptyProcess;
|
|
517
540
|
this.shuttingDown = false;
|
|
518
541
|
this.hasAcceptedInput = false;
|
|
@@ -523,6 +546,13 @@ export class AbstractPtyAdapter {
|
|
|
523
546
|
ptyProcess.onData((data) => this.handleData(data));
|
|
524
547
|
ptyProcess.onExit(({ exitCode }) => this.handleExit(exitCode));
|
|
525
548
|
this.afterStart();
|
|
549
|
+
if (usedFallback) {
|
|
550
|
+
this.emit({
|
|
551
|
+
type: "stdout",
|
|
552
|
+
text: t("pty.fallback.warning"),
|
|
553
|
+
timestamp: nowIso(),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
526
556
|
this.setStatus("idle", `${this.options.kind} adapter is ready.`);
|
|
527
557
|
}
|
|
528
558
|
catch (err) {
|
|
@@ -584,6 +614,10 @@ export class AbstractPtyAdapter {
|
|
|
584
614
|
this.state.pendingApproval = null;
|
|
585
615
|
return true;
|
|
586
616
|
}
|
|
617
|
+
async resolveAllApprovals(action) {
|
|
618
|
+
const ok = await this.resolveApproval(action);
|
|
619
|
+
return ok ? 1 : 0;
|
|
620
|
+
}
|
|
587
621
|
async submitUserInput(_answers) {
|
|
588
622
|
return false;
|
|
589
623
|
}
|
|
@@ -110,7 +110,7 @@ export class OpenCodeServerAdapter {
|
|
|
110
110
|
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
111
111
|
}
|
|
112
112
|
if (this.pendingPermission) {
|
|
113
|
-
throw new Error("An OpenCode approval request is pending. Reply with /confirm
|
|
113
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
114
114
|
}
|
|
115
115
|
const normalized = normalizeOutput(text).trim();
|
|
116
116
|
if (!normalized) {
|
|
@@ -163,7 +163,7 @@ export class OpenCodeServerAdapter {
|
|
|
163
163
|
throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
|
|
164
164
|
}
|
|
165
165
|
if (this.pendingPermission) {
|
|
166
|
-
throw new Error("An OpenCode approval request is pending. Reply with /confirm
|
|
166
|
+
throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
|
|
167
167
|
}
|
|
168
168
|
this.outputBatcher.clear();
|
|
169
169
|
this.clearStreamedPartState();
|
|
@@ -254,6 +254,10 @@ export class OpenCodeServerAdapter {
|
|
|
254
254
|
this.setStatus("busy");
|
|
255
255
|
return true;
|
|
256
256
|
}
|
|
257
|
+
async resolveAllApprovals(action) {
|
|
258
|
+
const ok = await this.resolveApproval(action);
|
|
259
|
+
return ok ? 1 : 0;
|
|
260
|
+
}
|
|
257
261
|
async submitUserInput(_answers) {
|
|
258
262
|
return false;
|
|
259
263
|
}
|