tessl i github:jeremylongshore/claude-code-plugins-plus-skills --skill evernote-debug-bundleDebug Evernote API issues with diagnostic tools and techniques. Use when troubleshooting API calls, inspecting requests/responses, or diagnosing integration problems. Trigger with phrases like "debug evernote", "evernote diagnostic", "troubleshoot evernote", "evernote logs", "inspect evernote".
Comprehensive debugging toolkit for Evernote API integrations, including request logging, ENML validation, token inspection, and diagnostic utilities.
// utils/debug-logger.js
const fs = require('fs');
const path = require('path');
class EvernoteDebugLogger {
constructor(options = {}) {
this.enabled = options.enabled ?? process.env.EVERNOTE_DEBUG === 'true';
this.logFile = options.logFile || 'evernote-debug.log';
this.logDir = options.logDir || './logs';
this.maxLogSize = options.maxLogSize || 10 * 1024 * 1024; // 10MB
if (this.enabled) {
fs.mkdirSync(this.logDir, { recursive: true });
}
}
log(operation, data) {
if (!this.enabled) return;
const entry = {
timestamp: new Date().toISOString(),
operation,
...data
};
// Console output
console.log(`[EVERNOTE DEBUG] ${operation}`, JSON.stringify(data, null, 2));
// File output
this.writeToFile(entry);
}
logRequest(operation, params) {
this.log(operation, {
type: 'REQUEST',
params: this.sanitizeParams(params)
});
}
logResponse(operation, response) {
this.log(operation, {
type: 'RESPONSE',
response: this.summarizeResponse(response)
});
}
logError(operation, error) {
this.log(operation, {
type: 'ERROR',
errorCode: error.errorCode,
parameter: error.parameter,
identifier: error.identifier,
key: error.key,
rateLimitDuration: error.rateLimitDuration,
message: error.message,
stack: error.stack
});
}
sanitizeParams(params) {
// Remove sensitive data from logs
const sanitized = { ...params };
if (sanitized.token) sanitized.token = '[REDACTED]';
if (sanitized.password) sanitized.password = '[REDACTED]';
if (sanitized.content && sanitized.content.length > 500) {
sanitized.content = sanitized.content.substring(0, 500) + '...[truncated]';
}
return sanitized;
}
summarizeResponse(response) {
if (!response) return null;
// Summarize common response types
if (response.guid) {
return {
guid: response.guid,
title: response.title,
created: response.created,
updated: response.updated,
contentLength: response.content?.length
};
}
if (Array.isArray(response)) {
return {
count: response.length,
items: response.slice(0, 3).map(item => ({
guid: item.guid,
title: item.title || item.name
}))
};
}
return response;
}
writeToFile(entry) {
const filePath = path.join(this.logDir, this.logFile);
const line = JSON.stringify(entry) + '\n';
try {
fs.appendFileSync(filePath, line);
this.rotateIfNeeded(filePath);
} catch (error) {
console.error('Failed to write debug log:', error.message);
}
}
rotateIfNeeded(filePath) {
try {
const stats = fs.statSync(filePath);
if (stats.size > this.maxLogSize) {
const rotated = `${filePath}.${Date.now()}`;
fs.renameSync(filePath, rotated);
}
} catch (error) {
// Ignore rotation errors
}
}
}
module.exports = EvernoteDebugLogger;// utils/instrumented-client.js
const Evernote = require('evernote');
const EvernoteDebugLogger = require('./debug-logger');
class InstrumentedEvernoteClient {
constructor(client, logger = null) {
this.client = client;
this.logger = logger || new EvernoteDebugLogger();
this._noteStore = null;
this._userStore = null;
}
get noteStore() {
if (!this._noteStore) {
this._noteStore = this.wrapStore(
this.client.getNoteStore(),
'NoteStore'
);
}
return this._noteStore;
}
get userStore() {
if (!this._userStore) {
this._userStore = this.wrapStore(
this.client.getUserStore(),
'UserStore'
);
}
return this._userStore;
}
wrapStore(store, storeName) {
const logger = this.logger;
return new Proxy(store, {
get(target, prop) {
const original = target[prop];
if (typeof original !== 'function') {
return original;
}
return async (...args) => {
const operation = `${storeName}.${prop}`;
const startTime = Date.now();
logger.logRequest(operation, {
args: args.map((arg, i) => ({
index: i,
type: typeof arg,
value: typeof arg === 'object' ? arg : String(arg).substring(0, 100)
}))
});
try {
const result = await original.apply(target, args);
const duration = Date.now() - startTime;
logger.logResponse(operation, {
duration,
result
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
logger.logError(operation, {
duration,
error
});
throw error;
}
};
}
});
}
}
module.exports = InstrumentedEvernoteClient;// utils/enml-validator.js
class ENMLValidator {
static validate(content) {
const errors = [];
const warnings = [];
// Required structure
if (!content.includes('<?xml version="1.0"')) {
errors.push({
type: 'MISSING_XML_DECLARATION',
message: 'Missing XML declaration',
fix: 'Add: <?xml version="1.0" encoding="UTF-8"?>'
});
}
if (!content.includes('<!DOCTYPE en-note')) {
errors.push({
type: 'MISSING_DOCTYPE',
message: 'Missing DOCTYPE declaration',
fix: 'Add: <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">'
});
}
if (!/<en-note[^>]*>/.test(content)) {
errors.push({
type: 'MISSING_ROOT',
message: 'Missing <en-note> root element',
fix: 'Wrap content in <en-note>...</en-note>'
});
}
if (!content.includes('</en-note>')) {
errors.push({
type: 'UNCLOSED_ROOT',
message: 'Missing closing </en-note> tag',
fix: 'Add closing </en-note> tag'
});
}
// Forbidden elements
const forbidden = [
{ pattern: /<script/i, element: 'script' },
{ pattern: /<form/i, element: 'form' },
{ pattern: /<input/i, element: 'input' },
{ pattern: /<button/i, element: 'button' },
{ pattern: /<iframe/i, element: 'iframe' },
{ pattern: /<object/i, element: 'object' },
{ pattern: /<embed/i, element: 'embed' },
{ pattern: /<applet/i, element: 'applet' },
{ pattern: /<meta/i, element: 'meta' },
{ pattern: /<link/i, element: 'link' },
{ pattern: /<style/i, element: 'style' }
];
forbidden.forEach(({ pattern, element }) => {
if (pattern.test(content)) {
errors.push({
type: 'FORBIDDEN_ELEMENT',
message: `Forbidden element: <${element}>`,
fix: `Remove all <${element}> elements`
});
}
});
// Forbidden attributes
const forbiddenAttrs = [
{ pattern: /\sclass\s*=/i, attr: 'class' },
{ pattern: /\sid\s*=/i, attr: 'id' },
{ pattern: /\sonclick\s*=/i, attr: 'onclick' },
{ pattern: /\sonload\s*=/i, attr: 'onload' },
{ pattern: /\sonerror\s*=/i, attr: 'onerror' },
{ pattern: /\sonmouseover\s*=/i, attr: 'onmouseover' }
];
forbiddenAttrs.forEach(({ pattern, attr }) => {
if (pattern.test(content)) {
errors.push({
type: 'FORBIDDEN_ATTRIBUTE',
message: `Forbidden attribute: ${attr}`,
fix: `Remove all ${attr}="..." attributes`
});
}
});
// Unclosed tags (basic check)
const voidElements = ['br', 'hr', 'img', 'en-media', 'en-todo', 'en-crypt'];
const unclosedPattern = /<(br|hr|img|en-media|en-todo)(?![^>]*\/>)[^>]*>/gi;
const unclosed = content.match(unclosedPattern);
if (unclosed) {
unclosed.forEach(tag => {
warnings.push({
type: 'UNCLOSED_VOID_ELEMENT',
message: `Void element should be self-closing: ${tag}`,
fix: 'Use self-closing syntax: <br/>, <hr/>, etc.'
});
});
}
// Check en-media hash format
const mediaPattern = /<en-media[^>]*hash="([^"]*)"[^>]*>/gi;
let match;
while ((match = mediaPattern.exec(content)) !== null) {
const hash = match[1];
if (!/^[a-f0-9]{32}$/i.test(hash)) {
errors.push({
type: 'INVALID_MEDIA_HASH',
message: `Invalid MD5 hash in en-media: ${hash}`,
fix: 'Use valid 32-character MD5 hex hash'
});
}
}
// Size warning
const sizeBytes = Buffer.byteLength(content, 'utf8');
const sizeMB = sizeBytes / (1024 * 1024);
if (sizeMB > 25) {
warnings.push({
type: 'LARGE_CONTENT',
message: `Note content is large: ${sizeMB.toFixed(2)} MB`,
fix: 'Consider splitting into multiple notes'
});
}
return {
valid: errors.length === 0,
errors,
warnings,
stats: {
sizeBytes,
sizeMB: sizeMB.toFixed(2),
hasTodos: /<en-todo/.test(content),
hasMedia: /<en-media/.test(content),
hasEncryption: /<en-crypt/.test(content)
}
};
}
static fix(content) {
let fixed = content;
// Add missing XML declaration
if (!fixed.includes('<?xml version="1.0"')) {
fixed = '<?xml version="1.0" encoding="UTF-8"?>\n' + fixed;
}
// Add missing DOCTYPE
if (!fixed.includes('<!DOCTYPE en-note')) {
fixed = fixed.replace(
'<?xml version="1.0" encoding="UTF-8"?>',
'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">'
);
}
// Remove forbidden elements
fixed = fixed.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
fixed = fixed.replace(/<form[^>]*>[\s\S]*?<\/form>/gi, '');
fixed = fixed.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
fixed = fixed.replace(/<iframe[^>]*>[\s\S]*?<\/iframe>/gi, '');
// Remove forbidden attributes
fixed = fixed.replace(/\s(class|id|onclick|onload|onerror)="[^"]*"/gi, '');
// Fix unclosed void elements
fixed = fixed.replace(/<(br|hr)(?![^>]*\/>)([^>]*)>/gi, '<$1$2/>');
return fixed;
}
}
module.exports = ENMLValidator;// utils/token-inspector.js
class TokenInspector {
static async inspect(client) {
const userStore = client.getUserStore();
const noteStore = client.getNoteStore();
const results = {
timestamp: new Date().toISOString(),
user: null,
accounting: null,
businessUser: null,
notebooks: null,
tags: null,
errors: []
};
// User info
try {
const user = await userStore.getUser();
results.user = {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
timezone: user.timezone,
privilege: this.privilegeToString(user.privilege),
created: new Date(user.created),
updated: new Date(user.updated),
active: user.active
};
if (user.accounting) {
results.accounting = {
uploadLimit: this.formatBytes(user.accounting.uploadLimit),
uploaded: this.formatBytes(user.accounting.uploaded),
remaining: this.formatBytes(
user.accounting.uploadLimit - user.accounting.uploaded
),
uploadLimitEnd: new Date(user.accounting.uploadLimitEnd),
premiumServiceStart: user.accounting.premiumServiceStart ?
new Date(user.accounting.premiumServiceStart) : null
};
}
} catch (error) {
results.errors.push({
operation: 'getUser',
error: error.message
});
}
// Notebook count
try {
const notebooks = await noteStore.listNotebooks();
results.notebooks = {
count: notebooks.length,
limit: 250,
remaining: 250 - notebooks.length
};
} catch (error) {
results.errors.push({
operation: 'listNotebooks',
error: error.message
});
}
// Tag count
try {
const tags = await noteStore.listTags();
results.tags = {
count: tags.length,
limit: 100000,
remaining: 100000 - tags.length
};
} catch (error) {
results.errors.push({
operation: 'listTags',
error: error.message
});
}
return results;
}
static privilegeToString(privilege) {
const map = {
1: 'NORMAL',
2: 'PREMIUM',
3: 'VIP',
5: 'MANAGER',
7: 'SUPPORT',
8: 'ADMIN'
};
return map[privilege] || 'UNKNOWN';
}
static formatBytes(bytes) {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return `${bytes.toFixed(2)} ${units[i]}`;
}
}
module.exports = TokenInspector;// scripts/diagnose.js
require('dotenv').config();
const Evernote = require('evernote');
const TokenInspector = require('../utils/token-inspector');
const ENMLValidator = require('../utils/enml-validator');
async function runDiagnostics() {
console.log('=== Evernote Diagnostic Report ===\n');
// 1. Check environment
console.log('1. Environment Check');
console.log('-------------------');
console.log('EVERNOTE_ACCESS_TOKEN:', process.env.EVERNOTE_ACCESS_TOKEN ? 'Set' : 'NOT SET');
console.log('EVERNOTE_SANDBOX:', process.env.EVERNOTE_SANDBOX || 'Not set (defaulting to production)');
console.log();
if (!process.env.EVERNOTE_ACCESS_TOKEN) {
console.error('ERROR: No access token configured');
process.exit(1);
}
// 2. Test connection
console.log('2. Connection Test');
console.log('-----------------');
const client = new Evernote.Client({
token: process.env.EVERNOTE_ACCESS_TOKEN,
sandbox: process.env.EVERNOTE_SANDBOX === 'true'
});
try {
const inspection = await TokenInspector.inspect(client);
if (inspection.user) {
console.log('User:', inspection.user.username);
console.log('Account type:', inspection.user.privilege);
console.log('Active:', inspection.user.active);
}
if (inspection.accounting) {
console.log('Upload quota:', inspection.accounting.remaining, 'remaining');
console.log('Quota resets:', inspection.accounting.uploadLimitEnd);
}
if (inspection.notebooks) {
console.log('Notebooks:', inspection.notebooks.count, '/', inspection.notebooks.limit);
}
if (inspection.tags) {
console.log('Tags:', inspection.tags.count);
}
if (inspection.errors.length > 0) {
console.log('\nErrors encountered:');
inspection.errors.forEach(e => console.log(` - ${e.operation}: ${e.error}`));
}
} catch (error) {
console.error('Connection failed:', error.message);
if (error.errorCode === 4) {
console.error('Token is invalid. Please re-authenticate.');
}
if (error.errorCode === 5) {
console.error('Token has expired. Please re-authenticate.');
}
process.exit(1);
}
// 3. Test note creation
console.log('\n3. Note Creation Test');
console.log('--------------------');
const testContent = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note><p>Diagnostic test note</p></en-note>`;
const validation = ENMLValidator.validate(testContent);
console.log('ENML Validation:', validation.valid ? 'PASSED' : 'FAILED');
if (!validation.valid) {
validation.errors.forEach(e => console.log(` Error: ${e.message}`));
}
console.log('\n=== Diagnostic Complete ===');
}
runDiagnostics().catch(console.error);{
"scripts": {
"diagnose": "node scripts/diagnose.js",
"diagnose:verbose": "EVERNOTE_DEBUG=true node scripts/diagnose.js",
"validate:enml": "node -e \"require('./utils/enml-validator').validate(require('fs').readFileSync(process.argv[1], 'utf8'))\" --"
}
}# Run full diagnostics
npm run diagnose
# Run with verbose logging
npm run diagnose:verbose
# Validate ENML file
npm run validate:enml note-content.xml| Issue | Diagnostic | Solution |
|---|---|---|
| Auth failures | Check token in logs | Re-authenticate |
| ENML errors | Use ENMLValidator | Auto-fix with validator |
| Rate limits | Check request frequency in logs | Add delays |
| Missing data | Inspect response logs | Check API parameters |
For rate limit handling, see evernote-rate-limits.
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.