Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Episodic Memory

Semantic search for Claude Code and Codex conversations. Remember past discussions, decisions, and patterns.
Semantic search for Claude Code, Codex, and Cursor conversations. Remember past discussions, decisions, and patterns.

## Testimonial

Expand Down Expand Up @@ -102,7 +102,7 @@ npm install -g github:obra/episodic-memory
### Quick Start

```bash
# Sync conversations from Claude Code and Codex and index them
# Sync conversations from Claude Code, Codex, and Cursor and index them
episodic-memory sync

# Search your conversation history
Expand All @@ -118,6 +118,23 @@ episodic-memory doctor codex
episodic-memory show path/to/conversation.jsonl
```

### Cursor support

Sync automatically indexes Cursor agent transcripts from `~/.cursor/projects`
(written by Cursor since early 2026). Conversations older than that exist only
inside Cursor's global SQLite store; backfill them once with:

```bash
# Export legacy Cursor conversations from state.vscdb (read-only), then index
episodic-memory import-cursor-history
episodic-memory sync
```

The importer skips conversations that already have a live agent transcript,
recovers each conversation's project from tool-call working directories, and
embeds original message timestamps. Re-running it only exports new
conversations; use `--force` to re-export everything.

### Command Line

```bash
Expand Down
19 changes: 12 additions & 7 deletions cli/episodic-memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@ function runScript(scriptPath, args) {
}

function showHelp() {
console.log(`episodic-memory - Manage and search Claude Code and Codex conversations
console.log(`episodic-memory - Manage and search Claude Code, Codex, and Cursor conversations

USAGE:
episodic-memory <command> [options]

COMMANDS:
sync Sync conversations from Claude Code and Codex and index them
index Index conversations for search
search Search indexed conversations
show Display a conversation in readable format
stats Show index statistics
doctor Diagnose Claude Code or Codex integration issues
sync Sync conversations from Claude Code, Codex, and Cursor and index them
index Index conversations for search
search Search indexed conversations
show Display a conversation in readable format
stats Show index statistics
doctor Diagnose Claude Code or Codex integration issues
import-cursor-history Export legacy Cursor conversations from state.vscdb for indexing

Run 'episodic-memory <command> --help' for command-specific help.

Expand Down Expand Up @@ -89,6 +90,10 @@ async function main() {
await runScript(join(distDir, 'sync-cli.js'), args);
break;

case 'import-cursor-history':
await runScript(join(distDir, 'cursor-import-cli.js'), args);
break;

case '--help':
case '-h':
case undefined:
Expand Down
1 change: 1 addition & 0 deletions dist/cursor-import-cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
91 changes: 91 additions & 0 deletions dist/cursor-import-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import path from 'path';
import { getDefaultCursorVscdbPath, collectLiveTranscriptIds, importCursorLegacy, } from './cursor-legacy.js';
import { getCursorDir, getCursorLegacyExportDir } from './paths.js';
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Usage: episodic-memory import-cursor-history [options]

Export legacy Cursor conversations from Cursor's global SQLite store
(state.vscdb) as JSONL transcripts, so sync can archive and index them.

Cursor only began writing per-session agent transcripts to ~/.cursor/projects
in early 2026; conversations older than that exist only in state.vscdb. This
command backfills them. Conversations that already have a live agent
transcript are skipped automatically.

The export is written to:
${getCursorLegacyExportDir()}
which sync scans as a conversation source. Run 'episodic-memory sync' after
importing to archive and index the exported conversations.

The database is opened read-only; Cursor's data is never modified.

OPTIONS:
--db <path> Path to state.vscdb (default: auto-detected per platform)
--out <dir> Export directory (default: shown above)
--force Re-export conversations whose output file already exists
--dry-run Report what would be exported without writing files

EXAMPLES:
# Export all legacy conversations, then index them
episodic-memory import-cursor-history
episodic-memory sync

# Preview without writing
episodic-memory import-cursor-history --dry-run
`);
process.exit(0);
}
function argValue(flag) {
const index = args.indexOf(flag);
if (index === -1)
return undefined;
const value = args[index + 1];
if (!value || value.startsWith('--')) {
console.error(`Error: ${flag} requires a value`);
process.exit(1);
}
return value;
}
const dbPath = argValue('--db') ?? getDefaultCursorVscdbPath();
if (!dbPath) {
console.error('Error: could not find Cursor state.vscdb; pass --db <path>');
process.exit(1);
}
const exportDir = argValue('--out') ?? getCursorLegacyExportDir();
const dryRun = args.includes('--dry-run');
const force = args.includes('--force');
const cursorProjectsDir = path.join(getCursorDir(), 'projects');
const liveTranscriptIds = collectLiveTranscriptIds(cursorProjectsDir);
console.log(`Importing legacy Cursor conversations${dryRun ? ' (dry run)' : ''}...`);
console.log(` Database: ${dbPath}`);
console.log(` Export dir: ${exportDir}`);
console.log(` Live transcripts found: ${liveTranscriptIds.size}\n`);
try {
const result = importCursorLegacy({
dbPath,
exportDir,
liveTranscriptIds,
force,
dryRun,
});
console.log(`✅ Import ${dryRun ? 'preview' : 'complete'}!`);
console.log(` Exported: ${result.exported}`);
console.log(` Skipped (live transcript exists): ${result.skippedLive}`);
console.log(` Skipped (already exported): ${result.skippedExisting}`);
console.log(` Skipped (empty): ${result.skippedEmpty}`);
if (result.errors.length > 0) {
console.log(`\n⚠️ Errors: ${result.errors.length}`);
for (const err of result.errors.slice(0, 10)) {
console.log(` ${err.composerId}: ${err.error}`);
}
}
if (!dryRun && result.exported > 0) {
console.log(`\nRun 'episodic-memory sync' to archive and index the exported conversations.`);
}
}
catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : error}`);
process.exit(1);
}
28 changes: 28 additions & 0 deletions dist/cursor-legacy.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export declare function getDefaultCursorVscdbPath(): string | undefined;
/**
* Collect composer IDs that already have live agent transcripts under
* <cursorDir>/projects/<slug>/agent-transcripts/<uuid>/, so the importer
* doesn't export conversations sync already picks up from there.
*/
export declare function collectLiveTranscriptIds(cursorProjectsDir: string): Set<string>;
export interface CursorLegacyImportOptions {
dbPath: string;
exportDir: string;
/** Composer IDs covered by live agent transcripts (skipped). */
liveTranscriptIds?: Set<string>;
/** Re-export conversations whose output file already exists. */
force?: boolean;
/** Report what would be exported without writing files. */
dryRun?: boolean;
}
export interface CursorLegacyImportResult {
exported: number;
skippedLive: number;
skippedExisting: number;
skippedEmpty: number;
errors: Array<{
composerId: string;
error: string;
}>;
}
export declare function importCursorLegacy(options: CursorLegacyImportOptions): CursorLegacyImportResult;
Loading