feat(evernote): add Evernote integration with 11 tools (#3456)

* feat(evernote): add Evernote integration with 11 tools

* fix(evernote): fix signed integer mismatch in Thrift version check

* fix(evernote): fix exception field mapping and add sandbox support

* fix(evernote): address PR review feedback

* fix(evernote): clamp maxNotes to Evernote's 250 limit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-07 00:52:57 -08:00
committed by GitHub
parent 53fd92a30a
commit 1ba1bc8edb
34 changed files with 3173 additions and 0 deletions

View File

@@ -1955,6 +1955,14 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
)
}
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
</svg>
)
}
export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -40,6 +40,7 @@ import {
ElasticsearchIcon,
ElevenLabsIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
EyeIcon,
FirecrawlIcon,
@@ -203,6 +204,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,

View File

@@ -0,0 +1,267 @@
---
title: Evernote
description: Manage notes, notebooks, and tags in Evernote
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="evernote"
color="#E0E0E0"
/>
## Usage Instructions
Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.
## Tools
### `evernote_copy_note`
Copy a note to another notebook in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to copy |
| `toNotebookGuid` | string | Yes | GUID of the destination notebook |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The copied note metadata |
| ↳ `guid` | string | New note GUID |
| ↳ `title` | string | Note title |
| ↳ `notebookGuid` | string | GUID of the destination notebook |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |
### `evernote_create_note`
Create a new note in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `title` | string | Yes | Title of the note |
| `content` | string | Yes | Content of the note \(plain text or ENML\) |
| `notebookGuid` | string | No | GUID of the notebook to create the note in \(defaults to default notebook\) |
| `tagNames` | string | No | Comma-separated list of tag names to apply |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The created note |
| ↳ `guid` | string | Unique identifier of the note |
| ↳ `title` | string | Title of the note |
| ↳ `content` | string | ENML content of the note |
| ↳ `notebookGuid` | string | GUID of the containing notebook |
| ↳ `tagNames` | array | Tag names applied to the note |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |
### `evernote_create_notebook`
Create a new notebook in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `name` | string | Yes | Name for the new notebook |
| `stack` | string | No | Stack name to group the notebook under |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notebook` | object | The created notebook |
| ↳ `guid` | string | Notebook GUID |
| ↳ `name` | string | Notebook name |
| ↳ `defaultNotebook` | boolean | Whether this is the default notebook |
| ↳ `serviceCreated` | number | Creation timestamp in milliseconds |
| ↳ `serviceUpdated` | number | Last updated timestamp in milliseconds |
| ↳ `stack` | string | Notebook stack name |
### `evernote_create_tag`
Create a new tag in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `name` | string | Yes | Name for the new tag |
| `parentGuid` | string | No | GUID of the parent tag for hierarchy |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tag` | object | The created tag |
| ↳ `guid` | string | Tag GUID |
| ↳ `name` | string | Tag name |
| ↳ `parentGuid` | string | Parent tag GUID |
| ↳ `updateSequenceNum` | number | Update sequence number |
### `evernote_delete_note`
Move a note to the trash in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the note was successfully deleted |
| `noteGuid` | string | GUID of the deleted note |
### `evernote_get_note`
Retrieve a note from Evernote by its GUID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to retrieve |
| `withContent` | boolean | No | Whether to include note content \(default: true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The retrieved note |
| ↳ `guid` | string | Unique identifier of the note |
| ↳ `title` | string | Title of the note |
| ↳ `content` | string | ENML content of the note |
| ↳ `contentLength` | number | Length of the note content |
| ↳ `notebookGuid` | string | GUID of the containing notebook |
| ↳ `tagGuids` | array | GUIDs of tags on the note |
| ↳ `tagNames` | array | Names of tags on the note |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |
| ↳ `active` | boolean | Whether the note is active \(not in trash\) |
### `evernote_get_notebook`
Retrieve a notebook from Evernote by its GUID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `notebookGuid` | string | Yes | GUID of the notebook to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notebook` | object | The retrieved notebook |
| ↳ `guid` | string | Notebook GUID |
| ↳ `name` | string | Notebook name |
| ↳ `defaultNotebook` | boolean | Whether this is the default notebook |
| ↳ `serviceCreated` | number | Creation timestamp in milliseconds |
| ↳ `serviceUpdated` | number | Last updated timestamp in milliseconds |
| ↳ `stack` | string | Notebook stack name |
### `evernote_list_notebooks`
List all notebooks in an Evernote account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `notebooks` | array | List of notebooks |
### `evernote_list_tags`
List all tags in an Evernote account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `tags` | array | List of tags |
### `evernote_search_notes`
Search for notes in Evernote using the Evernote search grammar
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `query` | string | Yes | Search query using Evernote search grammar \(e.g., "tag:work intitle:meeting"\) |
| `notebookGuid` | string | No | Restrict search to a specific notebook by GUID |
| `offset` | number | No | Starting index for results \(default: 0\) |
| `maxNotes` | number | No | Maximum number of notes to return \(default: 25\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `totalNotes` | number | Total number of matching notes |
| `notes` | array | List of matching note metadata |
### `evernote_update_note`
Update an existing note in Evernote
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Evernote developer token |
| `noteGuid` | string | Yes | GUID of the note to update |
| `title` | string | No | New title for the note |
| `content` | string | No | New content for the note \(plain text or ENML\) |
| `notebookGuid` | string | No | GUID of the notebook to move the note to |
| `tagNames` | string | No | Comma-separated list of tag names \(replaces existing tags\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `note` | object | The updated note |
| ↳ `guid` | string | Unique identifier of the note |
| ↳ `title` | string | Title of the note |
| ↳ `content` | string | ENML content of the note |
| ↳ `notebookGuid` | string | GUID of the containing notebook |
| ↳ `tagNames` | array | Tag names on the note |
| ↳ `created` | number | Creation timestamp in milliseconds |
| ↳ `updated` | number | Last updated timestamp in milliseconds |

View File

@@ -35,6 +35,7 @@
"elasticsearch",
"elevenlabs",
"enrich",
"evernote",
"exa",
"file",
"firecrawl",

View File

@@ -0,0 +1,38 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { copyNote } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteCopyNoteAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, noteGuid, toNotebookGuid } = body
if (!apiKey || !noteGuid || !toNotebookGuid) {
return NextResponse.json(
{ success: false, error: 'apiKey, noteGuid, and toNotebookGuid are required' },
{ status: 400 }
)
}
const note = await copyNote(apiKey, noteGuid, toNotebookGuid)
return NextResponse.json({
success: true,
output: { note },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to copy note', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,51 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createNote } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteCreateNoteAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, title, content, notebookGuid, tagNames } = body
if (!apiKey || !title || !content) {
return NextResponse.json(
{ success: false, error: 'apiKey, title, and content are required' },
{ status: 400 }
)
}
const parsedTags = tagNames
? (() => {
const tags =
typeof tagNames === 'string'
? tagNames
.split(',')
.map((t: string) => t.trim())
.filter(Boolean)
: tagNames
return tags.length > 0 ? tags : undefined
})()
: undefined
const note = await createNote(apiKey, title, content, notebookGuid || undefined, parsedTags)
return NextResponse.json({
success: true,
output: { note },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to create note', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,38 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createNotebook } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteCreateNotebookAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, name, stack } = body
if (!apiKey || !name) {
return NextResponse.json(
{ success: false, error: 'apiKey and name are required' },
{ status: 400 }
)
}
const notebook = await createNotebook(apiKey, name, stack || undefined)
return NextResponse.json({
success: true,
output: { notebook },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to create notebook', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,38 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { createTag } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteCreateTagAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, name, parentGuid } = body
if (!apiKey || !name) {
return NextResponse.json(
{ success: false, error: 'apiKey and name are required' },
{ status: 400 }
)
}
const tag = await createTag(apiKey, name, parentGuid || undefined)
return NextResponse.json({
success: true,
output: { tag },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to create tag', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,41 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { deleteNote } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteDeleteNoteAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, noteGuid } = body
if (!apiKey || !noteGuid) {
return NextResponse.json(
{ success: false, error: 'apiKey and noteGuid are required' },
{ status: 400 }
)
}
await deleteNote(apiKey, noteGuid)
return NextResponse.json({
success: true,
output: {
success: true,
noteGuid,
},
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to delete note', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,38 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { getNote } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteGetNoteAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, noteGuid, withContent = true } = body
if (!apiKey || !noteGuid) {
return NextResponse.json(
{ success: false, error: 'apiKey and noteGuid are required' },
{ status: 400 }
)
}
const note = await getNote(apiKey, noteGuid, withContent)
return NextResponse.json({
success: true,
output: { note },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to get note', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,38 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { getNotebook } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteGetNotebookAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, notebookGuid } = body
if (!apiKey || !notebookGuid) {
return NextResponse.json(
{ success: false, error: 'apiKey and notebookGuid are required' },
{ status: 400 }
)
}
const notebook = await getNotebook(apiKey, notebookGuid)
return NextResponse.json({
success: true,
output: { notebook },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to get notebook', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,799 @@
/**
* Evernote API client using Thrift binary protocol over HTTP.
* Implements only the NoteStore methods needed for the integration.
*/
import {
ThriftReader,
ThriftWriter,
TYPE_BOOL,
TYPE_I32,
TYPE_I64,
TYPE_LIST,
TYPE_STRING,
TYPE_STRUCT,
} from './thrift'
export interface EvernoteNotebook {
guid: string
name: string
defaultNotebook: boolean
serviceCreated: number | null
serviceUpdated: number | null
stack: string | null
}
export interface EvernoteNote {
guid: string
title: string
content: string | null
contentLength: number | null
created: number | null
updated: number | null
deleted: number | null
active: boolean
notebookGuid: string | null
tagGuids: string[]
tagNames: string[]
}
export interface EvernoteNoteMetadata {
guid: string
title: string | null
contentLength: number | null
created: number | null
updated: number | null
notebookGuid: string | null
tagGuids: string[]
}
export interface EvernoteTag {
guid: string
name: string
parentGuid: string | null
updateSequenceNum: number | null
}
export interface EvernoteSearchResult {
startIndex: number
totalNotes: number
notes: EvernoteNoteMetadata[]
}
/** Extract shard ID from an Evernote developer token */
function extractShardId(token: string): string {
const match = token.match(/S=s(\d+)/)
if (!match) {
throw new Error('Invalid Evernote token format: cannot extract shard ID')
}
return `s${match[1]}`
}
/** Get the NoteStore URL for the given token */
function getNoteStoreUrl(token: string): string {
const shardId = extractShardId(token)
const host = token.includes(':Sandbox') ? 'sandbox.evernote.com' : 'www.evernote.com'
return `https://${host}/shard/${shardId}/notestore`
}
/** Make a Thrift RPC call to the NoteStore */
async function callNoteStore(token: string, writer: ThriftWriter): Promise<ThriftReader> {
const url = getNoteStoreUrl(token)
const body = writer.toBuffer()
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-thrift',
Accept: 'application/x-thrift',
},
body: new Uint8Array(body),
})
if (!response.ok) {
throw new Error(`Evernote API HTTP error: ${response.status} ${response.statusText}`)
}
const arrayBuffer = await response.arrayBuffer()
const reader = new ThriftReader(arrayBuffer)
const msg = reader.readMessageBegin()
if (reader.isException(msg.type)) {
const ex = reader.readException()
throw new Error(`Evernote API error: ${ex.message}`)
}
return reader
}
/** Check for Evernote-specific exceptions in the response struct. Returns true if handled. */
function checkEvernoteException(reader: ThriftReader, fieldId: number, fieldType: number): boolean {
if (fieldId === 1 && fieldType === TYPE_STRUCT) {
let message = ''
let errorCode = 0
reader.readStruct((r, fid, ftype) => {
if (fid === 1 && ftype === TYPE_I32) {
errorCode = r.readI32()
} else if (fid === 2 && ftype === TYPE_STRING) {
message = r.readString()
} else {
r.skip(ftype)
}
})
throw new Error(`Evernote error (${errorCode}): ${message}`)
}
if (fieldId === 2 && fieldType === TYPE_STRUCT) {
let message = ''
let errorCode = 0
reader.readStruct((r, fid, ftype) => {
if (fid === 1 && ftype === TYPE_I32) {
errorCode = r.readI32()
} else if (fid === 2 && ftype === TYPE_STRING) {
message = r.readString()
} else {
r.skip(ftype)
}
})
throw new Error(`Evernote system error (${errorCode}): ${message}`)
}
if (fieldId === 3 && fieldType === TYPE_STRUCT) {
let identifier = ''
let key = ''
reader.readStruct((r, fid, ftype) => {
if (fid === 1 && ftype === TYPE_STRING) {
identifier = r.readString()
} else if (fid === 2 && ftype === TYPE_STRING) {
key = r.readString()
} else {
r.skip(ftype)
}
})
throw new Error(`Evernote not found: ${identifier}${key ? ` (${key})` : ''}`)
}
return false
}
function readNotebook(reader: ThriftReader): EvernoteNotebook {
const notebook: EvernoteNotebook = {
guid: '',
name: '',
defaultNotebook: false,
serviceCreated: null,
serviceUpdated: null,
stack: null,
}
reader.readStruct((r, fieldId, fieldType) => {
switch (fieldId) {
case 1:
if (fieldType === TYPE_STRING) notebook.guid = r.readString()
else r.skip(fieldType)
break
case 2:
if (fieldType === TYPE_STRING) notebook.name = r.readString()
else r.skip(fieldType)
break
case 4:
if (fieldType === TYPE_BOOL) notebook.defaultNotebook = r.readBool()
else r.skip(fieldType)
break
case 5:
if (fieldType === TYPE_I64) notebook.serviceCreated = Number(r.readI64())
else r.skip(fieldType)
break
case 6:
if (fieldType === TYPE_I64) notebook.serviceUpdated = Number(r.readI64())
else r.skip(fieldType)
break
case 9:
if (fieldType === TYPE_STRING) notebook.stack = r.readString()
else r.skip(fieldType)
break
default:
r.skip(fieldType)
}
})
return notebook
}
function readNote(reader: ThriftReader): EvernoteNote {
const note: EvernoteNote = {
guid: '',
title: '',
content: null,
contentLength: null,
created: null,
updated: null,
deleted: null,
active: true,
notebookGuid: null,
tagGuids: [],
tagNames: [],
}
reader.readStruct((r, fieldId, fieldType) => {
switch (fieldId) {
case 1:
if (fieldType === TYPE_STRING) note.guid = r.readString()
else r.skip(fieldType)
break
case 2:
if (fieldType === TYPE_STRING) note.title = r.readString()
else r.skip(fieldType)
break
case 3:
if (fieldType === TYPE_STRING) note.content = r.readString()
else r.skip(fieldType)
break
case 5:
if (fieldType === TYPE_I32) note.contentLength = r.readI32()
else r.skip(fieldType)
break
case 6:
if (fieldType === TYPE_I64) note.created = Number(r.readI64())
else r.skip(fieldType)
break
case 7:
if (fieldType === TYPE_I64) note.updated = Number(r.readI64())
else r.skip(fieldType)
break
case 8:
if (fieldType === TYPE_I64) note.deleted = Number(r.readI64())
else r.skip(fieldType)
break
case 9:
if (fieldType === TYPE_BOOL) note.active = r.readBool()
else r.skip(fieldType)
break
case 11:
if (fieldType === TYPE_STRING) note.notebookGuid = r.readString()
else r.skip(fieldType)
break
case 12:
if (fieldType === TYPE_LIST) {
const { size } = r.readListBegin()
for (let i = 0; i < size; i++) {
note.tagGuids.push(r.readString())
}
} else {
r.skip(fieldType)
}
break
case 15:
if (fieldType === TYPE_LIST) {
const { size } = r.readListBegin()
for (let i = 0; i < size; i++) {
note.tagNames.push(r.readString())
}
} else {
r.skip(fieldType)
}
break
default:
r.skip(fieldType)
}
})
return note
}
function readTag(reader: ThriftReader): EvernoteTag {
const tag: EvernoteTag = {
guid: '',
name: '',
parentGuid: null,
updateSequenceNum: null,
}
reader.readStruct((r, fieldId, fieldType) => {
switch (fieldId) {
case 1:
if (fieldType === TYPE_STRING) tag.guid = r.readString()
else r.skip(fieldType)
break
case 2:
if (fieldType === TYPE_STRING) tag.name = r.readString()
else r.skip(fieldType)
break
case 3:
if (fieldType === TYPE_STRING) tag.parentGuid = r.readString()
else r.skip(fieldType)
break
case 4:
if (fieldType === TYPE_I32) tag.updateSequenceNum = r.readI32()
else r.skip(fieldType)
break
default:
r.skip(fieldType)
}
})
return tag
}
function readNoteMetadata(reader: ThriftReader): EvernoteNoteMetadata {
const meta: EvernoteNoteMetadata = {
guid: '',
title: null,
contentLength: null,
created: null,
updated: null,
notebookGuid: null,
tagGuids: [],
}
reader.readStruct((r, fieldId, fieldType) => {
switch (fieldId) {
case 1:
if (fieldType === TYPE_STRING) meta.guid = r.readString()
else r.skip(fieldType)
break
case 2:
if (fieldType === TYPE_STRING) meta.title = r.readString()
else r.skip(fieldType)
break
case 5:
if (fieldType === TYPE_I32) meta.contentLength = r.readI32()
else r.skip(fieldType)
break
case 6:
if (fieldType === TYPE_I64) meta.created = Number(r.readI64())
else r.skip(fieldType)
break
case 7:
if (fieldType === TYPE_I64) meta.updated = Number(r.readI64())
else r.skip(fieldType)
break
case 11:
if (fieldType === TYPE_STRING) meta.notebookGuid = r.readString()
else r.skip(fieldType)
break
case 12:
if (fieldType === TYPE_LIST) {
const { size } = r.readListBegin()
for (let i = 0; i < size; i++) {
meta.tagGuids.push(r.readString())
}
} else {
r.skip(fieldType)
}
break
default:
r.skip(fieldType)
}
})
return meta
}
export async function listNotebooks(token: string): Promise<EvernoteNotebook[]> {
const writer = new ThriftWriter()
writer.writeMessageBegin('listNotebooks', 0)
writer.writeStringField(1, token)
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
const notebooks: EvernoteNotebook[] = []
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_LIST) {
const { size } = r.readListBegin()
for (let i = 0; i < size; i++) {
notebooks.push(readNotebook(r))
}
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
return notebooks
}
export async function getNote(
token: string,
guid: string,
withContent = true
): Promise<EvernoteNote> {
const writer = new ThriftWriter()
writer.writeMessageBegin('getNote', 0)
writer.writeStringField(1, token)
writer.writeStringField(2, guid)
writer.writeBoolField(3, withContent)
writer.writeBoolField(4, false)
writer.writeBoolField(5, false)
writer.writeBoolField(6, false)
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let note: EvernoteNote | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
note = readNote(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!note) {
throw new Error('No note returned from Evernote API')
}
return note
}
/** Wrap content in ENML if it's not already */
function wrapInEnml(content: string): string {
if (content.includes('<!DOCTYPE en-note')) {
return content
}
const escaped = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br/>')
return `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>${escaped}</en-note>`
}
export async function createNote(
token: string,
title: string,
content: string,
notebookGuid?: string,
tagNames?: string[]
): Promise<EvernoteNote> {
const writer = new ThriftWriter()
writer.writeMessageBegin('createNote', 0)
writer.writeStringField(1, token)
writer.writeFieldBegin(TYPE_STRUCT, 2)
writer.writeStringField(2, title)
writer.writeStringField(3, wrapInEnml(content))
if (notebookGuid) {
writer.writeStringField(11, notebookGuid)
}
if (tagNames && tagNames.length > 0) {
writer.writeStringListField(15, tagNames)
}
writer.writeFieldStop()
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let note: EvernoteNote | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
note = readNote(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!note) {
throw new Error('No note returned from Evernote API')
}
return note
}
export async function updateNote(
token: string,
guid: string,
title?: string,
content?: string,
notebookGuid?: string,
tagNames?: string[]
): Promise<EvernoteNote> {
const writer = new ThriftWriter()
writer.writeMessageBegin('updateNote', 0)
writer.writeStringField(1, token)
writer.writeFieldBegin(TYPE_STRUCT, 2)
writer.writeStringField(1, guid)
if (title !== undefined) {
writer.writeStringField(2, title)
}
if (content !== undefined) {
writer.writeStringField(3, wrapInEnml(content))
}
if (notebookGuid !== undefined) {
writer.writeStringField(11, notebookGuid)
}
if (tagNames !== undefined) {
writer.writeStringListField(15, tagNames)
}
writer.writeFieldStop()
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let note: EvernoteNote | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
note = readNote(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!note) {
throw new Error('No note returned from Evernote API')
}
return note
}
export async function deleteNote(token: string, guid: string): Promise<number> {
const writer = new ThriftWriter()
writer.writeMessageBegin('deleteNote', 0)
writer.writeStringField(1, token)
writer.writeStringField(2, guid)
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let usn = 0
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_I32) {
usn = r.readI32()
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
return usn
}
export async function searchNotes(
token: string,
query: string,
notebookGuid?: string,
offset = 0,
maxNotes = 25
): Promise<EvernoteSearchResult> {
const writer = new ThriftWriter()
writer.writeMessageBegin('findNotesMetadata', 0)
writer.writeStringField(1, token)
// NoteFilter (field 2)
writer.writeFieldBegin(TYPE_STRUCT, 2)
if (query) {
writer.writeStringField(3, query)
}
if (notebookGuid) {
writer.writeStringField(4, notebookGuid)
}
writer.writeFieldStop()
// offset (field 3)
writer.writeI32Field(3, offset)
// maxNotes (field 4)
writer.writeI32Field(4, maxNotes)
// NotesMetadataResultSpec (field 5)
writer.writeFieldBegin(TYPE_STRUCT, 5)
writer.writeBoolField(2, true) // includeTitle
writer.writeBoolField(5, true) // includeContentLength
writer.writeBoolField(6, true) // includeCreated
writer.writeBoolField(7, true) // includeUpdated
writer.writeBoolField(11, true) // includeNotebookGuid
writer.writeBoolField(12, true) // includeTagGuids
writer.writeFieldStop()
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
const result: EvernoteSearchResult = {
startIndex: 0,
totalNotes: 0,
notes: [],
}
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
r.readStruct((r2, fid2, ftype2) => {
switch (fid2) {
case 1:
if (ftype2 === TYPE_I32) result.startIndex = r2.readI32()
else r2.skip(ftype2)
break
case 2:
if (ftype2 === TYPE_I32) result.totalNotes = r2.readI32()
else r2.skip(ftype2)
break
case 3:
if (ftype2 === TYPE_LIST) {
const { size } = r2.readListBegin()
for (let i = 0; i < size; i++) {
result.notes.push(readNoteMetadata(r2))
}
} else {
r2.skip(ftype2)
}
break
default:
r2.skip(ftype2)
}
})
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
return result
}
export async function getNotebook(token: string, guid: string): Promise<EvernoteNotebook> {
const writer = new ThriftWriter()
writer.writeMessageBegin('getNotebook', 0)
writer.writeStringField(1, token)
writer.writeStringField(2, guid)
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let notebook: EvernoteNotebook | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
notebook = readNotebook(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!notebook) {
throw new Error('No notebook returned from Evernote API')
}
return notebook
}
export async function createNotebook(
token: string,
name: string,
stack?: string
): Promise<EvernoteNotebook> {
const writer = new ThriftWriter()
writer.writeMessageBegin('createNotebook', 0)
writer.writeStringField(1, token)
writer.writeFieldBegin(TYPE_STRUCT, 2)
writer.writeStringField(2, name)
if (stack) {
writer.writeStringField(9, stack)
}
writer.writeFieldStop()
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let notebook: EvernoteNotebook | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
notebook = readNotebook(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!notebook) {
throw new Error('No notebook returned from Evernote API')
}
return notebook
}
export async function listTags(token: string): Promise<EvernoteTag[]> {
const writer = new ThriftWriter()
writer.writeMessageBegin('listTags', 0)
writer.writeStringField(1, token)
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
const tags: EvernoteTag[] = []
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_LIST) {
const { size } = r.readListBegin()
for (let i = 0; i < size; i++) {
tags.push(readTag(r))
}
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
return tags
}
export async function createTag(
token: string,
name: string,
parentGuid?: string
): Promise<EvernoteTag> {
const writer = new ThriftWriter()
writer.writeMessageBegin('createTag', 0)
writer.writeStringField(1, token)
writer.writeFieldBegin(TYPE_STRUCT, 2)
writer.writeStringField(2, name)
if (parentGuid) {
writer.writeStringField(3, parentGuid)
}
writer.writeFieldStop()
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let tag: EvernoteTag | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
tag = readTag(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!tag) {
throw new Error('No tag returned from Evernote API')
}
return tag
}
export async function copyNote(
token: string,
noteGuid: string,
toNotebookGuid: string
): Promise<EvernoteNote> {
const writer = new ThriftWriter()
writer.writeMessageBegin('copyNote', 0)
writer.writeStringField(1, token)
writer.writeStringField(2, noteGuid)
writer.writeStringField(3, toNotebookGuid)
writer.writeFieldStop()
const reader = await callNoteStore(token, writer)
let note: EvernoteNote | null = null
reader.readStruct((r, fieldId, fieldType) => {
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
note = readNote(r)
} else {
if (!checkEvernoteException(r, fieldId, fieldType)) {
r.skip(fieldType)
}
}
})
if (!note) {
throw new Error('No note returned from Evernote API')
}
return note
}

