@unlinearity/cli-wechat-bridge 1.0.7 → 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/LICENSE.txt +661 -21
- package/README.md +49 -2
- package/dist/bridge/bridge-adapters.claude.js +194 -5
- package/dist/bridge/bridge-adapters.codex.js +84 -37
- package/dist/bridge/bridge-adapters.core.js +58 -10
- package/dist/bridge/bridge-adapters.opencode.js +20 -12
- package/dist/bridge/bridge-adapters.shared.js +101 -1
- package/dist/bridge/bridge-adapters.shell.js +1 -1
- package/dist/bridge/bridge-process-reaper.js +7 -2
- package/dist/bridge/bridge-utils.js +40 -14
- package/dist/bridge/claude-hooks.js +14 -16
- package/dist/bridge/wechat-bridge.js +59 -10
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion.js +1 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +111 -0
- package/dist/daemon/wechat-daemon.js +197 -61
- 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 +3 -0
- package/package.json +2 -2
|
@@ -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();
|
|
@@ -495,9 +518,27 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
495
518
|
this.sessionPartialLine = "";
|
|
496
519
|
this.seedSessionReplayCutoff(startedAtMs);
|
|
497
520
|
}
|
|
498
|
-
let
|
|
521
|
+
let chunk;
|
|
499
522
|
try {
|
|
500
|
-
|
|
523
|
+
const stat = fs.statSync(this.sessionFilePath);
|
|
524
|
+
if (stat.size < this.sessionReadOffset) {
|
|
525
|
+
this.sessionReadOffset = 0;
|
|
526
|
+
this.sessionPartialLine = "";
|
|
527
|
+
}
|
|
528
|
+
if (stat.size === this.sessionReadOffset) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const fd = fs.openSync(this.sessionFilePath, "r");
|
|
532
|
+
try {
|
|
533
|
+
const bytesToRead = stat.size - this.sessionReadOffset;
|
|
534
|
+
const buf = Buffer.alloc(bytesToRead);
|
|
535
|
+
fs.readSync(fd, buf, 0, bytesToRead, this.sessionReadOffset);
|
|
536
|
+
chunk = buf.toString("utf8");
|
|
537
|
+
this.sessionReadOffset = stat.size;
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
fs.closeSync(fd);
|
|
541
|
+
}
|
|
501
542
|
}
|
|
502
543
|
catch {
|
|
503
544
|
this.sessionFilePath = null;
|
|
@@ -505,15 +546,6 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
505
546
|
this.sessionPartialLine = "";
|
|
506
547
|
return;
|
|
507
548
|
}
|
|
508
|
-
if (content.length < this.sessionReadOffset) {
|
|
509
|
-
this.sessionReadOffset = 0;
|
|
510
|
-
this.sessionPartialLine = "";
|
|
511
|
-
}
|
|
512
|
-
if (content.length === this.sessionReadOffset) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const chunk = content.slice(this.sessionReadOffset);
|
|
516
|
-
this.sessionReadOffset = content.length;
|
|
517
549
|
const lines = `${this.sessionPartialLine}${chunk}`.split(/\r?\n/);
|
|
518
550
|
this.sessionPartialLine = lines.pop() ?? "";
|
|
519
551
|
for (const line of lines) {
|
|
@@ -774,7 +806,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
774
806
|
this.recoverStaleBusyStateIfNeeded();
|
|
775
807
|
this.recoverStaleActiveTurnStateIfNeeded();
|
|
776
808
|
if (this.pendingApproval) {
|
|
777
|
-
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.");
|
|
778
810
|
}
|
|
779
811
|
if (this.pendingUserInputRequest || this.state.pendingUserInput) {
|
|
780
812
|
throw new Error("Codex is waiting for user input. Reply with /answer and your response, or use /stop.");
|
|
@@ -1238,7 +1270,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1238
1270
|
throw new Error("A thread id is required to resume a Codex thread.");
|
|
1239
1271
|
}
|
|
1240
1272
|
if (this.pendingApproval) {
|
|
1241
|
-
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.");
|
|
1242
1274
|
}
|
|
1243
1275
|
if (!options.startup &&
|
|
1244
1276
|
(this.pendingTurnStart ||
|
|
@@ -1339,7 +1371,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1339
1371
|
status: this.state.status,
|
|
1340
1372
|
pendingTurnStart: this.pendingTurnStart,
|
|
1341
1373
|
hasActiveTurn: Boolean(this.activeTurn),
|
|
1342
|
-
hasPendingApproval: Boolean(this.pendingApproval || this.
|
|
1374
|
+
hasPendingApproval: Boolean(this.pendingApproval || this.pendingApprovalRequests.length),
|
|
1343
1375
|
hasPendingUserInput: Boolean(this.pendingUserInputRequest || this.state.pendingUserInput),
|
|
1344
1376
|
activeTurnId: this.state.activeTurnId,
|
|
1345
1377
|
})) {
|
|
@@ -1357,7 +1389,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1357
1389
|
if (!this.activeTurn ||
|
|
1358
1390
|
this.pendingTurnStart ||
|
|
1359
1391
|
this.pendingApproval ||
|
|
1360
|
-
this.
|
|
1392
|
+
this.pendingApprovalRequests.length ||
|
|
1361
1393
|
this.state.status === "busy" ||
|
|
1362
1394
|
this.state.status === "awaiting_approval" ||
|
|
1363
1395
|
this.state.activeTurnId) {
|
|
@@ -1577,7 +1609,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1577
1609
|
return true;
|
|
1578
1610
|
}
|
|
1579
1611
|
clearPendingApprovalState() {
|
|
1580
|
-
this.
|
|
1612
|
+
this.pendingApprovalRequests = [];
|
|
1581
1613
|
this.pendingApproval = null;
|
|
1582
1614
|
this.state.pendingApproval = null;
|
|
1583
1615
|
this.state.pendingApprovalOrigin = undefined;
|
|
@@ -1627,10 +1659,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1627
1659
|
const requestId = ++this.rpcRequestCounter;
|
|
1628
1660
|
const requestKey = this.rpcRequestKey(requestId);
|
|
1629
1661
|
const responsePromise = new Promise((resolve, reject) => {
|
|
1662
|
+
const timer = setTimeout(() => {
|
|
1663
|
+
this.pendingRpcRequests.delete(requestKey);
|
|
1664
|
+
reject(new Error(`Codex RPC request timed out after 30s (method: ${method})`));
|
|
1665
|
+
}, 30_000);
|
|
1630
1666
|
this.pendingRpcRequests.set(requestKey, {
|
|
1631
1667
|
method,
|
|
1632
|
-
resolve,
|
|
1633
|
-
reject,
|
|
1668
|
+
resolve: (value) => { clearTimeout(timer); resolve(value); },
|
|
1669
|
+
reject: (err) => { clearTimeout(timer); reject(err); },
|
|
1634
1670
|
});
|
|
1635
1671
|
});
|
|
1636
1672
|
try {
|
|
@@ -1837,10 +1873,11 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1837
1873
|
case "serverRequest/resolved": {
|
|
1838
1874
|
const requestId = getCodexRpcRequestId(params.requestId);
|
|
1839
1875
|
if (requestId !== null &&
|
|
1840
|
-
this.
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
+
}
|
|
1844
1881
|
if (this.state.status === "awaiting_approval") {
|
|
1845
1882
|
this.setStatus("busy", "Codex approval resolved.");
|
|
1846
1883
|
}
|
|
@@ -1962,14 +1999,14 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
1962
1999
|
});
|
|
1963
2000
|
return;
|
|
1964
2001
|
}
|
|
1965
|
-
this.
|
|
2002
|
+
this.pendingApprovalRequests.push({
|
|
1966
2003
|
requestId,
|
|
1967
2004
|
method,
|
|
1968
2005
|
threadId: trackedTurn.threadId,
|
|
1969
2006
|
turnId: trackedTurn.turnId,
|
|
1970
2007
|
origin: trackedTurn.origin,
|
|
1971
2008
|
params,
|
|
1972
|
-
};
|
|
2009
|
+
});
|
|
1973
2010
|
this.pendingApproval = request;
|
|
1974
2011
|
this.state.pendingApproval = request;
|
|
1975
2012
|
this.state.pendingApprovalOrigin = trackedTurn.origin;
|
|
@@ -2133,8 +2170,8 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2133
2170
|
: completedTrackedTurn.origin === "local"
|
|
2134
2171
|
? "Local terminal turn completed."
|
|
2135
2172
|
: this.currentPreview;
|
|
2136
|
-
if (this.
|
|
2137
|
-
this.
|
|
2173
|
+
if (this.pendingApprovalRequests.length > 0 &&
|
|
2174
|
+
this.pendingApprovalRequests.some((r) => r.turnId === trackedTurn.turnId)) {
|
|
2138
2175
|
this.clearPendingApprovalState();
|
|
2139
2176
|
}
|
|
2140
2177
|
if (this.pendingUserInputRequest &&
|
|
@@ -2228,7 +2265,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2228
2265
|
this.activeTurn.origin !== "wechat" ||
|
|
2229
2266
|
this.pendingTurnStart ||
|
|
2230
2267
|
this.pendingApproval ||
|
|
2231
|
-
this.
|
|
2268
|
+
this.pendingApprovalRequests.length ||
|
|
2232
2269
|
this.pendingUserInputRequest ||
|
|
2233
2270
|
this.state.pendingUserInput ||
|
|
2234
2271
|
!this.collectTurnOutput(turnId)) {
|
|
@@ -2246,7 +2283,7 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2246
2283
|
const activeTurn = this.activeTurn;
|
|
2247
2284
|
const finalText = this.collectTurnOutput(turnId);
|
|
2248
2285
|
const lastActivityAtMs = this.turnLastActivityAtMs.get(turnId) ?? null;
|
|
2249
|
-
const pendingApproval = Boolean(this.pendingApproval || this.
|
|
2286
|
+
const pendingApproval = Boolean(this.pendingApproval || this.pendingApprovalRequests.length);
|
|
2250
2287
|
const pendingUserInput = Boolean(this.pendingUserInputRequest || this.state.pendingUserInput);
|
|
2251
2288
|
const nowMs = Date.now();
|
|
2252
2289
|
if (!shouldAutoCompleteCodexWechatTurnAfterFinalReply({
|
|
@@ -2326,7 +2363,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2326
2363
|
};
|
|
2327
2364
|
child.once("exit", () => finish());
|
|
2328
2365
|
try {
|
|
2329
|
-
child.
|
|
2366
|
+
if (child.pid) {
|
|
2367
|
+
killProcessTreeSync(child.pid);
|
|
2368
|
+
}
|
|
2369
|
+
else {
|
|
2370
|
+
child.kill();
|
|
2371
|
+
}
|
|
2330
2372
|
}
|
|
2331
2373
|
catch {
|
|
2332
2374
|
finish();
|
|
@@ -2357,7 +2399,12 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
|
|
|
2357
2399
|
}
|
|
2358
2400
|
if (this.nativeProcess) {
|
|
2359
2401
|
try {
|
|
2360
|
-
this.nativeProcess.
|
|
2402
|
+
if (this.nativeProcess.pid) {
|
|
2403
|
+
killProcessTreeSync(this.nativeProcess.pid);
|
|
2404
|
+
}
|
|
2405
|
+
else {
|
|
2406
|
+
this.nativeProcess.kill();
|
|
2407
|
+
}
|
|
2361
2408
|
}
|
|
2362
2409
|
catch {
|
|
2363
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
|
}
|
|
@@ -294,10 +299,12 @@ export class LocalCompanionProxyAdapter {
|
|
|
294
299
|
}
|
|
295
300
|
}
|
|
296
301
|
detachPanelSocket() {
|
|
302
|
+
this.rejectPendingRequests("Companion socket disconnected unexpectedly.");
|
|
297
303
|
this.detachMessageListener?.();
|
|
298
304
|
this.detachMessageListener = null;
|
|
299
305
|
if (this.socket) {
|
|
300
306
|
this.socket.removeAllListeners();
|
|
307
|
+
this.socket.on("error", () => { });
|
|
301
308
|
this.socket.destroy();
|
|
302
309
|
this.socket = null;
|
|
303
310
|
}
|
|
@@ -393,7 +400,14 @@ export class LocalCompanionProxyAdapter {
|
|
|
393
400
|
}
|
|
394
401
|
const id = `${++this.requestCounter}`;
|
|
395
402
|
const response = new Promise((resolve, reject) => {
|
|
396
|
-
|
|
403
|
+
const timer = setTimeout(() => {
|
|
404
|
+
this.pendingRequests.delete(id);
|
|
405
|
+
reject(new Error(`${this.options.kind} companion request timed out after 30s (command: ${payload.command})`));
|
|
406
|
+
}, 30_000);
|
|
407
|
+
this.pendingRequests.set(id, {
|
|
408
|
+
resolve: (value) => { clearTimeout(timer); resolve(value); },
|
|
409
|
+
reject: (err) => { clearTimeout(timer); reject(err); },
|
|
410
|
+
});
|
|
397
411
|
});
|
|
398
412
|
sendLocalCompanionMessage(socket, {
|
|
399
413
|
type: "request",
|
|
@@ -501,10 +515,27 @@ export class AbstractPtyAdapter {
|
|
|
501
515
|
try {
|
|
502
516
|
spawnTarget = resolveSpawnTarget(this.options.command, this.options.kind);
|
|
503
517
|
const env = this.buildEnv();
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
}
|
|
508
539
|
this.pty = ptyProcess;
|
|
509
540
|
this.shuttingDown = false;
|
|
510
541
|
this.hasAcceptedInput = false;
|
|
@@ -515,6 +546,13 @@ export class AbstractPtyAdapter {
|
|
|
515
546
|
ptyProcess.onData((data) => this.handleData(data));
|
|
516
547
|
ptyProcess.onExit(({ exitCode }) => this.handleExit(exitCode));
|
|
517
548
|
this.afterStart();
|
|
549
|
+
if (usedFallback) {
|
|
550
|
+
this.emit({
|
|
551
|
+
type: "stdout",
|
|
552
|
+
text: t("pty.fallback.warning"),
|
|
553
|
+
timestamp: nowIso(),
|
|
554
|
+
});
|
|
555
|
+
}
|
|
518
556
|
this.setStatus("idle", `${this.options.kind} adapter is ready.`);
|
|
519
557
|
}
|
|
520
558
|
catch (err) {
|
|
@@ -576,6 +614,10 @@ export class AbstractPtyAdapter {
|
|
|
576
614
|
this.state.pendingApproval = null;
|
|
577
615
|
return true;
|
|
578
616
|
}
|
|
617
|
+
async resolveAllApprovals(action) {
|
|
618
|
+
const ok = await this.resolveApproval(action);
|
|
619
|
+
return ok ? 1 : 0;
|
|
620
|
+
}
|
|
579
621
|
async submitUserInput(_answers) {
|
|
580
622
|
return false;
|
|
581
623
|
}
|
|
@@ -588,15 +630,21 @@ export class AbstractPtyAdapter {
|
|
|
588
630
|
return;
|
|
589
631
|
}
|
|
590
632
|
this.shuttingDown = true;
|
|
633
|
+
const pty = this.pty;
|
|
634
|
+
this.pty = null;
|
|
635
|
+
this.state.status = "stopped";
|
|
636
|
+
this.state.pid = undefined;
|
|
591
637
|
try {
|
|
592
|
-
|
|
638
|
+
const exitPromise = new Promise((resolve) => {
|
|
639
|
+
const timeout = setTimeout(resolve, 3_000);
|
|
640
|
+
pty.onExit(() => { clearTimeout(timeout); resolve(); });
|
|
641
|
+
});
|
|
642
|
+
pty.kill();
|
|
643
|
+
await exitPromise;
|
|
593
644
|
}
|
|
594
645
|
catch {
|
|
595
646
|
// Best effort shutdown.
|
|
596
647
|
}
|
|
597
|
-
this.pty = null;
|
|
598
|
-
this.state.status = "stopped";
|
|
599
|
-
this.state.pid = undefined;
|
|
600
648
|
}
|
|
601
649
|
getState() {
|
|
602
650
|
return JSON.parse(JSON.stringify(this.state));
|
|
@@ -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
|
}
|
|
@@ -285,21 +289,25 @@ export class OpenCodeServerAdapter {
|
|
|
285
289
|
if (this.nativeProcess) {
|
|
286
290
|
const proc = this.nativeProcess;
|
|
287
291
|
this.nativeProcess = null;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
292
|
+
if (proc.pid != null) {
|
|
293
|
+
try {
|
|
294
|
+
killProcessTreeSync(proc.pid);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Best effort.
|
|
298
|
+
}
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
if (this.serverProcess) {
|
|
296
302
|
const proc = this.serverProcess;
|
|
297
303
|
this.serverProcess = null;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
304
|
+
if (proc.pid != null) {
|
|
305
|
+
try {
|
|
306
|
+
killProcessTreeSync(proc.pid);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// Best effort.
|
|
310
|
+
}
|
|
303
311
|
}
|
|
304
312
|
}
|
|
305
313
|
this.client = null;
|
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
6
7
|
import { spawn as spawnPty } from "node-pty";
|
|
7
8
|
import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
|
|
8
9
|
import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
|
|
@@ -1214,3 +1215,102 @@ export function resolveSpawnTarget(command, kind, options = {}) {
|
|
|
1214
1215
|
}
|
|
1215
1216
|
return { file: resolved, args: [...forwardArgs] };
|
|
1216
1217
|
}
|
|
1218
|
+
export function spawnFallbackProcess(file, args, options) {
|
|
1219
|
+
const child = spawn(file, args, {
|
|
1220
|
+
cwd: options.cwd,
|
|
1221
|
+
env: { ...options.env, TERM: "dumb" },
|
|
1222
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1223
|
+
windowsHide: false,
|
|
1224
|
+
shell: false,
|
|
1225
|
+
});
|
|
1226
|
+
if (!child.pid) {
|
|
1227
|
+
throw new Error(`Failed to spawn fallback process: ${file}`);
|
|
1228
|
+
}
|
|
1229
|
+
const dataListeners = [];
|
|
1230
|
+
const exitListeners = [];
|
|
1231
|
+
child.stdout?.on("data", (chunk) => {
|
|
1232
|
+
const text = chunk.toString("utf8");
|
|
1233
|
+
for (const listener of dataListeners) {
|
|
1234
|
+
listener(text);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
child.stderr?.on("data", (chunk) => {
|
|
1238
|
+
const text = chunk.toString("utf8");
|
|
1239
|
+
for (const listener of dataListeners) {
|
|
1240
|
+
listener(text);
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
child.on("exit", (code, signal) => {
|
|
1244
|
+
const exitCode = code ?? (signal ? 128 : 1);
|
|
1245
|
+
for (const listener of exitListeners) {
|
|
1246
|
+
listener({ exitCode });
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
return {
|
|
1250
|
+
pid: child.pid,
|
|
1251
|
+
write(data) {
|
|
1252
|
+
child.stdin?.write(data);
|
|
1253
|
+
},
|
|
1254
|
+
kill(signal) {
|
|
1255
|
+
try {
|
|
1256
|
+
if (process.platform === "win32") {
|
|
1257
|
+
spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
1258
|
+
stdio: "ignore",
|
|
1259
|
+
windowsHide: true,
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
child.kill(signal);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
catch {
|
|
1267
|
+
// Best effort.
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
onData(callback) {
|
|
1271
|
+
dataListeners.push(callback);
|
|
1272
|
+
return {
|
|
1273
|
+
dispose() {
|
|
1274
|
+
const index = dataListeners.indexOf(callback);
|
|
1275
|
+
if (index >= 0)
|
|
1276
|
+
dataListeners.splice(index, 1);
|
|
1277
|
+
},
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
onExit(callback) {
|
|
1281
|
+
exitListeners.push(callback);
|
|
1282
|
+
return {
|
|
1283
|
+
dispose() {
|
|
1284
|
+
const index = exitListeners.indexOf(callback);
|
|
1285
|
+
if (index >= 0)
|
|
1286
|
+
exitListeners.splice(index, 1);
|
|
1287
|
+
},
|
|
1288
|
+
};
|
|
1289
|
+
},
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
export function buildSpawnDiagnostic(error, spawnTarget, platform = process.platform) {
|
|
1293
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1294
|
+
const target = spawnTarget ? spawnTarget.file : "(unknown)";
|
|
1295
|
+
const lines = [
|
|
1296
|
+
t("spawn.diagnostic.title", { target, error: errorMessage }),
|
|
1297
|
+
t("spawn.diagnostic.fixesHeader"),
|
|
1298
|
+
];
|
|
1299
|
+
if (errorMessage.includes("posix_spawnp") || errorMessage.includes("node-pty")) {
|
|
1300
|
+
lines.push(t("spawn.diagnostic.nodePty"));
|
|
1301
|
+
if (platform === "darwin") {
|
|
1302
|
+
lines.push(t("spawn.diagnostic.xcode"));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else if (errorMessage.includes("ENOENT") || errorMessage.includes("spawn")) {
|
|
1306
|
+
lines.push(t("spawn.diagnostic.notFound", { target }));
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
lines.push(t("spawn.diagnostic.generic"));
|
|
1310
|
+
}
|
|
1311
|
+
lines.push(t("spawn.diagnostic.nodeVersion"));
|
|
1312
|
+
if (platform === "win32") {
|
|
1313
|
+
lines.push(t("spawn.diagnostic.winAdmin"));
|
|
1314
|
+
}
|
|
1315
|
+
return lines.join("\n");
|
|
1316
|
+
}
|
|
@@ -67,7 +67,7 @@ export class ShellAdapter extends AbstractPtyAdapter {
|
|
|
67
67
|
throw new Error("shell is still working. Wait for the current reply or use /stop.");
|
|
68
68
|
}
|
|
69
69
|
if (this.pendingApproval || this.state.status === "awaiting_approval") {
|
|
70
|
-
throw new Error("A shell approval request is pending. Reply with /confirm
|
|
70
|
+
throw new Error("A shell approval request is pending. Reply with /confirm or /deny.");
|
|
71
71
|
}
|
|
72
72
|
const rejectionMessage = getInteractiveShellCommandRejectionMessage(text);
|
|
73
73
|
if (rejectionMessage) {
|
|
@@ -328,9 +328,14 @@ export function killProcessTreeSync(pid) {
|
|
|
328
328
|
}
|
|
329
329
|
else {
|
|
330
330
|
try {
|
|
331
|
-
process.kill(pid);
|
|
331
|
+
process.kill(-pid);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
try {
|
|
335
|
+
process.kill(pid);
|
|
336
|
+
}
|
|
337
|
+
catch { /* best effort */ }
|
|
332
338
|
}
|
|
333
|
-
catch { /* best effort */ }
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
export async function reapPeerBridgeProcesses(params = {}) {
|