@unlinearity/cli-wechat-bridge 1.1.5 → 1.2.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.
Files changed (89) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +434 -427
  3. package/bin/_run-entry.mjs +47 -9
  4. package/bin/wechat-check-update.mjs +6 -1
  5. package/bin/wechat-claude.mjs +6 -1
  6. package/bin/wechat-codex.mjs +6 -1
  7. package/bin/wechat-daemon.mjs +6 -1
  8. package/bin/wechat-opencode.mjs +6 -1
  9. package/bin/wechat-pi.mjs +6 -1
  10. package/bin/wechat-setup.mjs +6 -1
  11. package/bin/wecom-check-update.mjs +10 -0
  12. package/bin/wecom-claude.mjs +10 -0
  13. package/bin/wecom-codex.mjs +10 -0
  14. package/bin/wecom-daemon.mjs +10 -0
  15. package/bin/wecom-opencode.mjs +10 -0
  16. package/bin/wecom-pi.mjs +10 -0
  17. package/bin/wecom-setup.mjs +10 -0
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +163 -5
  20. package/dist/bridge/bridge-adapters.codex.js +331 -29
  21. package/dist/bridge/bridge-adapters.core.js +27 -2
  22. package/dist/bridge/bridge-adapters.opencode.js +156 -1
  23. package/dist/bridge/bridge-adapters.pi.js +46 -1
  24. package/dist/bridge/bridge-adapters.shared.js +8 -1
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +8 -65
  27. package/dist/bridge/bridge-process-reaper.js +41 -2
  28. package/dist/bridge/bridge-state.js +17 -4
  29. package/dist/bridge/bridge-utils.js +10 -127
  30. package/dist/bridge/claude-hook.js +6 -0
  31. package/dist/bridge/claude-native-controls.js +133 -0
  32. package/dist/bridge/native-terminal-control.js +259 -0
  33. package/dist/bridge/wechat-bridge.js +520 -274
  34. package/dist/bridge/wechat-forwarding.js +8 -58
  35. package/dist/bridge/wechat-outbound-queue.js +8 -0
  36. package/dist/channels/wechat/bridge-final-reply.js +56 -0
  37. package/dist/channels/wechat/channel-message.js +24 -0
  38. package/dist/channels/wechat/index.js +11 -0
  39. package/dist/channels/wechat/wechat-channel-port.js +67 -0
  40. package/dist/channels/wechat/wechat-driver.js +191 -0
  41. package/dist/channels/wechat/wechat-forwarding.js +97 -0
  42. package/dist/channels/wechat/wechat-outbound-queue.js +118 -0
  43. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  44. package/dist/channels/wecom/setup-cli.js +12 -0
  45. package/dist/channels/wecom/setup.js +182 -0
  46. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  47. package/dist/channels/wecom/wecom-config.js +72 -0
  48. package/dist/channels/wecom/wecom-driver.js +106 -0
  49. package/dist/channels/wecom/wecom-media.js +147 -0
  50. package/dist/channels/wecom/wecom-message.js +149 -0
  51. package/dist/channels/wecom/wecom-transport.js +314 -0
  52. package/dist/commands/check-update.js +6 -0
  53. package/dist/companion/codex-remote-client.js +30 -3
  54. package/dist/companion/codex-visible-client-link.js +107 -0
  55. package/dist/companion/local-companion-link.js +6 -0
  56. package/dist/companion/local-companion-start.js +62 -11
  57. package/dist/companion/local-companion.js +25 -1
  58. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  59. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  60. package/dist/core/bridge-control.js +42 -0
  61. package/dist/core/bridge-defer.js +25 -0
  62. package/dist/core/bridge-event-forwarder.js +50 -0
  63. package/dist/core/bridge-message-router.js +32 -0
  64. package/dist/core/channel-driver.js +1 -0
  65. package/dist/core/channel-types.js +3 -0
  66. package/dist/core/conversation-routing.js +58 -0
  67. package/dist/core/direct-run.js +23 -0
  68. package/dist/core/inbound-message-claims.js +67 -0
  69. package/dist/core/outbound-target.js +32 -0
  70. package/dist/core/text-utils.js +69 -0
  71. package/dist/core/turn-coordinator.js +85 -0
  72. package/dist/daemon/daemon-link.js +9 -0
  73. package/dist/daemon/emoji-bindings.js +6 -0
  74. package/dist/daemon/wechat-daemon.js +827 -209
  75. package/dist/i18n/index.js +6 -0
  76. package/dist/i18n/messages-en.js +11 -3
  77. package/dist/i18n/messages-zh.js +11 -3
  78. package/dist/runtime/create-runtime-host.js +6 -0
  79. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  80. package/dist/runtime/runtime-types.js +3 -0
  81. package/dist/utils/atomic-file.js +6 -0
  82. package/dist/utils/doctor.js +36 -10
  83. package/dist/utils/version-checker.js +17 -11
  84. package/dist/wechat/channel-config.js +6 -0
  85. package/dist/wechat/setup.js +119 -13
  86. package/dist/wechat/wechat-channel.js +6 -0
  87. package/dist/wechat/wechat-transport.js +169 -58
  88. package/package.json +14 -4
  89. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -1,9 +1,16 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import net from "node:net";