View File

@@ -0,0 +1,255 @@
/**
* Minimal Thrift binary protocol encoder/decoder for Evernote API.
* Supports only the types needed for NoteStore operations.
*/
const THRIFT_VERSION_1 = 0x80010000
const MESSAGE_CALL = 1
const MESSAGE_EXCEPTION = 3
const TYPE_STOP = 0
const TYPE_BOOL = 2
const TYPE_I32 = 8
const TYPE_I64 = 10
const TYPE_STRING = 11
const TYPE_STRUCT = 12
const TYPE_LIST = 15
export class ThriftWriter {
private buffer: number[] = []
writeMessageBegin(name: string, seqId: number): void {
this.writeI32(THRIFT_VERSION_1 | MESSAGE_CALL)
this.writeString(name)
this.writeI32(seqId)
}
writeFieldBegin(type: number, id: number): void {
this.buffer.push(type)
this.writeI16(id)
}
writeFieldStop(): void {
this.buffer.push(TYPE_STOP)
}
writeString(value: string): void {
const encoded = new TextEncoder().encode(value)
this.writeI32(encoded.length)
for (const byte of encoded) {
this.buffer.push(byte)
}
}
writeBool(value: boolean): void {
this.buffer.push(value ? 1 : 0)
}
writeI16(value: number): void {
this.buffer.push((value >> 8) & 0xff)
this.buffer.push(value & 0xff)
}
writeI32(value: number): void {
this.buffer.push((value >> 24) & 0xff)
this.buffer.push((value >> 16) & 0xff)
this.buffer.push((value >> 8) & 0xff)
this.buffer.push(value & 0xff)
}
writeI64(value: bigint): void {
const buf = new ArrayBuffer(8)
const view = new DataView(buf)
view.setBigInt64(0, value, false)
for (let i = 0; i < 8; i++) {
this.buffer.push(view.getUint8(i))
}
}
writeStringField(id: number, value: string): void {
this.writeFieldBegin(TYPE_STRING, id)
this.writeString(value)
}
writeBoolField(id: number, value: boolean): void {
this.writeFieldBegin(TYPE_BOOL, id)
this.writeBool(value)
}
writeI32Field(id: number, value: number): void {
this.writeFieldBegin(TYPE_I32, id)
this.writeI32(value)
}
writeStringListField(id: number, values: string[]): void {
this.writeFieldBegin(TYPE_LIST, id)
this.buffer.push(TYPE_STRING)
this.writeI32(values.length)
for (const v of values) {
this.writeString(v)
}
}
toBuffer(): Buffer {
return Buffer.from(this.buffer)
}
}
export class ThriftReader {
private view: DataView
private pos = 0
constructor(buffer: ArrayBuffer) {
this.view = new DataView(buffer)
}
readMessageBegin(): { name: string; type: number; seqId: number } {
const versionAndType = this.readI32()
const version = versionAndType & 0xffff0000
if (version !== (THRIFT_VERSION_1 | 0)) {
throw new Error(`Unsupported Thrift version: 0x${version.toString(16)}`)
}
const type = versionAndType & 0x000000ff
const name = this.readString()
const seqId = this.readI32()
return { name, type, seqId }
}
readFieldBegin(): { type: number; id: number } {
const type = this.view.getUint8(this.pos++)
if (type === TYPE_STOP) {
return { type: TYPE_STOP, id: 0 }
}
const id = this.view.getInt16(this.pos, false)
this.pos += 2
return { type, id }
}
readString(): string {
const length = this.readI32()
const bytes = new Uint8Array(this.view.buffer, this.pos, length)
this.pos += length
return new TextDecoder().decode(bytes)
}
readBool(): boolean {
return this.view.getUint8(this.pos++) !== 0
}
readI32(): number {
const value = this.view.getInt32(this.pos, false)
this.pos += 4
return value
}
readI64(): bigint {
const value = this.view.getBigInt64(this.pos, false)
this.pos += 8
return value
}
readBinary(): Uint8Array {
const length = this.readI32()
const bytes = new Uint8Array(this.view.buffer, this.pos, length)
this.pos += length
return bytes
}
readListBegin(): { elementType: number; size: number } {
const elementType = this.view.getUint8(this.pos++)
const size = this.readI32()
return { elementType, size }
}
/** Skip a value of the given Thrift type */
skip(type: number): void {
switch (type) {
case TYPE_BOOL:
this.pos += 1
break
case 6: // I16
this.pos += 2
break
case 3: // BYTE
this.pos += 1
break
case TYPE_I32:
this.pos += 4
break
case TYPE_I64:
case 4: // DOUBLE
this.pos += 8
break
case TYPE_STRING: {
const len = this.readI32()
this.pos += len
break
}
case TYPE_STRUCT:
this.skipStruct()
break
case TYPE_LIST:
case 14: {
// SET
const { elementType, size } = this.readListBegin()
for (let i = 0; i < size; i++) {
this.skip(elementType)
}
break
}
case 13: {
// MAP
const keyType = this.view.getUint8(this.pos++)
const valueType = this.view.getUint8(this.pos++)
const count = this.readI32()
for (let i = 0; i < count; i++) {
this.skip(keyType)
this.skip(valueType)
}
break
}
default:
throw new Error(`Cannot skip unknown Thrift type: ${type}`)
}
}
private skipStruct(): void {
for (;;) {
const { type } = this.readFieldBegin()
if (type === TYPE_STOP) break
this.skip(type)
}
}
/** Read struct fields, calling the handler for each field */
readStruct<T>(handler: (reader: ThriftReader, fieldId: number, fieldType: number) => void): void {
for (;;) {
const { type, id } = this.readFieldBegin()
if (type === TYPE_STOP) break
handler(this, id, type)
}
}
/** Check if this is an exception response */
isException(messageType: number): boolean {
return messageType === MESSAGE_EXCEPTION
}
/** Read a Thrift application exception */
readException(): { message: string; type: number } {
let message = ''
let type = 0
this.readStruct((reader, fieldId, fieldType) => {
if (fieldId === 1 && fieldType === TYPE_STRING) {
message = reader.readString()
} else if (fieldId === 2 && fieldType === TYPE_I32) {
type = reader.readI32()
} else {
reader.skip(fieldType)
}
})
return { message, type }
}
}
export { TYPE_BOOL, TYPE_I32, TYPE_I64, TYPE_LIST, TYPE_STOP, TYPE_STRING, TYPE_STRUCT }

