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
|
@@ -5,11 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
|
|
8
|
-
import { delay, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
|
|
8
|
+
import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
|
|
9
9
|
import { BridgeController } from "../bridge/bridge-controller.js";
|
|
10
10
|
import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
|
|
11
11
|
import { readBridgeLockFile, } from "../bridge/bridge-state.js";
|
|
12
|
-
import { killProcessTreeSync, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
|
|
12
|
+
import { getProcessRecordByPid, isWechatDaemonCommandLine, killProcessTreeSync, listWechatDaemonProcesses, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
|
|
13
13
|
import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, formatDuration, formatMirroredUserInputMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatSessionSwitchMessage, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "../bridge/bridge-utils.js";
|
|
14
14
|
import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-bridge.js";
|
|
15
15
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
@@ -17,7 +17,7 @@ import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
|
17
17
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
18
18
|
import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
|
|
19
19
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
20
|
-
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION,
|
|
20
|
+
import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
|
|
21
21
|
const MODULE_FILE = fileURLToPath(import.meta.url);
|
|
22
22
|
const MODULE_DIR = path.dirname(MODULE_FILE);
|
|
23
23
|
const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".js";
|
|
@@ -29,6 +29,9 @@ const WECHAT_SEND_RETRY_BASE_MS = 750;
|
|
|
29
29
|
const SINGLE_BRIDGE_STOP_TIMEOUT_MS = 10_000;
|
|
30
30
|
const SINGLE_BRIDGE_FORCE_STOP_TIMEOUT_MS = 3_000;
|
|
31
31
|
const SINGLE_BRIDGE_STOP_POLL_MS = 250;
|
|
32
|
+
const DAEMON_TAKEOVER_STOP_TIMEOUT_MS = 10_000;
|
|
33
|
+
const DAEMON_TAKEOVER_FORCE_STOP_TIMEOUT_MS = 3_000;
|
|
34
|
+
const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
|
|
32
35
|
const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
|
|
33
36
|
const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
|
|
34
37
|
const DAEMON_ADAPTERS = ["codex", "claude", "opencode"];
|
|
@@ -132,6 +135,27 @@ export function parseDaemonSwitchCommand(text) {
|
|
|
132
135
|
return null;
|
|
133
136
|
}
|
|
134
137
|
}
|
|
138
|
+
export function defaultDaemonSessionStartMode(adapter) {
|
|
139
|
+
return adapter === "claude" || adapter === "opencode" ? "new" : "restore";
|
|
140
|
+
}
|
|
141
|
+
export function resolveDaemonSessionStartMode(params) {
|
|
142
|
+
if (params.reuseExistingVisible && params.visibleConnected) {
|
|
143
|
+
return "restore";
|
|
144
|
+
}
|
|
145
|
+
if (params.explicitSessionStartMode) {
|
|
146
|
+
return params.explicitSessionStartMode;
|
|
147
|
+
}
|
|
148
|
+
if (params.adapter === "codex") {
|
|
149
|
+
return "restore";
|
|
150
|
+
}
|
|
151
|
+
if (params.slotCreated) {
|
|
152
|
+
return "new";
|
|
153
|
+
}
|
|
154
|
+
if (!params.visibleConnected && !params.sharedSessionId) {
|
|
155
|
+
return "new";
|
|
156
|
+
}
|
|
157
|
+
return "restore";
|
|
158
|
+
}
|
|
135
159
|
function toPendingApproval(request) {
|
|
136
160
|
const rawRequest = request.request;
|
|
137
161
|
if (typeof rawRequest.code === "string") {
|
|
@@ -168,6 +192,9 @@ export function buildVisibleClientLaunchArgs(params) {
|
|
|
168
192
|
if (params.adapter !== "codex") {
|
|
169
193
|
args.push("--adapter", params.adapter);
|
|
170
194
|
}
|
|
195
|
+
if (params.sessionStartMode && params.sessionStartMode !== "restore") {
|
|
196
|
+
args.push("--session-start-mode", params.sessionStartMode);
|
|
197
|
+
}
|
|
171
198
|
args.push("--cwd", params.cwd, ...(params.cliArgs ?? []));
|
|
172
199
|
return args;
|
|
173
200
|
}
|
|
@@ -291,6 +318,17 @@ function formatNoActiveAdapterMessage() {
|
|
|
291
318
|
].join("\n");
|
|
292
319
|
}
|
|
293
320
|
export function formatDaemonSwitchResultDetail(result) {
|
|
321
|
+
if (result.activated === false) {
|
|
322
|
+
const previous = result.previousActiveAdapter
|
|
323
|
+
? ` Active terminal remains ${result.previousActiveAdapter}.`
|
|
324
|
+
: " No terminal is active yet.";
|
|
325
|
+
if (result.openedVisible) {
|
|
326
|
+
return result.created
|
|
327
|
+
? `Started the bridge slot and tried to open the visible CLI, but it has not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`
|
|
328
|
+
: `Tried to open a visible CLI for the existing slot, but it has not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`;
|
|
329
|
+
}
|
|
330
|
+
return `The visible CLI is not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`;
|
|
331
|
+
}
|
|
294
332
|
if (result.openedVisible && result.visibleConnected) {
|
|
295
333
|
return result.created
|
|
296
334
|
? "Started a new visible CLI."
|
|
@@ -542,32 +580,56 @@ class WechatDaemon {
|
|
|
542
580
|
profile: request.profile,
|
|
543
581
|
cliArgs: request.cliArgs,
|
|
544
582
|
openVisible: request.openVisible ?? true,
|
|
583
|
+
sessionStartMode: request.sessionStartMode,
|
|
584
|
+
reuseExistingVisible: request.reuseExistingVisible ?? true,
|
|
545
585
|
});
|
|
546
586
|
case "switch_adapter":
|
|
547
587
|
return await this.ensureSlot(request.adapter, {
|
|
548
588
|
profile: request.profile,
|
|
549
589
|
cliArgs: request.cliArgs,
|
|
550
590
|
openVisible: request.openVisible ?? true,
|
|
591
|
+
sessionStartMode: request.sessionStartMode,
|
|
592
|
+
reuseExistingVisible: request.reuseExistingVisible ?? true,
|
|
551
593
|
});
|
|
552
594
|
}
|
|
553
595
|
}
|
|
554
596
|
async ensureSlot(adapter, options = {}) {
|
|
597
|
+
const previousActiveAdapter = this.activeAdapter ?? undefined;
|
|
555
598
|
let slot = this.slots.get(adapter);
|
|
556
599
|
let created = false;
|
|
557
600
|
if (!slot) {
|
|
601
|
+
const createSessionStartMode = options.sessionStartMode ?? defaultDaemonSessionStartMode(adapter);
|
|
558
602
|
slot = await this.createSlot(adapter, {
|
|
559
603
|
profile: options.profile ?? this.profile,
|
|
604
|
+
sessionStartMode: createSessionStartMode,
|
|
560
605
|
});
|
|
561
606
|
this.slots.set(adapter, slot);
|
|
562
607
|
created = true;
|
|
563
608
|
}
|
|
564
|
-
this.activeAdapter = adapter;
|
|
565
609
|
let openedVisible = false;
|
|
566
610
|
let visibleConnected = isVisibleClientAlive(this.cwd, adapter);
|
|
611
|
+
const sessionStartMode = resolveDaemonSessionStartMode({
|
|
612
|
+
adapter,
|
|
613
|
+
explicitSessionStartMode: options.sessionStartMode,
|
|
614
|
+
slotCreated: created,
|
|
615
|
+
visibleConnected,
|
|
616
|
+
sharedSessionId: getSharedSessionIdFromAdapterState(slot.runtime.getState()),
|
|
617
|
+
reuseExistingVisible: options.reuseExistingVisible !== false,
|
|
618
|
+
});
|
|
619
|
+
if (!created &&
|
|
620
|
+
options.reuseExistingVisible === false &&
|
|
621
|
+
sessionStartMode === "new" &&
|
|
622
|
+
(adapter === "claude" || adapter === "opencode") &&
|
|
623
|
+
visibleConnected) {
|
|
624
|
+
await this.startFreshSlotSession(slot);
|
|
625
|
+
appendDaemonLog(`fresh_session_started: adapter=${adapter} source=start_command`);
|
|
626
|
+
}
|
|
567
627
|
if (options.openVisible !== false && !visibleConnected) {
|
|
628
|
+
slot.controller.syncLocalClientEndpoint();
|
|
568
629
|
const launch = openVisibleClient({
|
|
569
630
|
adapter,
|
|
570
631
|
cwd: this.cwd,
|
|
632
|
+
sessionStartMode,
|
|
571
633
|
cliArgs: options.cliArgs,
|
|
572
634
|
onError: (error) => {
|
|
573
635
|
appendDaemonLog(`visible_client_open_error: adapter=${adapter} error=${truncatePreview(error.message, 400)}`);
|
|
@@ -588,12 +650,18 @@ class WechatDaemon {
|
|
|
588
650
|
appendDaemonLog(`visible_client_connect_timeout: adapter=${adapter} cwd=${this.cwd} timeout_ms=${VISIBLE_CLIENT_CONNECT_TIMEOUT_MS} cleaned_launcher=${cleanedLauncher}`);
|
|
589
651
|
}
|
|
590
652
|
}
|
|
591
|
-
|
|
653
|
+
const activated = options.openVisible === false || visibleConnected;
|
|
654
|
+
if (activated) {
|
|
655
|
+
this.activeAdapter = adapter;
|
|
656
|
+
}
|
|
657
|
+
appendDaemonLog(`switch_adapter: adapter=${adapter} created=${created} opened_visible=${openedVisible} visible_connected=${visibleConnected} activated=${activated} previous_active=${previousActiveAdapter ?? "(none)"} session_start_mode=${sessionStartMode}`);
|
|
592
658
|
return {
|
|
593
659
|
activeAdapter: adapter,
|
|
594
660
|
created,
|
|
595
661
|
openedVisible,
|
|
596
662
|
visibleConnected,
|
|
663
|
+
activated,
|
|
664
|
+
previousActiveAdapter,
|
|
597
665
|
};
|
|
598
666
|
}
|
|
599
667
|
async createSlot(adapter, options) {
|
|
@@ -604,6 +672,7 @@ class WechatDaemon {
|
|
|
604
672
|
cwd: this.cwd,
|
|
605
673
|
profile: options.profile,
|
|
606
674
|
lifecycle: "persistent",
|
|
675
|
+
sessionStartMode: options.sessionStartMode,
|
|
607
676
|
companionLaunchMode: "daemon_auto",
|
|
608
677
|
});
|
|
609
678
|
const controller = new BridgeController(runtime, this.cwd);
|
|
@@ -624,9 +693,26 @@ class WechatDaemon {
|
|
|
624
693
|
});
|
|
625
694
|
await runtime.start();
|
|
626
695
|
controller.syncLocalClientEndpoint();
|
|
627
|
-
appendDaemonLog(`slot_started: adapter=${adapter} command=${resolveDefaultAdapterCommand(adapter)} cwd=${this.cwd}`);
|
|
696
|
+
appendDaemonLog(`slot_started: adapter=${adapter} command=${resolveDefaultAdapterCommand(adapter)} cwd=${this.cwd} session_start_mode=${options.sessionStartMode ?? "restore"}`);
|
|
628
697
|
return slot;
|
|
629
698
|
}
|
|
699
|
+
async startFreshSlotSession(slot) {
|
|
700
|
+
await slot.outputBatcher.flushNow();
|
|
701
|
+
slot.outputBatcher.clear();
|
|
702
|
+
slot.pendingConfirmation = null;
|
|
703
|
+
slot.pendingUserInput = null;
|
|
704
|
+
slot.activeTask = null;
|
|
705
|
+
if (slot.adapter === "claude") {
|
|
706
|
+
await slot.runtime.reset();
|
|
707
|
+
}
|
|
708
|
+
else if (slot.adapter === "opencode") {
|
|
709
|
+
if (!slot.runtime.createSession) {
|
|
710
|
+
throw new Error("/new is not available in opencode mode.");
|
|
711
|
+
}
|
|
712
|
+
await slot.runtime.createSession();
|
|
713
|
+
}
|
|
714
|
+
slot.controller.syncLocalClientEndpoint();
|
|
715
|
+
}
|
|
630
716
|
handleSlotEvent(slot, event) {
|
|
631
717
|
slot.controller.syncLocalClientEndpoint();
|
|
632
718
|
const adapterState = slot.runtime.getState();
|
|
@@ -703,7 +789,7 @@ class WechatDaemon {
|
|
|
703
789
|
const pending = toPendingUserInput(event.request);
|
|
704
790
|
slot.pendingUserInput = pending;
|
|
705
791
|
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
706
|
-
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "
|
|
792
|
+
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
707
793
|
}));
|
|
708
794
|
break;
|
|
709
795
|
case "mirrored_user_input":
|
|
@@ -779,9 +865,13 @@ class WechatDaemon {
|
|
|
779
865
|
if (switchAdapter) {
|
|
780
866
|
const result = await this.ensureSlot(switchAdapter, {
|
|
781
867
|
openVisible: true,
|
|
868
|
+
reuseExistingVisible: true,
|
|
782
869
|
});
|
|
783
870
|
const detail = formatDaemonSwitchResultDetail(result);
|
|
784
|
-
|
|
871
|
+
const heading = result.activated
|
|
872
|
+
? `Active terminal: ${switchAdapter}.`
|
|
873
|
+
: `Could not activate terminal: ${switchAdapter}.`;
|
|
874
|
+
await this.queueWechatMessage(message.senderId, `${heading}\n${detail}`);
|
|
785
875
|
return;
|
|
786
876
|
}
|
|
787
877
|
if (message.text.trim().toLowerCase() === "/daemon-stop") {
|
|
@@ -1027,15 +1117,180 @@ class WechatDaemon {
|
|
|
1027
1117
|
}
|
|
1028
1118
|
}
|
|
1029
1119
|
}
|
|
1030
|
-
|
|
1031
|
-
const
|
|
1120
|
+
function clearDaemonWorkspaceEndpoints(endpoint) {
|
|
1121
|
+
for (const adapter of DAEMON_ADAPTERS) {
|
|
1122
|
+
clearLocalCompanionEndpoint(endpoint.cwd, undefined, { adapter });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function isEndpointDaemonProcess(endpoint) {
|
|
1126
|
+
const record = getProcessRecordByPid(endpoint.pid);
|
|
1127
|
+
return Boolean(record && isWechatDaemonCommandLine(record.commandLine));
|
|
1128
|
+
}
|
|
1129
|
+
function selectDaemonProcessesToStop(records, excludedPids) {
|
|
1130
|
+
const recordPids = new Set(records.map((record) => record.pid));
|
|
1131
|
+
return records.filter((record) => {
|
|
1132
|
+
if (excludedPids.has(record.pid)) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
return !records.some((candidate) => candidate.parentPid === record.pid &&
|
|
1136
|
+
recordPids.has(candidate.pid) &&
|
|
1137
|
+
!excludedPids.has(candidate.pid));
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
async function stopDaemonPeerProcesses(params) {
|
|
1141
|
+
const excludedPids = new Set([process.pid, process.ppid]);
|
|
1142
|
+
const peerRecords = selectDaemonProcessesToStop(params.listDaemonProcesses(params.cwd), excludedPids);
|
|
1143
|
+
const stoppedPids = [];
|
|
1144
|
+
for (const peer of peerRecords) {
|
|
1145
|
+
params.daemonLog(`daemon_peer_takeover_attempt: pid=${peer.pid} cwd=${params.cwd} command=${truncatePreview(peer.commandLine, 400)}`);
|
|
1146
|
+
params.killProcess(peer.pid);
|
|
1147
|
+
if (await waitForProcessExit({
|
|
1148
|
+
pid: peer.pid,
|
|
1149
|
+
timeoutMs: params.timeoutMs,
|
|
1150
|
+
pollMs: params.pollMs,
|
|
1151
|
+
isAlive: params.isAlive,
|
|
1152
|
+
sleep: params.sleep,
|
|
1153
|
+
})) {
|
|
1154
|
+
stoppedPids.push(peer.pid);
|
|
1155
|
+
params.daemonLog(`daemon_peer_takeover_complete: pid=${peer.pid}`);
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
params.daemonLog(`daemon_peer_takeover_timeout: pid=${peer.pid}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return stoppedPids;
|
|
1162
|
+
}
|
|
1163
|
+
export async function cleanupDaemonBeforeStart(deps = {}) {
|
|
1164
|
+
const readEndpoint = deps.readEndpoint ?? readDaemonEndpoint;
|
|
1165
|
+
const isAlive = deps.isAlive ?? isPidAlive;
|
|
1166
|
+
const sendRequest = deps.sendRequest ?? sendDaemonRequest;
|
|
1167
|
+
const killProcess = deps.killProcess ?? killProcessTreeSync;
|
|
1168
|
+
const clearEndpoint = deps.clearEndpoint ?? clearDaemonEndpoint;
|
|
1169
|
+
const clearWorkspaceEndpoints = deps.clearWorkspaceEndpoints ?? clearDaemonWorkspaceEndpoints;
|
|
1170
|
+
const isDaemonProcess = deps.isDaemonProcess ?? isEndpointDaemonProcess;
|
|
1171
|
+
const listDaemonProcesses = deps.listDaemonProcesses ??
|
|
1172
|
+
((cwd) => listWechatDaemonProcesses({
|
|
1173
|
+
cwd,
|
|
1174
|
+
excludePids: [process.pid, process.ppid],
|
|
1175
|
+
}));
|
|
1176
|
+
const sleepFn = deps.sleep ?? sleep;
|
|
1177
|
+
const cleanupLog = deps.log ?? log;
|
|
1178
|
+
const daemonLog = deps.daemonLog ?? appendDaemonLog;
|
|
1179
|
+
const stopTimeoutMs = deps.stopTimeoutMs ?? DAEMON_TAKEOVER_STOP_TIMEOUT_MS;
|
|
1180
|
+
const forceStopTimeoutMs = deps.forceStopTimeoutMs ?? DAEMON_TAKEOVER_FORCE_STOP_TIMEOUT_MS;
|
|
1181
|
+
const pollMs = deps.pollMs ?? DAEMON_TAKEOVER_STOP_POLL_MS;
|
|
1182
|
+
const endpoint = readEndpoint();
|
|
1183
|
+
const cleanupCwd = endpoint?.cwd ?? deps.cwd;
|
|
1032
1184
|
if (!endpoint) {
|
|
1033
|
-
|
|
1185
|
+
if (cleanupCwd) {
|
|
1186
|
+
await stopDaemonPeerProcesses({
|
|
1187
|
+
cwd: cleanupCwd,
|
|
1188
|
+
listDaemonProcesses,
|
|
1189
|
+
killProcess,
|
|
1190
|
+
isAlive,
|
|
1191
|
+
sleep: sleepFn,
|
|
1192
|
+
timeoutMs: forceStopTimeoutMs,
|
|
1193
|
+
pollMs,
|
|
1194
|
+
daemonLog,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return { action: "none" };
|
|
1034
1198
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1199
|
+
const clearDaemonArtifacts = () => {
|
|
1200
|
+
clearWorkspaceEndpoints(endpoint);
|
|
1201
|
+
clearEndpoint(endpoint.pid);
|
|
1202
|
+
};
|
|
1203
|
+
if (endpoint.pid === process.pid || !isAlive(endpoint.pid)) {
|
|
1204
|
+
cleanupLog(`Found stale wechat-daemon endpoint for ${endpoint.cwd} (pid=${endpoint.pid}). Cleaning it before daemon startup.`);
|
|
1205
|
+
daemonLog(`daemon_stale_endpoint_cleanup: pid=${endpoint.pid} cwd=${endpoint.cwd}`);
|
|
1206
|
+
clearDaemonArtifacts();
|
|
1207
|
+
await stopDaemonPeerProcesses({
|
|
1208
|
+
cwd: endpoint.cwd,
|
|
1209
|
+
listDaemonProcesses,
|
|
1210
|
+
killProcess,
|
|
1211
|
+
isAlive,
|
|
1212
|
+
sleep: sleepFn,
|
|
1213
|
+
timeoutMs: forceStopTimeoutMs,
|
|
1214
|
+
pollMs,
|
|
1215
|
+
daemonLog,
|
|
1216
|
+
});
|
|
1217
|
+
return { action: "cleared_stale_endpoint", endpoint };
|
|
1218
|
+
}
|
|
1219
|
+
cleanupLog(`Found existing wechat-daemon for ${endpoint.cwd} (pid=${endpoint.pid}). Stopping it before daemon startup...`);
|
|
1220
|
+
daemonLog(`daemon_takeover_attempt: pid=${endpoint.pid} cwd=${endpoint.cwd}`);
|
|
1221
|
+
let shutdownAcknowledged = false;
|
|
1222
|
+
try {
|
|
1223
|
+
const response = await sendRequest(endpoint, { command: "shutdown" }, { timeoutMs: 1_000 });
|
|
1224
|
+
if (response.ok) {
|
|
1225
|
+
shutdownAcknowledged = true;
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
daemonLog(`daemon_shutdown_request_failed: pid=${endpoint.pid} error=${truncatePreview(response.error, 400)}`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
daemonLog(`daemon_shutdown_request_failed: pid=${endpoint.pid} error=${truncatePreview(error instanceof Error ? error.message : String(error), 400)}`);
|
|
1037
1233
|
}
|
|
1038
|
-
|
|
1234
|
+
let forced = false;
|
|
1235
|
+
let stopped = await waitForProcessExit({
|
|
1236
|
+
pid: endpoint.pid,
|
|
1237
|
+
timeoutMs: stopTimeoutMs,
|
|
1238
|
+
pollMs,
|
|
1239
|
+
isAlive,
|
|
1240
|
+
sleep: sleepFn,
|
|
1241
|
+
});
|
|
1242
|
+
if (!stopped) {
|
|
1243
|
+
if (!shutdownAcknowledged && !isDaemonProcess(endpoint)) {
|
|
1244
|
+
daemonLog(`daemon_force_stop_skipped_unverified: pid=${endpoint.pid} cwd=${endpoint.cwd}`);
|
|
1245
|
+
clearDaemonArtifacts();
|
|
1246
|
+
await stopDaemonPeerProcesses({
|
|
1247
|
+
cwd: endpoint.cwd,
|
|
1248
|
+
listDaemonProcesses,
|
|
1249
|
+
killProcess,
|
|
1250
|
+
isAlive,
|
|
1251
|
+
sleep: sleepFn,
|
|
1252
|
+
timeoutMs: forceStopTimeoutMs,
|
|
1253
|
+
pollMs,
|
|
1254
|
+
daemonLog,
|
|
1255
|
+
});
|
|
1256
|
+
return { action: "cleared_stale_endpoint", endpoint };
|
|
1257
|
+
}
|
|
1258
|
+
forced = true;
|
|
1259
|
+
cleanupLog(`Existing daemon pid=${endpoint.pid} did not stop in ${formatDuration(stopTimeoutMs)}. Forcing cleanup...`);
|
|
1260
|
+
daemonLog(`daemon_force_stop_attempt: pid=${endpoint.pid} cwd=${endpoint.cwd}`);
|
|
1261
|
+
try {
|
|
1262
|
+
killProcess(endpoint.pid);
|
|
1263
|
+
}
|
|
1264
|
+
catch (error) {
|
|
1265
|
+
if (isAlive(endpoint.pid)) {
|
|
1266
|
+
daemonLog(`daemon_force_stop_failed: pid=${endpoint.pid} error=${truncatePreview(error instanceof Error ? error.message : String(error), 400)}`);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
stopped = await waitForProcessExit({
|
|
1270
|
+
pid: endpoint.pid,
|
|
1271
|
+
timeoutMs: forceStopTimeoutMs,
|
|
1272
|
+
pollMs,
|
|
1273
|
+
isAlive,
|
|
1274
|
+
sleep: sleepFn,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
if (!stopped && isAlive(endpoint.pid)) {
|
|
1278
|
+
throw new Error(`Could not stop existing wechat-daemon automatically (pid=${endpoint.pid}, cwd=${endpoint.cwd}).`);
|
|
1279
|
+
}
|
|
1280
|
+
clearDaemonArtifacts();
|
|
1281
|
+
cleanupLog(`Cleaned previous wechat-daemon for ${endpoint.cwd}; daemon startup can continue.`);
|
|
1282
|
+
daemonLog(`daemon_takeover_complete: pid=${endpoint.pid} cwd=${endpoint.cwd} forced=${forced}`);
|
|
1283
|
+
await stopDaemonPeerProcesses({
|
|
1284
|
+
cwd: endpoint.cwd,
|
|
1285
|
+
listDaemonProcesses,
|
|
1286
|
+
killProcess,
|
|
1287
|
+
isAlive,
|
|
1288
|
+
sleep: sleepFn,
|
|
1289
|
+
timeoutMs: forceStopTimeoutMs,
|
|
1290
|
+
pollMs,
|
|
1291
|
+
daemonLog,
|
|
1292
|
+
});
|
|
1293
|
+
return { action: "stopped", endpoint, forced };
|
|
1039
1294
|
}
|
|
1040
1295
|
async function waitForProcessExit(params) {
|
|
1041
1296
|
const deadline = Date.now() + params.timeoutMs;
|
|
@@ -1077,6 +1332,7 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1077
1332
|
const clearEndpoint = deps.clearEndpoint ?? clearSingleBridgeEndpoint;
|
|
1078
1333
|
const sleepFn = deps.sleep ?? sleep;
|
|
1079
1334
|
const cleanupLog = deps.log ?? log;
|
|
1335
|
+
const daemonLog = deps.daemonLog ?? appendDaemonLog;
|
|
1080
1336
|
const stopTimeoutMs = deps.stopTimeoutMs ?? SINGLE_BRIDGE_STOP_TIMEOUT_MS;
|
|
1081
1337
|
const forceStopTimeoutMs = deps.forceStopTimeoutMs ?? SINGLE_BRIDGE_FORCE_STOP_TIMEOUT_MS;
|
|
1082
1338
|
const pollMs = deps.pollMs ?? SINGLE_BRIDGE_STOP_POLL_MS;
|
|
@@ -1090,18 +1346,18 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1090
1346
|
};
|
|
1091
1347
|
if (!isAlive(lock.pid)) {
|
|
1092
1348
|
cleanupLog(`Found stale single bridge lock for ${lock.cwd} (pid=${lock.pid} dead). Cleaning it before daemon startup.`);
|
|
1093
|
-
|
|
1349
|
+
daemonLog(`single_bridge_stale_cleanup: pid=${lock.pid} adapter=${lock.adapter} cwd=${lock.cwd}`);
|
|
1094
1350
|
clearBridgeArtifacts();
|
|
1095
1351
|
return { action: "cleared_stale_lock", lock };
|
|
1096
1352
|
}
|
|
1097
1353
|
cleanupLog(`Found existing single bridge for ${lock.cwd} (pid=${lock.pid}, adapter=${lock.adapter}). Stopping it before daemon startup...`);
|
|
1098
|
-
|
|
1354
|
+
daemonLog(`single_bridge_takeover_attempt: pid=${lock.pid} adapter=${lock.adapter} cwd=${lock.cwd}`);
|
|
1099
1355
|
try {
|
|
1100
1356
|
killProcess(lock.pid, "SIGTERM");
|
|
1101
1357
|
}
|
|
1102
1358
|
catch (error) {
|
|
1103
1359
|
if (isAlive(lock.pid)) {
|
|
1104
|
-
|
|
1360
|
+
daemonLog(`single_bridge_sigterm_failed: pid=${lock.pid} error=${truncatePreview(error instanceof Error ? error.message : String(error), 400)}`);
|
|
1105
1361
|
}
|
|
1106
1362
|
}
|
|
1107
1363
|
let forced = false;
|
|
@@ -1115,13 +1371,13 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1115
1371
|
if (!stopped) {
|
|
1116
1372
|
forced = true;
|
|
1117
1373
|
cleanupLog(`Single bridge pid=${lock.pid} did not stop in ${formatDuration(stopTimeoutMs)}. Forcing cleanup...`);
|
|
1118
|
-
|
|
1374
|
+
daemonLog(`single_bridge_force_stop_attempt: pid=${lock.pid} adapter=${lock.adapter} cwd=${lock.cwd}`);
|
|
1119
1375
|
try {
|
|
1120
1376
|
killProcess(lock.pid, "SIGKILL");
|
|
1121
1377
|
}
|
|
1122
1378
|
catch (error) {
|
|
1123
1379
|
if (isAlive(lock.pid)) {
|
|
1124
|
-
|
|
1380
|
+
daemonLog(`single_bridge_sigkill_failed: pid=${lock.pid} error=${truncatePreview(error instanceof Error ? error.message : String(error), 400)}`);
|
|
1125
1381
|
}
|
|
1126
1382
|
}
|
|
1127
1383
|
stopped = await waitForProcessExit({
|
|
@@ -1137,12 +1393,12 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1137
1393
|
}
|
|
1138
1394
|
clearBridgeArtifacts();
|
|
1139
1395
|
cleanupLog(`Cleaned previous single bridge for ${lock.cwd}; daemon startup can continue.`);
|
|
1140
|
-
|
|
1396
|
+
daemonLog(`single_bridge_takeover_complete: pid=${lock.pid} adapter=${lock.adapter} cwd=${lock.cwd} forced=${forced}`);
|
|
1141
1397
|
return { action: "stopped", lock, forced };
|
|
1142
1398
|
}
|
|
1143
1399
|
export async function runDaemon(options) {
|
|
1144
1400
|
migrateLegacyChannelFiles((message) => log(message));
|
|
1145
|
-
await
|
|
1401
|
+
await cleanupDaemonBeforeStart({ cwd: options.cwd });
|
|
1146
1402
|
await cleanupSingleBridgeBeforeDaemon();
|
|
1147
1403
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
1148
1404
|
logger: (message) => appendDaemonLog(message),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "cli-wechat-bridge",
|
|
3
|
-
"version": "1.0.
|
|
2
|
+
"name": "cli-wechat-bridge",
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"lint:fix": "eslint bin src test --fix",
|
|
56
56
|
"typecheck:src": "tsc -p tsconfig.typecheck.src.json --noEmit --pretty false",
|
|
57
57
|
"quality": "npm run lint && npm run typecheck:src && bun test test && npm run build",
|
|
58
|
-
"smoke:global": "node scripts/smoke-global-install.mjs",
|
|
59
|
-
"publish:dual": "node scripts/publish-dual.mjs",
|
|
58
|
+
"smoke:global": "node scripts/smoke-global-install.mjs",
|
|
59
|
+
"publish:dual": "node scripts/publish-dual.mjs",
|
|
60
60
|
"setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
|
|
61
61
|
"start": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts",
|
|
62
62
|
"check": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts --check",
|
|
@@ -80,16 +80,15 @@
|
|
|
80
80
|
"test:watch": "bun test --watch test"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@anthropic-ai/sdk": "^0.80.0",
|
|
84
83
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
85
84
|
"@opencode-ai/sdk": "^1.3.2",
|
|
86
|
-
"ajv-formats": "^3.0.1",
|
|
87
85
|
"node-pty": "^1.1.0",
|
|
88
86
|
"qrcode-terminal": "^0.12.0"
|
|
89
87
|
},
|
|
90
88
|
"devDependencies": {
|
|
91
89
|
"@eslint/js": "^10.0.1",
|
|
92
90
|
"@types/bun": "latest",
|
|
91
|
+
"@types/node": "^24.12.4",
|
|
93
92
|
"eslint": "^10.3.0",
|
|
94
93
|
"typescript": "^6.0.2",
|
|
95
94
|
"typescript-eslint": "^8.59.3"
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import { ensureWorkspaceChannelDir, getWorkspaceChannelPaths, } from "../wechat/channel-config.js";
|
|
4
|
-
export function buildCodexPanelToken() {
|
|
5
|
-
return crypto.randomBytes(18).toString("hex");
|
|
6
|
-
}
|
|
7
|
-
export function writeCodexPanelEndpoint(endpoint) {
|
|
8
|
-
const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
|
|
9
|
-
fs.writeFileSync(endpointFile, JSON.stringify(endpoint, null, 2), "utf8");
|
|
10
|
-
}
|
|
11
|
-
export function readCodexPanelEndpoint(cwd) {
|
|
12
|
-
try {
|
|
13
|
-
const { endpointFile } = getWorkspaceChannelPaths(cwd);
|
|
14
|
-
if (!fs.existsSync(endpointFile)) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
return JSON.parse(fs.readFileSync(endpointFile, "utf8"));
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export function clearCodexPanelEndpoint(cwd, instanceId) {
|
|
24
|
-
try {
|
|
25
|
-
const { endpointFile } = getWorkspaceChannelPaths(cwd);
|
|
26
|
-
if (!fs.existsSync(endpointFile)) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (!instanceId) {
|
|
30
|
-
fs.rmSync(endpointFile, { force: true });
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const endpoint = readCodexPanelEndpoint(cwd);
|
|
34
|
-
if (!endpoint || endpoint.instanceId === instanceId) {
|
|
35
|
-
fs.rmSync(endpointFile, { force: true });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
// Best effort cleanup.
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
export function sendCodexPanelMessage(socket, message) {
|
|
43
|
-
socket.write(`${JSON.stringify(message)}\n`);
|
|
44
|
-
}
|
|
45
|
-
export function attachCodexPanelMessageListener(socket, onMessage) {
|
|
46
|
-
let buffer = "";
|
|
47
|
-
const onData = (chunk) => {
|
|
48
|
-
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
49
|
-
while (true) {
|
|
50
|
-
const newlineIndex = buffer.indexOf("\n");
|
|
51
|
-
if (newlineIndex < 0) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const line = buffer.slice(0, newlineIndex).trim();
|
|
55
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
56
|
-
if (!line) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
onMessage(JSON.parse(line));
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Ignore malformed local IPC frames.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
socket.setEncoding("utf8");
|
|
68
|
-
socket.on("data", onData);
|
|
69
|
-
return () => {
|
|
70
|
-
socket.off("data", onData);
|
|
71
|
-
};
|
|
72
|
-
}
|