cursor-history 0.6.0 → 0.8.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 (73) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +306 -3
  3. package/dist/cli/commands/backup.d.ts +9 -0
  4. package/dist/cli/commands/backup.d.ts.map +1 -0
  5. package/dist/cli/commands/backup.js +168 -0
  6. package/dist/cli/commands/backup.js.map +1 -0
  7. package/dist/cli/commands/export.d.ts.map +1 -1
  8. package/dist/cli/commands/export.js +38 -6
  9. package/dist/cli/commands/export.js.map +1 -1
  10. package/dist/cli/commands/list-backups.d.ts +9 -0
  11. package/dist/cli/commands/list-backups.d.ts.map +1 -0
  12. package/dist/cli/commands/list-backups.js +166 -0
  13. package/dist/cli/commands/list-backups.js.map +1 -0
  14. package/dist/cli/commands/list.d.ts.map +1 -1
  15. package/dist/cli/commands/list.js +44 -9
  16. package/dist/cli/commands/list.js.map +1 -1
  17. package/dist/cli/commands/migrate-session.d.ts +12 -0
  18. package/dist/cli/commands/migrate-session.d.ts.map +1 -0
  19. package/dist/cli/commands/migrate-session.js +125 -0
  20. package/dist/cli/commands/migrate-session.js.map +1 -0
  21. package/dist/cli/commands/migrate.d.ts +13 -0
  22. package/dist/cli/commands/migrate.d.ts.map +1 -0
  23. package/dist/cli/commands/migrate.js +122 -0
  24. package/dist/cli/commands/migrate.js.map +1 -0
  25. package/dist/cli/commands/restore.d.ts +9 -0
  26. package/dist/cli/commands/restore.d.ts.map +1 -0
  27. package/dist/cli/commands/restore.js +192 -0
  28. package/dist/cli/commands/restore.js.map +1 -0
  29. package/dist/cli/commands/search.d.ts.map +1 -1
  30. package/dist/cli/commands/search.js +30 -2
  31. package/dist/cli/commands/search.js.map +1 -1
  32. package/dist/cli/commands/show.d.ts.map +1 -1
  33. package/dist/cli/commands/show.js +31 -3
  34. package/dist/cli/commands/show.js.map +1 -1
  35. package/dist/cli/index.js +10 -0
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/core/backup.d.ts +89 -0
  38. package/dist/core/backup.d.ts.map +1 -0
  39. package/dist/core/backup.js +709 -0
  40. package/dist/core/backup.js.map +1 -0
  41. package/dist/core/migrate.d.ts +40 -0
  42. package/dist/core/migrate.d.ts.map +1 -0
  43. package/dist/core/migrate.js +586 -0
  44. package/dist/core/migrate.js.map +1 -0
  45. package/dist/core/storage.d.ts +78 -6
  46. package/dist/core/storage.d.ts.map +1 -1
  47. package/dist/core/storage.js +305 -24
  48. package/dist/core/storage.js.map +1 -1
  49. package/dist/core/types.d.ts +252 -0
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/lib/backup.d.ts +98 -0
  52. package/dist/lib/backup.d.ts.map +1 -0
  53. package/dist/lib/backup.js +108 -0
  54. package/dist/lib/backup.js.map +1 -0
  55. package/dist/lib/config.d.ts +1 -0
  56. package/dist/lib/config.d.ts.map +1 -1
  57. package/dist/lib/config.js +1 -0
  58. package/dist/lib/config.js.map +1 -1
  59. package/dist/lib/errors.d.ts +229 -0
  60. package/dist/lib/errors.d.ts.map +1 -1
  61. package/dist/lib/errors.js +361 -0
  62. package/dist/lib/errors.js.map +1 -1
  63. package/dist/lib/index.d.ts +80 -3
  64. package/dist/lib/index.d.ts.map +1 -1
  65. package/dist/lib/index.js +132 -13
  66. package/dist/lib/index.js.map +1 -1
  67. package/dist/lib/platform.d.ts +11 -0
  68. package/dist/lib/platform.d.ts.map +1 -1
  69. package/dist/lib/platform.js +32 -0
  70. package/dist/lib/platform.js.map +1 -1
  71. package/dist/lib/types.d.ts +255 -0
  72. package/dist/lib/types.d.ts.map +1 -1
  73. package/package.json +23 -3
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2025 Borui
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,115 @@
1
- # cursor-history
1
+ # Cursor History
2
2
 