View File

@@ -0,0 +1,35 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { listNotebooks } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteListNotebooksAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey } = body
if (!apiKey) {
return NextResponse.json({ success: false, error: 'apiKey is required' }, { status: 400 })
}
const notebooks = await listNotebooks(apiKey)
return NextResponse.json({
success: true,
output: { notebooks },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to list notebooks', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,35 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { listTags } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteListTagsAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey } = body
if (!apiKey) {
return NextResponse.json({ success: false, error: 'apiKey is required' }, { status: 400 })
}
const tags = await listTags(apiKey)
return NextResponse.json({
success: true,
output: { tags },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to list tags', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { searchNotes } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteSearchNotesAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, query, notebookGuid, offset = 0, maxNotes = 25 } = body
if (!apiKey || !query) {
return NextResponse.json(
{ success: false, error: 'apiKey and query are required' },
{ status: 400 }
)
}
const clampedMaxNotes = Math.min(Math.max(Number(maxNotes) || 25, 1), 250)
const result = await searchNotes(
apiKey,
query,
notebookGuid || undefined,
Number(offset),
clampedMaxNotes
)
return NextResponse.json({
success: true,
output: {
totalNotes: result.totalNotes,
notes: result.notes,
},
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to search notes', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { updateNote } from '@/app/api/tools/evernote/lib/client'
export const dynamic = 'force-dynamic'
const logger = createLogger('EvernoteUpdateNoteAPI')
export async function POST(request: NextRequest) {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
const { apiKey, noteGuid, title, content, notebookGuid, tagNames } = body
if (!apiKey || !noteGuid) {
return NextResponse.json(
{ success: false, error: 'apiKey and noteGuid are required' },
{ status: 400 }
)
}
const parsedTags = tagNames
? (() => {
const tags =
typeof tagNames === 'string'
? tagNames
.split(',')
.map((t: string) => t.trim())
.filter(Boolean)
: tagNames
return tags.length > 0 ? tags : undefined
})()
: undefined
const note = await updateNote(
apiKey,
noteGuid,
title || undefined,
content || undefined,
notebookGuid || undefined,
parsedTags
)
return NextResponse.json({
success: true,
output: { note },
})
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Failed to update note', { error: message })
return NextResponse.json({ success: false, error: message }, { status: 500 })
}
}

View File

@@ -0,0 +1,308 @@
import { EvernoteIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const EvernoteBlock: BlockConfig = {
type: 'evernote',
name: 'Evernote',
description: 'Manage notes, notebooks, and tags in Evernote',
longDescription:
'Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.',
docsLink: 'https://docs.sim.ai/tools/evernote',
category: 'tools',
bgColor: '#E0E0E0',
icon: EvernoteIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Create Note', id: 'create_note' },
{ label: 'Get Note', id: 'get_note' },
{ label: 'Update Note', id: 'update_note' },
{ label: 'Delete Note', id: 'delete_note' },
{ label: 'Copy Note', id: 'copy_note' },
{ label: 'Search Notes', id: 'search_notes' },
{ label: 'Get Notebook', id: 'get_notebook' },
{ label: 'Create Notebook', id: 'create_notebook' },
{ label: 'List Notebooks', id: 'list_notebooks' },
{ label: 'Create Tag', id: 'create_tag' },
{ label: 'List Tags', id: 'list_tags' },
],
value: () => 'create_note',
},
{
id: 'apiKey',
title: 'Developer Token',
type: 'short-input',
password: true,
placeholder: 'Enter your Evernote developer token',
required: true,
},
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Note title',
condition: { field: 'operation', value: 'create_note' },
required: { field: 'operation', value: 'create_note' },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Note content (plain text or ENML)',
condition: { field: 'operation', value: 'create_note' },
required: { field: 'operation', value: 'create_note' },
},
{
id: 'noteGuid',
title: 'Note GUID',
type: 'short-input',
placeholder: 'Enter the note GUID',
condition: {
field: 'operation',
value: ['get_note', 'update_note', 'delete_note', 'copy_note'],
},
required: {
field: 'operation',
value: ['get_note', 'update_note', 'delete_note', 'copy_note'],
},
},
{
id: 'updateTitle',
title: 'New Title',
type: 'short-input',
placeholder: 'New title (leave empty to keep current)',
condition: { field: 'operation', value: 'update_note' },
},
{
id: 'updateContent',
title: 'New Content',
type: 'long-input',
placeholder: 'New content (leave empty to keep current)',
condition: { field: 'operation', value: 'update_note' },
},
{
id: 'toNotebookGuid',
title: 'Destination Notebook GUID',
type: 'short-input',
placeholder: 'GUID of the destination notebook',
condition: { field: 'operation', value: 'copy_note' },
required: { field: 'operation', value: 'copy_note' },
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'e.g., "tag:work intitle:meeting"',
condition: { field: 'operation', value: 'search_notes' },
required: { field: 'operation', value: 'search_notes' },
},
{
id: 'notebookGuid',
title: 'Notebook GUID',
type: 'short-input',
placeholder: 'Notebook GUID',
condition: {
field: 'operation',
value: ['create_note', 'update_note', 'search_notes', 'get_notebook'],
},
required: { field: 'operation', value: 'get_notebook' },
},
{
id: 'notebookName',
title: 'Notebook Name',
type: 'short-input',
placeholder: 'Name for the new notebook',
condition: { field: 'operation', value: 'create_notebook' },
required: { field: 'operation', value: 'create_notebook' },
},
{
id: 'stack',
title: 'Stack',
type: 'short-input',
placeholder: 'Stack name (optional)',
condition: { field: 'operation', value: 'create_notebook' },
mode: 'advanced',
},
{
id: 'tagName',
title: 'Tag Name',
type: 'short-input',
placeholder: 'Name for the new tag',
condition: { field: 'operation', value: 'create_tag' },
required: { field: 'operation', value: 'create_tag' },
},
{
id: 'parentGuid',
title: 'Parent Tag GUID',
type: 'short-input',
placeholder: 'Parent tag GUID (optional)',
condition: { field: 'operation', value: 'create_tag' },
mode: 'advanced',
},
{
id: 'tagNames',
title: 'Tags',
type: 'short-input',
placeholder: 'Comma-separated tags (e.g., "work, meeting, urgent")',
condition: { field: 'operation', value: ['create_note', 'update_note'] },
mode: 'advanced',
},
{
id: 'maxNotes',
title: 'Max Results',
type: 'short-input',
placeholder: '25',
condition: { field: 'operation', value: 'search_notes' },
mode: 'advanced',
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: '0',
condition: { field: 'operation', value: 'search_notes' },
mode: 'advanced',
},
{
id: 'withContent',
title: 'Include Content',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
condition: { field: 'operation', value: 'get_note' },
mode: 'advanced',
},
],
tools: {
access: [
'evernote_copy_note',
'evernote_create_note',
'evernote_create_notebook',
'evernote_create_tag',
'evernote_delete_note',
'evernote_get_note',
'evernote_get_notebook',
'evernote_list_notebooks',
'evernote_list_tags',
'evernote_search_notes',
'evernote_update_note',
],
config: {
tool: (params) => `evernote_${params.operation}`,
params: (params) => {
const { operation, apiKey, ...rest } = params
switch (operation) {
case 'create_note':
return {
apiKey,
title: rest.title,
content: rest.content,
notebookGuid: rest.notebookGuid || undefined,
tagNames: rest.tagNames || undefined,
}
case 'get_note':
return {
apiKey,
noteGuid: rest.noteGuid,
withContent: rest.withContent !== 'false',
}
case 'update_note':
return {
apiKey,
noteGuid: rest.noteGuid,
title: rest.updateTitle || undefined,
content: rest.updateContent || undefined,
notebookGuid: rest.notebookGuid || undefined,
tagNames: rest.tagNames || undefined,
}
case 'delete_note':
return {
apiKey,
noteGuid: rest.noteGuid,
}
case 'copy_note':
return {
apiKey,
noteGuid: rest.noteGuid,
toNotebookGuid: rest.toNotebookGuid,
}
case 'search_notes':
return {
apiKey,
query: rest.query,
notebookGuid: rest.notebookGuid || undefined,
offset: rest.offset ? Number(rest.offset) : 0,
maxNotes: rest.maxNotes ? Number(rest.maxNotes) : 25,
}
case 'get_notebook':
return {
apiKey,
notebookGuid: rest.notebookGuid,
}
case 'create_notebook':
return {
apiKey,
name: rest.notebookName,
stack: rest.stack || undefined,
}
case 'list_notebooks':
return { apiKey }
case 'create_tag':
return {
apiKey,
name: rest.tagName,
parentGuid: rest.parentGuid || undefined,
}
case 'list_tags':
return { apiKey }
default:
return { apiKey }
}
},
},
},
inputs: {
apiKey: { type: 'string', description: 'Evernote developer token' },
operation: { type: 'string', description: 'Operation to perform' },
title: { type: 'string', description: 'Note title' },
content: { type: 'string', description: 'Note content' },
noteGuid: { type: 'string', description: 'Note GUID' },
updateTitle: { type: 'string', description: 'New note title' },
updateContent: { type: 'string', description: 'New note content' },
toNotebookGuid: { type: 'string', description: 'Destination notebook GUID' },
query: { type: 'string', description: 'Search query' },
notebookGuid: { type: 'string', description: 'Notebook GUID' },
notebookName: { type: 'string', description: 'Notebook name' },
stack: { type: 'string', description: 'Notebook stack name' },
tagName: { type: 'string', description: 'Tag name' },
parentGuid: { type: 'string', description: 'Parent tag GUID' },
tagNames: { type: 'string', description: 'Comma-separated tag names' },
maxNotes: { type: 'string', description: 'Maximum number of results' },
offset: { type: 'string', description: 'Starting index for results' },
withContent: { type: 'string', description: 'Whether to include note content' },
},
outputs: {
note: { type: 'json', description: 'Note data' },
notebook: { type: 'json', description: 'Notebook data' },
notebooks: { type: 'json', description: 'List of notebooks' },
tag: { type: 'json', description: 'Tag data' },
tags: { type: 'json', description: 'List of tags' },
totalNotes: { type: 'number', description: 'Total number of matching notes' },
notes: { type: 'json', description: 'List of note metadata' },
success: { type: 'boolean', description: 'Whether the operation succeeded' },
noteGuid: { type: 'string', description: 'GUID of the affected note' },
},
}

View File

@@ -38,6 +38,7 @@ import { ElasticsearchBlock } from '@/blocks/blocks/elasticsearch'
import { ElevenLabsBlock } from '@/blocks/blocks/elevenlabs'
import { EnrichBlock } from '@/blocks/blocks/enrich'
import { EvaluatorBlock } from '@/blocks/blocks/evaluator'
import { EvernoteBlock } from '@/blocks/blocks/evernote'
import { ExaBlock } from '@/blocks/blocks/exa'
import { FileBlock, FileV2Block, FileV3Block } from '@/blocks/blocks/file'
import { FirecrawlBlock } from '@/blocks/blocks/firecrawl'
@@ -235,6 +236,7 @@ export const registry: Record<string, BlockConfig> = {
elasticsearch: ElasticsearchBlock,
elevenlabs: ElevenLabsBlock,
enrich: EnrichBlock,
evernote: EvernoteBlock,
evaluator: EvaluatorBlock,
exa: ExaBlock,
file: FileBlock,

View File

@@ -1955,6 +1955,14 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
)
}
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
</svg>
)
}
export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg

