cli-wechat-bridge 1.0.5 → 1.0.7

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.
@@ -1,12 +1,13 @@
1
1
  import { spawn as spawnChildProcess } from "node:child_process";
2
2
  import { OPENCODE_SERVER_HOST, OPENCODE_SERVER_READY_TIMEOUT_MS, OPENCODE_SSE_RECONNECT_DELAY_MS, OPENCODE_SESSION_IDLE_SETTLE_MS, OPENCODE_WECHAT_WORKING_NOTICE_DELAY_MS, buildCliEnvironment, isRecord, describeUnknownError, resolveSpawnTarget, reserveLocalPort, waitForTcpPort, delay, } from "./bridge-adapters.shared.js";
3
3
  import { killProcessTreeSync } from "./bridge-process-reaper.js";
4
- import { buildOneTimeCode, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
4
+ import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, normalizeOutput, nowIso, truncatePreview, OutputBatcher, } from "./bridge-utils.js";
5
5
  const OPENCODE_DEBUG_ENABLED = /^(1|true|yes|on)$/i.test(process.env.WECHAT_OPENCODE_DEBUG ?? "");
6
6
  const OPENCODE_DUPLICATE_EVENT_TTL_MS = 150;
7
7
  const OPENCODE_WECHAT_MIRROR_SUPPRESSION_TTL_MS = 30_000;
8
8
  const OPENCODE_RECENT_LOCAL_PROMPT_TTL_MS = 10_000;
9
9
  const OPENCODE_LOCAL_SESSION_CREATE_FOLLOW_TTL_MS = 5_000;
10
+ const OPENCODE_TUI_SELECT_RETRY_DELAYS_MS = [250, 750, 1_500];
10
11
  /* ------------------------------------------------------------------ */
11
12
  /* Adapter */
12
13
  /* ------------------------------------------------------------------ */
@@ -82,7 +83,7 @@ export class OpenCodeServerAdapter {
82
83
  this.startSseListener();
83
84
  if (this.options.renderMode === "companion") {
84
85
  await this.startNativeClient();
85
- await this.syncVisibleSessionToShared({ force: true });
86
+ await this.syncVisibleSessionToShared({ force: true, retry: true });
86
87
  }
87
88
  else {
88
89
  this.state.pid = serverProcess.pid;
@@ -177,6 +178,7 @@ export class OpenCodeServerAdapter {
177
178
  clearTrackedTurn: true,
178
179
  syncVisible: true,
179
180
  forceVisibleSync: true,
181
+ retryVisibleSync: true,
180
182
  });
181
183
  }
