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.
- package/bin/claude-chats-sync.js +717 -620
- package/package.json +57 -57
package/bin/claude-chats-sync.js
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
.replace(
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Windows:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Unix:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
Claude Code
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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();
|