@unlinearity/cli-wechat-bridge 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +661 -21
- package/README.md +49 -2
- package/dist/bridge/bridge-adapters.claude.js +8 -0
- package/dist/bridge/bridge-adapters.codex.js +26 -13
- package/dist/bridge/bridge-adapters.core.js +19 -5
- package/dist/bridge/bridge-adapters.opencode.js +14 -10
- package/dist/bridge/bridge-process-reaper.js +7 -2
- package/dist/bridge/bridge-utils.js +4 -2
- package/dist/bridge/claude-hooks.js +5 -0
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +80 -0
- package/dist/daemon/wechat-daemon.js +165 -10
- package/dist/wechat/channel-config.js +1 -0
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
|
|
8
8
|
import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
|
|
@@ -14,6 +14,7 @@ import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, form
|
|
|
14
14
|
import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-bridge.js";
|
|
15
15
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
16
16
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
17
|
+
import { listBindings, loadEmojiBindings, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
17
18
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
18
19
|
import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
|
|
19
20
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
@@ -135,6 +136,22 @@ export function parseDaemonSwitchCommand(text) {
|
|
|
135
136
|
return null;
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
export function parseEmojiBindingsCommand(text) {
|
|
140
|
+
const trimmed = text.trim();
|
|
141
|
+
const lower = trimmed.toLowerCase();
|
|
142
|
+
if (lower === "/bindings") {
|
|
143
|
+
return { type: "list" };
|
|
144
|
+
}
|
|
145
|
+
const bindMatch = trimmed.match(/^\/bind\s+(\[[^\]]+\])\s+(.+)$/i);
|
|
146
|
+
if (bindMatch) {
|
|
147
|
+
return { type: "bind", emoji: bindMatch[1], command: bindMatch[2].trim() };
|
|
148
|
+
}
|
|
149
|
+
const unbindMatch = trimmed.match(/^\/unbind\s+(\[[^\]]+\])$/i);
|
|
150
|
+
if (unbindMatch) {
|
|
151
|
+
return { type: "unbind", emoji: unbindMatch[1] };
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
138
155
|
export function defaultDaemonSessionStartMode(adapter) {
|
|
139
156
|
return adapter === "claude" || adapter === "opencode" ? "new" : "restore";
|
|
140
157
|
}
|
|
@@ -208,6 +225,33 @@ export function buildWindowsVisibleClientLaunchCommand(params) {
|
|
|
208
225
|
...params.args.map((arg) => quoteWindowsCommandArg(arg)),
|
|
209
226
|
].join(" ");
|
|
210
227
|
}
|
|
228
|
+
const LINUX_TERMINALS = [
|
|
229
|
+
{ cmd: "gnome-terminal", buildArgs: (title) => ["--title", title, "--"] },
|
|
230
|
+
{ cmd: "konsole", buildArgs: (title) => ["-p", `tabtitle=${title}`, "-e"] },
|
|
231
|
+
{ cmd: "xfce4-terminal", buildArgs: (title) => ["--title", title, "-e"] },
|
|
232
|
+
{ cmd: "xterm", buildArgs: (title) => ["-title", title, "-e"] },
|
|
233
|
+
];
|
|
234
|
+
let cachedLinuxTerminal;
|
|
235
|
+
function detectLinuxTerminal() {
|
|
236
|
+
if (cachedLinuxTerminal !== undefined) {
|
|
237
|
+
return cachedLinuxTerminal;
|
|
238
|
+
}
|
|
239
|
+
for (const entry of LINUX_TERMINALS) {
|
|
240
|
+
try {
|
|
241
|
+
execFileSync("which", [entry.cmd], { stdio: "ignore" });
|
|
242
|
+
cachedLinuxTerminal = entry;
|
|
243
|
+
return entry;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// not found, try next
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
cachedLinuxTerminal = null;
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function shellQuotePosix(s) {
|
|
253
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
254
|
+
}
|
|
211
255
|
function formatLaunchPreview(launch) {
|
|
212
256
|
return [launch.command, ...launch.args].join(" ");
|
|
213
257
|
}
|
|
@@ -242,12 +286,53 @@ function openVisibleClient(params) {
|
|
|
242
286
|
pid: child.pid,
|
|
243
287
|
};
|
|
244
288
|
}
|
|
289
|
+
const title = `wechat-${params.adapter}`;
|
|
290
|
+
const fullArgs = [process.execPath, ...args];
|
|
291
|
+
if (process.platform === "darwin") {
|
|
292
|
+
const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
|
|
293
|
+
const script = `tell application "Terminal"
|
|
294
|
+
activate
|
|
295
|
+
do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
|
|
296
|
+
end tell`;
|
|
297
|
+
const child = spawn("osascript", ["-e", script], {
|
|
298
|
+
cwd: params.cwd,
|
|
299
|
+
detached: true,
|
|
300
|
+
stdio: "ignore",
|
|
301
|
+
});
|
|
302
|
+
child.once("error", (error) => {
|
|
303
|
+
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
304
|
+
});
|
|
305
|
+
child.unref();
|
|
306
|
+
return {
|
|
307
|
+
command: "osascript",
|
|
308
|
+
args: ["-e", script],
|
|
309
|
+
pid: child.pid,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const terminal = detectLinuxTerminal();
|
|
313
|
+
if (terminal) {
|
|
314
|
+
const termArgs = [...terminal.buildArgs(title), ...fullArgs];
|
|
315
|
+
const child = spawn(terminal.cmd, termArgs, {
|
|
316
|
+
cwd: params.cwd,
|
|
317
|
+
env: process.env,
|
|
318
|
+
detached: true,
|
|
319
|
+
stdio: "ignore",
|
|
320
|
+
});
|
|
321
|
+
child.once("error", (error) => {
|
|
322
|
+
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
323
|
+
});
|
|
324
|
+
child.unref();
|
|
325
|
+
return {
|
|
326
|
+
command: terminal.cmd,
|
|
327
|
+
args: termArgs,
|
|
328
|
+
pid: child.pid,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
245
331
|
const child = spawn(process.execPath, args, {
|
|
246
332
|
cwd: params.cwd,
|
|
247
333
|
env: process.env,
|
|
248
334
|
detached: true,
|
|
249
335
|
stdio: "ignore",
|
|
250
|
-
windowsHide: false,
|
|
251
336
|
});
|
|
252
337
|
child.once("error", (error) => {
|
|
253
338
|
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -375,6 +460,7 @@ class WechatDaemon {
|
|
|
375
460
|
startedAt = new Date().toISOString();
|
|
376
461
|
bridgeStartedAtMs = Date.now();
|
|
377
462
|
activeAdapter = null;
|
|
463
|
+
takenOverAdapter;
|
|
378
464
|
textSendChain = Promise.resolve();
|
|
379
465
|
attachmentSendChain = Promise.resolve();
|
|
380
466
|
pendingWechatForwardTasks = new Set();
|
|
@@ -598,7 +684,11 @@ class WechatDaemon {
|
|
|
598
684
|
let slot = this.slots.get(adapter);
|
|
599
685
|
let created = false;
|
|
600
686
|
if (!slot) {
|
|
601
|
-
const createSessionStartMode = options.sessionStartMode ??
|
|
687
|
+
const createSessionStartMode = options.sessionStartMode ??
|
|
688
|
+
(adapter === this.takenOverAdapter ? "restore" : defaultDaemonSessionStartMode(adapter));
|
|
689
|
+
if (adapter === this.takenOverAdapter) {
|
|
690
|
+
this.takenOverAdapter = undefined;
|
|
691
|
+
}
|
|
602
692
|
slot = await this.createSlot(adapter, {
|
|
603
693
|
profile: options.profile ?? this.profile,
|
|
604
694
|
sessionStartMode: createSessionStartMode,
|
|
@@ -861,6 +951,30 @@ class WechatDaemon {
|
|
|
861
951
|
await this.queueWechatMessage(message.senderId, "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
|
|
862
952
|
return;
|
|
863
953
|
}
|
|
954
|
+
const emojiMatch = resolveEmojiCommand(message.text);
|
|
955
|
+
if (emojiMatch) {
|
|
956
|
+
const switchTarget = parseDaemonSwitchCommand(emojiMatch.command);
|
|
957
|
+
if (switchTarget && emojiMatch.remainder) {
|
|
958
|
+
const result = await this.ensureSlot(switchTarget, {
|
|
959
|
+
openVisible: true,
|
|
960
|
+
reuseExistingVisible: true,
|
|
961
|
+
});
|
|
962
|
+
if (result.activated) {
|
|
963
|
+
message = { ...message, text: emojiMatch.remainder };
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
const detail = formatDaemonSwitchResultDetail(result);
|
|
967
|
+
await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchTarget}.\n${detail}`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
const rewritten = emojiMatch.remainder
|
|
973
|
+
? `${emojiMatch.command} ${emojiMatch.remainder}`
|
|
974
|
+
: emojiMatch.command;
|
|
975
|
+
message = { ...message, text: rewritten };
|
|
976
|
+
}
|
|
977
|
+
}
|
|
864
978
|
const switchAdapter = parseDaemonSwitchCommand(message.text);
|
|
865
979
|
if (switchAdapter) {
|
|
866
980
|
const result = await this.ensureSlot(switchAdapter, {
|
|
@@ -881,6 +995,11 @@ class WechatDaemon {
|
|
|
881
995
|
}, 0);
|
|
882
996
|
return;
|
|
883
997
|
}
|
|
998
|
+
const bindingsCmd = parseEmojiBindingsCommand(message.text);
|
|
999
|
+
if (bindingsCmd) {
|
|
1000
|
+
await this.handleEmojiBindingsCommand(message.senderId, bindingsCmd);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
884
1003
|
const slot = this.getActiveSlot();
|
|
885
1004
|
if (!slot) {
|
|
886
1005
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
@@ -910,6 +1029,32 @@ class WechatDaemon {
|
|
|
910
1029
|
}
|
|
911
1030
|
await this.dispatchInboundWechatText(message, slot);
|
|
912
1031
|
}
|
|
1032
|
+
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
1033
|
+
switch (cmd.type) {
|
|
1034
|
+
case "list": {
|
|
1035
|
+
const map = listBindings();
|
|
1036
|
+
if (map.size === 0) {
|
|
1037
|
+
await this.queueWechatMessage(senderId, "No emoji bindings configured.");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const lines = Array.from(map.entries()).map(([emoji, command]) => `${emoji} → ${command}`);
|
|
1041
|
+
await this.queueWechatMessage(senderId, `Emoji bindings:\n${lines.join("\n")}`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
case "bind": {
|
|
1045
|
+
setBinding(cmd.emoji, cmd.command);
|
|
1046
|
+
await this.queueWechatMessage(senderId, `Bound ${cmd.emoji} → ${cmd.command}`);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
case "unbind": {
|
|
1050
|
+
const removed = removeBinding(cmd.emoji);
|
|
1051
|
+
await this.queueWechatMessage(senderId, removed
|
|
1052
|
+
? `Unbound ${cmd.emoji}`
|
|
1053
|
+
: `No binding found for ${cmd.emoji}`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
913
1058
|
async handleSystemCommand(message, activeSlot, command) {
|
|
914
1059
|
switch (command.type) {
|
|
915
1060
|
case "status":
|
|
@@ -1322,7 +1467,7 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1322
1467
|
const readLock = deps.readLock ?? readBridgeLockFile;
|
|
1323
1468
|
const isAlive = deps.isAlive ?? isPidAlive;
|
|
1324
1469
|
const killProcess = deps.killProcess ?? ((pid, signal) => {
|
|
1325
|
-
if (signal === "SIGKILL") {
|
|
1470
|
+
if (signal === "SIGKILL" || process.platform === "win32") {
|
|
1326
1471
|
killProcessTreeSync(pid);
|
|
1327
1472
|
return;
|
|
1328
1473
|
}
|
|
@@ -1398,8 +1543,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1398
1543
|
}
|
|
1399
1544
|
export async function runDaemon(options) {
|
|
1400
1545
|
migrateLegacyChannelFiles((message) => log(message));
|
|
1546
|
+
loadEmojiBindings();
|
|
1401
1547
|
await cleanupDaemonBeforeStart({ cwd: options.cwd });
|
|
1402
|
-
await cleanupSingleBridgeBeforeDaemon();
|
|
1548
|
+
const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
|
|
1403
1549
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
1404
1550
|
logger: (message) => appendDaemonLog(message),
|
|
1405
1551
|
});
|
|
@@ -1426,17 +1572,26 @@ export async function runDaemon(options) {
|
|
|
1426
1572
|
authorizedUserId: credentials.userId,
|
|
1427
1573
|
transport: new WeChatTransport({ log, logError }),
|
|
1428
1574
|
});
|
|
1575
|
+
if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
|
|
1576
|
+
daemon.takenOverAdapter = cleanupResult.lock.adapter;
|
|
1577
|
+
}
|
|
1429
1578
|
await daemon.startIpcServer();
|
|
1430
1579
|
await daemon.runInitialAdapter(options);
|
|
1431
|
-
|
|
1580
|
+
let shutdownInProgress = false;
|
|
1581
|
+
const handleSignal = (signal) => {
|
|
1582
|
+
if (shutdownInProgress) {
|
|
1583
|
+
log(`Received ${signal} during shutdown, forcing exit.`);
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
shutdownInProgress = true;
|
|
1432
1587
|
log(`Received ${signal}. Stopping daemon.`);
|
|
1433
1588
|
void daemon.shutdown().finally(() => process.exit(0));
|
|
1434
1589
|
};
|
|
1435
|
-
process.
|
|
1436
|
-
process.
|
|
1437
|
-
process.
|
|
1590
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
1591
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
1592
|
+
process.on("SIGHUP", () => handleSignal("SIGHUP"));
|
|
1438
1593
|
if (process.platform === "win32") {
|
|
1439
|
-
process.
|
|
1594
|
+
process.on("SIGBREAK", () => handleSignal("SIGBREAK"));
|
|
1440
1595
|
}
|
|
1441
1596
|
process.on("exit", () => {
|
|
1442
1597
|
clearDaemonEndpoint();
|
|
@@ -23,6 +23,7 @@ export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-pane
|
|
|
23
23
|
export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
|
|
24
24
|
export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
|
|
25
25
|
export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
|
|
26
|
+
export const EMOJI_BINDINGS_FILE = path.join(CHANNEL_DATA_DIR, "emoji-bindings.json");
|
|
26
27
|
const LEGACY_GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
|
|
27
28
|
const LEGACY_REPO_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
|
|
28
29
|
const LEGACY_ENV_CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlinearity/cli-wechat-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"terminal"
|
|
20
20
|
],
|
|
21
21
|
"author": "UNLINEARITY",
|
|
22
|
-
"license": "
|
|
22
|
+
"license": "AGPL-3.0-or-later",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
25
|
"url": "git+https://github.com/UNLINEARITY/CLI-WeChat-Bridge.git"
|