@unlinearity/cli-wechat-bridge 1.0.0

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.
Files changed (49) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +531 -0
  3. package/bin/_run-entry.mjs +41 -0
  4. package/bin/wechat-bridge-claude.mjs +5 -0
  5. package/bin/wechat-bridge-codex.mjs +5 -0
  6. package/bin/wechat-bridge-opencode.mjs +5 -0
  7. package/bin/wechat-bridge-shell.mjs +5 -0
  8. package/bin/wechat-bridge.mjs +5 -0
  9. package/bin/wechat-check-update.mjs +5 -0
  10. package/bin/wechat-claude-start.mjs +5 -0
  11. package/bin/wechat-claude.mjs +5 -0
  12. package/bin/wechat-codex-start.mjs +5 -0
  13. package/bin/wechat-codex.mjs +5 -0
  14. package/bin/wechat-opencode-start.mjs +5 -0
  15. package/bin/wechat-opencode.mjs +5 -0
  16. package/package.json +86 -0
  17. package/src/bridge/bridge-adapters.claude.ts +1047 -0
  18. package/src/bridge/bridge-adapters.codex.ts +2733 -0
  19. package/src/bridge/bridge-adapters.core.ts +877 -0
  20. package/src/bridge/bridge-adapters.opencode.ts +2390 -0
  21. package/src/bridge/bridge-adapters.shared.ts +1647 -0
  22. package/src/bridge/bridge-adapters.shell.ts +456 -0
  23. package/src/bridge/bridge-adapters.ts +30 -0
  24. package/src/bridge/bridge-controller.ts +58 -0
  25. package/src/bridge/bridge-final-reply.ts +56 -0
  26. package/src/bridge/bridge-process-reaper.ts +417 -0
  27. package/src/bridge/bridge-state.ts +520 -0
  28. package/src/bridge/bridge-types.ts +218 -0
  29. package/src/bridge/bridge-utils.ts +1524 -0
  30. package/src/bridge/claude-hook.ts +92 -0
  31. package/src/bridge/claude-hooks.ts +376 -0
  32. package/src/bridge/wechat-bridge.ts +1305 -0
  33. package/src/commands/check-update.ts +46 -0
  34. package/src/companion/codex-panel-link.ts +151 -0
  35. package/src/companion/codex-panel.ts +225 -0
  36. package/src/companion/codex-remote-client.ts +178 -0
  37. package/src/companion/local-companion-link.ts +338 -0
  38. package/src/companion/local-companion-start.ts +520 -0
  39. package/src/companion/local-companion.ts +532 -0
  40. package/src/media/media-types.ts +11 -0
  41. package/src/runtime/create-runtime-host.ts +16 -0
  42. package/src/runtime/legacy-adapter-runtime.ts +55 -0
  43. package/src/runtime/runtime-types.ts +51 -0
  44. package/src/utils/version-checker.ts +197 -0
  45. package/src/wechat/channel-config.ts +141 -0
  46. package/src/wechat/setup.ts +232 -0
  47. package/src/wechat/standalone-bot.ts +483 -0
  48. package/src/wechat/wechat-channel.ts +608 -0
  49. package/src/wechat/wechat-transport.ts +1290 -0
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import {
4
+ checkForUpdate,
5
+ getCurrentVersion,
6
+ fetchLatestVersion,
7
+ compareVersions,
8
+ } from "../utils/version-checker.ts";
9
+
10
+ async function main(): Promise<void> {
11
+ const currentVersion = await getCurrentVersion();
12
+
13
+ console.log(`CLI WeChat Bridge Version Check`);
14
+ console.log(`Current version: v${currentVersion}\n`);
15
+
16
+ console.log(`Checking for updates...`);
17
+
18
+ const versionInfo = await checkForUpdate(true); // 强制检查
19
+
20
+ if (!versionInfo) {
21
+ console.log(`ERROR: Unable to check for updates, please check your network connection`);
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!versionInfo.hasUpdate) {
26
+ console.log(`OK: Already up to date (v${versionInfo.latest})`);
27
+ process.exit(0);
28
+ }
29
+
30
+ console.log(`[New Version Available] v${versionInfo.latest}`);
31
+ console.log(`Current version: v${versionInfo.current}\n`);
32
+
33
+ console.log(`Update instructions:`);
34
+ console.log(` cd CLI-WeChat-Bridge`);
35
+ console.log(` git pull`);
36
+ console.log(` bun install`);
37
+ console.log(` npm install -g .\n`);
38
+
39
+ console.log(`For more information:`);
40
+ console.log(` https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases`);
41
+ }
42
+
43
+ main().catch((error) => {
44
+ console.error(`Error checking for updates: ${error.message}`);
45
+ process.exit(1);
46
+ });
@@ -0,0 +1,151 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import type net from "node:net";
4
+
5
+ import {
6
+ ensureWorkspaceChannelDir,
7
+ getWorkspaceChannelPaths,
8
+ } from "../wechat/channel-config.ts";
9
+ import type { BridgeAdapterState, BridgeEvent } from "../bridge/bridge-types.ts";
10
+
11
+ export type CodexPanelCommand =
12
+ | { command: "send_input"; text: string }
13
+ | { command: "list_resume_sessions"; limit?: number }
14
+ | { command: "list_resume_threads"; limit?: number }
15
+ | { command: "resume_session"; sessionId: string }
16
+ | { command: "resume_thread"; threadId: string }
17
+ | { command: "interrupt" }
18
+ | { command: "reset" }
19
+ | { command: "dispose" }
20
+ | { command: "resolve_approval"; action: "confirm" | "deny" };
21
+
22
+ export type CodexPanelEndpoint = {
23
+ instanceId: string;
24
+ port: number;
25
+ token: string;
26
+ cwd: string;
27
+ command: string;
28
+ profile?: string;
29
+ sharedThreadId?: string;
30
+ startedAt: string;
31
+ };
32
+
33
+ export type CodexPanelMessage =
34
+ | {
35
+ type: "hello";
36
+ token: string;
37
+ panelPid: number;
38
+ }
39
+ | {
40
+ type: "hello_ack";
41
+ }
42
+ | {
43
+ type: "request";
44
+ id: string;
45
+ payload: CodexPanelCommand;
46
+ }
47
+ | {
48
+ type: "response";
49
+ id: string;
50
+ ok: boolean;
51
+ result?: unknown;
52
+ error?: string;
53
+ }
54
+ | {
55
+ type: "event";
56
+ event: BridgeEvent;
57
+ }
58
+ | {
59
+ type: "state";
60
+ state: BridgeAdapterState;
61
+ };
62
+
63
+ export function buildCodexPanelToken(): string {
64
+ return crypto.randomBytes(18).toString("hex");
65
+ }
66
+
67
+ export function writeCodexPanelEndpoint(endpoint: CodexPanelEndpoint): void {
68
+ const { endpointFile } = ensureWorkspaceChannelDir(endpoint.cwd);
69
+ fs.writeFileSync(
70
+ endpointFile,
71
+ JSON.stringify(endpoint, null, 2),
72
+ "utf8",
73
+ );
74
+ }
75
+
76
+ export function readCodexPanelEndpoint(cwd: string): CodexPanelEndpoint | null {
77
+ try {
78
+ const { endpointFile } = getWorkspaceChannelPaths(cwd);
79
+ if (!fs.existsSync(endpointFile)) {
80
+ return null;
81
+ }
82
+ return JSON.parse(
83
+ fs.readFileSync(endpointFile, "utf8"),
84
+ ) as CodexPanelEndpoint;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ export function clearCodexPanelEndpoint(cwd: string, instanceId?: string): void {
91
+ try {
92
+ const { endpointFile } = getWorkspaceChannelPaths(cwd);
93
+ if (!fs.existsSync(endpointFile)) {
94
+ return;
95
+ }
96
+
97
+ if (!instanceId) {
98
+ fs.rmSync(endpointFile, { force: true });
99
+ return;
100
+ }
101
+
102
+ const endpoint = readCodexPanelEndpoint(cwd);
103
+ if (!endpoint || endpoint.instanceId === instanceId) {
104
+ fs.rmSync(endpointFile, { force: true });
105
+ }
106
+ } catch {
107
+ // Best effort cleanup.
108
+ }
109
+ }
110
+
111
+ export function sendCodexPanelMessage(
112
+ socket: net.Socket,
113
+ message: CodexPanelMessage,
114
+ ): void {
115
+ socket.write(`${JSON.stringify(message)}\n`);
116
+ }
117
+
118
+ export function attachCodexPanelMessageListener(
119
+ socket: net.Socket,
120
+ onMessage: (message: CodexPanelMessage) => void,
121
+ ): () => void {
122
+ let buffer = "";
123
+ const onData = (chunk: string | Buffer) => {
124
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
125
+
126
+ while (true) {
127
+ const newlineIndex = buffer.indexOf("\n");
128
+ if (newlineIndex < 0) {
129
+ return;
130
+ }
131
+
132
+ const line = buffer.slice(0, newlineIndex).trim();
133
+ buffer = buffer.slice(newlineIndex + 1);
134
+ if (!line) {
135
+ continue;
136
+ }
137
+
138
+ try {
139
+ onMessage(JSON.parse(line) as CodexPanelMessage);
140
+ } catch {
141
+ // Ignore malformed local IPC frames.
142
+ }
143
+ }
144
+ };
145
+
146
+ socket.setEncoding("utf8");
147
+ socket.on("data", onData);
148
+ return () => {
149
+ socket.off("data", onData);
150
+ };
151
+ }
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+
6
+ import { createBridgeAdapter } from "../bridge/bridge-adapters.ts";
7
+ import {
8
+ attachCodexPanelMessageListener,
9
+ readCodexPanelEndpoint,
10
+ sendCodexPanelMessage,
11
+ type CodexPanelMessage,
12
+ } from "./codex-panel-link.ts";
13
+ import { migrateLegacyChannelFiles } from "../wechat/channel-config.ts";
14
+
15
+ function log(message: string): void {
16
+ process.stderr.write(`[codex-panel] ${message}\n`);
17
+ }
18
+
19
+ type CodexPanelCliOptions = {
20
+ cwd: string;
21
+ };
22
+
23
+ function parseCliArgs(argv: string[]): CodexPanelCliOptions {
24
+ let cwd = process.cwd();
25
+
26
+ for (let i = 0; i < argv.length; i += 1) {
27
+ const arg = argv[i];
28
+ const next = argv[i + 1];
29
+
30
+ if (arg === "--help" || arg === "-h") {
31
+ process.stdout.write(
32
+ [
33
+ "Usage: wechat-codex [--cwd <path>]",
34
+ "",
35
+ 'Starts the visible Codex panel and connects it to the running "wechat-bridge-codex" instance for the current directory.',
36
+ "",
37
+ ].join("\n"),
38
+ );
39
+ process.exit(0);
40
+ }
41
+
42
+ if (arg === "--cwd") {
43
+ if (!next) {
44
+ throw new Error("--cwd requires a value");
45
+ }
46
+ cwd = path.resolve(next);
47
+ i += 1;
48
+ continue;
49
+ }
50
+
51
+ throw new Error(`Unknown argument: ${arg}`);
52
+ }
53
+
54
+ return { cwd };
55
+ }
56
+
57
+ async function main(): Promise<void> {
58
+ migrateLegacyChannelFiles(log);
59
+ const options = parseCliArgs(process.argv.slice(2));
60
+
61
+ const endpoint = readCodexPanelEndpoint(options.cwd);
62
+ if (!endpoint) {
63
+ throw new Error(
64
+ `No active Codex bridge endpoint was found for ${options.cwd}. Start "wechat-bridge-codex" in that directory first.`,
65
+ );
66
+ }
67
+
68
+ const socket = await new Promise<net.Socket>((resolve, reject) => {
69
+ const nextSocket = net.connect({
70
+ host: "127.0.0.1",
71
+ port: endpoint.port,
72
+ });
73
+
74
+ nextSocket.once("connect", () => resolve(nextSocket));
75
+ nextSocket.once("error", (error) => reject(error));
76
+ });
77
+
78
+ socket.setNoDelay(true);
79
+
80
+ const adapter = createBridgeAdapter({
81
+ kind: "codex",
82
+ command: endpoint.command,
83
+ cwd: endpoint.cwd,
84
+ profile: endpoint.profile,
85
+ initialSharedThreadId: endpoint.sharedThreadId,
86
+ renderMode: "panel",
87
+ });
88
+
89
+ let shuttingDown = false;
90
+ let helloAcknowledged = false;
91
+ let detachListener: (() => void) | null = null;
92
+
93
+ const publishState = () => {
94
+ sendCodexPanelMessage(socket, {
95
+ type: "state",
96
+ state: adapter.getState(),
97
+ });
98
+ };
99
+
100
+ const sendResponse = (id: string, ok: boolean, result?: unknown, error?: string) => {
101
+ sendCodexPanelMessage(socket, {
102
+ type: "response",
103
+ id,
104
+ ok,
105
+ result,
106
+ error,
107
+ });
108
+ };
109
+
110
+ const closePanel = async (exitCode = 0) => {
111
+ if (shuttingDown) {
112
+ return;
113
+ }
114
+ shuttingDown = true;
115
+
116
+ detachListener?.();
117
+ detachListener = null;
118
+ try {
119
+ await adapter.dispose();
120
+ } catch {
121
+ // Best effort cleanup.
122
+ }
123
+ try {
124
+ socket.end();
125
+ socket.destroy();
126
+ } catch {
127
+ // Best effort cleanup.
128
+ }
129
+ process.exit(exitCode);
130
+ };
131
+
132
+ adapter.setEventSink((event) => {
133
+ sendCodexPanelMessage(socket, {
134
+ type: "event",
135
+ event,
136
+ });
137
+ publishState();
138
+ });
139
+
140
+ detachListener = attachCodexPanelMessageListener(socket, (message: CodexPanelMessage) => {
141
+ if (!helloAcknowledged) {
142
+ if (message.type === "hello_ack") {
143
+ helloAcknowledged = true;
144
+ }
145
+ return;
146
+ }
147
+
148
+ if (message.type !== "request") {
149
+ return;
150
+ }
151
+
152
+ void (async () => {
153
+ try {
154
+ switch (message.payload.command) {
155
+ case "send_input":
156
+ await adapter.sendInput(message.payload.text);
157
+ sendResponse(message.id, true);
158
+ break;
159
+ case "list_resume_sessions":
160
+ case "list_resume_threads":
161
+ sendResponse(
162
+ message.id,
163
+ true,
164
+ await adapter.listResumeSessions(message.payload.limit),
165
+ );
166
+ break;
167
+ case "resume_session":
168
+ await adapter.resumeSession(message.payload.sessionId);
169
+ publishState();
170
+ sendResponse(message.id, true);
171
+ break;
172
+ case "resume_thread":
173
+ await adapter.resumeSession(message.payload.threadId);
174
+ publishState();
175
+ sendResponse(message.id, true);
176
+ break;
177
+ case "interrupt":
178
+ sendResponse(message.id, true, await adapter.interrupt());
179
+ break;
180
+ case "reset":
181
+ await adapter.reset();
182
+ publishState();
183
+ sendResponse(message.id, true);
184
+ break;
185
+ case "resolve_approval":
186
+ sendResponse(
187
+ message.id,
188
+ true,
189
+ await adapter.resolveApproval(message.payload.action),
190
+ );
191
+ break;
192
+ case "dispose":
193
+ sendResponse(message.id, true);
194
+ await closePanel(0);
195
+ break;
196
+ }
197
+ } catch (error) {
198
+ const text = error instanceof Error ? error.message : String(error);
199
+ sendResponse(message.id, false, undefined, text);
200
+ }
201
+ })();
202
+ });
203
+
204
+ socket.once("close", () => {
205
+ void closePanel(0);
206
+ });
207
+ socket.once("error", () => {
208
+ void closePanel(1);
209
+ });
210
+
211
+ sendCodexPanelMessage(socket, {
212
+ type: "hello",
213
+ token: endpoint.token,
214
+ panelPid: process.pid,
215
+ });
216
+
217
+ await adapter.start();
218
+ publishState();
219
+ log(`Connected to bridge ${endpoint.instanceId}.`);
220
+ }
221
+
222
+ main().catch((error) => {
223
+ log(error instanceof Error ? error.message : String(error));
224
+ process.exit(1);
225
+ });
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ assertNoReservedExtraCliArgs,
8
+ buildCliEnvironment,
9
+ buildCodexCliArgs,
10
+ resolveSpawnTarget,
11
+ } from "../bridge/bridge-adapters.shared.ts";
12
+ import {
13
+ clearLocalCompanionOccupancy,
14
+ readLocalCompanionEndpoint,
15
+ updateLocalCompanionOccupancy,
16
+ type LocalCompanionEndpoint,
17
+ } from "./local-companion-link.ts";
18
+ import { migrateLegacyChannelFiles } from "../wechat/channel-config.ts";
19
+ import { CODEX_REMOTE_AUTH_TOKEN_ENV } from "../runtime/runtime-types.ts";
20
+
21
+ type CodexRemoteClientCliOptions = {
22
+ cwd: string;
23
+ cliArgs: string[];
24
+ };
25
+
26
+ function log(message: string): void {
27
+ process.stderr.write(`[codex-remote-client] ${message}\n`);
28
+ }
29
+
30
+ export function parseCliArgs(argv: string[]): CodexRemoteClientCliOptions {
31
+ let cwd = process.cwd();
32
+ const cliArgs: string[] = [];
33
+
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const arg = argv[i];
36
+ const next = argv[i + 1];
37
+
38
+ if (arg === "--help" || arg === "-h") {
39
+ process.stdout.write(
40
+ [
41
+ "Usage: wechat-codex [--cwd <path>] [...codex args]",
42
+ "",
43
+ 'Starts the visible native Codex client and connects it to the running "wechat-bridge-codex" instance for the current directory.',
44
+ "Unknown arguments are forwarded to the Codex client.",
45
+ "",
46
+ ].join("\n"),
47
+ );
48
+ process.exit(0);
49
+ }
50
+
51
+ if (arg === "--cwd") {
52
+ if (!next) {
53
+ throw new Error("--cwd requires a value");
54
+ }
55
+ cwd = path.resolve(next);
56
+ i += 1;
57
+ continue;
58
+ }
59
+
60
+ cliArgs.push(arg);
61
+ }
62
+
63
+ return { cwd, cliArgs };
64
+ }
65
+
66
+ export function readCodexRuntimeEndpoint(cwd: string): LocalCompanionEndpoint {
67
+ const endpoint = readLocalCompanionEndpoint(cwd);
68
+ if (!endpoint || endpoint.kind !== "codex") {
69
+ throw new Error(
70
+ `No active Codex bridge endpoint was found for ${cwd}. Start "wechat-bridge-codex" in that directory first.`,
71
+ );
72
+ }
73
+
74
+ if (endpoint.runtimeKind !== "codex_runtime_host" || (!endpoint.serverUrl && !endpoint.serverPort)) {
75
+ throw new Error(
76
+ `The running Codex bridge for ${cwd} is using an older local companion protocol. Restart "wechat-bridge-codex" in that directory first.`,
77
+ );
78
+ }
79
+
80
+ return endpoint;
81
+ }
82
+
83
+ export function buildRemoteCodexClientArgs(
84
+ endpoint: LocalCompanionEndpoint,
85
+ options: { extraCliArgs?: string[] } = {},
86
+ ): string[] {
87
+ const extraCliArgs = options.extraCliArgs ?? [];
88
+ assertNoReservedExtraCliArgs(
89
+ extraCliArgs,
90
+ ["--remote", "--remote-auth-token-env"],
91
+ "Codex remote connection",
92
+ );
93
+ const remoteUrl = endpoint.serverUrl ?? `ws://127.0.0.1:${endpoint.serverPort ?? endpoint.port}`;
94
+ const args = buildCodexCliArgs(remoteUrl, {
95
+ profile: endpoint.profile,
96
+ resumeThreadId: endpoint.sharedThreadId,
97
+ });
98
+ const tokenEnvName = endpoint.remoteAuthTokenEnv ?? CODEX_REMOTE_AUTH_TOKEN_ENV;
99
+ return [...args, "--remote-auth-token-env", tokenEnvName, ...extraCliArgs];
100
+ }
101
+
102
+ export function buildRemoteCodexClientEnv(
103
+ endpoint: LocalCompanionEndpoint,
104
+ env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
105
+ ): Record<string, string> {
106
+ const tokenEnvName = endpoint.remoteAuthTokenEnv ?? CODEX_REMOTE_AUTH_TOKEN_ENV;
107
+ const nextEnv = buildCliEnvironment("codex", { env });
108
+ nextEnv[tokenEnvName] = endpoint.token;
109
+ return nextEnv;
110
+ }
111
+
112
+ export async function runCodexRemoteClientFromEndpoint(
113
+ endpoint: LocalCompanionEndpoint,
114
+ options: { extraCliArgs?: string[] } = {},
115
+ ): Promise<number> {
116
+ const spawnTarget = resolveSpawnTarget(endpoint.command, "codex");
117
+ const args = buildRemoteCodexClientArgs(endpoint, {
118
+ extraCliArgs: options.extraCliArgs,
119
+ });
120
+ const env = buildRemoteCodexClientEnv(endpoint);
121
+
122
+ return await new Promise<number>((resolve, reject) => {
123
+ const child = spawn(
124
+ spawnTarget.file,
125
+ [...spawnTarget.args, ...args],
126
+ {
127
+ cwd: endpoint.cwd,
128
+ env,
129
+ stdio: "inherit",
130
+ windowsHide: false,
131
+ },
132
+ );
133
+
134
+ if (typeof child.pid === "number") {
135
+ updateLocalCompanionOccupancy(endpoint.cwd, {
136
+ companionPid: child.pid,
137
+ companionConnectedAt: new Date().toISOString(),
138
+ }, endpoint.instanceId);
139
+ }
140
+
141
+ child.once("error", (error) => {
142
+ clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId);
143
+ reject(error);
144
+ });
145
+ child.once("exit", (code, signal) => {
146
+ clearLocalCompanionOccupancy(endpoint.cwd, endpoint.instanceId);
147
+ if (signal) {
148
+ process.kill(process.pid, signal);
149
+ return;
150
+ }
151
+ resolve(code ?? 0);
152
+ });
153
+ });
154
+ }
155
+
156
+ export async function runCodexRemoteClient(
157
+ options: CodexRemoteClientCliOptions,
158
+ ): Promise<number> {
159
+ const endpoint = readCodexRuntimeEndpoint(options.cwd);
160
+ return await runCodexRemoteClientFromEndpoint(endpoint, {
161
+ extraCliArgs: options.cliArgs,
162
+ });
163
+ }
164
+
165
+ async function main(): Promise<void> {
166
+ migrateLegacyChannelFiles(log);
167
+ const options = parseCliArgs(process.argv.slice(2));
168
+ const exitCode = await runCodexRemoteClient(options);
169
+ process.exit(exitCode);
170
+ }
171
+
172
+ const isDirectRun = Boolean((import.meta as ImportMeta & { main?: boolean }).main);
173
+ if (isDirectRun) {
174
+ main().catch((error) => {
175
+ log(error instanceof Error ? error.message : String(error));
176
+ process.exit(1);
177
+ });
178
+ }