CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

evernote-debug-bundle

tessl i github:jeremylongshore/claude-code-plugins-plus-skills --skill evernote-debug-bundle

Debug 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".

77%

Overall

SKILL.md
Review
Evals

Evernote Debug Bundle

Overview

Comprehensive debugging toolkit for Evernote API integrations, including request logging, ENML validation, token inspection, and diagnostic utilities.

Prerequisites

  • Evernote SDK installed
  • Node.js environment
  • Understanding of common Evernote errors

Instructions

Step 1: Debug Logger

// 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;

Step 2: Instrumented Client Wrapper

// 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;

Step 3: ENML Validator

// 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;

Step 4: Token Inspector

// 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;

Step 5: Diagnostic CLI

// 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);

Step 6: Package.json Scripts

{
  "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'))\" --"
  }
}

Output

  • Debug logger with request/response tracking
  • Instrumented client wrapper for automatic logging
  • ENML validator with auto-fix capability
  • Token and account inspector
  • Diagnostic CLI script

Usage

# Run full diagnostics
npm run diagnose

# Run with verbose logging
npm run diagnose:verbose

# Validate ENML file
npm run validate:enml note-content.xml

Error Handling

IssueDiagnosticSolution
Auth failuresCheck token in logsRe-authenticate
ENML errorsUse ENMLValidatorAuto-fix with validator
Rate limitsCheck request frequency in logsAdd delays
Missing dataInspect response logsCheck API parameters

Resources

  • Error Handling
  • ENML DTD
  • API Reference

Next Steps

For rate limit handling, see evernote-rate-limits.

Repository
github.com/jeremylongshore/claude-code-plugins-plus-skills
Last updated
Created

Is this your skill?

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.