cli-wechat-bridge 1.0.6 → 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,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import net from "node:net";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { buildLocalCompanionToken } from "../companion/local-companion-link.js";
5
6
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
@@ -14,6 +15,69 @@ const CLAUDE_COMPACT_DEDUP_MS = 2_000;
14
15
  const CLAUDE_BRACKETED_PASTE_START = "\u001b[200~";
15
16
  const CLAUDE_BRACKETED_PASTE_END = "\u001b[201~";
16
17
  const CLAUDE_REMOTE_ENTER_DELAY_MS = 40;
18
+ const CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT = 4_000;
19
+ function isClaudeWorkspaceTrustPrompt(text) {
20
+ const compact = text.replace(/\s+/g, " ").trim();
21
+ return (/Accessing workspace:/i.test(compact) &&
22
+ /Quick safety check:/i.test(compact) &&
23
+ /project you created or one you trust/i.test(compact) &&
24
+ /I trust this folder/i.test(compact));
25
+ }
26
+ function isRecord(value) {
27
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
28
+ }
29
+ export function normalizeClaudeProjectConfigKey(cwd) {
30
+ return path.resolve(cwd).replace(/\\/g, "/");
31
+ }
32
+ export function ensureClaudeWorkspaceTrustAccepted(cwd, homeDir = process.env.USERPROFILE || process.env.HOME || os.homedir()) {
33
+ const configPath = path.join(homeDir, ".claude.json");
34
+ let config = {};
35
+ try {
36
+ if (fs.existsSync(configPath)) {
37
+ const raw = fs.readFileSync(configPath, "utf8");
38
+ config = raw.trim() ? JSON.parse(raw) : {};
39
+ }
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ if (!isRecord(config)) {
45
+ return false;
46
+ }
47
+ const projectKey = normalizeClaudeProjectConfigKey(cwd);
48
+ const projects = isRecord(config.projects) ? config.projects : {};
49
+ const currentProject = isRecord(projects[projectKey])
50
+ ? projects[projectKey]
51
+ : {};
52
+ if (currentProject.hasTrustDialogAccepted === true) {
53
+ return false;
54
+ }
55
+ const nextConfig = {
56
+ ...config,
57
+ projects: {
58
+ ...projects,
59
+ [projectKey]: {
60
+ ...currentProject,
61
+ hasTrustDialogAccepted: true,
62
+ },
63
+ },
64
+ };
65
+ const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
66
+ try {
67
+ fs.writeFileSync(tempPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
68
+ fs.renameSync(tempPath, configPath);
69
+ return true;
70
+ }
71
+ catch {
72
+ try {
73
+ fs.rmSync(tempPath, { force: true });
74
+ }
75
+ catch {
76
+ // Best effort cleanup.
77
+ }
78
+ return false;
79
+ }
80
+ }
17
81
  export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
18
82
  hookServer = null;
19
83
  hookPort = null;
@@ -32,11 +96,20 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
32
96
  workingNoticeSent = false;
33
97
  workingNoticeDelayMs = CLAUDE_WECHAT_WORKING_NOTICE_DELAY_MS;
34
98
  lastCompactCompletionAtMs = 0;
99
+ startupOutputBuffer = "";
100
+ hasAutoConfirmedWorkspaceTrustPrompt = false;
35
101
  constructor(options) {
36
102
  super(options);
37
- this.runtimeSessionId = options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
38
- this.resumeConversationId = options.initialResumeConversationId ?? null;
39
- this.transcriptPath = options.initialTranscriptPath ?? null;
103
+ const shouldRestoreInitialSession = options.sessionStartMode !== "new";
104
+ this.runtimeSessionId = shouldRestoreInitialSession
105
+ ? options.initialSharedSessionId ?? options.initialSharedThreadId ?? null
106
+ : null;
107
+ this.resumeConversationId = shouldRestoreInitialSession
108
+ ? options.initialResumeConversationId ?? null
109
+ : null;
110
+ this.transcriptPath = shouldRestoreInitialSession
111
+ ? options.initialTranscriptPath ?? null
112
+ : null;
40
113
  if (this.runtimeSessionId) {
41
114
  this.state.sharedSessionId = this.runtimeSessionId;
42
115
  this.state.activeRuntimeSessionId = this.runtimeSessionId;
@@ -52,6 +125,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
52
125
  if (this.pty) {
53
126
  return;
54
127
  }
128
+ this.startupOutputBuffer = "";
129
+ this.hasAutoConfirmedWorkspaceTrustPrompt = false;
130
+ ensureClaudeWorkspaceTrustAccepted(this.options.cwd);
55
131
  // Validate transcript file exists before launching Claude CLI.
56
132
  // After a compact, the old transcript is deleted and the persisted
57
133
  // resumeConversationId becomes invalid, causing --resume to crash.
@@ -209,6 +285,9 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
209
285
  return;
210
286
  }
211
287
  this.state.lastOutputAt = nowIso();
288
+ if (this.maybeAutoConfirmWorkspaceTrustPrompt(text)) {
289
+ return;
290
+ }
212
291
  if (this.shouldTreatClaudeOutputAsCompactCompletion(text)) {
213
292
  this.completeClaudeCompact();
214
293
  return;
@@ -241,6 +320,18 @@ export class ClaudeCompanionAdapter extends AbstractPtyAdapter {
241
320
  return;
242
321
  }
243
322
  }
323
+ maybeAutoConfirmWorkspaceTrustPrompt(text) {
324
+ if (this.hasAcceptedInput || this.hasAutoConfirmedWorkspaceTrustPrompt) {
325
+ return false;
326
+ }
327
+ this.startupOutputBuffer = `${this.startupOutputBuffer}${text}`.slice(-CLAUDE_STARTUP_OUTPUT_BUFFER_LIMIT);
328
+ if (!isClaudeWorkspaceTrustPrompt(this.startupOutputBuffer)) {
329
+ return false;
330
+ }
331
+ this.hasAutoConfirmedWorkspaceTrustPrompt = true;
332
+ this.writeToPty("\r");
333
+ return true;
334
+ }
244
335
  handleExit(exitCode) {
245
336
  this.detachLocalTerminal();
246
337
  this.clearWechatWorkingNotice(true);
@@ -7,7 +7,7 @@ import { AbstractPtyAdapter } from "./bridge-adapters.core.js";
7
7
  import * as shared from "./bridge-adapters.shared.js";
8
8
  import { ensureWorkspaceChannelDir } from "../wechat/channel-config.js";
9
9
  import { CODEX_REMOTE_AUTH_TOKEN_ENV, LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
10
- const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_LOG_LIMIT, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCliEnvironment, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
10
+ const { CODEX_APP_SERVER_HOST, CODEX_APP_SERVER_READY_TIMEOUT_MS, CODEX_FINAL_REPLY_SETTLE_DELAY_MS, CODEX_RECENT_SESSION_KEY_LIMIT, CODEX_RPC_CONNECT_RETRY_MS, CODEX_RPC_RECONNECT_TIMEOUT_MS, CODEX_SESSION_FALLBACK_SCAN_INTERVAL_MS, CODEX_SESSION_LOCAL_MIRROR_FALLBACK_WINDOW_MS, CODEX_SESSION_POLL_INTERVAL_MS, CODEX_STARTUP_WARMUP_MS, CODEX_THREAD_SIGNAL_TTL_MS, INTERRUPT_SETTLE_DELAY_MS, appendBoundedLog, buildCodexApprovalRequest, buildCodexCliArgs, buildCodexDynamicToolCallFailureResponse, buildCodexMcpServerElicitationDeclineResponse, buildCodexPermissionsRequestApprovalResponse, buildCodexUserInputRequest, coerceWebSocketMessageData, delay, describeUnknownError, extractCodexFinalTextFromItem, extractCodexThreadFollowIdFromStatusChanged, extractCodexThreadStartedThreadId, extractCodexUserMessageText, findCodexSessionFile, findRecentCodexSessionFileForCwd, getCodexRpcRequestId, getCodexApprovalAutoResponse, getCodexWechatOutboundAttachmentDenyMessage, getNotificationThreadId, getNotificationTurnId, isRecord, isRecentIsoTimestamp, listCodexResumeSessions, normalizeComparablePath, normalizeCodexRpcError, reserveLocalPort, resolveSpawnTarget, shouldAutoCompleteCodexWechatTurnAfterFinalReply, shouldIgnoreCodexSessionReplayEntry, shouldRecoverCodexStaleBusyState, waitForTcpPort, } = shared;
11
11
  const CODEX_LOCAL_THREAD_ANNOUNCE_SETTLE_MS = 150;
12
12
  export class CodexPtyAdapter extends AbstractPtyAdapter {
13
13
  runtimeKind = "codex_runtime_host";
@@ -64,7 +64,9 @@ export class CodexPtyAdapter extends AbstractPtyAdapter {
64
64
  localClientInstanceId = `${process.pid}-${Date.now().toString(36)}`;
65
65
  constructor(options) {
66
66
  super(options);
67
- this.resumeThreadId = options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
67
+ this.resumeThreadId = options.sessionStartMode === "new"
68
+ ? null
69
+ : options.initialSharedSessionId ?? options.initialSharedThreadId ?? null;
68
70
  if (this.resumeThreadId && options.renderMode !== "panel") {
69
71
  this.state.sharedSessionId = this.resumeThreadId;
70
72
  this.state.sharedThreadId = this.resumeThreadId;
@@ -20,21 +20,29 @@ export class LocalCompanionProxyAdapter {
20
20
  reconnectShutdownTimer = null;
21
21
  constructor(options) {
22
22
  this.options = options;
23
+ const shouldRestoreInitialSession = options.sessionStartMode !== "new";
24
+ const initialSharedSessionId = shouldRestoreInitialSession
25
+ ? options.initialSharedSessionId ?? options.initialSharedThreadId
26
+ : undefined;
23
27
  this.state = {
24
28
  kind: options.kind,
25
29
  status: "stopped",
26
30
  cwd: options.cwd,
27
31
  command: options.command,
28
32
  profile: options.profile,
29
- sharedSessionId: options.initialSharedSessionId ?? options.initialSharedThreadId,
33
+ sharedSessionId: initialSharedSessionId,
30
34
  sharedThreadId: options.kind === "codex" || options.kind === "opencode"
31
- ? options.initialSharedSessionId ?? options.initialSharedThreadId
35
+ ? initialSharedSessionId
32
36
  : undefined,
33
37
  activeRuntimeSessionId: options.kind === "claude" || options.kind === "opencode"
34
- ? options.initialSharedSessionId ?? options.initialSharedThreadId
38
+ ? initialSharedSessionId
39
+ : undefined,
40
+ resumeConversationId: shouldRestoreInitialSession && options.kind === "claude"
41
+ ? options.initialResumeConversationId
42
+ : undefined,
43
+ transcriptPath: shouldRestoreInitialSession && options.kind === "claude"
44
+ ? options.initialTranscriptPath
35
45
  : undefined,
36
- resumeConversationId: options.kind === "claude" ? options.initialResumeConversationId : undefined,
37
- transcriptPath: options.kind === "claude" ? options.initialTranscriptPath : undefined,
38
46
  };
39
47
  }
40
48
  setEventSink(sink) {
@@ -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 { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, buildOneTimeCode, isWechatOutboundAttachmentMutationTool, isWechatOutboundAttachmentWriteCommand, 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();
@@ -1854,7 +1882,9 @@ export class OpenCodeServerAdapter {
1854
1882
  this.recordSessionSwitch(sessionId, options.source, options.reason, options.notify);
1855
1883
  }
1856
1884
  if (options.syncVisible !== false && (changed || options.forceVisibleSync)) {
1857
- void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session);
1885
+ void this.syncVisibleSessionSelection(typeof session === "string" ? { id: session } : session, {
1886
+ retry: options.retryVisibleSync,
1887
+ });
1858
1888
  }
1859
1889
  return changed;
1860
1890
  }
@@ -1865,7 +1895,7 @@ export class OpenCodeServerAdapter {
1865
1895
  await this.syncVisibleSessionSelection({
1866
1896
  id: this.activeSessionId,
1867
1897
  workspaceID: this.activeWorkspaceId ?? undefined,
1868
- }, { force: options.force });
1898
+ }, { force: options.force, retry: options.retry });
1869
1899
  }
1870
1900
  async syncVisibleSessionSelection(session, options = {}) {
1871
1901
  if (!this.client?.tui ||
@@ -1876,13 +1906,9 @@ export class OpenCodeServerAdapter {
1876
1906
  this.suppressedTuiSessionSelectId = session.id;
1877
1907
  this.logDebug(`[opencode-adapter:tui] selectSession session=${session.id} workspace=${session.workspaceID ?? this.activeWorkspaceId ?? "(none)"}`);
1878
1908
  try {
1879
- const result = await this.client.tui.selectSession({
1880
- directory: this.options.cwd,
1881
- workspace: session.workspaceID ?? this.activeWorkspaceId ?? undefined,
1882
- sessionID: session.id,
1883
- });
1884
- if (result.error !== undefined) {
1885
- throw result.error;
1909
+ await this.sendVisibleSessionSelection(session);
1910
+ if (options.retry === true) {
1911
+ this.scheduleVisibleSessionSelectionRetries(session);
1886
1912
  }
1887
1913
  }
1888
1914
  catch (err) {
@@ -1892,6 +1918,33 @@ export class OpenCodeServerAdapter {
1892
1918
  this.logDebug(`[opencode-adapter:tui] selectSession failed for ${session.id}: ${describeUnknownError(err)}`);
1893
1919
  }
1894
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
+ }
1895
1948
  isLocalSessionNavigationCommand(command) {
1896
1949
  if (!command) {
1897
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 { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, containsWechatOutboundAttachmentPathDeep, detectCliApproval, isWechatOutboundAttachmentWriteCommand, 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;
@@ -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
@@ -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`);
@@ -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
  });