182
184
  async interrupt() {
@@ -365,7 +367,7 @@ export class OpenCodeServerAdapter {
365
367
  if (this.nativeProcess) {
366
368
  return;
367
369
  }
368
- const env = buildCliEnvironment(this.options.kind);
370
+ const env = this.buildNativeClientEnv();
369
371
  const attachArgs = await this.buildNativeAttachArgs();
370
372
  const target = resolveSpawnTarget(this.options.command, this.options.kind, { env });
371
373
  const startedAt = nowIso();
@@ -410,6 +412,7 @@ export class OpenCodeServerAdapter {
410
412
  }
411
413
  async buildNativeAttachArgs() {
412
414
  const args = ["attach", this.getServerUrl()];
415
+ args.push("--dir", this.options.cwd);
413
416
  const sessionId = this.activeSessionId;
414
417
  if (sessionId && (await this.hasSession(sessionId))) {
415
418
  args.push("--session", sessionId);
@@ -417,6 +420,16 @@ export class OpenCodeServerAdapter {
417
420
  args.push(...(this.options.extraCliArgs ?? []));
418
421
  return args;
419
422
  }
423
+ buildNativeClientEnv() {
424
+ const env = buildCliEnvironment(this.options.kind);
425
+ if (this.activeSessionId) {
426
+ env.OPENCODE_ROUTE = JSON.stringify({
427
+ type: "session",
428
+ sessionID: this.activeSessionId,
429
+ });
430
+ }
431
+ return env;
432
+ }
420
433
  async createSdkClient() {
421
434
  try {
422
435
  const { createOpencodeClient } = await import("@opencode-ai/sdk/v2");
@@ -441,6 +454,21 @@ export class OpenCodeServerAdapter {
441
454
  if (!this.client) {
442
455
  return;
443
456
  }
457
+ if (this.options.sessionStartMode === "new") {
458
+ const session = this.unwrapOrThrow(await this.client.session.create({
459
+ directory: this.options.cwd,
460
+ workspace: this.activeWorkspaceId ?? undefined,
461
+ }));
462
+ this.switchSharedSession(session, {
463
+ source: "local",
464
+ reason: "local_follow",
465
+ syncVisible: false,
466
+ });
467
+ this.state.lastSessionSwitchAt = undefined;
468
+ this.state.lastSessionSwitchSource = undefined;
469
+ this.state.lastSessionSwitchReason = undefined;
470
+ return;
471
+ }
444
472
  let listedSessions = [];
445
473
  try {
446
474
  const result = await this.client.session.list();
@@ -801,6 +829,11 @@ export class OpenCodeServerAdapter {
801
829
  return;
802
830
  }
803
831
  this.clearWechatWorkingNotice();
832
+ const denyMessage = this.getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission);
833
+ if (denyMessage) {
834
+ this.rejectPermission(pendingPermission, denyMessage);
835
+ return;
836
+ }
804
837
  const approval = this.toPendingApproval(pendingPermission);
805
838
  this.pendingPermission = pendingPermission;
806
839
  this.state.pendingApproval = approval;
@@ -817,6 +850,57 @@ export class OpenCodeServerAdapter {
817
850
  this.state.pendingApproval = null;
818
851
  this.state.pendingApprovalOrigin = undefined;
819
852
  }
853
+ getWechatOutboundAttachmentPermissionDenyMessage(properties, pendingPermission) {
854
+ const metadata = isRecord(properties.metadata) ? properties.metadata : {};
855
+ const command = this.extractPermissionCommand(properties, metadata);
856
+ if (isWechatOutboundAttachmentWriteCommand(command)) {
857
+ return WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
858
+ }
859
+ const targetPath = [
860
+ command,
861
+ typeof metadata.path === "string" ? metadata.path : "",
862
+ typeof metadata.file === "string" ? metadata.file : "",
863
+ typeof metadata.filePath === "string" ? metadata.filePath : "",
864
+ typeof metadata.filepath === "string" ? metadata.filepath : "",
865
+ typeof metadata.parentDir === "string" ? metadata.parentDir : "",
866
+ typeof metadata.target === "string" ? metadata.target : "",
867
+ typeof metadata.detail === "string" ? metadata.detail : "",
868
+ Array.isArray(properties.patterns)
869
+ ? properties.patterns.filter((value) => typeof value === "string").join(" ")
870
+ : "",
871
+ typeof properties.pattern === "string" ? properties.pattern : "",
872
+ ].join(" ");
873
+ return isWechatOutboundAttachmentMutationTool(pendingPermission.request.toolName, targetPath)
874
+ ? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
875
+ : null;
876
+ }
877
+ rejectPermission(pendingPermission, denyMessage) {
878
+ const client = this.client;
879
+ if (!client) {
880
+ return;
881
+ }
882
+ void (async () => {
883
+ try {
884
+ const result = await client.permission.respond({
885
+ sessionID: pendingPermission.sessionId,
886
+ permissionID: pendingPermission.permissionId,
887
+ directory: this.options.cwd,
888
+ workspace: this.activeWorkspaceId ?? undefined,
889
+ response: "reject",
890
+ });
891
+ if (result.error !== undefined) {
892
+ throw new Error(`SDK error: ${describeUnknownError(result.error)}`);
893
+ }
894
+ }
895
+ catch (err) {
896
+ this.emit({
897
+ type: "stderr",
898
+ text: `${denyMessage}\nFailed to reject permission: ${describeUnknownError(err)}`,
899
+ timestamp: nowIso(),
900
+ });
901
+ }
902
+ })();
903
+ }
820
904
  toPendingApproval(pendingPermission) {
821
905
  return {
822
906
  ...pendingPermission.request,
@@ -834,24 +918,14 @@ export class OpenCodeServerAdapter {
834
918
  if (!sessionId || !permissionId) {
835
919
  return null;
836
920
  }
837
- const toolName = typeof properties.type === "string"
838
- ? properties.type
839
- : typeof properties.permission === "string"
840
- ? properties.permission
841
- : undefined;
921
+ const toolName = this.extractPermissionToolName(properties);
842
922
  const title = typeof properties.title === "string"
843
923
  ? properties.title
844
924
  : typeof properties.permission === "string"
845
925
  ? `Permission request: ${properties.permission}`
846
926
  : undefined;
847
927
  const metadata = isRecord(properties.metadata) ? properties.metadata : {};
848
- const command = typeof metadata.command === "string"
849
- ? metadata.command
850
- : typeof metadata.detail === "string"
851
- ? metadata.detail
852
- : Array.isArray(properties.patterns)
853
- ? properties.patterns.filter((value) => typeof value === "string").join(", ")
854
- : undefined;
928
+ const command = this.extractPermissionCommand(properties, metadata);
855
929
  return {
856
930
  sessionId,
857
931
  permissionId,
@@ -869,6 +943,26 @@ export class OpenCodeServerAdapter {
869
943
  },
870
944
  };
871
945
  }
946
+ extractPermissionToolName(properties) {
947
+ return typeof properties.type === "string"
948
+ ? properties.type
949
+ : typeof properties.permission === "string"
950
+ ? properties.permission
951
+ : undefined;
952
+ }
953
+ extractPermissionCommand(properties, metadata) {
954
+ if (typeof metadata.command === "string") {
955
+ return metadata.command;
956
+ }
957
+ if (typeof metadata.detail === "string") {
958
+ return metadata.detail;
959
+ }
960
+ if (Array.isArray(properties.patterns)) {
961
+ const patterns = properties.patterns.filter((value) => typeof value === "string");
962
+ return patterns.length > 0 ? patterns.join(", ") : undefined;
963
+ }
964
+ return undefined;
965
+ }
872
966
  handleSessionCreated(properties) {
873
967
  if (!isRecord(properties)) {
874
968
  return;
@@ -1788,7 +1882,9 @@ export class OpenCodeServerAdapter {
1788
1882
  this.recordSessionSwitch(sessionId, options.source, options.reason, options.notify);
1789
1883
  }
1790
1884
  if (options.syncVisible !== false && (changed || options.forceVisibleSync)) {
1791
- void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session);
1885
+ void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session, {
1886
+ retry: options.retryVisibleSync,
1887
+ });
1792
1888
  }
1793
1889
  return changed;
1794
1890
  }
@@ -1799,7 +1895,7 @@ export class OpenCodeServerAdapter {
1799
1895
  await this.syncVisibleSessionSelection({
1800
1896
  id: this.activeSessionId,
1801
1897
  workspaceID: this.activeWorkspaceId ?? undefined,
1802
- }, { force: options.force });
1898
+ }, { force: options.force, retry: options.retry });
1803
1899
  }
1804
1900
  async syncVisibleSessionSelection(session, options = {}) {
1805
1901
  if (!this.client?.tui ||
@@ -1810,13 +1906,9 @@ export class OpenCodeServerAdapter {
1810
1906
  this.suppressedTuiSessionSelectId = session.id;
1811
1907
  this.logDebug(`[opencode-adapter:tui] selectSession session=${session.id} workspace=${session.workspaceID ?? this.activeWorkspaceId ?? "(none)"}`);
1812
1908
  try {
1813
- const result = await this.client.tui.selectSession({
1814
- directory: this.options.cwd,
1815
- workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
1816
- sessionID: session.id,
1817
- });
1818
- if (result.error !== undefined) {
1819
- throw result.error;
1909
+ await this.sendVisibleSessionSelection(session);
1910
+ if (options.retry === true) {
1911
+ this.scheduleVisibleSessionSelectionRetries(session);
1820
1912
  }
1821
1913
  }
1822
1914
  catch (err) {
@@ -1826,6 +1918,33 @@ export class OpenCodeServerAdapter {
1826
1918
  this.logDebug(`[opencode-adapter:tui] selectSession failed for ${session.id}: ${describeUnknownError(err)}`);
1827
1919
  }
1828
1920
  }
1921
+ async sendVisibleSessionSelection(session) {
1922
+ if (!this.client?.tui) {
1923
+ return;
1924
+ }
1925
+ const result = await this.client.tui.selectSession({
1926
+ directory: this.options.cwd,
1927
+ workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
1928
+ sessionID: session.id,
1929
+ });
1930
+ if (result.error !== undefined) {
1931
+ throw result.error;
1932
+ }
1933
+ }
1934
+ scheduleVisibleSessionSelectionRetries(session) {
1935
+ for (const retryDelayMs of OPENCODE_TUI_SELECT_RETRY_DELAYS_MS) {
1936
+ setTimeout(() => {
1937
+ if (this.shuttingDown ||
1938
+ this.options.renderMode !== "companion" ||
1939
+ this.activeSessionId !== session.id) {
1940
+ return;
1941
+ }
1942
+ void this.sendVisibleSessionSelection(session).catch((err) => {
1943
+ this.logDebug(`[opencode-adapter:tui] selectSession retry failed for ${session.id}: ${describeUnknownError(err)}`);
1944
+ });
1945
+ }, retryDelayMs).unref?.();
1946
+ }
1947
+ }
1829
1948
  isLocalSessionNavigationCommand(command) {
1830
1949
  if (!command) {
1831
1950
  return false;
@@ -2,13 +2,10 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import net from "node:net";
4
4
  import { fileURLToPath } from "node:url";
5
- import { spawn as spawnChild, spawnSync } from "node:child_process";
5
+ import { spawnSync } from "node:child_process";
6
6
  import { spawn as spawnPty } from "node-pty";
7
- import { attachLocalCompanionMessageListener, buildLocalCompanionToken, clearLocalCompanionEndpoint, sendLocalCompanionMessage, writeLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
8
- import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
- import { buildClaudeFailureMessage, buildClaudeHookSettings, buildClaudePermissionDecisionHookOutput, buildClaudePermissionApprovalRequest, extractClaudeResumeConversationId, findInjectedClaudePromptIndex, normalizeClaudeAssistantMessage, parseClaudeHookPayload, } from "./claude-hooks.js";
10
- import { detectCliApproval, isHighRiskShellCommand, normalizeOutput, nowIso, truncatePreview, } from "./bridge-utils.js";
11
- import { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
7
+ import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, isWechatOutboundAttachmentWriteCommand, isHighRiskShellCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
8
+ import { getNotificationThreadId, isRecord, } from "./bridge-adapter-common.js";
12
9
  export { coerceWebSocketMessageData, describeUnknownError, getCodexRpcRequestId, getLocalCompanionCommandName, getNotificationThreadId, getNotificationTurnId, getSharedSessionIdFromAdapterState, isRecentIsoTimestamp, isRecord, normalizeCodexRpcError, quotePosixCommandArg, quoteWindowsCommandArg, } from "./bridge-adapter-common.js";
13
10
  export const DEFAULT_COLS = 120;
14
11
  export const DEFAULT_ROWS = 30;
@@ -152,8 +149,220 @@ export function buildCodexApprovalRequest(method, params) {
152
149
  commandPreview: truncatePreview(preview, 180),
153
150
  };
154
151
  }
152
+ if (method === "item/permissions/requestApproval") {
153
+ const reason = typeof params.reason === "string" ? params.reason : "";
154
+ const preview = summarizeCodexPermissionsRequest(params.permissions);
155
+ return {
156
+ source: "cli",
157
+ summary: reason
158
+ ? `Codex needs approval before granting extra permissions: ${truncatePreview(reason, 160)}`
159
+ : "Codex needs approval before granting extra permissions.",
160
+ commandPreview: truncatePreview(preview, 180),
161
+ };
162
+ }
163
+ return null;
164
+ }
165
+ export function getCodexWechatOutboundAttachmentDenyMessage(method, params) {
166
+ if (!isRecord(params)) {
167
+ return null;
168
+ }
169
+ if (method === "item/commandExecution/requestApproval") {
170
+ return isWechatOutboundAttachmentWriteCommand(params.command) ||
171
+ containsWechatOutboundAttachmentPathDeep(params.additionalPermissions)
172
+ ? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
173
+ : null;
174
+ }
175
+ if (method === "item/fileChange/requestApproval") {
176
+ return containsWechatOutboundAttachmentPath(params.grantRoot)
177
+ ? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
178
+ : null;
179
+ }
180
+ if (method === "item/permissions/requestApproval") {
181
+ return containsWechatOutboundAttachmentPathDeep(params.permissions)
182
+ ? WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
183
+ : null;
184
+ }
155
185
  return null;
156
186
  }
187
+ function commandApprovalAllowsAccept(availableDecisions) {
188
+ if (!Array.isArray(availableDecisions)) {
189
+ return true;
190
+ }
191
+ return availableDecisions.some((decision) => decision === "accept");
192
+ }
193
+ function normalizePermissionPath(pathValue) {
194
+ const normalized = pathValue.trim().replace(/\\/g, "/").toLowerCase();
195
+ return normalized === "/" ? normalized : normalized.replace(/\/+$/g, "");
196
+ }
197
+ function isHighRiskPermissionPath(pathValue) {
198
+ const normalized = normalizePermissionPath(pathValue);
199
+ if (!normalized) {
200
+ return false;
201
+ }
202
+ if (normalized === "/" || /^[a-z]:$/i.test(normalized)) {
203
+ return true;
204
+ }
205
+ return /^[a-z]:(?:\/windows|\/program files(?: \(x86\))?|\/programdata)(?:\/|$)/i.test(normalized);
206
+ }
207
+ function collectPermissionPaths(value, output) {
208
+ if (typeof value === "string") {
209
+ output.push(value);
210
+ return;
211
+ }
212
+ if (Array.isArray(value)) {
213
+ for (const item of value) {
214
+ collectPermissionPaths(item, output);
215
+ }
216
+ return;
217
+ }
218
+ if (!isRecord(value)) {
219
+ return;
220
+ }
221
+ if (typeof value.path === "string") {
222
+ output.push(value.path);
223
+ }
224
+ if (typeof value.pattern === "string") {
225
+ output.push(value.pattern);
226
+ }
227
+ for (const item of Object.values(value)) {
228
+ collectPermissionPaths(item, output);
229
+ }
230
+ }
231
+ function hasRootSpecialPermissionPath(value) {
232
+ if (Array.isArray(value)) {
233
+ return value.some(hasRootSpecialPermissionPath);
234
+ }
235
+ if (!isRecord(value)) {
236
+ return false;
237
+ }
238
+ if ((value.kind === "root" || value.kind === "Root") ||
239
+ (value.value === "root" || value.value === "Root")) {
240
+ return true;
241
+ }
242
+ return Object.values(value).some(hasRootSpecialPermissionPath);
243
+ }
244
+ function containsHighRiskPermissionTarget(value) {
245
+ if (hasRootSpecialPermissionPath(value)) {
246
+ return true;
247
+ }
248
+ const paths = [];
249
+ collectPermissionPaths(value, paths);
250
+ return paths.some(isHighRiskPermissionPath);
251
+ }
252
+ export function getCodexApprovalAutoResponse(method, params) {
253
+ if (method === "item/commandExecution/requestApproval") {
254
+ const command = typeof params.command === "string" ? params.command : "";
255
+ if (!commandApprovalAllowsAccept(params.availableDecisions)) {
256
+ return null;
257
+ }
258
+ if (command && isHighRiskShellCommand(command)) {
259
+ return null;
260
+ }
261
+ if (containsHighRiskPermissionTarget(params.additionalPermissions)) {
262
+ return null;
263
+ }
264
+ return {
265
+ result: { decision: "accept" },
266
+ reason: command
267
+ ? `low-risk command ${truncatePreview(command, 120)}`
268
+ : "low-risk command approval",
269
+ };
270
+ }
271
+ if (method === "item/fileChange/requestApproval") {
272
+ if (containsHighRiskPermissionTarget(params.grantRoot)) {
273
+ return null;
274
+ }
275
+ return {
276
+ result: { decision: "accept" },
277
+ reason: "low-risk file change approval",
278
+ };
279
+ }
280
+ if (method === "item/permissions/requestApproval") {
281
+ if (containsHighRiskPermissionTarget(params.permissions)) {
282
+ return null;
283
+ }
284
+ return {
285
+ result: buildCodexPermissionsRequestApprovalResponse(params, "confirm", {
286
+ strictAutoReview: true,
287
+ }),
288
+ reason: "low-risk permission grant",
289
+ };
290
+ }
291
+ return null;
292
+ }
293
+ function collectStringValues(value) {
294
+ return Array.isArray(value)
295
+ ? value.filter((item) => typeof item === "string" && item.trim().length > 0)
296
+ : [];
297
+ }
298
+ function summarizeCodexPermissionsRequest(permissions) {
299
+ if (!isRecord(permissions)) {
300
+ return "Additional permissions requested.";
301
+ }
302
+ const parts = [];
303
+ if (isRecord(permissions.network) && permissions.network.enabled === true) {
304
+ parts.push("network access");
305
+ }
306
+ if (isRecord(permissions.fileSystem)) {
307
+ const readPaths = collectStringValues(permissions.fileSystem.read);
308
+ const writePaths = collectStringValues(permissions.fileSystem.write);
309
+ if (readPaths.length > 0) {
310
+ parts.push(`read: ${readPaths.join(", ")}`);
311
+ }
312
+ if (writePaths.length > 0) {
313
+ parts.push(`write: ${writePaths.join(", ")}`);
314
+ }
315
+ if (Array.isArray(permissions.fileSystem.entries) && permissions.fileSystem.entries.length > 0) {
316
+ parts.push(`filesystem entries: ${permissions.fileSystem.entries.length}`);
317
+ }
318
+ }
319
+ return parts.length > 0 ? parts.join("; ") : "Additional permissions requested.";
320
+ }
321
+ function clonePermissionObject(value) {
322
+ if (!isRecord(value)) {
323
+ return null;
324
+ }
325
+ return JSON.parse(JSON.stringify(value));
326
+ }
327
+ export function buildCodexPermissionsRequestApprovalResponse(params, action = "deny", options = {}) {
328
+ const permissions = {};
329
+ if (action === "confirm" && isRecord(params) && isRecord(params.permissions)) {
330
+ const network = clonePermissionObject(params.permissions.network);
331
+ const fileSystem = clonePermissionObject(params.permissions.fileSystem);
332
+ if (network) {
333
+ permissions.network = network;
334
+ }
335
+ if (fileSystem) {
336
+ permissions.fileSystem = fileSystem;
337
+ }
338
+ }
339
+ const response = {
340
+ permissions,
341
+ scope: "turn",
342
+ };
343
+ if (options.strictAutoReview) {
344
+ response.strictAutoReview = true;
345
+ }
346
+ return response;
347
+ }
348
+ export function buildCodexMcpServerElicitationDeclineResponse() {
349
+ return {
350
+ action: "decline",
351
+ content: null,
352
+ _meta: null,
353
+ };
354
+ }
355
+ export function buildCodexDynamicToolCallFailureResponse() {
356
+ return {
357
+ contentItems: [
358
+ {
359
+ type: "inputText",
360
+ text: "Dynamic tool calls are not supported by the WeChat bridge.",
361
+ },
362
+ ],
363
+ success: false,
364
+ };
365
+ }
157
366
  export function buildCodexUserInputRequest(params) {
158
367
  if (!isRecord(params) || !Array.isArray(params.questions)) {
159
368
  return null;
@@ -287,6 +496,7 @@ export function shouldRecoverCodexStaleBusyState(params) {
287
496
  !params.pendingTurnStart &&
288
497
  !params.hasActiveTurn &&
289
498
  !params.hasPendingApproval &&
499
+ !params.hasPendingUserInput &&
290
500
  !params.activeTurnId);
291
501
  }
292
502
  export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
@@ -295,6 +505,7 @@ export function shouldAutoCompleteCodexWechatTurnAfterFinalReply(params) {
295
505
  params.activeTurnOrigin === "wechat" &&
296
506
  !params.pendingTurnStart &&
297
507
  !params.hasPendingApproval &&
508
+ !params.hasPendingUserInput &&
298
509
  params.hasFinalOutput &&
299
510
  !params.hasCompletedTurn &&
300
511
  typeof params.lastActivityAtMs === "number" &&
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
2
3
  const PEER_BRIDGE_EXIT_TIMEOUT_MS = 4_000;
3
4
  const PEER_BRIDGE_EXIT_POLL_MS = 100;
4
5
  function isPidAlive(pid) {
@@ -24,7 +25,30 @@ function isRecord(value) {
24
25
  }
25
26
  export function isWechatBridgeCommandLine(commandLine) {
26
27
  return (/wechat-bridge\.(?:ts|js|mjs)/i.test(commandLine) &&
27
- /(?:^|\s)--adapter(?:\s|=)/i.test(commandLine));
28
+ /(?:^|\s|["'])--adapter(?:(?:["']?\s)|=)/i.test(commandLine));
29
+ }
30
+ export function isWechatDaemonCommandLine(commandLine) {
31
+ return /wechat-daemon\.(?:ts|js|mjs)/i.test(commandLine);
32
+ }
33
+ function normalizeComparablePath(filePath) {
34
+ const normalized = path.resolve(filePath);
35
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
36
+ }
37
+ function extractCommandLineOption(commandLine, optionName) {
38
+ const escaped = optionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39
+ const pattern = new RegExp(`(?:^|\\s|["'])--${escaped}(?:(?:["']?\\s+)|=)(?:"([^"]+)"|'([^']+)'|(\\S+))`, "i");
40
+ const match = pattern.exec(commandLine);
41
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
42
+ }
43
+ export function isWechatDaemonCommandLineForCwd(commandLine, cwd) {
44
+ if (!isWechatDaemonCommandLine(commandLine)) {
45
+ return false;
46
+ }
47
+ const commandCwd = extractCommandLineOption(commandLine, "cwd");
48
+ if (!commandCwd) {
49
+ return true;
50
+ }
51
+ return normalizeComparablePath(commandCwd) === normalizeComparablePath(cwd);
28
52
  }
29
53
  /**
30
54
  * Detects `opencode serve` command lines spawned by the bridge adapter.
@@ -165,6 +189,21 @@ export function listPeerBridgeProcesses(currentPid = process.pid) {
165
189
  ? listWindowsBridgeProcesses(currentPid)
166
190
  : listPosixBridgeProcesses(currentPid);
167
191
  }
192
+ export function getProcessRecordByPid(pid, currentPid = process.pid) {
193
+ if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) {
194
+ return null;
195
+ }
196
+ return listAllProcessesRaw(currentPid).find((record) => record.pid === pid) ?? null;
197
+ }
198
+ export function listWechatDaemonProcesses(params = {}) {
199
+ const currentPid = params.currentPid ?? process.pid;
200
+ const excludePids = new Set(params.excludePids ?? []);
201
+ return listAllProcessesRaw(currentPid)
202
+ .filter((record) => !excludePids.has(record.pid))
203
+ .filter((record) => params.cwd
204
+ ? isWechatDaemonCommandLineForCwd(record.commandLine, params.cwd)
205
+ : isWechatDaemonCommandLine(record.commandLine));
206
+ }
168
207
  /**
169
208
  * List all managed OpenCode child processes (`serve` and `attach`) whose parent
170
209
  * PID is no longer alive. These are orphaned processes left behind when a bridge
@@ -169,14 +169,17 @@ export class BridgeStateStore {
169
169
  lifecycle: options.lifecycle,
170
170
  };
171
171
  this.acquireLock();
172
+ const shouldRestoreSessionState = options.sessionStartMode !== "new";
172
173
  const persisted = this.readStateFile();
173
- const persistedSharedSessionId = resolveRestorableSharedSessionId(persisted, options);
174
- const persistedResumeConversationId = options.adapter === "claude" &&
174
+ const persistedSharedSessionId = resolveRestorableSharedSessionId(shouldRestoreSessionState ? persisted : null, options);
175
+ const persistedResumeConversationId = shouldRestoreSessionState &&
176
+ options.adapter === "claude" &&
175
177
  persisted?.cwd === options.cwd &&
176
178
  typeof persisted.resumeConversationId === "string"
177
179
  ? persisted.resumeConversationId
178
180
  : undefined;
179
- const persistedTranscriptPath = options.adapter === "claude" &&
181
+ const persistedTranscriptPath = shouldRestoreSessionState &&
182
+ options.adapter === "claude" &&
180
183
  persisted?.cwd === options.cwd &&
181
184
  typeof persisted.transcriptPath === "string"
182
185
  ? persisted.transcriptPath