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.
- package/LICENSE +7 -0
- package/README.md +306 -3
- package/dist/cli/commands/backup.d.ts +9 -0
- package/dist/cli/commands/backup.d.ts.map +1 -0
- package/dist/cli/commands/backup.js +168 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/export.d.ts.map +1 -1
- package/dist/cli/commands/export.js +38 -6
- package/dist/cli/commands/export.js.map +1 -1
- package/dist/cli/commands/list-backups.d.ts +9 -0
- package/dist/cli/commands/list-backups.d.ts.map +1 -0
- package/dist/cli/commands/list-backups.js +166 -0
- package/dist/cli/commands/list-backups.js.map +1 -0
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/commands/list.js +44 -9
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/migrate-session.d.ts +12 -0
- package/dist/cli/commands/migrate-session.d.ts.map +1 -0
- package/dist/cli/commands/migrate-session.js +125 -0
- package/dist/cli/commands/migrate-session.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts +13 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +122 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/restore.d.ts +9 -0
- package/dist/cli/commands/restore.d.ts.map +1 -0
- package/dist/cli/commands/restore.js +192 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/search.js +30 -2
- package/dist/cli/commands/search.js.map +1 -1
- package/dist/cli/commands/show.d.ts.map +1 -1
- package/dist/cli/commands/show.js +31 -3
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/backup.d.ts +89 -0
- package/dist/core/backup.d.ts.map +1 -0
- package/dist/core/backup.js +709 -0
- package/dist/core/backup.js.map +1 -0
- package/dist/core/migrate.d.ts +40 -0
- package/dist/core/migrate.d.ts.map +1 -0
- package/dist/core/migrate.js +586 -0
- package/dist/core/migrate.js.map +1 -0
- package/dist/core/storage.d.ts +78 -6
- package/dist/core/storage.d.ts.map +1 -1
- package/dist/core/storage.js +305 -24
- package/dist/core/storage.js.map +1 -1
- package/dist/core/types.d.ts +252 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/lib/backup.d.ts +98 -0
- package/dist/lib/backup.d.ts.map +1 -0
- package/dist/lib/backup.js +108 -0
- package/dist/lib/backup.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +1 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/errors.d.ts +229 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +361 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/index.d.ts +80 -3
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +132 -13
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/platform.d.ts +11 -0
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/platform.js +32 -0
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/types.d.ts +255 -0
- package/dist/lib/types.d.ts.map +1 -1
- 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
|
-
#
|
|
1
|
+
# Cursor History
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/logo.png" alt="cursor-history logo" width="200">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/cursor-history)
|
|
8
|
+
[](https://www.npmjs.com/package/cursor-history)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://nodejs.org/)
|
|
11
|
+
[](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;
|
|
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)
|