cli-wechat-bridge 1.1.7 → 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 (82) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +461 -434
  3. package/bin/_run-entry.mjs +29 -6
  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 +6 -1
  12. package/bin/wecom-claude.mjs +6 -1
  13. package/bin/wecom-codex.mjs +6 -1
  14. package/bin/wecom-daemon.mjs +6 -1
  15. package/bin/wecom-opencode.mjs +6 -1
  16. package/bin/wecom-pi.mjs +6 -1
  17. package/bin/wecom-setup.mjs +6 -1
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +35 -1
  20. package/dist/bridge/bridge-adapters.codex.js +118 -7
  21. package/dist/bridge/bridge-adapters.core.js +25 -1
  22. package/dist/bridge/bridge-adapters.opencode.js +156 -1
  23. package/dist/bridge/bridge-adapters.pi.js +45 -0
  24. package/dist/bridge/bridge-adapters.shared.js +6 -0
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +6 -0
  27. package/dist/bridge/bridge-process-reaper.js +6 -0
  28. package/dist/bridge/bridge-state.js +6 -0
  29. package/dist/bridge/bridge-utils.js +6 -31
  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 +250 -274
  34. package/dist/bridge/wechat-forwarding.js +6 -0
  35. package/dist/bridge/wechat-outbound-queue.js +6 -0
  36. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  37. package/dist/channels/wechat/index.js +6 -0
  38. package/dist/channels/wechat/wechat-driver.js +191 -0
  39. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  40. package/dist/channels/wechat/wechat-outbound-queue.js +39 -0
  41. package/dist/channels/wecom/bridge-final-reply.js +6 -0
  42. package/dist/channels/wecom/setup-cli.js +6 -0
  43. package/dist/channels/wecom/setup.js +6 -0
  44. package/dist/channels/wecom/wecom-config.js +6 -0
  45. package/dist/channels/wecom/wecom-driver.js +106 -0
  46. package/dist/channels/wecom/wecom-media.js +6 -0
  47. package/dist/channels/wecom/wecom-transport.js +6 -0
  48. package/dist/commands/check-update.js +6 -0
  49. package/dist/companion/codex-remote-client.js +28 -2
  50. package/dist/companion/codex-visible-client-link.js +107 -0
  51. package/dist/companion/local-companion-link.js +6 -0
  52. package/dist/companion/local-companion-start.js +13 -2
  53. package/dist/companion/local-companion.js +23 -0
  54. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  55. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  56. package/dist/core/bridge-control.js +10 -0
  57. package/dist/core/bridge-defer.js +25 -0
  58. package/dist/core/channel-driver.js +1 -0
  59. package/dist/core/conversation-routing.js +58 -0
  60. package/dist/core/direct-run.js +6 -0
  61. package/dist/core/inbound-message-claims.js +6 -0
  62. package/dist/core/outbound-target.js +32 -0
  63. package/dist/core/text-utils.js +6 -0
  64. package/dist/core/turn-coordinator.js +85 -0
  65. package/dist/daemon/daemon-link.js +6 -0
  66. package/dist/daemon/emoji-bindings.js +6 -0
  67. package/dist/daemon/wechat-daemon.js +529 -205
  68. package/dist/i18n/index.js +6 -0
  69. package/dist/i18n/messages-en.js +7 -1
  70. package/dist/i18n/messages-zh.js +7 -1
  71. package/dist/runtime/create-runtime-host.js +6 -0
  72. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  73. package/dist/runtime/runtime-types.js +3 -0
  74. package/dist/utils/atomic-file.js +6 -0
  75. package/dist/utils/doctor.js +6 -0
  76. package/dist/utils/version-checker.js +17 -11
  77. package/dist/wechat/channel-config.js +6 -0
  78. package/dist/wechat/setup.js +6 -0
  79. package/dist/wechat/wechat-channel.js +6 -0
  80. package/dist/wechat/wechat-transport.js +169 -58
  81. package/package.json +108 -106
  82. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -1,2 +1,8 @@
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
  /** @deprecated Import from ../channels/wechat/wechat-forwarding.ts instead. */
2
8
  export * from "../channels/wechat/wechat-forwarding.js";
@@ -1,2 +1,8 @@
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
  /** @deprecated Import from ../channels/wechat/wechat-outbound-queue.ts instead. */
