cli-wechat-bridge 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -150
- package/bin/_run-entry.mjs +12 -0
- package/bin/wechat-claude-start.mjs +7 -2
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex-start.mjs +7 -2
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode-start.mjs +7 -2
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi-start.mjs +10 -0
- package/bin/wechat-pi.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +2 -0
- package/dist/bridge/bridge-adapters.claude.js +21 -1
- package/dist/bridge/bridge-adapters.codex.js +85 -6
- package/dist/bridge/bridge-adapters.core.js +112 -6
- package/dist/bridge/bridge-adapters.js +5 -3
- package/dist/bridge/bridge-adapters.opencode.js +390 -52
- package/dist/bridge/bridge-adapters.pi.js +707 -0
- package/dist/bridge/bridge-adapters.shared.js +22 -102
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-state.js +42 -6
- package/dist/bridge/bridge-utils.js +94 -237
- package/dist/bridge/wechat-bridge.js +77 -131
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +20 -6
- package/dist/companion/local-companion-link.js +13 -3
- package/dist/companion/local-companion-start.js +64 -16
- package/dist/companion/local-companion.js +8 -5
- package/dist/companion/pi-tui-bridge-extension.js +256 -0
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +264 -77
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +4 -5
- package/dist/i18n/messages-zh.js +4 -5
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +27 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -9
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +113 -102
- package/bin/wechat-bridge-claude.mjs +0 -5
- package/bin/wechat-bridge-codex.mjs +0 -5
- package/bin/wechat-bridge-opencode.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
|
@@ -10,6 +10,7 @@ import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
|
|
|
10
10
|
import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
11
11
|
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;
|
|
12
12
|
const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
|
|
13
|
+
const CODEX_NON_BLOCKING_USER_INPUT_TIMEOUT_MS = 120_000;
|
|
13
14
|
export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
14
15
|
runtimeKind = "codex_runtime_host";
|
|
15
16
|
appServer = null;
|
|
@@ -38,6 +39,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
38
39
|
pendingThreadFollowId = null;
|
|
39
40
|
pendingApprovalRequests = [];
|
|
40
41
|
pendingUserInputRequest = null;
|
|
42
|
+
pendingUserInputTimer = null;
|
|
43
|
+
userInputAutoResolutionDelayMs = CODEX_NON_BLOCKING_USER_INPUT_TIMEOUT_MS;
|
|
41
44
|
queuedTurnNotifications = [];
|
|
42
45
|
queuedTurnServerRequests = [];
|
|
43
46
|
mirroredUserInputTurnIds = new Set();
|
|
@@ -526,6 +529,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
526
529
|
this.sessionPartialLine = "";
|
|
527
530
|
}
|
|
528
531
|
if (stat.size === this.sessionReadOffset) {
|
|
532
|
+
this.flushCompleteSessionPartialLine();
|
|
529
533
|
return;
|
|
530
534
|
}
|
|
531
535
|
const fd = fs.openSync(this.sessionFilePath, "r");
|
|
@@ -551,6 +555,22 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
551
555
|
for (const line of lines) {
|
|
552
556
|
this.handleSessionLogLine(line);
|
|
553
557
|
}
|
|
558
|
+
this.flushCompleteSessionPartialLine();
|
|
559
|
+
}
|
|
560
|
+
flushCompleteSessionPartialLine() {
|
|
561
|
+
const line = this.sessionPartialLine.trim();
|
|
562
|
+
if (!line) {
|
|
563
|
+
this.sessionPartialLine = "";
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
JSON.parse(line);
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.sessionPartialLine = "";
|
|
573
|
+
this.handleSessionLogLine(line);
|
|
554
574
|
}
|
|
555
575
|
seedSessionReplayCutoff(startedAtMs) {
|
|
556
576
|
if (this.sessionIgnoreBeforeMs !== null ||
|
|
@@ -1166,6 +1186,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1166
1186
|
Date.now() < reconnectDeadline) {
|
|
1167
1187
|
try {
|
|
1168
1188
|
await this.connectRpcClient();
|
|
1189
|
+
// The new connection starts with an empty subscription set.
|
|
1190
|
+
// Resubscribe the shared thread so turn notifications and approval
|
|
1191
|
+
// requests keep flowing: without this, a mid-turn reconnect
|
|
1192
|
+
// silently stops delivery, and approvals answered over the new
|
|
1193
|
+
// connection (with the old request id) are ignored by the server.
|
|
1194
|
+
if (this.state.sharedThreadId) {
|
|
1195
|
+
await this.tryEnsureSharedThreadSubscribed(this.state.sharedThreadId);
|
|
1196
|
+
}
|
|
1169
1197
|
return true;
|
|
1170
1198
|
}
|
|
1171
1199
|
catch (error) {
|
|
@@ -1211,6 +1239,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1211
1239
|
},
|
|
1212
1240
|
capabilities: {
|
|
1213
1241
|
experimentalApi: true,
|
|
1242
|
+
requestAttestation: false,
|
|
1214
1243
|
},
|
|
1215
1244
|
});
|
|
1216
1245
|
}
|
|
@@ -1243,8 +1272,6 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1243
1272
|
approvalsReviewer: "user",
|
|
1244
1273
|
sandbox: "workspace-write",
|
|
1245
1274
|
serviceName: "wechat-bridge",
|
|
1246
|
-
experimentalRawEvents: false,
|
|
1247
|
-
persistExtendedHistory: true,
|
|
1248
1275
|
});
|
|
1249
1276
|
const threadId = this.extractThreadIdFromResponse(response);
|
|
1250
1277
|
if (!threadId) {
|
|
@@ -1375,7 +1402,20 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1375
1402
|
this.state.status !== "awaiting_input") {
|
|
1376
1403
|
return;
|
|
1377
1404
|
}
|
|
1378
|
-
|
|
1405
|
+
// Remember the interrupted turn as completed before resetting: the
|
|
1406
|
+
// server may still deliver a late turn/completed for it, and without
|
|
1407
|
+
// this entry it would be tracked as a brand-new local turn — emitting a
|
|
1408
|
+
// duplicate task_complete (or a ghost reply) for work we already
|
|
1409
|
+
// reported as interrupted.
|
|
1410
|
+
const interruptedTurnId = this.activeTurn?.turnId;
|
|
1411
|
+
this.resetTurnTracking({
|
|
1412
|
+
preserveThread: true,
|
|
1413
|
+
preserveCompletedTurns: true,
|
|
1414
|
+
});
|
|
1415
|
+
if (interruptedTurnId) {
|
|
1416
|
+
this.rememberCompletedTurn(interruptedTurnId);
|
|
1417
|
+
this.bridgeOwnedTurnIds.add(interruptedTurnId);
|
|
1418
|
+
}
|
|
1379
1419
|
this.setStatus("idle", "Codex task interrupted.");
|
|
1380
1420
|
this.emit({
|
|
1381
1421
|
type: "task_complete",
|
|
@@ -1444,9 +1484,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1444
1484
|
this.turnErrorById.clear();
|
|
1445
1485
|
this.turnLastActivityAtMs.clear();
|
|
1446
1486
|
this.mirroredUserInputTurnIds.clear();
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1487
|
+
if (!options.preserveCompletedTurns) {
|
|
1488
|
+
this.bridgeOwnedTurnIds.clear();
|
|
1489
|
+
this.completedTurnIds.clear();
|
|
1490
|
+
this.completedTurnOrder = [];
|
|
1491
|
+
}
|
|
1450
1492
|
this.pendingInjectedInputs = [];
|
|
1451
1493
|
this.recentBridgeThreadSignalAtById.clear();
|
|
1452
1494
|
this.sessionFinalText = null;
|
|
@@ -1640,6 +1682,10 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1640
1682
|
this.state.pendingApprovalOrigin = undefined;
|
|
1641
1683
|
}
|
|
1642
1684
|
clearPendingUserInputState() {
|
|
1685
|
+
if (this.pendingUserInputTimer) {
|
|
1686
|
+
clearTimeout(this.pendingUserInputTimer);
|
|
1687
|
+
this.pendingUserInputTimer = null;
|
|
1688
|
+
}
|
|
1643
1689
|
this.pendingUserInputRequest = null;
|
|
1644
1690
|
this.state.pendingUserInput = null;
|
|
1645
1691
|
this.state.pendingUserInputOrigin = undefined;
|
|
@@ -1816,6 +1862,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1816
1862
|
if (!threadId || !turnId) {
|
|
1817
1863
|
return null;
|
|
1818
1864
|
}
|
|
1865
|
+
if (this.hasCompletedTurn(turnId)) {
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1819
1868
|
if (this.bridgeOwnedTurnIds.has(turnId)) {
|
|
1820
1869
|
return {
|
|
1821
1870
|
threadId,
|
|
@@ -1926,6 +1975,15 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1926
1975
|
}
|
|
1927
1976
|
}
|
|
1928
1977
|
handleRpcServerRequest(requestId, method, params) {
|
|
1978
|
+
if (method === "currentTime/read") {
|
|
1979
|
+
this.sendRpcMessage({
|
|
1980
|
+
id: requestId,
|
|
1981
|
+
result: {
|
|
1982
|
+
currentTimeAt: Math.floor(Date.now() / 1_000),
|
|
1983
|
+
},
|
|
1984
|
+
});
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1929
1987
|
if (method === "mcpServer/elicitation/request") {
|
|
1930
1988
|
this.sendRpcMessage({
|
|
1931
1989
|
id: requestId,
|
|
@@ -2076,6 +2134,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2076
2134
|
threadId: trackedTurn.threadId,
|
|
2077
2135
|
turnId: trackedTurn.turnId,
|
|
2078
2136
|
origin: trackedTurn.origin,
|
|
2137
|
+
isBlocking: params.isBlocking !== false,
|
|
2079
2138
|
};
|
|
2080
2139
|
this.state.pendingUserInput = request;
|
|
2081
2140
|
this.state.pendingUserInputOrigin = trackedTurn.origin;
|
|
@@ -2086,6 +2145,26 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2086
2145
|
request,
|
|
2087
2146
|
timestamp: nowIso(),
|
|
2088
2147
|
});
|
|
2148
|
+
if (this.pendingUserInputRequest.isBlocking) {
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const pendingRequest = this.pendingUserInputRequest;
|
|
2152
|
+
this.pendingUserInputTimer = setTimeout(() => {
|
|
2153
|
+
if (this.pendingUserInputRequest !== pendingRequest) {
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
this.sendRpcMessage({
|
|
2157
|
+
id: pendingRequest.requestId,
|
|
2158
|
+
result: {
|
|
2159
|
+
answers: {},
|
|
2160
|
+
},
|
|
2161
|
+
});
|
|
2162
|
+
this.clearPendingUserInputState();
|
|
2163
|
+
if (this.state.status === "awaiting_input") {
|
|
2164
|
+
this.setStatus("busy", "Codex non-blocking user input timed out.");
|
|
2165
|
+
}
|
|
2166
|
+
}, this.userInputAutoResolutionDelayMs);
|
|
2167
|
+
this.pendingUserInputTimer.unref?.();
|
|
2089
2168
|
}
|
|
2090
2169
|
handleThreadStatusChanged(params) {
|
|
2091
2170
|
const threadId = extractCodexThreadFollowIdFromStatusChanged(params);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import { spawn as spawnPty } from "node-pty";
|
|
3
4
|
import { t } from "../i18n/index.js";
|
|
4
5
|
import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
@@ -6,12 +7,46 @@ import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./
|
|
|
6
7
|
import * as shared from "./bridge-adapters.shared.js";
|
|
7
8
|
import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
|
|
8
9
|
const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, buildSpawnDiagnostic, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, spawnFallbackProcess, } = shared;
|
|
10
|
+
/**
|
|
11
|
+
* On Windows, node-pty (ConPTY) uses CreateProcess which can execute .cmd/.bat
|
|
12
|
+
* files directly — no need for the cmd.exe wrapper that resolveSpawnTarget adds
|
|
13
|
+
* for child_process.spawn compatibility. The wrapper causes double-quoting when
|
|
14
|
+
* the script path contains spaces: node-pty quotes the already-quoted path,
|
|
15
|
+
* making cmd.exe treat the literal quotes as part of the filename.
|
|
16
|
+
*/
|
|
17
|
+
function unwrapCmdExeForPty(rawTarget) {
|
|
18
|
+
if (process.platform !== "win32") {
|
|
19
|
+
return rawTarget;
|
|
20
|
+
}
|
|
21
|
+
const fileName = path.basename(rawTarget.file).toLowerCase();
|
|
22
|
+
if (fileName !== "cmd.exe") {
|
|
23
|
+
return rawTarget;
|
|
24
|
+
}
|
|
25
|
+
const args = rawTarget.args;
|
|
26
|
+
if (args.length < 4 || args[0] !== "/d" || args[1] !== "/s" || args[2] !== "/c") {
|
|
27
|
+
return rawTarget;
|
|
28
|
+
}
|
|
29
|
+
// Strip surrounding double-quotes from the /c argument to recover the
|
|
30
|
+
// actual script path. quoteForCmd wraps paths containing spaces in
|
|
31
|
+
// double-quotes, so we reverse that here.
|
|
32
|
+
const commandLine = args[3] ?? "";
|
|
33
|
+
const actualScript = commandLine.replace(/^"(.*)"$/s, "$1");
|
|
34
|
+
// Remaining args beyond the /c handler (args[4..]) are extra arguments
|
|
35
|
+
// that were passed through wrapWithCmdExe; forward them as the new target's
|
|
36
|
+
// args so they stay prepended before buildSpawnArgs().
|
|
37
|
+
return {
|
|
38
|
+
file: actualScript,
|
|
39
|
+
args: args.slice(4),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
9
42
|
export class LocalCompanionProxyAdapter {
|
|
10
43
|
options;
|
|
11
44
|
state;
|
|
12
45
|
eventSink = () => undefined;
|
|
13
46
|
server = null;
|
|
14
47
|
socket = null;
|
|
48
|
+
// Pending senders waiting for the companion's first state frame.
|
|
49
|
+
readyWaiters = new Set();
|
|
15
50
|
detachMessageListener = null;
|
|
16
51
|
requestCounter = 0;
|
|
17
52
|
endpoint = null;
|
|
@@ -35,7 +70,7 @@ export class LocalCompanionProxyAdapter {
|
|
|
35
70
|
sharedThreadId: options.kind === "codex" || options.kind === "opencode"
|
|
36
71
|
? initialSharedSessionId
|
|
37
72
|
: undefined,
|
|
38
|
-
activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode"
|
|
73
|
+
activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode" || options.kind === "pi"
|
|
39
74
|
? initialSharedSessionId
|
|
40
75
|
: undefined,
|
|
41
76
|
resumeConversationId: shouldRestoreInitialSession && options.kind === "claude"
|
|
@@ -66,11 +101,22 @@ export class LocalCompanionProxyAdapter {
|
|
|
66
101
|
});
|
|
67
102
|
this.server = server;
|
|
68
103
|
server.on("error", (error) => {
|
|
104
|
+
// Roll back the half-initialized server: start() early-returns while
|
|
105
|
+
// this.server is set, so a leftover handle would make every retry
|
|
106
|
+
// silently "succeed" without ever listening.
|
|
107
|
+
if (this.server === server) {
|
|
108
|
+
this.server = null;
|
|
109
|
+
}
|
|
110
|
+
server.close();
|
|
69
111
|
reject(error);
|
|
70
112
|
});
|
|
71
113
|
server.listen(0, CODEX_APP_SERVER_HOST, () => {
|
|
72
114
|
const address = server.address();
|
|
73
115
|
if (!address || typeof address === "string") {
|
|
116
|
+
if (this.server === server) {
|
|
117
|
+
this.server = null;
|
|
118
|
+
}
|
|
119
|
+
server.close();
|
|
74
120
|
reject(new Error(`Failed to allocate a local ${this.options.kind} companion port.`));
|
|
75
121
|
return;
|
|
76
122
|
}
|
|
@@ -141,8 +187,17 @@ export class LocalCompanionProxyAdapter {
|
|
|
141
187
|
const ok = await this.resolveApproval(action);
|
|
142
188
|
return ok ? 1 : 0;
|
|
143
189
|
}
|
|
144
|
-
async submitUserInput(
|
|
145
|
-
|
|
190
|
+
async submitUserInput(answers) {
|
|
191
|
+
try {
|
|
192
|
+
const result = await this.sendRequest({
|
|
193
|
+
command: "submit_user_input",
|
|
194
|
+
answers,
|
|
195
|
+
});
|
|
196
|
+
return result === true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
146
201
|
}
|
|
147
202
|
async dispose() {
|
|
148
203
|
this.shuttingDown = true;
|
|
@@ -277,6 +332,9 @@ export class LocalCompanionProxyAdapter {
|
|
|
277
332
|
this.state.activeTurnOrigin = undefined;
|
|
278
333
|
this.state.pendingApprovalOrigin = undefined;
|
|
279
334
|
Object.assign(this.state, message.state);
|
|
335
|
+
if (this.state.pid) {
|
|
336
|
+
this.settleReadyWaiters();
|
|
337
|
+
}
|
|
280
338
|
this.eventSink({
|
|
281
339
|
type: "status",
|
|
282
340
|
status: this.state.status,
|
|
@@ -299,6 +357,10 @@ export class LocalCompanionProxyAdapter {
|
|
|
299
357
|
}
|
|
300
358
|
}
|
|
301
359
|
detachPanelSocket() {
|
|
360
|
+
this.settleReadyWaiters(new Error(formatCompanionNotConnectedMessage({
|
|
361
|
+
kind: this.options.kind,
|
|
362
|
+
launchMode: this.options.companionLaunchMode,
|
|
363
|
+
})));
|
|
302
364
|
this.rejectPendingRequests("Companion socket disconnected unexpectedly.");
|
|
303
365
|
this.detachMessageListener?.();
|
|
304
366
|
this.detachMessageListener = null;
|
|
@@ -387,8 +449,44 @@ export class LocalCompanionProxyAdapter {
|
|
|
387
449
|
}
|
|
388
450
|
this.pendingRequests.clear();
|
|
389
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Wait for the companion's first state frame (which reports the worker
|
|
454
|
+
* pid). The companion sends it only after its adapter finished starting —
|
|
455
|
+
* for opencode that spans spawning `opencode serve`, connecting, and the
|
|
456
|
+
* initial session scan — so a WeChat message arriving inside that window
|
|
457
|
+
* must wait instead of failing with "connected but not ready yet".
|
|
458
|
+
*/
|
|
459
|
+
waitForCompanionReady() {
|
|
460
|
+
if (this.state.pid) {
|
|
461
|
+
return Promise.resolve();
|
|
462
|
+
}
|
|
463
|
+
return new Promise((resolve, reject) => {
|
|
464
|
+
const entry = {
|
|
465
|
+
timer: setTimeout(() => {
|
|
466
|
+
this.readyWaiters.delete(entry);
|
|
467
|
+
reject(new Error(`${this.options.kind} companion is connected but not ready yet. Wait for it to finish starting.`));
|
|
468
|
+
}, 30_000),
|
|
469
|
+
settle: (error) => {
|
|
470
|
+
clearTimeout(entry.timer);
|
|
471
|
+
this.readyWaiters.delete(entry);
|
|
472
|
+
if (error) {
|
|
473
|
+
reject(error);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
resolve();
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
this.readyWaiters.add(entry);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
settleReadyWaiters(error) {
|
|
484
|
+
for (const entry of [...this.readyWaiters]) {
|
|
485
|
+
entry.settle(error);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
390
488
|
async sendRequest(payload) {
|
|
391
|
-
|
|
489
|
+
let socket = this.socket;
|
|
392
490
|
if (!socket) {
|
|
393
491
|
throw new Error(formatCompanionNotConnectedMessage({
|
|
394
492
|
kind: this.options.kind,
|
|
@@ -396,7 +494,14 @@ export class LocalCompanionProxyAdapter {
|
|
|
396
494
|
}));
|
|
397
495
|
}
|
|
398
496
|
if (!this.state.pid && payload.command !== "dispose") {
|
|
399
|
-
|
|
497
|
+
await this.waitForCompanionReady();
|
|
498
|
+
socket = this.socket;
|
|
499
|
+
if (!socket) {
|
|
500
|
+
throw new Error(formatCompanionNotConnectedMessage({
|
|
501
|
+
kind: this.options.kind,
|
|
502
|
+
launchMode: this.options.companionLaunchMode,
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
400
505
|
}
|
|
401
506
|
const id = `${++this.requestCounter}`;
|
|
402
507
|
const response = new Promise((resolve, reject) => {
|
|
@@ -513,7 +618,8 @@ export class AbstractPtyAdapter {
|
|
|
513
618
|
this.setStatus("starting", `Starting ${this.options.kind} adapter...`);
|
|
514
619
|
let spawnTarget = null;
|
|
515
620
|
try {
|
|
516
|
-
|
|
621
|
+
const rawTarget = resolveSpawnTarget(this.options.command, this.options.kind);
|
|
622
|
+
spawnTarget = unwrapCmdExeForPty(rawTarget);
|
|
517
623
|
const env = this.buildEnv();
|
|
518
624
|
const spawnArgs = [...spawnTarget.args, ...this.buildSpawnArgs()];
|
|
519
625
|
let ptyProcess;
|
|
@@ -2,7 +2,7 @@ import { ClaudeCompanionAdapter } from "./bridge-adapters.claude.js";
|
|
|
2
2
|
import { LocalCompanionProxyAdapter } from "./bridge-adapters.core.js";
|
|
3
3
|
import { CodexPtyAdapter } from "./bridge-adapters.codex.js";
|
|
4
4
|
import { OpenCodeServerAdapter } from "./bridge-adapters.opencode.js";
|
|
5
|
-
import {
|
|
5
|
+
import { PiTuiAdapter } from "./bridge-adapters.pi.js";
|
|
6
6
|
export * from "./bridge-adapters.shared.js";
|
|
7
7
|
export function createBridgeAdapter(options) {
|
|
8
8
|
switch (options.kind) {
|
|
@@ -18,8 +18,10 @@ export function createBridgeAdapter(options) {
|
|
|
18
18
|
return options.renderMode === "companion"
|
|
19
19
|
? new OpenCodeServerAdapter(options)
|
|
20
20
|
: new LocalCompanionProxyAdapter(options);
|
|
21
|
-
case "
|
|
22
|
-
return
|
|
21
|
+
case "pi":
|
|
22
|
+
return options.renderMode === "companion"
|
|
23
|
+
? new PiTuiAdapter(options)
|
|
24
|
+
: new LocalCompanionProxyAdapter(options);
|
|
23
25
|
default:
|
|
24
26
|
throw new Error(`Unsupported adapter: ${options.kind}`);
|
|
25
27
|
}
|