View File

@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteCopyNoteParams, EvernoteCopyNoteResponse } from './types'
export const evernoteCopyNoteTool: ToolConfig<EvernoteCopyNoteParams, EvernoteCopyNoteResponse> = {
id: 'evernote_copy_note',
name: 'Evernote Copy Note',
description: 'Copy a note to another notebook in Evernote',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
noteGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the note to copy',
},
toNotebookGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the destination notebook',
},
},
request: {
url: '/api/tools/evernote/copy-note',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
noteGuid: params.noteGuid,
toNotebookGuid: params.toNotebookGuid,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to copy note')
}
return {
success: true,
output: { note: data.output.note },
}
},
outputs: {
note: {
type: 'object',
description: 'The copied note metadata',
properties: {
guid: { type: 'string', description: 'New note GUID' },
title: { type: 'string', description: 'Note title' },
notebookGuid: {
type: 'string',
description: 'GUID of the destination notebook',
optional: true,
},
created: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
updated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
},
},
},
}

View File

@@ -0,0 +1,101 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteCreateNoteParams, EvernoteCreateNoteResponse } from './types'
export const evernoteCreateNoteTool: ToolConfig<
EvernoteCreateNoteParams,
EvernoteCreateNoteResponse
> = {
id: 'evernote_create_note',
name: 'Evernote Create Note',
description: 'Create a new note in Evernote',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the note',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Content of the note (plain text or ENML)',
},
notebookGuid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'GUID of the notebook to create the note in (defaults to default notebook)',
},
tagNames: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of tag names to apply',
},
},
request: {
url: '/api/tools/evernote/create-note',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
title: params.title,
content: params.content,
notebookGuid: params.notebookGuid || null,
tagNames: params.tagNames || null,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to create note')
}
return {
success: true,
output: { note: data.output.note },
}
},
outputs: {
note: {
type: 'object',
description: 'The created note',
properties: {
guid: { type: 'string', description: 'Unique identifier of the note' },
title: { type: 'string', description: 'Title of the note' },
content: { type: 'string', description: 'ENML content of the note', optional: true },
notebookGuid: {
type: 'string',
description: 'GUID of the containing notebook',
optional: true,
},
tagNames: {
type: 'array',
description: 'Tag names applied to the note',
optional: true,
},
created: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
updated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
},
},
},
}