2
8
  export * from "../channels/wechat/wechat-outbound-queue.js";
@@ -1,4 +1,4 @@
1
- import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, splitWechatTextIntoChunks, } from "../../bridge/bridge-utils.js";
1
+ import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, } from "../../bridge/bridge-utils.js";
2
2
  export const OPENCODE_EMPTY_VISIBLE_REPLY_MESSAGE = "OpenCode 没有产生可发送到微信的可见回复。请查看本地终端输出,或重试这条消息。";
3
3
  export async function forwardWechatFinalReply(params) {
4
4
  const { adapter, rawText, sender, onEmptyVisibleReply } = params;
@@ -6,28 +6,19 @@ export async function forwardWechatFinalReply(params) {
6
6
  const sanitizedText = sanitizeWechatFinalReplyText(adapter, parsed.visibleText);
7
7
  const visibleText = formatFinalReplyMessage(adapter, sanitizedText).trim();
8
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;
9
+ // Send the reply as a single message, matching the official WeChat bot
10
+ // client: it posts the full text in one sendmessage call without
11
+ // chunking. A failed send surfaces through the sender's own retry and
12
+ // pending-queue handling instead of being split up.
13
+ const sent = await sender.sendText(visibleText);
14
+ if (sent === false) {
15
+ // The send channel is failing (e.g. an expired context token).
16
+ // Report what was dropped instead of ending silently; this notice
17
+ // itself is best effort and may also fail.
18
+ if (parsed.attachments.length > 0) {
19
+ await sender.sendText(`[bridge] Reply delivery was interrupted; ${parsed.attachments.length} attachment(s) could not be sent. Send a new message to retry.`);
30
20
  }
21
+ return;
31
22
  }
32
23
  }
33
24
  else if (adapter === "opencode" && parsed.visibleText.trim()) {
@@ -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
  export * from "./bridge-final-reply.js";
2
8
  export * from "./channel-message.js";
3
9
  export * from "./wechat-channel-port.js";
@@ -0,0 +1,191 @@
1
+ import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isRetryableWechatSendError, isWechatContextUnavailableError, } from "../../channels/wechat/wechat-forwarding.js";
2
+ import { describeWechatTransportError, isWechatContextTokenStaleError, } from "../../wechat/wechat-transport.js";
3
+ import { formatDuration } from "../../core/text-utils.js";
4
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
+ /** Per-recipient typing tickets with a random-refresh TTL and retry backoff. */
6
+ class TypingTicketCache {
7
+ fetchTicket;
8
+ log;
9
+ ttlMs;
10
+ initialRetryMs;
11
+ maxRetryMs;
12
+ cache = new Map();
13
+ constructor(fetchTicket, log, ttlMs = 24 * 60 * 60 * 1000, initialRetryMs = 2_000, maxRetryMs = 60 * 60 * 1000) {
14
+ this.fetchTicket = fetchTicket;
15
+ this.log = log;
16
+ this.ttlMs = ttlMs;
17
+ this.initialRetryMs = initialRetryMs;
18
+ this.maxRetryMs = maxRetryMs;
19
+ }
20
+ async getFor(recipientId) {
21
+ const now = Date.now();
22
+ const entry = this.cache.get(recipientId);
23
+ if (entry && now < entry.nextFetchAt) {
24
+ return entry.ticket;
25
+ }
26
+ let ticket;
27
+ try {
28
+ ticket = await this.fetchTicket(recipientId);
29
+ }
30
+ catch {
31
+ ticket = "";
32
+ }
33
+ if (ticket) {
34
+ this.cache.set(recipientId, {
35
+ ticket,
36
+ nextFetchAt: now + Math.random() * this.ttlMs,
37
+ retryDelayMs: this.initialRetryMs,
38
+ });
39
+ this.log(`wechat_typing_ticket_${entry ? "refreshed" : "cached"}: recipient=${recipientId}`);
40
+ }
41
+ else {
42
+ const retryDelayMs = Math.min((entry?.retryDelayMs ?? this.initialRetryMs / 2) * 2, this.maxRetryMs);
43
+ this.cache.set(recipientId, {
44
+ ticket: entry?.ticket ?? "",
45
+ nextFetchAt: now + retryDelayMs,
46
+ retryDelayMs,
47
+ });
48
+ this.log(`wechat_typing_ticket_fetch_failed: recipient=${recipientId} retry_in=${formatDuration(retryDelayMs)}`);
49
+ }
50
+ return this.cache.get(recipientId)?.ticket ?? "";
51
+ }
52
+ /** Cached ticket without triggering a fetch (for cancel sends). */
53
+ peekCached(recipientId) {
54
+ return this.cache.get(recipientId)?.ticket ?? "";
55
+ }
56
+ }
57
+ const WECHAT_CONTEXT_STALE_HINT = "WeChat conversation context is stale or unavailable. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
58
+ /** ChannelDriver over the WeChat iLink long-poll transport. */
59
+ export class WechatChannelDriver {
60
+ id = "wechat";
61
+ displayName = "WeChat";
62
+ operatorDescription = "the configured WeChat owner";
63
+ capabilities = {
64
+ streamingReplies: false,
65
+ outboundAttachments: false,
66
+ multiConversation: false,
67
+ pushInbound: false,
68
+ };
69
+ transport;
70
+ logError;
71
+ buildInboundPromptImpl;
72
+ typingKeepaliveMs;
73
+ typingTickets;
74
+ typingTimers = new Map();
75
+ constructor(options) {
76
+ this.transport = options.transport;
77
+ this.logError = options.logError;
78
+ this.buildInboundPromptImpl = options.buildInboundPrompt;
79
+ this.typingKeepaliveMs = options.typingKeepaliveMs ?? 5_000;
80
+ this.typingTickets = new TypingTicketCache((recipientId) => this.transport.fetchTypingTicket(recipientId), (message) => this.logError(`[typing] ${message}`));
81
+ }
82
+ defaultConversation() {
83
+ return null;
84
+ }
85
+ directConversation(senderId) {
86
+ return {
87
+ channelId: "wechat",
88
+ conversationId: senderId,
89
+ recipientId: senderId,
90
+ };
91
+ }
92
+ async sendText(params) {
93
+ const { target, text, context, log } = params;
94
+ const senderId = target.recipientId;
95
+ const startedAtMs = Date.now();
96
+ log(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
97
+ for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
98
+ try {
99
+ await this.transport.sendText(senderId, text);
100
+ log(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
101
+ return { status: "sent" };
102
+ }
103
+ catch (err) {
104
+ if (err instanceof Error && err.name === "AbortError") {
105
+ log(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
106
+ }
107
+ if (isWechatContextUnavailableError(err)) {
108
+ if (isWechatContextTokenStaleError(err)) {
109
+ this.transport.clearCachedContextToken(senderId);
110
+ }
111
+ this.logError(`Failed to send WeChat ${context}: ${WECHAT_CONTEXT_STALE_HINT}`);
112
+ log(isWechatContextTokenStaleError(err)
113
+ ? formatWechatContextTokenStaleLogEntry({
114
+ context,
115
+ recipientId: senderId,
116
+ error: err,
117
+ })
118
+ : formatWechatSendFailureLogEntry({
119
+ context,
120
+ recipientId: senderId,
121
+ error: err,
122
+ }));
123
+ return {
124
+ status: "target_stale",
125
+ error: err,
126
+ target,
127
+ };
128
+ }
129
+ if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
130
+ const delayMs = computeWechatSendRetryDelayMs(attempt);
131
+ this.logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
132
+ log(formatWechatSendRetryLogEntry({
133
+ context,
134
+ recipientId: senderId,
135
+ attempt,
136
+ delayMs,
137
+ error: err,
138
+ }));
139
+ await delay(delayMs);
140
+ continue;
141
+ }
142
+ this.logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
143
+ log(formatWechatSendFailureLogEntry({
144
+ context,
145
+ recipientId: senderId,
146
+ error: err,
147
+ }));
148
+ return { status: "failed", error: err, target };
149
+ }
150
+ }
151
+ return { status: "failed", error: new Error("unreachable"), target };
152
+ }
153
+ buildInboundPrompt(text, attachments) {
154
+ return this.buildInboundPromptImpl(text, attachments);
155
+ }
156
+ async beginTyping(recipientId) {
157
+ if (this.typingTimers.has(recipientId)) {
158
+ return;
159
+ }
160
+ const ticket = await this.typingTickets.getFor(recipientId);
161
+ if (!ticket) {
162
+ return;
163
+ }
164
+ const sent = await this.transport.sendTyping(recipientId, ticket, 1);
165
+ if (!sent) {
166
+ return;
167
+ }
168
+ const timer = setInterval(() => {
169
+ void this.transport.sendTyping(recipientId, ticket, 1);
170
+ }, this.typingKeepaliveMs);
171
+ timer.unref?.();
172
+ this.typingTimers.set(recipientId, timer);
173
+ }
174
+ async endTyping(recipientId) {
175
+ const timer = this.typingTimers.get(recipientId);
176
+ if (timer) {
177
+ clearInterval(timer);
178
+ this.typingTimers.delete(recipientId);
179
+ }
180
+ const ticket = this.typingTickets.peekCached(recipientId);
181
+ if (!ticket) {
182
+ return;
183
+ }
184
+ await this.transport.sendTyping(recipientId, ticket, 2);
185
+ }
186
+ async endAllTyping() {
187
+ for (const recipientId of [...this.typingTimers.keys()]) {
188
+ await this.endTyping(recipientId);
189
+ }
190
+ }
191
+ }
@@ -1,5 +1,23 @@
1
1
  import { truncatePreview } from "../../bridge/bridge-utils.js";
2
2
  import { classifyWechatTransportError, describeWechatTransportError, isWechatContextTokenStaleError, } from "../../wechat/wechat-transport.js";
3
+ export const WECHAT_SEND_CONTEXTS = [
4
+ "final_reply",
5
+ "message",
6
+ "notice",
7
+ "approval_required",
8
+ "user_input_required",
9
+ "mirrored_user_input",
10
+ "session_switched",
11
+ "thread_switched",
12
+ "task_failed",
13
+ "fatal_error",
14
+ "inbound_error",
15
+ "thinking",
16
+ ];
17
+ export function isWechatSendContext(value) {
18
+ return typeof value === "string" &&
19
+ WECHAT_SEND_CONTEXTS.includes(value);
20
+ }
3
21
  export const WECHAT_SEND_MAX_ATTEMPTS = 3;
4
22
  export const CODEX_LOCAL_THREAD_NOTICE_SUPPRESS_MS = 500;
5
23
  const WECHAT_SEND_RETRY_BASE_MS = 750;
@@ -1,8 +1,33 @@
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 path from "node:path";
4
10
  import { getWorkspaceChannelPaths } from "../../wechat/channel-config.js";
5
11
  import { writeJsonFileAtomic } from "../../utils/atomic-file.js";
12
+ /** Contexts worth redelivering after the channel recovers. */
13
+ const PENDING_WORTHY_CONTEXTS = new Set([
14
+ "final_reply",
15
+ "approval_required",
16
+ "user_input_required",
17
+ "task_failed",
18
+ "fatal_error",
19
+ ]);
20
+ /** Drop queued messages older than this — stale replies are noise, not value. */
21
+ const PENDING_MAX_AGE_MS = 30 * 60 * 1000;
22
+ /** Cap the backlog so one recovery never floods the chat. */
23
+ const PENDING_MAX_MESSAGES = 10;
24
+ function isExpired(message, now = Date.now()) {
25
+ const queuedAtMs = Date.parse(message.queuedAt);
26
+ if (!Number.isFinite(queuedAtMs)) {
27
+ return true;
28
+ }
29
+ return now - queuedAtMs > PENDING_MAX_AGE_MS;
30
+ }
6
31
  export function getPendingWechatMessagesFile(cwd) {
7
32
  return getPendingChannelMessagesFile(cwd, "wechat");
8
33
  }
@@ -41,6 +66,7 @@ export class PendingWechatMessageStore {
41
66
  this.messages = readPendingMessages(filePath);
42
67
  }
43
68
  list() {
69
+ this.compact();
44
70
  return this.messages.map((message) => ({
45
71
  ...message,
46
72
  target: message.target ? { ...message.target } : undefined,
@@ -52,6 +78,9 @@ export class PendingWechatMessageStore {
52
78
  if (!normalizedRecipientId || !normalizedText) {
53
79
  return null;
54
80
  }
81
+ if (!PENDING_WORTHY_CONTEXTS.has(context)) {
82
+ return null;
83
+ }
55
84
  const message = {
56
85
  id: crypto.randomUUID(),
57
86
  recipientId: normalizedRecipientId,
@@ -61,9 +90,19 @@ export class PendingWechatMessageStore {
61
90
  target: target ? { ...target } : undefined,
62
91
  };
63
92
  this.messages.push(message);
93
+ this.compact();
64
94
  this.persist();
65
95
  return { ...message };
66
96
  }
97
+ /** Drop expired entries and trim the backlog to the cap (oldest first). */
98
+ compact() {
99
+ const now = Date.now();
100
+ const fresh = this.messages.filter((message) => !isExpired(message, now));
101
+ while (fresh.length > PENDING_MAX_MESSAGES) {
102
+ fresh.shift();
103
+ }
104
+ this.messages = fresh;
105
+ }
67
106
  remove(id) {
68
107
  const next = this.messages.filter((message) => message.id !== id);
69
108
  if (next.length === this.messages.length) {
@@ -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 path from "node:path";
2
8
  import { formatFinalReplyMessage, parseWechatFinalReply, sanitizeWechatFinalReplyText, } from "../../bridge/bridge-utils.js";
3
9
  export async function forwardWecomFinalReply(params) {
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import { runWecomSetup } from "./setup.js";
3
9
  runWecomSetup().catch((error) => {
4
10
  console.error(error instanceof Error ? error.message : String(error));
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import crypto from "node:crypto";
3
9
  import readline from "node:readline";
4
10
  import { Writable } from "node:stream";
@@ -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 path from "node:path";
3
9
  import { CHANNEL_DATA_DIR, ensureChannelDataDir, } from "../../wechat/channel-config.js";
@@ -0,0 +1,106 @@
1
+ import { buildWecomInboundPrompt, formatWecomVisibleText, } from "./wecom-message.js";
2
+ import { truncatePreview } from "../../core/text-utils.js";
3
+ export function toWecomOutputKind(context) {
4
+ const known = new Set([
5
+ "status",
6
+ "notice",
7
+ "thinking",
8
+ "final_reply",
9
+ "approval_required",
10
+ "user_input_required",
11
+ "mirrored_input",
12
+ "task_failed",
13
+ "fatal_error",
14
+ ]);
15
+ return known.has(context)
16
+ ? context
17
+ : context === "mirrored_user_input"
18
+ ? "mirrored_input"
19
+ : "notice";
20
+ }
21
+ /** ChannelDriver over the official WeCom smart-bot WebSocket transport. */
22
+ export class WecomChannelDriver {
23
+ id = "wecom";
24
+ displayName = "WeCom";
25
+ operatorDescription = "the paired WeCom operator";
26
+ capabilities = {
27
+ streamingReplies: true,
28
+ outboundAttachments: true,
29
+ multiConversation: true,
30
+ pushInbound: true,
31
+ };
32
+ transport;
33
+ accountId;
34
+ operatorId;
35
+ logError;
36
+ constructor(options) {
37
+ this.transport = options.transport;
38
+ this.accountId = options.accountId;
39
+ this.operatorId = options.operatorId;
40
+ this.logError = options.logError;
41
+ }
42
+ defaultConversation() {
43
+ return this.directConversation(this.operatorId);
44
+ }
45
+ directConversation(senderId) {
46
+ return {
47
+ channelId: "wecom",
48
+ accountId: this.accountId,
49
+ conversationId: senderId,
50
+ recipientId: senderId,
51
+ metadata: { chatType: "direct" },
52
+ };
53
+ }
54
+ start(handlers) {
55
+ this.transport.setHandlers({
56
+ onMessage: async (message) => {
57
+ await handlers.onInboundMessage(message);
58
+ },
59
+ onUnauthorized: handlers.onUnauthorizedSender
60
+ ? async (senderId, chatType) => {
61
+ await handlers.onUnauthorizedSender(senderId, chatType);
62
+ }
63
+ : undefined,
64
+ onFatal: handlers.onChannelFatal
65
+ ? async (error) => {
66
+ await handlers.onChannelFatal(error);
67
+ }
68
+ : undefined,
69
+ onConnected: handlers.onChannelConnected
70
+ ? async () => {
71
+ await handlers.onChannelConnected();
72
+ }
73
+ : undefined,
74
+ });
75
+ this.transport.start();
76
+ }
77
+ waitUntilReady() {
78
+ return this.transport.waitUntilConnected();
79
+ }
80
+ async sendText(params) {
81
+ const { target, text, context, log } = params;
82
+ const startedAtMs = Date.now();
83
+ log(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
84
+ try {
85
+ await this.transport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
86
+ log(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
87
+ return { status: "sent" };
88
+ }
89
+ catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ log(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
92
+ this.logError(`Failed to send WeCom ${context}: ${message}`);
93
+ return {
94
+ status: "target_stale",
95
+ error,
96
+ target,
97
+ };
98
+ }
99
+ }
100
+ buildInboundPrompt(text, attachments) {
101
+ return buildWecomInboundPrompt(text, attachments);
102
+ }
103
+ formatVisibleText(text) {
104
+ return formatWecomVisibleText(text);
105
+ }
106
+ }
@@ -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 path from "node:path";
3
9
  import { decryptFile } from "@wecom/aibot-node-sdk";
@@ -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 path from "node:path";
3
9
  import { generateReqId, WSAuthFailureError, WSClient, WSReconnectExhaustedError, } from "@wecom/aibot-node-sdk";
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import { checkForUpdate, getCurrentVersion, } from "../utils/version-checker.js";
3
9
  async function main() {
4
10
  const currentVersion = await getCurrentVersion();
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import crypto from "node:crypto";
3
9
  import { spawn } from "node:child_process";
4
10
  import net from "node:net";
5
11
  import path from "node:path";
6
12
  import { assertNoReservedExtraCliArgs, buildCliEnvironment, buildCodexCliArgs, resolveSpawnTarget, } from "../bridge/bridge-adapters.shared.js";
7
13
  import { killProcessTreeSync } from "../bridge/bridge-process-reaper.js";
8
- import { CODEX_VISIBLE_CONTROL_HOST, parseCodexVisibleSwitchRequest, sendCodexVisibleSwitchResponse, } from "./codex-visible-client-link.js";
14
+ import { CODEX_VISIBLE_CONTROL_HOST, parseCodexVisibleControlRequest, sendCodexVisibleShutdownResponse, sendCodexVisibleSwitchResponse, } from "./codex-visible-client-link.js";
9
15
  import { clearLocalCompanionOccupancy, readLocalCompanionEndpoint, updateCodexVisibleControl, updateLocalCompanionOccupancy, } from "./local-companion-link.js";
10
16
  import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
11
17
  import { CODEX_REMOTE_AUTH_TOKEN_ENV } from "../runtime/runtime-types.js";
@@ -302,11 +308,20 @@ export class CodexVisibleClientSupervisor {
302
308
  socket.destroy();
303
309
  return;
304
310
  }
305
- const request = parseCodexVisibleSwitchRequest(parsed);
311
+ const request = parseCodexVisibleControlRequest(parsed);
306
312
  if (!request || request.token !== this.controlToken) {
307
313
  socket.destroy();
308
314
  return;
309
315
  }
316
+ if (request.type === "shutdown") {
317
+ sendCodexVisibleShutdownResponse(socket, {
318
+ type: "shutdown_result",
319
+ id: request.id,
320
+ ok: true,
321
+ });
322
+ void this.shutdownFromControl();
323
+ return;
324
+ }
310
325
  void this.switchThread(request.threadId).then(() => {
311
326
  sendCodexVisibleSwitchResponse(socket, {
312
327
  type: "switch_thread_result",
@@ -340,6 +355,17 @@ export class CodexVisibleClientSupervisor {
340
355
  this.controlServer = server;
341
356
  this.controlPort = address.port;
342
357
  }
358
+ async shutdownFromControl() {
359
+ await this.switchChain.catch(() => undefined);
360
+ if (this.runSettled) {
361
+ return;
362
+ }
363
+ const child = this.child;
364
+ if (child) {
365
+ await this.stopChild(child);
366
+ }
367
+ this.finishRun(undefined, 0);
368
+ }
343
369
  async closeControlServer() {
344
370
  const server = this.controlServer;
345
371
  this.controlServer = null;