evernote-migration-deep-dive
Deep dive into Evernote data migration strategies. Use when migrating to/from Evernote, bulk data transfers, or complex migration scenarios. Trigger with phrases like "migrate to evernote", "migrate from evernote", "evernote data transfer", "bulk evernote migration". allowed-tools: Read, Write, Edit, Bash(npm:*), Grep version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>
Allowed Tools
No tools specified
Provided by Plugin
evernote-pack
Claude Code skill pack for Evernote (24 skills)
Installation
This skill is included in the evernote-pack plugin:
/plugin install evernote-pack@claude-code-plugins-plus
Click to copy
Instructions
# Evernote Migration Deep Dive
## Overview
Comprehensive guide for migrating data to and from Evernote, including bulk operations, format conversions, and maintaining data integrity.
## Prerequisites
- Understanding of Evernote data model
- Source/target system access
- Sufficient API quota for migration
- Backup strategy in place
## Migration Scenarios
| Scenario | Source | Target | Complexity |
|----------|--------|--------|------------|
| Import to Evernote | External | Evernote | Medium |
| Export from Evernote | Evernote | External | Medium |
| Evernote to Evernote | Account A | Account B | High |
| Bulk archive | Evernote | Archive storage | Low |
## Instructions
### Step 1: Migration Planning
```javascript
// migration/planner.js
class MigrationPlanner {
constructor(sourceClient, targetConfig) {
this.source = sourceClient;
this.target = targetConfig;
this.plan = null;
}
/**
* Analyze source data
*/
async analyzeSource() {
const noteStore = this.source.getNoteStore();
// Get all notebooks
const notebooks = await noteStore.listNotebooks();
// Get all tags
const tags = await noteStore.listTags();
// Count notes per notebook
const noteCounts = {};
for (const notebook of notebooks) {
const filter = new Evernote.NoteStore.NoteFilter({
notebookGuid: notebook.guid
});
const result = await noteStore.findNotesMetadata(
filter, 0, 1,
new Evernote.NoteStore.NotesMetadataResultSpec({})
);
noteCounts[notebook.guid] = result.totalNotes;
}
// Estimate sizes
const analysis = {
notebooks: notebooks.length,
tags: tags.length,
totalNotes: Object.values(noteCounts).reduce((a, b) => a + b, 0),
notebookDetails: notebooks.map(nb => ({
guid: nb.guid,
name: nb.name,
noteCount: noteCounts[nb.guid]
})),
estimatedDuration: this.estimateDuration(Object.values(noteCounts).reduce((a, b) => a + b, 0)),
recommendations: []
};
// Generate recommendations
if (analysis.totalNotes > 10000) {
analysis.recommendations.push('Consider incremental migration');
}
if (analysis.notebooks > 100) {
analysis.recommendations.push('Plan notebook mapping before migration');
}
return analysis;
}
/**
* Estimate migration duration
*/
estimateDuration(noteCount) {
// Rough estimate: 1 note per second with rate limiting
const seconds = noteCount;
const hours = Math.ceil(seconds / 3600);
return `Approximately ${hours} hour(s)`;
}
/**
* Create migration plan
*/
async createPlan(options = {}) {
const analysis = await this.analyzeSource();
this.plan = {
id: `migration-${Date.now()}`,
createdAt: new Date().toISOString(),
source: {
type: 'evernote',
analysis
},
target: this.target,
options: {
includeResources: options.includeResources ?? true,
preserveTags: options.preserveTags ?? true,
preserveTimestamps: options.preserveTimestamps ?? true,
notebookMapping: options.notebookMapping || {},
excludeNotebooks: options.excludeNotebooks || [],
batchSize: options.batchSize || 50,
concurrency: options.concurrency || 1
},
status: 'planned',
progress: {
total: analysis.totalNotes,
completed: 0,
failed: 0
}
};
return this.plan;
}
}
module.exports = MigrationPlanner;
```
### Step 2: Export from Evernote
```javascript
// migration/evernote-exporter.js
const fs = require('fs').promises;
const path = require('path');
class EvernoteExporter {
constructor(noteStore, options = {}) {
this.noteStore = noteStore;
this.outputDir = options.outputDir || './export';
this.format = options.format || 'enex'; // enex, json, markdown
}
/**
* Export all data
*/
async exportAll(progressCallback) {
await fs.mkdir(this.outputDir, { recursive: true });
const notebooks = await this.noteStore.listNotebooks();
const tags = await this.noteStore.listTags();
// Export metadata
await this.exportMetadata(notebooks, tags);
// Export each notebook
let total = 0;
let exported = 0;
for (const notebook of notebooks) {
const noteCount = await this.exportNotebook(notebook, (progress) => {
progressCallback?.({
phase: 'notes',
notebook: notebook.name,
...progress
});
});
total += noteCount;
exported += noteCount;
}
return {
notebooks: notebooks.length,
tags: tags.length,
notes: total,
outputDir: this.outputDir
};
}
/**
* Export metadata
*/
async exportMetadata(notebooks, tags) {
const metadata = {
exportedAt: new Date().toISOString(),
notebooks: notebooks.map(nb => ({
guid: nb.guid,
name: nb.name,
stack: nb.stack,
defaultNotebook: nb.defaultNotebook
})),
tags: tags.map(t => ({
guid: t.guid,
name: t.name,
parentGuid: t.parentGuid
}))
};
await fs.writeFile(
path.join(this.outputDir, 'metadata.json'),
JSON.stringify(metadata, null, 2)
);
}
/**
* Export notebook
*/
async exportNotebook(notebook, progressCallback) {
const notebookDir = path.join(this.outputDir, this.sanitizeName(notebook.name));
await fs.mkdir(notebookDir, { recursive: true });
const filter = new Evernote.NoteStore.NoteFilter({
notebookGuid: notebook.guid
});
const spec = new Evernote.NoteStore.NotesMetadataResultSpec({
includeTitle: true,
includeUpdated: true
});
// Get note list
const result = await this.noteStore.findNotesMetadata(filter, 0, 10000, spec);
const total = result.notes.length;
let exported = 0;
for (const noteMeta of result.notes) {
try {
// Get full note with resources
const note = await this.noteStore.getNote(
noteMeta.guid,
true, // withContent
true, // withResourcesData
false, // withResourcesRecognition
false // withResourcesAlternateData
);
await this.exportNote(note, notebookDir);
exported++;
progressCallback?.({
current: exported,
total,
note: note.title
});
// Rate limit protection
await this.sleep(100);
} catch (error) {
console.error(`Failed to export note ${noteMeta.guid}:`, error.message);
}
}
return exported;
}
/**
* Export single note
*/
async exportNote(note, outputDir) {
const noteDir = path.join(outputDir, this.sanitizeName(note.title) + '_' + note.guid.slice(0, 8));
await fs.mkdir(noteDir, { recursive: true });
switch (this.format) {
case 'json':
await this.exportAsJson(note, noteDir);
break;
case 'markdown':
await this.exportAsMarkdown(note, noteDir);
break;
case 'enex':
default:
await this.exportAsEnex(note, noteDir);
}
// Export resources
if (note.resources) {
const resourcesDir = path.join(noteDir, 'resources');
await fs.mkdir(resourcesDir, { recursive: true });
for (const resource of note.resources) {
await this.exportResource(resource, resourcesDir);
}
}
}
/**
* Export as JSON
*/
async exportAsJson(note, outputDir) {
const data = {
guid: note.guid,
title: note.title,
content: note.content,
created: note.created,
updated: note.updated,
tagGuids: note.tagGuids,
resources: note.resources?.map(r => ({
guid: r.guid,
mime: r.mime,
fileName: r.attributes?.fileName,
hash: Buffer.from(r.data.bodyHash).toString('hex')
}))
};
await fs.writeFile(
path.join(outputDir, 'note.json'),
JSON.stringify(data, null, 2)
);
}
/**
* Export as Markdown
*/
async exportAsMarkdown(note, outputDir) {
let markdown = `# ${note.title}\n\n`;
markdown += `Created: ${new Date(note.created).toISOString()}\n`;
markdown += `Updated: ${new Date(note.updated).toISOString()}\n\n`;
markdown += `---\n\n`;
markdown += this.enmlToMarkdown(note.content);
await fs.writeFile(
path.join(outputDir, 'note.md'),
markdown
);
}
/**
* Convert ENML to Markdown
*/
enmlToMarkdown(enml) {
let md = enml
// Remove XML declaration and DOCTYPE
.replace(/<\?xml[^>]*\?>/g, '')
.replace(/]*>/g, '')
// Remove en-note wrapper
.replace(/<\/?en-note[^>]*>/g, '')
// Convert headers
.replace(/]*>(.*?)<\/h1>/gi, '# $1\n\n')
.replace(/