Ctrl + k

or run

tessl search
Log in

evernote-local-dev-loop

tessl install github:jeremylongshore/claude-code-plugins-plus-skills --skill evernote-local-dev-loop
github.com/jeremylongshore/claude-code-plugins-plus-skills

Set up efficient local development workflow for Evernote integrations. Use when configuring dev environment, setting up sandbox testing, or optimizing development iteration speed. Trigger with phrases like "evernote dev setup", "evernote local development", "evernote sandbox", "test evernote locally".

Review Score

84%

Validation Score

12/16

Implementation Score

77%

Activation Score

90%

Evernote Local Dev Loop

Overview

Configure an efficient local development environment for Evernote API integration with sandbox testing, hot reload, and debugging tools.

Prerequisites

  • Completed evernote-install-auth setup
  • Node.js 18+ or Python 3.10+
  • Evernote sandbox account at https://sandbox.evernote.com

Instructions

Step 1: Project Structure

mkdir evernote-project && cd evernote-project
npm init -y
npm install evernote dotenv nodemon express express-session

# Create project structure
mkdir -p src/{routes,services,utils}
touch src/index.js src/evernote-client.js .env .env.example
evernote-project/
├── src/
│   ├── index.js              # Express server entry
│   ├── evernote-client.js    # Evernote client wrapper
│   ├── routes/
│   │   └── oauth.js          # OAuth routes
│   ├── services/
│   │   └── notes.js          # Note operations
│   └── utils/
│       └── enml.js           # ENML helpers
├── .env                      # Local environment
├── .env.example              # Template
└── package.json

Step 2: Environment Configuration

# .env
EVERNOTE_CONSUMER_KEY=your-consumer-key
EVERNOTE_CONSUMER_SECRET=your-consumer-secret
EVERNOTE_SANDBOX=true
EVERNOTE_DEV_TOKEN=your-sandbox-dev-token
SESSION_SECRET=dev-session-secret-change-in-prod
PORT=3000
NODE_ENV=development
# .env.example (commit this, not .env)
EVERNOTE_CONSUMER_KEY=
EVERNOTE_CONSUMER_SECRET=
EVERNOTE_SANDBOX=true
EVERNOTE_DEV_TOKEN=
SESSION_SECRET=
PORT=3000
NODE_ENV=development

Step 3: Evernote Client Wrapper

// src/evernote-client.js
const Evernote = require('evernote');

class EvernoteService {
  constructor(accessToken) {
    this.client = new Evernote.Client({
      token: accessToken,
      sandbox: process.env.EVERNOTE_SANDBOX === 'true'
    });
    this._noteStore = null;
    this._userStore = null;
  }

  // Lazy-load NoteStore
  get noteStore() {
    if (!this._noteStore) {
      this._noteStore = this.client.getNoteStore();
    }
    return this._noteStore;
  }

  // Lazy-load UserStore
  get userStore() {
    if (!this._userStore) {
      this._userStore = this.client.getUserStore();
    }
    return this._userStore;
  }

  // Create client for OAuth flow
  static createOAuthClient() {
    return new Evernote.Client({
      consumerKey: process.env.EVERNOTE_CONSUMER_KEY,
      consumerSecret: process.env.EVERNOTE_CONSUMER_SECRET,
      sandbox: process.env.EVERNOTE_SANDBOX === 'true'
    });
  }

  // Quick dev client using developer token
  static createDevClient() {
    if (!process.env.EVERNOTE_DEV_TOKEN) {
      throw new Error('EVERNOTE_DEV_TOKEN not set - get one from sandbox.evernote.com');
    }
    return new EvernoteService(process.env.EVERNOTE_DEV_TOKEN);
  }
}

module.exports = EvernoteService;

Step 4: ENML Utility Helpers

// src/utils/enml.js

const ENML_HEADER = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">`;

/**
 * Wrap content in valid ENML structure
 */
function wrapInENML(content) {
  return `${ENML_HEADER}
<en-note>
${content}
</en-note>`;
}

/**
 * Convert plain text to ENML
 */
function textToENML(text) {
  const escaped = text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/\n/g, '<br/>');

  return wrapInENML(`<div>${escaped}</div>`);
}

/**
 * Convert Markdown to basic ENML (simplified)
 */
