diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 5525e048cfa..9e3ae80ad02 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1806,6 +1806,14 @@ export function Mem0Icon(props: SVGProps) { ) } +export function EvernoteIcon(props: SVGProps) { + return ( + + + + ) +} + export function ElevenLabsIcon(props: SVGProps) { return ( = { elasticsearch: ElasticsearchIcon, elevenlabs: ElevenLabsIcon, enrich: EnrichSoIcon, + evernote: EvernoteIcon, exa: ExaAIIcon, file_v3: DocumentIcon, firecrawl: FirecrawlIcon, diff --git a/apps/docs/content/docs/en/tools/evernote.mdx b/apps/docs/content/docs/en/tools/evernote.mdx new file mode 100644 index 00000000000..4c024edea38 --- /dev/null +++ b/apps/docs/content/docs/en/tools/evernote.mdx @@ -0,0 +1,267 @@ +--- +title: Evernote +description: Manage notes, notebooks, and tags in Evernote +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## 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 | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index f8d851049fe..39b5699c579 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -35,6 +35,7 @@ "elasticsearch", "elevenlabs", "enrich", + "evernote", "exa", "file", "firecrawl", diff --git a/apps/sim/app/api/tools/evernote/copy-note/route.ts b/apps/sim/app/api/tools/evernote/copy-note/route.ts new file mode 100644 index 00000000000..1011072a750 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/copy-note/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/create-note/route.ts b/apps/sim/app/api/tools/evernote/create-note/route.ts new file mode 100644 index 00000000000..ef1c97f5982 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/create-note/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/create-notebook/route.ts b/apps/sim/app/api/tools/evernote/create-notebook/route.ts new file mode 100644 index 00000000000..37ab2522d86 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/create-notebook/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/create-tag/route.ts b/apps/sim/app/api/tools/evernote/create-tag/route.ts new file mode 100644 index 00000000000..188516cbe87 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/create-tag/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/delete-note/route.ts b/apps/sim/app/api/tools/evernote/delete-note/route.ts new file mode 100644 index 00000000000..e55b298496a --- /dev/null +++ b/apps/sim/app/api/tools/evernote/delete-note/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/get-note/route.ts b/apps/sim/app/api/tools/evernote/get-note/route.ts new file mode 100644 index 00000000000..f71c84aa7d5 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/get-note/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/get-notebook/route.ts b/apps/sim/app/api/tools/evernote/get-notebook/route.ts new file mode 100644 index 00000000000..2f0e6db5d5d --- /dev/null +++ b/apps/sim/app/api/tools/evernote/get-notebook/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/lib/client.ts b/apps/sim/app/api/tools/evernote/lib/client.ts new file mode 100644 index 00000000000..05b80eb4829 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/lib/client.ts @@ -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 { + 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 { + 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 { + 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('/g, '>') + .replace(/\n/g, '
') + return `${escaped}` +} + +export async function createNote( + token: string, + title: string, + content: string, + notebookGuid?: string, + tagNames?: string[] +): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 +} diff --git a/apps/sim/app/api/tools/evernote/lib/thrift.ts b/apps/sim/app/api/tools/evernote/lib/thrift.ts new file mode 100644 index 00000000000..3f51b6933b4 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/lib/thrift.ts @@ -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(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 } diff --git a/apps/sim/app/api/tools/evernote/list-notebooks/route.ts b/apps/sim/app/api/tools/evernote/list-notebooks/route.ts new file mode 100644 index 00000000000..be5e3df9c5f --- /dev/null +++ b/apps/sim/app/api/tools/evernote/list-notebooks/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/list-tags/route.ts b/apps/sim/app/api/tools/evernote/list-tags/route.ts new file mode 100644 index 00000000000..2475d64ee49 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/list-tags/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/search-notes/route.ts b/apps/sim/app/api/tools/evernote/search-notes/route.ts new file mode 100644 index 00000000000..2687779e593 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/search-notes/route.ts @@ -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 }) + } +} diff --git a/apps/sim/app/api/tools/evernote/update-note/route.ts b/apps/sim/app/api/tools/evernote/update-note/route.ts new file mode 100644 index 00000000000..4a3fb884504 --- /dev/null +++ b/apps/sim/app/api/tools/evernote/update-note/route.ts @@ -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 }) + } +} diff --git a/apps/sim/blocks/blocks/evernote.ts b/apps/sim/blocks/blocks/evernote.ts new file mode 100644 index 00000000000..acc7fde5ccb --- /dev/null +++ b/apps/sim/blocks/blocks/evernote.ts @@ -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' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 7ff0b918dd1..26a1fb9aa86 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -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' @@ -234,6 +235,7 @@ export const registry: Record = { elasticsearch: ElasticsearchBlock, elevenlabs: ElevenLabsBlock, enrich: EnrichBlock, + evernote: EvernoteBlock, evaluator: EvaluatorBlock, exa: ExaBlock, file: FileBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 5525e048cfa..9e3ae80ad02 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1806,6 +1806,14 @@ export function Mem0Icon(props: SVGProps) { ) } +export function EvernoteIcon(props: SVGProps) { + return ( + + + + ) +} + export function ElevenLabsIcon(props: SVGProps) { return ( = { + 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/create_note.ts b/apps/sim/tools/evernote/create_note.ts new file mode 100644 index 00000000000..281735f6ac1 --- /dev/null +++ b/apps/sim/tools/evernote/create_note.ts @@ -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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/create_notebook.ts b/apps/sim/tools/evernote/create_notebook.ts new file mode 100644 index 00000000000..ba46e48b50b --- /dev/null +++ b/apps/sim/tools/evernote/create_notebook.ts @@ -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 }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/create_tag.ts b/apps/sim/tools/evernote/create_tag.ts new file mode 100644 index 00000000000..aeaa3d2dbf6 --- /dev/null +++ b/apps/sim/tools/evernote/create_tag.ts @@ -0,0 +1,70 @@ +import type { ToolConfig } from '@/tools/types' +import type { EvernoteCreateTagParams, EvernoteCreateTagResponse } from './types' + +export const evernoteCreateTagTool: ToolConfig = + { + 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, + }, + }, + }, + }, + } diff --git a/apps/sim/tools/evernote/delete_note.ts b/apps/sim/tools/evernote/delete_note.ts new file mode 100644 index 00000000000..6983a78d3f8 --- /dev/null +++ b/apps/sim/tools/evernote/delete_note.ts @@ -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', + }, + }, +} diff --git a/apps/sim/tools/evernote/get_note.ts b/apps/sim/tools/evernote/get_note.ts new file mode 100644 index 00000000000..4773bd23700 --- /dev/null +++ b/apps/sim/tools/evernote/get_note.ts @@ -0,0 +1,87 @@ +import type { ToolConfig } from '@/tools/types' +import type { EvernoteGetNoteParams, EvernoteGetNoteResponse } from './types' + +export const evernoteGetNoteTool: ToolConfig = { + 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)' }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/get_notebook.ts b/apps/sim/tools/evernote/get_notebook.ts new file mode 100644 index 00000000000..78a2fd59fa6 --- /dev/null +++ b/apps/sim/tools/evernote/get_notebook.ts @@ -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 }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/index.ts b/apps/sim/tools/evernote/index.ts new file mode 100644 index 00000000000..08819e0baf4 --- /dev/null +++ b/apps/sim/tools/evernote/index.ts @@ -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' diff --git a/apps/sim/tools/evernote/list_notebooks.ts b/apps/sim/tools/evernote/list_notebooks.ts new file mode 100644 index 00000000000..b2b9756c7e8 --- /dev/null +++ b/apps/sim/tools/evernote/list_notebooks.ts @@ -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 }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/list_tags.ts b/apps/sim/tools/evernote/list_tags.ts new file mode 100644 index 00000000000..65cb5a04fdd --- /dev/null +++ b/apps/sim/tools/evernote/list_tags.ts @@ -0,0 +1,55 @@ +import type { ToolConfig } from '@/tools/types' +import type { EvernoteListTagsParams, EvernoteListTagsResponse } from './types' + +export const evernoteListTagsTool: ToolConfig = { + 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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/search_notes.ts b/apps/sim/tools/evernote/search_notes.ts new file mode 100644 index 00000000000..a75056434d3 --- /dev/null +++ b/apps/sim/tools/evernote/search_notes.ts @@ -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 }, + }, + }, + }, +} diff --git a/apps/sim/tools/evernote/types.ts b/apps/sim/tools/evernote/types.ts new file mode 100644 index 00000000000..153594b3cc1 --- /dev/null +++ b/apps/sim/tools/evernote/types.ts @@ -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 + } +} diff --git a/apps/sim/tools/evernote/update_note.ts b/apps/sim/tools/evernote/update_note.ts new file mode 100644 index 00000000000..48872e6c6e4 --- /dev/null +++ b/apps/sim/tools/evernote/update_note.ts @@ -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, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3539724f68d..1e03cfe7698 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -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, @@ -3122,6 +3135,17 @@ export const tools: Record = { 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,