obsidian-migration-deep-dive
Execute major Obsidian plugin rewrites and migration strategies. Use when migrating to or from Obsidian, performing major plugin rewrites, or re-platforming existing note systems to Obsidian. Trigger with phrases like "migrate to obsidian", "obsidian migration", "convert notes to obsidian", "obsidian replatform".
What this skill does
# Obsidian Migration Deep Dive
## Current State
!`node --version 2>/dev/null || echo 'N/A'`
!`ls *.enex *.json *.zip 2>/dev/null | head -10 || echo 'No export files in cwd'`
## Overview
Migrate notes from Notion, Evernote, Roam Research, Bear, and Apple Notes into Obsidian -- handling attachment relocation, internal link conversion to `[[wikilinks]]`, tag migration, and frontmatter generation.
## Prerequisites
- Exported data from the source application (see each section for format)
- A target Obsidian vault created and opened at least once
- Node.js 18+ for running migration scripts
- Backup of source data before starting
## Instructions
### Step 1: Pre-Migration Assessment
```bash
#!/bin/bash
# assess-migration.sh <export-directory>
EXPORT_DIR="${1:-.}"
echo "=== Migration Assessment: $EXPORT_DIR ==="
echo "File counts:"
for ext in md html enex json csv pdf png jpg gif zip; do
count=$(find "$EXPORT_DIR" -name "*.$ext" 2>/dev/null | wc -l)
[ "$count" -gt 0 ] && echo " .$ext: $count"
done
echo "Total size: $(du -sh "$EXPORT_DIR" 2>/dev/null | cut -f1)"
echo "Max directory depth: $(find "$EXPORT_DIR" -type d | awk -F/ '{print NF-1}' | sort -n | tail -1)"
echo "Sample filenames:"
find "$EXPORT_DIR" -type f | head -5
```
### Step 2: Notion Export Migration
Notion exports as a zip containing markdown files, CSV databases, and attachments. The markdown uses Notion-style links and has UUIDs appended to filenames.
```javascript
// notion-to-obsidian.mjs
import { readdir, readFile, writeFile, mkdir, copyFile } from 'fs/promises';
import { join, basename, extname, dirname } from 'path';
const NOTION_EXPORT = process.argv[2]; // Unzipped Notion export
const VAULT_DIR = process.argv[3]; // Target Obsidian vault
if (!NOTION_EXPORT || !VAULT_DIR) {
console.error('Usage: node notion-to-obsidian.mjs <notion-export-dir> <vault-dir>');
process.exit(1);
}
// Step 1: Build a filename map (strip Notion UUIDs from names)
// Notion appends " abc123def456" to every filename
function cleanNotionName(filename) {
return filename.replace(/\s+[a-f0-9]{32}(?=\.\w+$|$)/, '');
}
async function* walkDir(dir) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) yield* walkDir(fullPath);
else yield fullPath;
}
}
async function migrate() {
const fileMap = new Map(); // original path -> clean path
const attachments = [];
const notes = [];
// Categorize files
for await (const filePath of walkDir(NOTION_EXPORT)) {
const ext = extname(filePath).toLowerCase();
const relPath = filePath.slice(NOTION_EXPORT.length + 1);
const cleanPath = relPath.split('/').map(cleanNotionName).join('/');
fileMap.set(relPath, cleanPath);
if (ext === '.md') notes.push({ src: filePath, dest: cleanPath });
else if (ext === '.csv') notes.push({ src: filePath, dest: cleanPath.replace('.csv', '.md'), isCSV: true });
else attachments.push({ src: filePath, dest: join('attachments', basename(cleanPath)) });
}
// Process markdown notes
for (const note of notes) {
let content;
if (note.isCSV) {
content = await convertCSVToMarkdown(note.src);
} else {
content = await readFile(note.src, 'utf-8');
}
// Convert Notion links to Obsidian wikilinks
// Notion: Page Title
// Obsidian: [[Page Title]]
content = content.replace(
/\[([^\]]+)\]\(([^)]+\.md)\)/g,
(match, text, href) => {
const decoded = decodeURIComponent(href);
const clean = cleanNotionName(basename(decoded, '.md'));
return `[[${clean}]]`;
}
);
// Convert Notion image references to Obsidian
// Notion: !description
// Obsidian: ![[image-name.png]]
content = content.replace(
/!\[([^\]]*)\]\(([^)]+)\)/g,
(match, alt, src) => {
const decoded = decodeURIComponent(src);
if (decoded.startsWith('http')) return match; // Keep external URLs
const clean = cleanNotionName(basename(decoded));
return `![[${clean}]]`;
}
);
// Add frontmatter
const title = basename(note.dest, extname(note.dest));
content = `---\ntitle: "${title}"\nsource: notion\nmigrated: ${new Date().toISOString().split('T')[0]}\n---\n\n${content}`;
const destPath = join(VAULT_DIR, note.dest);
await mkdir(dirname(destPath), { recursive: true });
await writeFile(destPath, content);
}
// Copy attachments
await mkdir(join(VAULT_DIR, 'attachments'), { recursive: true });
for (const att of attachments) {
await copyFile(att.src, join(VAULT_DIR, att.dest));
}
console.log(`Migrated ${notes.length} notes, ${attachments.length} attachments`);
}
async function convertCSVToMarkdown(csvPath) {
const raw = await readFile(csvPath, 'utf-8');
const lines = raw.trim().split('\n');
if (lines.length === 0) return '';
const headers = lines[0].split(',').map(h => h.replace(/^"|"$/g, ''));
const rows = lines.slice(1).map(line =>
line.split(',').map(c => c.replace(/^"|"$/g, ''))
);
let md = `| ${headers.join(' | ')} |\n`;
md += `| ${headers.map(() => '---').join(' | ')} |\n`;
for (const row of rows) {
md += `| ${row.join(' | ')} |\n`;
}
return md;
}
migrate().catch(console.error);
```
Run it:
```bash
unzip Notion-Export-*.zip -d notion-export
node notion-to-obsidian.mjs notion-export ~/my-vault
```
### Step 3: Evernote ENEX Migration
ENEX files are XML containing notes with HTML content and embedded attachments (base64).
```javascript
// evernote-to-obsidian.mjs
import { readFile, writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import { parseString } from 'xml2js'; // npm install xml2js
import TurndownService from 'turndown'; // npm install turndown
const ENEX_FILE = process.argv[2];
const VAULT_DIR = process.argv[3];
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
function parseENEX(xml) {
return new Promise((resolve, reject) => {
parseString(xml, (err, result) => {
if (err) reject(err);
else resolve(result['en-export']?.note || []);
});
});
}
function sanitizeFilename(name) {
return name.replace(/[<>:"/\\|?*]/g, '-').replace(/\s+/g, ' ').trim();
}
async function migrate() {
const xml = await readFile(ENEX_FILE, 'utf-8');
const notes = await parseENEX(xml);
await mkdir(join(VAULT_DIR, 'attachments'), { recursive: true });
let count = 0;
for (const note of notes) {
const title = sanitizeFilename(note.title?.[0] || `Untitled-${count}`);
const html = note.content?.[0] || '';
const created = note.created?.[0] || '';
const tags = note.tag || [];
// Convert HTML to Markdown
// Strip ENEX wrapper: <en-note>...</en-note>
const bodyHtml = html.replace(/<\/?en-note[^>]*>/g, '');
let markdown = turndown.turndown(bodyHtml);
// Build frontmatter
const fm = [
'---',
`title: "${title}"`,
`source: evernote`,
`created: ${formatEvernoteDate(created)}`,
`migrated: ${new Date().toISOString().split('T')[0]}`,
];
if (tags.length > 0) {
fm.push(`tags: [${tags.map(t => `"${t}"`).join(', ')}]`);
}
fm.push('---', '');
// Extract attachments (base64 resources)
const resources = note.resource || [];
for (const res of resources) {
const mime = res.mime?.[0] || 'application/octet-stream';
const data = res.data?.[0]?._ || res.data?.[0] || '';
const filename = res['resource-attributes']?.[0]?.['file-name']?.[0]
|| `attachment-${count}-${resources.indexOf(res)}.${mime.split('/')[1] || 'bin'}`;
const attPath = join(VAULT_DIR, 'attachments', sanitizeFilename(filename));
await writeFile(attPath, Buffer.from(data, 'base64'));
// Replace en-media tags in markdown with Obsidian embeds
markdown = markdown.replace(
new RegExp(`\\[.*?\\]\\(.*?${escapeRegex(filename)}.*?Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.