2
8
  import path from "node:path";
3
9
  import { spawn as spawnPty } from "node-pty";
4
10
  import { t } from "../i18n/index.js";
5
11
  import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, sendLocalCompanionMessage, updateLocalCompanionOccupancy, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
6
- import { detectCliApproval, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
12
+ import { detectCliApproval, } from "./bridge-utils.js";
13
+ import { normalizeOutput, nowIso, truncatePreview } from "../core/text-utils.js";
7
14
  import * as shared from "./bridge-adapters.shared.js";
8
15
  import { LOCAL_CLIENT_PROTOCOL_VERSION } from "../runtime/runtime-types.js";
9
16
  const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, buildPtySpawnOptions, buildSpawnDiagnostic, getLocalCompanionCommandName, getSharedSessionIdFromAdapterState, LOCAL_COMPANION_RECONNECT_GRACE_MS, resolveSpawnTarget, spawnFallbackProcess, } = shared;
@@ -14,7 +21,7 @@ const { CODEX_APP_SERVER_HOST, INTERRUPT_SETTLE_DELAY_MS, buildCliEnvironment, b
14
21
  * the script path contains spaces: node-pty quotes the already-quoted path,
15
22
  * making cmd.exe treat the literal quotes as part of the filename.
16
23
  */
17
- function unwrapCmdExeForPty(rawTarget) {
24
+ export function unwrapCmdExeForPty(rawTarget) {
18
25
  if (process.platform !== "win32") {
19
26
  return rawTarget;
20
27
  }
@@ -165,6 +172,15 @@ export class LocalCompanionProxyAdapter {
165
172
  command: "create_session",
166
173
  });
167
174
  }
175
+ async listModels() {
176
+ return await this.sendRequest({ command: "list_models" });
177
+ }
178
+ async selectModel(modelId) {
179
+ return await this.sendRequest({ command: "select_model", modelId });
180
+ }
181
+ async setPlanMode(enabled) {
182
+ return await this.sendRequest({ command: "set_plan_mode", enabled });
183
+ }
168
184
  async interrupt() {
169
185
  const result = await this.sendRequest({
170
186
  command: "interrupt",
@@ -611,6 +627,15 @@ export class AbstractPtyAdapter {
611
627
  setEventSink(sink) {
612
628
  this.eventSink = sink;
613
629
  }
630
+ async listModels() {
631
+ throw new Error(`/${this.options.kind} model selection is not available.`);
632
+ }
633
+ async selectModel(_modelId) {
634
+ throw new Error(`/${this.options.kind} model selection is not available.`);
635
+ }
636
+ async setPlanMode(_enabled) {
637
+ throw new Error(`/${this.options.kind} plan mode is not available.`);
638
+ }
614
639
  async start() {
615
640
  if (this.pty) {
616
641
  return;
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import { spawn as spawnChildProcess } from "node:child_process";
2
8
  import { randomBytes } from "node:crypto";
3
9
  import fs from "node:fs";
@@ -85,6 +91,139 @@ export class OpenCodeServerAdapter {
85
91
  pendingLocalSessionCreateFollowUntilMs = 0;
86
92
  pendingPermission = null;
87
93
  pendingQuestion = null;
94
+ previousAgentBySession = new Map();
95
+ async listModels() {
96
+ this.assertRemoteControlReady();
97
+ const session = await this.ensureControlSession();
98
+ const models = await this.listControlModels();
99
+ return models.map((model) => ({
100
+ id: model.id,
101
+ displayName: model.displayName,
102
+ isCurrent: session.model?.providerID === model.providerID &&
103
+ session.model.id === model.modelID,
104
+ }));
105
+ }
106
+ async selectModel(modelId) {
107
+ this.assertRemoteControlReady();
108
+ const session = await this.ensureControlSession();
109
+ const models = await this.listControlModels();
110
+ const target = models.find((model) => model.id === modelId);
111
+ if (!target) {
112
+ throw new Error("This OpenCode model is no longer available. Send /model again.");
113
+ }
114
+ const currentVariant = session.model?.variant;
115
+ const variant = currentVariant && currentVariant !== "default" && target.variants.includes(currentVariant)
116
+ ? currentVariant
117
+ : "default";
118
+ const result = await this.client.v2.session.switchModel({
119
+ sessionID: session.id,
120
+ model: {
121
+ providerID: target.providerID,
122
+ id: target.modelID,
123
+ variant,
124
+ },
125
+ });
126
+ this.unwrapOrThrow(result);
127
+ const updated = await this.getSessionForCurrentDirectory(session.id);
128
+ if (!updated ||
129
+ updated.model?.providerID !== target.providerID ||
130
+ updated.model.id !== target.modelID ||
131
+ (updated.model.variant ?? "default") !== variant) {
132
+ throw new Error("OpenCode did not confirm the selected model.");
133
+ }
134
+ return { id: target.id, displayName: target.displayName, isCurrent: true };
135
+ }
136
+ async setPlanMode(enabled) {
137
+ this.assertRemoteControlReady();
138
+ const session = await this.ensureControlSession();
139
+ const agents = await this.listPrimaryAgents();
140
+ const planAgent = agents.find((agent) => agent.name === "plan");
141
+ if (!planAgent) {
142
+ throw new Error("OpenCode primary agent 'plan' is not available.");
143
+ }
144
+ const currentAgent = session.agent ?? agents[0]?.name;
145
+ if (!currentAgent) {
146
+ throw new Error("OpenCode did not return any primary agents.");
147
+ }
148
+ if (enabled && currentAgent === "plan") {
149
+ return true;
150
+ }
151
+ if (!enabled && currentAgent !== "plan") {
152
+ this.previousAgentBySession.delete(session.id);
153
+ return false;
154
+ }
155
+ const previousAgent = this.previousAgentBySession.get(session.id);
156
+ const target = enabled
157
+ ? "plan"
158
+ : agents.some((agent) => agent.name === previousAgent)
159
+ ? previousAgent
160
+ : agents.find((agent) => agent.name === "build")?.name ??
161
+ agents.find((agent) => agent.name !== "plan")?.name;
162
+ if (!target) {
163
+ throw new Error("OpenCode did not return an agent that can replace plan mode.");
164
+ }
165
+ const result = await this.client.v2.session.switchAgent({
166
+ sessionID: session.id,
167
+ agent: target,
168
+ });
169
+ this.unwrapOrThrow(result);
170
+ const updated = await this.getSessionForCurrentDirectory(session.id);
171
+ if (!updated || updated.agent !== target) {
172
+ throw new Error("OpenCode did not confirm the selected agent.");
173
+ }
174
+ if (enabled) {
175
+ this.previousAgentBySession.set(session.id, currentAgent);
176
+ }
177
+ else {
178
+ this.previousAgentBySession.delete(session.id);
179
+ }
180
+ return enabled;
181
+ }
182
+ assertRemoteControlReady() {
183
+ if (!this.client) {
184
+ throw new Error("OpenCode is not running.");
185
+ }
186
+ if (this.state.status === "busy") {
187
+ throw new Error("OpenCode is still working. Wait for the current reply or use /stop.");
188
+ }
189
+ if (this.pendingPermission) {
190
+ throw new Error("An OpenCode approval request is pending. Reply with /confirm or /deny.");
191
+ }
192
+ if (this.pendingQuestion) {
193
+ throw new Error("OpenCode is waiting for user input. Reply with /answer or use /stop.");
194
+ }
195
+ }
196
+ async ensureControlSession() {
197
+ const session = await this.ensureSession();
198
+ if (session.id !== this.activeSessionId) {
199
+ this.switchSharedSession(session, {
200
+ source: "wechat",
201
+ reason: "wechat_resume",
202
+ syncVisible: true,
203
+ forceVisibleSync: true,
204
+ });
205
+ }
206
+ return session;
207
+ }
208
+ async listPrimaryAgents() {
209
+ if (!this.client)
210
+ throw new Error("OpenCode is not running.");
211
+ const agents = this.unwrapOrThrow(await this.client.app.agents({
212
+ directory: this.options.cwd,
213
+ workspace: this.activeWorkspaceId ?? undefined,
214
+ }, { signal: AbortSignal.timeout(10_000) }));
215
+ return agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden);
216
+ }
217
+ async listControlModels() {
218
+ if (!this.client)
219
+ throw new Error("OpenCode is not running.");
220
+ const providers = this.unwrapOrThrow(await this.client.provider.list({ directory: this.options.cwd, workspace: this.activeWorkspaceId ?? undefined }, { signal: AbortSignal.timeout(10_000) }));
221
+ return providers.all.filter((provider) => providers.connected.includes(provider.id)).flatMap((provider) => Object.entries(provider.models).filter(([id, model]) => model.status !== "deprecated" && !(provider.id === "opencode" && id.includes("-nano"))).map(([id, model]) => ({
222
+ id: `${provider.id}/${id}`, displayName: `${model.name || id} (${provider.name})`,
223
+ providerID: provider.id, modelID: id,
224
+ variants: Object.keys(model.variants ?? {}),
225
+ })));
226
+ }
88
227
  constructor(options) {
89
228
  this.options = options;
90
229
  this.state = {
@@ -170,10 +309,21 @@ export class OpenCodeServerAdapter {
170
309
  this.recordPendingWechatPromptMirrorSuppression(session.id, normalized);
171
310
  this.beginTrackedTurn(normalized, "wechat");
172
311
  try {
312
+ const selection = await this.getSessionForCurrentDirectory(session.id) ?? session;
173
313
  const result = await this.client.session.promptAsync({
174
314
  sessionID: session.id,
175
315
  directory: this.options.cwd,
176
316
  workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
317
+ ...(selection.agent ? { agent: selection.agent } : {}),
318
+ ...(selection.model
319
+ ? {
320
+ model: {
321
+ providerID: selection.model.providerID,
322
+ modelID: selection.model.id,
323
+ },
324
+ variant: selection.model.variant ?? "default",
325
+ }
326
+ : {}),
177
327
  parts: [{ type: "text", text: normalized }],
178
328
  });
179
329
  if (result.error !== undefined) {
@@ -411,6 +561,7 @@ export class OpenCodeServerAdapter {
411
561
  this.pendingLocalSessionCreateFollowUntilMs = 0;
412
562
  this.clearObservedMessageTracking();
413
563
  this.recentSdkEventObservations.clear();
564
+ this.previousAgentBySession.clear();
414
565
  this.outputBatcher.clear();
415
566
  this.clearStreamedPartState();
416
567
  this.clearPendingPermissionState();
@@ -1535,7 +1686,11 @@ export class OpenCodeServerAdapter {
1535
1686
  return;
1536
1687
  }
1537
1688
  const sessionId = this.extractSessionId(properties);
1538
- if (!sessionId || sessionId !== this.activeSessionId) {
1689
+ if (!sessionId) {
1690
+ return;
1691
+ }
1692
+ this.previousAgentBySession.delete(sessionId);
1693
+ if (sessionId !== this.activeSessionId) {
1539
1694
  return;
1540
1695
  }
1541
1696
  this.clearTrackedTurnForLocalSessionSwitch();
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import crypto from "node:crypto";
2
8
  import fs from "node:fs";
3
9
  import net from "node:net";
@@ -7,7 +13,7 @@ import { spawn } from "node:child_process";
7
13
  import { fileURLToPath } from "node:url";
8
14
  import { assertNoReservedExtraCliArgs, buildCliEnvironment, describeUnknownError, isRecord, resolveSpawnTarget, } from "./bridge-adapters.shared.js";
9
15
  import { killProcessTreeSync } from "./bridge-process-reaper.js";
10
- import { normalizeOutput, nowIso } from "./bridge-utils.js";
16
+ import { normalizeOutput, nowIso } from "../core/text-utils.js";
11
17
  const PI_TUI_CONNECT_WARNING_MS = 30_000;
12
18
  const PI_TUI_REQUEST_TIMEOUT_MS = 30_000;
13
19
  const PI_TUI_RECONNECT_TIMEOUT_MS = 10_000;
@@ -356,6 +362,41 @@ export class PiTuiAdapter {
356
362
  this.pendingSessionSwitch = null;
357
363
  }
358
364
  }
365
+ async listModels() {
366
+ if (this.state.status === "busy") {
367
+ throw new Error("Pi is still working. Wait for the current turn before listing models.");
368
+ }
369
+ const response = await this.sendCommand("list_models");
370
+ if (!isRecord(response.data) || !Array.isArray(response.data.models)) {
371
+ throw new Error("Pi TUI returned an invalid model list.");
372
+ }
373
+ return response.data.models
374
+ .filter((model) => isRecord(model))
375
+ .filter((model) => typeof model.id === "string" && typeof model.displayName === "string")
376
+ .map((model) => ({
377
+ id: model.id,
378
+ displayName: model.displayName,
379
+ isCurrent: model.isCurrent === true,
380
+ }));
381
+ }
382
+ async selectModel(modelId) {
383
+ if (this.state.status === "busy") {
384
+ throw new Error("Pi is still working. Wait for the current turn before switching models.");
385
+ }
386
+ const response = await this.sendCommand("select_model", { modelId });
387
+ if (!isRecord(response.data) || !isRecord(response.data.model)) {
388
+ throw new Error("Pi TUI returned an invalid selected model.");
389
+ }
390
+ const model = response.data.model;
391
+ if (typeof model.id !== "string" || typeof model.displayName !== "string") {
392
+ throw new Error("Pi TUI returned an invalid selected model.");
393
+ }
394
+ return {
395
+ id: model.id,
396
+ displayName: model.displayName,
397
+ isCurrent: true,
398
+ };
399
+ }
359
400
  async interrupt() {
360
401
  if (!this.socket || this.state.status !== "busy") {
361
402
  return false;
@@ -569,6 +610,10 @@ export class PiTuiAdapter {
569
610
  else if (this.currentAssistantText) {
570
611
  this.emit({ type: "final_reply", text: this.currentAssistantText, timestamp: nowIso() });
571
612
  }
613
+ // Mirror the other adapters: a settled turn always reports completion so
614
+ // bridge/daemon ownership state is released even when the turn produced
615
+ // no visible reply.
616
+ this.emit({ type: "task_complete", timestamp: nowIso() });
572
617
  this.state.lastOutputAt = nowIso();
573
618
  this.clearActiveTurn();
574
619
  this.setStatus("idle");
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import fs from "node:fs";
2
8
  import os from "node:os";
3
9
  import path from "node:path";
@@ -6,7 +12,8 @@ import { fileURLToPath } from "node:url";
6
12
  import { spawn, spawnSync } from "node:child_process";
7
13
  import { t } from "../i18n/index.js";
8
14
  import { spawn as spawnPty } from "node-pty";
9
- import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, isStrictApprovalModeEnabled, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
15
+ import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, isStrictApprovalModeEnabled, } from "./bridge-utils.js";
16
+ import { normalizeOutput, truncatePreview } from "../core/text-utils.js";
10
17
  import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
11
18
  export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
12
19
  export const DEFAULT_COLS = 120;
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import { clearLocalCompanionEndpoint, readLocalCompanionEndpoint, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
2
8
  import { hasLocalClientEndpointProvider } from "../runtime/runtime-types.js";
3
9
  export class BridgeController {
@@ -1,65 +1,8 @@
1
- import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, splitWechatTextIntoChunks, } from "./bridge-utils.js";
2
- export const OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE = "OpenCode 没有产生可发送到微信的可见回复。请查看本地终端输出,或重试这条消息。";
3
- export async function forwardWechatFinalReply(params) {
4
- const { adapter, rawText, sender, onEmptyVisibleReply } = params;
5
- const parsed = parseWechatFinalReply(rawText);
6
- const sanitizedText = sanitizeWechatFinalReplyText(adapter, parsed.visibleText);
7
- const visibleText = formatFinalReplyMessage(adapter, sanitizedText).trim();
8
- if (visibleText) {
9
- // Send long replies in bounded chunks: a single oversized sendmessage call
10
- // can be rejected by the WeChat API, silently losing the whole reply.
11
- const chunks = splitWechatTextIntoChunks(visibleText);
12
- for (let index = 0; index < chunks.length; index += 1) {
13
- const sent = await sender.sendText(chunks[index]);
14
- if (sent === false) {
15
- // The send channel is failing (e.g. an expired context token).
16
- // Report what was dropped instead of ending mid-reply silently;
17
- // this notice itself is best effort and may also fail.
18
- const remainingChunks = chunks.length - index - 1;
19
- if (remainingChunks > 0 || parsed.attachments.length > 0) {
20
- const parts = [];
21
- if (remainingChunks > 0) {
22
- parts.push(`${remainingChunks} reply chunk(s)`);
23
- }
24
- if (parsed.attachments.length > 0) {
25
- parts.push(`${parsed.attachments.length} attachment(s)`);
26
- }
27
- await sender.sendText(`[bridge] Reply delivery was interrupted; ${parts.join(" and ")} could not be sent. Send a new message to retry.`);
28
- }
29
- return;
30
- }
31
- }
32
- }
33
- else if (adapter === "opencode" && parsed.visibleText.trim()) {
34
- onEmptyVisibleReply?.({
35
- adapter,
36
- rawVisibleText: parsed.visibleText,
37
- });
38
- const sent = await sender.sendText(OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE);
39
- if (sent === false) {
40
- return;
41
- }
42
- }
43
- for (const attachment of parsed.attachments) {
44
- try {
45
- switch (attachment.kind) {
46
- case "image":
47
- await sender.sendImage(attachment.path);
48
- break;
49
- case "file":
50
- await sender.sendFile(attachment.path);
51
- break;
52
- case "voice":
53
- await sender.sendVoice(attachment.path);
54
- break;
55
- case "video":
56
- await sender.sendVideo(attachment.path);
57
- break;
58
- }
59
- }
60
- catch (error) {
61
- const errorText = error instanceof Error ? error.message : String(error ?? "unknown error");
62
- await sender.sendText(`Failed to send ${attachment.kind} attachment: ${attachment.path}\n${errorText}`);
63
- }
64
- }
65
- }
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ /** @deprecated Import from ../channels/wechat/bridge-final-reply.ts instead. */
8
+ export * from "../channels/wechat/bridge-final-reply.js";
@@ -1,3 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import { spawnSync } from "node:child_process";
2
8
  import path from "node:path";
3
9
  const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
@@ -113,7 +119,7 @@ function normalizeBridgeProcessRecord(value) {
113
119
  }
114
120
  return record;
115
121
  }
116
- export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
122
+ function parseWindowsProcessProbeRecords(stdout) {
117
123
  const trimmed = stdout.trim();
118
124
  if (!trimmed) {
119
125
  return [];
@@ -128,7 +134,13 @@ export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = proces
128
134
  const values = Array.isArray(parsed) ? parsed : [parsed];
129
135
  return values
130
136
  .map(normalizeBridgeProcessRecord)
131
- .filter((record) => Boolean(record))
137
+ .filter((record) => Boolean(record));
138
+ }
139
+ export function parseWindowsProcessRecordProbeOutput(stdout, currentPid = process.pid) {
140
+ return (parseWindowsProcessProbeRecords(stdout).find((record) => record.pid !== currentPid) ?? null);
141
+ }
142
+ export function parseWindowsBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
143
+ return parseWindowsProcessProbeRecords(stdout)
132
144
  .filter((record) => record.pid !== currentPid && isWechatBridgeCommandLine(record.commandLine));
133
145
  }
134
146
  export function parsePosixBridgeProcessProbeOutput(stdout, currentPid = process.pid) {
@@ -195,10 +207,37 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
195
207
  ? listWindowsBridgeProcesses(currentPid)
196
208
  : listPosixBridgeProcesses(currentPid);
197
209
  }
210
+ function getWindowsProcessRecordByPid(pid, currentPid = process.pid) {
211
+ const probe = spawnSync("powershell.exe", [
212
+ "-NoLogo",
213
+ "-NoProfile",
214
+ "-NonInteractive",
215
+ "-ExecutionPolicy",
216
+ "Bypass",
217
+ "-Command",
218
+ [
219
+ "$ErrorActionPreference='Stop';",
220
+ `Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}'`,
221
+ "| Select-Object ProcessId,ParentProcessId,Name,CommandLine",
222
+ "| ConvertTo-Json -Compress",
223
+ ].join(" "),
224
+ ], {
225
+ encoding: "utf8",
226
+ windowsHide: true,
227
+ timeout: 8_000,
228
+ });
229
+ if (probe.status !== 0 || typeof probe.stdout !== "string") {
230
+ return null;
231
+ }
232
+ return parseWindowsProcessRecordProbeOutput(probe.stdout, currentPid);
233
+ }
198
234
  export function getProcessRecordByPid(pid, currentPid = process.pid) {
199
235
  if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
200
236
  return null;
201
237
  }
238
+ if (process.platform === "win32") {
239
+ return getWindowsProcessRecordByPid(pid, currentPid);
240
+ }
202
241
  return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
203
242
  }
204
243
  export function listWechatDaemonProcesses(params = {}) {
@@ -1,6 +1,12 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
1
7
  import fs from "node:fs";
2
8
  import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureWorkspaceChannelDir, ensureChannelDataDir, } from "../wechat/channel-config.js";
3
- import { buildInstanceId } from "./bridge-utils.js";
9
+ import { buildInstanceId } from "../core/text-utils.js";
4
10
  import { writeJsonFileAtomic } from "../utils/atomic-file.js";
5
11
  import { getProcessRecordByPid, isWechatBridgeCommandLine, } from "./bridge-process-reaper.js";
6
12
  const ORPHAN_LOCK_RECLAIM_TIMEOUT_MS = 2_000;
@@ -79,6 +85,9 @@ export function normalizeBridgeLockPayload(value) {
79
85
  startedAt: record.startedAt,
80
86
  lifecycle: record.lifecycle === "companion_bound" ? "companion_bound" : "persistent",
81
87
  legacyLifecycleFallback: hasExplicitLifecycle ? undefined : true,
88
+ ...(record.channelId === "wechat" || record.channelId === "wecom"
89
+ ? { channelId: record.channelId }
90
+ : {}),
82
91
  };
83
92
  }
84
93
  export function readBridgeLockFile() {
@@ -148,11 +157,13 @@ function buildLockConflictError(lock) {
148
157
  * pid is alive but belongs to something else, and "unknown" when the command
149
158
  * line could not be resolved (probe failure, restricted permissions).
150
159
  */
151
- export function classifyLockHolderProcess(pid) {
152
- if (!isPidAlive(pid)) {
160
+ export function classifyLockHolderProcess(pid, dependencies = {}) {
161
+ const isProcessAlive = dependencies.isPidAlive ?? isPidAlive;
162
+ const getProcessRecord = dependencies.getProcessRecordByPid ?? getProcessRecordByPid;
163
+ if (!isProcessAlive(pid)) {
153
164
  return "unknown";
154
165
  }
155
- const record = getProcessRecordByPid(pid);
166
+ const record = getProcessRecord(pid);
156
167
  if (!record) {
157
168
  return "unknown";
158
169
  }
@@ -200,6 +211,7 @@ export class BridgeStateStore {
200
211
  cwd: options.cwd,
201
212
  startedAt: new Date(this.bridgeStartedAtMs).toISOString(),
202
213
  lifecycle: options.lifecycle,
214
+ channelId: options.channelId,
203
215
  };
204
216
  this.acquireLock();
205
217
  const shouldRestoreSessionState = options.sessionStartMode !== "new";
@@ -231,6 +243,7 @@ export class BridgeStateStore {
231
243
  resumeConversationId: persistedResumeConversationId,
232
244
  transcriptPath: persistedTranscriptPath,
233
245
  lastActivityAt: persisted?.lastActivityAt,
246
+ channelId: options.channelId,
234
247
  pendingConfirmation: null,
235
248
  pendingUserInput: null,
236
249
  };