cli-wechat-bridge 1.0.5 → 1.0.7
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 +126 -366
- package/dist/bridge/bridge-adapters.claude.js +127 -6
- package/dist/bridge/bridge-adapters.codex.js +219 -7
- package/dist/bridge/bridge-adapters.core.js +13 -5
- package/dist/bridge/bridge-adapters.opencode.js +143 -24
- package/dist/bridge/bridge-adapters.shared.js +217 -6
- package/dist/bridge/bridge-process-reaper.js +40 -1
- package/dist/bridge/bridge-state.js +6 -3
- package/dist/bridge/bridge-utils.js +49 -2
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +69 -3
- package/dist/commands/check-update.js +1 -1
- package/dist/companion/local-companion-start.js +27 -2
- package/dist/companion/local-companion.js +21 -4
- package/dist/daemon/wechat-daemon.js +277 -21
- package/package.json +5 -6
- package/dist/companion/codex-panel-link.js +0 -72
- package/dist/companion/codex-panel.js +0 -179
- package/dist/media/media-types.js +0 -1
- package/dist/wechat/standalone-bot.js +0 -355
|
@@ -1,16 +1,83 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import net from "node:net";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
|
|
5
6
|
import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
6
|
-
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
7
|
+
import { buildClaudeFailureMessage, buildClaudeHookScript, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeAssistantMessageText, extractClaudeResumeConversationId, extractClaudeTranscriptFinalReply, findInjectedClaudePromptIndex, getClaudePermissionAutoResponse, getClaudeWechatOutboundAttachmentDenyMessage, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
|
|
7
8
|
import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
8
9
|
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
9
10
|
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;
|
|
11
|
+
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
12
|
const CLAUDE_COMPACT_OUTPUT_LINE_RE = /^Compacted(?:\s*\(.*full summary.*\))?$/i;
|
|
12
13
|
const CLAUDE_COMPACT_FAILURE_RE = /Error:\s*Error during compaction:|(?:^|\b)API Error:|\b(?:compact|compaction)\s+failed\b|^Error:/i;
|
|
13
14
|
const CLAUDE_COMPACT_DEDUP_MS = 2_000;
|
|
15
|
+
const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
|
|
16
|
+
const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
|
|
17
|
+
const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
|
|
18
|
+
const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
|
|
19
|
+
function isClaudeWorkspaceTrustPrompt(text) {
|
|
20
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
21
|
+
return (/Accessing workspace:/i.test(compact) &&
|
|
22
|
+
/Quick safety check:/i.test(compact) &&
|
|
23
|
+
/project you created or one you trust/i.test(compact) &&
|
|
24
|
+
/I trust this folder/i.test(compact));
|
|
25
|
+
}
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
export function normalizeClaudeProjectConfigKey(cwd) {
|
|
30
|
+
return path.resolve(cwd).replace(/\\/g, "/");
|
|
31
|
+
}
|
|
32
|
+
export function ensureClaudeWorkspaceTrustAccepted(cwd, homeDir = process.env.USERPROFILE || process.env.HOME || os.homedir()) {
|
|
33
|
+
const configPath = path.join(homeDir, ".claude.json");
|
|
34
|
+
let config = {};
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(configPath)) {
|
|
37
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
38
|
+
config = raw.trim() ? JSON.parse(raw) : {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!isRecord(config)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const projectKey = normalizeClaudeProjectConfigKey(cwd);
|
|
48
|
+
const projects = isRecord(config.projects) ? config.projects : {};
|
|
49
|
+
const currentProject = isRecord(projects[projectKey])
|
|
50
|
+
? projects[projectKey]
|
|
51
|
+
: {};
|
|
52
|
+
if (currentProject.hasTrustDialogAccepted === true) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const nextConfig = {
|
|
56
|
+
...config,
|
|
57
|
+
projects: {
|
|
58
|
+
...projects,
|
|
59
|
+
[projectKey]: {
|
|
60
|
+
...currentProject,
|
|
61
|
+
hasTrustDialogAccepted: true,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
68
|
+
fs.renameSync(tempPath, configPath);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(tempPath, { force: true });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Best effort cleanup.
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
14
81
|
export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
15
82
|
hookServer = null;
|
|
16
83
|
hookPort = null;
|
|
@@ -29,11 +96,20 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
29
96
|
workingNoticeSent = false;
|
|
30
97
|
workingNoticeDelayMs = CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS;
|
|
31
98
|
lastCompactCompletionAtMs = 0;
|
|
99
|
+
startupOutputBuffer = "";
|
|
100
|
+
hasAutoConfirmedWorkspaceTrustPrompt = false;
|
|
32
101
|
constructor(options) {
|
|
33
102
|
super(options);
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
103
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
104
|
+
this.runtimeSessionId = shouldRestoreInitialSession
|
|
105
|
+
? options.initialSharedSessionId ?? options.initialSharedThreadId ?? null
|
|
106
|
+
: null;
|
|
107
|
+
this.resumeConversationId = shouldRestoreInitialSession
|
|
108
|
+
? options.initialResumeConversationId ?? null
|
|
109
|
+
: null;
|
|
110
|
+
this.transcriptPath = shouldRestoreInitialSession
|
|
111
|
+
? options.initialTranscriptPath ?? null
|
|
112
|
+
: null;
|
|
37
113
|
if (this.runtimeSessionId) {
|
|
38
114
|
this.state.sharedSessionId = this.runtimeSessionId;
|
|
39
115
|
this.state.activeRuntimeSessionId = this.runtimeSessionId;
|
|
@@ -49,6 +125,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
49
125
|
if (this.pty) {
|
|
50
126
|
return;
|
|
51
127
|
}
|
|
128
|
+
this.startupOutputBuffer = "";
|
|
129
|
+
this.hasAutoConfirmedWorkspaceTrustPrompt = false;
|
|
130
|
+
ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
|
|
52
131
|
// Validate transcript file exists before launching Claude CLI.
|
|
53
132
|
// After a compact, the old transcript is deleted and the persisted
|
|
54
133
|
// resumeConversationId becomes invalid, causing --resume to crash.
|
|
@@ -99,7 +178,8 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
99
178
|
this.pendingCliApprovalHints = null;
|
|
100
179
|
this.clearWechatWorkingNotice(true);
|
|
101
180
|
this.setStatus("busy");
|
|
102
|
-
this.writeToPty(
|
|
181
|
+
this.writeToPty(this.buildRemoteInputPayload(text));
|
|
182
|
+
await delay(CLAUDE_REMOTE_ENTER_DELAY_MS);
|
|
103
183
|
this.writeToPty("\r");
|
|
104
184
|
this.armWechatWorkingNotice();
|
|
105
185
|
}
|
|
@@ -205,6 +285,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
205
285
|
return;
|
|
206
286
|
}
|
|
207
287
|
this.state.lastOutputAt = nowIso();
|
|
288
|
+
if (this.maybeAutoConfirmWorkspaceTrustPrompt(text)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
208
291
|
if (this.shouldTreatClaudeOutputAsCompactCompletion(text)) {
|
|
209
292
|
this.completeClaudeCompact();
|
|
210
293
|
return;
|
|
@@ -237,6 +320,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
237
320
|
return;
|
|
238
321
|
}
|
|
239
322
|
}
|
|
323
|
+
maybeAutoConfirmWorkspaceTrustPrompt(text) {
|
|
324
|
+
if (this.hasAcceptedInput || this.hasAutoConfirmedWorkspaceTrustPrompt) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
this.startupOutputBuffer = `${this.startupOutputBuffer}${text}`.slice(-CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT);
|
|
328
|
+
if (!isClaudeWorkspaceTrustPrompt(this.startupOutputBuffer)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
this.hasAutoConfirmedWorkspaceTrustPrompt = true;
|
|
332
|
+
this.writeToPty("\r");
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
240
335
|
handleExit(exitCode) {
|
|
241
336
|
this.detachLocalTerminal();
|
|
242
337
|
this.clearWechatWorkingNotice(true);
|
|
@@ -464,6 +559,13 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
464
559
|
timestamp: nowIso(),
|
|
465
560
|
});
|
|
466
561
|
}
|
|
562
|
+
buildRemoteInputPayload(text) {
|
|
563
|
+
const normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
564
|
+
if (!normalizedText.includes("\n")) {
|
|
565
|
+
return normalizedText;
|
|
566
|
+
}
|
|
567
|
+
return `${CLAUDE_BRACKETED_PASTE_START}${normalizedText}${CLAUDE_BRACKETED_PASTE_END}`;
|
|
568
|
+
}
|
|
467
569
|
shouldTreatClaudeOutputAsCompactCompletion(text) {
|
|
468
570
|
if (this.state.status !== "busy" &&
|
|
469
571
|
this.state.status !== "awaiting_approval" &&
|
|
@@ -707,6 +809,25 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
|
|
|
707
809
|
}
|
|
708
810
|
handleClaudePermissionRequest(requestId, payload, socket) {
|
|
709
811
|
this.clearWechatWorkingNotice();
|
|
812
|
+
const denyMessage = getClaudeWechatOutboundAttachmentDenyMessage(payload);
|
|
813
|
+
if (denyMessage) {
|
|
814
|
+
this.pendingApproval = null;
|
|
815
|
+
this.state.pendingApproval = null;
|
|
816
|
+
this.state.pendingApprovalOrigin = undefined;
|
|
817
|
+
if (this.state.status === "awaiting_approval") {
|
|
818
|
+
this.setStatus("busy");
|
|
819
|
+
}
|
|
820
|
+
this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput("deny", denyMessage));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const autoResponse = getClaudePermissionAutoResponse(payload);
|
|
824
|
+
if (autoResponse) {
|
|
825
|
+
if (!this.pendingApproval && !this.state.pendingApproval) {
|
|
826
|
+
this.setStatus("busy", `Claude approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
827
|
+
}
|
|
828
|
+
this.respondToClaudeHook(socket, requestId, buildClaudePermissionDecisionHookOutput(autoResponse.action));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
710
831
|
this.flushPendingClaudeHookApprovals();
|
|
711
832
|
this.pendingHookApprovals.set(requestId, {
|
|
712
833
|
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,
|
|
10
|
+
const { CODEX_APP_SERVER_HOST, 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, 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;
|
|
@@ -63,7 +64,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
63
64
|
localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
|
|
64
65
|
constructor(options) {
|
|
65
66
|
super(options);
|
|
66
|
-
this.resumeThreadId = options.
|
|
67
|
+
this.resumeThreadId = options.sessionStartMode === "new"
|
|
68
|
+
? null
|
|
69
|
+
: options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
|
|
67
70
|
if (this.resumeThreadId && options.renderMode !== "panel") {
|
|
68
71
|
this.state.sharedSessionId = this.resumeThreadId;
|
|
69
72
|
this.state.sharedThreadId = this.resumeThreadId;
|
|
@@ -194,6 +197,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
194
197
|
}
|
|
195
198
|
return await super.resolveApproval(action);
|
|
196
199
|
}
|
|
200
|
+
async submitUserInput(answers) {
|
|
201
|
+
if (!this.pendingUserInputRequest) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
const request = this.pendingUserInputRequest;
|
|
205
|
+
const responseAnswers = {};
|
|
206
|
+
for (const [questionId, values] of Object.entries(answers)) {
|
|
207
|
+
responseAnswers[questionId] = {
|
|
208
|
+
answers: values,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
this.sendRpcMessage({
|
|
212
|
+
id: request.requestId,
|
|
213
|
+
result: {
|
|
214
|
+
answers: responseAnswers,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
this.clearPendingUserInputState();
|
|
218
|
+
this.setStatus("busy", "Codex user input submitted.");
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
197
221
|
async dispose() {
|
|
198
222
|
this.resetTurnTracking({ preserveThread: false });
|
|
199
223
|
if (this.isEmbeddedCliMode()) {
|
|
@@ -752,6 +776,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
752
776
|
if (this.pendingApproval) {
|
|
753
777
|
throw new Error("A Codex approval request is pending. Reply with /confirm <code> or /deny.");
|
|
754
778
|
}
|
|
779
|
+
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
780
|
+
throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
|
|
781
|
+
}
|
|
755
782
|
if (this.pendingTurnStart || this.activeTurn || this.state.status === "busy") {
|
|
756
783
|
const origin = this.state.activeTurnOrigin;
|
|
757
784
|
if (origin === "local") {
|
|
@@ -766,6 +793,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
766
793
|
this.rememberInjectedInput(text);
|
|
767
794
|
this.clearPendingApprovalState();
|
|
768
795
|
const threadId = await this.ensureThreadStarted();
|
|
796
|
+
const subscribedBeforeTurnStart = await this.tryEnsureSharedThreadSubscribed(threadId);
|
|
769
797
|
this.pendingTurnStart = true;
|
|
770
798
|
this.pendingTurnThreadId = threadId;
|
|
771
799
|
this.interruptPendingTurnStart = false;
|
|
@@ -793,6 +821,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
793
821
|
turnId,
|
|
794
822
|
origin: "wechat",
|
|
795
823
|
});
|
|
824
|
+
if (!subscribedBeforeTurnStart) {
|
|
825
|
+
await this.tryEnsureSharedThreadSubscribed(threadId);
|
|
826
|
+
}
|
|
796
827
|
if (this.interruptPendingTurnStart) {
|
|
797
828
|
await this.requestActiveTurnInterrupt();
|
|
798
829
|
this.armInterruptFallback();
|
|
@@ -813,11 +844,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
813
844
|
if (this.isNativePanelMode() && !this.nativeProcess) {
|
|
814
845
|
return false;
|
|
815
846
|
}
|
|
816
|
-
const turnPending = this.pendingTurnStart ||
|
|
847
|
+
const turnPending = this.pendingTurnStart ||
|
|
848
|
+
this.state.status === "busy" ||
|
|
849
|
+
this.state.status === "awaiting_approval" ||
|
|
850
|
+
this.state.status === "awaiting_input";
|
|
817
851
|
if (!turnPending) {
|
|
818
852
|
return false;
|
|
819
853
|
}
|
|
820
854
|
this.clearPendingApprovalState();
|
|
855
|
+
this.clearPendingUserInputState();
|
|
821
856
|
if (this.pendingTurnStart && !this.activeTurn) {
|
|
822
857
|
this.interruptPendingTurnStart = true;
|
|
823
858
|
this.armInterruptFallback();
|
|
@@ -980,6 +1015,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
980
1015
|
attachRpcSocket(socket) {
|
|
981
1016
|
this.rpcSocket = socket;
|
|
982
1017
|
this.rpcShuttingDown = false;
|
|
1018
|
+
this.subscribedThreadIds.clear();
|
|
983
1019
|
socket.addEventListener("message", (event) => {
|
|
984
1020
|
this.handleRpcMessageData(event.data);
|
|
985
1021
|
});
|
|
@@ -991,6 +1027,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
991
1027
|
const socket = this.rpcSocket;
|
|
992
1028
|
this.rpcSocket = null;
|
|
993
1029
|
this.rpcShuttingDown = true;
|
|
1030
|
+
this.subscribedThreadIds.clear();
|
|
994
1031
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
995
1032
|
if (!socket) {
|
|
996
1033
|
this.rpcShuttingDown = false;
|
|
@@ -1028,6 +1065,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1028
1065
|
cleanPanelExitInProgress: this.cleanPanelExitInProgress,
|
|
1029
1066
|
});
|
|
1030
1067
|
this.rpcSocket = null;
|
|
1068
|
+
this.subscribedThreadIds.clear();
|
|
1031
1069
|
this.rejectPendingRpcRequests("Codex websocket connection closed.");
|
|
1032
1070
|
this.rpcShuttingDown = false;
|
|
1033
1071
|
if (expectedShutdown) {
|
|
@@ -1156,9 +1194,44 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1156
1194
|
throw new Error("Codex did not return a thread id for the bridge session.");
|
|
1157
1195
|
}
|
|
1158
1196
|
this.rememberBridgeOwnedThreadSignal(threadId);
|
|
1197
|
+
this.subscribedThreadIds.add(threadId);
|
|
1159
1198
|
this.updateSharedThread(threadId);
|
|
1160
1199
|
return threadId;
|
|
1161
1200
|
}
|
|
1201
|
+
async tryEnsureSharedThreadSubscribed(threadId) {
|
|
1202
|
+
if (this.subscribedThreadIds.has(threadId)) {
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
await this.ensureSharedThreadSubscribed(threadId);
|
|
1207
|
+
return true;
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async ensureSharedThreadSubscribed(threadId) {
|
|
1214
|
+
if (this.subscribedThreadIds.has(threadId)) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const response = await this.sendRpcRequest("thread/resume", {
|
|
1218
|
+
threadId,
|
|
1219
|
+
cwd: this.options.cwd,
|
|
1220
|
+
approvalPolicy: "on-request",
|
|
1221
|
+
approvalsReviewer: "user",
|
|
1222
|
+
sandbox: "workspace-write",
|
|
1223
|
+
excludeTurns: true,
|
|
1224
|
+
});
|
|
1225
|
+
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1226
|
+
if (!resumedThreadId) {
|
|
1227
|
+
throw new Error("Codex did not return a thread id while subscribing the bridge client.");
|
|
1228
|
+
}
|
|
1229
|
+
this.rememberBridgeOwnedThreadSignal(resumedThreadId);
|
|
1230
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1231
|
+
if (resumedThreadId !== this.sharedThreadId) {
|
|
1232
|
+
this.updateSharedThread(resumedThreadId);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1162
1235
|
async resumeSharedThread(threadId, options = {}) {
|
|
1163
1236
|
const trimmedThreadId = threadId.trim();
|
|
1164
1237
|
if (!trimmedThreadId) {
|
|
@@ -1180,12 +1253,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1180
1253
|
approvalPolicy: "on-request",
|
|
1181
1254
|
approvalsReviewer: "user",
|
|
1182
1255
|
sandbox: "workspace-write",
|
|
1256
|
+
excludeTurns: true,
|
|
1183
1257
|
});
|
|
1184
1258
|
const resumedThreadId = this.extractThreadIdFromResponse(response);
|
|
1185
1259
|
if (!resumedThreadId) {
|
|
1186
1260
|
throw new Error("Codex did not return a thread id while resuming the saved thread.");
|
|
1187
1261
|
}
|
|
1188
1262
|
this.rememberBridgeOwnedThreadSignal(resumedThreadId);
|
|
1263
|
+
this.subscribedThreadIds.add(resumedThreadId);
|
|
1189
1264
|
this.sessionFilePath = null;
|
|
1190
1265
|
this.sessionReadOffset = 0;
|
|
1191
1266
|
this.sessionPartialLine = "";
|
|
@@ -1238,7 +1313,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1238
1313
|
this.clearInterruptTimer();
|
|
1239
1314
|
this.interruptTimer = setTimeout(() => {
|
|
1240
1315
|
this.interruptTimer = null;
|
|
1241
|
-
if (this.state.status !== "busy" &&
|
|
1316
|
+
if (this.state.status !== "busy" &&
|
|
1317
|
+
this.state.status !== "awaiting_approval" &&
|
|
1318
|
+
this.state.status !== "awaiting_input") {
|
|
1242
1319
|
return;
|
|
1243
1320
|
}
|
|
1244
1321
|
this.resetTurnTracking({ preserveThread: true });
|
|
@@ -1263,6 +1340,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1263
1340
|
pendingTurnStart: this.pendingTurnStart,
|
|
1264
1341
|
hasActiveTurn: Boolean(this.activeTurn),
|
|
1265
1342
|
hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequest),
|
|
1343
|
+
hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
|
|
1266
1344
|
activeTurnId: this.state.activeTurnId,
|
|
1267
1345
|
})) {
|
|
1268
1346
|
return;
|
|
@@ -1301,6 +1379,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1301
1379
|
this.interruptPendingTurnStart = false;
|
|
1302
1380
|
this.pendingThreadFollowId = null;
|
|
1303
1381
|
this.clearPendingApprovalState();
|
|
1382
|
+
this.clearPendingUserInputState();
|
|
1304
1383
|
this.queuedTurnNotifications = [];
|
|
1305
1384
|
this.queuedTurnServerRequests = [];
|
|
1306
1385
|
this.turnFinalMessages.clear();
|
|
@@ -1503,6 +1582,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1503
1582
|
this.state.pendingApproval = null;
|
|
1504
1583
|
this.state.pendingApprovalOrigin = undefined;
|
|
1505
1584
|
}
|
|
1585
|
+
clearPendingUserInputState() {
|
|
1586
|
+
this.pendingUserInputRequest = null;
|
|
1587
|
+
this.state.pendingUserInput = null;
|
|
1588
|
+
this.state.pendingUserInputOrigin = undefined;
|
|
1589
|
+
}
|
|
1506
1590
|
cleanupTurnArtifacts(turnId) {
|
|
1507
1591
|
this.clearFinalReplyCompletionTimerForTurn(turnId);
|
|
1508
1592
|
this.turnFinalMessages.delete(turnId);
|
|
@@ -1570,6 +1654,13 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1570
1654
|
socket.send(JSON.stringify(payload));
|
|
1571
1655
|
}
|
|
1572
1656
|
async respondToApprovalRequest(request, action) {
|
|
1657
|
+
if (request.method === "item/permissions/requestApproval") {
|
|
1658
|
+
this.sendRpcMessage({
|
|
1659
|
+
id: request.requestId,
|
|
1660
|
+
result: buildCodexPermissionsRequestApprovalResponse(request.params, action),
|
|
1661
|
+
});
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1573
1664
|
const decision = action === "confirm" ? "accept" : "decline";
|
|
1574
1665
|
this.sendRpcMessage({
|
|
1575
1666
|
id: request.requestId,
|
|
@@ -1754,6 +1845,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1754
1845
|
this.setStatus("busy", "Codex approval resolved.");
|
|
1755
1846
|
}
|
|
1756
1847
|
}
|
|
1848
|
+
if (requestId !== null &&
|
|
1849
|
+
this.pendingUserInputRequest &&
|
|
1850
|
+
requestId === this.pendingUserInputRequest.requestId &&
|
|
1851
|
+
trackedTurn.turnId === this.pendingUserInputRequest.turnId) {
|
|
1852
|
+
this.clearPendingUserInputState();
|
|
1853
|
+
if (this.state.status === "awaiting_input") {
|
|
1854
|
+
this.setStatus("busy", "Codex user input resolved.");
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1757
1857
|
return;
|
|
1758
1858
|
}
|
|
1759
1859
|
case "turn/completed": {
|
|
@@ -1764,8 +1864,24 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1764
1864
|
}
|
|
1765
1865
|
}
|
|
1766
1866
|
handleRpcServerRequest(requestId, method, params) {
|
|
1867
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1868
|
+
this.sendRpcMessage({
|
|
1869
|
+
id: requestId,
|
|
1870
|
+
result: buildCodexMcpServerElicitationDeclineResponse(),
|
|
1871
|
+
});
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (method === "item/tool/call") {
|
|
1875
|
+
this.sendRpcMessage({
|
|
1876
|
+
id: requestId,
|
|
1877
|
+
result: buildCodexDynamicToolCallFailureResponse(),
|
|
1878
|
+
});
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1767
1881
|
if (method !== "item/commandExecution/requestApproval" &&
|
|
1768
|
-
method !== "item/fileChange/requestApproval"
|
|
1882
|
+
method !== "item/fileChange/requestApproval" &&
|
|
1883
|
+
method !== "item/permissions/requestApproval" &&
|
|
1884
|
+
method !== "item/tool/requestUserInput") {
|
|
1769
1885
|
this.sendRpcMessage({
|
|
1770
1886
|
id: requestId,
|
|
1771
1887
|
error: {
|
|
@@ -1793,16 +1909,57 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1793
1909
|
});
|
|
1794
1910
|
return;
|
|
1795
1911
|
}
|
|
1796
|
-
const trackedTurn = this.identifyTrackedTurn("server/request", params)
|
|
1912
|
+
const trackedTurn = this.identifyTrackedTurn("server/request", params) ??
|
|
1913
|
+
this.fallbackTrackedTurnForServerRequest(params);
|
|
1797
1914
|
if (!trackedTurn) {
|
|
1915
|
+
this.sendRpcMessage({
|
|
1916
|
+
id: requestId,
|
|
1917
|
+
error: {
|
|
1918
|
+
code: -32602,
|
|
1919
|
+
message: "Invalid Codex server request payload: missing threadId or turnId.",
|
|
1920
|
+
},
|
|
1921
|
+
});
|
|
1798
1922
|
return;
|
|
1799
1923
|
}
|
|
1800
1924
|
this.handleTrackedTurnStarted(trackedTurn);
|
|
1801
1925
|
this.handleTrackedTurnServerRequest(requestId, method, params, trackedTurn);
|
|
1802
1926
|
}
|
|
1803
1927
|
handleTrackedTurnServerRequest(requestId, method, params, trackedTurn) {
|
|
1928
|
+
if (method === "item/tool/requestUserInput") {
|
|
1929
|
+
this.handleTrackedTurnUserInputRequest(requestId, params, trackedTurn);
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const denyMessage = getCodexWechatOutboundAttachmentDenyMessage(method, params);
|
|
1933
|
+
if (denyMessage) {
|
|
1934
|
+
this.sendRpcMessage({
|
|
1935
|
+
id: requestId,
|
|
1936
|
+
result: method === "item/permissions/requestApproval"
|
|
1937
|
+
? buildCodexPermissionsRequestApprovalResponse(params, "deny")
|
|
1938
|
+
: { decision: "decline" },
|
|
1939
|
+
});
|
|
1940
|
+
this.state.lastOutputAt = nowIso();
|
|
1941
|
+
this.setStatus("busy", `Codex approval auto-denied: ${truncatePreview(denyMessage, 180)}`);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const autoResponse = getCodexApprovalAutoResponse(method, params);
|
|
1945
|
+
if (autoResponse) {
|
|
1946
|
+
this.sendRpcMessage({
|
|
1947
|
+
id: requestId,
|
|
1948
|
+
result: autoResponse.result,
|
|
1949
|
+
});
|
|
1950
|
+
this.state.lastOutputAt = nowIso();
|
|
1951
|
+
this.setStatus("busy", `Codex approval auto-approved: ${truncatePreview(autoResponse.reason, 180)}`);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1804
1954
|
const request = buildCodexApprovalRequest(method, params);
|
|
1805
1955
|
if (!request) {
|
|
1956
|
+
this.sendRpcMessage({
|
|
1957
|
+
id: requestId,
|
|
1958
|
+
error: {
|
|
1959
|
+
code: -32602,
|
|
1960
|
+
message: "Invalid Codex approval request payload.",
|
|
1961
|
+
},
|
|
1962
|
+
});
|
|
1806
1963
|
return;
|
|
1807
1964
|
}
|
|
1808
1965
|
this.pendingApprovalRequest = {
|
|
@@ -1811,18 +1968,63 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1811
1968
|
threadId: trackedTurn.threadId,
|
|
1812
1969
|
turnId: trackedTurn.turnId,
|
|
1813
1970
|
origin: trackedTurn.origin,
|
|
1971
|
+
params,
|
|
1814
1972
|
};
|
|
1815
1973
|
this.pendingApproval = request;
|
|
1816
1974
|
this.state.pendingApproval = request;
|
|
1817
1975
|
this.state.pendingApprovalOrigin = trackedTurn.origin;
|
|
1818
1976
|
this.state.lastOutputAt = nowIso();
|
|
1819
|
-
this.setStatus("awaiting_approval",
|
|
1977
|
+
this.setStatus("awaiting_approval", `Codex approval is required: ${truncatePreview(request.commandPreview, 180)}`);
|
|
1820
1978
|
this.emit({
|
|
1821
1979
|
type: "approval_required",
|
|
1822
1980
|
request,
|
|
1823
1981
|
timestamp: nowIso(),
|
|
1824
1982
|
});
|
|
1825
1983
|
}
|
|
1984
|
+
fallbackTrackedTurnForServerRequest(params) {
|
|
1985
|
+
if (this.activeTurn) {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
const threadId = getNotificationThreadId(params);
|
|
1989
|
+
const turnId = getNotificationTurnId(params);
|
|
1990
|
+
if (!threadId || !turnId) {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
threadId,
|
|
1995
|
+
turnId,
|
|
1996
|
+
origin: this.bridgeOwnedTurnIds.has(turnId) ? "wechat" : "local",
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
handleTrackedTurnUserInputRequest(requestId, params, trackedTurn) {
|
|
2000
|
+
const request = buildCodexUserInputRequest(params);
|
|
2001
|
+
if (!request) {
|
|
2002
|
+
this.sendRpcMessage({
|
|
2003
|
+
id: requestId,
|
|
2004
|
+
error: {
|
|
2005
|
+
code: -32602,
|
|
2006
|
+
message: "Invalid Codex user input request payload.",
|
|
2007
|
+
},
|
|
2008
|
+
});
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
this.pendingUserInputRequest = {
|
|
2012
|
+
requestId,
|
|
2013
|
+
method: "item/tool/requestUserInput",
|
|
2014
|
+
threadId: trackedTurn.threadId,
|
|
2015
|
+
turnId: trackedTurn.turnId,
|
|
2016
|
+
origin: trackedTurn.origin,
|
|
2017
|
+
};
|
|
2018
|
+
this.state.pendingUserInput = request;
|
|
2019
|
+
this.state.pendingUserInputOrigin = trackedTurn.origin;
|
|
2020
|
+
this.state.lastOutputAt = nowIso();
|
|
2021
|
+
this.setStatus("awaiting_input", "Codex is waiting for user input.");
|
|
2022
|
+
this.emit({
|
|
2023
|
+
type: "user_input_required",
|
|
2024
|
+
request,
|
|
2025
|
+
timestamp: nowIso(),
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
1826
2028
|
handleThreadStatusChanged(params) {
|
|
1827
2029
|
const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
|
|
1828
2030
|
if (!threadId) {
|
|
@@ -1935,6 +2137,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1935
2137
|
this.pendingApprovalRequest.turnId === trackedTurn.turnId) {
|
|
1936
2138
|
this.clearPendingApprovalState();
|
|
1937
2139
|
}
|
|
2140
|
+
if (this.pendingUserInputRequest &&
|
|
2141
|
+
this.pendingUserInputRequest.turnId === trackedTurn.turnId) {
|
|
2142
|
+
this.clearPendingUserInputState();
|
|
2143
|
+
}
|
|
1938
2144
|
if (this.activeTurn?.turnId === trackedTurn.turnId) {
|
|
1939
2145
|
this.setActiveTurn(null);
|
|
1940
2146
|
}
|
|
@@ -2023,6 +2229,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2023
2229
|
this.pendingTurnStart ||
|
|
2024
2230
|
this.pendingApproval ||
|
|
2025
2231
|
this.pendingApprovalRequest ||
|
|
2232
|
+
this.pendingUserInputRequest ||
|
|
2233
|
+
this.state.pendingUserInput ||
|
|
2026
2234
|
!this.collectTurnOutput(turnId)) {
|
|
2027
2235
|
return;
|
|
2028
2236
|
}
|
|
@@ -2039,6 +2247,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2039
2247
|
const finalText = this.collectTurnOutput(turnId);
|
|
2040
2248
|
const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
|
|
2041
2249
|
const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequest);
|
|
2250
|
+
const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
|
|
2042
2251
|
const nowMs = Date.now();
|
|
2043
2252
|
if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
|
|
2044
2253
|
candidateTurnId: turnId,
|
|
@@ -2046,6 +2255,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2046
2255
|
activeTurnOrigin: activeTurn?.origin,
|
|
2047
2256
|
pendingTurnStart: this.pendingTurnStart,
|
|
2048
2257
|
hasPendingApproval: pendingApproval,
|
|
2258
|
+
hasPendingUserInput: pendingUserInput,
|
|
2049
2259
|
hasFinalOutput: Boolean(finalText),
|
|
2050
2260
|
hasCompletedTurn: this.hasCompletedTurn(turnId),
|
|
2051
2261
|
lastActivityAtMs,
|
|
@@ -2056,6 +2266,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2056
2266
|
activeTurn.origin === "wechat" &&
|
|
2057
2267
|
!this.pendingTurnStart &&
|
|
2058
2268
|
!pendingApproval &&
|
|
2269
|
+
!pendingUserInput &&
|
|
2059
2270
|
finalText &&
|
|
2060
2271
|
typeof lastActivityAtMs === "number") {
|
|
2061
2272
|
const remainingMs = CODEX_FINAL_REPLY_SETTLE_DELAY_MS - (nowMs - lastActivityAtMs);
|
|
@@ -2073,6 +2284,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2073
2284
|
return;
|
|
2074
2285
|
}
|
|
2075
2286
|
this.clearPendingApprovalState();
|
|
2287
|
+
this.clearPendingUserInputState();
|
|
2076
2288
|
this.setActiveTurn(null);
|
|
2077
2289
|
this.cleanupTurnArtifacts(turnId);
|
|
2078
2290
|
this.state.lastOutputAt = nowIso();
|
|
@@ -20,21 +20,29 @@ export class LocalCompanionProxyAdapter {
|
|
|
20
20
|
reconnectShutdownTimer = null;
|
|
21
21
|
constructor(options) {
|
|
22
22
|
this.options = options;
|
|
23
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
24
|
+
const initialSharedSessionId = shouldRestoreInitialSession
|
|
25
|
+
? options.initialSharedSessionId ?? options.initialSharedThreadId
|
|
26
|
+
: undefined;
|
|
23
27
|
this.state = {
|
|
24
28
|
kind: options.kind,
|
|
25
29
|
status: "stopped",
|
|
26
30
|
cwd: options.cwd,
|
|
27
31
|
command: options.command,
|
|
28
32
|
profile: options.profile,
|
|
29
|
-
sharedSessionId:
|
|
33
|
+
sharedSessionId: initialSharedSessionId,
|
|
30
34
|
sharedThreadId: options.kind === "codex" || options.kind === "opencode"
|
|
31
|
-
?
|
|
35
|
+
? initialSharedSessionId
|
|
32
36
|
: undefined,
|
|
33
37
|
activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode"
|
|
34
|
-
?
|
|
38
|
+
? initialSharedSessionId
|
|
39
|
+
: undefined,
|
|
40
|
+
resumeConversationId: shouldRestoreInitialSession && options.kind === "claude"
|
|
41
|
+
? options.initialResumeConversationId
|
|
42
|
+
: undefined,
|
|
43
|
+
transcriptPath: shouldRestoreInitialSession && options.kind === "claude"
|
|
44
|
+
? options.initialTranscriptPath
|
|
35
45
|
: undefined,
|
|
36
|
-
resumeConversationId: options.kind === "claude" ? options.initialResumeConversationId : undefined,
|
|
37
|
-
transcriptPath: options.kind === "claude" ? options.initialTranscriptPath : undefined,
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
setEventSink(sink) {
|