@unlinearity/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.
@@ -71,6 +71,11 @@ export function buildClaudeHookSettings(command) {
71
71
  ],
72
72
  Stop: [hook],
73
73
  StopFailure: [hook],
74
+ PreToolUse: [hook],
75
+ PostToolUse: [hook],
76
+ PreCompact: [hook],
77
+ PostCompact: [hook],
78
+ SubagentStop: [hook],
74
79
  },
75
80
  };
76
81
  }
@@ -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, fetchLatestVersion, compareVersions, } from "../utils/version-checker.js";
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 or reuses a Codex, Claude, or OpenCode bridge for the current directory, waits for the local endpoint, then opens the visible companion or panel.",
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 { adapter, cwd, profile, timeoutMs, cliArgs };
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: initialEndpoint.sharedSessionId ?? initialEndpoint.sharedThreadId,
88
- initialResumeConversationId: initialEndpoint.resumeConversationId,
89
- initialTranscriptPath: initialEndpoint.transcriptPath,
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
+ }