Synthèse automatique
Ce skill fournit des bonnes pratiques pour stocker et traiter les données Evernote : schéma relationnel pour carnets, notes, tags et pièces jointes, et traitement du contenu ENML.
Points forts
- Propose un schéma SQL complet et cohérent couvrant utilisateurs, carnets, notes, tags, ressources et état de synchronisation
- Couvre les points sensibles du modèle Evernote : limites de quotas, contenu ENML, hachage du contenu et des pièces jointes
- Donne des index et contraintes d'unicité adaptés aux requêtes fréquentes (par utilisateur, carnet, date de mise à jour)
- Structuré en étapes concrètes, avec des exemples de code directement réutilisables
Limites
- Le skill est tronqué : la partie sur le traitement ENML s'arrête au milieu d'un extrait JavaScript
- Le DDL mélange une syntaxe PostgreSQL et des déclarations INDEX inline qui ne sont pas valides en PostgreSQL tel quel
- Ne traite pas la résolution de conflits de synchronisation, le chiffrement des données ni les quotas réels de l'API Evernote
À utiliser lorsqu'on conçoit une base locale ou un service de synchronisation qui doit stocker des notes, tags et pièces jointes Evernote.
À éviter si l'on a seulement besoin d'appeler l'API Evernote en lecture ponctuelle, sans persistance locale ni traitement ENML.
Analyse de sécurité
SûrThe skill is a documentation/architecture guide with sample PostgreSQL schema and JavaScript helper classes for Evernote data handling. It does not instruct the agent to run shell commands, fetch remote payloads, expose secrets, or disable safety controls. The only destructive code is a clearly labeled GDPR deletion helper using parameterized SQL and an fs.rm call scoped to a user-specific storage path; it is contextual example code, not an imperative to execute. The declared tools are limited to Read, Write, Edit, and Grep, with no bash or network access.
Aucun point d'attention détecté
Comment cette analyse est faiteExemples
Design a PostgreSQL schema to cache Evernote notebooks, notes, tags and attachments locally, including sync state tracking.Write code that parses Evernote ENML note content, extracts plain text and to-do items, and stores the note body in my database.Implement storage for Evernote note resources: hash the binary data, keep metadata like MIME type and size, and deduplicate identical attachments.name: evernote-data-handling description: | Best practices for handling Evernote data. Use when implementing data storage, processing notes, handling attachments, or ensuring data integrity. Trigger with phrases like "evernote data", "handle evernote notes", "evernote storage", "process evernote content". allowed-tools: Read, Write, Edit, Grep version: 1.0.0 license: MIT author: Jeremy Longshore jeremy@intentsolutions.io
Evernote Data Handling
Overview
Best practices for handling Evernote data including notes, attachments, ENML content, and synchronization data.
Prerequisites
- Understanding of Evernote data model
- Database for local storage
- File storage for attachments
Evernote Data Model
User
├── Notebooks (max 250-1000)
│ └── Notes (max 100,000)
│ ├── Content (ENML format)
│ ├── Resources (attachments)
│ ├── Tags (many-to-many)
│ └── Attributes (metadata)
├── Tags (max 100,000)
└── Saved Searches (max 100)
Instructions
Step 1: Data Schema Design
-- PostgreSQL schema for Evernote data
-- Users
CREATE TABLE evernote_users (
id SERIAL PRIMARY KEY,
evernote_user_id BIGINT UNIQUE NOT NULL,
username VARCHAR(255),
email VARCHAR(255),
privilege INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Notebooks
CREATE TABLE notebooks (
guid UUID PRIMARY KEY,
user_id INTEGER REFERENCES evernote_users(id),
name VARCHAR(255) NOT NULL,
stack VARCHAR(255),
default_notebook BOOLEAN DEFAULT FALSE,
created_at BIGINT,
updated_at BIGINT,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_notebooks_user (user_id),
INDEX idx_notebooks_stack (stack)
);
-- Tags
CREATE TABLE tags (
guid UUID PRIMARY KEY,
user_id INTEGER REFERENCES evernote_users(id),
name VARCHAR(255) NOT NULL,
parent_guid UUID REFERENCES tags(guid),
update_sequence_num INTEGER,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tags_user (user_id),
INDEX idx_tags_parent (parent_guid),
UNIQUE(user_id, name)
);
-- Notes
CREATE TABLE notes (
guid UUID PRIMARY KEY,
user_id INTEGER REFERENCES evernote_users(id),
notebook_guid UUID REFERENCES notebooks(guid),
title VARCHAR(255) NOT NULL,
content TEXT, -- ENML content
content_hash BYTEA,
content_length INTEGER,
created_at BIGINT,
updated_at BIGINT,
deleted_at BIGINT,
active BOOLEAN DEFAULT TRUE,
update_sequence_num INTEGER,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_notes_user (user_id),
INDEX idx_notes_notebook (notebook_guid),
INDEX idx_notes_updated (updated_at),
INDEX idx_notes_active (active)
);
-- Note-Tag relationship
CREATE TABLE note_tags (
note_guid UUID REFERENCES notes(guid),
tag_guid UUID REFERENCES tags(guid),
PRIMARY KEY (note_guid, tag_guid)
);
-- Resources (attachments)
CREATE TABLE resources (
guid UUID PRIMARY KEY,
note_guid UUID REFERENCES notes(guid),
mime_type VARCHAR(100) NOT NULL,
width INTEGER,
height INTEGER,
duration INTEGER,
data_hash BYTEA NOT NULL,
data_size INTEGER NOT NULL,
file_name VARCHAR(255),
source_url TEXT,
storage_path VARCHAR(500), -- Path to stored file
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_resources_note (note_guid),
INDEX idx_resources_hash (data_hash)
);
-- Sync state
CREATE TABLE sync_state (
user_id INTEGER PRIMARY KEY REFERENCES evernote_users(id),
last_update_count INTEGER DEFAULT 0,
last_sync_time BIGINT,
full_sync_required BOOLEAN DEFAULT TRUE
);
Step 2: ENML Content Processing
// services/enml-processor.js
const cheerio = require('cheerio');
const crypto = require('crypto');
class ENMLProcessor {
/**
* Parse ENML content
*/
parse(enml) {
const $ = cheerio.load(enml, {
xmlMode: true,
decodeEntities: true
});
return {
$,
title: $('title').text(),
text: this.extractText($),
todos: this.extractTodos($),
links: this.extractLinks($),
media: this.extractMedia($),
checksum: this.computeChecksum(enml)
};
}
/**
* Extract plain text from ENML
*/
extractText($) {
// Remove en-media and en-crypt elements
$('en-media, en-crypt').remove();
// Get text content
return $('en-note')
.text()
.replace(/\s+/g, ' ')
.trim();
}
/**
* Extract todos
*/
extractTodos($) {
const todos = [];
$('en-todo').each((i, el) => {
const checked = $(el).attr('checked') === 'true';
// Get text after the todo element
const text = $(el).next().text().trim() ||
$(el).parent().text().trim();
todos.push({
index: i,
checked,
text: text.slice(0, 200) // Limit length
});
});
return todos;
}
/**
* Extract links
*/
extractLinks($) {
const links = [];
$('a[href]').each((i, el) => {
const href = $(el).attr('href');
const text = $(el).text();
// Skip javascript: and mailto: links
if (href && !href.startsWith('javascript:') && !href.startsWith('mailto:')) {
links.push({ href, text });
}
});
return links;
}
/**
* Extract media references
*/
extractMedia($) {
const media = [];
$('en-media').each((i, el) => {
media.push({
type: $(el).attr('type'),
hash: $(el).attr('hash'),
width: parseInt($(el).attr('width')) || null,
height: parseInt($(el).attr('height')) || null
});
});
return media;
}
/**
* Compute content checksum
*/
computeChecksum(content) {
return crypto.createHash('md5').update(content).digest('hex');
}
/**
* Validate ENML structure
*/
validate(enml) {
const errors = [];
if (!enml.includes('<?xml version="1.0"')) {
errors.push('Missing XML declaration');
}
if (!enml.includes('<!DOCTYPE en-note')) {
errors.push('Missing DOCTYPE');
}
if (!enml.includes('<en-note>') || !enml.includes('</en-note>')) {
errors.push('Missing en-note root element');
}
return { valid: errors.length === 0, errors };
}
/**
* Sanitize content for storage
*/
sanitize(enml) {
const $ = cheerio.load(enml, { xmlMode: true });
// Remove potentially sensitive data
$('en-crypt').each((i, el) => {
$(el).replaceWith('[Encrypted content]');
});
return $.html();
}
}
module.exports = ENMLProcessor;
Step 3: Resource (Attachment) Handling
// services/resource-handler.js
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
class ResourceHandler {
constructor(options) {
this.storageRoot = options.storageRoot || './storage/resources';
this.maxSizeBytes = options.maxSizeBytes || 100 * 1024 * 1024; // 100MB
}
/**
* Store resource from Evernote
*/
async storeResource(resource, userId) {
const hash = Buffer.from(resource.data.bodyHash).toString('hex');
const storagePath = this.getStoragePath(userId, hash, resource.mime);
// Ensure directory exists
await fs.mkdir(path.dirname(storagePath), { recursive: true });
// Write file
await fs.writeFile(storagePath, resource.data.body);
return {
storagePath,
hash,
size: resource.data.size,
mime: resource.mime
};
}
/**
* Get storage path for resource
*/
getStoragePath(userId, hash, mimeType) {
const ext = this.getExtension(mimeType);
// Shard by first 2 chars of hash for performance
const shard = hash.substring(0, 2);
return path.join(this.storageRoot, userId.toString(), shard, `${hash}${ext}`);
}
/**
* Get file extension from MIME type
*/
getExtension(mimeType) {
const map = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/gif': '.gif',
'application/pdf': '.pdf',
'audio/wav': '.wav',
'audio/mpeg': '.mp3'
};
return map[mimeType] || '';
}
/**
* Retrieve stored resource
*/
async getResource(storagePath) {
try {
const data = await fs.readFile(storagePath);
return { data, exists: true };
} catch (error) {
if (error.code === 'ENOENT') {
return { data: null, exists: false };
}
throw error;
}
}
/**
* Delete resource
*/
async deleteResource(storagePath) {
try {
await fs.unlink(storagePath);
return { deleted: true };
} catch (error) {
if (error.code === 'ENOENT') {
return { deleted: false, reason: 'not_found' };
}
throw error;
}
}
/**
* Calculate storage usage for user
*/
async getUserStorageUsage(userId) {
const userDir = path.join(this.storageRoot, userId.toString());
let totalSize = 0;
let fileCount = 0;
async function walkDir(dir) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await walkDir(fullPath);
} else {
const stat = await fs.stat(fullPath);
totalSize += stat.size;
fileCount++;
}
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
await walkDir(userDir);
return {
totalBytes: totalSize,
totalMB: (totalSize / (1024 * 1024)).toFixed(2),
fileCount
};
}
}
module.exports = ResourceHandler;
Step 4: Sync Data Manager
// services/sync-manager.js
class SyncManager {
constructor(db, noteStore) {
this.db = db;
this.noteStore = noteStore;
}
/**
* Process sync chunk and update local data
*/
async processSyncChunk(userId, chunk) {
const results = {
notes: { created: 0, updated: 0, deleted: 0 },
notebooks: { created: 0, updated: 0, deleted: 0 },
tags: { created: 0, updated: 0, deleted: 0 }
};
// Process notebooks
if (chunk.notebooks) {
for (const notebook of chunk.notebooks) {
await this.upsertNotebook(userId, notebook);
results.notebooks.updated++;
}
}
// Process expunged notebooks
if (chunk.expungedNotebooks) {
for (const guid of chunk.expungedNotebooks) {
await this.deleteNotebook(guid);
results.notebooks.deleted++;
}
}
// Process tags
if (chunk.tags) {
for (const tag of chunk.tags) {
await this.upsertTag(userId, tag);
results.tags.updated++;
}
}
// Process expunged tags
if (chunk.expungedTags) {
for (const guid of chunk.expungedTags) {
await this.deleteTag(guid);
results.tags.deleted++;
}
}
// Process notes
if (chunk.notes) {
for (const note of chunk.notes) {
const isNew = await this.upsertNote(userId, note);
if (isNew) results.notes.created++;
else results.notes.updated++;
}
}
// Process expunged notes
if (chunk.expungedNotes) {
for (const guid of chunk.expungedNotes) {
await this.deleteNote(guid);
results.notes.deleted++;
}
}
return results;
}
/**
* Upsert notebook
*/
async upsertNotebook(userId, notebook) {
return this.db.query(`
INSERT INTO notebooks (guid, user_id, name, stack, default_notebook, created_at, updated_at, synced_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
ON CONFLICT (guid) DO UPDATE SET
name = EXCLUDED.name,
stack = EXCLUDED.stack,
default_notebook = EXCLUDED.default_notebook,
updated_at = EXCLUDED.updated_at,
synced_at = NOW()
`, [notebook.guid, userId, notebook.name, notebook.stack, notebook.defaultNotebook,
notebook.serviceCreated, notebook.serviceUpdated]);
}
/**
* Upsert note (without content)
*/
async upsertNote(userId, note) {
const existing = await this.db.query(
'SELECT guid FROM notes WHERE guid = $1',
[note.guid]
);
await this.db.query(`
INSERT INTO notes (guid, user_id, notebook_guid, title, content_hash, content_length,
created_at, updated_at, active, update_sequence_num, synced_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
ON CONFLICT (guid) DO UPDATE SET
notebook_guid = EXCLUDED.notebook_guid,
title = EXCLUDED.title,
content_hash = EXCLUDED.content_hash,
content_length = EXCLUDED.content_length,
updated_at = EXCLUDED.updated_at,
active = EXCLUDED.active,
update_sequence_num = EXCLUDED.update_sequence_num,
synced_at = NOW()
`, [note.guid, userId, note.notebookGuid, note.title,
note.contentHash, note.contentLength, note.created,
note.updated, note.active !== false, note.updateSequenceNum]);
// Update tags
if (note.tagGuids) {
await this.updateNoteTags(note.guid, note.tagGuids);
}
return existing.rows.length === 0; // Return true if new
}
/**
* Update note tags
*/
async updateNoteTags(noteGuid, tagGuids) {
await this.db.query('DELETE FROM note_tags WHERE note_guid = $1', [noteGuid]);
if (tagGuids.length > 0) {
const values = tagGuids.map((tagGuid, i) =>
`($1, $${i + 2})`
).join(', ');
await this.db.query(
`INSERT INTO note_tags (note_guid, tag_guid) VALUES ${values}`,
[noteGuid, ...tagGuids]
);
}
}
/**
* Delete note (soft delete)
*/
async deleteNote(guid) {
return this.db.query(
'UPDATE notes SET active = FALSE, deleted_at = $1, synced_at = NOW() WHERE guid = $2',
[Date.now(), guid]
);
}
/**
* Update sync state
*/
async updateSyncState(userId, updateCount) {
return this.db.query(`
INSERT INTO sync_state (user_id, last_update_count, last_sync_time, full_sync_required)
VALUES ($1, $2, $3, FALSE)
ON CONFLICT (user_id) DO UPDATE SET
last_update_count = EXCLUDED.last_update_count,
last_sync_time = EXCLUDED.last_sync_time,
full_sync_required = FALSE
`, [userId, updateCount, Date.now()]);
}
}
module.exports = SyncManager;
Step 5: Data Export
// services/data-export.js
const archiver = require('archiver');
class DataExporter {
constructor(db, resourceHandler) {
this.db = db;
this.resourceHandler = resourceHandler;
}
/**
* Export all user data
*/
async exportUserData(userId, outputStream) {
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(outputStream);
// Export notes
const notes = await this.db.query(
'SELECT * FROM notes WHERE user_id = $1 AND active = TRUE',
[userId]
);
// Add notes as JSON
archive.append(JSON.stringify(notes.rows, null, 2), {
name: 'notes.json'
});
// Export notebooks
const notebooks = await this.db.query(
'SELECT * FROM notebooks WHERE user_id = $1',
[userId]
);
archive.append(JSON.stringify(notebooks.rows, null, 2), {
name: 'notebooks.json'
});
// Export tags
const tags = await this.db.query(
'SELECT * FROM tags WHERE user_id = $1',
[userId]
);
archive.append(JSON.stringify(tags.rows, null, 2), {
name: 'tags.json'
});
// Export resources
const resources = await this.db.query(`
SELECT r.* FROM resources r
JOIN notes n ON r.note_guid = n.guid
WHERE n.user_id = $1
`, [userId]);
for (const resource of resources.rows) {
if (resource.storage_path) {
const { data, exists } = await this.resourceHandler.getResource(
resource.storage_path
);
if (exists) {
archive.append(data, {
name: `resources/${resource.guid}${path.extname(resource.storage_path)}`
});
}
}
}
await archive.finalize();
}
/**
* Delete all user data (GDPR right to erasure)
*/
async deleteUserData(userId) {
// Delete in order of dependencies
await this.db.query('DELETE FROM note_tags WHERE note_guid IN (SELECT guid FROM notes WHERE user_id = $1)', [userId]);
await this.db.query('DELETE FROM resources WHERE note_guid IN (SELECT guid FROM notes WHERE user_id = $1)', [userId]);
await this.db.query('DELETE FROM notes WHERE user_id = $1', [userId]);
await this.db.query('DELETE FROM tags WHERE user_id = $1', [userId]);
await this.db.query('DELETE FROM notebooks WHERE user_id = $1', [userId]);
await this.db.query('DELETE FROM sync_state WHERE user_id = $1', [userId]);
await this.db.query('DELETE FROM evernote_users WHERE id = $1', [userId]);
// Delete stored resources
const userStoragePath = path.join(this.resourceHandler.storageRoot, userId.toString());
await fs.rm(userStoragePath, { recursive: true, force: true });
return { deleted: true };
}
}
module.exports = DataExporter;
Output
- Database schema for Evernote data
- ENML content processor
- Resource/attachment handler
- Sync data manager
- Data export utility
Data Handling Checklist
- [ ] Database schema created with proper indexes
- [ ] ENML content validation implemented
- [ ] Resource storage configured
- [ ] Sync state management implemented
- [ ] Data export capability
- [ ] Data deletion capability (GDPR)
- [ ] Backup procedures documented
Resources
Next Steps
For enterprise features, see evernote-enterprise-rbac.
Du fichier au premier essai
Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.
Consulter la source et ses fichiers annexesCe téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.
1. Télécharger le fichier
Télécharger SKILL.mdDepuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.
Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.
mkdir -p '.skill-downloads/evernote-data-handling-mmh8qqvd' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/evernote-data-handling-mmh8qqvd/download' --output '.skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md.part' && mv '.skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md.part' '.skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md'2. Vérifier les prérequis
Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.
Outils déclarés par l’auteur (à vérifier dans votre agent) : Read, Write, Edit, Grep
Extrait original : Prerequisites
- Understanding of Evernote data model
- Database for local storage
- File storage for attachments
Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.
3. Charger la skill
Après vérification, copiez le dossier complet dans le projet avec l’une des commandes ci-dessous, puis ouvrez une nouvelle session de l’outil. La commande s’arrête si un dossier du même nom existe déjà : comparez les fichiers avant de le remplacer.
Claude Code
test -f '.skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md' && test -s '.skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/evernote-data-handling' && cp -R '.skill-downloads/evernote-data-handling-mmh8qqvd' '.claude/skills/evernote-data-handling'OpenSkills
OpenSkills doit déjà être installé. La procédure locale install/read est contrôlée avec OpenSkills 1.5.0, qui exige Node.js ≥ 20.6.0. Aucun slug du catalogue n’est résolu automatiquement.
Installation officielle d’OpenSkillsopenskills install './.skill-downloads/evernote-data-handling-mmh8qqvd'openskills read 'evernote-data-handling-mmh8qqvd'La lecture doit afficher le contenu du fichier installé et son dossier de base. Elle ne lance ni la skill ni ses dépendances.
Pour un agent qui lit AGENTS.md, synchronisez ensuite l’index. Cette commande modifie AGENTS.md : relisez le diff avant de l’utiliser.
openskills syncMode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.
4. Faire un premier essai
Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :
Lis .skill-downloads/evernote-data-handling-mmh8qqvd/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.
Créer des cartes interactives avec kepler.gl
Data & IA
Création de cartes interactives et export HTML autonome avec le package Python keplergl, à partir de données géospatiales (DataFrame, GeoJSON, CSV).
Optimisation de paramètres par agent
Data & IA
Guide d'optimisation de systèmes configurables avec objectif mesurable, via des essais bornés et une recommandation fondée sur des preuves.
Relance des visiteurs ICP
Data & IA
Identifiez les utilisateurs inscrits correspondant au profil client idéal mais qui n'ont pas activé, et rédigez des messages de relance personnalisés pour les réengager.