View File

@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteCreateNotebookParams, EvernoteCreateNotebookResponse } from './types'
export const evernoteCreateNotebookTool: ToolConfig<
EvernoteCreateNotebookParams,
EvernoteCreateNotebookResponse
> = {
id: 'evernote_create_notebook',
name: 'Evernote Create Notebook',
description: 'Create a new notebook in Evernote',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new notebook',
},
stack: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Stack name to group the notebook under',
},
},
request: {
url: '/api/tools/evernote/create-notebook',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
name: params.name,
stack: params.stack || null,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to create notebook')
}
return {
success: true,
output: { notebook: data.output.notebook },
}
},
outputs: {
notebook: {
type: 'object',
description: 'The created notebook',
properties: {
guid: { type: 'string', description: 'Notebook GUID' },
name: { type: 'string', description: 'Notebook name' },
defaultNotebook: { type: 'boolean', description: 'Whether this is the default notebook' },
serviceCreated: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
serviceUpdated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
stack: { type: 'string', description: 'Notebook stack name', optional: true },
},
},
},
}

View File

@@ -0,0 +1,70 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteCreateTagParams, EvernoteCreateTagResponse } from './types'
export const evernoteCreateTagTool: ToolConfig<EvernoteCreateTagParams, EvernoteCreateTagResponse> =
{
id: 'evernote_create_tag',
name: 'Evernote Create Tag',
description: 'Create a new tag in Evernote',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name for the new tag',
},
parentGuid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'GUID of the parent tag for hierarchy',
},
},
request: {
url: '/api/tools/evernote/create-tag',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
name: params.name,
parentGuid: params.parentGuid || null,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to create tag')
}
return {
success: true,
output: { tag: data.output.tag },
}
},
outputs: {
tag: {
type: 'object',
description: 'The created tag',
properties: {
guid: { type: 'string', description: 'Tag GUID' },
name: { type: 'string', description: 'Tag name' },
parentGuid: { type: 'string', description: 'Parent tag GUID', optional: true },
updateSequenceNum: {
type: 'number',
description: 'Update sequence number',
optional: true,
},
},
},
},
}

