@unlinearity/cli-wechat-bridge 1.0.0 → 1.0.2

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.
Files changed (81) hide show
  1. package/bin/_run-entry.mjs +2 -8
  2. package/bin/wechat-bridge-claude.mjs +2 -2
  3. package/bin/wechat-bridge-codex.mjs +2 -2
  4. package/bin/wechat-bridge-opencode.mjs +2 -2
  5. package/bin/wechat-bridge-shell.mjs +2 -2
  6. package/bin/wechat-bridge.mjs +2 -2
  7. package/bin/wechat-check-update.mjs +2 -2
  8. package/bin/wechat-claude-start.mjs +2 -2
  9. package/bin/wechat-claude.mjs +2 -2
  10. package/bin/wechat-codex-start.mjs +2 -2
  11. package/bin/wechat-codex.mjs +2 -2
  12. package/bin/wechat-opencode-start.mjs +2 -2
  13. package/bin/wechat-opencode.mjs +2 -2
  14. package/bin/wechat-setup.mjs +5 -0
  15. package/dist/bridge/bridge-adapters.claude.js +829 -0
  16. package/dist/bridge/bridge-adapters.codex.js +2206 -0
  17. package/dist/bridge/bridge-adapters.core.js +668 -0
  18. package/dist/bridge/bridge-adapters.js +26 -0
  19. package/dist/bridge/bridge-adapters.opencode.js +1777 -0
  20. package/dist/bridge/bridge-adapters.shared.js +1098 -0
  21. package/dist/bridge/bridge-adapters.shell.js +363 -0
  22. package/dist/bridge/bridge-controller.js +44 -0
  23. package/dist/bridge/bridge-final-reply.js +31 -0
  24. package/dist/bridge/bridge-process-reaper.js +348 -0
  25. package/dist/bridge/bridge-state.js +356 -0
  26. package/dist/bridge/bridge-types.js +1 -0
  27. package/dist/bridge/bridge-utils.js +1150 -0
  28. package/dist/bridge/claude-hook.js +82 -0
  29. package/dist/bridge/claude-hooks.js +267 -0
  30. package/dist/bridge/wechat-bridge.js +904 -0
  31. package/dist/commands/check-update.js +30 -0
  32. package/dist/companion/codex-panel-link.js +72 -0
  33. package/dist/companion/codex-panel.js +179 -0
  34. package/dist/companion/codex-remote-client.js +117 -0
  35. package/dist/companion/local-companion-link.js +210 -0
  36. package/dist/companion/local-companion-start.js +372 -0
  37. package/dist/companion/local-companion.js +411 -0
  38. package/dist/media/media-types.js +1 -0
  39. package/dist/runtime/create-runtime-host.js +12 -0
  40. package/dist/runtime/legacy-adapter-runtime.js +40 -0
  41. package/dist/runtime/runtime-types.js +5 -0
  42. package/dist/utils/version-checker.js +161 -0
  43. package/dist/wechat/channel-config.js +103 -0
  44. package/dist/wechat/setup.js +283 -0
  45. package/dist/wechat/standalone-bot.js +356 -0
  46. package/dist/wechat/wechat-channel.js +481 -0
  47. package/dist/wechat/wechat-transport.js +887 -0
  48. package/package.json +6 -2
  49. package/src/bridge/bridge-adapters.claude.ts +0 -1047
  50. package/src/bridge/bridge-adapters.codex.ts +0 -2733
  51. package/src/bridge/bridge-adapters.core.ts +0 -877
  52. package/src/bridge/bridge-adapters.opencode.ts +0 -2390
  53. package/src/bridge/bridge-adapters.shared.ts +0 -1647
  54. package/src/bridge/bridge-adapters.shell.ts +0 -456
  55. package/src/bridge/bridge-adapters.ts +0 -30
  56. package/src/bridge/bridge-controller.ts +0 -58
  57. package/src/bridge/bridge-final-reply.ts +0 -56
  58. package/src/bridge/bridge-process-reaper.ts +0 -417
  59. package/src/bridge/bridge-state.ts +0 -520
  60. package/src/bridge/bridge-types.ts +0 -218
  61. package/src/bridge/bridge-utils.ts +0 -1524
  62. package/src/bridge/claude-hook.ts +0 -92
  63. package/src/bridge/claude-hooks.ts +0 -376
  64. package/src/bridge/wechat-bridge.ts +0 -1305
  65. package/src/commands/check-update.ts +0 -46
  66. package/src/companion/codex-panel-link.ts +0 -151
  67. package/src/companion/codex-panel.ts +0 -225
  68. package/src/companion/codex-remote-client.ts +0 -178
  69. package/src/companion/local-companion-link.ts +0 -338
  70. package/src/companion/local-companion-start.ts +0 -520
  71. package/src/companion/local-companion.ts +0 -532
  72. package/src/media/media-types.ts +0 -11
  73. package/src/runtime/create-runtime-host.ts +0 -16
  74. package/src/runtime/legacy-adapter-runtime.ts +0 -55
  75. package/src/runtime/runtime-types.ts +0 -51
  76. package/src/utils/version-checker.ts +0 -197
  77. package/src/wechat/channel-config.ts +0 -141
  78. package/src/wechat/setup.ts +0 -232
  79. package/src/wechat/standalone-bot.ts +0 -483
  80. package/src/wechat/wechat-channel.ts +0 -608
  81. package/src/wechat/wechat-transport.ts +0 -1290