function markdownToENML(markdown) {
  let html = markdown
    // Headers
    .replace(/^### (.+)$/gm, '<h3>$1</h3>')
    .replace(/^## (.+)$/gm, '<h2>$1</h2>')
    .replace(/^# (.+)$/gm, '<h1>$1</h1>')
    // Bold and italic
    .replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
    .replace(/\*(.+?)\*/g, '<i>$1</i>')
    // Links
    .replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')
    // Lists (simplified)
    .replace(/^- (.+)$/gm, '<div>• $1</div>')
    // Line breaks
    .replace(/\n\n/g, '</div><div>')
    .replace(/\n/g, '<br/>');

  return wrapInENML(`<div>${html}</div>`);
}

/**
 * Create todo checkbox
 */
function createTodo(text, checked = false) {
  return `<en-todo checked="${checked}"/> ${text}<br/>`;
}

/**
 * Validate ENML has required structure
 */
function validateENML(content) {
  const errors = [];

  if (!content.includes('<?xml version="1.0"')) {
    errors.push('Missing XML declaration');
  }
  if (!content.includes('<!DOCTYPE en-note')) {
    errors.push('Missing DOCTYPE declaration');
  }
  if (!content.includes('<en-note>') || !content.includes('</en-note>')) {
    errors.push('Missing <en-note> root element');
  }

  // Check for forbidden elements
  const forbidden = ['<script', '<form', '<input', '<iframe', '<button'];
  forbidden.forEach(tag => {
    if (content.toLowerCase().includes(tag)) {
      errors.push(`Forbidden element: ${tag}`);
    }
  });

  return {
    valid: errors.length === 0,
    errors
  };
}

module.exports = {
  ENML_HEADER,
  wrapInENML,
  textToENML,
  markdownToENML,
  createTodo,
  validateENML
};

Step 5: Express Server with OAuth

// src/index.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const EvernoteService = require('./evernote-client');

const app = express();

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: process.env.NODE_ENV === 'production' }
}));

app.use(express.json());

// OAuth initiation
app.get('/auth/evernote', (req, res) => {
  const client = EvernoteService.createOAuthClient();
  const callbackUrl = `http://localhost:${process.env.PORT}/auth/evernote/callback`;

  client.getRequestToken(callbackUrl, (error, oauthToken, oauthTokenSecret) => {
    if (error) {
      console.error('OAuth error:', error);
      return res.status(500).json({ error: 'Failed to get request token' });
    }

    req.session.oauthToken = oauthToken;
    req.session.oauthTokenSecret = oauthTokenSecret;
    res.redirect(client.getAuthorizeUrl(oauthToken));
  });
});

// OAuth callback
app.get('/auth/evernote/callback', (req, res) => {
  const client = EvernoteService.createOAuthClient();

  client.getAccessToken(
    req.session.oauthToken,
    req.session.oauthTokenSecret,
    req.query.oauth_verifier,
    (error, accessToken, accessTokenSecret, results) => {
      if (error) {
        console.error('Access token error:', error);
        return res.status(500).json({ error: 'Failed to get access token' });
      }

      req.session.accessToken = accessToken;
      req.session.evernoteExpires = results.edam_expires;

      res.json({
        success: true,
        message: 'Authenticated successfully',
        expiresAt: new Date(parseInt(results.edam_expires))
      });
    }
  );
});

// Dev endpoint - uses dev token
app.get('/dev/test', async (req, res) => {
  try {
    const evernote = EvernoteService.createDevClient();
    const user = await evernote.userStore.getUser();
    const notebooks = await evernote.noteStore.listNotebooks();

    res.json({
      user: user.username,
      notebookCount: notebooks.length,
      notebooks: notebooks.map(n => ({ name: n.name, guid: n.guid }))
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
  console.log('Sandbox mode:', process.env.EVERNOTE_SANDBOX === 'true');
  console.log('Dev endpoints:');
  console.log(`  GET /dev/test - Test with dev token`);
  console.log(`  GET /auth/evernote - Start OAuth flow`);
});

Step 6: Package.json Scripts

{
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js",
    "test": "node --test",
    "test:note": "node -e \"require('./src/test-note.js')\"",
    "lint": "eslint src/"
  }
}

Step 7: Quick Test Script

// src/test-note.js
require('dotenv').config();
const EvernoteService = require('./evernote-client');
const { textToENML } = require('./utils/enml');

async function testCreateNote() {
  const evernote = EvernoteService.createDevClient();

  // Create a test note
  const note = new (require('evernote')).Types.Note();
  note.title = `Dev Test - ${new Date().toISOString()}`;
  note.content = textToENML('This is a test note from the dev loop');

  const created = await evernote.noteStore.createNote(note);
  console.log('Created note:', created.guid);

  // Clean up - delete test note
  await evernote.noteStore.deleteNote(created.guid);
  console.log('Deleted test note');

  console.log('Dev loop test passed!');
}

testCreateNote().catch(console.error);

Development Workflow

# Terminal 1: Run dev server with hot reload
npm run dev

# Terminal 2: Test API endpoints
curl http://localhost:3000/dev/test

# Quick note creation test
npm run test:note

Sandbox vs Production

AspectSandboxProduction
URLsandbox.evernote.comwww.evernote.com
Dev TokenAvailableNot available
Rate LimitsSameSame
DataSeparate accountReal user data
OAuthRequired for appsRequired

Output

  • Working local development server
  • Hot-reload enabled with nodemon
  • OAuth flow implemented
  • Dev token shortcut for quick testing
  • ENML utility functions

Error Handling

ErrorCauseSolution
EVERNOTE_DEV_TOKEN not setMissing dev tokenGet from sandbox.evernote.com/api/DeveloperToken.action
Invalid consumer keyWrong sandbox vs prodVerify EVERNOTE_SANDBOX matches your key type
Session undefinedMissing express-sessionEnsure session middleware is configured

Resources

Next Steps

Proceed to evernote-sdk-patterns for advanced SDK usage patterns.