weflow-cli 1.5.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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/bin/weflow-cli-electron.cjs +59 -0
  4. package/bin/weflow-cli.ts +2311 -0
  5. package/cli.cjs +3 -0
  6. package/dist/bin/weflow-cli.d.ts +3 -0
  7. package/dist/bin/weflow-cli.d.ts.map +1 -0
  8. package/dist/bin/weflow-cli.js +2270 -0
  9. package/dist/bin/weflow-cli.js.map +1 -0
  10. package/dist/mcp-server/index.d.ts +3 -0
  11. package/dist/mcp-server/index.d.ts.map +1 -0
  12. package/dist/mcp-server/index.js +690 -0
  13. package/dist/mcp-server/index.js.map +1 -0
  14. package/dist/src/core/dbPathService.d.ts +40 -0
  15. package/dist/src/core/dbPathService.d.ts.map +1 -0
  16. package/dist/src/core/dbPathService.js +324 -0
  17. package/dist/src/core/dbPathService.js.map +1 -0
  18. package/dist/src/core/keyService.d.ts +59 -0
  19. package/dist/src/core/keyService.d.ts.map +1 -0
  20. package/dist/src/core/keyService.js +368 -0
  21. package/dist/src/core/keyService.js.map +1 -0
  22. package/dist/src/core/ntCore.d.ts +62 -0
  23. package/dist/src/core/ntCore.d.ts.map +1 -0
  24. package/dist/src/core/ntCore.js +191 -0
  25. package/dist/src/core/ntCore.js.map +1 -0
  26. package/dist/src/core/sqlcipherCore.d.ts +119 -0
  27. package/dist/src/core/sqlcipherCore.d.ts.map +1 -0
  28. package/dist/src/core/sqlcipherCore.js +693 -0
  29. package/dist/src/core/sqlcipherCore.js.map +1 -0
  30. package/dist/src/core/wcdbCore.d.ts +738 -0
  31. package/dist/src/core/wcdbCore.d.ts.map +1 -0
  32. package/dist/src/core/wcdbCore.js +4545 -0
  33. package/dist/src/core/wcdbCore.js.map +1 -0
  34. package/dist/src/core/wechatClient.d.ts +23 -0
  35. package/dist/src/core/wechatClient.d.ts.map +1 -0
  36. package/dist/src/core/wechatClient.js +166 -0
  37. package/dist/src/core/wechatClient.js.map +1 -0
  38. package/dist/src/services/chatService.d.ts +25 -0
  39. package/dist/src/services/chatService.d.ts.map +1 -0
  40. package/dist/src/services/chatService.js +291 -0
  41. package/dist/src/services/chatService.js.map +1 -0
  42. package/dist/src/services/configService.d.ts +43 -0
  43. package/dist/src/services/configService.d.ts.map +1 -0
  44. package/dist/src/services/configService.js +156 -0
  45. package/dist/src/services/configService.js.map +1 -0
  46. package/dist/src/services/exportService.d.ts +30 -0
  47. package/dist/src/services/exportService.d.ts.map +1 -0
  48. package/dist/src/services/exportService.js +273 -0
  49. package/dist/src/services/exportService.js.map +1 -0
  50. package/dist/src/services/wechat-formatter.d.ts +69 -0
  51. package/dist/src/services/wechat-formatter.d.ts.map +1 -0
  52. package/dist/src/services/wechat-formatter.js +248 -0
  53. package/dist/src/services/wechat-formatter.js.map +1 -0
  54. package/dist/src/services/wechatMessageService.d.ts +28 -0
  55. package/dist/src/services/wechatMessageService.d.ts.map +1 -0
  56. package/dist/src/services/wechatMessageService.js +341 -0
  57. package/dist/src/services/wechatMessageService.js.map +1 -0
  58. package/dist/src/services/wereadService.d.ts +206 -0
  59. package/dist/src/services/wereadService.d.ts.map +1 -0
  60. package/dist/src/services/wereadService.js +145 -0
  61. package/dist/src/services/wereadService.js.map +1 -0
  62. package/dist/src/services/whitelistService.d.ts +20 -0
  63. package/dist/src/services/whitelistService.d.ts.map +1 -0
  64. package/dist/src/services/whitelistService.js +60 -0
  65. package/dist/src/services/whitelistService.js.map +1 -0
  66. package/dist/src/types.d.ts +140 -0
  67. package/dist/src/types.d.ts.map +1 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils/errors.d.ts +25 -0
  71. package/dist/src/utils/errors.d.ts.map +1 -0
  72. package/dist/src/utils/errors.js +43 -0
  73. package/dist/src/utils/errors.js.map +1 -0
  74. package/dist/src/utils/pathUtils.d.ts +5 -0
  75. package/dist/src/utils/pathUtils.d.ts.map +1 -0
  76. package/dist/src/utils/pathUtils.js +16 -0
  77. package/dist/src/utils/pathUtils.js.map +1 -0
  78. package/dist/src/utils/pythonRunner.d.ts +27 -0
  79. package/dist/src/utils/pythonRunner.d.ts.map +1 -0
  80. package/dist/src/utils/pythonRunner.js +62 -0
  81. package/dist/src/utils/pythonRunner.js.map +1 -0
  82. package/dist/src/utils/talkerUtils.d.ts +14 -0
  83. package/dist/src/utils/talkerUtils.d.ts.map +1 -0
  84. package/dist/src/utils/talkerUtils.js +73 -0
  85. package/dist/src/utils/talkerUtils.js.map +1 -0
  86. package/mcp-server/index.ts +757 -0
  87. package/package.json +72 -0
  88. package/resources/key/win32/x64/wx_key.dll +0 -0
  89. package/resources/wcdb/win32/x64/SDL2.dll +0 -0
  90. package/resources/wcdb/win32/x64/WCDB.dll +0 -0
  91. package/resources/wcdb/win32/x64/msvcp140.dll +0 -0
  92. package/resources/wcdb/win32/x64/msvcp140_1.dll +0 -0
  93. package/resources/wcdb/win32/x64/vcruntime140.dll +0 -0
  94. package/resources/wcdb/win32/x64/vcruntime140_1.dll +0 -0
  95. package/resources/wcdb/win32/x64/wcdb_api.dll +0 -0
  96. package/scripts/_batch_link_sources.py +73 -0
  97. package/scripts/_utils.py +531 -0
  98. package/scripts/annual_report.py +796 -0
  99. package/scripts/auto_tag.py +128 -0
  100. package/scripts/biz_daily.py +896 -0
  101. package/scripts/chat_report.py +397 -0
  102. package/scripts/chat_stats.py +702 -0
  103. package/scripts/classify_daily.py +411 -0
  104. package/scripts/compile_wiki.py +258 -0
  105. package/scripts/create_reading_notes.py +370 -0
  106. package/scripts/enrich_backlinks.py +163 -0
  107. package/scripts/export_chat_html.py +766 -0
  108. package/scripts/extract_3x_key.py +86 -0
  109. package/scripts/extract_todos.py +404 -0
  110. package/scripts/fav_server.py +392 -0
  111. package/scripts/fix_topics.py +111 -0
  112. package/scripts/generate_ai_report.py +367 -0
  113. package/scripts/generate_html.py +1372 -0
  114. package/scripts/generate_review.py +170 -0
  115. package/scripts/mcp_bridge.py +398 -0
  116. package/scripts/nt_decrypt.py +562 -0
  117. package/scripts/pipeline.py +145 -0
  118. package/scripts/promote_all.py +310 -0
  119. package/scripts/promote_ideas.py +297 -0
  120. package/scripts/rag_chat.py +191 -0
  121. package/scripts/scan_decrypt_4x.py +329 -0
  122. package/scripts/semantic_search.py +431 -0
  123. package/scripts/sync_fav.py +146 -0
  124. package/scripts/sync_weread.py +192 -0
  125. package/scripts/vault_rag.py +139 -0
  126. package/scripts/vault_search.py +141 -0
  127. package/src/core/dbPathService.ts +348 -0
  128. package/src/core/keyService.ts +409 -0
  129. package/src/core/ntCore.ts +224 -0
  130. package/src/core/sqlcipherCore.ts +764 -0
  131. package/src/core/wcdbCore.ts +4568 -0
  132. package/src/core/wechatClient.ts +204 -0
  133. package/src/lz4.d.ts +10 -0
  134. package/src/services/chatService.ts +296 -0
  135. package/src/services/configService.ts +197 -0
  136. package/src/services/exportService.ts +305 -0
  137. package/src/services/wechat-formatter.ts +348 -0
  138. package/src/services/wechatMessageService.ts +383 -0
  139. package/src/services/wereadService.ts +308 -0
  140. package/src/services/whitelistService.ts +72 -0
  141. package/src/types.ts +141 -0
  142. package/src/utils/errors.ts +42 -0
  143. package/src/utils/pathUtils.ts +16 -0
  144. package/src/utils/pythonRunner.ts +81 -0
  145. package/src/utils/talkerUtils.ts +84 -0
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ WeChat 4.x Database Key Scanner & Decryptor
4
+ Scans Weixin.exe process memory for WCDB key+salt pairs and tests decryption.
5
+ """
6
+ import ctypes
7
+ from ctypes import wintypes, c_void_p, c_size_t, create_string_buffer, byref, sizeof
8
+ import re
9
+ import hashlib
10
+ import hmac as hmac_lib
11
+ import sys
12
+ import os
13
+ from pathlib import Path
14
+
15
+ try:
16
+ from Crypto.Cipher import AES
17
+ except ImportError:
18
+ print("Install pycryptodome: pip install pycryptodome")
19
+ sys.exit(1)
20
+
21
+ # ========== Constants ==========
22
+ PROCESS_VM_READ = 0x0010
23
+ PROCESS_QUERY_INFORMATION = 0x0400
24
+ MEM_COMMIT = 0x1000
25
+ PAGE_NOACCESS = 0x01
26
+ PAGE_GUARD = 0x100
27
+ PAGE_SIZE = 4096
28
+ EXPECTED_MAGIC = b'SQLite format 3\x00'
29
+
30
+ # ========== Memory Structures ==========
31
+ class MEMORY_BASIC_INFORMATION(ctypes.Structure):
32
+ _fields_ = [
33
+ ('BaseAddress', ctypes.c_void_p),
34
+ ('AllocationBase', ctypes.c_void_p),
35
+ ('AllocationProtect', wintypes.DWORD),
36
+ ('PartitionId', wintypes.WORD),
37
+ ('RegionSize', ctypes.c_size_t),
38
+ ('State', wintypes.DWORD),
39
+ ('Protect', wintypes.DWORD),
40
+ ('Type', wintypes.DWORD),
41
+ ]
42
+
43
+ kernel32 = ctypes.windll.kernel32
44
+ ReadProcessMemory = kernel32.ReadProcessMemory
45
+ ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
46
+ ReadProcessMemory.restype = wintypes.BOOL
47
+
48
+ VirtualQueryEx = kernel32.VirtualQueryEx
49
+ VirtualQueryEx.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
50
+ VirtualQueryEx.restype = ctypes.c_size_t
51
+
52
+
53
+ def find_weixin_pid():
54
+ """Find Weixin.exe process ID."""
55
+ import pymem
56
+ import pymem.process
57
+ for proc in pymem.process.list_processes():
58
+ try:
59
+ name = proc.szExeFile
60
+ if isinstance(name, bytes):
61
+ name = name.decode('utf-8', errors='ignore')
62
+ if name.lower() == 'weixin.exe':
63
+ return proc.th32ProcessID
64
+ except:
65
+ pass
66
+ return None
67
+
68
+
69
+ def scan_memory_keys(pid):
70
+ """Scan process memory for x'<64hex_key><32hex_salt>' patterns."""
71
+ hProcess = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
72
+ if not hProcess:
73
+ print(f"Failed to open PID {pid}")
74
+ return []
75
+
76
+ pattern = re.compile(rb"x'([0-9a-fA-F]{64})([0-9a-fA-F]{32})'")
77
+ keys_found = []
78
+
79
+ address = 0x10000
80
+ region_count = 0
81
+
82
+ print(f"Scanning process {pid} memory...")
83
+
84
+ while address < 0x7FFFFFFFFFFF:
85
+ mbi = MEMORY_BASIC_INFORMATION()
86
+ result = VirtualQueryEx(hProcess, ctypes.c_void_p(address), ctypes.byref(mbi), sizeof(mbi))
87
+ if result == 0:
88
+ break
89
+
90
+ region_count += 1
91
+ if region_count % 1000 == 0:
92
+ print(f" ... {region_count} regions scanned, {len(keys_found)} keys found")
93
+
94
+ region_addr = mbi.BaseAddress or 0
95
+ region_size = mbi.RegionSize or 0
96
+
97
+ if (mbi.State == MEM_COMMIT and
98
+ region_size > 256 and region_size < 200 * 1024 * 1024 and
99
+ mbi.Protect not in (0, PAGE_NOACCESS, PAGE_GUARD)):
100
+
101
+ pos = region_addr
102
+ end = region_addr + region_size
103
+ while pos < end:
104
+ chunk_size = min(65536, end - pos)
105
+ buf = create_string_buffer(chunk_size)
106
+ bytesRead = c_size_t(0)
107
+ ok = ReadProcessMemory(hProcess, ctypes.c_void_p(pos), buf, chunk_size, byref(bytesRead))
108
+ if ok and bytesRead.value > 0:
109
+ data = buf.raw[:bytesRead.value]
110
+ for m in pattern.finditer(data):
111
+ abs_addr = pos + m.start()
112
+ key_hex = m.group(1).decode()
113
+ salt_hex = m.group(2).decode()
114
+ keys_found.append((key_hex, salt_hex, abs_addr))
115
+ pos += chunk_size
116
+
117
+ address = region_addr + region_size
118
+
119
+ kernel32.CloseHandle(hProcess)
120
+
121
+ # Deduplicate
122
+ seen = set()
123
+ unique_keys = []
124
+ for k, s, a in keys_found:
125
+ pair = (k, s)
126
+ if pair not in seen:
127
+ seen.add(pair)
128
+ unique_keys.append((k, s, a))
129
+
130
+ print(f"Found {len(unique_keys)} unique key+salt pairs in {region_count} regions")
131
+ return unique_keys
132
+
133
+
134
+ def try_decrypt_page0(enc_data, key_bytes):
135
+ """Try to decrypt page 0 with given key using various parameters."""
136
+
137
+ # Parameter combinations to try
138
+ kdf_configs = [
139
+ # (use_pbkdf2, sha_name, iterations, salt_source)
140
+ (False, None, 0, 'none'), # Raw key, no KDF
141
+ (True, 'sha512', 256000, 'file'), # SQLCipher 4 standard
142
+ (True, 'sha1', 64000, 'file'), # SQLCipher 3 standard
143
+ (True, 'sha256', 256000, 'file'),
144
+ (True, 'sha512', 64000, 'file'),
145
+ (True, 'sha1', 256000, 'file'),
146
+ (True, 'sha512', 4000, 'file'),
147
+ ]
148
+
149
+ file_salt = enc_data[:16] # First 16 bytes of file (zero for MSG0.db)
150
+
151
+ for use_pbkdf2, sha, iters, salt_src in kdf_configs:
152
+ if use_pbkdf2:
153
+ salt = file_salt if salt_src == 'file' else bytes(16)
154
+ try:
155
+ dk = hashlib.pbkdf2_hmac(sha, key_bytes, salt, iters, dklen=32)
156
+ except:
157
+ continue
158
+ else:
159
+ dk = key_bytes
160
+
161
+ for rsv in [48, 80, 64, 32, 96]:
162
+ if rsv >= PAGE_SIZE:
163
+ continue
164
+
165
+ iv_offset = PAGE_SIZE - rsv
166
+ iv = enc_data[iv_offset:iv_offset + 16]
167
+ if len(iv) < 16:
168
+ continue
169
+
170
+ enc_len = PAGE_SIZE - 16 - rsv # For page 0: skip 16-byte salt
171
+ encrypted = enc_data[16:16 + enc_len]
172
+
173
+ if len(encrypted) % 16 != 0:
174
+ continue
175
+
176
+ try:
177
+ cipher = AES.new(dk, AES.MODE_CBC, iv)
178
+ decrypted = cipher.decrypt(encrypted)
179
+ if decrypted[:16] == EXPECTED_MAGIC:
180
+ return {
181
+ 'key': key_bytes.hex(),
182
+ 'pbkdf2': use_pbkdf2,
183
+ 'sha': sha,
184
+ 'iterations': iters,
185
+ 'reserved': rsv,
186
+ 'iv_offset': iv_offset,
187
+ }
188
+ except:
189
+ pass
190
+
191
+ return None
192
+
193
+
194
+ def decrypt_full(enc_path, out_path, key_bytes, params):
195
+ """Decrypt entire database using discovered parameters."""
196
+ with open(enc_path, 'rb') as f:
197
+ enc = f.read()
198
+
199
+ rsv = params['reserved']
200
+ use_pbkdf2 = params['pbkdf2']
201
+ sha = params['sha']
202
+ iters = params['iterations']
203
+
204
+ file_salt = enc[:16]
205
+
206
+ if use_pbkdf2:
207
+ dk = hashlib.pbkdf2_hmac(sha, key_bytes, file_salt, iters, dklen=32)
208
+ else:
209
+ dk = key_bytes
210
+
211
+ result = bytearray()
212
+ # Write SQLite header
213
+ result.extend(b'SQLite format 3\x00')
214
+
215
+ num_pages = len(enc) // PAGE_SIZE
216
+
217
+ for pg_num in range(num_pages):
218
+ page_start = pg_num * PAGE_SIZE
219
+ page_data = enc[page_start:page_start + PAGE_SIZE]
220
+
221
+ if pg_num == 0:
222
+ # Page 0: salt(16) + encrypted_data + reserved
223
+ iv_offset = PAGE_SIZE - rsv
224
+ iv = page_data[iv_offset:iv_offset + 16]
225
+ enc_data = page_data[16:PAGE_SIZE - rsv]
226
+ else:
227
+ # Other pages: encrypted_data + reserved
228
+ iv_offset = PAGE_SIZE - rsv
229
+ iv = page_data[iv_offset:iv_offset + 16]
230
+ enc_data = page_data[:PAGE_SIZE - rsv]
231
+
232
+ cipher = AES.new(dk, AES.MODE_CBC, iv)
233
+ decrypted = cipher.decrypt(enc_data)
234
+ result.extend(decrypted)
235
+
236
+ # Write reserved area
237
+ reserved = page_data[PAGE_SIZE - rsv:PAGE_SIZE]
238
+ result.extend(reserved)
239
+
240
+ if pg_num % 1000 == 0 and pg_num > 0:
241
+ print(f" ... decrypted {pg_num}/{num_pages} pages")
242
+
243
+ with open(out_path, 'wb') as f:
244
+ f.write(result)
245
+ print(f"Decrypted database written to: {out_path}")
246
+
247
+
248
+ def main():
249
+ import argparse
250
+ parser = argparse.ArgumentParser(description='WeChat 4.x Database Decryptor')
251
+ parser.add_argument('--db', help='Path to encrypted database (e.g., MSG0.db)')
252
+ parser.add_argument('--out', help='Output path for decrypted database')
253
+ parser.add_argument('--scan-only', action='store_true', help='Only scan for keys, no decryption')
254
+ parser.add_argument('--key', help='Specific key hex to try (64 chars)')
255
+ parser.add_argument('--salt', help='Specific salt hex to try with key (32 chars)')
256
+ args = parser.parse_args()
257
+
258
+ pid = find_weixin_pid()
259
+ if not pid:
260
+ print("Weixin.exe not running. Start WeChat 4.x first.")
261
+ sys.exit(1)
262
+ print(f"Found Weixin.exe PID: {pid}")
263
+
264
+ # Scan memory for keys
265
+ keys = scan_memory_keys(pid)
266
+ print(f"\nFound {len(keys)} key+salt pairs:")
267
+ for key_hex, salt_hex, addr in keys:
268
+ print(f" {key_hex[:32]}... | {salt_hex} @ {hex(addr)}")
269
+
270
+ if args.scan_only:
271
+ return
272
+
273
+ # If specific key+salt provided, use it
274
+ if args.key:
275
+ keys.insert(0, (args.key, args.salt or '00' * 16, 0))
276
+
277
+ # Database to decrypt
278
+ db_path = args.db
279
+ if not db_path:
280
+ # Try to find MSG0.db
281
+ wx_docs = os.path.expandvars(r'%USERPROFILE%\Documents\WeChat Files')
282
+ if os.path.isdir(wx_docs):
283
+ for wxid_dir in os.listdir(wx_docs):
284
+ msg_path = os.path.join(wx_docs, wxid_dir, 'Msg', 'Multi', 'MSG0.db')
285
+ if os.path.exists(msg_path):
286
+ db_path = msg_path
287
+ print(f"\nAuto-detected database: {db_path}")
288
+ break
289
+
290
+ if not db_path or not os.path.exists(db_path):
291
+ print("No database file found. Use --db to specify path.")
292
+ sys.exit(1)
293
+
294
+ out_path = args.out or db_path.replace('.db', '_decrypted.db')
295
+
296
+ with open(db_path, 'rb') as f:
297
+ enc_data = f.read()
298
+
299
+ print(f"\nTesting {len(keys)} keys against {db_path}...")
300
+ print(f"File size: {len(enc_data)} bytes, {len(enc_data) // PAGE_SIZE} pages")
301
+ print(f"File salt (first 16 bytes): {enc_data[:16].hex()}")
302
+
303
+ for idx, (key_hex, salt_hex, addr) in enumerate(keys):
304
+ key_bytes = bytes.fromhex(key_hex)
305
+ result = try_decrypt_page0(enc_data, key_bytes)
306
+ if result:
307
+ print(f"\n*** SUCCESS with key #{idx + 1} ***")
308
+ print(f" Key: {key_hex}")
309
+ print(f" Memory salt: {salt_hex}")
310
+ print(f" PBKDF2: {result['pbkdf2']}")
311
+ if result['pbkdf2']:
312
+ print(f" Hash: {result['sha']}")
313
+ print(f" Iterations: {result['iterations']}")
314
+ print(f" Reserved: {result['reserved']} bytes")
315
+
316
+ decrypt_full(db_path, out_path, key_bytes, result)
317
+ break
318
+ elif idx % 5 == 0:
319
+ print(f" ... tested {idx + 1}/{len(keys)} keys")
320
+ else:
321
+ print("\nNo key could decrypt the database.")
322
+ print("Possible reasons:")
323
+ print(" - Database uses a different encryption format")
324
+ print(" - Key is not in the x'<hex>' format in memory")
325
+ print(" - Database was created with a key from a different session")
326
+
327
+
328
+ if __name__ == '__main__':
329
+ main()