@@ -0,0 +1,363 @@
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 <code> 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
+ }
@@ -0,0 +1,44 @@
1
+ import { clearLocalCompanionEndpoint, readLocalCompanionEndpoint, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
2
+ import { hasLocalClientEndpointProvider } from "../runtime/runtime-types.js";
3
+ export class BridgeController {
4
+ endpointInstanceId = null;
5
+ adapter;
6
+ cwd;
7
+ constructor(adapter, cwd) {
8
+ this.adapter = adapter;
9
+ this.cwd = cwd;
10
+ }
11
+ syncLocalClientEndpoint() {
12
+ if (!hasLocalClientEndpointProvider(this.adapter)) {
13
+ return;
14
+ }
15
+ const endpoint = this.adapter.getLocalClientEndpoint();
16
+ if (!endpoint) {
17
+ this.clearLocalClientEndpoint();
18
+ return;
19
+ }
20
+ const existing = readLocalCompanionEndpoint(this.cwd);
21
+ const adapterState = this.adapter.getState();
22
+ const nextEndpoint = existing?.instanceId === endpoint.instanceId
23
+ ? {
24
+ ...endpoint,
25
+ companionPid: endpoint.companionPid ?? existing.companionPid,
26
+ companionConnectedAt: endpoint.companionConnectedAt ?? existing.companionConnectedAt,
27
+ companionStatus: adapterState.status,
28
+ companionLastStateAt: new Date().toISOString(),
29
+ companionWorkerPid: adapterState.pid,
30
+ }
31
+ : {
32
+ ...endpoint,
33
+ companionStatus: adapterState.status,
34
+ companionLastStateAt: new Date().toISOString(),
35
+ companionWorkerPid: adapterState.pid,
36
+ };
37
+ this.endpointInstanceId = endpoint.instanceId;
38
+ writeLocalCompanionEndpoint(nextEndpoint);
39
+ }
40
+ clearLocalClientEndpoint() {
41
+ clearLocalCompanionEndpoint(this.cwd, this.endpointInstanceId ?? undefined);
42
+ this.endpointInstanceId = null;
43
+ }
44
+ }
@@ -0,0 +1,31 @@
1
+ import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, } from "./bridge-utils.js";
2
+ export async function forwardWechatFinalReply(params) {
3
+ const { adapter, rawText, sender } = params;
4
+ const parsed = parseWechatFinalReply(rawText);
5
+ const visibleText = formatFinalReplyMessage(adapter, sanitizeWechatFinalReplyText(adapter, parsed.visibleText)).trim();
6
+ if (visibleText) {
7
+ await sender.sendText(visibleText);
8
+ }
9
+ for (const attachment of parsed.attachments) {
10
+ try {
11
+ switch (attachment.kind) {
12
+ case "image":
13
+ await sender.sendImage(attachment.path);
14
+ break;
15
+ case "file":
16
+ await sender.sendFile(attachment.path);
17
+ break;
18
+ case "voice":
19
+ await sender.sendVoice(attachment.path);
20
+ break;
21
+ case "video":
22
+ await sender.sendVideo(attachment.path);
23
+ break;
24
+ }
25
+ }
26
+ catch (error) {
27
+ const errorText = error instanceof Error ? error.message : String(error ?? "unknown error");
28
+ await sender.sendText(`Failed to send ${attachment.kind} attachment: ${attachment.path}\n${errorText}`);
29
+ }
30
+ }
31
+ }