@unlinearity/cli-wechat-bridge 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,15 @@
1
1
  import crypto from "node:crypto";
2
- import { createCipheriv } from "node:crypto";
2
+ import { createCipheriv, createDecipheriv } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import { CONTEXT_CACHE_FILE, CREDENTIALS_FILE, ensureChannelDataDir, INBOUND_MESSAGE_CLAIMS_DIR, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
5
+ import { CONTEXT_CACHE_FILE, CREDENTIALS_FILE, ensureChannelDataDir, INBOUND_ATTACHMENTS_DIR, INBOUND_MESSAGE_CLAIMS_DIR, migrateLegacyChannelFiles, SYNC_BUF_FILE, } from "./channel-config.js";
6
6
  export const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
7
7
  const CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
8
8
  const CHANNEL_VERSION = "0.3.0";
9
9
  const RECENT_MESSAGE_CACHE_SIZE = 500;
10
10
  const BYTES_PER_MB = 1024 * 1024;
11
11
  const SEND_TIMEOUT_MS = 15_000;
12
+ const INBOUND_DOWNLOAD_TIMEOUT_MS = 30_000;
12
13
  const CDN_MAX_RETRIES = 3;
13
14
  const ERROR_CAUSE_DEPTH_LIMIT = 4;
14
15
  const INBOUND_MESSAGE_CLAIM_TTL_MS = 10 * 60 * 1000;
@@ -29,6 +30,26 @@ export function isWechatSyncSessionTimeout(response) {
29
30
  return (response.errcode === SYNC_SESSION_TIMEOUT_ERRCODE &&
30
31
  /session timeout/i.test(response.errmsg ?? ""));
31
32
  }
33
+ export class WechatApiResponseError extends Error {
34
+ endpoint;
35
+ ret;
36
+ errcode;
37
+ errmsg;
38
+ constructor(params) {
39
+ const errmsg = params.errmsg ?? "";
40
+ super(`${params.endpoint} failed: ret=${params.ret} errcode=${params.errcode} errmsg=${errmsg}`);
41
+ this.name = "WechatApiResponseError";
42
+ this.endpoint = params.endpoint;
43
+ this.ret = params.ret;
44
+ this.errcode = params.errcode;
45
+ this.errmsg = errmsg;
46
+ }
47
+ }
48
+ export function isWechatContextTokenStaleError(error) {
49
+ return (error instanceof WechatApiResponseError &&
50
+ error.endpoint === "sendmessage" &&
51
+ error.ret === -2);
52
+ }
32
53
  const DEFAULT_MEDIA_UPLOAD_LIMIT_MB = {
33
54
  image: 20,
34
55
  file: 50,
@@ -41,6 +62,14 @@ const MEDIA_UPLOAD_LIMIT_ENV_KEYS = {
41
62
  voice: "WECHAT_MAX_VOICE_MB",
42
63
  video: "WECHAT_MAX_VIDEO_MB",
43
64
  };
65
+ const DEFAULT_MEDIA_INBOUND_LIMIT_MB = {
66
+ image: 20,
67
+ file: 50,
68
+ };
69
+ const MEDIA_INBOUND_LIMIT_ENV_KEYS = {
70
+ image: "WECHAT_MAX_INBOUND_IMAGE_MB",
71
+ file: "WECHAT_MAX_INBOUND_FILE_MB",
72
+ };
44
73
  const RETRYABLE_HTTP_STATUS_CODES = new Set([408, 425, 429]);
45
74
  const RETRYABLE_NETWORK_ERROR_CODES = new Set([
46
75
  "ECONNABORTED",
@@ -129,7 +158,7 @@ export function assertWechatApiResponseOk(endpoint, raw) {
129
158
  readStringResponseField(response, "message") ??
130
159
  readStringResponseField(response, "msg") ??
131
160
  "";
132
- throw new Error(`${endpoint} failed: ret=${ret} errcode=${errcode} errmsg=${errmsg}`);
161
+ throw new WechatApiResponseError({ endpoint, ret, errcode, errmsg });
133
162
  }
134
163
  function describeErrorNode(value) {
135
164
  if (value instanceof Error) {
@@ -325,9 +354,34 @@ function encryptAesEcb(plaintext, key) {
325
354
  const cipher = createCipheriv("aes-128-ecb", key, null);
326
355
  return Buffer.concat([cipher.update(plaintext), cipher.final()]);
327
356
  }
357
+ function decryptAesEcb(ciphertext, key) {
358
+ const decipher = createDecipheriv("aes-128-ecb", key, null);
359
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
360
+ }
328
361
  function aesEcbPaddedSize(plaintextSize) {
329
362
  return Math.ceil((plaintextSize + 1) / 16) * 16;
330
363
  }
364
+ function isHexAesKey(value) {
365
+ return /^[a-f0-9]{32}$/i.test(value);
366
+ }
367
+ export function decodeInboundMediaAesKey(value) {
368
+ const trimmed = value.trim();
369
+ if (isHexAesKey(trimmed)) {
370
+ return Buffer.from(trimmed, "hex");
371
+ }
372
+ const decoded = Buffer.from(trimmed, "base64");
373
+ if (decoded.length === 16) {
374
+ return decoded;
375
+ }
376
+ const decodedText = decoded.toString("utf8").trim();
377
+ if (isHexAesKey(decodedText)) {
378
+ return Buffer.from(decodedText, "hex");
379
+ }
380
+ throw new Error("Unsupported inbound media aes key format.");
381
+ }
382
+ export function decryptInboundMediaPayload(ciphertext, aesKey) {
383
+ return decryptAesEcb(ciphertext, decodeInboundMediaAesKey(aesKey));
384
+ }
331
385
  export function formatByteSize(bytes) {
332
386
  if (!Number.isFinite(bytes) || bytes < 0) {
333
387
  return "0 B";
@@ -350,6 +404,14 @@ export function resolveMediaUploadLimitBytes(label, env = process.env) {
350
404
  const limitMb = Number.isFinite(parsedMb) && parsedMb > 0 ? parsedMb : fallbackMb;
351
405
  return Math.floor(limitMb * BYTES_PER_MB);
352
406
  }
407
+ export function resolveInboundMediaDownloadLimitBytes(label, env = process.env) {
408
+ const envKey = MEDIA_INBOUND_LIMIT_ENV_KEYS[label];
409
+ const raw = env[envKey];
410
+ const fallbackMb = DEFAULT_MEDIA_INBOUND_LIMIT_MB[label];
411
+ const parsedMb = raw ? Number(raw) : Number.NaN;
412
+ const limitMb = Number.isFinite(parsedMb) && parsedMb > 0 ? parsedMb : fallbackMb;
413
+ return Math.floor(limitMb * BYTES_PER_MB);
414
+ }
353
415
  export function assertMediaUploadSizeAllowed(label, rawsize, env = process.env) {
354
416
  const limitBytes = resolveMediaUploadLimitBytes(label, env);
355
417
  if (rawsize <= limitBytes) {
@@ -359,6 +421,15 @@ export function assertMediaUploadSizeAllowed(label, rawsize, env = process.env)
359
421
  const labelName = label.charAt(0).toUpperCase() + label.slice(1);
360
422
  throw new Error(`${labelName} too large: ${formatByteSize(rawsize)} exceeds ${formatByteSize(limitBytes)} limit. Set ${envKey} to override.`);
361
423
  }
424
+ function assertInboundMediaDownloadSizeAllowed(label, rawsize, env = process.env) {
425
+ const limitBytes = resolveInboundMediaDownloadLimitBytes(label, env);
426
+ if (rawsize <= limitBytes) {
427
+ return;
428
+ }
429
+ const envKey = MEDIA_INBOUND_LIMIT_ENV_KEYS[label];
430
+ const labelName = label.charAt(0).toUpperCase() + label.slice(1);
431
+ throw new Error(`${labelName} too large: ${formatByteSize(rawsize)} exceeds ${formatByteSize(limitBytes)} inbound limit. Set ${envKey} to override.`);
432
+ }
362
433
  function encodeMessageAesKey(aeskey) {
363
434
  return Buffer.from(aeskey.toString("hex")).toString("base64");
364
435
  }
@@ -379,6 +450,32 @@ async function getUploadUrl(account, params) {
379
450
  function buildCdnUploadUrl(cdnBaseUrl, uploadParam, filekey) {
380
451
  return `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
381
452
  }
453
+ export function buildCdnDownloadUrl(cdnBaseUrl, downloadParam) {
454
+ return `${cdnBaseUrl}/download?encrypted_query_param=${encodeURIComponent(downloadParam)}`;
455
+ }
456
+ function isTrustedCdnDownloadUrl(url) {
457
+ const trustedHost = new URL(CDN_BASE_URL).hostname;
458
+ return url.protocol === "https:" && url.hostname === trustedHost;
459
+ }
460
+ function resolveCdnDownloadUrl(media) {
461
+ const fullUrl = media.full_url?.trim();
462
+ if (fullUrl) {
463
+ try {
464
+ const url = new URL(fullUrl);
465
+ if (isTrustedCdnDownloadUrl(url)) {
466
+ return url.toString();
467
+ }
468
+ }
469
+ catch {
470
+ // Fall back to the encrypted query param below.
471
+ }
472
+ }
473
+ const downloadParam = media.encrypt_query_param?.trim();
474
+ if (!downloadParam) {
475
+ throw new Error("Inbound media is missing encrypt_query_param.");
476
+ }
477
+ return buildCdnDownloadUrl(CDN_BASE_URL, downloadParam);
478
+ }
382
479
  async function uploadBufferToCdn(params) {
383
480
  const ciphertext = encryptAesEcb(params.buf, params.aeskey);
384
481
  const cdnUrl = buildCdnUploadUrl(CDN_BASE_URL, params.uploadParam, params.filekey);
@@ -421,6 +518,47 @@ async function uploadBufferToCdn(params) {
421
518
  }
422
519
  return { downloadParam };
423
520
  }
521
+ async function downloadBufferFromCdn(params) {
522
+ const cdnUrl = resolveCdnDownloadUrl(params.media);
523
+ const limitBytes = resolveInboundMediaDownloadLimitBytes(params.kind);
524
+ let lastError;
525
+ for (let attempt = 1; attempt <= CDN_MAX_RETRIES; attempt += 1) {
526
+ const controller = new AbortController();
527
+ const timer = setTimeout(() => controller.abort(), INBOUND_DOWNLOAD_TIMEOUT_MS);
528
+ try {
529
+ const res = await fetch(cdnUrl, {
530
+ method: "GET",
531
+ signal: controller.signal,
532
+ });
533
+ clearTimeout(timer);
534
+ if (res.status >= 400 && res.status < 500) {
535
+ const errMsg = res.headers.get("x-error-message") ?? (await res.text());
536
+ throw new Error(`CDN client error ${res.status}: ${errMsg}`);
537
+ }
538
+ if (res.status !== 200) {
539
+ const errMsg = res.headers.get("x-error-message") ?? `status ${res.status}`;
540
+ throw new Error(`CDN server error: ${errMsg}`);
541
+ }
542
+ const contentLength = Number(res.headers.get("content-length") ?? "");
543
+ if (Number.isFinite(contentLength) && contentLength > aesEcbPaddedSize(limitBytes)) {
544
+ throw new Error(`${params.kind} download too large: ${formatByteSize(contentLength)} encrypted payload exceeds ${formatByteSize(limitBytes)} inbound limit.`);
545
+ }
546
+ return Buffer.from(await res.arrayBuffer());
547
+ }
548
+ catch (err) {
549
+ clearTimeout(timer);
550
+ lastError = err;
551
+ if (err instanceof Error && err.message.includes("client error")) {
552
+ throw err;
553
+ }
554
+ if (attempt >= CDN_MAX_RETRIES) {
555
+ break;
556
+ }
557
+ params.onRetry?.(attempt);
558
+ }
559
+ }
560
+ throw lastError instanceof Error ? lastError : new Error("CDN download failed");
561
+ }
424
562
  function extractReferenceLabel(item) {
425
563
  const ref = item.ref_msg;
426
564
  if (!ref) {
@@ -436,11 +574,53 @@ function extractReferenceLabel(item) {
436
574
  }
437
575
  return parts.length ? `Quoted: ${parts.join(" | ")}` : null;
438
576
  }
439
- function extractTextFromMessage(message) {
577
+ function parseExpectedSize(value) {
578
+ const parsed = typeof value === "number"
579
+ ? value
580
+ : typeof value === "string"
581
+ ? Number(value)
582
+ : Number.NaN;
583
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
584
+ }
585
+ function buildInboundAttachmentDescriptor(kind, item) {
586
+ if (kind === "image") {
587
+ const imageItem = item.image_item;
588
+ const media = imageItem?.media;
589
+ const aesKey = media?.aes_key ?? media?.aeskey ?? imageItem?.aes_key ?? imageItem?.aeskey;
590
+ if (!media || !aesKey?.trim()) {
591
+ return null;
592
+ }
593
+ return {
594
+ kind,
595
+ fileName: imageItem?.file_name?.trim() || "wechat-image.jpg",
596
+ media,
597
+ aesKey,
598
+ expectedSizeBytes: parseExpectedSize(imageItem?.mid_size),
599
+ };
600
+ }
601
+ const fileItem = item.file_item;
602
+ const media = fileItem?.media;
603
+ const aesKey = media?.aes_key ?? media?.aeskey ?? fileItem?.aes_key ?? fileItem?.aeskey;
604
+ if (!media || !aesKey?.trim()) {
605
+ return null;
606
+ }
607
+ return {
608
+ kind,
609
+ fileName: fileItem?.file_name?.trim() || "wechat-file",
610
+ media,
611
+ aesKey,
612
+ expectedSizeBytes: parseExpectedSize(fileItem?.len),
613
+ };
614
+ }
615
+ function formatUnsupportedInboundAttachment(kind) {
616
+ return `[WeChat ${kind} attachment could not be downloaded: missing media metadata]`;
617
+ }
618
+ export function extractInboundMessageContent(message) {
440
619
  if (!message.item_list?.length) {
441
- return "";
620
+ return { text: "", attachments: [] };
442
621
  }
443
622
  const lines = [];
623
+ const attachments = [];
444
624
  for (const item of message.item_list) {
445
625
  const reference = extractReferenceLabel(item);
446
626
  if (reference && !lines.includes(reference)) {
@@ -458,8 +638,29 @@ function extractTextFromMessage(message) {
458
638
  lines.push(transcript);
459
639
  }
460
640
  }
641
+ if (item.type === MSG_ITEM_IMAGE) {
642
+ const attachment = buildInboundAttachmentDescriptor("image", item);
643
+ if (attachment) {
644
+ attachments.push(attachment);
645
+ }
646
+ else {
647
+ lines.push(formatUnsupportedInboundAttachment("image"));
648
+ }
649
+ }
650
+ if (item.type === MSG_ITEM_FILE) {
651
+ const attachment = buildInboundAttachmentDescriptor("file", item);
652
+ if (attachment) {
653
+ attachments.push(attachment);
654
+ }
655
+ else {
656
+ lines.push(formatUnsupportedInboundAttachment("file"));
657
+ }
658
+ }
461
659
  }
462
- return lines.join("\n").trim();
660
+ return {
661
+ text: lines.join("\n").trim(),
662
+ attachments,
663
+ };
463
664
  }
464
665
  function buildMessageKey(message) {
465
666
  return [
@@ -542,15 +743,42 @@ function formatTimestamp(timestampMs) {
542
743
  }
543
744
  return new Date(timestampMs).toISOString();
544
745
  }
746
+ function appendInboundAttachmentFailureText(text, failureLines) {
747
+ if (failureLines.length === 0) {
748
+ return text;
749
+ }
750
+ return [text, ...failureLines].filter(Boolean).join("\n").trim();
751
+ }
752
+ function sanitizeInboundAttachmentFileName(fileName, fallback) {
753
+ const lastSegment = fileName.trim().replace(/\\/g, "/").split("/").pop() ?? "";
754
+ const withoutControlChars = Array.from(lastSegment)
755
+ .map((character) => (character.charCodeAt(0) < 32 ? "_" : character))
756
+ .join("");
757
+ const sanitized = withoutControlChars
758
+ .replace(/[<>:"/\\|?*]+/g, "_")
759
+ .replace(/\s+/g, " ")
760
+ .trim();
761
+ return sanitized.slice(0, 160) || fallback;
762
+ }
763
+ function buildInboundAttachmentFilePath(params) {
764
+ const timestamp = formatTimestamp(params.createdAtMs);
765
+ const day = timestamp.slice(0, 10);
766
+ const directory = path.join(INBOUND_ATTACHMENTS_DIR, day);
767
+ const fallback = params.kind === "image" ? "wechat-image.jpg" : "wechat-file";
768
+ const safeFileName = sanitizeInboundAttachmentFileName(params.fileName, fallback);
769
+ const uniquePrefix = `${timestamp.replace(/[:.]/g, "-")}-${crypto.randomBytes(4).toString("hex")}`;
770
+ return path.join(directory, `${uniquePrefix}-${safeFileName}`);
771
+ }
545
772
  export class WeChatTransport {
546
773
  logger;
547
774
  recentMessageKeys = new Set();
548
775
  recentMessageOrder = [];
549
- contextTokenCache = new Map(Object.entries(readJsonFile(CONTEXT_CACHE_FILE) ?? {}));
776
+ contextTokenCache;
550
777
  syncBuffer = "";
551
778
  constructor(logger) {
552
779
  this.logger = logger;
553
780
  migrateLegacyChannelFiles((message) => this.logger.log(message));
781
+ this.contextTokenCache = new Map(Object.entries(readJsonFile(CONTEXT_CACHE_FILE) ?? {}));
554
782
  this.syncBuffer = this.readSyncBuffer();
555
783
  }
556
784
  getCredentials() {
@@ -572,6 +800,8 @@ export class WeChatTransport {
572
800
  `max_file_mb: ${resolveMediaUploadLimitBytes("file") / BYTES_PER_MB}`,
573
801
  `max_voice_mb: ${resolveMediaUploadLimitBytes("voice") / BYTES_PER_MB}`,
574
802
  `max_video_mb: ${resolveMediaUploadLimitBytes("video") / BYTES_PER_MB}`,
803
+ `max_inbound_image_mb: ${resolveInboundMediaDownloadLimitBytes("image") / BYTES_PER_MB}`,
804
+ `max_inbound_file_mb: ${resolveInboundMediaDownloadLimitBytes("file") / BYTES_PER_MB}`,
575
805
  `account_id: ${account?.accountId ?? "(none)"}`,
576
806
  `user_id: ${account?.userId ?? "(none)"}`,
577
807
  `saved_at: ${account?.savedAt ?? "(none)"}`,
@@ -615,8 +845,8 @@ export class WeChatTransport {
615
845
  if (rawMessage.message_type !== MSG_TYPE_USER) {
616
846
  continue;
617
847
  }
618
- const text = extractTextFromMessage(rawMessage);
619
- if (!text) {
848
+ const extracted = extractInboundMessageContent(rawMessage);
849
+ if (!extracted.text && extracted.attachments.length === 0) {
620
850
  continue;
621
851
  }
622
852
  const messageKey = buildMessageKey(rawMessage);
@@ -636,11 +866,14 @@ export class WeChatTransport {
636
866
  ignoredBacklogCount += 1;
637
867
  continue;
638
868
  }
869
+ const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
870
+ const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
639
871
  messages.push({
640
872
  senderId,
641
873
  sender: normalizeSender(senderId),
642
874
  sessionId: rawMessage.session_id ?? "",
643
875
  text,
876
+ attachments,
644
877
  contextToken: rawMessage.context_token,
645
878
  createdAt: formatTimestamp(rawMessage.create_time_ms),
646
879
  createdAtMs,
@@ -648,6 +881,46 @@ export class WeChatTransport {
648
881
  }
649
882
  return { messages, ignoredBacklogCount };
650
883
  }
884
+ async downloadInboundAttachments(descriptors, rawMessage) {
885
+ const attachments = [];
886
+ const failureLines = [];
887
+ for (const descriptor of descriptors) {
888
+ try {
889
+ const encrypted = await downloadBufferFromCdn({
890
+ media: descriptor.media,
891
+ kind: descriptor.kind,
892
+ onRetry: (attempt) => {
893
+ this.logger.log(`CDN download attempt ${attempt} failed for inbound ${descriptor.kind}, retrying...`);
894
+ },
895
+ });
896
+ const plaintext = decryptInboundMediaPayload(encrypted, descriptor.aesKey);
897
+ assertInboundMediaDownloadSizeAllowed(descriptor.kind, plaintext.length);
898
+ if (descriptor.expectedSizeBytes !== undefined &&
899
+ plaintext.length !== descriptor.expectedSizeBytes) {
900
+ this.logger.log(`Inbound ${descriptor.kind} size differs from metadata: expected=${descriptor.expectedSizeBytes} actual=${plaintext.length}`);
901
+ }
902
+ const filePath = buildInboundAttachmentFilePath({
903
+ kind: descriptor.kind,
904
+ fileName: descriptor.fileName,
905
+ createdAtMs: rawMessage.create_time_ms,
906
+ });
907
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
908
+ fs.writeFileSync(filePath, plaintext);
909
+ attachments.push({
910
+ kind: descriptor.kind,
911
+ path: filePath,
912
+ fileName: path.basename(filePath),
913
+ sizeBytes: plaintext.length,
914
+ });
915
+ }
916
+ catch (error) {
917
+ const message = `Failed to download inbound WeChat ${descriptor.kind} (${descriptor.fileName}): ${describeWechatTransportError(error)}`;
918
+ this.logger.logError(message);
919
+ failureLines.push(`[${message}]`);
920
+ }
921
+ }
922
+ return { attachments, failureLines };
923
+ }
651
924
  async sendText(senderId, text) {
652
925
  const trimmed = text.trim();
653
926
  if (!trimmed) {
@@ -925,6 +1198,15 @@ export class WeChatTransport {
925
1198
  fs.rmSync(CONTEXT_CACHE_FILE, { force: true });
926
1199
  }
927
1200
  }
1201
+ clearCachedContextToken(recipientId) {
1202
+ const normalizedRecipientId = recipientId.trim();
1203
+ if (!normalizedRecipientId || !this.contextTokenCache.has(normalizedRecipientId)) {
1204
+ return false;
1205
+ }
1206
+ this.contextTokenCache.delete(normalizedRecipientId);
1207
+ writeJsonFile(CONTEXT_CACHE_FILE, Object.fromEntries(this.contextTokenCache));
1208
+ return true;
1209
+ }
928
1210
  generateClientId() {
929
1211
  return `wechat-bridge:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
930
1212
  }
package/package.json CHANGED
@@ -1,98 +1,62 @@
1
- {
2
- "name": "@unlinearity/cli-wechat-bridge",
3
- "version": "1.0.4",
4
- "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
5
- "type": "module",
6
- "files": [
7
- "bin/",
8
- "dist/",
9
- "README.md",
10
- "LICENSE.txt"
11
- ],
12
- "keywords": [
13
- "wechat",
14
- "cli",
15
- "bridge",
16
- "codex",
17
- "claude",
18
- "opencode",
19
- "terminal"
20
- ],
21
- "author": "UNLINEARITY",
22
- "license": "MIT",
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/UNLINEARITY/CLI-WeChat-Bridge.git"
26
- },
27
- "bugs": {
28
- "url": "https://github.com/UNLINEARITY/CLI-WeChat-Bridge/issues"
29
- },
30
- "homepage": "https://github.com/UNLINEARITY/CLI-WeChat-Bridge#readme",
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "bin": {
35
- "wechat-bridge": "bin/wechat-bridge.mjs",
36
- "wechat-bridge-codex": "bin/wechat-bridge-codex.mjs",
37
- "wechat-codex": "bin/wechat-codex.mjs",
38
- "wechat-codex-start": "bin/wechat-codex-start.mjs",
39
- "wechat-claude": "bin/wechat-claude.mjs",
40
- "wechat-claude-start": "bin/wechat-claude-start.mjs",
41
- "wechat-bridge-claude": "bin/wechat-bridge-claude.mjs",
42
- "wechat-bridge-shell": "bin/wechat-bridge-shell.mjs",
43
- "wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
44
- "wechat-opencode": "bin/wechat-opencode.mjs",
45
- "wechat-opencode-start": "bin/wechat-opencode-start.mjs",
46
- "wechat-setup": "bin/wechat-setup.mjs",
47
- "wechat-check-update": "bin/wechat-check-update.mjs"
48
- },
49
- "scripts": {
50
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
51
- "build": "npm run clean && tsc -p tsconfig.build.json",
52
- "prepack": "npm run build",
53
- "lint": "eslint bin src test",
54
- "lint:fix": "eslint bin src test --fix",
55
- "typecheck:src": "tsc -p tsconfig.typecheck.src.json --noEmit --pretty false",
56
- "quality": "npm run lint && npm run typecheck:src && bun test test && npm run build",
57
- "smoke:global": "node scripts/smoke-global-install.mjs",
58
- "setup": "node --no-warnings --experimental-strip-types src/wechat/setup.ts",
59
- "start": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts",
60
- "check": "node --no-warnings --experimental-strip-types src/wechat/wechat-channel.ts --check",
61
- "bridge": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts",
62
- "bridge:codex": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter codex",
63
- "codex:panel": "node --no-warnings --experimental-strip-types src/companion/codex-remote-client.ts",
64
- "codex:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts",
65
- "claude:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts --adapter claude",
66
- "claude:companion": "node --no-warnings --experimental-strip-types src/companion/local-companion.ts --adapter claude",
67
- "bridge:claude": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter claude",
68
- "bridge:shell": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter shell",
69
- "bridge:opencode": "node --no-warnings --experimental-strip-types src/bridge/wechat-bridge.ts --adapter opencode",
70
- "opencode:panel": "node --no-warnings --experimental-strip-types src/companion/local-companion.ts --adapter opencode",
71
- "opencode:start": "node --no-warnings --experimental-strip-types src/companion/local-companion-start.ts --adapter opencode",
72
- "bridge:bun": "bun src/bridge/wechat-bridge.ts",
73
- "test": "bun test test",
74
- "test:bridge": "bun test test/bridge",
75
- "test:companion": "bun test test/companion",
76
- "test:wechat": "bun test test/wechat",
77
- "test:watch": "bun test --watch test"
78
- },
79
- "dependencies": {
80
- "@anthropic-ai/sdk": "^0.80.0",
81
- "@modelcontextprotocol/sdk": "^1.27.1",
82
- "@opencode-ai/sdk": "^1.3.2",
83
- "ajv-formats": "^3.0.1",
84
- "node-pty": "^1.1.0",
85
- "qrcode-terminal": "^0.12.0"
86
- },
87
- "devDependencies": {
88
- "@eslint/js": "^10.0.1",
89
- "@types/bun": "latest",
90
- "eslint": "^10.3.0",
91
- "typescript": "^6.0.2",
92
- "typescript-eslint": "^8.59.3"
93
- },
94
- "engines": {
95
- "bun": ">=1.0.0",
96
- "node": ">=24.0.0"
97
- }
98
- }
1
+ {
2
+ "name": "@unlinearity/cli-wechat-bridge",
3
+ "version": "1.0.6",
4
+ "description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
5
+ "type": "module",
6
+ "files": [
7
+ "bin/",
8
+ "dist/",
9
+ "README.md",
10
+ "LICENSE.txt"
11
+ ],
12
+ "keywords": [
13
+ "wechat",
14
+ "cli",
15
+ "bridge",
16
+ "codex",
17
+ "claude",
18
+ "opencode",
19
+ "terminal"
20
+ ],
21
+ "author": "UNLINEARITY",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/UNLINEARITY/CLI-WeChat-Bridge.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/UNLINEARITY/CLI-WeChat-Bridge/issues"
29
+ },
30
+ "homepage": "https://github.com/UNLINEARITY/CLI-WeChat-Bridge#readme",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "bin": {
35
+ "wechat-bridge": "bin/wechat-bridge.mjs",
36
+ "wechat-bridge-codex": "bin/wechat-bridge-codex.mjs",
37
+ "wechat-codex": "bin/wechat-codex.mjs",
38
+ "wechat-codex-start": "bin/wechat-codex-start.mjs",
39
+ "wechat-claude": "bin/wechat-claude.mjs",
40
+ "wechat-claude-start": "bin/wechat-claude-start.mjs",
41
+ "wechat-bridge-claude": "bin/wechat-bridge-claude.mjs",
42
+ "wechat-bridge-shell": "bin/wechat-bridge-shell.mjs",
43
+ "wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
44
+ "wechat-opencode": "bin/wechat-opencode.mjs",
45
+ "wechat-opencode-start": "bin/wechat-opencode-start.mjs",
46
+ "wechat-daemon": "bin/wechat-daemon.mjs",
47
+ "wechat-setup": "bin/wechat-setup.mjs",
48
+ "wechat-check-update": "bin/wechat-check-update.mjs"
49
+ },
50
+ "dependencies": {
51
+ "@anthropic-ai/sdk": "^0.80.0",
52
+ "@modelcontextprotocol/sdk": "^1.27.1",
53
+ "@opencode-ai/sdk": "^1.3.2",
54
+ "ajv-formats": "^3.0.1",
55
+ "node-pty": "^1.1.0",
56
+ "qrcode-terminal": "^0.12.0"
57
+ },
58
+ "engines": {
59
+ "bun": ">=1.0.0",
60
+ "node": ">=24.0.0"
61
+ }
62
+ }