@walkerch/wxecho 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.
package/dist/cli.js ADDED
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'module';
4
+ const require = createRequire(import.meta.url);
5
+
6
+
7
+ // src/cli.ts
8
+ import { Command } from "commander";
9
+
10
+ // src/utils/python.ts
11
+ import { spawn } from "child_process";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { dirname } from "path";
15
+ var __filename = fileURLToPath(import.meta.url);
16
+ var __dirname = dirname(__filename);
17
+ var PY_DIR = path.resolve(__dirname, "../../py");
18
+ function runPythonScript(scriptName, args = []) {
19
+ return new Promise((resolve2, reject) => {
20
+ const scriptPath = path.join(PY_DIR, scriptName);
21
+ const child = spawn("python3", [scriptPath, ...args], {
22
+ cwd: PY_DIR,
23
+ stdio: "inherit",
24
+ env: { ...process.env, PYTHONIOENCODING: "utf-8" }
25
+ });
26
+ child.on("close", (code) => {
27
+ if (code === 0) {
28
+ resolve2();
29
+ } else {
30
+ reject(new Error(`Python \u811A\u672C\u9000\u51FA\uFF0C\u4EE3\u7801: ${code}`));
31
+ }
32
+ });
33
+ child.on("error", (error) => {
34
+ reject(new Error(`\u542F\u52A8 Python \u5931\u8D25: ${error.message}`));
35
+ });
36
+ });
37
+ }
38
+
39
+ // src/commands/export.ts
40
+ function runExport(cmd) {
41
+ const args = [];
42
+ const opts = cmd.opts();
43
+ const name = cmd.args[0];
44
+ if (opts.list) {
45
+ args.push("-l");
46
+ if (opts.top) args.push("--top", opts.top);
47
+ } else if (name) {
48
+ args.push("-n", name);
49
+ } else {
50
+ console.error("\u9519\u8BEF\uFF1A\u9700\u8981\u63D0\u4F9B\u8054\u7CFB\u4EBA\u540D\u79F0\u6216\u4F7F\u7528 --list");
51
+ console.log("\n\u7528\u6CD5\u793A\u4F8B\uFF1A");
52
+ console.log(" wxecho export -l # \u5217\u51FA\u6240\u6709\u4F1A\u8BDD");
53
+ console.log(' wxecho export -n "\u5F20\u4E09" # \u5BFC\u51FA\u5F20\u4E09\u7684\u804A\u5929');
54
+ console.log(" wxecho export -u wxid_xxxxx # \u901A\u8FC7\u7528\u6237\u540D\u5BFC\u51FA");
55
+ process.exit(1);
56
+ }
57
+ if (opts.username) args.push("-u", opts.username);
58
+ if (opts.output) args.push("-o", opts.output);
59
+ if (opts.myWxid) args.push("--my-wxid", opts.myWxid);
60
+ return runPythonScript("export_chat.py", args).then(() => {
61
+ }).catch((error) => {
62
+ console.error("\u5BFC\u51FA\u5931\u8D25:", error);
63
+ process.exit(1);
64
+ });
65
+ }
66
+
67
+ // src/utils/decrypt_db.ts
68
+ import * as crypto from "node:crypto";
69
+ import * as fs from "node:fs";
70
+ import * as path2 from "node:path";
71
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
72
+ import { dirname as dirname3 } from "node:path";
73
+ function getScriptDir() {
74
+ return dirname3(fileURLToPath2(import.meta.url));
75
+ }
76
+ var PAGE_SZ = 4096;
77
+ var KEY_SZ = 32;
78
+ var SALT_SZ = 16;
79
+ var IV_SZ = 16;
80
+ var HMAC_SZ = 64;
81
+ var RESERVE_SZ = IV_SZ + HMAC_SZ;
82
+ var SQLITE_HDR = Buffer.from("SQLite format 3\0");
83
+ function deriveMacKey(encKey, salt) {
84
+ const macSalt = Buffer.alloc(SALT_SZ);
85
+ for (let i = 0; i < SALT_SZ; i++) {
86
+ macSalt[i] = salt[i] ^ 58;
87
+ }
88
+ return crypto.pbkdf2Sync(encKey, macSalt, 2, KEY_SZ, "sha512");
89
+ }
90
+ function decryptPage(encKey, pageData, pgno) {
91
+ const iv = pageData.subarray(PAGE_SZ - RESERVE_SZ, PAGE_SZ - RESERVE_SZ + IV_SZ);
92
+ let encrypted;
93
+ if (pgno === 1) {
94
+ encrypted = pageData.subarray(SALT_SZ, PAGE_SZ - RESERVE_SZ);
95
+ } else {
96
+ encrypted = pageData.subarray(0, PAGE_SZ - RESERVE_SZ);
97
+ }
98
+ const cipher = crypto.createDecipheriv("aes-256-cbc", encKey, iv);
99
+ try {
100
+ var decrypted = Buffer.concat([cipher.update(encrypted), cipher.final()]);
101
+ } catch {
102
+ const cipher2 = crypto.createDecipheriv("aes-256-cbc", encKey, iv);
103
+ cipher2.setAutoPadding(false);
104
+ decrypted = Buffer.concat([cipher2.update(encrypted), cipher2.final()]);
105
+ }
106
+ if (pgno === 1) {
107
+ const page = Buffer.alloc(PAGE_SZ);
108
+ SQLITE_HDR.copy(page);
109
+ decrypted.copy(page, SQLITE_HDR.length);
110
+ return page;
111
+ } else {
112
+ const page = Buffer.alloc(PAGE_SZ);
113
+ decrypted.copy(page, 0);
114
+ return page;
115
+ }
116
+ }
117
+ function decryptDatabase(dbPath, outPath, encKeyHex) {
118
+ const encKey = Buffer.from(encKeyHex, "hex");
119
+ const fileSize = fs.statSync(dbPath).size;
120
+ const totalPages = Math.ceil(fileSize / PAGE_SZ);
121
+ const fd = fs.openSync(dbPath, "r");
122
+ try {
123
+ const page1 = Buffer.alloc(PAGE_SZ);
124
+ fs.readSync(fd, page1, 0, PAGE_SZ, 0);
125
+ const salt = page1.subarray(0, SALT_SZ);
126
+ const macKey = deriveMacKey(encKey, salt);
127
+ const p1HmacData = Buffer.concat([
128
+ page1.subarray(SALT_SZ, PAGE_SZ - RESERVE_SZ + IV_SZ),
129
+ Buffer.from([1, 0, 0, 0])
130
+ // pgno=1 as little-endian uint32
131
+ ]);
132
+ const computedHmac = crypto.createHmac("sha512", macKey).update(p1HmacData).digest();
133
+ const storedHmac = page1.subarray(PAGE_SZ - HMAC_SZ, PAGE_SZ);
134
+ if (!computedHmac.equals(storedHmac)) {
135
+ console.log(` [ERROR] Page 1 HMAC mismatch! salt: ${salt.toString("hex")}`);
136
+ return false;
137
+ }
138
+ console.log(` HMAC OK, ${totalPages} pages`);
139
+ fs.mkdirSync(path2.dirname(outPath), { recursive: true });
140
+ const outFd = fs.openSync(outPath, "w");
141
+ try {
142
+ for (let pgno = 1; pgno <= totalPages; pgno++) {
143
+ const pageBuf = Buffer.alloc(PAGE_SZ);
144
+ const bytesRead = fs.readSync(fd, pageBuf, 0, PAGE_SZ, (pgno - 1) * PAGE_SZ);
145
+ if (bytesRead < PAGE_SZ) {
146
+ if (bytesRead > 0) {
147
+ pageBuf.fill(0, bytesRead);
148
+ } else {
149
+ break;
150
+ }
151
+ }
152
+ const decrypted = decryptPage(encKey, pageBuf, pgno);
153
+ fs.writeSync(outFd, decrypted, 0, PAGE_SZ, (pgno - 1) * PAGE_SZ);
154
+ if (pgno === 1 && decrypted.subarray(0, 16).compare(SQLITE_HDR) !== 0) {
155
+ console.log(` [WARN] Decrypted header mismatch!`);
156
+ }
157
+ if (pgno % 1e4 === 0) {
158
+ console.log(` Progress: ${pgno}/${totalPages} (${(100 * pgno / totalPages).toFixed(1)}%)`);
159
+ }
160
+ }
161
+ } finally {
162
+ fs.closeSync(outFd);
163
+ }
164
+ } finally {
165
+ fs.closeSync(fd);
166
+ }
167
+ return true;
168
+ }
169
+ function stripKeyMetadata(keys) {
170
+ const result = {};
171
+ for (const [k, v] of Object.entries(keys)) {
172
+ if (!k.startsWith("_")) {
173
+ result[k] = v;
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+ function isSafeRelPath(relPath) {
179
+ const normalized = path2.normalize(relPath).replace(/\\/g, "/");
180
+ return !normalized.includes("..");
181
+ }
182
+ function getKeyInfo(keys, relPath) {
183
+ if (!isSafeRelPath(relPath)) return null;
184
+ const normalized = relPath.replace(/\\/g, "/");
185
+ const variants = [relPath, normalized, normalized.replace("/", "\\")];
186
+ for (const candidate of variants) {
187
+ if (candidate in keys && !candidate.startsWith("_")) {
188
+ return keys[candidate];
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ async function verifySqlite(outPath) {
194
+ try {
195
+ const { execSync: execSync2 } = await import("child_process");
196
+ const safePath = outPath.replace(/'/g, "'\\''");
197
+ const output = execSync2(
198
+ `python3 -c "import sqlite3; conn=sqlite3.connect('${safePath}'); print(','.join([r[0] for r in conn.execute('SELECT name FROM sqlite_master WHERE type=\\'table\\'').fetchall()]))"`,
199
+ { encoding: "utf-8" }
200
+ ).trim();
201
+ return output.split(",").filter(Boolean);
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+ async function runDecrypt(config) {
207
+ const { dbDir, outDir, keysFile } = config;
208
+ if (!fs.existsSync(keysFile)) {
209
+ console.error(`[ERROR] Key file not found: ${keysFile}`);
210
+ console.error('Please run "wxecho keys" first.');
211
+ process.exit(1);
212
+ }
213
+ const keysRaw = JSON.parse(fs.readFileSync(keysFile, "utf-8"));
214
+ const keys = stripKeyMetadata(keysRaw);
215
+ console.log(`Loaded ${Object.keys(keys).length} database keys`);
216
+ console.log(`Output directory: ${outDir}`);
217
+ fs.mkdirSync(outDir, { recursive: true });
218
+ const dbFiles = [];
219
+ function walkDir(dir) {
220
+ try {
221
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
222
+ for (const entry of entries) {
223
+ const full = path2.join(dir, entry.name);
224
+ if (entry.isDirectory()) {
225
+ walkDir(full);
226
+ } else if (entry.isFile() && entry.name.endsWith(".db") && !entry.name.endsWith("-wal") && !entry.name.endsWith("-shm")) {
227
+ const rel = path2.relative(dbDir, full).replace(/\\/g, "/");
228
+ dbFiles.push({ rel, abs: full, size: fs.statSync(full).size });
229
+ }
230
+ }
231
+ } catch {
232
+ }
233
+ }
234
+ walkDir(dbDir);
235
+ dbFiles.sort((a, b) => a.size - b.size);
236
+ console.log(`Found ${dbFiles.length} database files
237
+ `);
238
+ let success = 0;
239
+ let failed = 0;
240
+ let totalBytes = 0;
241
+ for (const { rel, abs, size } of dbFiles) {
242
+ const keyInfo = getKeyInfo(keys, rel);
243
+ if (!keyInfo) {
244
+ console.log(`SKIP: ${rel} (no key)`);
245
+ failed++;
246
+ continue;
247
+ }
248
+ const outPath = path2.join(outDir, rel);
249
+ process.stdout.write(`Decrypting: ${rel} (${(size / 1024 / 1024).toFixed(1)}MB) ... `);
250
+ const ok = decryptDatabase(abs, outPath, keyInfo.enc_key);
251
+ if (ok) {
252
+ const tables = await verifySqlite(outPath);
253
+ if (tables) {
254
+ const tableNames = tables.slice(0, 5).join(", ");
255
+ const suffix = tables.length > 5 ? ` ...total ${tables.length}` : "";
256
+ console.log(` OK! Tables: ${tableNames}${suffix}`);
257
+ success++;
258
+ totalBytes += size;
259
+ } else {
260
+ console.log(` [WARN] SQLite verification failed`);
261
+ failed++;
262
+ }
263
+ } else {
264
+ failed++;
265
+ }
266
+ }
267
+ console.log(`
268
+ ${"=".repeat(60)}`);
269
+ console.log(`Result: ${success} succeeded, ${failed} failed, ${dbFiles.length} total`);
270
+ console.log(`Decrypted data: ${(totalBytes / 1024 / 1024 / 1024).toFixed(2)}GB`);
271
+ console.log(`Decrypted files at: ${outDir}`);
272
+ }
273
+ function detectWeChatDbDir() {
274
+ const home = process.env.HOME || "";
275
+ let bestDir = null;
276
+ let bestMtime = 0;
277
+ try {
278
+ const xwechatRoot = path2.join(home, "Documents", "xwechat_files");
279
+ if (!fs.existsSync(xwechatRoot)) return null;
280
+ const accounts = fs.readdirSync(xwechatRoot);
281
+ for (const account of accounts) {
282
+ const dbStorage = path2.join(xwechatRoot, account, "db_storage");
283
+ const msgDir = path2.join(dbStorage, "message");
284
+ if (fs.existsSync(msgDir)) {
285
+ try {
286
+ const mtime = fs.statSync(msgDir).mtimeMs;
287
+ if (mtime > bestMtime) {
288
+ bestMtime = mtime;
289
+ bestDir = dbStorage;
290
+ }
291
+ } catch {
292
+ }
293
+ }
294
+ }
295
+ } catch {
296
+ return null;
297
+ }
298
+ return bestDir;
299
+ }
300
+ function getConfig() {
301
+ const scriptDir = getScriptDir();
302
+ const pyDir = path2.resolve(scriptDir, "..", "py");
303
+ const keysFile = path2.join(pyDir, "all_keys.json");
304
+ let dbDir = path2.join(process.env.HOME || "", "Documents", "xwechat_files", "your_wxid", "db_storage");
305
+ const configJsonPath = path2.join(pyDir, "config.json");
306
+ if (fs.existsSync(configJsonPath)) {
307
+ try {
308
+ const cfg = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
309
+ if (cfg.db_dir && !cfg.db_dir.includes("your_wxid") && fs.existsSync(cfg.db_dir)) {
310
+ dbDir = cfg.db_dir;
311
+ }
312
+ } catch {
313
+ }
314
+ }
315
+ if (dbDir.includes("your_wxid") || !fs.existsSync(dbDir)) {
316
+ const detected = detectWeChatDbDir();
317
+ if (detected) {
318
+ dbDir = detected;
319
+ console.log(`[+] Auto-detected WeChat data directory: ${dbDir}`);
320
+ }
321
+ }
322
+ return {
323
+ dbDir,
324
+ outDir: path2.join(pyDir, "decrypted"),
325
+ keysFile
326
+ };
327
+ }
328
+
329
+ // src/commands/decrypt.ts
330
+ async function runDecryptCmd() {
331
+ console.log("=".repeat(60));
332
+ console.log(" WeChat 4.0 Database Decryptor");
333
+ console.log("=".repeat(60));
334
+ console.log();
335
+ const config = getConfig();
336
+ await runDecrypt(config);
337
+ }
338
+
339
+ // src/commands/keys.ts
340
+ import { spawn as spawn2 } from "child_process";
341
+ import path3 from "path";
342
+ import { fileURLToPath as fileURLToPath3 } from "url";
343
+ import { dirname as dirname4 } from "path";
344
+ import fs2 from "fs";
345
+ var __filename2 = fileURLToPath3(import.meta.url);
346
+ var __dirname2 = dirname4(__filename2);
347
+ var PY_DIR2 = path3.resolve(__dirname2, "../../py");
348
+ async function runKeys(options) {
349
+ const outputFile = options.output || "all_keys.json";
350
+ const binaryPath = path3.join(PY_DIR2, "find_all_keys_macos");
351
+ console.log("\u6B63\u5728\u4ECE\u5FAE\u4FE1\u8FDB\u7A0B\u63D0\u53D6\u5BC6\u94A5...\n");
352
+ console.log("\u6CE8\u610F\uFF1A\u6B64\u64CD\u4F5C\u9700\u8981\uFF1A");
353
+ console.log(" 1. \u5FAE\u4FE1\u6B63\u5728\u8FD0\u884C\u4E14\u5DF2\u767B\u5F55");
354
+ console.log(" 2. \u5DF2\u6267\u884C\u91CD\u7B7E\u540D\u7684\u5FAE\u4FE1\uFF08sudo codesign --force --deep --sign - /Applications/WeChat.app\uFF09\n");
355
+ if (!fs2.existsSync(binaryPath)) {
356
+ console.log("\u7F16\u8BD1\u5BC6\u94A5\u626B\u63CF\u5668...");
357
+ await compileBinary();
358
+ }
359
+ await extractKeys(binaryPath, outputFile);
360
+ }
361
+ function compileBinary() {
362
+ return new Promise((resolve2, reject) => {
363
+ const compile = spawn2("cc", [
364
+ "-O2",
365
+ "-o",
366
+ path3.join(PY_DIR2, "find_all_keys_macos"),
367
+ path3.join(PY_DIR2, "find_all_keys_macos.c"),
368
+ "-framework",
369
+ "Foundation"
370
+ ], { cwd: PY_DIR2 });
371
+ compile.on("close", (code) => {
372
+ if (code !== 0) {
373
+ reject(new Error("\u7F16\u8BD1\u5931\u8D25"));
374
+ return;
375
+ }
376
+ console.log("\u7F16\u8BD1\u6210\u529F\uFF0C\u73B0\u5728\u8FD0\u884C\u5BC6\u94A5\u63D0\u53D6...\n");
377
+ resolve2();
378
+ });
379
+ compile.on("error", reject);
380
+ });
381
+ }
382
+ function extractKeys(binaryPath, outputFile) {
383
+ return new Promise((resolve2, reject) => {
384
+ const child = spawn2("sudo", [binaryPath], {
385
+ cwd: PY_DIR2,
386
+ stdio: "inherit"
387
+ });
388
+ child.on("close", (code) => {
389
+ if (code === 0) {
390
+ console.log(`
391
+ \u5BC6\u94A5\u5DF2\u4FDD\u5B58\u5230 ${outputFile}`);
392
+ resolve2();
393
+ } else {
394
+ reject(new Error(`\u5BC6\u94A5\u63D0\u53D6\u5931\u8D25\uFF0C\u9000\u51FA\u7801: ${code}`));
395
+ }
396
+ });
397
+ child.on("error", reject);
398
+ });
399
+ }
400
+
401
+ // src/utils/doctor.ts
402
+ import { execSync } from "child_process";
403
+ async function doctor() {
404
+ console.log("\u6B63\u5728\u68C0\u6D4B\u73AF\u5883\u4F9D\u8D56...\n");
405
+ const checks = [
406
+ { name: "Python 3", cmd: "python3 --version", required: true },
407
+ { name: "Xcode Command Line Tools", cmd: "xcode-select -p", required: true },
408
+ { name: "\u5FAE\u4FE1\u8FDB\u7A0B", cmd: "pgrep -x WeChat", required: true },
409
+ { name: "pycryptodome", cmd: 'python3 -c "import Crypto; print(Crypto.__version__)"', required: true }
410
+ ];
411
+ const infoChecks = [
412
+ { name: "\u5FAE\u4FE1\u7B7E\u540D\u72B6\u6001", cmd: 'codesign -d -v /Applications/WeChat.app 2>&1 || echo "NOT signed"', required: false }
413
+ ];
414
+ let allPassed = true;
415
+ for (const check of checks) {
416
+ try {
417
+ const output = execSync(check.cmd, { stdio: "pipe" }).toString().trim();
418
+ console.log(`\u2713 ${check.name}: ${output}`);
419
+ } catch {
420
+ if (check.required) {
421
+ console.log(`\u2717 ${check.name} - \u672A\u901A\u8FC7`);
422
+ allPassed = false;
423
+ } else {
424
+ console.log(`\u26A0 ${check.name} - \u53EF\u9009`);
425
+ }
426
+ }
427
+ }
428
+ for (const check of infoChecks) {
429
+ try {
430
+ const output = execSync(check.cmd, { stdio: "pipe" }).toString().trim();
431
+ console.log(`\u2139 ${check.name}: ${output}`);
432
+ } catch {
433
+ console.log(`\u2139 ${check.name}: \u65E0\u6CD5\u68C0\u6D4B`);
434
+ }
435
+ }
436
+ console.log("\n--- \u68C0\u67E5\u7ED3\u679C ---");
437
+ if (allPassed) {
438
+ console.log("\u2713 \u73AF\u5883\u5C31\u7EEA\uFF01\u53EF\u4EE5\u5F00\u59CB\u4F7F\u7528\u3002\n");
439
+ console.log("\u4F7F\u7528\u6B65\u9AA4\uFF1A");
440
+ console.log(" 1. wxecho keys # \u63D0\u53D6\u5BC6\u94A5\uFF08\u9700\u8981 sudo\uFF09");
441
+ console.log(" 2. wxecho decrypt # \u89E3\u5BC6\u6570\u636E\u5E93");
442
+ console.log(" 3. wxecho export -l # \u5217\u51FA\u6240\u6709\u4F1A\u8BDD");
443
+ console.log(' 4. wxecho export -n "\u8054\u7CFB\u4EBA" # \u5BFC\u51FA\u804A\u5929');
444
+ } else {
445
+ console.log("\u2717 \u73AF\u5883\u68C0\u67E5\u672A\u901A\u8FC7\uFF0C\u8BF7\u5B89\u88C5\u7F3A\u5931\u7684\u4F9D\u8D56\u3002\n");
446
+ console.log("\u5B89\u88C5\u4F9D\u8D56\uFF1A");
447
+ console.log(" pip install pycryptodome");
448
+ console.log(" xcode-select --install");
449
+ console.log(" \u91CD\u7B7E\u5FAE\u4FE1: sudo codesign --force --deep --sign - /Applications/WeChat.app");
450
+ }
451
+ }
452
+
453
+ // src/cli.ts
454
+ var program = new Command();
455
+ program.name("wxecho").description("macOS \u5FAE\u4FE1\u804A\u5929\u8BB0\u5F55\u4E00\u952E\u89E3\u5BC6\u5BFC\u51FA\u5DE5\u5177").version("1.0.0");
456
+ program.command("export").description("\u5BFC\u51FA\u804A\u5929\u8BB0\u5F55").argument("[name]", "\u8054\u7CFB\u4EBA\u6635\u79F0\u6216\u5907\u6CE8\uFF08\u6A21\u7CCA\u641C\u7D22\uFF09").option("-u, --username <wxid>", "\u7CBE\u786E\u5339\u914D\u7528\u6237\u540D").option("-o, --output <dir>", "\u8F93\u51FA\u76EE\u5F55").option("-l, --list", "\u5217\u51FA\u6240\u6709\u4F1A\u8BDD").option("--top <n>", "\u5217\u51FA\u524D N \u4E2A\u4F1A\u8BDD", "20").option("--my-wxid <wxid>", "\u81EA\u5DF1\u7684\u5FAE\u4FE1 ID").action(runExport);
457
+ program.command("decrypt").description("\u89E3\u5BC6\u5FAE\u4FE1\u6570\u636E\u5E93").action(runDecryptCmd);
458
+ program.command("keys").description("\u4ECE\u5FAE\u4FE1\u8FDB\u7A0B\u63D0\u53D6\u6570\u636E\u5E93\u5BC6\u94A5\uFF08\u9700\u8981 sudo\uFF09").option("-o, --output <file>", "\u8F93\u51FA\u6587\u4EF6", "all_keys.json").action(runKeys);
459
+ program.command("doctor").description("\u68C0\u6D4B\u73AF\u5883\u4F9D\u8D56").action(doctor);
460
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@walkerch/wxecho",
3
+ "version": "1.0.0",
4
+ "description": "macOS native chat history export tool with local database decryption support",
5
+ "keywords": ["macos", "chat-export", "decrypt", "sqlite", "wxecho", "database", "cli"],
6
+ "license": "MIT",
7
+ "author": "Xinhai Chang <changxinhai@pku.edu.cn>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/chang-xinhai/WxEcho"
11
+ },
12
+ "type": "module",
13
+ "main": "./dist/cli.js",
14
+ "bin": {
15
+ "wxecho": "./bin/wxecho"
16
+ },
17
+ "files": [
18
+ "dist/cli.js",
19
+ "bin/",
20
+ "py/config.py",
21
+ "py/export_chat.py",
22
+ "py/key_utils.py",
23
+ "py/*.c",
24
+ "py/find_all_keys_macos"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsx scripts/build-cli.ts",
28
+ "build:landing": "cd landing && npm ci && npm run build",
29
+ "build:all": "npm run build && npm run build:landing",
30
+ "prepublishOnly": "npm run build",
31
+ "dev": "tsx src/cli.ts",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "dependencies": {
35
+ "commander": "^12.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^20.14.0",
39
+ "esbuild": "^0.21.0",
40
+ "tsx": "^4.15.0",
41
+ "typescript": "^5.4.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0",
45
+ "python": ">=3.8"
46
+ },
47
+ "os": [
48
+ "darwin"
49
+ ],
50
+ "publishConfig": {
51
+ "registry": "https://registry.npmjs.org/"
52
+ }
53
+ }