cli-wechat-bridge 1.0.5

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 (54) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +637 -0
  3. package/bin/_run-entry.mjs +35 -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-daemon.mjs +5 -0
  15. package/bin/wechat-opencode-start.mjs +5 -0
  16. package/bin/wechat-opencode.mjs +5 -0
  17. package/bin/wechat-setup.mjs +5 -0
  18. package/dist/bridge/bridge-adapter-common.js +95 -0
  19. package/dist/bridge/bridge-adapters.claude.js +829 -0
  20. package/dist/bridge/bridge-adapters.codex.js +2228 -0
  21. package/dist/bridge/bridge-adapters.core.js +717 -0
  22. package/dist/bridge/bridge-adapters.js +26 -0
  23. package/dist/bridge/bridge-adapters.opencode.js +2129 -0
  24. package/dist/bridge/bridge-adapters.shared.js +1005 -0
  25. package/dist/bridge/bridge-adapters.shell.js +363 -0
  26. package/dist/bridge/bridge-controller.js +48 -0
  27. package/dist/bridge/bridge-final-reply.js +46 -0
  28. package/dist/bridge/bridge-process-reaper.js +348 -0
  29. package/dist/bridge/bridge-state.js +362 -0
  30. package/dist/bridge/bridge-types.js +1 -0
  31. package/dist/bridge/bridge-utils.js +1240 -0
  32. package/dist/bridge/claude-hook.js +82 -0
  33. package/dist/bridge/claude-hooks.js +267 -0
  34. package/dist/bridge/wechat-bridge.js +1026 -0
  35. package/dist/commands/check-update.js +30 -0
  36. package/dist/companion/codex-panel-link.js +72 -0
  37. package/dist/companion/codex-panel.js +179 -0
  38. package/dist/companion/codex-remote-client.js +124 -0
  39. package/dist/companion/local-companion-link.js +240 -0
  40. package/dist/companion/local-companion-start.js +420 -0
  41. package/dist/companion/local-companion.js +424 -0
  42. package/dist/daemon/daemon-link.js +175 -0
  43. package/dist/daemon/wechat-daemon.js +1202 -0
  44. package/dist/media/media-types.js +1 -0
  45. package/dist/runtime/create-runtime-host.js +12 -0
  46. package/dist/runtime/legacy-adapter-runtime.js +46 -0
  47. package/dist/runtime/runtime-types.js +5 -0
  48. package/dist/utils/version-checker.js +161 -0
  49. package/dist/wechat/channel-config.js +196 -0
  50. package/dist/wechat/setup.js +283 -0
  51. package/dist/wechat/standalone-bot.js +355 -0
  52. package/dist/wechat/wechat-channel.js +492 -0
  53. package/dist/wechat/wechat-transport.js +1213 -0
  54. package/package.json +101 -0