3
- CLI tool and library to browse, search, and export your Cursor AI chat history.
3
+ <p align="center">
4
+ <img src="docs/logo.png" alt="cursor-history logo" width="200">
5
+ </p>
6
+
7
+ [![npm version](https://img.shields.io/npm/v/cursor-history.svg)](https://www.npmjs.com/package/cursor-history)
8
+ [![npm downloads](https://img.shields.io/npm/dm/cursor-history.svg)](https://www.npmjs.com/package/cursor-history)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![Node.js](https://img.shields.io/badge/Node.js-20%2B-green.svg)](https://nodejs.org/)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0%2B-blue.svg)](https://www.typescriptlang.org/)
12
+
13
+ **The ultimate open-source tool for browsing, searching, exporting, and backing up your Cursor AI chat history.**
14
+
15
+ A POSIX-style CLI tool that does one thing well: access your Cursor AI chat history. Built on Unix philosophy—simple, composable, and focused.
16
+
17
+ ```bash
18
+ # Pipe-friendly: combine with other tools
19
+ cursor-history list --json | jq '.[] | select(.messageCount > 10)'
20
+ cursor-history export 1 | grep -i "api" | head -20
21
+ cursor-history search "bug" --json | jq -r '.[].sessionId' | xargs -I {} cursor-history export {}
22
+ ```
23
+
24
+ Never lose a conversation again. Whether you need to find that perfect code snippet from last week, migrate your history to a new machine, or create reliable backups of all your AI-assisted development sessions—cursor-history has you covered. Free, open-source, and built by the community for the community.
25
+
26
+ ## Example Output
27
+
28
+ ### List Sessions
29
+
30
+ <pre>
31
+ <span style="color: #888">cursor-history list</span>
32
+
33
+ <span style="color: #5fd7ff">cursor-history</span> - Chat History Browser
34
+
35
+ <span style="color: #5fd7ff">Sessions (showing 3 of 42):</span>
36
+
37
+ <span style="color: #af87ff">#1</span> <span style="color: #87d787">12/26 09:15 AM</span> <span style="color: #d7d787">cursor_chat_history</span>
38
+ <span style="color: #888">15 messages · Updated 2 min ago</span>
39
+ <span style="color: #fff">"Help me fix the migration path issue..."</span>
40
+
41
+ <span style="color: #af87ff">#2</span> <span style="color: #87d787">12/25 03:22 PM</span> <span style="color: #d7d787">my-react-app</span>
42
+ <span style="color: #888">8 messages · Updated 18 hours ago</span>
43
+ <span style="color: #fff">"Add authentication to the app..."</span>
44
+
45
+ <span style="color: #af87ff">#3</span> <span style="color: #87d787">12/24 11:30 AM</span> <span style="color: #d7d787">api-server</span>
46
+ <span style="color: #888">23 messages · Updated 2 days ago</span>
47
+ <span style="color: #fff">"Create REST endpoints for users..."</span>
48
+ </pre>
49
+
50
+ ### Show Session Details
51
+
52
+ <pre>
53
+ <span style="color: #888">cursor-history show 1</span>
54
+
55
+ <span style="color: #5fd7ff">Session #1</span> · <span style="color: #d7d787">cursor_chat_history</span>
56
+ <span style="color: #888">15 messages · Created 12/26 09:15 AM</span>
57
+
58
+ ────────────────────────────────────────
59
+
60
+ <span style="color: #87d787">You:</span> <span style="color: #888">09:15:23 AM</span>
61
+
62
+ Help me fix the migration path issue in the codebase
63
+
64
+ ────────────────────────────────────────
65
+
66
+ <span style="color: #af87ff">Assistant:</span> <span style="color: #888">09:15:45 AM</span>
67
+
68
+ I'll help you fix the migration path issue. Let me first examine
69
+ the relevant files.
70
+
71
+ ────────────────────────────────────────
72
+
73
+ <span style="color: #d7af5f">Tool:</span> <span style="color: #888">09:15:46 AM</span>
74
+ <span style="color: #d7af5f">🔧 Read File</span>
75
+ <span style="color: #888">File:</span> <span style="color: #5fd7ff">src/core/migrate.ts</span>
76
+ <span style="color: #888">Content:</span> <span style="color: #fff">export function migrateSession(sessionId: string...</span>
77
+ <span style="color: #87d787">Status: ✓ completed</span>
78
+
79
+ ────────────────────────────────────────
80
+
81
+ <span style="color: #d7af5f">Tool:</span> <span style="color: #888">09:16:02 AM</span>
82
+ <span style="color: #d7af5f">🔧 Edit File</span>
83
+ <span style="color: #888">File:</span> <span style="color: #5fd7ff">src/core/migrate.ts</span>
84
+
85
+ <span style="color: #87d787">```diff</span>
86
+ <span style="color: #87d787"> + function transformPath(path: string): string {</span>
87
+ <span style="color: #87d787"> + return path.replace(sourcePrefix, destPrefix);</span>
88
+ <span style="color: #87d787"> + }</span>
89
+ <span style="color: #87d787">```</span>
90
+
91
+ <span style="color: #87d787">Status: ✓ completed</span>
92
+
93
+ ────────────────────────────────────────
94
+
95
+ <span style="color: #5f87d7">Thinking:</span> <span style="color: #888">09:16:02 AM</span>
96
+ <span style="color: #5f87d7">💭</span> <span style="color: #888">Now I need to update the function to call transformPath
97
+ for each file reference in the bubble data...</span>
98
+
99
+ ────────────────────────────────────────
100
+
101
+ <span style="color: #af87ff">Assistant:</span> <span style="color: #888">09:16:30 AM</span>
102
+
103
+ I've added the path transformation logic. The migration will now
104
+ update all file paths when moving sessions between workspaces.
105
+
106
+ ────────────────────────────────────────
107
+
108
+ <span style="color: #ff5f5f">Error:</span> <span style="color: #888">09:17:01 AM</span>
109
+ <span style="color: #ff5f5f">❌</span> <span style="color: #ff5f5f">Build failed: Cannot find module './utils'</span>
110
+
111
+ ────────────────────────────────────────
112
+ </pre>
4
113
 
5
114
  ## Features
6
115
 
@@ -14,6 +123,8 @@ CLI tool and library to browse, search, and export your Cursor AI chat history.
14
123
  - Message timestamps
15
124
  - **Search** - Find conversations by keyword with highlighted matches
16
125
  - **Export** - Save sessions as Markdown or JSON files
126
+ - **Migrate** - Move or copy sessions between workspaces (e.g., when renaming projects)
127
+ - **Backup & Restore** - Create full backups of all chat history and restore when needed
17
128
  - **Cross-platform** - Works on macOS, Windows, and Linux
18
129
 
19
130
  ## Installation
@@ -128,6 +239,65 @@ cursor-history export --all -o ./exports/
128
239
  cursor-history export 1 --force
129
240
  ```
130
241
 
242
+ ### Migrate Sessions
243
+
244
+ ```bash
245
+ # Move a single session to another workspace
246
+ cursor-history migrate-session 1 /path/to/new/project
247
+
248
+ # Move multiple sessions (comma-separated indices or IDs)
249
+ cursor-history migrate-session 1,3,5 /path/to/project
250
+
251
+ # Copy instead of move (keeps original)
252
+ cursor-history migrate-session --copy 1 /path/to/project
253
+
254
+ # Preview what would happen without making changes
255
+ cursor-history migrate-session --dry-run 1 /path/to/project
256
+
257
+ # Move all sessions from one workspace to another
258
+ cursor-history migrate /old/project /new/project
259
+
260
+ # Copy all sessions (backup)
261
+ cursor-history migrate --copy /project /backup/project
262
+
263
+ # Force merge with existing sessions at destination
264
+ cursor-history migrate --force /old/project /existing/project
265
+ ```
266
+
267
+ ### Backup & Restore
268
+
269
+ ```bash
270
+ # Create a backup of all chat history
271
+ cursor-history backup
272
+
273
+ # Create backup to specific file
274
+ cursor-history backup -o ~/my-backup.zip
275
+
276
+ # Overwrite existing backup
277
+ cursor-history backup --force
278
+
279
+ # List available backups
280
+ cursor-history list-backups
281
+
282
+ # List backups in a specific directory
283
+ cursor-history list-backups -d /path/to/backups
284
+
285
+ # Restore from a backup
286
+ cursor-history restore ~/cursor-history-backups/backup.zip
287
+
288
+ # Restore to a custom location
289
+ cursor-history restore backup.zip --target /custom/cursor/data
290
+
291
+ # Force overwrite existing data
292
+ cursor-history restore backup.zip --force
293
+
294
+ # View sessions from a backup without restoring
295
+ cursor-history list --backup ~/backup.zip
296
+ cursor-history show 1 --backup ~/backup.zip
297
+ cursor-history search "query" --backup ~/backup.zip
298
+ cursor-history export 1 --backup ~/backup.zip
299
+ ```
300
+
131
301
  ### Global Options
132
302
 
133
303
  ```bash
@@ -213,6 +383,79 @@ for (const match of results) {
213
383
  const markdown = exportSessionToMarkdown(0);
214
384
  ```
215
385
 
386
+ ### Migration API
387
+
388
+ ```typescript
389
+ import { migrateSession, migrateWorkspace } from 'cursor-history';
390
+
391
+ // Move a session to another workspace
392
+ const results = migrateSession({
393
+ sessions: 3, // index or ID
394
+ destination: '/path/to/new/project'
395
+ });
396
+
397
+ // Copy multiple sessions (keeps originals)
398
+ const results = migrateSession({
399
+ sessions: [1, 3, 5],
400
+ destination: '/path/to/project',
401
+ mode: 'copy'
402
+ });
403
+
404
+ // Migrate all sessions between workspaces
405
+ const result = migrateWorkspace({
406
+ source: '/old/project',
407
+ destination: '/new/project'
408
+ });
409
+ console.log(`Migrated ${result.successCount} sessions`);
410
+ ```
411
+
412
+ ### Backup API
413
+
414
+ ```typescript
415
+ import {
416
+ createBackup,
417
+ restoreBackup,
418
+ validateBackup,
419
+ listBackups,
420
+ getDefaultBackupDir
421
+ } from 'cursor-history';
422
+
423
+ // Create a backup
424
+ const result = await createBackup({
425
+ outputPath: '~/my-backup.zip',
426
+ force: true,
427
+ onProgress: (progress) => {
428
+ console.log(`${progress.phase}: ${progress.filesCompleted}/${progress.totalFiles}`);
429
+ }
430
+ });
431
+ console.log(`Backup created: ${result.backupPath}`);
432
+ console.log(`Sessions: ${result.manifest.stats.sessionCount}`);
433
+
434
+ // Validate a backup
435
+ const validation = validateBackup('~/backup.zip');
436
+ if (validation.status === 'valid') {
437
+ console.log('Backup is valid');
438
+ } else if (validation.status === 'warnings') {
439
+ console.log('Backup has warnings:', validation.corruptedFiles);
440
+ }
441
+
442
+ // Restore from backup
443
+ const restoreResult = restoreBackup({
444
+ backupPath: '~/backup.zip',
445
+ force: true
446
+ });
447
+ console.log(`Restored ${restoreResult.filesRestored} files`);
448
+
449
+ // List available backups
450
+ const backups = listBackups(); // Scans ~/cursor-history-backups/
451
+ for (const backup of backups) {
452
+ console.log(`${backup.filename}: ${backup.manifest?.stats.sessionCount} sessions`);
453
+ }
454
+
455
+ // Read sessions from backup without restoring
456
+ const sessions = listSessions({ backupPath: '~/backup.zip' });
457
+ ```
458
+
216
459
  ### Available Functions
217
460
 
218
461
  | Function | Description |
@@ -224,6 +467,13 @@ const markdown = exportSessionToMarkdown(0);
224
467
  | `exportSessionToMarkdown(index, config?)` | Export session to Markdown |
225
468
  | `exportAllSessionsToJson(config?)` | Export all sessions to JSON |
226
469
  | `exportAllSessionsToMarkdown(config?)` | Export all sessions to Markdown |
470
+ | `migrateSession(config)` | Move/copy sessions to another workspace |
471
+ | `migrateWorkspace(config)` | Move/copy all sessions between workspaces |
472
+ | `createBackup(config?)` | Create full backup of all chat history |
473
+ | `restoreBackup(config)` | Restore chat history from backup |
474
+ | `validateBackup(path)` | Validate backup integrity |
475
+ | `listBackups(directory?)` | List available backup files |
476
+ | `getDefaultBackupDir()` | Get default backup directory path |
227
477
  | `getDefaultDataPath()` | Get platform-specific Cursor data path |
228
478
 
229
479
  ### Configuration Options
@@ -235,6 +485,7 @@ interface LibraryConfig {
235
485
  limit?: number; // Pagination limit
236
486
  offset?: number; // Pagination offset
237
487
  context?: number; // Search context lines
488
+ backupPath?: string; // Read from backup file instead of live data
238
489
  }
239
490
  ```
240
491
 
@@ -243,8 +494,14 @@ interface LibraryConfig {
243
494
  ```typescript
244
495
  import {
245
496
  listSessions,
497
+ createBackup,
246
498
  isDatabaseLockedError,
247
- isDatabaseNotFoundError
499
+ isDatabaseNotFoundError,
500
+ isSessionNotFoundError,
501
+ isWorkspaceNotFoundError,
502
+ isBackupError,
503
+ isRestoreError,
504
+ isInvalidBackupError
248
505
  } from 'cursor-history';
249
506
 
250
507
  try {
@@ -254,6 +511,23 @@ try {
254
511
  console.error('Database locked - close Cursor and retry');
255
512
  } else if (isDatabaseNotFoundError(err)) {
256
513
  console.error('Cursor data not found');
514
+ } else if (isSessionNotFoundError(err)) {
515
+ console.error('Session not found');
516
+ } else if (isWorkspaceNotFoundError(err)) {
517
+ console.error('Workspace not found - open project in Cursor first');
518
+ }
519
+ }
520
+
521
+ // Backup-specific errors
522
+ try {
523
+ const result = await createBackup();
524
+ } catch (err) {
525
+ if (isBackupError(err)) {
526
+ console.error('Backup failed:', err.message);
527
+ } else if (isInvalidBackupError(err)) {
528
+ console.error('Invalid backup file');
529
+ } else if (isRestoreError(err)) {
530
+ console.error('Restore failed:', err.message);
257
531
  }
258
532
  }
259
533
  ```
@@ -300,6 +574,35 @@ This project uses GitHub Actions for automatic NPM publishing. To release a new
300
574
  2. Go to your GitHub repository settings → Secrets and variables → Actions
301
575
  3. Add a new repository secret named `NPM_TOKEN` with your NPM token
302
576
 
577
+ ## Contributing
578
+
579
+ We welcome contributions from the community! Here's how you can help:
580
+
581
+ ### Reporting Issues
582
+
583
+ - **Bug reports**: [Open an issue](https://github.com/S2thend/cursor_chat_history/issues/new) with steps to reproduce, expected vs actual behavior, and your environment (OS, Node.js version)
584
+ - **Feature requests**: [Open an issue](https://github.com/S2thend/cursor_chat_history/issues/new) describing the feature and its use case
585
+
586
+ ### Submitting Pull Requests
587
+
588
+ 1. Fork the repository
589
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
590
+ 3. Make your changes
591
+ 4. Run tests and linting (`npm test && npm run lint`)
592
+ 5. Commit your changes (`git commit -m 'Add my feature'`)
593
+ 6. Push to your fork (`git push origin feature/my-feature`)
594
+ 7. [Open a Pull Request](https://github.com/S2thend/cursor_chat_history/pulls)
595
+
596
+ ### Development Setup
597
+
598
+ ```bash
599
+ git clone https://github.com/S2thend/cursor_chat_history.git
600
+ cd cursor_chat_history
601
+ npm install
602
+ npm run build
603
+ npm test
604
+ ```
605
+
303
606
  ## License
304
607
 
305
608
  MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Backup command - create full backup of all chat history
3
+ */
4
+ import type { Command } from 'commander';
5
+ /**
6
+ * Register the backup command
7
+ */
8
+ export declare function registerBackupCommand(program: Command): void;
9
+ //# sourceMappingURL=backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/backup.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsGzC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoF5D"}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Backup command - create full backup of all chat history
3
+ */
4
+ import pc from 'picocolors';
5
+ import { createBackup } from '../../core/backup.js';
6
+ import { handleError, ExitCode } from '../errors.js';
7
+ import { expandPath, contractPath } from '../../lib/platform.js';
8
+ /**
9
+ * Format file size for display
10
+ */
11
+ function formatSize(bytes) {
12
+ if (bytes < 1024) {
13
+ return `${bytes} B`;
14
+ }
15
+ if (bytes < 1024 * 1024) {
16
+ return `${(bytes / 1024).toFixed(1)} KB`;
17
+ }
18
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
19
+ }
20
+ /**
21
+ * Format duration for display
22
+ */
23
+ function formatDuration(ms) {
24
+ if (ms < 1000) {
25
+ return `${ms}ms`;
26
+ }
27
+ return `${(ms / 1000).toFixed(1)}s`;
28
+ }
29
+ /**
30
+ * T021: Progress display for backup command
31
+ */
32
+ function displayProgress(progress) {
33
+ const phases = {
34
+ scanning: '🔍 Scanning for database files...',
35
+ 'backing-up': '📦 Backing up databases...',
36
+ compressing: '🗜️ Compressing into zip...',
37
+ finalizing: '✨ Finalizing backup...',
38
+ };
39
+ const phaseText = phases[progress.phase];
40
+ const fileProgress = progress.totalFiles > 0
41
+ ? ` [${progress.filesCompleted}/${progress.totalFiles}]`
42
+ : '';
43
+ const currentFile = progress.currentFile ? ` ${pc.dim(progress.currentFile)}` : '';
44
+ // Clear line and print progress
45
+ process.stdout.write(`\r${phaseText}${fileProgress}${currentFile}`.padEnd(80));
46
+ }
47
+ /**
48
+ * Format backup result for JSON output
49
+ */
50
+ function formatBackupResultJson(result) {
51
+ return JSON.stringify({
52
+ success: result.success,
53
+ backupPath: result.backupPath,
54
+ durationMs: result.durationMs,
55
+ ...(result.error && { error: result.error }),
56
+ manifest: result.manifest,
57
+ }, null, 2);
58
+ }
59
+ /**
60
+ * Format backup result for human-readable output
61
+ */
62
+ function formatBackupResult(result) {
63
+ const lines = [];
64
+ if (result.success) {
65
+ lines.push(pc.green('✓ Backup created successfully!'));
66
+ lines.push('');
67
+ lines.push(` ${pc.bold('Location:')} ${contractPath(result.backupPath)}`);
68
+ lines.push(` ${pc.bold('Size:')} ${formatSize(result.manifest.stats.totalSize)}`);
69
+ lines.push(` ${pc.bold('Sessions:')} ${result.manifest.stats.sessionCount}`);
70
+ lines.push(` ${pc.bold('Workspaces:')} ${result.manifest.stats.workspaceCount}`);
71
+ lines.push(` ${pc.bold('Files:')} ${result.manifest.files.length} database files`);
72
+ lines.push(` ${pc.bold('Duration:')} ${formatDuration(result.durationMs)}`);
73
+ }
74
+ else {
75
+ lines.push(pc.red('✗ Backup failed'));
76
+ lines.push('');
77
+ if (result.error) {
78
+ lines.push(` ${pc.bold('Error:')} ${result.error}`);
79
+ }
80
+ }
81
+ return lines.join('\n');
82
+ }
83
+ /**
84
+ * Register the backup command
85
+ */
86
+ export function registerBackupCommand(program) {
87
+ program
88
+ .command('backup')
89
+ .description('Create a full backup of all Cursor chat history')
90
+ .option('-o, --output <path>', 'Output file path (default: ~/cursor-history-backups/<timestamp>.zip)')
91
+ .option('-f, --force', 'Overwrite existing backup file')
92
+ .action(async (options, command) => {
93
+ const globalOptions = command.parent?.opts();
94
+ const useJson = options.json ?? globalOptions?.json ?? false;
95
+ const customPath = options.dataPath ?? globalOptions?.dataPath;
96
+ try {
97
+ // Resolve output path if provided
98
+ const outputPath = options.output ? expandPath(options.output) : undefined;
99
+ // Show progress if not JSON mode
100
+ const onProgress = useJson ? undefined : displayProgress;
101
+ // Create backup
102
+ const result = await createBackup({
103
+ sourcePath: customPath ? expandPath(customPath) : undefined,
104
+ outputPath,
105
+ force: options.force ?? false,
106
+ onProgress,
107
+ });
108
+ // Clear progress line
109
+ if (!useJson) {
110
+ process.stdout.write('\r'.padEnd(80) + '\r');
111
+ }
112
+ // Handle different error cases with appropriate exit codes
113
+ if (!result.success) {
114
+ // T022: No data to backup
115
+ if (result.error?.includes('No Cursor data found')) {
116
+ if (useJson) {
117
+ console.log(formatBackupResultJson(result));
118
+ }
119
+ else {
120
+ console.error(pc.yellow('No Cursor data found to backup.'));
121
+ console.error(pc.dim('Make sure Cursor has been used and has chat history.'));
122
+ }
123
+ process.exit(ExitCode.USAGE_ERROR);
124
+ }
125
+ // T023: File exists without --force
126
+ if (result.error?.includes('already exists')) {
127
+ if (useJson) {
128
+ console.log(formatBackupResultJson(result));
129
+ }
130
+ else {
131
+ console.error(pc.red('Backup file already exists.'));
132
+ console.error(pc.dim('Use --force to overwrite.'));
133
+ }
134
+ process.exit(ExitCode.NOT_FOUND);
135
+ }
136
+ // T024: Insufficient disk space
137
+ if (result.error?.includes('Insufficient disk space')) {
138
+ if (useJson) {
139
+ console.log(formatBackupResultJson(result));
140
+ }
141
+ else {
142
+ console.error(pc.red('Insufficient disk space for backup.'));
143
+ }
144
+ process.exit(ExitCode.IO_ERROR);
145
+ }
146
+ // Generic error
147
+ if (useJson) {
148
+ console.log(formatBackupResultJson(result));
149
+ }
150
+ else {
151
+ console.error(formatBackupResult(result));
152
+ }
153
+ process.exit(ExitCode.GENERAL_ERROR);
154
+ }
155
+ // Success
156
+ if (useJson) {
157
+ console.log(formatBackupResultJson(result));
158
+ }
159
+ else {
160
+ console.log(formatBackupResult(result));
161
+ }
162
+ }
163
+ catch (error) {
164
+ handleError(error);
165
+ }
166
+ });
167
+ }
168
+ //# sourceMappingURL=backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../../src/cli/commands/backup.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/export.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBzC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoJ5D"}
1
+ {"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/export.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BzC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiM5D"}
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Export command - export chat sessions to files
3
3
  */
4
+ import pc from 'picocolors';
4
5
  import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
5
6
  import { dirname, join } from 'node:path';
6
7
  import { getSession, listSessions, findWorkspaces } from '../../core/storage.js';
8
+ import { validateBackup } from '../../core/backup.js';
7
9
  import { exportToMarkdown, exportToJson } from '../../core/parser.js';
8
10
  import { formatExportSuccess, formatExportResultJson } from '../formatters/index.js';
9
11
  import { SessionNotFoundError, FileExistsError, handleError, CliError, ExitCode, } from '../errors.js';
@@ -19,11 +21,33 @@ export function registerExportCommand(program) {
19
21
  .option('-f, --format <format>', 'Output format: md or json', 'md')
20
22
  .option('--force', 'Overwrite existing files')
21
23
  .option('-a, --all', 'Export all sessions')
24
+ .option('-b, --backup <path>', 'Export from backup file instead of live data')
22
25
  .action(async (indexArg, options, command) => {
23
26
  const globalOptions = command.parent?.opts();
24
27
  const useJson = options.json ?? globalOptions?.json ?? false;
25
28
  const customPath = options.dataPath ?? globalOptions?.dataPath;
26
29
  const format = options.format === 'json' ? 'json' : 'md';
30
+ const backupPath = options.backup ? expandPath(options.backup) : undefined;
31
+ // T037: Validate backup if exporting from backup
32
+ if (backupPath) {
33
+ const validation = validateBackup(backupPath);
34
+ if (validation.status === 'invalid') {
35
+ if (useJson) {
36
+ console.log(JSON.stringify({ error: 'Invalid backup', errors: validation.errors }));
37
+ }
38
+ else {
39
+ console.error(pc.red('Invalid backup file:'));
40
+ for (const err of validation.errors) {
41
+ console.error(pc.dim(` ${err}`));
42
+ }
43
+ }
44
+ process.exit(3);
45
+ }
46
+ if (validation.status === 'warnings' && !useJson) {
47
+ console.error(pc.yellow(`Warning: Backup has integrity issues (${validation.corruptedFiles.length} corrupted files)`));
48
+ console.error(pc.dim('Continuing with intact files...\n'));
49
+ }
50
+ }
27
51
  try {
28
52
  // Validate arguments
29
53
  if (!options.all && !indexArg) {
@@ -32,7 +56,7 @@ export function registerExportCommand(program) {
32
56
  const exported = [];
33
57
  if (options.all) {
34
58
  // Export all sessions
35
- const sessions = listSessions({ limit: 0, all: true }, customPath ? expandPath(customPath) : undefined);
59
+ const sessions = listSessions({ limit: 0, all: true }, customPath ? expandPath(customPath) : undefined, backupPath);
36
60
  if (sessions.length === 0) {
37
61
  throw new CliError('No sessions to export.', ExitCode.NOT_FOUND);
38
62
  }
@@ -42,9 +66,13 @@ export function registerExportCommand(program) {
42
66
  if (!existsSync(outputDir)) {
43
67
  mkdirSync(outputDir, { recursive: true });
44
68
  }
45
- const workspaces = findWorkspaces(customPath ? expandPath(customPath) : undefined);
69
+ const workspaces = findWorkspaces(customPath ? expandPath(customPath) : undefined, backupPath);
70
+ // Show backup source indicator if exporting from backup
71
+ if (backupPath && !useJson) {
72
+ console.log(pc.dim(`Exporting from backup: ${contractPath(backupPath)}\n`));
73
+ }
46
74
  for (const summary of sessions) {
47
- const session = getSession(summary.index, customPath ? expandPath(customPath) : undefined);
75
+ const session = getSession(summary.index, customPath ? expandPath(customPath) : undefined, backupPath);
48
76
  if (!session)
49
77
  continue;
50
78
  const workspace = workspaces.find((w) => w.id === session.workspaceId);
@@ -74,12 +102,12 @@ export function registerExportCommand(program) {
74
102
  if (isNaN(index) || index < 1) {
75
103
  throw new CliError(`Invalid index: ${indexArg}. Must be a positive number.`, ExitCode.USAGE_ERROR);
76
104
  }
77
- const session = getSession(index, customPath ? expandPath(customPath) : undefined);
105
+ const session = getSession(index, customPath ? expandPath(customPath) : undefined, backupPath);
78
106
  if (!session) {
79
- const sessions = listSessions({ limit: 0, all: true }, customPath ? expandPath(customPath) : undefined);
107
+ const sessions = listSessions({ limit: 0, all: true }, customPath ? expandPath(customPath) : undefined, backupPath);
80
108
  throw new SessionNotFoundError(index, sessions.length);
81
109
  }
82
- const workspaces = findWorkspaces(customPath ? expandPath(customPath) : undefined);
110
+ const workspaces = findWorkspaces(customPath ? expandPath(customPath) : undefined, backupPath);
83
111
  const workspace = workspaces.find((w) => w.id === session.workspaceId);
84
112
  const workspacePath = workspace?.path;
85
113
  // Determine output path
@@ -103,6 +131,10 @@ export function registerExportCommand(program) {
103
131
  if (dir !== '.' && !existsSync(dir)) {
104
132
  mkdirSync(dir, { recursive: true });
105
133
  }
134
+ // Show backup source indicator if exporting from backup
135
+ if (backupPath && !useJson) {
136
+ console.log(pc.dim(`Exporting from backup: ${contractPath(backupPath)}\n`));
137
+ }
106
138
  // Export
107
139
  const content = format === 'json'
108
140
  ? exportToJson(session, workspacePath)