@unlinearity/cli-wechat-bridge 1.0.0 → 1.0.2
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/bin/_run-entry.mjs +2 -8
- package/bin/wechat-bridge-claude.mjs +2 -2
- package/bin/wechat-bridge-codex.mjs +2 -2
- package/bin/wechat-bridge-opencode.mjs +2 -2
- package/bin/wechat-bridge-shell.mjs +2 -2
- package/bin/wechat-bridge.mjs +2 -2
- package/bin/wechat-check-update.mjs +2 -2
- package/bin/wechat-claude-start.mjs +2 -2
- package/bin/wechat-claude.mjs +2 -2
- package/bin/wechat-codex-start.mjs +2 -2
- package/bin/wechat-codex.mjs +2 -2
- package/bin/wechat-opencode-start.mjs +2 -2
- package/bin/wechat-opencode.mjs +2 -2
- package/bin/wechat-setup.mjs +5 -0
- package/dist/bridge/bridge-adapters.claude.js +829 -0
- package/dist/bridge/bridge-adapters.codex.js +2206 -0
- package/dist/bridge/bridge-adapters.core.js +668 -0
- package/dist/bridge/bridge-adapters.js +26 -0
- package/dist/bridge/bridge-adapters.opencode.js +1777 -0
- package/dist/bridge/bridge-adapters.shared.js +1098 -0
- package/dist/bridge/bridge-adapters.shell.js +363 -0
- package/dist/bridge/bridge-controller.js +44 -0
- package/dist/bridge/bridge-final-reply.js +31 -0
- package/dist/bridge/bridge-process-reaper.js +348 -0
- package/dist/bridge/bridge-state.js +356 -0
- package/dist/bridge/bridge-types.js +1 -0
- package/dist/bridge/bridge-utils.js +1150 -0
- package/dist/bridge/claude-hook.js +82 -0
- package/dist/bridge/claude-hooks.js +267 -0
- package/dist/bridge/wechat-bridge.js +904 -0
- package/dist/commands/check-update.js +30 -0
- package/dist/companion/codex-panel-link.js +72 -0
- package/dist/companion/codex-panel.js +179 -0
- package/dist/companion/codex-remote-client.js +117 -0
- package/dist/companion/local-companion-link.js +210 -0
- package/dist/companion/local-companion-start.js +372 -0
- package/dist/companion/local-companion.js +411 -0
- package/dist/media/media-types.js +1 -0
- package/dist/runtime/create-runtime-host.js +12 -0
- package/dist/runtime/legacy-adapter-runtime.js +40 -0
- package/dist/runtime/runtime-types.js +5 -0
- package/dist/utils/version-checker.js +161 -0
- package/dist/wechat/channel-config.js +103 -0
- package/dist/wechat/setup.js +283 -0
- package/dist/wechat/standalone-bot.js +356 -0
- package/dist/wechat/wechat-channel.js +481 -0
- package/dist/wechat/wechat-transport.js +887 -0
- package/package.json +6 -2
- package/src/bridge/bridge-adapters.claude.ts +0 -1047
- package/src/bridge/bridge-adapters.codex.ts +0 -2733
- package/src/bridge/bridge-adapters.core.ts +0 -877
- package/src/bridge/bridge-adapters.opencode.ts +0 -2390
- package/src/bridge/bridge-adapters.shared.ts +0 -1647
- package/src/bridge/bridge-adapters.shell.ts +0 -456
- package/src/bridge/bridge-adapters.ts +0 -30
- package/src/bridge/bridge-controller.ts +0 -58
- package/src/bridge/bridge-final-reply.ts +0 -56
- package/src/bridge/bridge-process-reaper.ts +0 -417
- package/src/bridge/bridge-state.ts +0 -520
- package/src/bridge/bridge-types.ts +0 -218
- package/src/bridge/bridge-utils.ts +0 -1524
- package/src/bridge/claude-hook.ts +0 -92
- package/src/bridge/claude-hooks.ts +0 -376
- package/src/bridge/wechat-bridge.ts +0 -1305
- package/src/commands/check-update.ts +0 -46
- package/src/companion/codex-panel-link.ts +0 -151
- package/src/companion/codex-panel.ts +0 -225
- package/src/companion/codex-remote-client.ts +0 -178
- package/src/companion/local-companion-link.ts +0 -338
- package/src/companion/local-companion-start.ts +0 -520
- package/src/companion/local-companion.ts +0 -532
- package/src/media/media-types.ts +0 -11
- package/src/runtime/create-runtime-host.ts +0 -16
- package/src/runtime/legacy-adapter-runtime.ts +0 -55
- package/src/runtime/runtime-types.ts +0 -51
- package/src/utils/version-checker.ts +0 -197
- package/src/wechat/channel-config.ts +0 -141
- package/src/wechat/setup.ts +0 -232
- package/src/wechat/standalone-bot.ts +0 -483
- package/src/wechat/wechat-channel.ts +0 -608
- package/src/wechat/wechat-transport.ts +0 -1290
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
5
|
+
import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.shared.js";
|
|
6
|
+
import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
|
|
7
|
+
import { attachLocalCompanionMessageListener, readLocalCompanionEndpoint, sendLocalCompanionMessage, } from "./local-companion-link.js";
|
|
8
|
+
import { migrateLegacyChannelFiles } from "../wechat/channel-config.js";
|
|
9
|
+
export const LOCAL_COMPANION_RECONNECT_RETRY_MS = 250;
|
|
10
|
+
function log(adapter, message) {
|
|
11
|
+
process.stderr.write(`[${adapter}-companion] ${message}\n`);
|
|
12
|
+
}
|
|
13
|
+
function parseCliArgs(argv) {
|
|
14
|
+
let adapter = null;
|
|
15
|
+
let cwd = process.cwd();
|
|
16
|
+
const cliArgs = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
18
|
+
const arg = argv[i];
|
|
19
|
+
const next = argv[i + 1];
|
|
20
|
+
if (arg === "--help" || arg === "-h") {
|
|
21
|
+
process.stdout.write([
|
|
22
|
+
"Usage: local-companion --adapter <codex|claude|opencode> [--cwd <path>] [...cli args]",
|
|
23
|
+
"",
|
|
24
|
+
'Starts the visible local companion and connects it to the matching running bridge for the current directory.',
|
|
25
|
+
"Unknown arguments are forwarded to the visible CLI client.",
|
|
26
|
+
"",
|
|
27
|
+
].join("\n"));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
if (arg === "--adapter") {
|
|
31
|
+
if (!next || !["codex", "claude", "opencode"].includes(next)) {
|
|
32
|
+
throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
|
|
33
|
+
}
|
|
34
|
+
adapter = next;
|
|
35
|
+
i += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--cwd") {
|
|
39
|
+
if (!next) {
|
|
40
|
+
throw new Error("--cwd requires a value");
|
|
41
|
+
}
|
|
42
|
+
cwd = path.resolve(next);
|
|
43
|
+
i += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
cliArgs.push(arg);
|
|
47
|
+
}
|
|
48
|
+
if (!adapter) {
|
|
49
|
+
throw new Error("Missing required --adapter <codex|claude|opencode>");
|
|
50
|
+
}
|
|
51
|
+
return { adapter, cwd, cliArgs };
|
|
52
|
+
}
|
|
53
|
+
function delay(ms) {
|
|
54
|
+
if (ms <= 0) {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
function readMatchingEndpoint(options) {
|
|
60
|
+
const endpoint = readLocalCompanionEndpoint(options.cwd);
|
|
61
|
+
if (!endpoint || endpoint.kind !== options.adapter) {
|
|
62
|
+
throw new Error(`No active ${options.adapter} bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-${options.adapter}" in that directory first.`);
|
|
63
|
+
}
|
|
64
|
+
return endpoint;
|
|
65
|
+
}
|
|
66
|
+
export function shouldReconnectLocalCompanion(params) {
|
|
67
|
+
return !params.shuttingDown && !params.closeReason;
|
|
68
|
+
}
|
|
69
|
+
export async function runLocalCompanion(options) {
|
|
70
|
+
const initialEndpoint = readMatchingEndpoint(options);
|
|
71
|
+
if (initialEndpoint.kind === "codex" &&
|
|
72
|
+
initialEndpoint.runtimeKind === "codex_runtime_host") {
|
|
73
|
+
return await runCodexRemoteClientFromEndpoint(initialEndpoint, {
|
|
74
|
+
extraCliArgs: options.cliArgs,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const adapter = createBridgeAdapter({
|
|
78
|
+
kind: initialEndpoint.kind,
|
|
79
|
+
command: initialEndpoint.command,
|
|
80
|
+
cwd: initialEndpoint.cwd,
|
|
81
|
+
profile: initialEndpoint.profile,
|
|
82
|
+
initialSharedSessionId: initialEndpoint.sharedSessionId ?? initialEndpoint.sharedThreadId,
|
|
83
|
+
initialResumeConversationId: initialEndpoint.resumeConversationId,
|
|
84
|
+
initialTranscriptPath: initialEndpoint.transcriptPath,
|
|
85
|
+
renderMode: initialEndpoint.kind === "codex" ? "panel" : "companion",
|
|
86
|
+
extraCliArgs: options.cliArgs,
|
|
87
|
+
});
|
|
88
|
+
let shuttingDown = false;
|
|
89
|
+
let closeReason = null;
|
|
90
|
+
let activeSocket = null;
|
|
91
|
+
let detachListener = null;
|
|
92
|
+
let reconnectPromise = null;
|
|
93
|
+
let resolveExitCode = null;
|
|
94
|
+
const exitCodePromise = new Promise((resolve) => {
|
|
95
|
+
resolveExitCode = resolve;
|
|
96
|
+
});
|
|
97
|
+
let signalHandlersRegistered = false;
|
|
98
|
+
const detachActiveSocket = (destroy = false) => {
|
|
99
|
+
const socket = activeSocket;
|
|
100
|
+
activeSocket = null;
|
|
101
|
+
detachListener?.();
|
|
102
|
+
detachListener = null;
|
|
103
|
+
if (!socket) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
socket.removeAllListeners("close");
|
|
107
|
+
socket.removeAllListeners("error");
|
|
108
|
+
if (destroy) {
|
|
109
|
+
try {
|
|
110
|
+
socket.destroy();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Best effort cleanup.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const resolveExit = (code) => {
|
|
118
|
+
const resolve = resolveExitCode;
|
|
119
|
+
if (!resolve) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
resolveExitCode = null;
|
|
123
|
+
resolve(code);
|
|
124
|
+
};
|
|
125
|
+
const unregisterSignalHandlers = () => {
|
|
126
|
+
if (!signalHandlersRegistered) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
signalHandlersRegistered = false;
|
|
130
|
+
process.removeListener("SIGINT", handleSigint);
|
|
131
|
+
process.removeListener("SIGTERM", handleSigterm);
|
|
132
|
+
process.removeListener("SIGHUP", handleSighup);
|
|
133
|
+
if (process.platform === "win32") {
|
|
134
|
+
process.removeListener("SIGBREAK", handleSigbreak);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const publishState = () => {
|
|
138
|
+
if (!activeSocket) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
sendLocalCompanionMessage(activeSocket, {
|
|
142
|
+
type: "state",
|
|
143
|
+
state: adapter.getState(),
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
const sendResponse = (socket, id, ok, result, error) => {
|
|
147
|
+
sendLocalCompanionMessage(socket, {
|
|
148
|
+
type: "response",
|
|
149
|
+
id,
|
|
150
|
+
ok,
|
|
151
|
+
result,
|
|
152
|
+
error,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const announceClosing = (reason, exitCode) => {
|
|
156
|
+
closeReason = reason;
|
|
157
|
+
if (!activeSocket) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
sendLocalCompanionMessage(activeSocket, {
|
|
161
|
+
type: "closing",
|
|
162
|
+
reason,
|
|
163
|
+
exitCode,
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
const closeCompanion = async (exitCode = 0, reason = "companion_shutdown") => {
|
|
167
|
+
if (shuttingDown) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
shuttingDown = true;
|
|
171
|
+
announceClosing(reason, exitCode);
|
|
172
|
+
detachActiveSocket(false);
|
|
173
|
+
try {
|
|
174
|
+
await adapter.dispose();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Best effort cleanup.
|
|
178
|
+
}
|
|
179
|
+
resolveExit(exitCode);
|
|
180
|
+
};
|
|
181
|
+
const handleBridgeRequest = async (socket, message) => {
|
|
182
|
+
try {
|
|
183
|
+
switch (message.payload.command) {
|
|
184
|
+
case "send_input":
|
|
185
|
+
await adapter.sendInput(message.payload.text);
|
|
186
|
+
sendResponse(socket, message.id, true);
|
|
187
|
+
break;
|
|
188
|
+
case "list_resume_sessions":
|
|
189
|
+
case "list_resume_threads":
|
|
190
|
+
sendResponse(socket, message.id, true, await adapter.listResumeSessions(message.payload.limit));
|
|
191
|
+
break;
|
|
192
|
+
case "resume_session":
|
|
193
|
+
await adapter.resumeSession(message.payload.sessionId);
|
|
194
|
+
publishState();
|
|
195
|
+
sendResponse(socket, message.id, true);
|
|
196
|
+
break;
|
|
197
|
+
case "resume_thread":
|
|
198
|
+
await adapter.resumeSession(message.payload.threadId);
|
|
199
|
+
publishState();
|
|
200
|
+
sendResponse(socket, message.id, true);
|
|
201
|
+
break;
|
|
202
|
+
case "interrupt":
|
|
203
|
+
sendResponse(socket, message.id, true, await adapter.interrupt());
|
|
204
|
+
break;
|
|
205
|
+
case "reset":
|
|
206
|
+
await adapter.reset();
|
|
207
|
+
publishState();
|
|
208
|
+
sendResponse(socket, message.id, true);
|
|
209
|
+
break;
|
|
210
|
+
case "resolve_approval":
|
|
211
|
+
sendResponse(socket, message.id, true, await adapter.resolveApproval(message.payload.action));
|
|
212
|
+
break;
|
|
213
|
+
case "dispose":
|
|
214
|
+
sendResponse(socket, message.id, true);
|
|
215
|
+
await closeCompanion(0, "bridge_dispose");
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
221
|
+
sendResponse(socket, message.id, false, undefined, text);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const connectToBridge = async (endpoint) => {
|
|
225
|
+
await new Promise((resolve, reject) => {
|
|
226
|
+
const socket = net.connect({
|
|
227
|
+
host: "127.0.0.1",
|
|
228
|
+
port: endpoint.port,
|
|
229
|
+
});
|
|
230
|
+
let settled = false;
|
|
231
|
+
let helloAcknowledged = false;
|
|
232
|
+
let localDetach = null;
|
|
233
|
+
const fail = (error) => {
|
|
234
|
+
if (settled) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
settled = true;
|
|
238
|
+
localDetach?.();
|
|
239
|
+
localDetach = null;
|
|
240
|
+
try {
|
|
241
|
+
socket.destroy();
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Best effort cleanup.
|
|
245
|
+
}
|
|
246
|
+
reject(error);
|
|
247
|
+
};
|
|
248
|
+
socket.once("connect", () => {
|
|
249
|
+
socket.setNoDelay(true);
|
|
250
|
+
localDetach = attachLocalCompanionMessageListener(socket, (message) => {
|
|
251
|
+
if (!helloAcknowledged) {
|
|
252
|
+
if (message.type === "hello_ack") {
|
|
253
|
+
helloAcknowledged = true;
|
|
254
|
+
closeReason = null;
|
|
255
|
+
activeSocket = socket;
|
|
256
|
+
detachListener = localDetach;
|
|
257
|
+
if (!settled) {
|
|
258
|
+
settled = true;
|
|
259
|
+
resolve();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (message.type !== "request") {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
void handleBridgeRequest(socket, message);
|
|
268
|
+
});
|
|
269
|
+
socket.once("close", () => {
|
|
270
|
+
if (!settled) {
|
|
271
|
+
fail(new Error(`The ${options.adapter} bridge closed the local companion socket before authentication.`));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (activeSocket === socket) {
|
|
275
|
+
detachActiveSocket(false);
|
|
276
|
+
void (async () => {
|
|
277
|
+
if (!shouldReconnectLocalCompanion({
|
|
278
|
+
shuttingDown,
|
|
279
|
+
closeReason,
|
|
280
|
+
})) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const reconnected = await reconnectToBridge();
|
|
284
|
+
if (!reconnected && !shuttingDown) {
|
|
285
|
+
await closeCompanion(1, "fatal_error");
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
socket.once("error", (error) => {
|
|
291
|
+
if (!settled) {
|
|
292
|
+
fail(error instanceof Error
|
|
293
|
+
? error
|
|
294
|
+
: new Error(String(error)));
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
sendLocalCompanionMessage(socket, {
|
|
298
|
+
type: "hello",
|
|
299
|
+
token: endpoint.token,
|
|
300
|
+
companionPid: process.pid,
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
socket.once("error", (error) => {
|
|
304
|
+
if (!settled) {
|
|
305
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const reconnectToBridge = async () => {
|
|
311
|
+
if (reconnectPromise) {
|
|
312
|
+
return await reconnectPromise;
|
|
313
|
+
}
|
|
314
|
+
reconnectPromise = (async () => {
|
|
315
|
+
const deadline = Date.now() + LOCAL_COMPANION_RECONNECT_GRACE_MS;
|
|
316
|
+
let lastError = "";
|
|
317
|
+
log(options.adapter, `Bridge connection dropped unexpectedly. Waiting up to ${Math.ceil(LOCAL_COMPANION_RECONNECT_GRACE_MS / 1000)}s to reconnect...`);
|
|
318
|
+
while (!shuttingDown && Date.now() < deadline) {
|
|
319
|
+
try {
|
|
320
|
+
const nextEndpoint = readMatchingEndpoint(options);
|
|
321
|
+
await connectToBridge(nextEndpoint);
|
|
322
|
+
publishState();
|
|
323
|
+
log(options.adapter, `Reconnected to bridge ${nextEndpoint.instanceId}.`);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
328
|
+
await delay(LOCAL_COMPANION_RECONNECT_RETRY_MS);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (lastError) {
|
|
332
|
+
log(options.adapter, `Bridge reconnection timed out after ${Math.ceil(LOCAL_COMPANION_RECONNECT_GRACE_MS / 1000)}s: ${lastError}`);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
log(options.adapter, `Bridge reconnection timed out after ${Math.ceil(LOCAL_COMPANION_RECONNECT_GRACE_MS / 1000)}s.`);
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
})();
|
|
339
|
+
try {
|
|
340
|
+
return await reconnectPromise;
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
reconnectPromise = null;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
adapter.setEventSink((event) => {
|
|
347
|
+
if (activeSocket) {
|
|
348
|
+
sendLocalCompanionMessage(activeSocket, {
|
|
349
|
+
type: "event",
|
|
350
|
+
event,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
publishState();
|
|
354
|
+
if (event.type === "fatal_error") {
|
|
355
|
+
void closeCompanion(1, "fatal_error");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (event.type === "status" && event.status === "stopped") {
|
|
359
|
+
void closeCompanion(0, "worker_exit");
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
const requestSignalShutdown = (signal) => {
|
|
363
|
+
log(options.adapter, `Received ${signal}. Closing local companion.`);
|
|
364
|
+
void closeCompanion(0, "signal");
|
|
365
|
+
};
|
|
366
|
+
const handleSigint = () => requestSignalShutdown("SIGINT");
|
|
367
|
+
const handleSigterm = () => requestSignalShutdown("SIGTERM");
|
|
368
|
+
const handleSighup = () => requestSignalShutdown("SIGHUP");
|
|
369
|
+
const handleSigbreak = () => requestSignalShutdown("SIGBREAK");
|
|
370
|
+
process.once("SIGINT", handleSigint);
|
|
371
|
+
process.once("SIGTERM", handleSigterm);
|
|
372
|
+
process.once("SIGHUP", handleSighup);
|
|
373
|
+
if (process.platform === "win32") {
|
|
374
|
+
process.once("SIGBREAK", handleSigbreak);
|
|
375
|
+
}
|
|
376
|
+
signalHandlersRegistered = true;
|
|
377
|
+
try {
|
|
378
|
+
await connectToBridge(initialEndpoint);
|
|
379
|
+
await adapter.start();
|
|
380
|
+
publishState();
|
|
381
|
+
log(options.adapter, `Connected to bridge ${initialEndpoint.instanceId}.`);
|
|
382
|
+
return await exitCodePromise;
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
unregisterSignalHandlers();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
389
|
+
migrateLegacyChannelFiles((message) => log("local", message));
|
|
390
|
+
try {
|
|
391
|
+
const options = parseCliArgs(argv);
|
|
392
|
+
const exitCode = await runLocalCompanion(options);
|
|
393
|
+
process.exit(exitCode);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
const adapter = (() => {
|
|
397
|
+
try {
|
|
398
|
+
return parseCliArgs(argv).adapter;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return "local";
|
|
402
|
+
}
|
|
403
|
+
})();
|
|
404
|
+
log(adapter, error instanceof Error ? error.message : String(error));
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const isDirectRun = Boolean(import.meta.main);
|
|
409
|
+
if (isDirectRun) {
|
|
410
|
+
void main();
|
|
411
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
2
|
+
import { CodexPtyAdapter } from "../bridge/bridge-adapters.codex.js";
|
|
3
|
+
import { LegacyAdapterRuntime } from "./legacy-adapter-runtime.js";
|
|
4
|
+
export function createRuntimeHost(options) {
|
|
5
|
+
if (options.kind === "codex") {
|
|
6
|
+
return new CodexPtyAdapter({
|
|
7
|
+
...options,
|
|
8
|
+
renderMode: options.renderMode ?? "headless",
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return new LegacyAdapterRuntime(createBridgeAdapter(options));
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class LegacyAdapterRuntime {
|
|
2
|
+
runtimeKind = "legacy_adapter";
|
|
3
|
+
adapter;
|
|
4
|
+
constructor(adapter) {
|
|
5
|
+
this.adapter = adapter;
|
|
6
|
+
}
|
|
7
|
+
setEventSink(sink) {
|
|
8
|
+
this.adapter.setEventSink(sink);
|
|
9
|
+
}
|
|
10
|
+
async start() {
|
|
11
|
+
await this.adapter.start();
|
|
12
|
+
}
|
|
13
|
+
async sendInput(text) {
|
|
14
|
+
await this.adapter.sendInput(text);
|
|
15
|
+
}
|
|
16
|
+
async listResumeSessions(limit) {
|
|
17
|
+
return await this.adapter.listResumeSessions(limit);
|
|
18
|
+
}
|
|
19
|
+
async resumeSession(sessionId) {
|
|
20
|
+
await this.adapter.resumeSession(sessionId);
|
|
21
|
+
}
|
|
22
|
+
async interrupt() {
|
|
23
|
+
return await this.adapter.interrupt();
|
|
24
|
+
}
|
|
25
|
+
async reset() {
|
|
26
|
+
await this.adapter.reset();
|
|
27
|
+
}
|
|
28
|
+
async resolveApproval(action) {
|
|
29
|
+
return await this.adapter.resolveApproval(action);
|
|
30
|
+
}
|
|
31
|
+
async submitUserInput(answers) {
|
|
32
|
+
return await this.adapter.submitUserInput(answers);
|
|
33
|
+
}
|
|
34
|
+
async dispose() {
|
|
35
|
+
await this.adapter.dispose();
|
|
36
|
+
}
|
|
37
|
+
getState() {
|
|
38
|
+
return this.adapter.getState();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { CHANNEL_DATA_DIR, ensureChannelDataDir } from "../wechat/channel-config.js";
|
|
5
|
+
const UPDATE_CHECK_FILE = path.join(CHANNEL_DATA_DIR, "update-check.json");
|
|
6
|
+
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24小时
|
|
7
|
+
/**
|
|
8
|
+
* 使用 git 命令获取远程仓库最新版本号
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchLatestVersion() {
|
|
11
|
+
try {
|
|
12
|
+
// 尝试使用 git ls-remote 获取所有 tags
|
|
13
|
+
const tagsOutput = execSync('git ls-remote --tags origin', {
|
|
14
|
+
cwd: path.resolve(import.meta.dirname, "..", ".."),
|
|
15
|
+
encoding: 'utf-8',
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
17
|
+
});
|
|
18
|
+
if (!tagsOutput) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
// 解析 tags,找到版本号格式(如 0.1.0, 0.2.0 等)
|
|
22
|
+
const versionTags = tagsOutput
|
|
23
|
+
.split('\n')
|
|
24
|
+
.filter(line => {
|
|
25
|
+
// 匹配 refs/tags/0.5.0 格式(不是 v0.5.0)
|
|
26
|
+
return line.includes('refs/tags/') &&
|
|
27
|
+
!line.includes('^{}') &&
|
|
28
|
+
/^\w+\s+refs\/tags\/\d+\.\d+\.\d+$/.test(line);
|
|
29
|
+
})
|
|
30
|
+
.map(line => {
|
|
31
|
+
const match = line.match(/refs\/tags\/(\d+\.\d+\.\d+)$/);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
})
|
|
34
|
+
.filter((v) => v !== null);
|
|
35
|
+
if (versionTags.length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// 按版本号排序,返回最新的
|
|
39
|
+
versionTags.sort((a, b) => compareVersions(b, a));
|
|
40
|
+
return versionTags[0];
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// 如果 git 命令失败,静默返回 null
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 从本地 package.json 读取当前版本
|
|
49
|
+
*/
|
|
50
|
+
export async function getCurrentVersion() {
|
|
51
|
+
const packageJsonPath = path.resolve(import.meta.dirname, "..", "..", "package.json");
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.promises.readFile(packageJsonPath, "utf-8");
|
|
54
|
+
const packageJson = JSON.parse(content);
|
|
55
|
+
return packageJson.version || "0.0.0";
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return "0.0.0";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 读取更新检查缓存
|
|
63
|
+
*/
|
|
64
|
+
function readUpdateCache() {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(UPDATE_CHECK_FILE)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const content = fs.readFileSync(UPDATE_CHECK_FILE, "utf-8");
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 写入更新检查缓存
|
|
78
|
+
*/
|
|
79
|
+
function writeUpdateCache(cache) {
|
|
80
|
+
try {
|
|
81
|
+
ensureChannelDataDir();
|
|
82
|
+
fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify(cache, null, 2));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// 静默失败,不影响正常使用
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 比较两个版本号
|
|
90
|
+
* @returns 返回值 > 0 表示 version1 更新,< 0 表示 version2 更新,= 0 表示相等
|
|
91
|
+
*/
|
|
92
|
+
export function compareVersions(v1, v2) {
|
|
93
|
+
const parts1 = v1.split(".").map(Number);
|
|
94
|
+
const parts2 = v2.split(".").map(Number);
|
|
95
|
+
for (let i = 0; i < 3; i++) {
|
|
96
|
+
const num1 = parts1[i] || 0;
|
|
97
|
+
const num2 = parts2[i] || 0;
|
|
98
|
+
if (num1 > num2)
|
|
99
|
+
return 1;
|
|
100
|
+
if (num1 < num2)
|
|
101
|
+
return -1;
|
|
102
|
+
}
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 检查是否有新版本可用
|
|
107
|
+
* @param forceCheck 是否强制检查(忽略缓存)
|
|
108
|
+
*/
|
|
109
|
+
export async function checkForUpdate(forceCheck = false) {
|
|
110
|
+
const currentVersion = await getCurrentVersion();
|
|
111
|
+
// 检查缓存
|
|
112
|
+
if (!forceCheck) {
|
|
113
|
+
const cache = readUpdateCache();
|
|
114
|
+
if (cache) {
|
|
115
|
+
const lastCheckTime = new Date(cache.lastCheck).getTime();
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
// 如果缓存未过期(24小时内),且已经通知过最新版本
|
|
118
|
+
if (now - lastCheckTime < CACHE_DURATION_MS) {
|
|
119
|
+
// 如果缓存的版本与当前版本一致,说明已经通知过
|
|
120
|
+
if (cache.lastNotifiedVersion === currentVersion) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 从 GitHub 获取最新版本
|
|
127
|
+
const latestVersion = await fetchLatestVersion();
|
|
128
|
+
if (!latestVersion) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// 比较版本
|
|
132
|
+
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
|
|
133
|
+
// 更新缓存
|
|
134
|
+
writeUpdateCache({
|
|
135
|
+
lastCheck: new Date().toISOString(),
|
|
136
|
+
lastNotifiedVersion: currentVersion,
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
current: currentVersion,
|
|
140
|
+
latest: latestVersion,
|
|
141
|
+
hasUpdate,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 格式化更新提示信息
|
|
146
|
+
*/
|
|
147
|
+
export function formatUpdateMessage(versionInfo) {
|
|
148
|
+
const { current, latest } = versionInfo;
|
|
149
|
+
return `
|
|
150
|
+
[Update Available] Version ${latest} is available (current: ${current})
|
|
151
|
+
|
|
152
|
+
Update instructions:
|
|
153
|
+
cd CLI-WeChat-Bridge
|
|
154
|
+
git pull
|
|
155
|
+
bun install
|
|
156
|
+
npm install -g .
|
|
157
|
+
|
|
158
|
+
For more information:
|
|
159
|
+
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
160
|
+
`;
|
|
161
|
+
}
|