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.
@@ -16,10 +16,12 @@ const WECHAT_ATTACHMENT_PROMPT_PREFIX = [
16
16
  "[WeChat bridge note]",
17
17
  "Your final reply will be forwarded back to a WeChat chat.",
18
18
  "If the user asks you to send a local file or media to WeChat and you know the local path, do not say that you lack a WeChat sending tool.",
19
- "For a real send request, prefer locating and sending the file directly instead of opening or reading it unless the user explicitly asked for that.",
19
+ "For a real send request, locate the original local file path and reference that path directly in the attachment block.",
20
+ "Do not create, copy, move, or write files into ~/.claude/channels/wechat, ~/.cli-bridge, or any outbound-attachments directory.",
21
+ "Do not call Write, cp, copy, xcopy, robocopy, mv, move, or shell redirection to stage outbound files for WeChat.",
20
22
  "Put any brief visible reply text first, then end the message with exactly one trailing block like:",
21
23
  "```wechat-attachments",
22
- "file C:\\Users\\name\\Desktop\\document.pdf",
24
+ "file C:\\Users\\name\\Desktop\\document.docx",
23
25
  "```",
24
26
  "Valid kinds: image, file, video, voice.",
25
27
  "Use `file` for PDFs and ordinary documents. Only include files you truly intend to upload.",
@@ -27,6 +29,10 @@ const WECHAT_ATTACHMENT_PROMPT_PREFIX = [
27
29
  "[User request]",
28
30
  ].join("\n");
29
31
  const WECHAT_ATTACHMENT_BLOCK_RE = /\n```wechat-attachments[ \t]*\n([\s\S]*?)\n```[ \t]*$/;
32
+ export const WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = "The WeChat bridge does not use outbound attachment directories. Do not create or copy files under .claude/channels/wechat/outbound-attachments or .cli-bridge/outbound-attachments. To send a file, put the original absolute local file path in the final ```wechat-attachments``` block.";
33
+ const WECHAT_OUTBOUND_ATTACHMENT_PATH_RE = /(?:^|\/)(?:(?:\.claude\/channels\/wechat\/|\.cli-bridge\/)?outbound-attachments)(?:\/|$)/i;
34
+ const WECHAT_OUTBOUND_ATTACHMENT_WRITE_COMMAND_RE = /\b(cp|copy|copy-item|xcopy|robocopy|mv|move|move-item|mkdir|md|new-item|ni|set-content|add-content|out-file|write-output|touch)\b|>\s*["']?[^&|]*outbound-attachments/i;
35
+ const WECHAT_OUTBOUND_ATTACHMENT_MUTATION_TOOL_RE = /^(?:write|edit|multiedit|notebookedit|patch|create|mkdir|move|copy|file[_-]?change|external_directory)$/i;
30
36
  const WECHAT_ATTACHMENT_KINDS = ["image", "file", "video", "voice"];
31
37
  const INLINE_IMAGE_EXTENSIONS = new Set([
32
38
  ".png",
@@ -211,6 +217,43 @@ export function shouldInjectWechatAttachmentPrompt(text) {
211
217
  mentionsLocalPath ||
212
218
  looksLikeShortSendCommand);
213
219
  }
220
+ export function containsWechatOutboundAttachmentPath(value) {
221
+ if (typeof value !== "string") {
222
+ return false;
223
+ }
224
+ const normalized = value.trim().replace(/\\/g, "/");
225
+ return WECHAT_OUTBOUND_ATTACHMENT_PATH_RE.test(normalized);
226
+ }
227
+ export function containsWechatOutboundAttachmentPathDeep(value) {
228
+ const seen = new WeakSet();
229
+ const visit = (candidate) => {
230
+ if (containsWechatOutboundAttachmentPath(candidate)) {
231
+ return true;
232
+ }
233
+ if (!candidate || typeof candidate !== "object") {
234
+ return false;
235
+ }
236
+ if (seen.has(candidate)) {
237
+ return false;
238
+ }
239
+ seen.add(candidate);
240
+ if (Array.isArray(candidate)) {
241
+ return candidate.some((item) => visit(item));
242
+ }
243
+ return Object.values(candidate).some((item) => visit(item));
244
+ };
245
+ return visit(value);
246
+ }
247
+ export function isWechatOutboundAttachmentWriteCommand(command) {
248
+ return (typeof command === "string" &&
249
+ containsWechatOutboundAttachmentPath(command) &&
250
+ WECHAT_OUTBOUND_ATTACHMENT_WRITE_COMMAND_RE.test(command));
251
+ }
252
+ export function isWechatOutboundAttachmentMutationTool(toolName, targetPath) {
253
+ return (typeof toolName === "string" &&
254
+ WECHAT_OUTBOUND_ATTACHMENT_MUTATION_TOOL_RE.test(toolName.trim()) &&
255
+ containsWechatOutboundAttachmentPath(targetPath));
256
+ }
214
257
  function formatPromptByteSize(bytes) {
215
258
  if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
216
259
  return null;
@@ -356,7 +399,11 @@ const HIGH_RISK_PATTERNS = [
356
399
  /\b(?:invoke-expression|iex)\b/i,
357
400
  /\bcurl\b.*\|\s*(?:iex|powershell)\b/i,
358
401
  /\binvoke-webrequest\b.*\|\s*(?:iex|powershell)\b/i,
402
+ /\brm\b\s+/i,
359
403
  /\brm\b\s+-[A-Za-z-]*r[A-Za-z-]*/i,
404
+ /\bfind\b[^\r\n]*\s-delete\b/i,
405
+ /\bfind\b[^\r\n]*\s-exec\s+(?:rm|rmdir|del|erase|remove-item)\b/i,
406
+ /\bxargs\b[^\r\n]*\b(?:rm|rmdir|del|erase|remove-item)\b/i,
360
407
  /\bsudo\b/i,
361
408
  /\bmkfs(?:\.\w+)?\b/i,
362
409
  /\bdd\b/i,
@@ -1,4 +1,19 @@
1
- import { normalizeOutput, truncatePreview } from "./bridge-utils.js";
1
+ import { WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE, containsWechatOutboundAttachmentPath, isHighRiskShellCommand, isWechatOutboundAttachmentWriteCommand, normalizeOutput, truncatePreview, } from "./bridge-utils.js";
2
+ export const CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE = WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
3
+ const FILE_MUTATION_TOOL_NAMES = new Set([
4
+ "Write",
5
+ "Edit",
6
+ "MultiEdit",
7
+ "NotebookEdit",
8
+ ]);
9
+ const LOW_RISK_READ_TOOL_NAMES = new Set([
10
+ "Glob",
11
+ "Grep",
12
+ "LS",
13
+ "NotebookRead",
14
+ "Read",
15
+ "TodoRead",
16
+ ]);
2
17
  function quoteWindowsCommandArg(value) {
3
18
  return `"${value.replace(/"/g, '""')}"`;
4
19
  }
@@ -166,14 +181,60 @@ export function buildClaudePermissionApprovalRequest(payload) {
166
181
  detailPreview,
167
182
  };
168
183
  }
169
- export function buildClaudePermissionDecisionHookOutput(action) {
184
+ function getClaudeToolInputString(payload, key) {
185
+ const value = payload.tool_input?.[key];
186
+ return typeof value === "string" ? value : "";
187
+ }
188
+ export function getClaudeWechatOutboundAttachmentDenyMessage(payload) {
189
+ const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
190
+ if (!toolName) {
191
+ return null;
192
+ }
193
+ if (FILE_MUTATION_TOOL_NAMES.has(toolName)) {
194
+ const filePath = getClaudeToolInputString(payload, "file_path");
195
+ return containsWechatOutboundAttachmentPath(filePath)
196
+ ? CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE
197
+ : null;
198
+ }
199
+ if (toolName === "Bash") {
200
+ const command = getClaudeToolInputString(payload, "command");
201
+ if (isWechatOutboundAttachmentWriteCommand(command)) {
202
+ return CLAUDE_WECHAT_OUTBOUND_ATTACHMENT_DENY_MESSAGE;
203
+ }
204
+ }
205
+ return null;
206
+ }
207
+ export function getClaudePermissionAutoResponse(payload) {
208
+ const toolName = typeof payload.tool_name === "string" ? payload.tool_name.trim() : "";
209
+ if (!toolName) {
210
+ return null;
211
+ }
212
+ if (LOW_RISK_READ_TOOL_NAMES.has(toolName)) {
213
+ return {
214
+ action: "confirm",
215
+ reason: `low-risk ${toolName} permission`,
216
+ };
217
+ }
218
+ if (toolName !== "Bash") {
219
+ return null;
220
+ }
221
+ const command = getClaudeToolInputString(payload, "command").trim();
222
+ if (!command || isHighRiskShellCommand(command)) {
223
+ return null;
224
+ }
225
+ return {
226
+ action: "confirm",
227
+ reason: `low-risk command ${truncatePreview(command, 120)}`,
228
+ };
229
+ }
230
+ export function buildClaudePermissionDecisionHookOutput(action, denyMessage = "Permission denied from WeChat bridge.") {
170
231
  const decision = action === "confirm"
171
232
  ? {
172
233
  behavior: "allow",
173
234
  }
174
235
  : {
175
236
  behavior: "deny",
176
- message: "Permission denied from WeChat bridge.",
237
+ message: denyMessage,
177
238
  interrupt: false,
178
239
  };
179
240
  return JSON.stringify({
@@ -8,7 +8,7 @@ import { ensureWechatCredentials } from "../wechat/setup.js";
8
8
  import { BridgeStateStore } from "./bridge-state.js";
9
9
  import { reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses } from "./bridge-process-reaper.js";
10
10
  import { createRuntimeHost } from "../runtime/create-runtime-host.js";
11
- import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
11
+ import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
12
12
  import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
13
13
  import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
14
14
  import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
@@ -111,6 +111,15 @@ function toPendingApproval(request) {
111
111
  createdAt: nowIso(),
112
112
  };
113
113
  }
114
+ function toPendingUserInput(request) {
115
+ if (typeof request.createdAt === "string") {
116
+ return request;
117
+ }
118
+ return {
119
+ ...request,
120
+ createdAt: nowIso(),
121
+ };
122
+ }
114
123
  export function shouldDeferCodexInboundMessage(params) {
115
124
  return (params.adapter === "codex" &&
116
125
  !params.hasPendingConfirmation &&
@@ -122,11 +131,13 @@ export function canDrainDeferredCodexInboundQueue(params) {
122
131
  return (params.adapter === "codex" &&
123
132
  params.deferredCount > 0 &&
124
133
  !params.hasPendingConfirmation &&
134
+ !params.hasPendingUserInput &&
125
135
  !params.hasPendingApproval &&
126
136
  !params.hasActiveTask &&
127
137
  !params.activeTurnId &&
128
138
  params.status !== "busy" &&
129
- params.status !== "awaiting_approval");
139
+ params.status !== "awaiting_approval" &&
140
+ params.status !== "awaiting_input");
130
141
  }
131
142
  export function formatDeferredCodexInboundQueueMessage(queuePosition) {
132
143
  return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
@@ -140,6 +151,7 @@ export function parseCliArgs(argv) {
140
151
  let cwd = process.cwd();
141
152
  let profile;
142
153
  let lifecycle = "persistent";
154
+ let sessionStartMode = "restore";
143
155
  for (let i = 0; i < argv.length; i += 1) {
144
156
  const arg = argv[i];
145
157
  const next = argv[i + 1];
@@ -179,6 +191,13 @@ export function parseCliArgs(argv) {
179
191
  lifecycle = next;
180
192
  i += 1;
181
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;
182
201
  case "--shutdown-on-parent-exit":
183
202
  lifecycle = "companion_bound";
184
203
  break;
@@ -200,11 +219,12 @@ export function parseCliArgs(argv) {
200
219
  cwd,
201
220
  profile,
202
221
  lifecycle,
222
+ sessionStartMode,
203
223
  };
204
224
  }
205
225
  function printUsageAndExit() {
206
226
  process.stdout.write([
207
- "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>]",
208
228
  "",
209
229
  "Examples:",
210
230
  " wechat-bridge-codex",
@@ -294,6 +314,7 @@ async function main() {
294
314
  cwd: options.cwd,
295
315
  profile: options.profile,
296
316
  lifecycle: options.lifecycle,
317
+ sessionStartMode: options.sessionStartMode,
297
318
  initialSharedSessionId: stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
298
319
  initialResumeConversationId: stateStore.getState().resumeConversationId,
299
320
  initialTranscriptPath: stateStore.getState().transcriptPath,
@@ -394,6 +415,7 @@ async function main() {
394
415
  status: adapterState.status,
395
416
  activeTurnId: adapterState.activeTurnId,
396
417
  hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
418
+ hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
397
419
  hasPendingApproval: Boolean(adapterState.pendingApproval),
398
420
  hasActiveTask: Boolean(activeTask),
399
421
  })) {
@@ -717,6 +739,9 @@ function wireAdapterEvents(params) {
717
739
  if (bridgeState.pendingConfirmation && !adapterState.pendingApproval) {
718
740
  stateStore.clearPendingConfirmation();
719
741
  }
742
+ if (bridgeState.pendingUserInput && !adapterState.pendingUserInput) {
743
+ stateStore.clearPendingUserInput();
744
+ }
720
745
  const authorizedUserId = stateStore.getState().authorizedUserId;
721
746
  switch (event.type) {
722
747
  case "stdout":
@@ -775,6 +800,14 @@ function wireAdapterEvents(params) {
775
800
  await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
776
801
  }));
777
802
  break;
803
+ case "user_input_required":
804
+ trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
805
+ const pending = toPendingUserInput(event.request);
806
+ stateStore.setPendingUserInput(pending);
807
+ stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
808
+ await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
809
+ }));
810
+ break;
778
811
  case "mirrored_user_input":
779
812
  stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
780
813
  if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
@@ -813,6 +846,7 @@ function wireAdapterEvents(params) {
813
846
  case "task_complete":
814
847
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
815
848
  stateStore.clearPendingConfirmation();
849
+ stateStore.clearPendingUserInput();
816
850
  if (options.adapter === "shell") {
817
851
  const summary = buildCompletionSummary({
818
852
  adapter: options.adapter,
@@ -829,6 +863,7 @@ function wireAdapterEvents(params) {
829
863
  case "task_failed":
830
864
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
831
865
  stateStore.clearPendingConfirmation();
866
+ stateStore.clearPendingUserInput();
832
867
  clearActiveTask();
833
868
  await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
834
869
  await maybeDrainDeferredInboundMessages();
@@ -838,6 +873,7 @@ function wireAdapterEvents(params) {
838
873
  logError(event.message);
839
874
  stateStore.appendLog(`fatal_error: ${event.message}`);
840
875
  stateStore.clearPendingConfirmation();
876
+ stateStore.clearPendingUserInput();
841
877
  clearActiveTask();
842
878
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
843
879
  await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
@@ -914,6 +950,7 @@ async function handleInboundMessage(params) {
914
950
  await outputBatcher.flushNow();
915
951
  outputBatcher.clear();
916
952
  stateStore.clearPendingConfirmation();
953
+ stateStore.clearPendingUserInput();
917
954
  stateStore.clearSharedSessionId();
918
955
  await adapter.createSession();
919
956
  stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
@@ -930,6 +967,7 @@ async function handleInboundMessage(params) {
930
967
  await outputBatcher.flushNow();
931
968
  outputBatcher.clear();
932
969
  stateStore.clearPendingConfirmation();
970
+ stateStore.clearPendingUserInput();
933
971
  stateStore.clearSharedSessionId();
934
972
  await adapter.reset();
935
973
  stateStore.appendLog("Worker reset by owner.");
@@ -974,11 +1012,39 @@ async function handleInboundMessage(params) {
974
1012
  await queueWechatMessage(message.senderId, "Approval denied.");
975
1013
  return null;
976
1014
  }
1015
+ case "answer": {
1016
+ const pending = state.pendingUserInput;
1017
+ if (!pending) {
1018
+ await queueWechatMessage(message.senderId, "No pending user input request.");
1019
+ return null;
1020
+ }
1021
+ const parsed = parsePendingUserInputAnswerCommand(systemCommand.raw, pending);
1022
+ if ("error" in parsed) {
1023
+ await queueWechatMessage(message.senderId, parsed.error);
1024
+ return null;
1025
+ }
1026
+ const submitted = await adapter.submitUserInput(parsed.answers);
1027
+ if (!submitted) {
1028
+ await queueWechatMessage(message.senderId, "The worker could not apply this answer.");
1029
+ return null;
1030
+ }
1031
+ stateStore.clearPendingUserInput();
1032
+ stateStore.appendLog(`User input answered: ${parsed.preview}`);
1033
+ await queueWechatMessage(message.senderId, "Answer submitted. Continuing...");
1034
+ return {
1035
+ startedAt: Date.now(),
1036
+ inputPreview: parsed.preview,
1037
+ };
1038
+ }
977
1039
  }
978
1040
  if (state.pendingConfirmation) {
979
1041
  await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
980
1042
  return null;
981
1043
  }
1044
+ if (state.pendingUserInput) {
1045
+ await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
1046
+ return null;
1047
+ }
982
1048
  const adapterState = adapter.getState();
983
1049
  if (shouldDeferCodexInboundMessage({
984
1050
  adapter: options.adapter,
@@ -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
  });