@@ -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,46 @@
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 createSession() {
23
+ if (!this.adapter.createSession) {
24
+ throw new Error(`/${this.adapter.getState().kind} does not support creating sessions from WeChat.`);
25
+ }
26
+ await this.adapter.createSession();
27
+ }
28
+ async interrupt() {
29
+ return await this.adapter.interrupt();
30
+ }
31
+ async reset() {
32
+ await this.adapter.reset();
33
+ }
34
+ async resolveApproval(action) {
35
+ return await this.adapter.resolveApproval(action);
36
+ }
37
+ async submitUserInput(answers) {
38
+ return await this.adapter.submitUserInput(answers);
39
+ }
40
+ async dispose() {
41
+ await this.adapter.dispose();
42
+ }
43
+ getState() {
44
+ return this.adapter.getState();
45
+ }
46
+ }
@@ -0,0 +1,5 @@
1
+ export const LOCAL_CLIENT_PROTOCOL_VERSION = 2;
2
+ export const CODEX_REMOTE_AUTH_TOKEN_ENV = "WECHAT_BRIDGE_CODEX_REMOTE_AUTH_TOKEN";
3
+ export function hasLocalClientEndpointProvider(runtime) {
4
+ return typeof runtime.getLocalClientEndpoint === "function";
5
+ }
@@ -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] ?? null;
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
+ }
@@ -0,0 +1,196 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
7
+ const PROJECT_DIR = path.resolve(MODULE_DIR, "..", "..");
8
+ export const DEFAULT_BASE_URL = process.env.WECHAT_ILINK_BASE_URL?.trim() || "https://ilinkai.weixin.qq.com";
9
+ export const BOT_TYPE = "3";
10
+ export function resolveChannelDataDir(env = process.env, homeDir = os.homedir()) {
11
+ const configured = env.CLI_BRIDGE_DATA_DIR?.trim();
12
+ return configured ? path.resolve(configured) : path.join(homeDir, ".cli-bridge");
13
+ }
14
+ export const CHANNEL_DATA_DIR = resolveChannelDataDir();
15
+ export const CREDENTIALS_FILE = path.join(CHANNEL_DATA_DIR, "account.json");
16
+ export const SYNC_BUF_FILE = path.join(CHANNEL_DATA_DIR, "sync_buf.txt");
17
+ export const CONTEXT_CACHE_FILE = path.join(CHANNEL_DATA_DIR, "context_tokens.json");
18
+ export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
19
+ export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
20
+ export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
21
+ export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
22
+ export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
23
+ export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
24
+ export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
25
+ export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
26
+ const LEGACY_GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
27
+ const LEGACY_REPO_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
28
+ const LEGACY_ENV_CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
29
+ ? path.resolve(process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR.trim())
30
+ : "";
31
+ const LEGACY_CHANNEL_SOURCE_DIRS = [
32
+ LEGACY_ENV_CHANNEL_DATA_DIR,
33
+ LEGACY_GLOBAL_CHANNEL_DATA_DIR,
34
+ LEGACY_REPO_CHANNEL_DATA_DIR,
35
+ ].filter(Boolean);
36
+ const LEGACY_CHANNEL_SOURCES = LEGACY_CHANNEL_SOURCE_DIRS.map((dataDir) => ({
37
+ dataDir,
38
+ }));
39
+ const LEGACY_MIGRATION_ITEMS = [
40
+ {
41
+ label: "credentials",
42
+ sourceName: "account.json",
43
+ targetName: "account.json",
44
+ kind: "file",
45
+ },
46
+ {
47
+ label: "sync state",
48
+ sourceName: "sync_buf.txt",
49
+ targetName: "sync_buf.txt",
50
+ kind: "file",
51
+ },
52
+ {
53
+ label: "context tokens",
54
+ sourceName: "context_tokens.json",
55
+ targetName: "context_tokens.json",
56
+ kind: "file",
57
+ },
58
+ {
59
+ label: "update check cache",
60
+ sourceName: "update-check.json",
61
+ targetName: "update-check.json",
62
+ kind: "file",
63
+ },
64
+ {
65
+ label: "workspace state",
66
+ sourceName: "workspaces",
67
+ targetName: "workspaces",
68
+ kind: "directory",
69
+ },
70
+ {
71
+ label: "inbound attachments",
72
+ sourceName: "inbound-attachments",
73
+ targetName: "inbound-attachments",
74
+ kind: "directory",
75
+ },
76
+ {
77
+ label: "legacy bridge log",
78
+ sourceName: "bridge.log",
79
+ targetName: "legacy-bridge.log",
80
+ kind: "file",
81
+ },
82
+ ];
83
+ export function ensureChannelDataDir() {
84
+ fs.mkdirSync(CHANNEL_DATA_DIR, { recursive: true });
85
+ }
86
+ export function normalizeWorkspacePath(cwd) {
87
+ return path.resolve(cwd);
88
+ }
89
+ function buildComparableWorkspacePath(cwd) {
90
+ const normalized = normalizeWorkspacePath(cwd);
91
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
92
+ }
93
+ function sanitizeWorkspaceSegment(value) {
94
+ const sanitized = value
95
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
96
+ .replace(/^-+|-+$/g, "")
97
+ .slice(0, 40);
98
+ return sanitized || "workspace";
99
+ }
100
+ export function buildWorkspaceKey(cwd) {
101
+ const normalized = normalizeWorkspacePath(cwd);
102
+ const digest = crypto
103
+ .createHash("sha256")
104
+ .update(buildComparableWorkspacePath(normalized))
105
+ .digest("hex")
106
+ .slice(0, 12);
107
+ const label = sanitizeWorkspaceSegment(path.basename(normalized));
108
+ return `${label}-${digest}`;
109
+ }
110
+ export function getWorkspaceChannelPaths(cwd) {
111
+ const workspaceDir = path.join(WORKSPACES_DIR, buildWorkspaceKey(cwd));
112
+ return {
113
+ workspaceDir,
114
+ stateFile: path.join(workspaceDir, "bridge-state.json"),
115
+ endpointFile: path.join(workspaceDir, "codex-panel-endpoint.json"),
116
+ };
117
+ }
118
+ export function getWorkspaceAdapterEndpointFile(cwd, adapter) {
119
+ return path.join(getWorkspaceChannelPaths(cwd).workspaceDir, `${adapter}-companion-endpoint.json`);
120
+ }
121
+ export function ensureWorkspaceChannelDir(cwd) {
122
+ ensureChannelDataDir();
123
+ const paths = getWorkspaceChannelPaths(cwd);
124
+ fs.mkdirSync(paths.workspaceDir, { recursive: true });
125
+ return paths;
126
+ }
127
+ function isSamePath(left, right) {
128
+ const normalizedLeft = path.resolve(left);
129
+ const normalizedRight = path.resolve(right);
130
+ return process.platform === "win32"
131
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
132
+ : normalizedLeft === normalizedRight;
133
+ }
134
+ function legacySourceHasMigratableData(source) {
135
+ return LEGACY_MIGRATION_ITEMS.some((item) => {
136
+ const sourcePath = path.join(source.dataDir, item.sourceName);
137
+ if (!fs.existsSync(sourcePath)) {
138
+ return false;
139
+ }
140
+ try {
141
+ const stat = fs.statSync(sourcePath);
142
+ return item.kind === "directory" ? stat.isDirectory() : stat.isFile();
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ });
148
+ }
149
+ function findLegacyChannelSource(channelDataDir = CHANNEL_DATA_DIR, legacySources = LEGACY_CHANNEL_SOURCES) {
150
+ return (legacySources.find((source) => !isSamePath(source.dataDir, channelDataDir) &&
151
+ legacySourceHasMigratableData(source)) ?? null);
152
+ }
153
+ export function migrateLegacyChannelFiles(log, options = {}) {
154
+ const channelDataDir = options.channelDataDir ?? CHANNEL_DATA_DIR;
155
+ const legacySources = (options.legacyDataDirs ?? LEGACY_CHANNEL_SOURCE_DIRS).map((dataDir) => ({ dataDir }));
156
+ const migrated = [];
157
+ const skippedExisting = [];
158
+ const legacySource = findLegacyChannelSource(channelDataDir, legacySources);
159
+ if (!legacySource) {
160
+ return migrated;
161
+ }
162
+ fs.mkdirSync(channelDataDir, { recursive: true });
163
+ for (const item of LEGACY_MIGRATION_ITEMS) {
164
+ const sourcePath = path.join(legacySource.dataDir, item.sourceName);
165
+ const targetPath = path.join(channelDataDir, item.targetName);
166
+ if (!fs.existsSync(sourcePath)) {
167
+ continue;
168
+ }
169
+ if (fs.existsSync(targetPath)) {
170
+ skippedExisting.push(item.label);
171
+ continue;
172
+ }
173
+ const stat = fs.statSync(sourcePath);
174
+ if (item.kind === "directory") {
175
+ if (!stat.isDirectory()) {
176
+ continue;
177
+ }
178
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
179
+ }
180
+ else {
181
+ if (!stat.isFile()) {
182
+ continue;
183
+ }
184
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
185
+ fs.copyFileSync(sourcePath, targetPath);
186
+ }
187
+ migrated.push(item.label);
188
+ }
189
+ if (migrated.length && log) {
190
+ const skippedText = skippedExisting.length
191
+ ? ` Skipped existing: ${skippedExisting.join(", ")}.`
192
+ : "";
193
+ log(`Migrated legacy ${migrated.join(", ")} from ${legacySource.dataDir} to ${channelDataDir}.${skippedText}`);
194
+ }
195
+ return migrated;
196
+ }