View File

@@ -0,0 +1,62 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteDeleteNoteParams, EvernoteDeleteNoteResponse } from './types'
export const evernoteDeleteNoteTool: ToolConfig<
EvernoteDeleteNoteParams,
EvernoteDeleteNoteResponse
> = {
id: 'evernote_delete_note',
name: 'Evernote Delete Note',
description: 'Move a note to the trash in Evernote',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
noteGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the note to delete',
},
},
request: {
url: '/api/tools/evernote/delete-note',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
noteGuid: params.noteGuid,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to delete note')
}
return {
success: true,
output: {
success: true,
noteGuid: data.output.noteGuid,
},
}
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the note was successfully deleted',
},
noteGuid: {
type: 'string',
description: 'GUID of the deleted note',
},
},
}

View File

@@ -0,0 +1,87 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteGetNoteParams, EvernoteGetNoteResponse } from './types'
export const evernoteGetNoteTool: ToolConfig<EvernoteGetNoteParams, EvernoteGetNoteResponse> = {
id: 'evernote_get_note',
name: 'Evernote Get Note',
description: 'Retrieve a note from Evernote by its GUID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
noteGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the note to retrieve',
},
withContent: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to include note content (default: true)',
},
},
request: {
url: '/api/tools/evernote/get-note',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
noteGuid: params.noteGuid,
withContent: params.withContent ?? true,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to get note')
}
return {
success: true,
output: { note: data.output.note },
}
},
outputs: {
note: {
type: 'object',
description: 'The retrieved note',
properties: {
guid: { type: 'string', description: 'Unique identifier of the note' },
title: { type: 'string', description: 'Title of the note' },
content: { type: 'string', description: 'ENML content of the note', optional: true },
contentLength: {
type: 'number',
description: 'Length of the note content',
optional: true,
},
notebookGuid: {
type: 'string',
description: 'GUID of the containing notebook',
optional: true,
},
tagGuids: { type: 'array', description: 'GUIDs of tags on the note', optional: true },
tagNames: { type: 'array', description: 'Names of tags on the note', optional: true },
created: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
updated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
active: { type: 'boolean', description: 'Whether the note is active (not in trash)' },
},
},
},
}

