@unlinearity/cli-wechat-bridge 1.1.3 → 1.1.5
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 +110 -140
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi.mjs +1 -1
- package/dist/bridge/bridge-adapters.claude.js +448 -11
- package/dist/bridge/bridge-adapters.codex.js +262 -11
- package/dist/bridge/bridge-adapters.core.js +76 -4
- package/dist/bridge/bridge-adapters.js +0 -3
- package/dist/bridge/bridge-adapters.opencode.js +497 -49
- package/dist/bridge/bridge-adapters.pi.js +44 -8
- package/dist/bridge/bridge-adapters.shared.js +13 -100
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-state.js +41 -6
- package/dist/bridge/bridge-utils.js +63 -229
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +93 -140
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +309 -36
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +33 -2
- package/dist/companion/local-companion-start.js +44 -13
- package/dist/companion/local-companion.js +6 -3
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +168 -56
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +2 -3
- package/dist/i18n/messages-zh.js +2 -3
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +23 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -11
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +5 -12
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
- 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-pi.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/bin/wechat-claude-start.mjs +0 -5
- package/bin/wechat-codex-start.mjs +0 -5
- package/bin/wechat-opencode-start.mjs +0 -5
- package/bin/wechat-pi-start.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
|
@@ -3,6 +3,7 @@ import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { CONTEXT_CACHE_FILE, CREDENTIALS_FILE, ensureChannelDataDir, INBOUND_ATTACHMENTS_DIR, INBOUND_MESSAGE_CLAIMS_DIR, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
|
|
6
|
+
import { writeJsonFileAtomic } from "../utils/atomic-file.js";
|
|
6
7
|
export const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
|
|
7
8
|
const CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
8
9
|
const CHANNEL_VERSION = "0.3.0";
|
|
@@ -10,6 +11,7 @@ const RECENT_MESSAGE_CACHE_SIZE = 500;
|
|
|
10
11
|
const BYTES_PER_MB = 1024 * 1024;
|
|
11
12
|
const SEND_TIMEOUT_MS = 15_000;
|
|
12
13
|
const INBOUND_DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
14
|
+
const CDN_UPLOAD_TIMEOUT_MS = 60_000;
|
|
13
15
|
const CDN_MAX_RETRIES = 3;
|
|
14
16
|
const ERROR_CAUSE_DEPTH_LIMIT = 4;
|
|
15
17
|
const INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
|
|
@@ -305,13 +307,7 @@ export function classifyWechatTransportError(error) {
|
|
|
305
307
|
return { kind: "unknown", retryable: false };
|
|
306
308
|
}
|
|
307
309
|
function writeJsonFile(filePath, value) {
|
|
308
|
-
|
|
309
|
-
const data = JSON.stringify(value, null, 2);
|
|
310
|
-
// Atomic write via temp file + rename, so a crash mid-write cannot leave a
|
|
311
|
-
// truncated/half-written JSON that would break readers on the next launch.
|
|
312
|
-
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
313
|
-
fs.writeFileSync(tempPath, data, "utf-8");
|
|
314
|
-
fs.renameSync(tempPath, filePath);
|
|
310
|
+
writeJsonFileAtomic(filePath, value);
|
|
315
311
|
}
|
|
316
312
|
function randomWechatUin() {
|
|
317
313
|
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
@@ -343,8 +339,11 @@ async function apiFetch(params) {
|
|
|
343
339
|
body: params.body,
|
|
344
340
|
signal: controller.signal,
|
|
345
341
|
});
|
|
346
|
-
|
|
342
|
+
// Keep the abort timer armed until the body has been fully read: a server
|
|
343
|
+
// that sends headers and then stalls would otherwise leave res.text()
|
|
344
|
+
// pending forever, freezing the long-poll loop with no timeout at all.
|
|
347
345
|
const text = await res.text();
|
|
346
|
+
clearTimeout(timer);
|
|
348
347
|
if (!res.ok) {
|
|
349
348
|
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
350
349
|
}
|
|
@@ -495,12 +494,18 @@ async function uploadBufferToCdn(params) {
|
|
|
495
494
|
let downloadParam;
|
|
496
495
|
let lastError;
|
|
497
496
|
for (let attempt = 1; attempt <= CDN_MAX_RETRIES; attempt += 1) {
|
|
497
|
+
// Abort guard mirrors the download path: without it a stalled CDN upload
|
|
498
|
+
// hangs the send queue forever (no timeout, no signal).
|
|
499
|
+
const controller = new AbortController();
|
|
500
|
+
const timer = setTimeout(() => controller.abort(), CDN_UPLOAD_TIMEOUT_MS);
|
|
498
501
|
try {
|
|
499
502
|
const res = await fetch(cdnUrl, {
|
|
500
503
|
method: "POST",
|
|
501
504
|
headers: { "Content-Type": "application/octet-stream" },
|
|
502
505
|
body: new Uint8Array(ciphertext),
|
|
506
|
+
signal: controller.signal,
|
|
503
507
|
});
|
|
508
|
+
clearTimeout(timer);
|
|
504
509
|
if (res.status >= 400 && res.status < 500) {
|
|
505
510
|
const errMsg = res.headers.get("x-error-message") ?? (await res.text());
|
|
506
511
|
throw new Error(`CDN client error ${res.status}: ${errMsg}`);
|
|
@@ -516,6 +521,7 @@ async function uploadBufferToCdn(params) {
|
|
|
516
521
|
break;
|
|
517
522
|
}
|
|
518
523
|
catch (err) {
|
|
524
|
+
clearTimeout(timer);
|
|
519
525
|
lastError = err;
|
|
520
526
|
if (err instanceof Error && err.message.includes("client error")) {
|
|
521
527
|
throw err;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlinearity/cli-wechat-bridge",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode,
|
|
3
|
+
"version": "1.1.5",
|
|
4
|
+
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and Pi over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin/",
|
|
@@ -34,20 +34,10 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"bin": {
|
|
37
|
-
"wechat-bridge": "bin/wechat-bridge.mjs",
|
|
38
|
-
"wechat-bridge-codex": "bin/wechat-bridge-codex.mjs",
|
|
39
37
|
"wechat-codex": "bin/wechat-codex.mjs",
|
|
40
|
-
"wechat-codex-start": "bin/wechat-codex-start.mjs",
|
|
41
38
|
"wechat-claude": "bin/wechat-claude.mjs",
|
|
42
|
-
"wechat-claude-start": "bin/wechat-claude-start.mjs",
|
|
43
|
-
"wechat-bridge-claude": "bin/wechat-bridge-claude.mjs",
|
|
44
|
-
"wechat-bridge-shell": "bin/wechat-bridge-shell.mjs",
|
|
45
|
-
"wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
|
|
46
39
|
"wechat-opencode": "bin/wechat-opencode.mjs",
|
|
47
|
-
"wechat-opencode-start": "bin/wechat-opencode-start.mjs",
|
|
48
|
-
"wechat-bridge-pi": "bin/wechat-bridge-pi.mjs",
|
|
49
40
|
"wechat-pi": "bin/wechat-pi.mjs",
|
|
50
|
-
"wechat-pi-start": "bin/wechat-pi-start.mjs",
|
|
51
41
|
"wechat-daemon": "bin/wechat-daemon.mjs",
|
|
52
42
|
"wechat-setup": "bin/wechat-setup.mjs",
|
|
53
43
|
"wechat-check-update": "bin/wechat-check-update.mjs"
|
|
@@ -72,5 +62,8 @@
|
|
|
72
62
|
"engines": {
|
|
73
63
|
"bun": ">=1.0.0",
|
|
74
64
|
"node": ">=24.0.0"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"postinstall": "node scripts/ensure-node-pty-permissions.mjs"
|
|
75
68
|
}
|
|
76
69
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Ensure the bundled node-pty `spawn-helper` is executable.
|
|
4
|
+
//
|
|
5
|
+
// node-pty ships its prebuilt `spawn-helper` binaries with mode 0644 (no exec
|
|
6
|
+
// bit) in the published tarball. On macOS/Linux that makes `posix_spawnp`
|
|
7
|
+
// fail when node-pty tries to fork a PTY, so the bridge silently drops to the
|
|
8
|
+
// non-PTY fallback. The Claude adapter relies on PTY interactive mode, so the
|
|
9
|
+
// fallback surfaces to users as:
|
|
10
|
+
//
|
|
11
|
+
// Error: Input must be provided ... when using --print
|
|
12
|
+
//
|
|
13
|
+
// Restoring the exec bit after install fixes PTY spawning. This runs on
|
|
14
|
+
// postinstall and is idempotent; it never fails the install.
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
|
|
26
|
+
let nodePtyDir;
|
|
27
|
+
try {
|
|
28
|
+
nodePtyDir = path.dirname(require.resolve("node-pty/package.json"));
|
|
29
|
+
} catch {
|
|
30
|
+
// node-pty is optional in unsupported environments; nothing to fix.
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const candidates = [
|
|
35
|
+
"prebuilds/darwin-arm64/spawn-helper",
|
|
36
|
+
"prebuilds/darwin-x64/spawn-helper",
|
|
37
|
+
"prebuilds/linux-x64/spawn-helper",
|
|
38
|
+
"prebuilds/linux-arm64/spawn-helper",
|
|
39
|
+
"build/Release/spawn-helper",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
let fixed = 0;
|
|
43
|
+
for (const relative of candidates) {
|
|
44
|
+
const helperPath = path.join(nodePtyDir, relative);
|
|
45
|
+
let stats;
|
|
46
|
+
try {
|
|
47
|
+
stats = fs.statSync(helperPath);
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!stats.isFile()) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if ((stats.mode & 0o111) !== 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.chmodSync(helperPath, stats.mode | 0o755);
|
|
59
|
+
fixed += 1;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[cli-wechat-bridge] Could not make node-pty spawn-helper executable: ${helperPath}\n` +
|
|
63
|
+
` ${error instanceof Error ? error.message : String(error)}\n` +
|
|
64
|
+
" PTY mode may be unavailable; run --doctor for details.",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (fixed > 0) {
|
|
70
|
+
console.log(
|
|
71
|
+
`[cli-wechat-bridge] Restored executable bit on ${fixed} node-pty spawn-helper binary(ies).`,
|
|
72
|
+
);
|
|
73
|
+
}
|
package/bin/wechat-bridge-pi.mjs
DELETED
package/bin/wechat-bridge.mjs
DELETED
package/bin/wechat-pi-start.mjs
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { getInteractiveShellCommandRejectionMessage, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
|
|
2
|
-
import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
|
|
3
|
-
import * as shared from "./bridge-adapters.shared.js";
|
|
4
|
-
const { buildShellInputPayload, buildShellProfileCommand, resolveShellRuntime, } = shared;
|
|
5
|
-
export class ShellCommandRejectedError extends Error {
|
|
6
|
-
constructor(message) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.name = "ShellCommandRejectedError";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
export class ShellAdapter extends AbstractPtyAdapter {
|
|
12
|
-
static COMPLETION_MARKER_PREFIX = "__WECHAT_BRIDGE_DONE__";
|
|
13
|
-
static POWERSHELL_LEADING_NOISE_PATTERNS = [
|
|
14
|
-
/^PS>>\s*/u,
|
|
15
|
-
/^PS [^>\n]*>\s*/u,
|
|
16
|
-
/^PS>\s*/u,
|
|
17
|
-
/^>>\s*/u,
|
|
18
|
-
/^>\s*/u,
|
|
19
|
-
/^function global:prompt \{ "" \}/u,
|
|
20
|
-
/^\$__wechatBridgePreviousErrorActionPreference = \$ErrorActionPreference/u,
|
|
21
|
-
/^\$ErrorActionPreference = 'Continue'/u,
|
|
22
|
-
/^\$global:LASTEXITCODE = 0/u,
|
|
23
|
-
/^try \{/u,
|
|
24
|
-
/^\$decoded = \[System\.Text\.Encoding\]::UTF8\.GetString\(\[System\.Convert\]::FromBase64String\(".*"\)\)/u,
|
|
25
|
-
/^\$scriptBlock = \[scriptblock\]::Create\(\$decoded\)/u,
|
|
26
|
-
/^& \$scriptBlock/u,
|
|
27
|
-
/^\} catch \{/u,
|
|
28
|
-
/^Write-Error \$_/u,
|
|
29
|
-
/^\$global:LASTEXITCODE = 1/u,
|
|
30
|
-
/^\} finally \{/u,
|
|
31
|
-
/^if \(-not \(\$global:LASTEXITCODE -is \[int\]\)\) \{ \$global:LASTEXITCODE = 0 \}/u,
|
|
32
|
-
/^Write-Output "__WECHAT_BRIDGE_DONE__:[^"]*"/u,
|
|
33
|
-
/^\$ErrorActionPreference = \$__wechatBridgePreviousErrorActionPreference/u,
|
|
34
|
-
/^\}/u,
|
|
35
|
-
];
|
|
36
|
-
pendingShellCommand = null;
|
|
37
|
-
interruptTimer = null;
|
|
38
|
-
outputBuffer = "";
|
|
39
|
-
currentCompletionMarker = null;
|
|
40
|
-
expectedEchoLines = [];
|
|
41
|
-
commandSequence = 0;
|
|
42
|
-
buildSpawnArgs() {
|
|
43
|
-
return this.getShellRuntime().launchArgs;
|
|
44
|
-
}
|
|
45
|
-
buildEnv() {
|
|
46
|
-
const env = super.buildEnv();
|
|
47
|
-
if (this.getShellRuntime().family === "posix") {
|
|
48
|
-
env.PS1 = "";
|
|
49
|
-
env.PROMPT = "";
|
|
50
|
-
env.RPROMPT = "";
|
|
51
|
-
}
|
|
52
|
-
return env;
|
|
53
|
-
}
|
|
54
|
-
afterStart() {
|
|
55
|
-
if (this.options.profile) {
|
|
56
|
-
this.writeToPty(`${buildShellProfileCommand(this.options.profile, this.getShellRuntime().family)}\r`);
|
|
57
|
-
}
|
|
58
|
-
if (this.getShellRuntime().family === "powershell") {
|
|
59
|
-
this.writeToPty('function global:prompt { "" }\r');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async sendInput(text) {
|
|
63
|
-
if (!this.pty) {
|
|
64
|
-
throw new Error("shell adapter is not running.");
|
|
65
|
-
}
|
|
66
|
-
if (this.state.status === "busy") {
|
|
67
|
-
throw new Error("shell is still working. Wait for the current reply or use /stop.");
|
|
68
|
-
}
|
|
69
|
-
if (this.pendingApproval || this.state.status === "awaiting_approval") {
|
|
70
|
-
throw new Error("A shell approval request is pending. Reply with /confirm or /deny.");
|
|
71
|
-
}
|
|
72
|
-
const rejectionMessage = getInteractiveShellCommandRejectionMessage(text);
|
|
73
|
-
if (rejectionMessage) {
|
|
74
|
-
throw new ShellCommandRejectedError(rejectionMessage);
|
|
75
|
-
}
|
|
76
|
-
if (isHighRiskShellCommand(text)) {
|
|
77
|
-
this.pendingShellCommand = text;
|
|
78
|
-
const request = {
|
|
79
|
-
source: "shell",
|
|
80
|
-
summary: "High-risk shell command detected. Confirmation is required.",
|
|
81
|
-
commandPreview: truncatePreview(text, 180),
|
|
82
|
-
};
|
|
83
|
-
this.pendingApproval = request;
|
|
84
|
-
this.state.pendingApproval = request;
|
|
85
|
-
this.state.pendingApprovalOrigin = "wechat";
|
|
86
|
-
this.setStatus("awaiting_approval", "Waiting for shell command approval.");
|
|
87
|
-
this.emit({
|
|
88
|
-
type: "approval_required",
|
|
89
|
-
request,
|
|
90
|
-
timestamp: nowIso(),
|
|
91
|
-
});
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
this.startShellCommand(text);
|
|
95
|
-
}
|
|
96
|
-
async interrupt() {
|
|
97
|
-
if (!this.pty) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
this.writeToPty("\u0003");
|
|
101
|
-
this.clearInterruptTimer();
|
|
102
|
-
this.interruptTimer = setTimeout(() => {
|
|
103
|
-
this.interruptTimer = null;
|
|
104
|
-
if (this.state.status === "busy" || this.state.status === "awaiting_approval") {
|
|
105
|
-
this.finishShellCommand({
|
|
106
|
-
summary: "Interrupted",
|
|
107
|
-
statusMessage: "Shell command interrupted.",
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}, 1_500);
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
async applyApproval(action, _pendingApproval) {
|
|
114
|
-
if (!this.pendingApproval) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
if (action === "deny") {
|
|
118
|
-
this.pendingShellCommand = null;
|
|
119
|
-
this.finishShellCommand({
|
|
120
|
-
summary: "Denied",
|
|
121
|
-
statusMessage: "Shell command denied.",
|
|
122
|
-
});
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
const command = this.pendingShellCommand;
|
|
126
|
-
if (!command) {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
this.pendingShellCommand = null;
|
|
130
|
-
this.startShellCommand(command);
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
handleData(rawText) {
|
|
134
|
-
const text = normalizeOutput(rawText);
|
|
135
|
-
if (!text) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
this.state.lastOutputAt = nowIso();
|
|
139
|
-
if (!this.hasAcceptedInput) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
this.outputBuffer = `${this.outputBuffer}${text}`;
|
|
143
|
-
this.flushShellOutputBuffer();
|
|
144
|
-
}
|
|
145
|
-
handleExit(exitCode) {
|
|
146
|
-
if (this.hasAcceptedInput) {
|
|
147
|
-
this.flushShellOutputBuffer(true);
|
|
148
|
-
}
|
|
149
|
-
this.resetShellCommandState();
|
|
150
|
-
super.handleExit(exitCode);
|
|
151
|
-
}
|
|
152
|
-
getShellRuntime() {
|
|
153
|
-
return resolveShellRuntime(this.options.command);
|
|
154
|
-
}
|
|
155
|
-
startShellCommand(text) {
|
|
156
|
-
if (!this.pty) {
|
|
157
|
-
throw new Error(`${this.options.kind} adapter is not running.`);
|
|
158
|
-
}
|
|
159
|
-
this.clearInterruptTimer();
|
|
160
|
-
this.resetShellCommandState({
|
|
161
|
-
preserveStatus: true,
|
|
162
|
-
preservePreview: true,
|
|
163
|
-
});
|
|
164
|
-
const completionMarker = this.createCompletionMarker();
|
|
165
|
-
const payload = buildShellInputPayload(text, this.getShellRuntime().family, completionMarker);
|
|
166
|
-
this.hasAcceptedInput = true;
|
|
167
|
-
this.currentPreview = truncatePreview(text);
|
|
168
|
-
this.state.lastInputAt = nowIso();
|
|
169
|
-
this.state.activeTurnOrigin = "wechat";
|
|
170
|
-
this.pendingApproval = null;
|
|
171
|
-
this.state.pendingApproval = null;
|
|
172
|
-
this.state.pendingApprovalOrigin = undefined;
|
|
173
|
-
this.currentCompletionMarker = completionMarker;
|
|
174
|
-
this.expectedEchoLines = normalizeOutput(payload)
|
|
175
|
-
.split("\n")
|
|
176
|
-
.map((line) => line.trim())
|
|
177
|
-
.filter(Boolean);
|
|
178
|
-
this.writeToPty(payload);
|
|
179
|
-
this.setStatus("busy");
|
|
180
|
-
}
|
|
181
|
-
flushShellOutputBuffer(force = false) {
|
|
182
|
-
const lastNewlineIndex = this.outputBuffer.lastIndexOf("\n");
|
|
183
|
-
if (!force && lastNewlineIndex < 0) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const sliceEnd = force
|
|
187
|
-
? this.outputBuffer.length
|
|
188
|
-
: Math.max(0, lastNewlineIndex + 1);
|
|
189
|
-
if (sliceEnd === 0) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const chunk = this.outputBuffer.slice(0, sliceEnd);
|
|
193
|
-
this.outputBuffer = this.outputBuffer.slice(sliceEnd);
|
|
194
|
-
const visibleLines = [];
|
|
195
|
-
let completedExitCode = null;
|
|
196
|
-
for (const rawLine of chunk.split("\n")) {
|
|
197
|
-
if (!rawLine && !force) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const completionExitCode = this.parseCompletionExitCode(rawLine);
|
|
201
|
-
if (completionExitCode !== null) {
|
|
202
|
-
completedExitCode = completionExitCode;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const filtered = this.filterShellOutputLine(rawLine);
|
|
206
|
-
if (filtered) {
|
|
207
|
-
visibleLines.push(filtered);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const visibleText = visibleLines.join("\n").trim();
|
|
211
|
-
if (visibleText) {
|
|
212
|
-
this.emit({
|
|
213
|
-
type: "stdout",
|
|
214
|
-
text: visibleText,
|
|
215
|
-
timestamp: nowIso(),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
if (completedExitCode !== null) {
|
|
219
|
-
this.finishShellCommand({ exitCode: completedExitCode });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
filterShellOutputLine(line) {
|
|
223
|
-
const cleanedLine = this.getShellRuntime().family === "powershell"
|
|
224
|
-
? this.stripLeadingPowerShellNoise(line)
|
|
225
|
-
: line;
|
|
226
|
-
const trimmed = cleanedLine.trim();
|
|
227
|
-
if (!trimmed) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
if (this.consumeExpectedEchoLine(trimmed)) {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
if (this.isShellPromptLine(trimmed)) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
if (this.getShellRuntime().family === "posix") {
|
|
237
|
-
if (trimmed === "__wechat_bridge_status=$?" ||
|
|
238
|
-
trimmed.startsWith("printf '%s:%s\\n'") ||
|
|
239
|
-
trimmed.startsWith("printf '__WECHAT_BRIDGE_DONE__:%s")) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return cleanedLine;
|
|
244
|
-
}
|
|
245
|
-
consumeExpectedEchoLine(line) {
|
|
246
|
-
if (!this.expectedEchoLines.length) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
const normalizedLine = this.normalizeEchoLine(line);
|
|
250
|
-
if (normalizedLine !== this.expectedEchoLines[0]) {
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
this.expectedEchoLines.shift();
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
normalizeEchoLine(line) {
|
|
257
|
-
const trimmed = line.trim();
|
|
258
|
-
if (this.getShellRuntime().family === "powershell") {
|
|
259
|
-
return trimmed.replace(/^(?:PS [^>]*>\s*|PS>\s*|>>\s*|>\s*)+/u, "").trim();
|
|
260
|
-
}
|
|
261
|
-
return trimmed.replace(/^(?:[$#>]\s*)+/u, "").trim();
|
|
262
|
-
}
|
|
263
|
-
isShellPromptLine(line) {
|
|
264
|
-
if (this.getShellRuntime().family === "powershell") {
|
|
265
|
-
return /^(?:PS [^>]*>\s*|PS>\s*|>>\s*|>\s*)+$/u.test(line);
|
|
266
|
-
}
|
|
267
|
-
return /^(?:[$#>]\s*)+$/u.test(line);
|
|
268
|
-
}
|
|
269
|
-
parseCompletionExitCode(line) {
|
|
270
|
-
const marker = this.currentCompletionMarker;
|
|
271
|
-
if (!marker) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
const trimmed = this.stripLeadingShellPromptTokens(line).trim();
|
|
275
|
-
const prefix = `${marker}:`;
|
|
276
|
-
if (!trimmed.startsWith(prefix)) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
const exitCodeText = trimmed.slice(prefix.length).trim();
|
|
280
|
-
if (!/^-?\d+$/u.test(exitCodeText)) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
return Number(exitCodeText);
|
|
284
|
-
}
|
|
285
|
-
finishShellCommand(options) {
|
|
286
|
-
const summary = options.summary ?? this.currentPreview;
|
|
287
|
-
this.resetShellCommandState();
|
|
288
|
-
this.setStatus("idle", options.statusMessage);
|
|
289
|
-
this.emit({
|
|
290
|
-
type: "task_complete",
|
|
291
|
-
exitCode: options.exitCode,
|
|
292
|
-
summary,
|
|
293
|
-
timestamp: nowIso(),
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
resetShellCommandState(options = {}) {
|
|
297
|
-
this.clearInterruptTimer();
|
|
298
|
-
this.clearCompletionTimer();
|
|
299
|
-
this.pendingApproval = null;
|
|
300
|
-
this.pendingShellCommand = null;
|
|
301
|
-
this.state.pendingApproval = null;
|
|
302
|
-
this.state.pendingApprovalOrigin = undefined;
|
|
303
|
-
this.state.activeTurnOrigin = undefined;
|
|
304
|
-
this.hasAcceptedInput = false;
|
|
305
|
-
this.outputBuffer = "";
|
|
306
|
-
this.currentCompletionMarker = null;
|
|
307
|
-
this.expectedEchoLines = [];
|
|
308
|
-
if (!options.preservePreview) {
|
|
309
|
-
this.currentPreview = "(idle)";
|
|
310
|
-
}
|
|
311
|
-
if (!options.preserveStatus && this.state.status !== "stopped") {
|
|
312
|
-
this.state.status = "idle";
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
clearInterruptTimer() {
|
|
316
|
-
if (!this.interruptTimer) {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
clearTimeout(this.interruptTimer);
|
|
320
|
-
this.interruptTimer = null;
|
|
321
|
-
}
|
|
322
|
-
createCompletionMarker() {
|
|
323
|
-
this.commandSequence += 1;
|
|
324
|
-
return `${ShellAdapter.COMPLETION_MARKER_PREFIX}:${this.commandSequence.toString(36)}`;
|
|
325
|
-
}
|
|
326
|
-
stripLeadingPowerShellNoise(line) {
|
|
327
|
-
let text = line.trimStart();
|
|
328
|
-
while (text) {
|
|
329
|
-
let stripped = false;
|
|
330
|
-
for (const pattern of ShellAdapter.POWERSHELL_LEADING_NOISE_PATTERNS) {
|
|
331
|
-
const next = text.replace(pattern, "");
|
|
332
|
-
if (next !== text) {
|
|
333
|
-
text = next.trimStart();
|
|
334
|
-
stripped = true;
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
if (!stripped) {
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return text;
|
|
343
|
-
}
|
|
344
|
-
stripLeadingShellPromptTokens(line) {
|
|
345
|
-
let text = line.trimStart();
|
|
346
|
-
if (this.getShellRuntime().family === "powershell") {
|
|
347
|
-
while (true) {
|
|
348
|
-
const next = text
|
|
349
|
-
.replace(/^PS>>\s*/u, "")
|
|
350
|
-
.replace(/^PS [^>\n]*>\s*/u, "")
|
|
351
|
-
.replace(/^PS>\s*/u, "")
|
|
352
|
-
.replace(/^>>\s*/u, "")
|
|
353
|
-
.replace(/^>\s*/u, "");
|
|
354
|
-
if (next === text) {
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
text = next.trimStart();
|
|
358
|
-
}
|
|
359
|
-
return text;
|
|
360
|
-
}
|
|
361
|
-
return text.replace(/^(?:[$#>]\s*)+/u, "").trimStart();
|
|
362
|
-
}
|
|
363
|
-
}
|