@unlinearity/cli-wechat-bridge 1.0.4 → 1.0.6
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 +136 -25
- package/bin/wechat-daemon.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +33 -3
- package/dist/bridge/bridge-adapters.codex.js +216 -6
- package/dist/bridge/bridge-adapters.core.js +46 -6
- package/dist/bridge/bridge-adapters.opencode.js +79 -13
- package/dist/bridge/bridge-adapters.shared.js +215 -1
- package/dist/bridge/bridge-controller.js +6 -2
- package/dist/bridge/bridge-utils.js +92 -9
- package/dist/bridge/claude-hooks.js +64 -3
- package/dist/bridge/wechat-bridge.js +102 -8
- package/dist/companion/codex-remote-client.js +8 -4
- package/dist/companion/local-companion-link.js +56 -26
- package/dist/companion/local-companion-start.js +48 -5
- package/dist/companion/local-companion.js +3 -1
- package/dist/daemon/daemon-link.js +175 -0
- package/dist/daemon/wechat-daemon.js +1202 -0
- package/dist/wechat/channel-config.js +126 -33
- package/dist/wechat/standalone-bot.js +5 -6
- package/dist/wechat/wechat-channel.js +21 -10
- package/dist/wechat/wechat-transport.js +291 -9
- package/package.json +62 -98
|
@@ -8,9 +8,10 @@ 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";
|
|
12
|
-
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.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
|
+
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
|
+
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
|
|
14
15
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
15
16
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
16
17
|
const PARENT_PROCESS_POLL_MS = 5_000;
|
|
@@ -75,16 +76,22 @@ export function formatUserFacingInboundError(params) {
|
|
|
75
76
|
export function formatWechatSendFailureLogEntry(params) {
|
|
76
77
|
return `wechat_send_failed: context=${params.context} recipient=${params.recipientId} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
77
78
|
}
|
|
79
|
+
export function formatWechatContextTokenStaleLogEntry(params) {
|
|
80
|
+
return `wechat_context_token_stale: context=${params.context} recipient=${params.recipientId} action=wechat_message_required error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
81
|
+
}
|
|
78
82
|
function formatWechatSendRetryLogEntry(params) {
|
|
79
83
|
return `wechat_send_retry: context=${params.context} recipient=${params.recipientId} attempt=${params.attempt} delay_ms=${params.delayMs} error=${truncatePreview(describeWechatTransportError(params.error), 400)}`;
|
|
80
84
|
}
|
|
81
|
-
function isRetryableWechatSendError(error) {
|
|
85
|
+
export function isRetryableWechatSendError(error) {
|
|
86
|
+
if (isWechatContextTokenStaleError(error)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
82
89
|
const classification = classifyWechatTransportError(error);
|
|
83
90
|
if (classification.retryable) {
|
|
84
91
|
return true;
|
|
85
92
|
}
|
|
86
93
|
const details = describeWechatTransportError(error);
|
|
87
|
-
return /^Error: sendmessage failed:/i.test(details) &&
|
|
94
|
+
return /^(?:Error|WechatApiResponseError): sendmessage failed:/i.test(details) &&
|
|
88
95
|
!/errcode=-14\b.*session timeout/i.test(details);
|
|
89
96
|
}
|
|
90
97
|
function computeWechatSendRetryDelayMs(attempt) {
|
|
@@ -104,6 +111,15 @@ function toPendingApproval(request) {
|
|
|
104
111
|
createdAt: nowIso(),
|
|
105
112
|
};
|
|
106
113
|
}
|
|
114
|
+
function toPendingUserInput(request) {
|
|
115
|
+
if (typeof request.createdAt === "string") {
|
|
116
|
+
return request;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
...request,
|
|
120
|
+
createdAt: nowIso(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
107
123
|
export function shouldDeferCodexInboundMessage(params) {
|
|
108
124
|
return (params.adapter === "codex" &&
|
|
109
125
|
!params.hasPendingConfirmation &&
|
|
@@ -115,11 +131,13 @@ export function canDrainDeferredCodexInboundQueue(params) {
|
|
|
115
131
|
return (params.adapter === "codex" &&
|
|
116
132
|
params.deferredCount > 0 &&
|
|
117
133
|
!params.hasPendingConfirmation &&
|
|
134
|
+
!params.hasPendingUserInput &&
|
|
118
135
|
!params.hasPendingApproval &&
|
|
119
136
|
!params.hasActiveTask &&
|
|
120
137
|
!params.activeTurnId &&
|
|
121
138
|
params.status !== "busy" &&
|
|
122
|
-
params.status !== "awaiting_approval"
|
|
139
|
+
params.status !== "awaiting_approval" &&
|
|
140
|
+
params.status !== "awaiting_input");
|
|
123
141
|
}
|
|
124
142
|
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
125
143
|
return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
|
|
@@ -214,6 +232,14 @@ function printUsageAndExit() {
|
|
|
214
232
|
}
|
|
215
233
|
async function main() {
|
|
216
234
|
const options = parseCliArgs(process.argv.slice(2));
|
|
235
|
+
const daemonEndpoint = readDaemonEndpoint();
|
|
236
|
+
if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
|
|
237
|
+
throw new Error(`wechat-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
|
|
238
|
+
}
|
|
239
|
+
if (daemonEndpoint) {
|
|
240
|
+
clearDaemonEndpoint(daemonEndpoint.pid);
|
|
241
|
+
log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
|
|
242
|
+
}
|
|
217
243
|
const credentials = await ensureWechatCredentials({
|
|
218
244
|
requireUserId: true,
|
|
219
245
|
validateExisting: true,
|
|
@@ -313,6 +339,17 @@ async function main() {
|
|
|
313
339
|
return true;
|
|
314
340
|
}
|
|
315
341
|
catch (err) {
|
|
342
|
+
if (isWechatContextTokenStaleError(err)) {
|
|
343
|
+
transport.clearCachedContextToken(senderId);
|
|
344
|
+
const hint = "WeChat conversation context is stale. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
|
|
345
|
+
logError(`Failed to send WeChat ${context}: ${hint}`);
|
|
346
|
+
stateStore.appendLog(formatWechatContextTokenStaleLogEntry({
|
|
347
|
+
context,
|
|
348
|
+
recipientId: senderId,
|
|
349
|
+
error: err,
|
|
350
|
+
}));
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
316
353
|
if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
|
|
317
354
|
const delayMs = computeWechatSendRetryDelayMs(attempt);
|
|
318
355
|
logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
|
|
@@ -368,6 +405,7 @@ async function main() {
|
|
|
368
405
|
status: adapterState.status,
|
|
369
406
|
activeTurnId: adapterState.activeTurnId,
|
|
370
407
|
hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
|
|
408
|
+
hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
|
|
371
409
|
hasPendingApproval: Boolean(adapterState.pendingApproval),
|
|
372
410
|
hasActiveTask: Boolean(activeTask),
|
|
373
411
|
})) {
|
|
@@ -691,6 +729,9 @@ function wireAdapterEvents(params) {
|
|
|
691
729
|
if (bridgeState.pendingConfirmation && !adapterState.pendingApproval) {
|
|
692
730
|
stateStore.clearPendingConfirmation();
|
|
693
731
|
}
|
|
732
|
+
if (bridgeState.pendingUserInput && !adapterState.pendingUserInput) {
|
|
733
|
+
stateStore.clearPendingUserInput();
|
|
734
|
+
}
|
|
694
735
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
695
736
|
switch (event.type) {
|
|
696
737
|
case "stdout":
|
|
@@ -749,6 +790,14 @@ function wireAdapterEvents(params) {
|
|
|
749
790
|
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
|
|
750
791
|
}));
|
|
751
792
|
break;
|
|
793
|
+
case "user_input_required":
|
|
794
|
+
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
795
|
+
const pending = toPendingUserInput(event.request);
|
|
796
|
+
stateStore.setPendingUserInput(pending);
|
|
797
|
+
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
798
|
+
await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
|
|
799
|
+
}));
|
|
800
|
+
break;
|
|
752
801
|
case "mirrored_user_input":
|
|
753
802
|
stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
|
|
754
803
|
if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
|
|
@@ -787,6 +836,7 @@ function wireAdapterEvents(params) {
|
|
|
787
836
|
case "task_complete":
|
|
788
837
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
789
838
|
stateStore.clearPendingConfirmation();
|
|
839
|
+
stateStore.clearPendingUserInput();
|
|
790
840
|
if (options.adapter === "shell") {
|
|
791
841
|
const summary = buildCompletionSummary({
|
|
792
842
|
adapter: options.adapter,
|
|
@@ -803,6 +853,7 @@ function wireAdapterEvents(params) {
|
|
|
803
853
|
case "task_failed":
|
|
804
854
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
805
855
|
stateStore.clearPendingConfirmation();
|
|
856
|
+
stateStore.clearPendingUserInput();
|
|
806
857
|
clearActiveTask();
|
|
807
858
|
await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
|
|
808
859
|
await maybeDrainDeferredInboundMessages();
|
|
@@ -812,6 +863,7 @@ function wireAdapterEvents(params) {
|
|
|
812
863
|
logError(event.message);
|
|
813
864
|
stateStore.appendLog(`fatal_error: ${event.message}`);
|
|
814
865
|
stateStore.clearPendingConfirmation();
|
|
866
|
+
stateStore.clearPendingUserInput();
|
|
815
867
|
clearActiveTask();
|
|
816
868
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
817
869
|
await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
|
|
@@ -837,6 +889,17 @@ function buildCompletionSummary(params) {
|
|
|
837
889
|
lines.push(`recent_output:\n${params.recentOutput}`);
|
|
838
890
|
return lines.join("\n");
|
|
839
891
|
}
|
|
892
|
+
function formatInboundMessagePreview(message) {
|
|
893
|
+
if (message.text.trim()) {
|
|
894
|
+
return message.text;
|
|
895
|
+
}
|
|
896
|
+
if (message.attachments.length > 0) {
|
|
897
|
+
return message.attachments
|
|
898
|
+
.map((attachment) => `${attachment.kind}: ${attachment.path}`)
|
|
899
|
+
.join("\n");
|
|
900
|
+
}
|
|
901
|
+
return "(empty)";
|
|
902
|
+
}
|
|
840
903
|
async function handleInboundMessage(params) {
|
|
841
904
|
const { message, options, stateStore, adapter, queueWechatMessage, outputBatcher, deferInboundMessage, } = params;
|
|
842
905
|
const state = stateStore.getState();
|
|
@@ -877,6 +940,7 @@ async function handleInboundMessage(params) {
|
|
|
877
940
|
await outputBatcher.flushNow();
|
|
878
941
|
outputBatcher.clear();
|
|
879
942
|
stateStore.clearPendingConfirmation();
|
|
943
|
+
stateStore.clearPendingUserInput();
|
|
880
944
|
stateStore.clearSharedSessionId();
|
|
881
945
|
await adapter.createSession();
|
|
882
946
|
stateStore.appendLog(`New ${options.adapter} session requested by owner.`);
|
|
@@ -893,6 +957,7 @@ async function handleInboundMessage(params) {
|
|
|
893
957
|
await outputBatcher.flushNow();
|
|
894
958
|
outputBatcher.clear();
|
|
895
959
|
stateStore.clearPendingConfirmation();
|
|
960
|
+
stateStore.clearPendingUserInput();
|
|
896
961
|
stateStore.clearSharedSessionId();
|
|
897
962
|
await adapter.reset();
|
|
898
963
|
stateStore.appendLog("Worker reset by owner.");
|
|
@@ -937,11 +1002,39 @@ async function handleInboundMessage(params) {
|
|
|
937
1002
|
await queueWechatMessage(message.senderId, "Approval denied.");
|
|
938
1003
|
return null;
|
|
939
1004
|
}
|
|
1005
|
+
case "answer": {
|
|
1006
|
+
const pending = state.pendingUserInput;
|
|
1007
|
+
if (!pending) {
|
|
1008
|
+
await queueWechatMessage(message.senderId, "No pending user input request.");
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
const parsed = parsePendingUserInputAnswerCommand(systemCommand.raw, pending);
|
|
1012
|
+
if ("error" in parsed) {
|
|
1013
|
+
await queueWechatMessage(message.senderId, parsed.error);
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const submitted = await adapter.submitUserInput(parsed.answers);
|
|
1017
|
+
if (!submitted) {
|
|
1018
|
+
await queueWechatMessage(message.senderId, "The worker could not apply this answer.");
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
stateStore.clearPendingUserInput();
|
|
1022
|
+
stateStore.appendLog(`User input answered: ${parsed.preview}`);
|
|
1023
|
+
await queueWechatMessage(message.senderId, "Answer submitted. Continuing...");
|
|
1024
|
+
return {
|
|
1025
|
+
startedAt: Date.now(),
|
|
1026
|
+
inputPreview: parsed.preview,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
940
1029
|
}
|
|
941
1030
|
if (state.pendingConfirmation) {
|
|
942
1031
|
await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
|
|
943
1032
|
return null;
|
|
944
1033
|
}
|
|
1034
|
+
if (state.pendingUserInput) {
|
|
1035
|
+
await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
945
1038
|
const adapterState = adapter.getState();
|
|
946
1039
|
if (shouldDeferCodexInboundMessage({
|
|
947
1040
|
adapter: options.adapter,
|
|
@@ -971,12 +1064,13 @@ async function handleInboundMessage(params) {
|
|
|
971
1064
|
}
|
|
972
1065
|
async function dispatchInboundWechatText(params) {
|
|
973
1066
|
const { message, options, stateStore, adapter } = params;
|
|
1067
|
+
const preview = formatInboundMessagePreview(message);
|
|
974
1068
|
const activeTask = {
|
|
975
1069
|
startedAt: Date.now(),
|
|
976
|
-
inputPreview: truncatePreview(
|
|
1070
|
+
inputPreview: truncatePreview(preview, 180),
|
|
977
1071
|
};
|
|
978
|
-
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(
|
|
979
|
-
await adapter.sendInput(buildWechatInboundPrompt(message.text));
|
|
1072
|
+
stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
|
|
1073
|
+
await adapter.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
|
|
980
1074
|
return activeTask;
|
|
981
1075
|
}
|
|
982
1076
|
const isDirectRun = Boolean(import.meta.main);
|
|
@@ -40,7 +40,7 @@ export function parseCliArgs(argv) {
|
|
|
40
40
|
return { cwd, cliArgs };
|
|
41
41
|
}
|
|
42
42
|
export function readCodexRuntimeEndpoint(cwd) {
|
|
43
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
43
|
+
const endpoint = readLocalCompanionEndpoint(cwd, { adapter: "codex" });
|
|
44
44
|
if (!endpoint || endpoint.kind !== "codex") {
|
|
45
45
|
throw new Error(`No active Codex bridge endpoint was found for ${cwd}. Start "wechat-bridge-codex" in that directory first.`);
|
|
46
46
|
}
|
|
@@ -83,14 +83,18 @@ export async function runCodexRemoteClientFromEndpoint(endpoint, options = {}) {
|
|
|
83
83
|
updateLocalCompanionOccupancy(endpoint.cwd, {
|
|
84
84
|
companionPid: child.pid,
|
|
85
85
|
companionConnectedAt: new Date().toISOString(),
|
|
86
|
-
}, endpoint.instanceId);
|
|
86
|
+
}, endpoint.instanceId, { adapter: "codex" });
|
|
87
87
|
}
|
|
88
88
|
child.once("error", (error) => {
|
|
89
|
-
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId
|
|
89
|
+
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
|
|
90
|
+
adapter: "codex",
|
|
91
|
+
});
|
|
90
92
|
reject(error);
|
|
91
93
|
});
|
|
92
94
|
child.once("exit", (code, signal) => {
|
|
93
|
-
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId
|
|
95
|
+
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
|
|
96
|
+
adapter: "codex",
|
|
97
|
+
});
|
|
94
98
|
if (signal) {
|
|
95
99
|
process.kill(process.pid, signal);
|
|
96
100
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import { ensureWorkspaceChannelDir, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
|
|
3
|
+
import { ensureWorkspaceChannelDir, getWorkspaceAdapterEndpointFile, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
|
|
4
4
|
import { LOCAL_CLIENT_PROTOCOL_VERSION, } from "../runtime/runtime-types.js";
|
|
5
5
|
function normalizeEndpoint(value) {
|
|
6
6
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -66,51 +66,81 @@ function normalizeEndpoint(value) {
|
|
|
66
66
|
export function buildLocalCompanionToken() {
|
|
67
67
|
return crypto.randomBytes(18).toString("hex");
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const payload = {
|
|
69
|
+
function serializeEndpoint(endpoint) {
|
|
70
|
+
return {
|
|
72
71
|
...endpoint,
|
|
73
72
|
protocolVersion: endpoint.protocolVersion ?? LOCAL_CLIENT_PROTOCOL_VERSION,
|
|
74
73
|
sharedThreadId: endpoint.kind === "codex" || endpoint.kind === "opencode"
|
|
75
74
|
? endpoint.sharedSessionId ?? endpoint.sharedThreadId
|
|
76
75
|
: undefined,
|
|
77
76
|
};
|
|
78
|
-
fs.writeFileSync(endpointFile, JSON.stringify(payload, null, 2), "utf8");
|
|
79
77
|
}
|
|
80
|
-
|
|
78
|
+
function readEndpointFile(filePath) {
|
|
79
|
+
if (!fs.existsSync(filePath)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return normalizeEndpoint(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
83
|
+
}
|
|
84
|
+
export function writeLocalCompanionEndpoint(endpoint, options = {}) {
|
|
85
|
+
const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
|
|
86
|
+
const payload = {
|
|
87
|
+
...serializeEndpoint(endpoint),
|
|
88
|
+
};
|
|
89
|
+
fs.writeFileSync(getWorkspaceAdapterEndpointFile(endpoint.cwd, endpoint.kind), JSON.stringify(payload, null, 2), "utf8");
|
|
90
|
+
if (options.writeLegacy !== false) {
|
|
91
|
+
fs.writeFileSync(endpointFile, JSON.stringify(payload, null, 2), "utf8");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function readLocalCompanionEndpoint(cwd, options = {}) {
|
|
81
95
|
try {
|
|
82
96
|
const { endpointFile } = getWorkspaceChannelPaths(cwd);
|
|
83
|
-
if (
|
|
84
|
-
|
|
97
|
+
if (options.adapter) {
|
|
98
|
+
const scoped = readEndpointFile(getWorkspaceAdapterEndpointFile(cwd, options.adapter));
|
|
99
|
+
if (scoped) {
|
|
100
|
+
return scoped;
|
|
101
|
+
}
|
|
102
|
+
const legacy = readEndpointFile(endpointFile);
|
|
103
|
+
return legacy?.kind === options.adapter ? legacy : null;
|
|
85
104
|
}
|
|
86
|
-
return
|
|
105
|
+
return readEndpointFile(endpointFile);
|
|
87
106
|
}
|
|
88
107
|
catch {
|
|
89
108
|
return null;
|
|
90
109
|
}
|
|
91
110
|
}
|
|
92
|
-
export function clearLocalCompanionEndpoint(cwd, instanceId) {
|
|
111
|
+
export function clearLocalCompanionEndpoint(cwd, instanceId, options = {}) {
|
|
93
112
|
try {
|
|
94
113
|
const { endpointFile } = getWorkspaceChannelPaths(cwd);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
const files = options.adapter
|
|
115
|
+
? [getWorkspaceAdapterEndpointFile(cwd, options.adapter), endpointFile]
|
|
116
|
+
: [
|
|
117
|
+
endpointFile,
|
|
118
|
+
getWorkspaceAdapterEndpointFile(cwd, "codex"),
|
|
119
|
+
getWorkspaceAdapterEndpointFile(cwd, "claude"),
|
|
120
|
+
getWorkspaceAdapterEndpointFile(cwd, "opencode"),
|
|
121
|
+
getWorkspaceAdapterEndpointFile(cwd, "shell"),
|
|
122
|
+
];
|
|
123
|
+
for (const filePath of files) {
|
|
124
|
+
if (!fs.existsSync(filePath)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!instanceId) {
|
|
128
|
+
fs.rmSync(filePath, { force: true });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const endpoint = readEndpointFile(filePath);
|
|
132
|
+
if (!endpoint || endpoint.instanceId === instanceId) {
|
|
133
|
+
fs.rmSync(filePath, { force: true });
|
|
134
|
+
}
|
|
105
135
|
}
|
|
106
136
|
}
|
|
107
137
|
catch {
|
|
108
138
|
// Best effort cleanup.
|
|
109
139
|
}
|
|
110
140
|
}
|
|
111
|
-
export function clearLocalCompanionOccupancy(cwd, instanceId) {
|
|
141
|
+
export function clearLocalCompanionOccupancy(cwd, instanceId, options = {}) {
|
|
112
142
|
try {
|
|
113
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
143
|
+
const endpoint = readLocalCompanionEndpoint(cwd, options);
|
|
114
144
|
if (!endpoint) {
|
|
115
145
|
return;
|
|
116
146
|
}
|
|
@@ -130,9 +160,9 @@ export function clearLocalCompanionOccupancy(cwd, instanceId) {
|
|
|
130
160
|
// Best effort cleanup.
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
|
-
export function updateLocalCompanionHealth(cwd, patch, instanceId) {
|
|
163
|
+
export function updateLocalCompanionHealth(cwd, patch, instanceId, options = {}) {
|
|
134
164
|
try {
|
|
135
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
165
|
+
const endpoint = readLocalCompanionEndpoint(cwd, options);
|
|
136
166
|
if (!endpoint) {
|
|
137
167
|
return;
|
|
138
168
|
}
|
|
@@ -150,9 +180,9 @@ export function updateLocalCompanionHealth(cwd, patch, instanceId) {
|
|
|
150
180
|
// Best effort cleanup.
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
|
-
export function updateLocalCompanionOccupancy(cwd, patch, instanceId) {
|
|
183
|
+
export function updateLocalCompanionOccupancy(cwd, patch, instanceId, options = {}) {
|
|
154
184
|
try {
|
|
155
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
185
|
+
const endpoint = readLocalCompanionEndpoint(cwd, options);
|
|
156
186
|
if (!endpoint) {
|
|
157
187
|
return;
|
|
158
188
|
}
|
|
@@ -9,6 +9,7 @@ import { readBridgeLockFile, shouldAutoReclaimBridgeLock, } from "../bridge/brid
|
|
|
9
9
|
import { clearLocalCompanionOccupancy, clearLocalCompanionEndpoint, readLocalCompanionEndpoint, } from "./local-companion-link.js";
|
|
10
10
|
import { runCodexRemoteClient } from "./codex-remote-client.js";
|
|
11
11
|
import { runLocalCompanion } from "./local-companion.js";
|
|
12
|
+
import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, sendDaemonRequest, } from "../daemon/daemon-link.js";
|
|
12
13
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
13
14
|
const MODULE_DIR = path.dirname(MODULE_FILE);
|
|
14
15
|
const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".js";
|
|
@@ -184,7 +185,12 @@ async function stopExistingBridge(lock, requestedAdapter) {
|
|
|
184
185
|
if (!(await waitForProcessExit(pid, 10_000))) {
|
|
185
186
|
throw new Error(`Timed out waiting for existing bridge pid=${pid} to exit.`);
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
+
if (lock.adapter === "codex" || lock.adapter === "claude" || lock.adapter === "opencode") {
|
|
189
|
+
clearLocalCompanionEndpoint(cwd, undefined, { adapter: lock.adapter });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
clearLocalCompanionEndpoint(cwd);
|
|
193
|
+
}
|
|
188
194
|
log(requestedAdapter, `Cleared stale local companion endpoint for previous workspace ${cwd}.`);
|
|
189
195
|
}
|
|
190
196
|
async function isEndpointReachable(endpoint) {
|
|
@@ -211,14 +217,14 @@ async function isEndpointReachable(endpoint) {
|
|
|
211
217
|
});
|
|
212
218
|
}
|
|
213
219
|
async function readUsableEndpoint(cwd, adapter) {
|
|
214
|
-
const endpoint = readLocalCompanionEndpoint(cwd);
|
|
220
|
+
const endpoint = readLocalCompanionEndpoint(cwd, { adapter });
|
|
215
221
|
if (!endpoint || endpoint.kind !== adapter) {
|
|
216
222
|
return { endpoint: null };
|
|
217
223
|
}
|
|
218
224
|
if (await isEndpointReachable(endpoint)) {
|
|
219
225
|
return { endpoint };
|
|
220
226
|
}
|
|
221
|
-
clearLocalCompanionEndpoint(cwd, endpoint.instanceId);
|
|
227
|
+
clearLocalCompanionEndpoint(cwd, endpoint.instanceId, { adapter });
|
|
222
228
|
log(adapter, `Removed stale local companion endpoint for ${cwd}.`);
|
|
223
229
|
return { endpoint: null };
|
|
224
230
|
}
|
|
@@ -229,7 +235,9 @@ function isCompanionAlive(endpoint) {
|
|
|
229
235
|
if (isPidAlive(endpoint.companionPid)) {
|
|
230
236
|
return true;
|
|
231
237
|
}
|
|
232
|
-
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId
|
|
238
|
+
clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId, {
|
|
239
|
+
adapter: endpoint.kind,
|
|
240
|
+
});
|
|
233
241
|
return false;
|
|
234
242
|
}
|
|
235
243
|
export function buildBackgroundBridgeArgs(entryPath, options) {
|
|
@@ -277,7 +285,9 @@ async function ensureBridgeReady(options) {
|
|
|
277
285
|
if (!lock || !lockProcessAlive) {
|
|
278
286
|
if (lock && !lockProcessAlive) {
|
|
279
287
|
log(options.adapter, `Found stale lock for ${options.cwd} (pid=${lock.pid} dead). Clearing.`);
|
|
280
|
-
clearLocalCompanionEndpoint(options.cwd
|
|
288
|
+
clearLocalCompanionEndpoint(options.cwd, undefined, {
|
|
289
|
+
adapter: options.adapter,
|
|
290
|
+
});
|
|
281
291
|
}
|
|
282
292
|
log(options.adapter, `Starting bridge in background for ${options.cwd}...`);
|
|
283
293
|
startBridgeInBackground(options);
|
|
@@ -322,6 +332,36 @@ async function ensureBridgeReady(options) {
|
|
|
322
332
|
await waitForEndpoint(options.cwd, options.adapter, options.timeoutMs);
|
|
323
333
|
return { shouldOpenVisibleClient: true };
|
|
324
334
|
}
|
|
335
|
+
export async function tryDelegateToDaemon(options, deps = {}) {
|
|
336
|
+
const readEndpoint = deps.readEndpoint ?? readDaemonEndpoint;
|
|
337
|
+
const isEndpointAlive = deps.isEndpointAlive ?? isDaemonEndpointAlive;
|
|
338
|
+
const sendRequest = deps.sendRequest ?? sendDaemonRequest;
|
|
339
|
+
const clearEndpoint = deps.clearEndpoint ?? clearDaemonEndpoint;
|
|
340
|
+
const endpoint = readEndpoint();
|
|
341
|
+
if (!endpoint) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (!(await isEndpointAlive(endpoint))) {
|
|
345
|
+
clearEndpoint(endpoint.pid);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (!isSameWorkspaceCwd(endpoint.cwd, options.cwd)) {
|
|
349
|
+
throw new Error(`wechat-daemon is running for ${endpoint.cwd}. This launcher requested ${options.cwd}; v1 daemon switching is limited to its startup cwd.`);
|
|
350
|
+
}
|
|
351
|
+
const response = await sendRequest(endpoint, {
|
|
352
|
+
command: "ensure_slot",
|
|
353
|
+
adapter: options.adapter,
|
|
354
|
+
cwd: options.cwd,
|
|
355
|
+
profile: options.profile,
|
|
356
|
+
cliArgs: options.cliArgs,
|
|
357
|
+
openVisible: true,
|
|
358
|
+
});
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw new Error(response.error);
|
|
361
|
+
}
|
|
362
|
+
log(options.adapter, `Delegated to running wechat-daemon for ${options.cwd}.`);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
325
365
|
export async function runVisibleClient(options, runners = {}) {
|
|
326
366
|
// Keep the foreground client in-process so Windows does not briefly flash an
|
|
327
367
|
// extra bootstrap console before the real companion UI appears.
|
|
@@ -347,6 +387,9 @@ export async function ensureCompanionStartWechatCredentials(adapter, ensureCrede
|
|
|
347
387
|
export async function runLocalCompanionStart(argv = process.argv.slice(2)) {
|
|
348
388
|
const options = parseCliArgs(argv);
|
|
349
389
|
await ensureCompanionStartWechatCredentials(options.adapter);
|
|
390
|
+
if (await tryDelegateToDaemon(options)) {
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
350
393
|
const ready = await ensureBridgeReady(options);
|
|
351
394
|
if (!ready.shouldOpenVisibleClient) {
|
|
352
395
|
return 0;
|
|
@@ -60,7 +60,9 @@ function delay(ms) {
|
|
|
60
60
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
61
|
}
|
|
62
62
|
function readMatchingEndpoint(options) {
|
|
63
|
-
const endpoint = readLocalCompanionEndpoint(options.cwd
|
|
63
|
+
const endpoint = readLocalCompanionEndpoint(options.cwd, {
|
|
64
|
+
adapter: options.adapter,
|
|
65
|
+
});
|
|
64
66
|
if (!endpoint || endpoint.kind !== options.adapter) {
|
|
65
67
|
throw new Error(`No active ${options.adapter} bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-${options.adapter}" in that directory first.`);
|
|
66
68
|
}
|