View File

@@ -0,0 +1,71 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteGetNotebookParams, EvernoteGetNotebookResponse } from './types'
export const evernoteGetNotebookTool: ToolConfig<
EvernoteGetNotebookParams,
EvernoteGetNotebookResponse
> = {
id: 'evernote_get_notebook',
name: 'Evernote Get Notebook',
description: 'Retrieve a notebook from Evernote by its GUID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
notebookGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the notebook to retrieve',
},
},
request: {
url: '/api/tools/evernote/get-notebook',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
notebookGuid: params.notebookGuid,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to get notebook')
}
return {
success: true,
output: { notebook: data.output.notebook },
}
},
outputs: {
notebook: {
type: 'object',
description: 'The retrieved notebook',
properties: {
guid: { type: 'string', description: 'Notebook GUID' },
name: { type: 'string', description: 'Notebook name' },
defaultNotebook: { type: 'boolean', description: 'Whether this is the default notebook' },
serviceCreated: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
serviceUpdated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
stack: { type: 'string', description: 'Notebook stack name', optional: true },
},
},
},
}

View File

@@ -0,0 +1,12 @@
export { evernoteCopyNoteTool } from './copy_note'
export { evernoteCreateNoteTool } from './create_note'
export { evernoteCreateNotebookTool } from './create_notebook'
export { evernoteCreateTagTool } from './create_tag'
export { evernoteDeleteNoteTool } from './delete_note'
export { evernoteGetNoteTool } from './get_note'
export { evernoteGetNotebookTool } from './get_notebook'
export { evernoteListNotebooksTool } from './list_notebooks'
export { evernoteListTagsTool } from './list_tags'
export { evernoteSearchNotesTool } from './search_notes'
export * from './types'
export { evernoteUpdateNoteTool } from './update_note'

View File

@@ -0,0 +1,64 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteListNotebooksParams, EvernoteListNotebooksResponse } from './types'
export const evernoteListNotebooksTool: ToolConfig<
EvernoteListNotebooksParams,
EvernoteListNotebooksResponse
> = {
id: 'evernote_list_notebooks',
name: 'Evernote List Notebooks',
description: 'List all notebooks in an Evernote account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
},
request: {
url: '/api/tools/evernote/list-notebooks',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to list notebooks')
}
return {
success: true,
output: { notebooks: data.output.notebooks },
}
},
outputs: {
notebooks: {
type: 'array',
description: 'List of notebooks',
properties: {
guid: { type: 'string', description: 'Notebook GUID' },
name: { type: 'string', description: 'Notebook name' },
defaultNotebook: { type: 'boolean', description: 'Whether this is the default notebook' },
serviceCreated: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
serviceUpdated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
stack: { type: 'string', description: 'Notebook stack name', optional: true },
},
},
},
}

View File

@@ -0,0 +1,55 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteListTagsParams, EvernoteListTagsResponse } from './types'
export const evernoteListTagsTool: ToolConfig<EvernoteListTagsParams, EvernoteListTagsResponse> = {
id: 'evernote_list_tags',
name: 'Evernote List Tags',
description: 'List all tags in an Evernote account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
},
request: {
url: '/api/tools/evernote/list-tags',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to list tags')
}
return {
success: true,
output: { tags: data.output.tags },
}
},
outputs: {
tags: {
type: 'array',
description: 'List of tags',
properties: {
guid: { type: 'string', description: 'Tag GUID' },
name: { type: 'string', description: 'Tag name' },
parentGuid: { type: 'string', description: 'Parent tag GUID', optional: true },
updateSequenceNum: {
type: 'number',
description: 'Update sequence number',
optional: true,
},
},
},
},
}

View File

