claude-chats-sync 0.0.4 → 0.0.7

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 (2) hide show
  1. package/bin/claude-chats-sync.js +717 -620
  2. package/package.json +57 -57
@@ -1,620 +1,717 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Claude Code Sync CLI
5
- *
6
- * 跨平台命令行工具,用于同步 Claude Code 聊天会话到项目目录
7
- * Cross-platform CLI tool to sync Claude Code chat sessions to project directory
8
- *
9
- * Usage:
10
- * node claude-sync-cli.js init # Initialize sync
11
- * node claude-sync-cli.js status # Check sync status
12
- * node claude-sync-cli.js open # Open history folder
13
- * node claude-sync-cli.js clean # Clean sensitive data from session files
14
- * node claude-sync-cli.js setup-git-filter # Setup Git filter for auto-cleaning
15
- */
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
- const os = require('os');
20
- const { execSync } = require('child_process');
21
-
22
- // ANSI 颜色代码 / ANSI color codes
23
- const colors = {
24
- reset: '\x1b[0m',
25
- red: '\x1b[31m',
26
- green: '\x1b[32m',
27
- yellow: '\x1b[33m',
28
- blue: '\x1b[34m',
29
- cyan: '\x1b[36m'
30
- };
31
-
32
- // 工具函数 / Utility functions
33
- function log(message, color = 'reset') {
34
- console.log(`${colors[color]}${message}${colors.reset}`);
35
- }
36
-
37
- function error(message) {
38
- log(`❌ Error: ${message}`, 'red');
39
- }
40
-
41
- function success(message) {
42
- log(`✅ ${message}`, 'green');
43
- }
44
-
45
- function info(message) {
46
- log(`ℹ️ ${message}`, 'cyan');
47
- }
48
-
49
- function warn(message) {
50
- log(`⚠️ ${message}`, 'yellow');
51
- }
52
-
53
- /**
54
- * 规范化项目路径为 Claude Code 格式
55
- * Normalize project path to Claude Code format
56
- *
57
- * Windows: D:\Projects\MyProject -> d--Projects-MyProject
58
- * Linux/Mac: /home/user/projects/my-project -> home-user-projects-my-project
59
- */
60
- function normalizeProjectPath(projectPath) {
61
- if (process.platform === 'win32') {
62
- // Windows: Replace backslashes and colons with dashes, preserve case
63
- return projectPath
64
- .replace(/\\/g, '-')
65
- .replace(/:/g, '-');
66
- } else {
67
- // Linux/Mac: Replace forward slashes with dashes, preserve case
68
- return projectPath
69
- .replace(/^\//, '') // Remove leading slash
70
- .replace(/\//g, '-'); // Replace remaining slashes with dashes
71
- }
72
- }
73
-
74
- /**
75
- * 获取 Claude Code 项目目录
76
- * Get Claude Code projects directory
77
- */
78
- function getClaudeProjectsDir() {
79
- return path.join(os.homedir(), '.claude', 'projects');
80
- }
81
-
82
- /**
83
- * 获取项目中的历史文件夹路径
84
- * Get history folder path in the project
85
- */
86
- function getHistoryFolderPath(projectPath, folderName = '.claudeCodeSessions') {
87
- return path.join(projectPath, folderName);
88
- }
89
-
90
- /**
91
- * 创建符号链接 (跨平台)
92
- * Create symbolic link (cross-platform)
93
- */
94
- function createSymlink(target, linkPath) {
95
- if (process.platform === 'win32') {
96
- // Windows: 使用 junction (不需要管理员权限)
97
- // Windows: Use junction (no admin privileges required)
98
- fs.symlinkSync(target, linkPath, 'junction');
99
- } else {
100
- // Unix: 使用符号链接
101
- // Unix: Use symbolic link
102
- fs.symlinkSync(target, linkPath);
103
- }
104
- }
105
-
106
- /**
107
- * 检查是否为符号链接
108
- * Check if path is a symbolic link
109
- */
110
- function isSymlink(symlinkPath) {
111
- try {
112
- const stats = fs.lstatSync(symlinkPath);
113
- return stats.isSymbolicLink() || (process.platform === 'win32' && stats.isDirectory());
114
- } catch {
115
- return false;
116
- }
117
- }
118
-
119
- /**
120
- * 移动目录 (递归)
121
- * Move directory (recursive)
122
- */
123
- function moveDirectory(src, dest) {
124
- // 创建目标目录
125
- fs.mkdirSync(dest, { recursive: true });
126
-
127
- // 递归复制所有文件和子目录
128
- const entries = fs.readdirSync(src, { withFileTypes: true });
129
-
130
- for (const entry of entries) {
131
- const srcPath = path.join(src, entry.name);
132
- const destPath = path.join(dest, entry.name);
133
-
134
- if (entry.isDirectory()) {
135
- moveDirectory(srcPath, destPath);
136
- } else {
137
- fs.copyFileSync(srcPath, destPath);
138
- }
139
- }
140
-
141
- // 删除源目录
142
- fs.rmSync(src, { recursive: true, force: true });
143
- }
144
-
145
- /**
146
- * 清理会话文件内容中的敏感信息
147
- * Clean sensitive information from session file content
148
- */
149
- function cleanSensitiveData(content) {
150
- // Pattern for Anthropic API keys (normal format)
151
- const apiKeyPattern = /"primaryApiKey"\s*:\s*"sk-ant-[^"]*"/g;
152
-
153
- // Pattern for API keys within escaped JSON strings
154
- const apiKeyPatternEscaped = /\\"primaryApiKey\\":\s*\\"sk-ant-[^"]*\\"/g;
155
-
156
- // Pattern for ANTHROPIC_AUTH_TOKEN (escaped format)
157
- const authTokenPatternEscaped = /\\"ANTHROPIC_AUTH_TOKEN\\"\\s*:\\s*\\"[^"]*\\"/g;
158
-
159
- // Pattern for other API keys
160
- const genericApiKeyPattern = /"(apiKey|api_key|authorization|token|bearer)"\s*:\s*"[^"]*"/gi;
161
-
162
- // Clean API keys
163
- let cleaned = content.replace(apiKeyPattern, '"primaryApiKey": "[REDACTED]"');
164
- cleaned = cleaned.replace(apiKeyPatternEscaped, '\\"primaryApiKey\\": \\"[REDACTED]\\"');
165
- cleaned = cleaned.replace(authTokenPatternEscaped, '\\"ANTHROPIC_AUTH_TOKEN\\": \\"[REDACTED]\\"');
166
- cleaned = cleaned.replace(genericApiKeyPattern, '"$1": "[REDACTED]"');
167
-
168
- return cleaned;
169
- }
170
-
171
- /**
172
- * 初始化同步
173
- * Initialize sync
174
- */
175
- function init(projectPath, options = {}) {
176
- const { folderName = '.claudeCodeSessions', force = false } = options;
177
-
178
- const historyFolder = getHistoryFolderPath(projectPath, folderName);
179
- const claudeProjectsDir = getClaudeProjectsDir();
180
- const normalizedPath = normalizeProjectPath(projectPath);
181
- const symlinkPath = path.join(claudeProjectsDir, normalizedPath);
182
-
183
- try {
184
- // 检查符号链接是否已存在
185
- // Check if symlink already exists
186
- if (fs.existsSync(symlinkPath)) {
187
- if (isSymlink(symlinkPath)) {
188
- success('Claude Code Chats Sync already initialized');
189
- info(`History folder: ${historyFolder}`);
190
- info(`Linked to: ${symlinkPath}`);
191
- return;
192
- } else if (fs.lstatSync(symlinkPath).isDirectory()) {
193
- // 现有真实目录 - 用户之前使用过 Claude Code
194
- // Existing real directory - user has used Claude Code before
195
- const files = fs.readdirSync(symlinkPath);
196
- const sessionFiles = files.filter(f => f.endsWith('.jsonl'));
197
-
198
- if (sessionFiles.length > 0 && !force) {
199
- warn(`Found ${sessionFiles.length} existing Claude Code session(s) in Claude's storage.`);
200
- info('Use --force to move them to your project folder');
201
- return;
202
- }
203
-
204
- if (sessionFiles.length > 0 && force) {
205
- // 移动现有目录到项目文件夹
206
- // Move existing directory to project folder
207
- moveDirectory(symlinkPath, historyFolder);
208
- success(`Moved ${sessionFiles.length} session(s) to project folder!`);
209
- } else {
210
- // 空目录,直接删除
211
- // Empty directory, just remove it
212
- fs.rmSync(symlinkPath, { recursive: true, force: true });
213
- }
214
- } else {
215
- error(`A file exists at Claude Code location: ${symlinkPath}`);
216
- return;
217
- }
218
- }
219
-
220
- // 创建历史文件夹 (如果不存在)
221
- // Create history folder if it doesn't exist
222
- if (!fs.existsSync(historyFolder)) {
223
- fs.mkdirSync(historyFolder, { recursive: true });
224
- success(`Created folder: ${historyFolder}`);
225
- }
226
-
227
- // 确保 .claude/projects 目录存在
228
- // Ensure .claude/projects directory exists
229
- if (!fs.existsSync(claudeProjectsDir)) {
230
- fs.mkdirSync(claudeProjectsDir, { recursive: true });
231
- }
232
-
233
- // 创建符号链接
234
- // Create symbolic link
235
- createSymlink(historyFolder, symlinkPath);
236
-
237
- success('Claude Code Chats Sync initialized!');
238
- info(`History folder: ${historyFolder}`);
239
- info(`Linked to: ${symlinkPath}`);
240
-
241
- // 添加到 .gitignore
242
- // Add to .gitignore
243
- addToGitIgnore(projectPath, folderName);
244
-
245
- // 设置 Git 过滤器
246
- // Setup Git filter
247
- setupGitFilter(projectPath, folderName, false);
248
-
249
- } catch (err) {
250
- error(`Failed to initialize: ${err.message}`);
251
- }
252
- }
253
-
254
- /**
255
- * 检查同步状态
256
- * Check sync status
257
- */
258
- function status(projectPath, options = {}) {
259
- const { folderName = '.claudeCodeSessions' } = options;
260
-
261
- const historyFolder = getHistoryFolderPath(projectPath, folderName);
262
- const claudeProjectsDir = getClaudeProjectsDir();
263
- const normalizedPath = normalizeProjectPath(projectPath);
264
- const symlinkPath = path.join(claudeProjectsDir, normalizedPath);
265
-
266
- log('\n📊 Claude Code Chats Sync Status\n', 'blue');
267
-
268
- // 检查历史文件夹
269
- // Check history folder
270
- if (fs.existsSync(historyFolder)) {
271
- const files = fs.readdirSync(historyFolder).filter(f => f.endsWith('.jsonl'));
272
- success('History folder exists');
273
- info(` Path: ${historyFolder}`);
274
- info(` Sessions: ${files.length}`);
275
- } else {
276
- error('History folder not found');
277
- }
278
-
279
- // 检查符号链接
280
- // Check symlink
281
- if (fs.existsSync(symlinkPath)) {
282
- success('Symlink created');
283
- info(` Path: ${symlinkPath}`);
284
- } else {
285
- error('Symlink not created');
286
- }
287
-
288
- console.log('');
289
- }
290
-
291
- /**
292
- * 添加到 .gitignore
293
- * Add to .gitignore
294
- */
295
- function addToGitIgnore(projectPath, folderName = '.claudeCodeSessions') {
296
- const gitignorePath = path.join(projectPath, '.gitignore');
297
-
298
- try {
299
- let content = '';
300
- if (fs.existsSync(gitignorePath)) {
301
- content = fs.readFileSync(gitignorePath, 'utf-8');
302
- }
303
-
304
- const ignoreEntry = `# Claude Code conversation history
305
- # Uncomment the line below to ignore session files, OR configure Git filter for safe sharing
306
- # ${folderName}/`;
307
-
308
- // 仅在不存在时添加
309
- // Only add if not already present
310
- if (!content.includes(`# ${folderName}/`) && !content.includes(`${folderName}/`)) {
311
- if (content && !content.endsWith('\n')) {
312
- content += '\n';
313
- }
314
- content += `\n${ignoreEntry}\n`;
315
- fs.writeFileSync(gitignorePath, content, 'utf-8');
316
- success('Added .gitignore entry (commented by default)');
317
- }
318
- } catch (err) {
319
- warn('Could not update .gitignore (not a Git repository?)');
320
- }
321
- }
322
-
323
- /**
324
- * 设置 Git 过滤器
325
- * Setup Git filter
326
- */
327
- function setupGitFilter(projectPath, folderName = '.claudeCodeSessions', showMessage = true) {
328
- try {
329
- // 检查是否为 Git 仓库
330
- // Check if we're in a Git repository
331
- const gitDir = path.join(projectPath, '.git');
332
- if (!fs.existsSync(gitDir)) {
333
- warn('Not a Git repository. Git filter will not be configured.');
334
- return;
335
- }
336
-
337
- // 创建清理过滤器脚本
338
- // Create clean filter script
339
- const filterScriptPath = path.join(projectPath, '.gitfilters', 'clean-sessions.js');
340
- const filterDir = path.dirname(filterScriptPath);
341
-
342
- if (!fs.existsSync(filterDir)) {
343
- fs.mkdirSync(filterDir, { recursive: true });
344
- }
345
-
346
- const filterScript = `#!/usr/bin/env node
347
- const fs = require('fs');
348
-
349
- // Pattern for Anthropic API keys (normal format)
350
- const apiKeyPattern = /"primaryApiKey"\\\\s*:\\\\s*"sk-ant-[^"]*"/g;
351
-
352
- // Pattern for API keys within escaped JSON strings
353
- const apiKeyPatternEscaped = /\\\\\\\\"primaryApiKey\\\\\\\\"\\\\s*:\\\\\\\\s*\\\\\\"sk-ant-[^"]*\\\\\\"/g;
354
-
355
- // Pattern for ANTHROPIC_AUTH_TOKEN (escaped format)
356
- const authTokenPatternEscaped = /\\\\"ANTHROPIC_AUTH_TOKEN\\\\"\\\\s*:\\\\\\s*\\\\"[^"]*\\\\"/g;
357
-
358
- // Pattern for other API keys
359
- const genericApiKeyPattern = /"(apiKey|api_key|authorization|token|bearer)"\\\\s*:\\\\s*"[^"]*"/gi;
360
-
361
- let data = '';
362
- process.stdin.setEncoding('utf8');
363
-
364
- process.stdin.on('data', (chunk) => {
365
- data += chunk;
366
- });
367
-
368
- process.stdin.on('end', () => {
369
- let cleaned = data.replace(apiKeyPattern, '"primaryApiKey": "[REDACTED]"');
370
- cleaned = cleaned.replace(apiKeyPatternEscaped, '\\\\\\\\"primaryApiKey\\\\\\\\": \\\\"[REDACTED]\\\\"');
371
- cleaned = cleaned.replace(authTokenPatternEscaped, '\\\\\\\\"ANTHROPIC_AUTH_TOKEN\\\\\\\\": \\\\"[REDACTED]\\\\"');
372
- cleaned = cleaned.replace(genericApiKeyPattern, '"$1": "[REDACTED]"');
373
- process.stdout.write(cleaned);
374
- });
375
- `;
376
-
377
- fs.writeFileSync(filterScriptPath, filterScript, 'utf-8');
378
-
379
- // 在 Unix-like 系统上设置为可执行
380
- // Make it executable on Unix-like systems
381
- if (process.platform !== 'win32') {
382
- try {
383
- fs.chmodSync(filterScriptPath, 0o755);
384
- } catch (e) {
385
- // Ignore permission errors
386
- }
387
- }
388
-
389
- // .gitconfig 中配置 Git 过滤器
390
- // Configure Git filter in .gitconfig
391
- const gitConfigPath = path.join(projectPath, '.gitconfig');
392
-
393
- let gitConfig = '';
394
- if (fs.existsSync(gitConfigPath)) {
395
- gitConfig = fs.readFileSync(gitConfigPath, 'utf-8');
396
- }
397
-
398
- if (!gitConfig.includes('[filter "claude-clean"]')) {
399
- if (gitConfig && !gitConfig.endsWith('\n')) {
400
- gitConfig += '\n';
401
- }
402
- gitConfig += `[filter "claude-clean"]
403
- clean = node .gitfilters/clean-sessions.js
404
- `;
405
- fs.writeFileSync(gitConfigPath, gitConfig, 'utf-8');
406
- }
407
-
408
- // 在本地 Git 配置中配置过滤器
409
- // Configure the filter in local Git config
410
- try {
411
- execSync(
412
- `git config filter.claude-clean.clean "node .gitfilters/clean-sessions.js"`,
413
- { cwd: projectPath, stdio: 'pipe' }
414
- );
415
- } catch (err) {
416
- warn(`Failed to configure local Git filter: ${err.message}`);
417
- }
418
-
419
- // 在 .gitattributes 中配置过滤器
420
- // Configure the filter in .gitattributes
421
- const gitAttributesPath = path.join(projectPath, '.gitattributes');
422
-
423
- let gitAttributes = '';
424
- if (fs.existsSync(gitAttributesPath)) {
425
- gitAttributes = fs.readFileSync(gitAttributesPath, 'utf-8');
426
- }
427
-
428
- const filterLine = `${folderName}/*.jsonl filter=claude-clean`;
429
-
430
- if (!gitAttributes.includes(filterLine)) {
431
- if (gitAttributes && !gitAttributes.endsWith('\n')) {
432
- gitAttributes += '\n';
433
- }
434
- gitAttributes += `\n# Claude Code sessions - clean sensitive data on commit\n${filterLine}\n`;
435
- fs.writeFileSync(gitAttributesPath, gitAttributes, 'utf-8');
436
- }
437
-
438
- if (showMessage) {
439
- success('Git filter configured');
440
- info('Session files will be automatically cleaned on commit');
441
- info('Original files remain unchanged. Only committed versions are cleaned.');
442
- }
443
-
444
- } catch (err) {
445
- error(`Failed to setup Git filter: ${err.message}`);
446
- }
447
- }
448
-
449
- /**
450
- * 清理会话文件中的敏感数据
451
- * Clean sensitive data from session files
452
- */
453
- function cleanSessions(projectPath, options = {}) {
454
- const { folderName = '.claudeCodeSessions' } = options;
455
-
456
- const historyFolder = getHistoryFolderPath(projectPath, folderName);
457
-
458
- if (!fs.existsSync(historyFolder)) {
459
- error('History folder does not exist');
460
- return;
461
- }
462
-
463
- const files = fs.readdirSync(historyFolder).filter(f => f.endsWith('.jsonl'));
464
-
465
- if (files.length === 0) {
466
- warn('No session files to clean');
467
- return;
468
- }
469
-
470
- info(`Cleaning ${files.length} session file(s)...`);
471
-
472
- let cleanedCount = 0;
473
- for (const file of files) {
474
- const filePath = path.join(historyFolder, file);
475
- const content = fs.readFileSync(filePath, 'utf-8');
476
- const cleaned = cleanSensitiveData(content);
477
-
478
- // 将清理后的内容写回文件
479
- // Write cleaned content back to file
480
- fs.writeFileSync(filePath, cleaned, 'utf-8');
481
- cleanedCount++;
482
- }
483
-
484
- success(`Cleaned ${cleanedCount} session file(s)`);
485
- info('Sensitive data has been redacted');
486
- }
487
-
488
- /**
489
- * 打开历史文件夹
490
- * Open history folder
491
- */
492
- function openFolder(projectPath, options = {}) {
493
- const { folderName = '.claudeCodeSessions' } = options;
494
-
495
- const historyFolder = getHistoryFolderPath(projectPath, folderName);
496
-
497
- if (!fs.existsSync(historyFolder)) {
498
- error('History folder does not exist. Please initialize first.');
499
- return;
500
- }
501
-
502
- try {
503
- const { exec } = require('child_process');
504
-
505
- let command;
506
- switch (process.platform) {
507
- case 'darwin':
508
- command = 'open';
509
- break;
510
- case 'win32':
511
- command = 'explorer';
512
- break;
513
- default:
514
- command = 'xdg-open';
515
- }
516
-
517
- exec(`${command} "${historyFolder}"`);
518
- success(`Opened history folder: ${historyFolder}`);
519
- } catch (err) {
520
- error(`Failed to open folder: ${err.message}`);
521
- }
522
- }
523
-
524
- /**
525
- * 显示帮助信息
526
- * Show help message
527
- */
528
- function showHelp() {
529
- const help = `
530
- Claude Code Sync CLI - 跨平台 Claude Code 会话同步工具
531
- Claude Code Sync CLI - Cross-platform Claude Code session sync tool
532
-
533
- Usage: node claude-sync-cli.js <command> [options]
534
-
535
- Commands:
536
- init Initialize sync for current project
537
- status Check sync status and session count
538
- open Open history folder in file manager
539
- clean Clean sensitive data from session files
540
- setup-git-filter Setup Git filter for automatic cleaning
541
- help Show this help message
542
-
543
- Options:
544
- --folder-name <name> History folder name (default: .claudeCodeSessions)
545
- --force Force migration of existing sessions
546
- --project-path <path> Project path (default: current directory)
547
-
548
- Examples:
549
- node claude-sync-cli.js init
550
- node claude-sync-cli.js init --folder-name .sessions
551
- node claude-sync-cli.js init --force
552
- node claude-sync-cli.js status
553
- node claude-sync-cli.js clean
554
- node claude-sync-cli.js setup-git-filter
555
-
556
- Environment Variables:
557
- ANTHROPIC_AUTH_TOKEN Recommended: Configure API key via env var
558
- ANTHROPIC_BASE_URL Optional: Third-party API endpoint
559
-
560
- For more information, visit: https://github.com/tubo70/claude-chats-sync-cli
561
- `;
562
-
563
- console.log(help);
564
- }
565
-
566
- /**
567
- * 主函数
568
- * Main function
569
- */
570
- function main() {
571
- const args = process.argv.slice(2);
572
- const command = args[0];
573
-
574
- // 解析选项
575
- // Parse options
576
- const options = {};
577
- let projectPath = process.cwd();
578
-
579
- for (let i = 1; i < args.length; i++) {
580
- const arg = args[i];
581
- if (arg === '--folder-name' && args[i + 1]) {
582
- options.folderName = args[++i];
583
- } else if (arg === '--project-path' && args[i + 1]) {
584
- projectPath = args[++i];
585
- } else if (arg === '--force') {
586
- options.force = true;
587
- }
588
- }
589
-
590
- if (!command || command === 'help' || command === '--help' || command === '-h') {
591
- showHelp();
592
- return;
593
- }
594
-
595
- switch (command) {
596
- case 'init':
597
- init(projectPath, options);
598
- break;
599
- case 'status':
600
- status(projectPath, options);
601
- break;
602
- case 'open':
603
- openFolder(projectPath, options);
604
- break;
605
- case 'clean':
606
- cleanSessions(projectPath, options);
607
- break;
608
- case 'setup-git-filter':
609
- setupGitFilter(projectPath, options.folderName, true);
610
- break;
611
- default:
612
- error(`Unknown command: ${command}`);
613
- info('Run "node claude-sync-cli.js help" for usage information');
614
- process.exit(1);
615
- }
616
- }
617
-
618
- // 运行主函数
619
- // Run main function
620
- main();
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Code Sync CLI
5
+ *
6
+ * 跨平台命令行工具,用于同步 Claude Code 聊天会话到项目目录
7
+ * Cross-platform CLI tool to sync Claude Code chat sessions to project directory
8
+ *
9
+ * Usage:
10
+ * node claude-sync-cli.js init # Initialize sync
11
+ * node claude-sync-cli.js status # Check sync status
12
+ * node claude-sync-cli.js open # Open history folder
13
+ * node claude-sync-cli.js clean # Clean sensitive data from session files
14
+ * node claude-sync-cli.js setup-git-filter # Setup Git filter for auto-cleaning
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const { execSync } = require('child_process');
21
+
22
+ // ANSI 颜色代码 / ANSI color codes
23
+ const colors = {
24
+ reset: '\x1b[0m',
25
+ red: '\x1b[31m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ blue: '\x1b[34m',
29
+ cyan: '\x1b[36m'
30
+ };
31
+
32
+ // 工具函数 / Utility functions
33
+ function log(message, color = 'reset') {
34
+ console.log(`${colors[color]}${message}${colors.reset}`);
35
+ }
36
+
37
+ function error(message) {
38
+ log(`❌ Error: ${message}`, 'red');
39
+ }
40
+
41
+ function success(message) {
42
+ log(`✅ ${message}`, 'green');
43
+ }
44
+
45
+ function info(message) {
46
+ log(`ℹ️ ${message}`, 'cyan');
47
+ }
48
+
49
+ function warn(message) {
50
+ log(`⚠️ ${message}`, 'yellow');
51
+ }
52
+
53
+ /**
54
+ * 规范化项目路径为 Claude Code 格式
55
+ * Normalize project path to Claude Code format
56
+ *
57
+ * Windows: D:\Projects\MyProject -> d--Projects-MyProject
58
+ * Linux/Mac: /home/user/projects/my-project -> -home-user-projects-my-project
59
+ */
60
+ function normalizeProjectPath(projectPath) {
61
+ if (process.platform === 'win32') {
62
+ // Windows: Replace backslashes and colons with dashes, preserve case
63
+ return projectPath
64
+ .replace(/\\/g, '-')
65
+ .replace(/:/g, '-');
66
+ } else {
67
+ // Linux/Mac: Replace forward slashes with dashes, preserve case
68
+ // Note: Claude Code adds a leading dash for Unix paths
69
+ return projectPath
70
+ .replace(/^\//, '-') // Replace leading slash with dash
71
+ .replace(/\//g, '-'); // Replace remaining slashes with dashes
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 获取 Claude Code 项目目录
77
+ * Get Claude Code projects directory
78
+ */
79
+ function getClaudeProjectsDir() {
80
+ return path.join(os.homedir(), '.claude', 'projects');
81
+ }
82
+
83
+ /**
84
+ * 获取项目中的历史文件夹路径
85
+ * Get history folder path in the project
86
+ */
87
+ function getHistoryFolderPath(projectPath, folderName = '.claudeCodeSessions') {
88
+ return path.join(projectPath, folderName);
89
+ }
90
+
91
+ /**
92
+ * 创建符号链接 (跨平台)
93
+ * Create symbolic link (cross-platform)
94
+ */
95
+ function createSymlink(target, linkPath) {
96
+ if (process.platform === 'win32') {
97
+ // Windows: 使用 junction (不需要管理员权限)
98
+ // Windows: Use junction (no admin privileges required)
99
+ fs.symlinkSync(target, linkPath, 'junction');
100
+ } else {
101
+ // Unix: 使用符号链接
102
+ // Unix: Use symbolic link
103
+ fs.symlinkSync(target, linkPath);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 检查是否为符号链接(并验证链接指向)
109
+ * Check if path is a symbolic link (and verify link target)
110
+ */
111
+ function isSymlink(symlinkPath, expectedTarget) {
112
+ try {
113
+ const stats = fs.lstatSync(symlinkPath);
114
+
115
+ // Unix: 检查是否为符号链接
116
+ if (process.platform !== 'win32') {
117
+ return stats.isSymbolicLink();
118
+ }
119
+
120
+ // Windows: 需要区分 junction 和普通目录
121
+ // 检查是否为符号链接或 junction
122
+ if (stats.isSymbolicLink()) {
123
+ return true;
124
+ }
125
+
126
+ // 对于 Windows,如果是目录,还需要检查是否为 junction
127
+ // 并且验证其指向是否正确
128
+ if (stats.isDirectory()) {
129
+ try {
130
+ // 读取链接目标
131
+ const target = fs.readlinkSync(symlinkPath);
132
+ // 如果能读取到链接目标,说明是 junction 或符号链接
133
+ // 如果提供了期望目标,则验证是否匹配
134
+ if (expectedTarget) {
135
+ return path.resolve(target) === path.resolve(expectedTarget);
136
+ }
137
+ return true;
138
+ } catch {
139
+ // 如果读取链接失败,说明是普通目录,不是链接
140
+ return false;
141
+ }
142
+ }
143
+
144
+ return false;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * 移动目录 (递归)
152
+ * Move directory (recursive)
153
+ */
154
+ function moveDirectory(src, dest) {
155
+ // 创建目标目录
156
+ fs.mkdirSync(dest, { recursive: true });
157
+
158
+ // 递归复制所有文件和子目录
159
+ const entries = fs.readdirSync(src, { withFileTypes: true });
160
+
161
+ for (const entry of entries) {
162
+ const srcPath = path.join(src, entry.name);
163
+ const destPath = path.join(dest, entry.name);
164
+
165
+ if (entry.isDirectory()) {
166
+ moveDirectory(srcPath, destPath);
167
+ } else {
168
+ fs.copyFileSync(srcPath, destPath);
169
+ }
170
+ }
171
+
172
+ // 删除源目录
173
+ fs.rmSync(src, { recursive: true, force: true });
174
+ }
175
+
176
+ /**
177
+ * 合并两个目录的文件
178
+ * Merge files from two directories
179
+ */
180
+ function mergeDirectories(src, dest) {
181
+ // 确保目标目录存在
182
+ fs.mkdirSync(dest, { recursive: true });
183
+
184
+ // 读取源目录中的所有文件
185
+ const entries = fs.readdirSync(src, { withFileTypes: true });
186
+
187
+ let mergedCount = 0;
188
+
189
+ for (const entry of entries) {
190
+ const srcPath = path.join(src, entry.name);
191
+ const destPath = path.join(dest, entry.name);
192
+
193
+ if (entry.isDirectory()) {
194
+ // 递归合并子目录
195
+ mergeDirectories(srcPath, destPath);
196
+ } else {
197
+ // 如果目标文件不存在,则复制;如果存在,跳过(保留目标文件)
198
+ if (!fs.existsSync(destPath)) {
199
+ fs.copyFileSync(srcPath, destPath);
200
+ mergedCount++;
201
+ }
202
+ }
203
+ }
204
+
205
+ return mergedCount;
206
+ }
207
+
208
+ /**
209
+ * 清理会话文件内容中的敏感信息
210
+ * Clean sensitive information from session file content
211
+ */
212
+ function cleanSensitiveData(content) {
213
+ // Pattern for Anthropic API keys (normal format)
214
+ const apiKeyPattern = /"primaryApiKey"\s*:\s*"sk-ant-[^"]*"/g;
215
+
216
+ // Pattern for API keys within escaped JSON strings
217
+ const apiKeyPatternEscaped = /\\"primaryApiKey\\":\s*\\"sk-ant-[^"]*\\"/g;
218
+
219
+ // Pattern for ANTHROPIC_AUTH_TOKEN (escaped format)
220
+ const authTokenPatternEscaped = /\\"ANTHROPIC_AUTH_TOKEN\\"\\s*:\\s*\\"[^"]*\\"/g;
221
+
222
+ // Pattern for other API keys
223
+ const genericApiKeyPattern = /"(apiKey|api_key|authorization|token|bearer)"\s*:\s*"[^"]*"/gi;
224
+
225
+ // Clean API keys
226
+ let cleaned = content.replace(apiKeyPattern, '"primaryApiKey": "[REDACTED]"');
227
+ cleaned = cleaned.replace(apiKeyPatternEscaped, '\\"primaryApiKey\\": \\"[REDACTED]\\"');
228
+ cleaned = cleaned.replace(authTokenPatternEscaped, '\\"ANTHROPIC_AUTH_TOKEN\\": \\"[REDACTED]\\"');
229
+ cleaned = cleaned.replace(genericApiKeyPattern, '"$1": "[REDACTED]"');
230
+
231
+ return cleaned;
232
+ }
233
+
234
+ /**
235
+ * 初始化同步
236
+ * Initialize sync
237
+ */
238
+ function init(projectPath, options = {}) {
239
+ const { folderName = '.claudeCodeSessions', force = false } = options;
240
+
241
+ const historyFolder = getHistoryFolderPath(projectPath, folderName);
242
+ const claudeProjectsDir = getClaudeProjectsDir();
243
+ const normalizedPath = normalizeProjectPath(projectPath);
244
+ const symlinkPath = path.join(claudeProjectsDir, normalizedPath);
245
+
246
+ try {
247
+ // 检查符号链接是否已存在
248
+ // Check if symlink already exists
249
+ if (fs.existsSync(symlinkPath)) {
250
+ if (isSymlink(symlinkPath, historyFolder)) {
251
+ success('Claude Code Chats Sync already initialized');
252
+ info(`History folder: ${historyFolder}`);
253
+ info(`Linked to: ${symlinkPath}`);
254
+ return;
255
+ } else if (fs.lstatSync(symlinkPath).isDirectory()) {
256
+ // 现有真实目录 - 用户之前使用过 Claude Code
257
+ // Existing real directory - user has used Claude Code before
258
+ const claudeStorageFiles = fs.readdirSync(symlinkPath);
259
+ const claudeStorageSessions = claudeStorageFiles.filter(f => f.endsWith('.jsonl'));
260
+
261
+ // 检查项目中是否已经有会话文件夹
262
+ const projectHistoryExists = fs.existsSync(historyFolder);
263
+ let projectSessions = [];
264
+ if (projectHistoryExists) {
265
+ projectSessions = fs.readdirSync(historyFolder).filter(f => f.endsWith('.jsonl'));
266
+ }
267
+
268
+ // 场景1: Claude存储和项目中都没有会话文件
269
+ if (claudeStorageSessions.length === 0 && projectSessions.length === 0) {
270
+ // 都是空目录,直接删除Claude存储的目录
271
+ fs.rmSync(symlinkPath, { recursive: true, force: true });
272
+ }
273
+ // 场景2: 只有Claude存储中有会话文件
274
+ else if (claudeStorageSessions.length > 0 && projectSessions.length === 0) {
275
+ if (!force) {
276
+ warn(`Found ${claudeStorageSessions.length} existing Claude Code session(s) in Claude's storage.`);
277
+ info('Use --force to move them to your project folder');
278
+ return;
279
+ }
280
+ // 移动Claude存储的目录到项目文件夹
281
+ moveDirectory(symlinkPath, historyFolder);
282
+ success(`Moved ${claudeStorageSessions.length} session(s) to project folder!`);
283
+ }
284
+ // 场景3: 只有项目中有会话文件
285
+ else if (claudeStorageSessions.length === 0 && projectSessions.length > 0) {
286
+ // 删除Claude存储中的空目录
287
+ fs.rmSync(symlinkPath, { recursive: true, force: true });
288
+ info(`Using existing ${projectSessions.length} session(s) from project folder`);
289
+ }
290
+ // 场景4: Claude存储和项目中都有会话文件 - 需要合并
291
+ else if (claudeStorageSessions.length > 0 && projectSessions.length > 0) {
292
+ if (!force) {
293
+ warn(`Found sessions in both locations:`);
294
+ info(` - Claude's storage: ${claudeStorageSessions.length} session(s)`);
295
+ info(` - Project folder: ${projectSessions.length} session(s)`);
296
+ info('Use --force to merge them into your project folder');
297
+ return;
298
+ }
299
+ // 合并目录: 将Claude存储的会话合并到项目中
300
+ const mergedCount = mergeDirectories(symlinkPath, historyFolder);
301
+ fs.rmSync(symlinkPath, { recursive: true, force: true });
302
+ success(`Merged ${mergedCount} session(s) from Claude's storage to project folder!`);
303
+ info(`Total sessions in project: ${projectSessions.length + mergedCount}`);
304
+ }
305
+ } else {
306
+ error(`A file exists at Claude Code location: ${symlinkPath}`);
307
+ return;
308
+ }
309
+ }
310
+
311
+ // 创建历史文件夹 (如果不存在)
312
+ // Create history folder if it doesn't exist
313
+ if (!fs.existsSync(historyFolder)) {
314
+ fs.mkdirSync(historyFolder, { recursive: true });
315
+ success(`Created folder: ${historyFolder}`);
316
+ } else {
317
+ // 显示项目中的会话数量
318
+ const existingSessions = fs.readdirSync(historyFolder).filter(f => f.endsWith('.jsonl'));
319
+ if (existingSessions.length > 0) {
320
+ info(`Using existing ${existingSessions.length} session(s) from project folder`);
321
+ }
322
+ }
323
+
324
+ // 确保 .claude/projects 目录存在
325
+ // Ensure .claude/projects directory exists
326
+ if (!fs.existsSync(claudeProjectsDir)) {
327
+ fs.mkdirSync(claudeProjectsDir, { recursive: true });
328
+ }
329
+
330
+ // 创建符号链接
331
+ // Create symbolic link
332
+ createSymlink(historyFolder, symlinkPath);
333
+
334
+ success('Claude Code Chats Sync initialized!');
335
+ info(`History folder: ${historyFolder}`);
336
+ info(`Linked to: ${symlinkPath}`);
337
+
338
+ // 添加到 .gitignore
339
+ // Add to .gitignore
340
+ addToGitIgnore(projectPath, folderName);
341
+
342
+ // 设置 Git 过滤器
343
+ // Setup Git filter
344
+ setupGitFilter(projectPath, folderName, false);
345
+
346
+ } catch (err) {
347
+ error(`Failed to initialize: ${err.message}`);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * 检查同步状态
353
+ * Check sync status
354
+ */
355
+ function status(projectPath, options = {}) {
356
+ const { folderName = '.claudeCodeSessions' } = options;
357
+
358
+ const historyFolder = getHistoryFolderPath(projectPath, folderName);
359
+ const claudeProjectsDir = getClaudeProjectsDir();
360
+ const normalizedPath = normalizeProjectPath(projectPath);
361
+ const symlinkPath = path.join(claudeProjectsDir, normalizedPath);
362
+
363
+ log('\n📊 Claude Code Chats Sync Status\n', 'blue');
364
+
365
+ // 检查历史文件夹
366
+ // Check history folder
367
+ if (fs.existsSync(historyFolder)) {
368
+ const files = fs.readdirSync(historyFolder).filter(f => f.endsWith('.jsonl'));
369
+ success('History folder exists');
370
+ info(` Path: ${historyFolder}`);
371
+ info(` Sessions: ${files.length}`);
372
+ } else {
373
+ error('History folder not found');
374
+ }
375
+
376
+ // 检查符号链接
377
+ // Check symlink
378
+ if (fs.existsSync(symlinkPath)) {
379
+ success('Symlink created');
380
+ info(` Path: ${symlinkPath}`);
381
+ } else {
382
+ error('Symlink not created');
383
+ }
384
+
385
+ console.log('');
386
+ }
387
+
388
+ /**
389
+ * 添加到 .gitignore
390
+ * Add to .gitignore
391
+ */
392
+ function addToGitIgnore(projectPath, folderName = '.claudeCodeSessions') {
393
+ const gitignorePath = path.join(projectPath, '.gitignore');
394
+
395
+ try {
396
+ let content = '';
397
+ if (fs.existsSync(gitignorePath)) {
398
+ content = fs.readFileSync(gitignorePath, 'utf-8');
399
+ }
400
+
401
+ const ignoreEntry = `# Claude Code conversation history
402
+ # Uncomment the line below to ignore session files, OR configure Git filter for safe sharing
403
+ # ${folderName}/`;
404
+
405
+ // 仅在不存在时添加
406
+ // Only add if not already present
407
+ if (!content.includes(`# ${folderName}/`) && !content.includes(`${folderName}/`)) {
408
+ if (content && !content.endsWith('\n')) {
409
+ content += '\n';
410
+ }
411
+ content += `\n${ignoreEntry}\n`;
412
+ fs.writeFileSync(gitignorePath, content, 'utf-8');
413
+ success('Added .gitignore entry (commented by default)');
414
+ }
415
+ } catch (err) {
416
+ warn('Could not update .gitignore (not a Git repository?)');
417
+ }
418
+ }
419
+
420
+ /**
421
+ * 设置 Git 过滤器
422
+ * Setup Git filter
423
+ */
424
+ function setupGitFilter(projectPath, folderName = '.claudeCodeSessions', showMessage = true) {
425
+ try {
426
+ // 检查是否为 Git 仓库
427
+ // Check if we're in a Git repository
428
+ const gitDir = path.join(projectPath, '.git');
429
+ if (!fs.existsSync(gitDir)) {
430
+ warn('Not a Git repository. Git filter will not be configured.');
431
+ return;
432
+ }
433
+
434
+ // 创建清理过滤器脚本
435
+ // Create clean filter script
436
+ const filterScriptPath = path.join(projectPath, '.gitfilters', 'clean-sessions.js');
437
+ const filterDir = path.dirname(filterScriptPath);
438
+
439
+ if (!fs.existsSync(filterDir)) {
440
+ fs.mkdirSync(filterDir, { recursive: true });
441
+ }
442
+
443
+ const filterScript = `#!/usr/bin/env node
444
+ const fs = require('fs');
445
+
446
+ // Pattern for Anthropic API keys (normal format)
447
+ const apiKeyPattern = /"primaryApiKey"\\\\s*:\\\\s*"sk-ant-[^"]*"/g;
448
+
449
+ // Pattern for API keys within escaped JSON strings
450
+ const apiKeyPatternEscaped = /\\\\\\\\"primaryApiKey\\\\\\\\"\\\\s*:\\\\\\\\s*\\\\\\"sk-ant-[^"]*\\\\\\"/g;
451
+
452
+ // Pattern for ANTHROPIC_AUTH_TOKEN (escaped format)
453
+ const authTokenPatternEscaped = /\\\\"ANTHROPIC_AUTH_TOKEN\\\\"\\\\s*:\\\\\\s*\\\\"[^"]*\\\\"/g;
454
+
455
+ // Pattern for other API keys
456
+ const genericApiKeyPattern = /"(apiKey|api_key|authorization|token|bearer)"\\\\s*:\\\\s*"[^"]*"/gi;
457
+
458
+ let data = '';
459
+ process.stdin.setEncoding('utf8');
460
+
461
+ process.stdin.on('data', (chunk) => {
462
+ data += chunk;
463
+ });
464
+
465
+ process.stdin.on('end', () => {
466
+ let cleaned = data.replace(apiKeyPattern, '"primaryApiKey": "[REDACTED]"');
467
+ cleaned = cleaned.replace(apiKeyPatternEscaped, '\\\\\\\\"primaryApiKey\\\\\\\\": \\\\"[REDACTED]\\\\"');
468
+ cleaned = cleaned.replace(authTokenPatternEscaped, '\\\\\\\\"ANTHROPIC_AUTH_TOKEN\\\\\\\\": \\\\"[REDACTED]\\\\"');
469
+ cleaned = cleaned.replace(genericApiKeyPattern, '"$1": "[REDACTED]"');
470
+ process.stdout.write(cleaned);
471
+ });
472
+ `;
473
+
474
+ fs.writeFileSync(filterScriptPath, filterScript, 'utf-8');
475
+
476
+ // Unix-like 系统上设置为可执行
477
+ // Make it executable on Unix-like systems
478
+ if (process.platform !== 'win32') {
479
+ try {
480
+ fs.chmodSync(filterScriptPath, 0o755);
481
+ } catch (e) {
482
+ // Ignore permission errors
483
+ }
484
+ }
485
+
486
+ // 在 .gitconfig 中配置 Git 过滤器
487
+ // Configure Git filter in .gitconfig
488
+ const gitConfigPath = path.join(projectPath, '.gitconfig');
489
+
490
+ let gitConfig = '';
491
+ if (fs.existsSync(gitConfigPath)) {
492
+ gitConfig = fs.readFileSync(gitConfigPath, 'utf-8');
493
+ }
494
+
495
+ if (!gitConfig.includes('[filter "claude-clean"]')) {
496
+ if (gitConfig && !gitConfig.endsWith('\n')) {
497
+ gitConfig += '\n';
498
+ }
499
+ gitConfig += `[filter "claude-clean"]
500
+ clean = node .gitfilters/clean-sessions.js
501
+ `;
502
+ fs.writeFileSync(gitConfigPath, gitConfig, 'utf-8');
503
+ }
504
+
505
+ // 在本地 Git 配置中配置过滤器
506
+ // Configure the filter in local Git config
507
+ try {
508
+ execSync(
509
+ `git config filter.claude-clean.clean "node .gitfilters/clean-sessions.js"`,
510
+ { cwd: projectPath, stdio: 'pipe' }
511
+ );
512
+ } catch (err) {
513
+ warn(`Failed to configure local Git filter: ${err.message}`);
514
+ }
515
+
516
+ // 在 .gitattributes 中配置过滤器
517
+ // Configure the filter in .gitattributes
518
+ const gitAttributesPath = path.join(projectPath, '.gitattributes');
519
+
520
+ let gitAttributes = '';
521
+ if (fs.existsSync(gitAttributesPath)) {
522
+ gitAttributes = fs.readFileSync(gitAttributesPath, 'utf-8');
523
+ }
524
+
525
+ const filterLine = `${folderName}/*.jsonl filter=claude-clean`;
526
+
527
+ if (!gitAttributes.includes(filterLine)) {
528
+ if (gitAttributes && !gitAttributes.endsWith('\n')) {
529
+ gitAttributes += '\n';
530
+ }
531
+ gitAttributes += `\n# Claude Code sessions - clean sensitive data on commit\n${filterLine}\n`;
532
+ fs.writeFileSync(gitAttributesPath, gitAttributes, 'utf-8');
533
+ }
534
+
535
+ if (showMessage) {
536
+ success('Git filter configured');
537
+ info('Session files will be automatically cleaned on commit');
538
+ info('Original files remain unchanged. Only committed versions are cleaned.');
539
+ }
540
+
541
+ } catch (err) {
542
+ error(`Failed to setup Git filter: ${err.message}`);
543
+ }
544
+ }
545
+
546
+ /**
547
+ * 清理会话文件中的敏感数据
548
+ * Clean sensitive data from session files
549
+ */
550
+ function cleanSessions(projectPath, options = {}) {
551
+ const { folderName = '.claudeCodeSessions' } = options;
552
+
553
+ const historyFolder = getHistoryFolderPath(projectPath, folderName);
554
+
555
+ if (!fs.existsSync(historyFolder)) {
556
+ error('History folder does not exist');
557
+ return;
558
+ }
559
+
560
+ const files = fs.readdirSync(historyFolder).filter(f => f.endsWith('.jsonl'));
561
+
562
+ if (files.length === 0) {
563
+ warn('No session files to clean');
564
+ return;
565
+ }
566
+
567
+ info(`Cleaning ${files.length} session file(s)...`);
568
+
569
+ let cleanedCount = 0;
570
+ for (const file of files) {
571
+ const filePath = path.join(historyFolder, file);
572
+ const content = fs.readFileSync(filePath, 'utf-8');
573
+ const cleaned = cleanSensitiveData(content);
574
+
575
+ // 将清理后的内容写回文件
576
+ // Write cleaned content back to file
577
+ fs.writeFileSync(filePath, cleaned, 'utf-8');
578
+ cleanedCount++;
579
+ }
580
+
581
+ success(`Cleaned ${cleanedCount} session file(s)`);
582
+ info('Sensitive data has been redacted');
583
+ }
584
+
585
+ /**
586
+ * 打开历史文件夹
587
+ * Open history folder
588
+ */
589
+ function openFolder(projectPath, options = {}) {
590
+ const { folderName = '.claudeCodeSessions' } = options;
591
+
592
+ const historyFolder = getHistoryFolderPath(projectPath, folderName);
593
+
594
+ if (!fs.existsSync(historyFolder)) {
595
+ error('History folder does not exist. Please initialize first.');
596
+ return;
597
+ }
598
+
599
+ try {
600
+ const { exec } = require('child_process');
601
+
602
+ let command;
603
+ switch (process.platform) {
604
+ case 'darwin':
605
+ command = 'open';
606
+ break;
607
+ case 'win32':
608
+ command = 'explorer';
609
+ break;
610
+ default:
611
+ command = 'xdg-open';
612
+ }
613
+
614
+ exec(`${command} "${historyFolder}"`);
615
+ success(`Opened history folder: ${historyFolder}`);
616
+ } catch (err) {
617
+ error(`Failed to open folder: ${err.message}`);
618
+ }
619
+ }
620
+
621
+ /**
622
+ * 显示帮助信息
623
+ * Show help message
624
+ */
625
+ function showHelp() {
626
+ const help = `
627
+ Claude Code Sync CLI - 跨平台 Claude Code 会话同步工具
628
+ Claude Code Sync CLI - Cross-platform Claude Code session sync tool
629
+
630
+ Usage: node claude-sync-cli.js <command> [options]
631
+
632
+ Commands:
633
+ init Initialize sync for current project
634
+ status Check sync status and session count
635
+ open Open history folder in file manager
636
+ clean Clean sensitive data from session files
637
+ setup-git-filter Setup Git filter for automatic cleaning
638
+ help Show this help message
639
+
640
+ Options:
641
+ --folder-name <name> History folder name (default: .claudeCodeSessions)
642
+ --force Force migration of existing sessions
643
+ --project-path <path> Project path (default: current directory)
644
+
645
+ Examples:
646
+ node claude-chats-sync.js init
647
+ node claude-chats-sync.js init --folder-name .sessions
648
+ node claude-chats-sync.js init --force
649
+ node claude-chats-sync.js status
650
+ node claude-chats-sync.js clean
651
+ node claude-chats-sync.js setup-git-filter
652
+
653
+ Environment Variables:
654
+ ANTHROPIC_AUTH_TOKEN Recommended: Configure API key via env var
655
+ ANTHROPIC_BASE_URL Optional: Third-party API endpoint
656
+
657
+ For more information, visit: https://github.com/tubo70/claude-chats-sync-cli
658
+ `;
659
+
660
+ console.log(help);
661
+ }
662
+
663
+ /**
664
+ * 主函数
665
+ * Main function
666
+ */
667
+ function main() {
668
+ const args = process.argv.slice(2);
669
+ const command = args[0];
670
+
671
+ // 解析选项
672
+ // Parse options
673
+ const options = {};
674
+ let projectPath = process.cwd();
675
+
676
+ for (let i = 1; i < args.length; i++) {
677
+ const arg = args[i];
678
+ if (arg === '--folder-name' && args[i + 1]) {
679
+ options.folderName = args[++i];
680
+ } else if (arg === '--project-path' && args[i + 1]) {
681
+ projectPath = args[++i];
682
+ } else if (arg === '--force') {
683
+ options.force = true;
684
+ }
685
+ }
686
+
687
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
688
+ showHelp();
689
+ return;
690
+ }
691
+
692
+ switch (command) {
693
+ case 'init':
694
+ init(projectPath, options);
695
+ break;
696
+ case 'status':
697
+ status(projectPath, options);
698
+ break;
699
+ case 'open':
700
+ openFolder(projectPath, options);
701
+ break;
702
+ case 'clean':
703
+ cleanSessions(projectPath, options);
704
+ break;
705
+ case 'setup-git-filter':
706
+ setupGitFilter(projectPath, options.folderName, true);
707
+ break;
708
+ default:
709
+ error(`Unknown command: ${command}`);
710
+ info('Run "node claude-sync-cli.js help" for usage information');
711
+ process.exit(1);
712
+ }
713
+ }
714
+
715
+ // 运行主函数
716
+ // Run main function
717
+ main();