cli-wechat-bridge 1.0.6 → 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 +157 -353
- package/dist/bridge/bridge-adapters.claude.js +102 -3
- package/dist/bridge/bridge-adapters.codex.js +30 -15
- package/dist/bridge/bridge-adapters.core.js +32 -10
- package/dist/bridge/bridge-adapters.opencode.js +79 -22
- package/dist/bridge/bridge-adapters.shared.js +3 -6
- package/dist/bridge/bridge-process-reaper.js +47 -3
- package/dist/bridge/bridge-state.js +6 -3
- package/dist/bridge/bridge-utils.js +4 -2
- package/dist/bridge/claude-hooks.js +5 -0
- package/dist/bridge/wechat-bridge.js +11 -1
- package/dist/commands/check-update.js +1 -1
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion-start.js +27 -2
- package/dist/companion/local-companion.js +21 -4
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +80 -0
- package/dist/daemon/wechat-daemon.js +440 -29
- package/dist/wechat/channel-config.js +1 -0
- package/package.json +3 -4
- package/dist/companion/codex-panel-link.js +0 -72
- package/dist/companion/codex-panel.js +0 -179
- package/dist/media/media-types.js +0 -1
- package/dist/wechat/standalone-bot.js +0 -355
|
@@ -151,6 +151,7 @@ export function parseCliArgs(argv) {
|
|
|
151
151
|
let cwd = process.cwd();
|
|
152
152
|
let profile;
|
|
153
153
|
let lifecycle = "persistent";
|
|
154
|
+
let sessionStartMode = "restore";
|
|
154
155
|
for (let i = 0; i < argv.length; i += 1) {
|
|
155
156
|
const arg = argv[i];
|
|
156
157
|
const next = argv[i + 1];
|
|
@@ -190,6 +191,13 @@ export function parseCliArgs(argv) {
|
|
|
190
191
|
lifecycle = next;
|
|
191
192
|
i += 1;
|
|
192
193
|
break;
|
|
194
|
+
case "--session-start-mode":
|
|
195
|
+
if (!next || !["restore", "new"].includes(next)) {
|
|
196
|
+
throw new Error(`Invalid session start mode: ${next ?? "(missing)"}`);
|
|
197
|
+
}
|
|
198
|
+
sessionStartMode = next;
|
|
199
|
+
i += 1;
|
|
200
|
+
break;
|
|
193
201
|
case "--shutdown-on-parent-exit":
|
|
194
202
|
lifecycle = "companion_bound";
|
|
195
203
|
break;
|
|
@@ -211,11 +219,12 @@ export function parseCliArgs(argv) {
|
|
|
211
219
|
cwd,
|
|
212
220
|
profile,
|
|
213
221
|
lifecycle,
|
|
222
|
+
sessionStartMode,
|
|
214
223
|
};
|
|
215
224
|
}
|
|
216
225
|
function printUsageAndExit() {
|
|
217
226
|
process.stdout.write([
|
|
218
|
-
"Usage: wechat-bridge --adapter <codex|claude|opencode|shell> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>]",
|
|
227
|
+
"Usage: wechat-bridge --adapter <codex|claude|opencode|shell> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>] [--session-start-mode <restore|new>]",
|
|
219
228
|
"",
|
|
220
229
|
"Examples:",
|
|
221
230
|
" wechat-bridge-codex",
|
|
@@ -305,6 +314,7 @@ async function main() {
|
|
|
305
314
|
cwd: options.cwd,
|
|
306
315
|
profile: options.profile,
|
|
307
316
|
lifecycle: options.lifecycle,
|
|
317
|
+
sessionStartMode: options.sessionStartMode,
|
|
308
318
|
initialSharedSessionId: stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
|
|
309
319
|
initialResumeConversationId: stateStore.getState().resumeConversationId,
|
|
310
320
|
initialTranscriptPath: stateStore.getState().transcriptPath,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { checkForUpdate, getCurrentVersion,
|
|
2
|
+
import { checkForUpdate, getCurrentVersion, } from "../utils/version-checker.js";
|
|
3
3
|
async function main() {
|
|
4
4
|
const currentVersion = await getCurrentVersion();
|
|
5
5
|
console.log(`CLI WeChat Bridge Version Check`);
|
|
@@ -210,10 +210,16 @@ export function sendLocalCompanionMessage(socket, message) {
|
|
|
210
210
|
return false;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
const MAX_IPC_BUFFER_SIZE = 1_048_576; // 1MB
|
|
213
214
|
export function attachLocalCompanionMessageListener(socket, onMessage) {
|
|
214
215
|
let buffer = "";
|
|
215
216
|
const onData = (chunk) => {
|
|
216
217
|
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
218
|
+
if (buffer.length > MAX_IPC_BUFFER_SIZE) {
|
|
219
|
+
socket.destroy(new Error("IPC buffer overflow: exceeded 1MB without complete frame"));
|
|
220
|
+
buffer = "";
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
217
223
|
while (true) {
|
|
218
224
|
const newlineIndex = buffer.indexOf("\n");
|
|
219
225
|
if (newlineIndex < 0) {
|
|
@@ -37,6 +37,9 @@ export function formatSwitchFailureMessage(cwd) {
|
|
|
37
37
|
export function formatRestartUnhealthyMessage(cwd) {
|
|
38
38
|
return `Detected unhealthy companion state for ${cwd}. Restarting bridge...`;
|
|
39
39
|
}
|
|
40
|
+
export function defaultSessionStartMode(adapter) {
|
|
41
|
+
return adapter === "claude" || adapter === "opencode" ? "new" : "restore";
|
|
42
|
+
}
|
|
40
43
|
export function decideLaunchAction(input) {
|
|
41
44
|
if (input.lockShouldAutoReclaim) {
|
|
42
45
|
return {
|
|
@@ -55,6 +58,13 @@ export function decideLaunchAction(input) {
|
|
|
55
58
|
failureMessage: formatSwitchFailureMessage(input.runningLock.cwd),
|
|
56
59
|
};
|
|
57
60
|
}
|
|
61
|
+
if (input.sessionStartMode === "new" &&
|
|
62
|
+
(input.requestedAdapter === "claude" || input.requestedAdapter === "opencode")) {
|
|
63
|
+
return {
|
|
64
|
+
kind: "start_bridge",
|
|
65
|
+
message: `Starting a fresh ${input.requestedAdapter} session for ${input.requestedCwd}...`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
58
68
|
if (!input.endpoint || !input.endpointIsReachable) {
|
|
59
69
|
return {
|
|
60
70
|
kind: "restart_unhealthy",
|
|
@@ -99,7 +109,8 @@ export function parseCliArgs(argv) {
|
|
|
99
109
|
" wechat-opencode-start [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [...opencode args]",
|
|
100
110
|
" local-companion-start [--adapter <codex|claude|opencode>] [--cwd <path>] [--profile <name-or-path>] [--timeout-ms <ms>] [...cli args]",
|
|
101
111
|
"",
|
|
102
|
-
"Starts
|
|
112
|
+
"Starts a bridge for the current directory, waits for the local endpoint, then opens the visible companion or panel.",
|
|
113
|
+
"Claude and OpenCode launchers start a fresh CLI session by default.",
|
|
103
114
|
"All adapters are companion-bound: closing the companion/panel also stops the bridge.",
|
|
104
115
|
"Unknown arguments are forwarded to the visible CLI client.",
|
|
105
116
|
"",
|
|
@@ -144,7 +155,14 @@ export function parseCliArgs(argv) {
|
|
|
144
155
|
}
|
|
145
156
|
cliArgs.push(arg);
|
|
146
157
|
}
|
|
147
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
adapter,
|
|
160
|
+
cwd,
|
|
161
|
+
profile,
|
|
162
|
+
timeoutMs,
|
|
163
|
+
sessionStartMode: defaultSessionStartMode(adapter),
|
|
164
|
+
cliArgs,
|
|
165
|
+
};
|
|
148
166
|
}
|
|
149
167
|
function isPidAlive(pid) {
|
|
150
168
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
@@ -247,6 +265,9 @@ export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
|
247
265
|
args.push("--experimental-strip-types");
|
|
248
266
|
}
|
|
249
267
|
args.push(entryPath, "--adapter", options.adapter, "--cwd", options.cwd, "--lifecycle", lifecycle);
|
|
268
|
+
if (options.sessionStartMode !== "restore") {
|
|
269
|
+
args.push("--session-start-mode", options.sessionStartMode);
|
|
270
|
+
}
|
|
250
271
|
if (options.profile) {
|
|
251
272
|
args.push("--profile", options.profile);
|
|
252
273
|
}
|
|
@@ -304,6 +325,7 @@ async function ensureBridgeReady(options) {
|
|
|
304
325
|
endpoint: endpointResult.endpoint,
|
|
305
326
|
endpointIsReachable: Boolean(endpointResult.endpoint),
|
|
306
327
|
companionIsAlive: isCompanionAlive(endpointResult.endpoint),
|
|
328
|
+
sessionStartMode: options.sessionStartMode,
|
|
307
329
|
});
|
|
308
330
|
log(options.adapter, decision.message);
|
|
309
331
|
if (decision.kind === "already_active") {
|
|
@@ -355,6 +377,8 @@ export async function tryDelegateToDaemon(options, deps = {}) {
|
|
|
355
377
|
profile: options.profile,
|
|
356
378
|
cliArgs: options.cliArgs,
|
|
357
379
|
openVisible: true,
|
|
380
|
+
sessionStartMode: options.sessionStartMode,
|
|
381
|
+
reuseExistingVisible: true,
|
|
358
382
|
});
|
|
359
383
|
if (!response.ok) {
|
|
360
384
|
throw new Error(response.error);
|
|
@@ -374,6 +398,7 @@ export async function runVisibleClient(options, runners = {}) {
|
|
|
374
398
|
return await (runners.localCompanion ?? runLocalCompanion)({
|
|
375
399
|
adapter: options.adapter,
|
|
376
400
|
cwd: options.cwd,
|
|
401
|
+
sessionStartMode: options.sessionStartMode,
|
|
377
402
|
cliArgs: options.cliArgs,
|
|
378
403
|
});
|
|
379
404
|
}
|
|
@@ -13,6 +13,7 @@ function log(adapter, message) {
|
|
|
13
13
|
function parseCliArgs(argv) {
|
|
14
14
|
let adapter = null;
|
|
15
15
|
let cwd = process.cwd();
|
|
16
|
+
let sessionStartMode = "restore";
|
|
16
17
|
const cliArgs = [];
|
|
17
18
|
for (let i = 0; i < argv.length; i += 1) {
|
|
18
19
|
const arg = argv[i];
|
|
@@ -46,12 +47,20 @@ function parseCliArgs(argv) {
|
|
|
46
47
|
i += 1;
|
|
47
48
|
continue;
|
|
48
49
|
}
|
|
50
|
+
if (arg === "--session-start-mode") {
|
|
51
|
+
if (!next || !["restore", "new"].includes(next)) {
|
|
52
|
+
throw new Error(`Invalid session start mode: ${next ?? "(missing)"}`);
|
|
53
|
+
}
|
|
54
|
+
sessionStartMode = next;
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
49
58
|
cliArgs.push(arg);
|
|
50
59
|
}
|
|
51
60
|
if (!adapter) {
|
|
52
61
|
throw new Error("Missing required --adapter <codex|claude|opencode>");
|
|
53
62
|
}
|
|
54
|
-
return { adapter, cwd, cliArgs };
|
|
63
|
+
return { adapter, cwd, sessionStartMode, cliArgs };
|
|
55
64
|
}
|
|
56
65
|
function delay(ms) {
|
|
57
66
|
if (ms <= 0) {
|
|
@@ -79,14 +88,22 @@ export async function runLocalCompanion(options) {
|
|
|
79
88
|
extraCliArgs: options.cliArgs,
|
|
80
89
|
});
|
|
81
90
|
}
|
|
91
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
82
92
|
const adapter = createBridgeAdapter({
|
|
83
93
|
kind: initialEndpoint.kind,
|
|
84
94
|
command: initialEndpoint.command,
|
|
85
95
|
cwd: initialEndpoint.cwd,
|
|
86
96
|
profile: initialEndpoint.profile,
|
|
87
|
-
initialSharedSessionId:
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
initialSharedSessionId: shouldRestoreInitialSession
|
|
98
|
+
? initialEndpoint.sharedSessionId ?? initialEndpoint.sharedThreadId
|
|
99
|
+
: undefined,
|
|
100
|
+
initialResumeConversationId: shouldRestoreInitialSession
|
|
101
|
+
? initialEndpoint.resumeConversationId
|
|
102
|
+
: undefined,
|
|
103
|
+
initialTranscriptPath: shouldRestoreInitialSession
|
|
104
|
+
? initialEndpoint.transcriptPath
|
|
105
|
+
: undefined,
|
|
106
|
+
sessionStartMode: options.sessionStartMode,
|
|
90
107
|
renderMode: initialEndpoint.kind === "codex" ? "panel" : "companion",
|
|
91
108
|
extraCliArgs: options.cliArgs,
|
|
92
109
|
});
|
|
@@ -80,10 +80,16 @@ export function sendDaemonResponse(socket, id, response) {
|
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
const MAX_IPC_BUFFER_SIZE = 1_048_576; // 1MB
|
|
83
84
|
export function attachDaemonRequestListener(socket, onRequest) {
|
|
84
85
|
let buffer = "";
|
|
85
86
|
const onData = (chunk) => {
|
|
86
87
|
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
88
|
+
if (buffer.length > MAX_IPC_BUFFER_SIZE) {
|
|
89
|
+
socket.destroy(new Error("IPC buffer overflow: exceeded 1MB without complete frame"));
|
|
90
|
+
buffer = "";
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
87
93
|
while (true) {
|
|
88
94
|
const newlineIndex = buffer.indexOf("\n");
|
|
89
95
|
if (newlineIndex < 0) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { EMOJI_BINDINGS_FILE } from "../wechat/channel-config.js";
|
|
3
|
+
export const DEFAULT_EMOJI_BINDINGS = {
|
|
4
|
+
"[OK]": "/confirm",
|
|
5
|
+
"[闭嘴]": "/stop",
|
|
6
|
+
"[拥抱]": "/claude",
|
|
7
|
+
"[强]": "/codex",
|
|
8
|
+
"[胜利]": "/opencode",
|
|
9
|
+
"[再见]": "/daemon-stop",
|
|
10
|
+
};
|
|
11
|
+
let bindingsMap = null;
|
|
12
|
+
function buildMap(bindings) {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
for (const [emoji, command] of Object.entries(bindings)) {
|
|
15
|
+
map.set(emoji.toLowerCase(), command);
|
|
16
|
+
}
|
|
17
|
+
return map;
|
|
18
|
+
}
|
|
19
|
+
export function loadEmojiBindings() {
|
|
20
|
+
if (bindingsMap) {
|
|
21
|
+
return bindingsMap;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(EMOJI_BINDINGS_FILE)) {
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(EMOJI_BINDINGS_FILE, "utf-8"));
|
|
26
|
+
if (raw.bindings && typeof raw.bindings === "object") {
|
|
27
|
+
bindingsMap = buildMap(raw.bindings);
|
|
28
|
+
return bindingsMap;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Fall through to defaults on parse error.
|
|
34
|
+
}
|
|
35
|
+
bindingsMap = buildMap(DEFAULT_EMOJI_BINDINGS);
|
|
36
|
+
return bindingsMap;
|
|
37
|
+
}
|
|
38
|
+
export function saveEmojiBindings(map) {
|
|
39
|
+
const bindings = {};
|
|
40
|
+
for (const [emoji, command] of map) {
|
|
41
|
+
bindings[emoji] = command;
|
|
42
|
+
}
|
|
43
|
+
const data = { bindings };
|
|
44
|
+
fs.writeFile(EMOJI_BINDINGS_FILE, JSON.stringify(data, null, 2) + "\n", () => { });
|
|
45
|
+
}
|
|
46
|
+
export function setBinding(emoji, command) {
|
|
47
|
+
const map = loadEmojiBindings();
|
|
48
|
+
map.set(emoji.toLowerCase(), command);
|
|
49
|
+
bindingsMap = map;
|
|
50
|
+
saveEmojiBindings(map);
|
|
51
|
+
}
|
|
52
|
+
export function removeBinding(emoji) {
|
|
53
|
+
const map = loadEmojiBindings();
|
|
54
|
+
const key = emoji.toLowerCase();
|
|
55
|
+
if (!map.has(key)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
map.delete(key);
|
|
59
|
+
bindingsMap = map;
|
|
60
|
+
saveEmojiBindings(map);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
export function listBindings() {
|
|
64
|
+
return loadEmojiBindings();
|
|
65
|
+
}
|
|
66
|
+
export function resolveEmojiCommand(text) {
|
|
67
|
+
const trimmed = text.trim();
|
|
68
|
+
if (!trimmed.startsWith("[")) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const map = loadEmojiBindings();
|
|
72
|
+
const lower = trimmed.toLowerCase();
|
|
73
|
+
for (const [emoji, command] of map) {
|
|
74
|
+
if (lower.startsWith(emoji)) {
|
|
75
|
+
const remainder = trimmed.slice(emoji.length).trim();
|
|
76
|
+
return { command, remainder };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|