🇺🇸 English | 🇨🇳 中文 | 🇫🇷 Français | 🇪🇸 Español
The ultimate open-source tool for browsing, searching, exporting, and backing up your Cursor AI chat history.
A POSIX-style CLI tool that does one thing well: access your Cursor AI chat history. Built on Unix philosophy—simple, composable, and focused.
# Pipe-friendly: combine with other tools
cursor-history list --json | jq '.sessions[] | select(.messageCount > 10)'
cursor-history export 1 | grep -i "api" | head -20
cursor-history search "bug" --json | jq -r '.results[].sessionId' | xargs -I {} cursor-history export {}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.
cursor-history list cursor-history - Chat History Browser Sessions (showing 3 of 42): #1 12/26 09:15 AM cursor_chat_history 15 messages · Updated 2 min ago "Help me fix the migration path issue..." #2 12/25 03:22 PM my-react-app 8 messages · Updated 18 hours ago "Add authentication to the app..." #3 12/24 11:30 AM api-server 23 messages · Updated 2 days ago "Create REST endpoints for users..."
cursor-history show 1 Session #1 · cursor_chat_history 15 messages · Created 12/26 09:15 AM ──────────────────────────────────────── You: 09:15:23 AM Help me fix the migration path issue in the codebase ──────────────────────────────────────── Assistant: 09:15:45 AM I'll help you fix the migration path issue. Let me first examine the relevant files. ──────────────────────────────────────── Tool: 09:15:46 AM 🔧 Read File File: src/core/migrate.ts Content: export function migrateSession(sessionId: string... Status: ✓ completed ──────────────────────────────────────── Tool: 09:16:02 AM 🔧 Edit File File: src/core/migrate.ts ```diff + function transformPath(path: string): string { + return path.replace(sourcePrefix, destPrefix); + } ``` Status: ✓ completed ──────────────────────────────────────── Thinking: 09:16:02 AM 💭 Now I need to update the function to call transformPath for each file reference in the bubble data... ──────────────────────────────────────── Assistant: 09:16:30 AM I've added the path transformation logic. The migration will now update all file paths when moving sessions between workspaces. ──────────────────────────────────────── Error: 09:17:01 AM ❌ Build failed: Cannot find module './utils' ────────────────────────────────────────
- Dual interface - Use as CLI tool or import as a library in your Node.js projects
- List sessions - View all chat sessions across workspaces
- View full conversations - See complete chat history with:
- AI responses with natural language explanations
- Full diff display for file edits and writes with syntax highlighting
- Detailed tool calls showing all parameters (file paths, search patterns, commands, etc.)
- AI reasoning and thinking blocks
- Message timestamps with explicit stored/inferred provenance
- Search - Find conversations by keyword with highlighted matches
- Export - Save sessions as Markdown or JSON files
- Migrate - Move or copy sessions between workspaces (e.g., when renaming projects)
- Backup & Restore - Create full backups of all chat history and restore when needed
- Cross-platform - Works on macOS, Windows, and Linux
The authoritative identity, scoped-index, workspace-I/O, source-fidelity, timestamp, input-limit, backup-permission, and upgrade rules are in the shipped Compatibility and Data-Integrity Contract. Library consumers that persist cursor-history output should read that contract before changing versions.
v0.17 introduced transitional Store/merged behavior that can change positional message keys, replacement signals, and timestamp-watermark assumptions. If your application incrementally stores library output—such as a vibe-history archive—keep cursor-history v0.16 pinned until you can validate the 0.18.0 corrective path. Back up the downstream archive before upgrading.
The confirmed no-consumer-change upgrade path is deliberately narrower: an archive populated from
v0.16 Composer-only data can become a complete Composer-backed merged view while retaining every
old session, Composer-message, and existing ordinal-derived tool key byte-for-byte. A changed
complete view still reports source: "global", so the unchanged consumer performs its existing
whole-session atomic replacement; a second identical sync performs no session/content mutations.
The unchanged consumer still executes one existing sync_metadata schema-version upsert statement
per synchronization; a fresh target may initialize that metadata row, while later same-version
upserts are value-preserving bookkeeping outside the session/content mutation count. Store-only turns
may be interleaved without renumbering old Composer identities. Do not use a maximum timestamp as the
incremental boundary, and never replace complete archived data with
source: "workspace-fallback".
Complete affected v0.17 Store/merged data instead has a documented one-time whole-session replacement path. Unstable v0.17 Store positional/cross-format synthetic IDs are not preserved. A degraded v0.17 result must be pinned, retried from complete sources, or migrated manually.
Session.idremains the native Cursor UUID. Physical source instances and locators are separate and are never encoded into the public ID.- CLI/core indices are one-based, public-library read indices are zero-based, and public-library migration selectors are one-based. All are ephemeral within the exact data source, workspace, catalog snapshot, and invocation that produced them; persist the native UUID instead.
- Migration resolves both numbers and UUIDs through the complete scoped logical catalog. Ambiguous rows retain their displayed positions and return the same typed ambiguity by either selector; they are never skipped, shifted, treated as not found, or mutated.
- For unchanged Composer input, sessions tied on
createdAtretain v0.16's stable discovery order. Composer-backed merged or ambiguous rows keep that tie position; new-only rows follow the legacy tie group in deterministic UUID order. - Structured numeric output declares
indexScope: "global" | "workspace"; workspace rows also carry the fullindexWorkspacePath. - Workspace matching uses normalized exact matching first, then one unambiguous complete-component suffix. Ambiguity fails before conversation payload is read.
- A workspace is a payload-I/O boundary by default.
--include-cross-workspace-sourcesorincludeCrossWorkspaceSources: truecan load complementary sources only for UUIDs already selected in scope; omitted contributors make the default view explicitly partial. - Legacy
sourcereports fidelity:globalis complete/replacement-safe andworkspace-fallbackis partial/unsafe to overwrite complete data.resolvedSource,sources, andresolutionreport actual Composer/Store provenance additively. - Every resolved message includes deterministic timestamp provenance. Human output marks inferred
times as approximate; JSON/library consumers receive
timestampSource. A legacy timestamp of unprovable origin is retained asunknown, not presented as directly stored. - When a usable Store database and transcript coexist inside the permitted scope, this is a supported normal case: the database is the sole Store conversation backbone and the transcript is retained as superseded provenance rather than merged heuristically. A known representation outside the workspace I/O boundary is not opened and makes the scoped view explicitly partial.
Round-trip a CLI index only inside the same workspace scope:
cursor-history --json --workspace /work/a list --all
cursor-history --json --workspace /work/a show 1
cursor-history --json --workspace /work/a search needle-a
cursor-history --workspace /work/a migrate-session 1 /work/destination --dry-runUse a stable UUID for reusable library addressing (read indices are zero-based):
import { getSession, listSessions } from 'cursor-history';
const workspace = '/work/a';
const page = await listSessions({ workspace, limit: 20 });
const first = page.data[0];
if (first) {
const session = await getSession(first.id, { workspace });
console.log(session.id, session.source, session.resolvedSource);
}Fatal JSON migration note: some v0.17 command-owned failures wrote JSON to stdout. The corrective release writes every fatal JSON object to stderr and leaves stdout empty; successful output remains on stdout. Existing error fields/types/values and exit-category meanings are preserved for the same fixture, with only documented safe additive fields allowed. Scripts that parsed fatal JSON from stdout must read stderr after a nonzero exit.
Public-library search correction in 0.18.0: existing messageIndex now identifies the matched
message in the complete returned session.messages array, offset is a zero-based UTF-16
code-unit position in that message's complete original content, and match/context values are
complete original source lines. v0.16/v0.17 returned placeholder or snippet-relative values;
consumers that persisted those coordinates must recompute them after upgrade. Session, message, and
tool identities do not change under this correction. Public-library JSON exports also gain an
additive zero-based index; v0.16/v0.17 exports omitted that property.
Temporary plaintext snapshot workspaces are owner-only (0700 directories and 0600 files on
POSIX) and cleaned on success and failure. New final archives default to 0600; force-overwrite
preserves an existing mode. backup --shared explicitly requests 0666 & ~currentUmask without
broadening temporary files, changing the process umask, or modifying parent permissions. Windows
uses its system per-user temporary directory, inherited ACLs, exclusive paths, and the same cleanup
contract; this release does not claim independently verified cross-user ACL isolation on Windows.
New manifests record the actual running package version as diagnostic producer metadata; it never
changes session/message identity, replica equivalence, deduplication, or incremental sync.
New backups keep the enclosing manifest.version at 1.0.0 and add an optional canonical
metadata-only Composer workspace/UUID inventory with its own independently validated
schemaVersion: 1; existing v1 readers may ignore this additive field. This
lets --workspace select an archived workspace without extracting other workspace databases. A
scoped backup read never extracts the shared global database; it returns the selected workspace
view as explicitly partial. Legacy backups with one workspace remain scoped-readable, while legacy
multi-workspace backups without this inventory fail closed with
BACKUP_WORKSPACE_SCOPE_METADATA_REQUIRED before database extraction.
Session-ID lookup is byte-exact and case-sensitive, including for canonical UUID syntax, matching
v0.16 behavior. Persist and reuse the exact Session.id spelling returned by Cursor. A differently
cased value is a distinct ID: it is not an alias for lookup, grouping, Composer/Store association,
or migration.
Rename/link to the requested backup path is the publication commit point. If a later permission
read, adjustment, or identity check fails, the command exits nonzero with
BACKUP_PUBLISHED_PERMISSION_FAILED. details.published: true means the commit point was crossed;
trust the reported pathname and inspect/correct its mode only when
details.pathIdentityVerified: true. When it is false, the pathname may have been replaced or
become unverifiable: do not chmod it based on the error, do not assume rollback, and do not blindly
retry with --force.
On POSIX the permission step follows no links: it verifies the published regular file has the same
lossless device/inode identity as private staging, changes mode only through that open descriptor,
and rechecks the final path. A replacement race fails without chmodding the replacement.
If non-force publication commits but its private sibling cannot be removed safely,
BACKUP_PUBLISHED_CLEANUP_FAILED reports output-path identity plus verified and unverified residue
paths. Never blindly delete, chmod, or force-retry an unverified path; a concurrent replacement is
left untouched.
Restore rejects empty inventories, unmanifested file payloads, invalid manifest type/path pairs,
duplicate destinations, and observed links beneath the canonical selected Cursor user root. It
stages only size/checksum-valid entries and preflights all destinations; --force does not bypass
those checks. Integrity-mismatched entries are reported and skipped. New destinations use an
atomic no-clobber publication, while forced replacements publish a new owner-private same-directory
inode instead of writing through an existing hard link. Portable Node path APIs cannot atomically
compare and then replace or unlink a destination, so a failure after any publication never attempts
automatic rollback. It leaves every published destination untouched and throws typed
RESTORE_ROLLBACK_INCOMPLETE details containing all safe manifest-relative residual entries plus
any verified or unverified private temporary residue paths. Stop Cursor and recover those entries
from a known-good backup; never blindly delete an unverified path.
Use an owner-controlled destination tree: Node 20 has no portable directory-relative no-follow
creation API, so restore does not claim atomic defense against a hostile process swapping an
ancestor between the final validation and directory-entry publication.
# Install globally
npm install -g cursor-history
# Use the CLI
cursor-history listIf you prefer pnpm:
# Install globally
pnpm add -g cursor-history
# Use the CLI
cursor-history list# Clone and build
git clone https://github.com/S2thend/cursor_chat_history.git
cd cursor_chat_history
npm install
npm run build
# Run directly
node dist/cli/index.js list
# Or link globally
npm link
cursor-history listEquivalent pnpm workflow:
# Clone and build
git clone https://github.com/S2thend/cursor_chat_history.git
cd cursor_chat_history
pnpm install
pnpm build
# Run directly
node dist/cli/index.js list
node dist/cli/index.js show 1 --json- Node.js 20.x or 22.x–26.x (Node 21 is not supported; Node.js 22.5+ is recommended for built-in SQLite support)
- Cursor IDE (with existing chat history)
cursor-history supports two SQLite drivers for maximum compatibility:
| Driver | Description | Node.js capability boundary |
|---|---|---|
node:sqlite |
Built-in module; selected only when it provides every API required by the operation | Import/read support starts in 22.5; online backup starts in 22.16.0 and 23.8.0 |
better-sqlite3 |
Native binding and automatic fallback when capable | Supported project majors: 20 and 22–26 |
Driver selection is per operation and probes capabilities, not only whether a module imports. In automatic mode cursor-history:
- prefers node:sqlite when it implements all APIs required by that operation; then
- falls back to an installed, capable better-sqlite3.
If neither provider is capable, the operation returns an actionable typed error. Store snapshot capability failures are fatal and are not silently converted into an empty/partial session or a transcript fallback.
You can force a specific driver using the environment variable:
# Force better-sqlite3
CURSOR_HISTORY_SQLITE_DRIVER=better-sqlite3 cursor-history list
# Force node:sqlite (the runtime must support every API needed by this operation)
CURSOR_HISTORY_SQLITE_DRIVER=node:sqlite cursor-history listA forced driver never falls back. If it lacks a required capability, cursor-history reports the driver, operation, missing capability, available alternative, and remedy.
To see which driver is being used:
DEBUG=cursor-history:* cursor-history listWhen using cursor-history as a library, you can control the driver programmatically:
import { setDriver, getActiveDriver, listSessions } from 'cursor-history';
// Force a specific driver before any operations
setDriver('better-sqlite3');
// Check which driver is active
const driver = getActiveDriver();
console.log(`Using driver: ${driver}`);
// Or configure via LibraryConfig
const result = await listSessions({
sqliteDriver: 'node:sqlite' // Force node:sqlite for this call
});# List recent sessions (default: 20)
cursor-history list
# List all sessions
cursor-history list --all
# List with composer IDs (for external tools)
cursor-history list --ids
# Limit results
cursor-history list -n 10
# List workspaces only
cursor-history list --workspaceslist --workspaces is intentionally unscoped discovery and cannot be combined with --workspace.
Choose a path from its output, then use that same --workspace value for scoped session commands.
# Show session by index number
cursor-history show 1
# Show with truncated messages (for quick overview)
cursor-history show 1 --short
# Show full AI thinking/reasoning text
cursor-history show 1 --think
# Show full file read content (not truncated)
cursor-history show 1 --fullread
# Show full error messages (not truncated to 300 chars)
cursor-history show 1 --error
# Filter by message type (user, assistant, tool, thinking, error)
cursor-history show 1 --only user
cursor-history show 1 --only user,assistant
cursor-history show 1 --only tool,error
# Combine options
cursor-history show 1 --short --think --fullread --error
cursor-history show 1 --only user,assistant --short
# Output as JSON
cursor-history show 1 --json# Search for keyword
cursor-history search "react hooks"
# Limit results
cursor-history search "api" -n 5
# Adjust context around matches
cursor-history search "error" --context 100# Export single session to Markdown
cursor-history export 1
# Export to specific file
cursor-history export 1 -o ./my-chat.md
# Export as JSON
cursor-history export 1 --format json
# Export all sessions to directory
cursor-history export --all -o ./exports/
# Overwrite existing files
cursor-history export 1 --force# Move a single session to another workspace
cursor-history migrate-session 1 /path/to/new/project
# Move multiple sessions (comma-separated indices or IDs)
cursor-history migrate-session 1,3,5 /path/to/project
# Copy instead of move (keeps original)
cursor-history migrate-session --copy 1 /path/to/project
# Preview what would happen without making changes
cursor-history migrate-session --dry-run 1 /path/to/project
# Move all sessions from one workspace to another
cursor-history migrate /old/project /new/project
# Copy all sessions (backup)
cursor-history migrate --copy /project /backup/project
# Force merge with existing sessions at destination
cursor-history migrate --force /old/project /existing/project# Create a backup of all chat history
cursor-history backup
# Create backup to specific file
cursor-history backup -o ~/my-backup.zip
# Overwrite existing backup
cursor-history backup --force
# List available backups
cursor-history list-backups
# List backups in a specific directory
cursor-history list-backups -d /path/to/backups
# Restore from a backup
cursor-history restore ~/cursor-history-backups/backup.zip
# Restore to a custom location
cursor-history restore backup.zip --target /custom/cursor/data
# Force overwrite existing data
cursor-history restore backup.zip --force
# View sessions from a backup without restoring
cursor-history list --backup ~/backup.zip
cursor-history show 1 --backup ~/backup.zip
cursor-history search "query" --backup ~/backup.zip
cursor-history export 1 --backup ~/backup.zip# Output as JSON (works with all commands)
cursor-history --json list
# Use custom Cursor data path
cursor-history --data-path ~/.cursor-alt list
# Filter by workspace
cursor-history --workspace /path/to/project listWhen browsing your chat history, you'll see:
- Complete conversations - All messages exchanged with Cursor AI
- Every message rendered - Each resolved message is shown once in order; consecutive duplicates are not folded, so distinct tool calls, provenance, and token data are never hidden
- Timestamps - Composer sessions retain their historical timestamp recovery and interpolation; Store messages show a time only when Cursor provides a directly mapped turn timestamp
- Resolved cross-stack sessions - When one byte-exact UUID exists in Composer and Store, compatible Composer identities are preserved while permitted sources produce one provenance-rich resolved view. Workspace scope is applied before payload reads: known off-scope sources remain unopened and make the result partial; permitted sources follow the documented backbone/alignment policy rather than an unconditional field union.
- AI tool actions - Detailed view of what Cursor AI did:
- File edits/writes - Full diff display with syntax highlighting showing exactly what changed
- File reads - File paths and content previews (use
--fullreadfor complete content) - Search operations - Patterns, paths, and search queries used
- Terminal commands - Complete command text
- Directory listings - Paths explored
- Tool errors - Failed/cancelled operations shown with ❌ status indicator and parameters
- User decisions - Shows if you accepted (✓), rejected (✗), or pending (⏳) on tool operations
- Errors - Error messages with ❌ emoji highlighting (extracted from
toolFormerData.additionalData.status)
- AI reasoning - See the AI's thinking process behind decisions (use
--thinkfor full text) - Code artifacts - Mermaid diagrams, code blocks, with syntax highlighting
- Natural language explanations - AI explanations combined with code for full context
- Default view - Full messages with truncated thinking (200 chars), file reads (100 chars), and errors (300 chars)
--shortmode - Truncates user and assistant messages to 300 chars for quick scanning--thinkflag - Shows complete AI reasoning/thinking text (not truncated)--fullreadflag - Shows full file read content instead of previews--errorflag - Shows full error messages instead of 300-char preview--only <types>flag - Filter messages by type:user,assistant,tool,thinking,error(comma-separated)
A natural-language assistant response that also contains structured tool calls matches both the assistant and tool filters. Tool-only records match only tool.
| Platform | Composer stack | Store stack |
|---|---|---|
| macOS | ~/Library/Application Support/Cursor/User/ |
~/.cursor/ |
| Windows | %APPDATA%/Cursor/User/ |
%USERPROFILE%\.cursor\ |
| Linux / WSL | ~/.config/Cursor/User/ |
~/.cursor/ |
The tool automatically finds and reads both stacks. Per-session store.db is the primary Store
message source. After a capable snapshot/read setup succeeds, the transcript may be a fallback
when the database is absent, contains no usable messages, or has a source-data corruption/read
failure. Driver capability and snapshot-infrastructure failures are fatal and never become a
transcript fallback. A usable database remains the sole Store backbone; a coexisting transcript is
retained only as superseded provenance.
Use --data-path <path> or CURSOR_DATA_PATH to point at a custom Cursor data tree. Use CURSOR_STORE_ROOT to configure the Store root independently. A Store root itself, or its chats, projects, or acp-sessions child, is accepted and normalized to the same root.
Inside WSL, Windows-side Store data is normally mounted at /mnt/c/Users/<windows-user>/.cursor. For example: CURSOR_STORE_ROOT=/mnt/c/Users/<windows-user>/.cursor cursor-history list --all. Use the WSL-side ~/.cursor path instead when the sessions were created by a Cursor agent running inside that WSL distribution.
When running the CLI inside WSL against a Windows-mounted project, do not reuse Windows-installed native node_modules. Native packages are platform-specific; install dependencies with Linux Node.js in a separate WSL dependency tree before running Linux-side tests or builds. cursor-history never installs or removes dependencies automatically.
In addition to the CLI, you can use cursor-history as a library in your Node.js projects:
import {
listSessions,
getSession,
searchSessions,
exportSessionToMarkdown
} from 'cursor-history';
// List all sessions with pagination
const result = await listSessions({ limit: 10 });
console.log(`Found ${result.pagination.total} sessions`);
for (const session of result.data) {
console.log(`${session.id}: ${session.messageCount} messages`);
}
// Get a specific session (zero-based index)
const session = await getSession(0);
console.log(session.messages);
// Search across all sessions
const results = await searchSessions('authentication', { context: 2 });
for (const match of results) {
// Complete message-array index, UTF-16 offset in complete content, and complete source line.
console.log(match.messageIndex, match.offset, match.match);
}
// Export to Markdown
const markdown = await exportSessionToMarkdown(0);These search-coordinate semantics are corrected in 0.18.0. If you persisted values returned by
v0.16/v0.17, recompute them after upgrading; they are not message identities. Library JSON exports
include an additive zero-based session index, consistent with the read API.
import { migrateSession, migrateWorkspace } from 'cursor-history';
// Move a session to another workspace
const moveResults = await migrateSession({
sessions: 3, // index or ID
destination: '/path/to/new/project'
});
console.log(moveResults);
// Copy multiple sessions (keeps originals)
const copyResults = await migrateSession({
sessions: [1, 3, 5],
destination: '/path/to/project',
mode: 'copy'
});
console.log(copyResults);
// Migrate all sessions between workspaces
const workspaceResult = await migrateWorkspace({
source: '/old/project',
destination: '/new/project'
});
console.log(`Migrated ${workspaceResult.successCount} sessions`);import {
createBackup,
restoreBackup,
validateBackup,
listBackups,
getDefaultBackupDir,
listSessions
} from 'cursor-history';
// Create a backup
const result = await createBackup({
outputPath: '~/my-backup.zip',
force: true,
onProgress: (progress) => {
console.log(`${progress.phase}: ${progress.filesCompleted}/${progress.totalFiles}`);
}
});
console.log(`Backup created: ${result.backupPath}`);
console.log(`Sessions: ${result.manifest.stats.sessionCount}`);
// Validate a backup
const validation = await validateBackup('~/backup.zip');
if (validation.status === 'valid') {
console.log('Backup is valid');
} else if (validation.status === 'warnings') {
console.log('Backup has warnings:', validation.corruptedFiles);
}
// Restore from backup
const restoreResult = await restoreBackup({
backupPath: '~/backup.zip',
force: true
});
console.log(`Restored ${restoreResult.filesRestored} files`);
// Check restoreResult.warnings: corrupt entries are skipped, never restored.
// List available backups
const backups = await listBackups(); // Scans ~/cursor-history-backups/
for (const backup of backups) {
console.log(`${backup.filename}: ${backup.manifest?.stats.sessionCount} sessions`);
}
// Read sessions from backup without restoring
const sessions = await listSessions({ backupPath: '~/backup.zip' });| Function | Description |
|---|---|
listSessions(config?) |
List sessions with pagination |
getSession(index, config?) |
Get full session by index |
searchSessions(query, config?) |
Search across sessions |
exportSessionToJson(index, config?) |
Export session to JSON |
exportSessionToMarkdown(index, config?) |
Export session to Markdown |
exportAllSessionsToJson(config?) |
Export all sessions to JSON |
exportAllSessionsToMarkdown(config?) |
Export all sessions to Markdown |
migrateSession(config) |
Move/copy sessions to another workspace |
migrateWorkspace(config) |
Move/copy all sessions between workspaces |
createBackup(config?) |
Create full backup of all chat history |
restoreBackup(config) |
Restore chat history from backup |
validateBackup(path) |
Validate backup integrity |
listBackups(directory?) |
List available backup files |
getDefaultBackupDir() |
Get default backup directory path |
getDefaultDataPath() |
Get platform-specific Cursor data path |
setDriver(name) |
Set SQLite driver ('better-sqlite3' or 'node:sqlite') |
getActiveDriver() |
Get currently active SQLite driver name |
import type { MessageType } from 'cursor-history';
interface LibraryConfig {
dataPath?: string; // Custom Cursor data path
workspace?: string; // Filter by workspace path
limit?: number; // Pagination limit
offset?: number; // Pagination offset
context?: number; // Search context lines
backupPath?: string; // Read from backup file instead of live data
sqliteDriver?: 'better-sqlite3' | 'node:sqlite'; // Force specific SQLite driver
messageFilter?: MessageType[]; // Filter messages by type (user, assistant, tool, thinking, error)
}import {
listSessions,
createBackup,
restoreBackup,
isDatabaseLockedError,
isDatabaseNotFoundError,
isSessionNotFoundError,
isWorkspaceNotFoundError,
isBackupError,
isBackupPublishedPermissionError,
isRestoreRollbackError,
isRestoreError,
isInvalidBackupError,
validateMessageTypes
} from 'cursor-history';
try {
const result = await listSessions();
} catch (err) {
if (isDatabaseLockedError(err)) {
console.error('Database locked - close Cursor and retry');
} else if (isDatabaseNotFoundError(err)) {
console.error('Cursor data not found');
} else if (isSessionNotFoundError(err)) {
console.error('Session not found');
} else if (isWorkspaceNotFoundError(err)) {
console.error('Workspace not found - open project in Cursor first');
}
}
try {
await createBackup({ outputPath: '/private/backups/cursor.zip' });
} catch (err) {
if (isBackupPublishedPermissionError(err)) {
if (err.details.pathIdentityVerified) {
console.error('Verified published backup needs a mode correction:', err.details.outputPath);
} else {
// The commit point was crossed, but this pathname is untrusted. Do not chmod it from here.
console.error('Published backup path requires identity recovery:', err.details.outputPath);
}
}
}
// Validate untyped filter values before passing them to a read operation
const invalidTypes = validateMessageTypes(['invalid']);
if (invalidTypes.length > 0) {
console.error('Invalid filter types:', invalidTypes);
}
// Backup-specific errors
try {
await createBackup();
} catch (err) {
if (isBackupError(err)) {
console.error('Backup failed:', err.message);
} else if (isInvalidBackupError(err)) {
console.error('Invalid backup file');
} else if (isRestoreError(err)) {
console.error('Restore failed:', err.message);
}
}
try {
await restoreBackup({ backupPath: '/private/backups/cursor.zip', force: true });
} catch (err) {
if (isRestoreRollbackError(err)) {
// These are manifest-relative paths, never private physical locators.
console.error('Manual recovery required for:', err.details.residualFiles);
}
}npm install
npm run buildWith pnpm:
pnpm install
pnpm buildnpm test # Run all tests
npm run test:watch # Watch modeWith pnpm:
pnpm test # Run all tests
pnpm test:watch # Watch mode
pnpm typecheckReleases use npm trusted publishing through GitHub Actions. No NPM_TOKEN repository secret is
used. Before the first release:
- Configure the npm package's trusted publisher for this exact GitHub repository, enter
npm-publish.ymlas the workflow filename (the file lives at.github/workflows/npm-publish.yml), set the environment tonpm-release-verification, and allow thenpm publishaction. - Create the GitHub environment
npm-release-verification, require designated maintainer reviewers, and prevent unreviewed bypass according to the repository's protection policy.
For each release:
- Update and validate all versioned package metadata and release notes, complete the documented release gates, and freeze one clean revision.
- Confirm the version tag does not already exist, then push only that tag (for example,
git push origin v0.18.0). Do not push or move a release tag before the revision is frozen. - The workflow validates sources and every supported runtime, packs exactly once, and binds the
candidate to its revision and SHA-256. After those gates pass, the actual
publishjob pauses at the protectednpm-release-verificationenvironment before it can request its OIDC token. - Download that checksum-addressed candidate and complete the private exact-artifact checks in docs/release-verification.md. Approve the environment only after those checks pass.
- Approval publishes those same preserved bytes with npm provenance; the workflow does not rebuild or repack them.
Any failed source, runtime, artifact, or private verification blocks publication. Never silently force-move a release tag; remediate an unpublished failed candidate explicitly, and use a new version if any bytes have already been published.
We welcome contributions from the community! Here's how you can help:
- Bug reports: Open an issue with steps to reproduce, expected vs actual behavior, and your environment (OS, Node.js version)
- Feature requests: Open an issue describing the feature and its use case
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes
- Run tests and linting (
npm test && npm run lint) - Commit your changes (
git commit -m 'Add my feature') - Push to your fork (
git push origin feature/my-feature) - Open a Pull Request
git clone https://github.com/S2thend/cursor_chat_history.git
cd cursor_chat_history
npm install
npm run build
npm testOr with pnpm:
git clone https://github.com/S2thend/cursor_chat_history.git
cd cursor_chat_history
pnpm install
pnpm build
pnpm testMIT