@@ -0,0 +1,92 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteSearchNotesParams, EvernoteSearchNotesResponse } from './types'
export const evernoteSearchNotesTool: ToolConfig<
EvernoteSearchNotesParams,
EvernoteSearchNotesResponse
> = {
id: 'evernote_search_notes',
name: 'Evernote Search Notes',
description: 'Search for notes in Evernote using the Evernote search grammar',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search query using Evernote search grammar (e.g., "tag:work intitle:meeting")',
},
notebookGuid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Restrict search to a specific notebook by GUID',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Starting index for results (default: 0)',
},
maxNotes: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of notes to return (default: 25)',
},
},
request: {
url: '/api/tools/evernote/search-notes',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
query: params.query,
notebookGuid: params.notebookGuid || null,
offset: params.offset ?? 0,
maxNotes: params.maxNotes ?? 25,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to search notes')
}
return {
success: true,
output: {
totalNotes: data.output.totalNotes,
notes: data.output.notes,
},
}
},
outputs: {
totalNotes: {
type: 'number',
description: 'Total number of matching notes',
},
notes: {
type: 'array',
description: 'List of matching note metadata',
properties: {
guid: { type: 'string', description: 'Note GUID' },
title: { type: 'string', description: 'Note title', optional: true },
contentLength: { type: 'number', description: 'Content length in bytes', optional: true },
created: { type: 'number', description: 'Creation timestamp', optional: true },
updated: { type: 'number', description: 'Last updated timestamp', optional: true },
notebookGuid: { type: 'string', description: 'Containing notebook GUID', optional: true },
tagGuids: { type: 'array', description: 'Tag GUIDs', optional: true },
},
},
},
}

View File

@@ -0,0 +1,166 @@
import type { ToolResponse } from '@/tools/types'
export interface EvernoteBaseParams {
apiKey: string
}
export interface EvernoteCreateNoteParams extends EvernoteBaseParams {
title: string
content: string
notebookGuid?: string
tagNames?: string
}
export interface EvernoteGetNoteParams extends EvernoteBaseParams {
noteGuid: string
withContent?: boolean
}
export interface EvernoteUpdateNoteParams extends EvernoteBaseParams {
noteGuid: string
title?: string
content?: string
notebookGuid?: string
tagNames?: string
}
export interface EvernoteDeleteNoteParams extends EvernoteBaseParams {
noteGuid: string
}
export interface EvernoteSearchNotesParams extends EvernoteBaseParams {
query: string
notebookGuid?: string
offset?: number
maxNotes?: number
}
export interface EvernoteListNotebooksParams extends EvernoteBaseParams {}
export interface EvernoteGetNotebookParams extends EvernoteBaseParams {
notebookGuid: string
}
export interface EvernoteCreateNotebookParams extends EvernoteBaseParams {
name: string
stack?: string
}
export interface EvernoteListTagsParams extends EvernoteBaseParams {}
export interface EvernoteCreateTagParams extends EvernoteBaseParams {
name: string
parentGuid?: string
}
export interface EvernoteCopyNoteParams extends EvernoteBaseParams {
noteGuid: string
toNotebookGuid: string
}
export interface EvernoteNoteOutput {
guid: string
title: string
content: string | null
contentLength: number | null
created: number | null
updated: number | null
active: boolean
notebookGuid: string | null
tagGuids: string[]
tagNames: string[]
}
export interface EvernoteNotebookOutput {
guid: string
name: string
defaultNotebook: boolean
serviceCreated: number | null
serviceUpdated: number | null
stack: string | null
}
export interface EvernoteNoteMetadataOutput {
guid: string
title: string | null
contentLength: number | null
created: number | null
updated: number | null
notebookGuid: string | null
tagGuids: string[]
}
export interface EvernoteTagOutput {
guid: string
name: string
parentGuid: string | null
updateSequenceNum: number | null
}
export interface EvernoteCreateNoteResponse extends ToolResponse {
output: {
note: EvernoteNoteOutput
}
}
export interface EvernoteGetNoteResponse extends ToolResponse {
output: {
note: EvernoteNoteOutput
}
}
export interface EvernoteUpdateNoteResponse extends ToolResponse {
output: {
note: EvernoteNoteOutput
}
}
export interface EvernoteDeleteNoteResponse extends ToolResponse {
output: {
success: boolean
noteGuid: string
}
}
export interface EvernoteSearchNotesResponse extends ToolResponse {
output: {
totalNotes: number
notes: EvernoteNoteMetadataOutput[]
}
}
export interface EvernoteListNotebooksResponse extends ToolResponse {
output: {
notebooks: EvernoteNotebookOutput[]
}
}
export interface EvernoteGetNotebookResponse extends ToolResponse {
output: {
notebook: EvernoteNotebookOutput
}
}
export interface EvernoteCreateNotebookResponse extends ToolResponse {
output: {
notebook: EvernoteNotebookOutput
}
}
export interface EvernoteListTagsResponse extends ToolResponse {
output: {
tags: EvernoteTagOutput[]
}
}
export interface EvernoteCreateTagResponse extends ToolResponse {
output: {
tag: EvernoteTagOutput
}
}
export interface EvernoteCopyNoteResponse extends ToolResponse {
output: {
note: EvernoteNoteOutput
}
}

View File

@@ -0,0 +1,104 @@
import type { ToolConfig } from '@/tools/types'
import type { EvernoteUpdateNoteParams, EvernoteUpdateNoteResponse } from './types'
export const evernoteUpdateNoteTool: ToolConfig<
EvernoteUpdateNoteParams,
EvernoteUpdateNoteResponse
> = {
id: 'evernote_update_note',
name: 'Evernote Update Note',
description: 'Update an existing note in Evernote',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Evernote developer token',
},
noteGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the note to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the note',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New content for the note (plain text or ENML)',
},
notebookGuid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'GUID of the notebook to move the note to',
},
tagNames: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of tag names (replaces existing tags)',
},
},
request: {
url: '/api/tools/evernote/update-note',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
noteGuid: params.noteGuid,
title: params.title || null,
content: params.content || null,
notebookGuid: params.notebookGuid || null,
tagNames: params.tagNames || null,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error || 'Failed to update note')
}
return {
success: true,
output: { note: data.output.note },
}
},
outputs: {
note: {
type: 'object',
description: 'The updated note',
properties: {
guid: { type: 'string', description: 'Unique identifier of the note' },
title: { type: 'string', description: 'Title of the note' },
content: { type: 'string', description: 'ENML content of the note', optional: true },
notebookGuid: {
type: 'string',
description: 'GUID of the containing notebook',
optional: true,
},
tagNames: { type: 'array', description: 'Tag names on the note', optional: true },
created: {
type: 'number',
description: 'Creation timestamp in milliseconds',
optional: true,
},
updated: {
type: 'number',
description: 'Last updated timestamp in milliseconds',
optional: true,
},
},
},
},
}

View File

@@ -426,6 +426,19 @@ import {
enrichSearchSimilarCompaniesTool,
enrichVerifyEmailTool,
} from '@/tools/enrich'
import {
evernoteCopyNoteTool,
evernoteCreateNotebookTool,
evernoteCreateNoteTool,
evernoteCreateTagTool,
evernoteDeleteNoteTool,
evernoteGetNotebookTool,
evernoteGetNoteTool,
evernoteListNotebooksTool,
evernoteListTagsTool,
evernoteSearchNotesTool,
evernoteUpdateNoteTool,
} from '@/tools/evernote'
import {
exaAnswerTool,
exaFindSimilarLinksTool,
@@ -3154,6 +3167,17 @@ export const tools: Record<string, ToolConfig> = {
elasticsearch_list_indices: elasticsearchListIndicesTool,
elasticsearch_cluster_health: elasticsearchClusterHealthTool,
elasticsearch_cluster_stats: elasticsearchClusterStatsTool,
evernote_copy_note: evernoteCopyNoteTool,
evernote_create_note: evernoteCreateNoteTool,
evernote_create_notebook: evernoteCreateNotebookTool,
evernote_create_tag: evernoteCreateTagTool,
evernote_delete_note: evernoteDeleteNoteTool,
evernote_get_note: evernoteGetNoteTool,
evernote_get_notebook: evernoteGetNotebookTool,
evernote_list_notebooks: evernoteListNotebooksTool,
evernote_list_tags: evernoteListTagsTool,
evernote_search_notes: evernoteSearchNotesTool,
evernote_update_note: evernoteUpdateNoteTool,
enrich_check_credits: enrichCheckCreditsTool,
enrich_company_funding: enrichCompanyFundingTool,
enrich_company_lookup: enrichCompanyLookupTool,