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.
- package/README.md +126 -366
- package/dist/bridge/bridge-adapters.claude.js +127 -6
- package/dist/bridge/bridge-adapters.codex.js +219 -7
- package/dist/bridge/bridge-adapters.core.js +13 -5
- package/dist/bridge/bridge-adapters.opencode.js +143 -24
- package/dist/bridge/bridge-adapters.shared.js +217 -6
- package/dist/bridge/bridge-process-reaper.js +40 -1
- package/dist/bridge/bridge-state.js +6 -3
- package/dist/bridge/bridge-utils.js +49 -2
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +69 -3
- package/dist/commands/check-update.js +1 -1
- package/dist/companion/local-companion-start.js +27 -2
- package/dist/companion/local-companion.js +21 -4
- package/dist/daemon/wechat-daemon.js +277 -21
- package/package.json +5 -6
- package/dist/companion/codex-panel-link.js +0 -72
- package/dist/companion/codex-panel.js +0 -179
- package/dist/media/media-types.js +0 -1
- package/dist/wechat/standalone-bot.js +0 -355
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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
|
|
112
|
+
"Starts a bridge for the current directory, waits for the local endpoint, then opens the visible companion or panel.",
|
|
113
|
+
"Claude and OpenCode launchers start a fresh CLI session by default.",
|
|
103
114
|
"All adapters are companion-bound: closing the companion/panel also stops the bridge.",
|
|
104
115
|
"Unknown arguments are forwarded to the visible CLI client.",
|
|
105
116
|
"",
|
|
@@ -144,7 +155,14 @@ export function parseCliArgs(argv) {
|
|
|
144
155
|
}
|
|
145
156
|
cliArgs.push(arg);
|
|
146
157
|
}
|
|
147
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
adapter,
|
|
160
|
+
cwd,
|
|
161
|
+
profile,
|
|
162
|
+
timeoutMs,
|
|
163
|
+
sessionStartMode: defaultSessionStartMode(adapter),
|
|
164
|
+
cliArgs,
|
|
165
|
+
};
|
|
148
166
|
}
|
|
149
167
|
function isPidAlive(pid) {
|
|
150
168
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
@@ -247,6 +265,9 @@ export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
|
247
265
|
args.push("--experimental-strip-types");
|
|
248
266
|
}
|
|
249
267
|
args.push(entryPath, "--adapter", options.adapter, "--cwd", options.cwd, "--lifecycle", lifecycle);
|
|
268
|
+
if (options.sessionStartMode !== "restore") {
|
|
269
|
+
args.push("--session-start-mode", options.sessionStartMode);
|
|
270
|
+
}
|
|
250
271
|
if (options.profile) {
|
|
251
272
|
args.push("--profile", options.profile);
|
|
252
273
|
}
|
|
@@ -304,6 +325,7 @@ async function ensureBridgeReady(options) {
|
|
|
304
325
|
endpoint: endpointResult.endpoint,
|
|
305
326
|
endpointIsReachable: Boolean(endpointResult.endpoint),
|
|
306
327
|
companionIsAlive: isCompanionAlive(endpointResult.endpoint),
|
|
328
|
+
sessionStartMode: options.sessionStartMode,
|
|
307
329
|
});
|
|
308
330
|
log(options.adapter, decision.message);
|
|
309
331
|
if (decision.kind === "already_active") {
|
|
@@ -355,6 +377,8 @@ export async function tryDelegateToDaemon(options, deps = {}) {
|
|
|
355
377
|
profile: options.profile,
|
|
356
378
|
cliArgs: options.cliArgs,
|
|
357
379
|
openVisible: true,
|
|
380
|
+
sessionStartMode: options.sessionStartMode,
|
|
381
|
+
reuseExistingVisible: true,
|
|
358
382
|
});
|
|
359
383
|
if (!response.ok) {
|
|
360
384
|
throw new Error(response.error);
|
|
@@ -374,6 +398,7 @@ export async function runVisibleClient(options, runners = {}) {
|
|
|
374
398
|
return await (runners.localCompanion ?? runLocalCompanion)({
|
|
375
399
|
adapter: options.adapter,
|
|
376
400
|
cwd: options.cwd,
|
|
401
|
+
sessionStartMode: options.sessionStartMode,
|
|
377
402
|
cliArgs: options.cliArgs,
|
|
378
403
|
});
|
|
379
404
|
}
|
|
@@ -13,6 +13,7 @@ function log(adapter, message) {
|
|
|
13
13
|
function parseCliArgs(argv) {
|
|
14
14
|
let adapter = null;
|
|
15
15
|
let cwd = process.cwd();
|
|
16
|
+
let sessionStartMode = "restore";
|
|
16
17
|
const cliArgs = [];
|
|
17
18
|
for (let i = 0; i < argv.length; i += 1) {
|
|
18
19
|
const arg = argv[i];
|
|
@@ -46,12 +47,20 @@ function parseCliArgs(argv) {
|
|
|
46
47
|
i += 1;
|
|
47
48
|
continue;
|
|
48
49
|
}
|
|
50
|
+
if (arg === "--session-start-mode") {
|
|
51
|
+
if (!next || !["restore", "new"].includes(next)) {
|
|
52
|
+
throw new Error(`Invalid session start mode: ${next ?? "(missing)"}`);
|
|
53
|
+
}
|
|
54
|
+
sessionStartMode = next;
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
49
58
|
cliArgs.push(arg);
|
|
50
59
|
}
|
|
51
60
|
if (!adapter) {
|
|
52
61
|
throw new Error("Missing required --adapter <codex|claude|opencode>");
|
|
53
62
|
}
|
|
54
|
-
return { adapter, cwd, cliArgs };
|
|
63
|
+
return { adapter, cwd, sessionStartMode, cliArgs };
|
|
55
64
|
}
|
|
56
65
|
function delay(ms) {
|
|
57
66
|
if (ms <= 0) {
|
|
@@ -79,14 +88,22 @@ export async function runLocalCompanion(options) {
|
|
|
79
88
|
extraCliArgs: options.cliArgs,
|
|
80
89
|
});
|
|
81
90
|
}
|
|
91
|
+
const shouldRestoreInitialSession = options.sessionStartMode !== "new";
|
|
82
92
|
const adapter = createBridgeAdapter({
|
|
83
93
|
kind: initialEndpoint.kind,
|
|
84
94
|
command: initialEndpoint.command,
|
|
85
95
|
cwd: initialEndpoint.cwd,
|
|
86
96
|
profile: initialEndpoint.profile,
|
|
87
|
-
initialSharedSessionId:
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
initialSharedSessionId: shouldRestoreInitialSession
|
|
98
|
+
? initialEndpoint.sharedSessionId ?? initialEndpoint.sharedThreadId
|
|
99
|
+
: undefined,
|
|
100
|
+
initialResumeConversationId: shouldRestoreInitialSession
|
|
101
|
+
? initialEndpoint.resumeConversationId
|
|
102
|
+
: undefined,
|
|
103
|
+
initialTranscriptPath: shouldRestoreInitialSession
|
|
104
|
+
? initialEndpoint.transcriptPath
|
|
105
|
+
: undefined,
|
|
106
|
+
sessionStartMode: options.sessionStartMode,
|
|
90
107
|
renderMode: initialEndpoint.kind === "codex" ? "panel" : "companion",
|
|
91
108
|
extraCliArgs: options.cliArgs,
|
|
92
109
|
});
|