Compare commits

..

4 Commits

Author SHA1 Message Date
Vikhyath Mondreti
fc5df60d8f remove dead code 2026-03-06 18:37:32 -08:00
Vikhyath Mondreti
adea9db89d another workflowid pass through 2026-03-06 18:25:36 -08:00
Vikhyath Mondreti
94abc424be fix resolve values fallback 2026-03-06 18:18:25 -08:00
Vikhyath Mondreti
c1c6ed66d1 improvement(selectors): simplify selectorContext + add tests 2026-03-06 18:03:40 -08:00
67 changed files with 112 additions and 5772 deletions

View File

@@ -710,155 +710,6 @@ export function PerplexityIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ObsidianIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const bl = `${id}-bl`
const tr = `${id}-tr`
const tl = `${id}-tl`
const br = `${id}-br`
const te = `${id}-te`
const le = `${id}-le`
const be = `${id}-be`
const me = `${id}-me`
const clip = `${id}-clip`
return (
<svg {...props} viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'>
<radialGradient
id={bl}
cx='0'
cy='0'
gradientTransform='matrix(-59 -225 150 -39 161.4 470)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.4' />
<stop offset='1' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tr}
cx='0'
cy='0'
gradientTransform='matrix(50 -379 280 37 360 374.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.6' />
<stop offset='1' stopColor='#fff' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tl}
cx='0'
cy='0'
gradientTransform='matrix(69 -319 218 47 175.4 307)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.8' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={br}
cx='0'
cy='0'
gradientTransform='matrix(-96 -163 187 -111 335.3 512.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.3' />
<stop offset='1' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={te}
cx='0'
cy='0'
gradientTransform='matrix(-36 166 -112 -24 310 128.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='0' />
<stop offset='1' stopColor='#fff' stopOpacity='.2' />
</radialGradient>
<radialGradient
id={le}
cx='0'
cy='0'
gradientTransform='matrix(88 89 -190 187 111 220.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={be}
cx='0'
cy='0'
gradientTransform='matrix(9 130 -276 20 215 284)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={me}
cx='0'
cy='0'
gradientTransform='matrix(-198 -104 327 -623 400 399.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='.5' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<clipPath id={clip}>
<path d='M.2.2h512v512H.2z' />
</clipPath>
<g clipPath={`url(#${clip})`}>
<path
d='M382.3 475.6c-3.1 23.4-26 41.6-48.7 35.3-32.4-8.9-69.9-22.8-103.6-25.4l-51.7-4a34 34 0 0 1-22-10.2l-89-91.7a34 34 0 0 1-6.7-37.7s55-121 57.1-127.3c2-6.3 9.6-61.2 14-90.6 1.2-7.9 5-15 11-20.3L248 8.9a34.1 34.1 0 0 1 49.6 4.3L386 125.6a37 37 0 0 1 7.6 22.4c0 21.3 1.8 65 13.6 93.2 11.5 27.3 32.5 57 43.5 71.5a17.3 17.3 0 0 1 1.3 19.2 1494 1494 0 0 1-44.8 70.6c-15 22.3-21.9 49.9-25 73.1z'
fill='#6c31e3'
/>
<path
d='M165.9 478.3c41.4-84 40.2-144.2 22.6-187-16.2-39.6-46.3-64.5-70-80-.6 2.3-1.3 4.4-2.2 6.5L60.6 342a34 34 0 0 0 6.6 37.7l89.1 91.7a34 34 0 0 0 9.6 7z'
fill={`url(#${bl})`}
/>
<path
d='M278.4 307.8c11.2 1.2 22.2 3.6 32.8 7.6 34 12.7 65 41.2 90.5 96.3 1.8-3.1 3.6-6.2 5.6-9.2a1536 1536 0 0 0 44.8-70.6 17 17 0 0 0-1.3-19.2c-11-14.6-32-44.2-43.5-71.5-11.8-28.2-13.5-72-13.6-93.2 0-8.1-2.6-16-7.6-22.4L297.6 13.2a34 34 0 0 0-1.5-1.7 96 96 0 0 1 2 54 198.3 198.3 0 0 1-17.6 41.3l-7.2 14.2a171 171 0 0 0-19.4 71c-1.2 29.4 4.8 66.4 24.5 115.8z'
fill={`url(#${tr})`}
/>
<path
d='M278.4 307.8c-19.7-49.4-25.8-86.4-24.5-115.9a171 171 0 0 1 19.4-71c2.3-4.8 4.8-9.5 7.2-14.1 7.1-13.9 14-27 17.6-41.4a96 96 0 0 0-2-54A34.1 34.1 0 0 0 248 9l-105.4 94.8a34.1 34.1 0 0 0-10.9 20.3l-12.8 85-.5 2.3c23.8 15.5 54 40.4 70.1 80a147 147 0 0 1 7.8 24.8c28-6.8 55.7-11 82.1-8.3z'
fill={`url(#${tl})`}
/>
<path
d='M333.6 511c22.7 6.2 45.6-12 48.7-35.4a187 187 0 0 1 19.4-63.9c-25.6-55-56.5-83.6-90.4-96.3-36-13.4-75.2-9-115 .7 8.9 40.4 3.6 93.3-30.4 162.2 4 1.8 8.1 3 12.5 3.3 0 0 24.4 2 53.6 4.1 29 2 72.4 17.1 101.6 25.2z'
fill={`url(#${br})`}
/>
<g clipRule='evenodd' fillRule='evenodd'>
<path
d='M254.1 190c-1.3 29.2 2.4 62.8 22.1 112.1l-6.2-.5c-17.7-51.5-21.5-78-20.2-107.6a174.7 174.7 0 0 1 20.4-72c2.4-4.9 8-14.1 10.5-18.8 7.1-13.7 11.9-21 16-33.6 5.7-17.5 4.5-25.9 3.8-34.1 4.6 29.9-12.7 56-25.7 82.4a177.1 177.1 0 0 0-20.7 72z'
fill={`url(#${te})`}
/>
<path
d='M194.3 293.4c2.4 5.4 4.6 9.8 6 16.5L195 311c-2.1-7.8-3.8-13.4-6.8-20-17.8-42-46.3-63.6-69.7-79.5 28.2 15.2 57.2 39 75.7 81.9z'
fill={`url(#${le})`}
/>
<path
d='M200.6 315.1c9.8 46-1.2 104.2-33.6 160.9 27.1-56.2 40.2-110.1 29.3-160z'
fill={`url(#${be})`}
/>
<path
d='M312.5 311c53.1 19.9 73.6 63.6 88.9 100-19-38.1-45.2-80.3-90.8-96-34.8-11.8-64.1-10.4-114.3 1l-1.1-5c53.2-12.1 81-13.5 117.3 0z'
fill={`url(#${me})`}
/>
</g>
</g>
</svg>
)
}
export function NotionIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='1em' height='1em' {...props}>
@@ -1955,14 +1806,6 @@ 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,7 +40,6 @@ import {
ElasticsearchIcon,
ElevenLabsIcon,
EnrichSoIcon,
EvernoteIcon,
ExaAIIcon,
EyeIcon,
FirecrawlIcon,
@@ -104,7 +103,6 @@ import {
MySQLIcon,
Neo4jIcon,
NotionIcon,
ObsidianIcon,
OnePasswordIcon,
OpenAIIcon,
OutlookIcon,
@@ -204,7 +202,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
elasticsearch: ElasticsearchIcon,
elevenlabs: ElevenLabsIcon,
enrich: EnrichSoIcon,
evernote: EvernoteIcon,
exa: ExaAIIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
@@ -268,7 +265,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
mysql: MySQLIcon,
neo4j: Neo4jIcon,
notion_v2: NotionIcon,
obsidian: ObsidianIcon,
onedrive: MicrosoftOneDriveIcon,
onepassword: OnePasswordIcon,
openai: OpenAIIcon,

View File

@@ -1,267 +0,0 @@
---
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

@@ -1014,36 +1014,4 @@ Get Jira users. If an account ID is provided, returns a single user. Otherwise,
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |
### `jira_search_users`
Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `query` | string | Yes | A query string to search for users. Can be an email address, display name, or partial match. |
| `maxResults` | number | No | Maximum number of users to return \(default: 50, max: 1000\) |
| `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | ISO 8601 timestamp of the operation |
| `users` | array | Array of matching Jira users |
| ↳ `accountId` | string | Atlassian account ID of the user |
| ↳ `displayName` | string | Display name of the user |
| ↳ `active` | boolean | Whether the user account is active |
| ↳ `emailAddress` | string | Email address of the user |
| ↳ `accountType` | string | Type of account \(e.g., atlassian, app, customer\) |
| ↳ `avatarUrl` | string | URL to the user avatar \(48x48\) |
| ↳ `timeZone` | string | User timezone |
| ↳ `self` | string | REST API URL for this user |
| `total` | number | Number of users returned in this page \(may be less than total matches\) |
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |

View File

@@ -35,7 +35,6 @@
"elasticsearch",
"elevenlabs",
"enrich",
"evernote",
"exa",
"file",
"firecrawl",
@@ -99,7 +98,6 @@
"mysql",
"neo4j",
"notion",
"obsidian",
"onedrive",
"onepassword",
"openai",

View File

@@ -1,323 +0,0 @@
---
title: Obsidian
description: Interact with your Obsidian vault via the Local REST API
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="obsidian"
color="#0F0F0F"
/>
## Usage Instructions
Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin.
## Tools
### `obsidian_append_active`
Append content to the currently active file in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `content` | string | Yes | Markdown content to append to the active file |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_append_note`
Append content to an existing note in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Markdown content to append to the note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the note |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_append_periodic_note`
Append content to the current periodic note (daily, weekly, monthly, quarterly, or yearly). Creates the note if it does not exist.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `period` | string | Yes | Period type: daily, weekly, monthly, quarterly, or yearly |
| `content` | string | Yes | Markdown content to append to the periodic note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `period` | string | Period type of the note |
| `appended` | boolean | Whether content was successfully appended |
### `obsidian_create_note`
Create or replace a note in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path for the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Markdown content for the note |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the created note |
| `created` | boolean | Whether the note was successfully created |
### `obsidian_delete_note`
Delete a note from your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note to delete relative to vault root |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the deleted note |
| `deleted` | boolean | Whether the note was successfully deleted |
### `obsidian_execute_command`
Execute a command in Obsidian (e.g. open daily note, toggle sidebar)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `commandId` | string | Yes | ID of the command to execute \(use List Commands operation to discover available commands\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commandId` | string | ID of the executed command |
| `executed` | boolean | Whether the command was successfully executed |
### `obsidian_get_active`
Retrieve the content of the currently active file in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the active file |
| `filename` | string | Path to the active file |
### `obsidian_get_note`
Retrieve the content of a note from your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the note |
| `filename` | string | Path to the note |
### `obsidian_get_periodic_note`
Retrieve the current periodic note (daily, weekly, monthly, quarterly, or yearly)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `period` | string | Yes | Period type: daily, weekly, monthly, quarterly, or yearly |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Markdown content of the periodic note |
| `period` | string | Period type of the note |
### `obsidian_list_commands`
List all available commands in Obsidian
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commands` | json | List of available commands with IDs and names |
| ↳ `id` | string | Command identifier |
| ↳ `name` | string | Human-readable command name |
### `obsidian_list_files`
List files and directories in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `path` | string | No | Directory path relative to vault root. Leave empty to list root. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | json | List of files and directories |
| ↳ `path` | string | File or directory path |
| ↳ `type` | string | Whether the entry is a file or directory |
### `obsidian_open_file`
Open a file in the Obsidian UI (creates the file if it does not exist)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the file relative to vault root |
| `newLeaf` | boolean | No | Whether to open the file in a new leaf/tab |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the opened file |
| `opened` | boolean | Whether the file was successfully opened |
### `obsidian_patch_active`
Insert or replace content at a specific heading, block reference, or frontmatter field in the active file
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `content` | string | Yes | Content to insert at the target location |
| `operation` | string | Yes | How to insert content: append, prepend, or replace |
| `targetType` | string | Yes | Type of target: heading, block, or frontmatter |
| `target` | string | Yes | Target identifier \(heading text, block reference ID, or frontmatter field name\) |
| `targetDelimiter` | string | No | Delimiter for nested headings \(default: "::"\) |
| `trimTargetWhitespace` | boolean | No | Whether to trim whitespace from target before matching \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `patched` | boolean | Whether the active file was successfully patched |
### `obsidian_patch_note`
Insert or replace content at a specific heading, block reference, or frontmatter field in a note
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `filename` | string | Yes | Path to the note relative to vault root \(e.g. "folder/note.md"\) |
| `content` | string | Yes | Content to insert at the target location |
| `operation` | string | Yes | How to insert content: append, prepend, or replace |
| `targetType` | string | Yes | Type of target: heading, block, or frontmatter |
| `target` | string | Yes | Target identifier \(heading text, block reference ID, or frontmatter field name\) |
| `targetDelimiter` | string | No | Delimiter for nested headings \(default: "::"\) |
| `trimTargetWhitespace` | boolean | No | Whether to trim whitespace from target before matching \(default: false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `filename` | string | Path of the patched note |
| `patched` | boolean | Whether the note was successfully patched |
### `obsidian_search`
Search for text across notes in your Obsidian vault
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | API key from Obsidian Local REST API plugin settings |
| `baseUrl` | string | Yes | Base URL for the Obsidian Local REST API |
| `query` | string | Yes | Text to search for across vault notes |
| `contextLength` | number | No | Number of characters of context around each match \(default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | json | Search results with filenames, scores, and matching contexts |
| ↳ `filename` | string | Path to the matching note |
| ↳ `score` | number | Relevance score |
| ↳ `matches` | json | Matching text contexts |
| ↳ `context` | string | Text surrounding the match |

View File

@@ -19,6 +19,7 @@ import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { markExecutionCancelled } from '@/lib/execution/cancellation'
import { decrementSSEConnections, incrementSSEConnections } from '@/lib/monitoring/sse-connections'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
import {
@@ -630,9 +631,11 @@ async function handleMessageStream(
}
const encoder = new TextEncoder()
let messageStreamDecremented = false
const stream = new ReadableStream({
async start(controller) {
incrementSSEConnections('a2a-message')
const sendEvent = (event: string, data: unknown) => {
try {
const jsonRpcResponse = {
@@ -842,10 +845,19 @@ async function handleMessageStream(
})
} finally {
await releaseLock(lockKey, lockValue)
if (!messageStreamDecremented) {
messageStreamDecremented = true
decrementSSEConnections('a2a-message')
}
controller.close()
}
},
cancel() {},
cancel() {
if (!messageStreamDecremented) {
messageStreamDecremented = true
decrementSSEConnections('a2a-message')
}
},
})
return new NextResponse(stream, {
@@ -1030,16 +1042,22 @@ async function handleTaskResubscribe(
{ once: true }
)
let sseDecremented = false
const cleanup = () => {
isCancelled = true
if (pollTimeoutId) {
clearTimeout(pollTimeoutId)
pollTimeoutId = null
}
if (!sseDecremented) {
sseDecremented = true
decrementSSEConnections('a2a-resubscribe')
}
}
const stream = new ReadableStream({
async start(controller) {
incrementSSEConnections('a2a-resubscribe')
const sendEvent = (event: string, data: unknown): boolean => {
if (isCancelled || abortSignal.aborted) return false
try {

View File

@@ -14,6 +14,7 @@ import { getSession } from '@/lib/auth'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { mcpConnectionManager } from '@/lib/mcp/connection-manager'
import { mcpPubSub } from '@/lib/mcp/pubsub'
import { decrementSSEConnections, incrementSSEConnections } from '@/lib/monitoring/sse-connections'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('McpEventsSSE')
@@ -49,11 +50,14 @@ export async function GET(request: NextRequest) {
for (const unsub of unsubscribers) {
unsub()
}
decrementSSEConnections('mcp-events')
logger.info(`SSE connection closed for workspace ${workspaceId}`)
}
const stream = new ReadableStream({
start(controller) {
incrementSSEConnections('mcp-events')
const send = (eventName: string, data: Record<string, unknown>) => {
if (cleaned) return
try {

View File

@@ -1,38 +0,0 @@
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

@@ -1,51 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,799 +0,0 @@
/**
* 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

@@ -1,255 +0,0 @@
/**
* 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

@@ -1,35 +0,0 @@
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

@@ -1,35 +0,0 @@
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

@@ -1,49 +0,0 @@
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

@@ -1,58 +0,0 @@
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

@@ -10,6 +10,7 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { env } from '@/lib/core/config/env'
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags'
import { generateRequestId } from '@/lib/core/utils/request'
import { decrementSSEConnections, incrementSSEConnections } from '@/lib/monitoring/sse-connections'
import { enrichTableSchema } from '@/lib/table/llm/wand'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
import { extractResponseText, parseResponsesUsage } from '@/providers/openai/utils'
@@ -330,10 +331,14 @@ export async function POST(req: NextRequest) {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
let wandStreamClosed = false
const readable = new ReadableStream({
async start(controller) {
incrementSSEConnections('wand')
const reader = response.body?.getReader()
if (!reader) {
wandStreamClosed = true
decrementSSEConnections('wand')
controller.close()
return
}
@@ -478,9 +483,18 @@ export async function POST(req: NextRequest) {
controller.close()
} finally {
reader.releaseLock()
if (!wandStreamClosed) {
wandStreamClosed = true
decrementSSEConnections('wand')
}
}
},
cancel() {
if (!wandStreamClosed) {
wandStreamClosed = true
decrementSSEConnections('wand')
}
},
cancel() {},
})
return new Response(readable, {

View File

@@ -22,6 +22,7 @@ import { createExecutionEventWriter, setExecutionMeta } from '@/lib/execution/ev
import { processInputFileFields } from '@/lib/execution/files'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { decrementSSEConnections, incrementSSEConnections } from '@/lib/monitoring/sse-connections'
import {
cleanupExecutionBase64Cache,
hydrateUserFilesWithBase64,
@@ -763,6 +764,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const encoder = new TextEncoder()
const timeoutController = createTimeoutAbortController(preprocessResult.executionTimeout?.sync)
let isStreamClosed = false
let sseDecremented = false
const eventWriter = createExecutionEventWriter(executionId)
setExecutionMeta(executionId, {
@@ -773,6 +775,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
incrementSSEConnections('workflow-execute')
let finalMetaStatus: 'complete' | 'error' | 'cancelled' | null = null
const sendEvent = (event: ExecutionEvent) => {
@@ -1156,6 +1159,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
if (executionId) {
await cleanupExecutionBase64Cache(executionId)
}
if (!sseDecremented) {
sseDecremented = true
decrementSSEConnections('workflow-execute')
}
if (!isStreamClosed) {
try {
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
@@ -1167,6 +1174,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
cancel() {
isStreamClosed = true
logger.info(`[${requestId}] Client disconnected from SSE stream`)
if (!sseDecremented) {
sseDecremented = true
decrementSSEConnections('workflow-execute')
}
},
})

View File

@@ -7,6 +7,7 @@ import {
getExecutionMeta,
readExecutionEvents,
} from '@/lib/execution/event-buffer'
import { decrementSSEConnections, incrementSSEConnections } from '@/lib/monitoring/sse-connections'
import { formatSSEEvent } from '@/lib/workflows/executor/execution-events'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
@@ -73,8 +74,10 @@ export async function GET(
let closed = false
let sseDecremented = false
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
incrementSSEConnections('execution-stream-reconnect')
let lastEventId = fromEventId
const pollDeadline = Date.now() + MAX_POLL_DURATION_MS
@@ -142,11 +145,20 @@ export async function GET(
controller.close()
} catch {}
}
} finally {
if (!sseDecremented) {
sseDecremented = true
decrementSSEConnections('execution-stream-reconnect')
}
}
},
cancel() {
closed = true
logger.info('Client disconnected from reconnection stream', { executionId })
if (!sseDecremented) {
sseDecremented = true
decrementSSEConnections('execution-stream-reconnect')
}
},
})

View File

@@ -1,308 +0,0 @@
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

@@ -47,7 +47,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
{ label: 'Add Watcher', id: 'add_watcher' },
{ label: 'Remove Watcher', id: 'remove_watcher' },
{ label: 'Get Users', id: 'get_users' },
{ label: 'Search Users', id: 'search_users' },
],
value: () => 'read',
},
@@ -674,31 +673,6 @@ Return ONLY the comment text - no explanations.`,
placeholder: 'Maximum users to return (default: 50)',
condition: { field: 'operation', value: 'get_users' },
},
// Search Users fields
{
id: 'searchUsersQuery',
title: 'Search Query',
type: 'short-input',
required: true,
placeholder: 'Enter email address or display name to search',
condition: { field: 'operation', value: 'search_users' },
},
{
id: 'searchUsersMaxResults',
title: 'Max Results',
type: 'short-input',
placeholder: 'Maximum users to return (default: 50)',
condition: { field: 'operation', value: 'search_users' },
mode: 'advanced',
},
{
id: 'searchUsersStartAt',
title: 'Start At',
type: 'short-input',
placeholder: 'Pagination start index (default: 0)',
condition: { field: 'operation', value: 'search_users' },
mode: 'advanced',
},
// Trigger SubBlocks
...getTrigger('jira_issue_created').subBlocks,
...getTrigger('jira_issue_updated').subBlocks,
@@ -733,7 +707,6 @@ Return ONLY the comment text - no explanations.`,
'jira_add_watcher',
'jira_remove_watcher',
'jira_get_users',
'jira_search_users',
],
config: {
tool: (params) => {
@@ -794,8 +767,6 @@ Return ONLY the comment text - no explanations.`,
return 'jira_remove_watcher'
case 'get_users':
return 'jira_get_users'
case 'search_users':
return 'jira_search_users'
default:
return 'jira_retrieve'
}
@@ -1052,18 +1023,6 @@ Return ONLY the comment text - no explanations.`,
: undefined,
}
}
case 'search_users': {
return {
...baseParams,
query: params.searchUsersQuery,
maxResults: params.searchUsersMaxResults
? Number.parseInt(params.searchUsersMaxResults)
: undefined,
startAt: params.searchUsersStartAt
? Number.parseInt(params.searchUsersStartAt)
: undefined,
}
}
default:
return baseParams
}
@@ -1143,13 +1102,6 @@ Return ONLY the comment text - no explanations.`,
},
usersStartAt: { type: 'string', description: 'Pagination start index for users' },
usersMaxResults: { type: 'string', description: 'Maximum users to return' },
// Search Users operation inputs
searchUsersQuery: {
type: 'string',
description: 'Search query (email address or display name)',
},
searchUsersMaxResults: { type: 'string', description: 'Maximum users to return from search' },
searchUsersStartAt: { type: 'string', description: 'Pagination start index for user search' },
},
outputs: {
// Common outputs across all Jira operations

View File

@@ -1,270 +0,0 @@
import { ObsidianIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const ObsidianBlock: BlockConfig = {
type: 'obsidian',
name: 'Obsidian',
description: 'Interact with your Obsidian vault via the Local REST API',
longDescription:
'Read, create, update, search, and delete notes in your Obsidian vault. Manage periodic notes, execute commands, and patch content at specific locations. Requires the Obsidian Local REST API plugin.',
docsLink: 'https://docs.sim.ai/tools/obsidian',
category: 'tools',
bgColor: '#0F0F0F',
icon: ObsidianIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'List Files', id: 'list_files' },
{ label: 'Get Note', id: 'get_note' },
{ label: 'Create Note', id: 'create_note' },
{ label: 'Append to Note', id: 'append_note' },
{ label: 'Patch Note', id: 'patch_note' },
{ label: 'Delete Note', id: 'delete_note' },
{ label: 'Search', id: 'search' },
{ label: 'Get Active File', id: 'get_active' },
{ label: 'Append to Active File', id: 'append_active' },
{ label: 'Patch Active File', id: 'patch_active' },
{ label: 'Open File', id: 'open_file' },
{ label: 'List Commands', id: 'list_commands' },
{ label: 'Execute Command', id: 'execute_command' },
{ label: 'Get Periodic Note', id: 'get_periodic_note' },
{ label: 'Append to Periodic Note', id: 'append_periodic_note' },
],
value: () => 'get_note',
},
{
id: 'baseUrl',
title: 'Base URL',
type: 'short-input',
placeholder: 'https://127.0.0.1:27124',
value: () => 'https://127.0.0.1:27124',
required: true,
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Obsidian Local REST API key',
password: true,
required: true,
},
{
id: 'path',
title: 'Directory Path',
type: 'short-input',
placeholder: 'Leave empty for vault root (e.g. "Projects/notes")',
condition: { field: 'operation', value: 'list_files' },
},
{
id: 'filename',
title: 'Note Path',
type: 'short-input',
placeholder: 'folder/note.md',
condition: {
field: 'operation',
value: ['get_note', 'create_note', 'append_note', 'patch_note', 'delete_note', 'open_file'],
},
required: {
field: 'operation',
value: ['get_note', 'create_note', 'append_note', 'patch_note', 'delete_note', 'open_file'],
},
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Markdown content',
condition: {
field: 'operation',
value: [
'create_note',
'append_note',
'patch_note',
'append_active',
'patch_active',
'append_periodic_note',
],
},
required: {
field: 'operation',
value: [
'create_note',
'append_note',
'patch_note',
'append_active',
'patch_active',
'append_periodic_note',
],
},
},
{
id: 'patchOperation',
title: 'Patch Operation',
type: 'dropdown',
options: [
{ label: 'Append', id: 'append' },
{ label: 'Prepend', id: 'prepend' },
{ label: 'Replace', id: 'replace' },
],
value: () => 'append',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
required: { field: 'operation', value: ['patch_note', 'patch_active'] },
},
{
id: 'targetType',
title: 'Target Type',
type: 'dropdown',
options: [
{ label: 'Heading', id: 'heading' },
{ label: 'Block Reference', id: 'block' },
{ label: 'Frontmatter', id: 'frontmatter' },
],
value: () => 'heading',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
required: { field: 'operation', value: ['patch_note', 'patch_active'] },
},
{
id: 'target',
title: 'Target',
type: 'short-input',
placeholder: 'Heading text, block ID, or frontmatter field',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
required: { field: 'operation', value: ['patch_note', 'patch_active'] },
},
{
id: 'targetDelimiter',
title: 'Target Delimiter',
type: 'short-input',
placeholder: ':: (default)',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
mode: 'advanced',
},
{
id: 'trimTargetWhitespace',
title: 'Trim Target Whitespace',
type: 'switch',
condition: { field: 'operation', value: ['patch_note', 'patch_active'] },
mode: 'advanced',
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Text to search for',
condition: { field: 'operation', value: 'search' },
required: { field: 'operation', value: 'search' },
},
{
id: 'contextLength',
title: 'Context Length',
type: 'short-input',
placeholder: '100',
condition: { field: 'operation', value: 'search' },
mode: 'advanced',
},
{
id: 'commandId',
title: 'Command ID',
type: 'short-input',
placeholder: 'e.g. daily-notes:open-today',
condition: { field: 'operation', value: 'execute_command' },
required: { field: 'operation', value: 'execute_command' },
},
{
id: 'newLeaf',
title: 'Open in New Tab',
type: 'switch',
condition: { field: 'operation', value: 'open_file' },
mode: 'advanced',
},
{
id: 'period',
title: 'Period',
type: 'dropdown',
options: [
{ label: 'Daily', id: 'daily' },
{ label: 'Weekly', id: 'weekly' },
{ label: 'Monthly', id: 'monthly' },
{ label: 'Quarterly', id: 'quarterly' },
{ label: 'Yearly', id: 'yearly' },
],
value: () => 'daily',
condition: { field: 'operation', value: ['get_periodic_note', 'append_periodic_note'] },
required: { field: 'operation', value: ['get_periodic_note', 'append_periodic_note'] },
},
],
tools: {
access: [
'obsidian_append_active',
'obsidian_append_note',
'obsidian_append_periodic_note',
'obsidian_create_note',
'obsidian_delete_note',
'obsidian_execute_command',
'obsidian_get_active',
'obsidian_get_note',
'obsidian_get_periodic_note',
'obsidian_list_commands',
'obsidian_list_files',
'obsidian_open_file',
'obsidian_patch_active',
'obsidian_patch_note',
'obsidian_search',
],
config: {
tool: (params) => `obsidian_${params.operation}`,
params: (params) => {
const result: Record<string, unknown> = {}
if (params.contextLength) {
result.contextLength = Number(params.contextLength)
}
if (params.patchOperation) {
result.operation = params.patchOperation
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
baseUrl: { type: 'string', description: 'Base URL for the Obsidian Local REST API' },
apiKey: { type: 'string', description: 'API key for authentication' },
filename: { type: 'string', description: 'Path to the note relative to vault root' },
content: { type: 'string', description: 'Markdown content for the note' },
path: { type: 'string', description: 'Directory path to list' },
query: { type: 'string', description: 'Text to search for' },
contextLength: { type: 'number', description: 'Characters of context around matches' },
commandId: { type: 'string', description: 'ID of the command to execute' },
patchOperation: { type: 'string', description: 'Patch operation: append, prepend, or replace' },
targetType: { type: 'string', description: 'Target type: heading, block, or frontmatter' },
target: { type: 'string', description: 'Target identifier for patch operations' },
targetDelimiter: { type: 'string', description: 'Delimiter for nested headings' },
trimTargetWhitespace: { type: 'boolean', description: 'Trim whitespace from target' },
newLeaf: { type: 'boolean', description: 'Open file in new tab' },
period: { type: 'string', description: 'Periodic note period type' },
},
outputs: {
content: { type: 'string', description: 'Markdown content of the note' },
filename: { type: 'string', description: 'Path to the note' },
files: { type: 'json', description: 'List of files and directories (path, type)' },
results: { type: 'json', description: 'Search results (filename, score, matches)' },
commands: { type: 'json', description: 'List of available commands (id, name)' },
created: { type: 'boolean', description: 'Whether the note was created' },
appended: { type: 'boolean', description: 'Whether content was appended' },
patched: { type: 'boolean', description: 'Whether content was patched' },
deleted: { type: 'boolean', description: 'Whether the note was deleted' },
executed: { type: 'boolean', description: 'Whether the command was executed' },
opened: { type: 'boolean', description: 'Whether the file was opened' },
commandId: { type: 'string', description: 'ID of the executed command' },
period: { type: 'string', description: 'Period type of the periodic note' },
},
}

View File

@@ -38,7 +38,6 @@ 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'
@@ -114,7 +113,6 @@ import { MySQLBlock } from '@/blocks/blocks/mysql'
import { Neo4jBlock } from '@/blocks/blocks/neo4j'
import { NoteBlock } from '@/blocks/blocks/note'
import { NotionBlock, NotionV2Block } from '@/blocks/blocks/notion'
import { ObsidianBlock } from '@/blocks/blocks/obsidian'
import { OneDriveBlock } from '@/blocks/blocks/onedrive'
import { OnePasswordBlock } from '@/blocks/blocks/onepassword'
import { OpenAIBlock } from '@/blocks/blocks/openai'
@@ -236,7 +234,6 @@ export const registry: Record<string, BlockConfig> = {
elasticsearch: ElasticsearchBlock,
elevenlabs: ElevenLabsBlock,
enrich: EnrichBlock,
evernote: EvernoteBlock,
evaluator: EvaluatorBlock,
exa: ExaBlock,
file: FileBlock,
@@ -323,7 +320,6 @@ export const registry: Record<string, BlockConfig> = {
note: NoteBlock,
notion: NotionBlock,
notion_v2: NotionV2Block,
obsidian: ObsidianBlock,
onepassword: OnePasswordBlock,
onedrive: OneDriveBlock,
openai: OpenAIBlock,

View File

@@ -710,155 +710,6 @@ export function PerplexityIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ObsidianIcon(props: SVGProps<SVGSVGElement>) {
const id = useId()
const bl = `${id}-bl`
const tr = `${id}-tr`
const tl = `${id}-tl`
const br = `${id}-br`
const te = `${id}-te`
const le = `${id}-le`
const be = `${id}-be`
const me = `${id}-me`
const clip = `${id}-clip`
return (
<svg {...props} viewBox='0 0 512 512' fill='none' xmlns='http://www.w3.org/2000/svg'>
<radialGradient
id={bl}
cx='0'
cy='0'
gradientTransform='matrix(-59 -225 150 -39 161.4 470)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.4' />
<stop offset='1' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tr}
cx='0'
cy='0'
gradientTransform='matrix(50 -379 280 37 360 374.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.6' />
<stop offset='1' stopColor='#fff' stopOpacity='.1' />
</radialGradient>
<radialGradient
id={tl}
cx='0'
cy='0'
gradientTransform='matrix(69 -319 218 47 175.4 307)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.8' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={br}
cx='0'
cy='0'
gradientTransform='matrix(-96 -163 187 -111 335.3 512.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.3' />
<stop offset='1' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={te}
cx='0'
cy='0'
gradientTransform='matrix(-36 166 -112 -24 310 128.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='0' />
<stop offset='1' stopColor='#fff' stopOpacity='.2' />
</radialGradient>
<radialGradient
id={le}
cx='0'
cy='0'
gradientTransform='matrix(88 89 -190 187 111 220.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.4' />
</radialGradient>
<radialGradient
id={be}
cx='0'
cy='0'
gradientTransform='matrix(9 130 -276 20 215 284)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<radialGradient
id={me}
cx='0'
cy='0'
gradientTransform='matrix(-198 -104 327 -623 400 399.2)'
gradientUnits='userSpaceOnUse'
r='1'
>
<stop offset='0' stopColor='#fff' stopOpacity='.2' />
<stop offset='.5' stopColor='#fff' stopOpacity='.2' />
<stop offset='1' stopColor='#fff' stopOpacity='.3' />
</radialGradient>
<clipPath id={clip}>
<path d='M.2.2h512v512H.2z' />
</clipPath>
<g clipPath={`url(#${clip})`}>
<path
d='M382.3 475.6c-3.1 23.4-26 41.6-48.7 35.3-32.4-8.9-69.9-22.8-103.6-25.4l-51.7-4a34 34 0 0 1-22-10.2l-89-91.7a34 34 0 0 1-6.7-37.7s55-121 57.1-127.3c2-6.3 9.6-61.2 14-90.6 1.2-7.9 5-15 11-20.3L248 8.9a34.1 34.1 0 0 1 49.6 4.3L386 125.6a37 37 0 0 1 7.6 22.4c0 21.3 1.8 65 13.6 93.2 11.5 27.3 32.5 57 43.5 71.5a17.3 17.3 0 0 1 1.3 19.2 1494 1494 0 0 1-44.8 70.6c-15 22.3-21.9 49.9-25 73.1z'
fill='#6c31e3'
/>
<path
d='M165.9 478.3c41.4-84 40.2-144.2 22.6-187-16.2-39.6-46.3-64.5-70-80-.6 2.3-1.3 4.4-2.2 6.5L60.6 342a34 34 0 0 0 6.6 37.7l89.1 91.7a34 34 0 0 0 9.6 7z'
fill={`url(#${bl})`}
/>
<path
d='M278.4 307.8c11.2 1.2 22.2 3.6 32.8 7.6 34 12.7 65 41.2 90.5 96.3 1.8-3.1 3.6-6.2 5.6-9.2a1536 1536 0 0 0 44.8-70.6 17 17 0 0 0-1.3-19.2c-11-14.6-32-44.2-43.5-71.5-11.8-28.2-13.5-72-13.6-93.2 0-8.1-2.6-16-7.6-22.4L297.6 13.2a34 34 0 0 0-1.5-1.7 96 96 0 0 1 2 54 198.3 198.3 0 0 1-17.6 41.3l-7.2 14.2a171 171 0 0 0-19.4 71c-1.2 29.4 4.8 66.4 24.5 115.8z'
fill={`url(#${tr})`}
/>
<path
d='M278.4 307.8c-19.7-49.4-25.8-86.4-24.5-115.9a171 171 0 0 1 19.4-71c2.3-4.8 4.8-9.5 7.2-14.1 7.1-13.9 14-27 17.6-41.4a96 96 0 0 0-2-54A34.1 34.1 0 0 0 248 9l-105.4 94.8a34.1 34.1 0 0 0-10.9 20.3l-12.8 85-.5 2.3c23.8 15.5 54 40.4 70.1 80a147 147 0 0 1 7.8 24.8c28-6.8 55.7-11 82.1-8.3z'
fill={`url(#${tl})`}
/>
<path
d='M333.6 511c22.7 6.2 45.6-12 48.7-35.4a187 187 0 0 1 19.4-63.9c-25.6-55-56.5-83.6-90.4-96.3-36-13.4-75.2-9-115 .7 8.9 40.4 3.6 93.3-30.4 162.2 4 1.8 8.1 3 12.5 3.3 0 0 24.4 2 53.6 4.1 29 2 72.4 17.1 101.6 25.2z'
fill={`url(#${br})`}
/>
<g clipRule='evenodd' fillRule='evenodd'>
<path
d='M254.1 190c-1.3 29.2 2.4 62.8 22.1 112.1l-6.2-.5c-17.7-51.5-21.5-78-20.2-107.6a174.7 174.7 0 0 1 20.4-72c2.4-4.9 8-14.1 10.5-18.8 7.1-13.7 11.9-21 16-33.6 5.7-17.5 4.5-25.9 3.8-34.1 4.6 29.9-12.7 56-25.7 82.4a177.1 177.1 0 0 0-20.7 72z'
fill={`url(#${te})`}
/>
<path
d='M194.3 293.4c2.4 5.4 4.6 9.8 6 16.5L195 311c-2.1-7.8-3.8-13.4-6.8-20-17.8-42-46.3-63.6-69.7-79.5 28.2 15.2 57.2 39 75.7 81.9z'
fill={`url(#${le})`}
/>
<path
d='M200.6 315.1c9.8 46-1.2 104.2-33.6 160.9 27.1-56.2 40.2-110.1 29.3-160z'
fill={`url(#${be})`}
/>
<path
d='M312.5 311c53.1 19.9 73.6 63.6 88.9 100-19-38.1-45.2-80.3-90.8-96-34.8-11.8-64.1-10.4-114.3 1l-1.1-5c53.2-12.1 81-13.5 117.3 0z'
fill={`url(#${me})`}
/>
</g>
</g>
</svg>
)
}
export function NotionIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' width='1em' height='1em' {...props}>
@@ -1955,14 +1806,6 @@ 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

@@ -1,10 +1,16 @@
/**
* Periodic memory telemetry for monitoring heap growth in production.
* Logs process.memoryUsage() and V8 heap stats every 60s.
* Periodic memory telemetry for diagnosing heap growth in production.
* Logs process.memoryUsage(), V8 heap stats, and active SSE connection
* counts every 60s, enabling correlation between connection leaks and
* memory spikes.
*/
import v8 from 'node:v8'
import { createLogger } from '@sim/logger'
import {
getActiveSSEConnectionCount,
getActiveSSEConnectionsByRoute,
} from '@/lib/monitoring/sse-connections'
const logger = createLogger('MemoryTelemetry', { logLevel: 'INFO' })
@@ -17,6 +23,16 @@ export function startMemoryTelemetry(intervalMs = 60_000) {
started = true
const timer = setInterval(() => {
// Trigger opportunistic (non-blocking) garbage collection if running on Bun.
// This signals JSC GC + mimalloc page purge without blocking the event loop,
// helping reclaim RSS that mimalloc otherwise retains under sustained load.
const bunGlobal = (globalThis as Record<string, unknown>).Bun as
| { gc?: (force: boolean) => void }
| undefined
if (typeof bunGlobal?.gc === 'function') {
bunGlobal.gc(false)
}
const mem = process.memoryUsage()
const heap = v8.getHeapStatistics()
@@ -33,6 +49,8 @@ export function startMemoryTelemetry(intervalMs = 60_000) {
? process.getActiveResourcesInfo().length
: -1,
uptimeMin: Math.round(process.uptime() / 60),
activeSSEConnections: getActiveSSEConnectionCount(),
sseByRoute: getActiveSSEConnectionsByRoute(),
})
}, intervalMs)
timer.unref()

View File

@@ -0,0 +1,27 @@
/**
* Tracks active SSE connections by route for memory leak diagnostics.
* Logged alongside periodic memory telemetry to correlate connection
* counts with heap growth.
*/
const connections = new Map<string, number>()
export function incrementSSEConnections(route: string) {
connections.set(route, (connections.get(route) ?? 0) + 1)
}
export function decrementSSEConnections(route: string) {
const count = (connections.get(route) ?? 0) - 1
if (count <= 0) connections.delete(route)
else connections.set(route, count)
}
export function getActiveSSEConnectionCount(): number {
let total = 0
for (const count of connections.values()) total += count
return total
}
export function getActiveSSEConnectionsByRoute(): Record<string, number> {
return Object.fromEntries(connections)
}

View File

@@ -1166,12 +1166,6 @@ export async function queueWebhookExecution(
})
}
// Slack requires an empty 200 for interactive payloads (view_submission, block_actions, etc.)
// A JSON body like {"message":"..."} is not a recognized response format and causes modal errors
if (foundWebhook.provider === 'slack') {
return new NextResponse(null, { status: 200 })
}
// Twilio Voice requires TwiML XML response
if (foundWebhook.provider === 'twilio_voice') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
@@ -1217,12 +1211,6 @@ export async function queueWebhookExecution(
)
}
if (foundWebhook.provider === 'slack') {
// Return empty 200 to avoid Slack showing an error dialog to the user,
// even though processing failed. The error is already logged above.
return new NextResponse(null, { status: 200 })
}
if (foundWebhook.provider === 'twilio_voice') {
const errorTwiml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>

View File

@@ -117,10 +117,6 @@ export async function loadDeployedWorkflowState(
resolvedWorkspaceId = wfRow?.workspaceId ?? undefined
}
if (!resolvedWorkspaceId) {
throw new Error(`Workflow ${workflowId} has no workspace`)
}
const { blocks: migratedBlocks } = await applyBlockMigrations(
state.blocks || {},
resolvedWorkspaceId
@@ -143,7 +139,7 @@ export async function loadDeployedWorkflowState(
interface MigrationContext {
blocks: Record<string, BlockState>
workspaceId: string
workspaceId?: string
migrated: boolean
}
@@ -152,7 +148,7 @@ type BlockMigration = (ctx: MigrationContext) => MigrationContext | Promise<Migr
function createMigrationPipeline(migrations: BlockMigration[]) {
return async (
blocks: Record<string, BlockState>,
workspaceId: string
workspaceId?: string
): Promise<{ blocks: Record<string, BlockState>; migrated: boolean }> => {
let ctx: MigrationContext = { blocks, workspaceId, migrated: false }
for (const migration of migrations) {
@@ -174,6 +170,7 @@ const applyBlockMigrations = createMigrationPipeline([
}),
async (ctx) => {
if (!ctx.workspaceId) return ctx
const { blocks, migrated } = await migrateCredentialIds(ctx.blocks, ctx.workspaceId)
return { ...ctx, blocks, migrated: ctx.migrated || migrated }
},
@@ -412,13 +409,9 @@ export async function loadWorkflowFromNormalizedTables(
blocksMap[block.id] = assembled
})
if (!workflowRow?.workspaceId) {
throw new Error(`Workflow ${workflowId} has no workspace`)
}
const { blocks: finalBlocks, migrated } = await applyBlockMigrations(
blocksMap,
workflowRow.workspaceId
workflowRow?.workspaceId ?? undefined
)
if (migrated) {

View File

@@ -1,78 +0,0 @@
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

@@ -1,101 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,70 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,87 +0,0 @@
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

@@ -1,71 +0,0 @@
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

@@ -1,12 +0,0 @@
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

@@ -1,64 +0,0 @@
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

@@ -1,55 +0,0 @@
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

@@ -1,92 +0,0 @@
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

@@ -1,166 +0,0 @@
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

@@ -1,104 +0,0 @@
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

@@ -17,7 +17,6 @@ import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs'
import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher'
import { jiraRetrieveTool } from '@/tools/jira/retrieve'
import { jiraSearchIssuesTool } from '@/tools/jira/search_issues'
import { jiraSearchUsersTool } from '@/tools/jira/search_users'
import { jiraTransitionIssueTool } from '@/tools/jira/transition_issue'
import { jiraUpdateTool } from '@/tools/jira/update'
import { jiraUpdateCommentTool } from '@/tools/jira/update_comment'
@@ -49,5 +48,4 @@ export {
jiraAddWatcherTool,
jiraRemoveWatcherTool,
jiraGetUsersTool,
jiraSearchUsersTool,
}

View File

@@ -1,166 +0,0 @@
import type { JiraSearchUsersParams, JiraSearchUsersResponse } from '@/tools/jira/types'
import { TIMESTAMP_OUTPUT, USER_OUTPUT_PROPERTIES } from '@/tools/jira/types'
import { getJiraCloudId, transformUser } from '@/tools/jira/utils'
import type { ToolConfig } from '@/tools/types'
export const jiraSearchUsersTool: ToolConfig<JiraSearchUsersParams, JiraSearchUsersResponse> = {
id: 'jira_search_users',
name: 'Jira Search Users',
description:
'Search for Jira users by email address or display name. Returns matching users with their accountId, displayName, and emailAddress.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'A query string to search for users. Can be an email address, display name, or partial match.',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of users to return (default: 50, max: 1000)',
},
startAt: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'The index of the first user to return (for pagination, default: 0)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description:
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: (params: JiraSearchUsersParams) => {
if (params.cloudId) {
const queryParams = new URLSearchParams()
queryParams.append('query', params.query)
if (params.maxResults !== undefined)
queryParams.append('maxResults', String(params.maxResults))
if (params.startAt !== undefined) queryParams.append('startAt', String(params.startAt))
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/user/search?${queryParams.toString()}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
method: 'GET',
headers: (params: JiraSearchUsersParams) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response, params?: JiraSearchUsersParams) => {
const fetchUsers = async (cloudId: string) => {
const queryParams = new URLSearchParams()
queryParams.append('query', params!.query)
if (params!.maxResults !== undefined)
queryParams.append('maxResults', String(params!.maxResults))
if (params!.startAt !== undefined) queryParams.append('startAt', String(params!.startAt))
const usersUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/user/search?${queryParams.toString()}`
const usersResponse = await fetch(usersUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${params!.accessToken}`,
},
})
if (!usersResponse.ok) {
let message = `Failed to search Jira users (${usersResponse.status})`
try {
const err = await usersResponse.json()
message = err?.errorMessages?.join(', ') || err?.message || message
} catch (_e) {}
throw new Error(message)
}
return usersResponse.json()
}
let data: any
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
data = await fetchUsers(cloudId)
} else {
if (!response.ok) {
let message = `Failed to search Jira users (${response.status})`
try {
const err = await response.json()
message = err?.errorMessages?.join(', ') || err?.message || message
} catch (_e) {}
throw new Error(message)
}
data = await response.json()
}
const users = Array.isArray(data) ? data.filter(Boolean) : []
return {
success: true,
output: {
ts: new Date().toISOString(),
users: users.map((user: any) => ({
...(transformUser(user) ?? { accountId: '', displayName: '' }),
self: user.self ?? null,
})),
total: users.length,
startAt: params?.startAt ?? 0,
maxResults: params?.maxResults ?? 50,
},
}
},
outputs: {
ts: TIMESTAMP_OUTPUT,
users: {
type: 'array',
description: 'Array of matching Jira users',
items: {
type: 'object',
properties: {
...USER_OUTPUT_PROPERTIES,
self: {
type: 'string',
description: 'REST API URL for this user',
optional: true,
},
},
},
},
total: {
type: 'number',
description: 'Number of users returned in this page (may be less than total matches)',
},
startAt: { type: 'number', description: 'Pagination start index' },
maxResults: { type: 'number', description: 'Maximum results per page' },
},
}

View File

@@ -1549,34 +1549,6 @@ export interface JiraGetUsersParams {
cloudId?: string
}
export interface JiraSearchUsersParams {
accessToken: string
domain: string
query: string
maxResults?: number
startAt?: number
cloudId?: string
}
export interface JiraSearchUsersResponse extends ToolResponse {
output: {
ts: string
users: Array<{
accountId: string
accountType?: string | null
active?: boolean | null
displayName: string
emailAddress?: string | null
avatarUrl?: string | null
timeZone?: string | null
self?: string | null
}>
total: number
startAt: number
maxResults: number
}
}
export interface JiraGetUsersResponse extends ToolResponse {
output: {
ts: string
@@ -1622,4 +1594,3 @@ export type JiraResponse =
| JiraAddWatcherResponse
| JiraRemoveWatcherResponse
| JiraGetUsersResponse
| JiraSearchUsersResponse

View File

@@ -1,66 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianAppendActiveParams, ObsidianAppendActiveResponse } from './types'
export const appendActiveTool: ToolConfig<
ObsidianAppendActiveParams,
ObsidianAppendActiveResponse
> = {
id: 'obsidian_append_active',
name: 'Obsidian Append to Active File',
description: 'Append content to the currently active file in Obsidian',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content to append to the active file',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/active/`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to append to active file: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
appended: true,
},
}
},
outputs: {
appended: {
type: 'boolean',
description: 'Whether content was successfully appended',
},
},
}

View File

@@ -1,74 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianAppendNoteParams, ObsidianAppendNoteResponse } from './types'
export const appendNoteTool: ToolConfig<ObsidianAppendNoteParams, ObsidianAppendNoteResponse> = {
id: 'obsidian_append_note',
name: 'Obsidian Append to Note',
description: 'Append content to an existing note in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note relative to vault root (e.g. "folder/note.md")',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content to append to the note',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to append to note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
appended: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the note',
},
appended: {
type: 'boolean',
description: 'Whether content was successfully appended',
},
},
}

View File

@@ -1,78 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianAppendPeriodicNoteParams, ObsidianAppendPeriodicNoteResponse } from './types'
export const appendPeriodicNoteTool: ToolConfig<
ObsidianAppendPeriodicNoteParams,
ObsidianAppendPeriodicNoteResponse
> = {
id: 'obsidian_append_periodic_note',
name: 'Obsidian Append to Periodic Note',
description:
'Append content to the current periodic note (daily, weekly, monthly, quarterly, or yearly). Creates the note if it does not exist.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
period: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Period type: daily, weekly, monthly, quarterly, or yearly',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content to append to the periodic note',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/periodic/${encodeURIComponent(params.period)}/`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to append to periodic note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
period: params?.period ?? '',
appended: true,
},
}
},
outputs: {
period: {
type: 'string',
description: 'Period type of the note',
},
appended: {
type: 'boolean',
description: 'Whether content was successfully appended',
},
},
}

View File

@@ -1,74 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianCreateNoteParams, ObsidianCreateNoteResponse } from './types'
export const createNoteTool: ToolConfig<ObsidianCreateNoteParams, ObsidianCreateNoteResponse> = {
id: 'obsidian_create_note',
name: 'Obsidian Create Note',
description: 'Create or replace a note in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path for the note relative to vault root (e.g. "folder/note.md")',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Markdown content for the note',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
}),
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to create note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
created: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the created note',
},
created: {
type: 'boolean',
description: 'Whether the note was successfully created',
},
},
}

View File

@@ -1,66 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianDeleteNoteParams, ObsidianDeleteNoteResponse } from './types'
export const deleteNoteTool: ToolConfig<ObsidianDeleteNoteParams, ObsidianDeleteNoteResponse> = {
id: 'obsidian_delete_note',
name: 'Obsidian Delete Note',
description: 'Delete a note from your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note to delete relative to vault root',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to delete note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
deleted: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the deleted note',
},
deleted: {
type: 'boolean',
description: 'Whether the note was successfully deleted',
},
},
}

View File

@@ -1,70 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianExecuteCommandParams, ObsidianExecuteCommandResponse } from './types'
export const executeCommandTool: ToolConfig<
ObsidianExecuteCommandParams,
ObsidianExecuteCommandResponse
> = {
id: 'obsidian_execute_command',
name: 'Obsidian Execute Command',
description: 'Execute a command in Obsidian (e.g. open daily note, toggle sidebar)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
commandId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'ID of the command to execute (use List Commands operation to discover available commands)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/commands/${encodeURIComponent(params.commandId.trim())}/`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to execute command: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
commandId: params?.commandId ?? '',
executed: true,
},
}
},
outputs: {
commandId: {
type: 'string',
description: 'ID of the executed command',
},
executed: {
type: 'boolean',
description: 'Whether the command was successfully executed',
},
},
}

View File

@@ -1,59 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianGetActiveParams, ObsidianGetActiveResponse } from './types'
export const getActiveTool: ToolConfig<ObsidianGetActiveParams, ObsidianGetActiveResponse> = {
id: 'obsidian_get_active',
name: 'Obsidian Get Active File',
description: 'Retrieve the content of the currently active file in Obsidian',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/active/`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/vnd.olrapi.note+json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
content: data.content ?? '',
filename: data.path ?? null,
},
}
},
outputs: {
content: {
type: 'string',
description: 'Markdown content of the active file',
},
filename: {
type: 'string',
description: 'Path to the active file',
optional: true,
},
},
}

View File

@@ -1,68 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianGetNoteParams, ObsidianGetNoteResponse } from './types'
export const getNoteTool: ToolConfig<ObsidianGetNoteParams, ObsidianGetNoteResponse> = {
id: 'obsidian_get_note',
name: 'Obsidian Get Note',
description: 'Retrieve the content of a note from your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note relative to vault root (e.g. "folder/note.md")',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'text/markdown',
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to get note: ${error.message ?? response.statusText}`)
}
const content = await response.text()
return {
success: true,
output: {
content,
filename: params?.filename ?? '',
},
}
},
outputs: {
content: {
type: 'string',
description: 'Markdown content of the note',
},
filename: {
type: 'string',
description: 'Path to the note',
},
},
}

View File

@@ -1,67 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianGetPeriodicNoteParams, ObsidianGetPeriodicNoteResponse } from './types'
export const getPeriodicNoteTool: ToolConfig<
ObsidianGetPeriodicNoteParams,
ObsidianGetPeriodicNoteResponse
> = {
id: 'obsidian_get_periodic_note',
name: 'Obsidian Get Periodic Note',
description: 'Retrieve the current periodic note (daily, weekly, monthly, quarterly, or yearly)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
period: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Period type: daily, weekly, monthly, quarterly, or yearly',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/periodic/${encodeURIComponent(params.period)}/`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'text/markdown',
}),
},
transformResponse: async (response, params) => {
const content = await response.text()
return {
success: true,
output: {
content,
period: params?.period ?? '',
},
}
},
outputs: {
content: {
type: 'string',
description: 'Markdown content of the periodic note',
},
period: {
type: 'string',
description: 'Period type of the note',
},
},
}

View File

@@ -1,16 +0,0 @@
export { appendActiveTool as obsidianAppendActiveTool } from './append_active'
export { appendNoteTool as obsidianAppendNoteTool } from './append_note'
export { appendPeriodicNoteTool as obsidianAppendPeriodicNoteTool } from './append_periodic_note'
export { createNoteTool as obsidianCreateNoteTool } from './create_note'
export { deleteNoteTool as obsidianDeleteNoteTool } from './delete_note'
export { executeCommandTool as obsidianExecuteCommandTool } from './execute_command'
export { getActiveTool as obsidianGetActiveTool } from './get_active'
export { getNoteTool as obsidianGetNoteTool } from './get_note'
export { getPeriodicNoteTool as obsidianGetPeriodicNoteTool } from './get_periodic_note'
export { listCommandsTool as obsidianListCommandsTool } from './list_commands'
export { listFilesTool as obsidianListFilesTool } from './list_files'
export { openFileTool as obsidianOpenFileTool } from './open_file'
export { patchActiveTool as obsidianPatchActiveTool } from './patch_active'
export { patchNoteTool as obsidianPatchNoteTool } from './patch_note'
export { searchTool as obsidianSearchTool } from './search'
export * from './types'

View File

@@ -1,68 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianListCommandsParams, ObsidianListCommandsResponse } from './types'
export const listCommandsTool: ToolConfig<
ObsidianListCommandsParams,
ObsidianListCommandsResponse
> = {
id: 'obsidian_list_commands',
name: 'Obsidian List Commands',
description: 'List all available commands in Obsidian',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/commands/`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to list commands: ${error.message ?? response.statusText}`)
}
const data = await response.json()
return {
success: true,
output: {
commands:
data.commands?.map((cmd: { id: string; name: string }) => ({
id: cmd.id ?? '',
name: cmd.name ?? '',
})) ?? [],
},
}
},
outputs: {
commands: {
type: 'json',
description: 'List of available commands with IDs and names',
properties: {
id: { type: 'string', description: 'Command identifier' },
name: { type: 'string', description: 'Human-readable command name' },
},
},
},
}

View File

@@ -1,76 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianListFilesParams, ObsidianListFilesResponse } from './types'
export const listFilesTool: ToolConfig<ObsidianListFilesParams, ObsidianListFilesResponse> = {
id: 'obsidian_list_files',
name: 'Obsidian List Files',
description: 'List files and directories in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
path: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Directory path relative to vault root. Leave empty to list root.',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
const path = params.path
? `/${params.path.trim().split('/').map(encodeURIComponent).join('/')}/`
: '/'
return `${base}/vault${path}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to list files: ${error.message ?? response.statusText}`)
}
const data = await response.json()
return {
success: true,
output: {
files:
data.files?.map((f: string | { path: string; type: string }) => {
if (typeof f === 'string') {
return { path: f, type: f.endsWith('/') ? 'directory' : 'file' }
}
return { path: f.path ?? '', type: f.type ?? 'file' }
}) ?? [],
},
}
},
outputs: {
files: {
type: 'json',
description: 'List of files and directories',
properties: {
path: { type: 'string', description: 'File or directory path' },
type: { type: 'string', description: 'Whether the entry is a file or directory' },
},
},
},
}

View File

@@ -1,73 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianOpenFileParams, ObsidianOpenFileResponse } from './types'
export const openFileTool: ToolConfig<ObsidianOpenFileParams, ObsidianOpenFileResponse> = {
id: 'obsidian_open_file',
name: 'Obsidian Open File',
description: 'Open a file in the Obsidian UI (creates the file if it does not exist)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the file relative to vault root',
},
newLeaf: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to open the file in a new leaf/tab',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
const leafParam = params.newLeaf ? '?newLeaf=true' : ''
return `${base}/open/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}${leafParam}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to open file: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
opened: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the opened file',
},
opened: {
type: 'boolean',
description: 'Whether the file was successfully opened',
},
},
}

View File

@@ -1,107 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianPatchActiveParams, ObsidianPatchActiveResponse } from './types'
export const patchActiveTool: ToolConfig<ObsidianPatchActiveParams, ObsidianPatchActiveResponse> = {
id: 'obsidian_patch_active',
name: 'Obsidian Patch Active File',
description:
'Insert or replace content at a specific heading, block reference, or frontmatter field in the active file',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Content to insert at the target location',
},
operation: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'How to insert content: append, prepend, or replace',
},
targetType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of target: heading, block, or frontmatter',
},
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Target identifier (heading text, block reference ID, or frontmatter field name)',
},
targetDelimiter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Delimiter for nested headings (default: "::")',
},
trimTargetWhitespace: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to trim whitespace from target before matching (default: false)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/active/`
},
method: 'PATCH',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
Operation: params.operation,
'Target-Type': params.targetType,
Target: encodeURIComponent(params.target),
}
if (params.targetDelimiter) {
headers['Target-Delimiter'] = params.targetDelimiter
}
if (params.trimTargetWhitespace) {
headers['Trim-Target-Whitespace'] = 'true'
}
return headers
},
body: (params) => params.content,
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to patch active file: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
patched: true,
},
}
},
outputs: {
patched: {
type: 'boolean',
description: 'Whether the active file was successfully patched',
},
},
}

View File

@@ -1,118 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianPatchNoteParams, ObsidianPatchNoteResponse } from './types'
export const patchNoteTool: ToolConfig<ObsidianPatchNoteParams, ObsidianPatchNoteResponse> = {
id: 'obsidian_patch_note',
name: 'Obsidian Patch Note',
description:
'Insert or replace content at a specific heading, block reference, or frontmatter field in a note',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
filename: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Path to the note relative to vault root (e.g. "folder/note.md")',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Content to insert at the target location',
},
operation: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'How to insert content: append, prepend, or replace',
},
targetType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of target: heading, block, or frontmatter',
},
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Target identifier (heading text, block reference ID, or frontmatter field name)',
},
targetDelimiter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Delimiter for nested headings (default: "::")',
},
trimTargetWhitespace: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether to trim whitespace from target before matching (default: false)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
return `${base}/vault/${params.filename.trim().split('/').map(encodeURIComponent).join('/')}`
},
method: 'PATCH',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'text/markdown',
Operation: params.operation,
'Target-Type': params.targetType,
Target: encodeURIComponent(params.target),
}
if (params.targetDelimiter) {
headers['Target-Delimiter'] = params.targetDelimiter
}
if (params.trimTargetWhitespace) {
headers['Trim-Target-Whitespace'] = 'true'
}
return headers
},
body: (params) => params.content,
},
transformResponse: async (response, params) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Failed to patch note: ${error.message ?? response.statusText}`)
}
return {
success: true,
output: {
filename: params?.filename ?? '',
patched: true,
},
}
},
outputs: {
filename: {
type: 'string',
description: 'Path of the patched note',
},
patched: {
type: 'boolean',
description: 'Whether the note was successfully patched',
},
},
}

View File

@@ -1,95 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { ObsidianSearchParams, ObsidianSearchResponse } from './types'
export const searchTool: ToolConfig<ObsidianSearchParams, ObsidianSearchResponse> = {
id: 'obsidian_search',
name: 'Obsidian Search',
description: 'Search for text across notes in your Obsidian vault',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'API key from Obsidian Local REST API plugin settings',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Base URL for the Obsidian Local REST API',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Text to search for across vault notes',
},
contextLength: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of characters of context around each match (default: 100)',
},
},
request: {
url: (params) => {
const base = params.baseUrl.replace(/\/$/, '')
const contextParam = params.contextLength ? `&contextLength=${params.contextLength}` : ''
return `${base}/search/simple/?query=${encodeURIComponent(params.query)}${contextParam}`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(`Search failed: ${error.message ?? response.statusText}`)
}
const data = await response.json()
return {
success: true,
output: {
results:
data?.map(
(item: {
filename: string
score: number
matches: Array<{ match: { start: number; end: number }; context: string }>
}) => ({
filename: item.filename ?? '',
score: item.score ?? 0,
matches:
item.matches?.map((m: { context: string }) => ({
context: m.context ?? '',
})) ?? [],
})
) ?? [],
},
}
},
outputs: {
results: {
type: 'json',
description: 'Search results with filenames, scores, and matching contexts',
properties: {
filename: { type: 'string', description: 'Path to the matching note' },
score: { type: 'number', description: 'Relevance score' },
matches: {
type: 'json',
description: 'Matching text contexts',
properties: {
context: { type: 'string', description: 'Text surrounding the match' },
},
},
},
},
},
}

View File

@@ -1,190 +0,0 @@
import type { ToolResponse } from '@/tools/types'
export interface ObsidianBaseParams {
apiKey: string
baseUrl: string
}
export interface ObsidianListFilesParams extends ObsidianBaseParams {
path?: string
}
export interface ObsidianListFilesResponse extends ToolResponse {
output: {
files: Array<{
path: string
type: string
}>
}
}
export interface ObsidianGetNoteParams extends ObsidianBaseParams {
filename: string
}
export interface ObsidianGetNoteResponse extends ToolResponse {
output: {
content: string
filename: string
}
}
export interface ObsidianCreateNoteParams extends ObsidianBaseParams {
filename: string
content: string
}
export interface ObsidianCreateNoteResponse extends ToolResponse {
output: {
filename: string
created: boolean
}
}
export interface ObsidianAppendNoteParams extends ObsidianBaseParams {
filename: string
content: string
}
export interface ObsidianAppendNoteResponse extends ToolResponse {
output: {
filename: string
appended: boolean
}
}
export interface ObsidianPatchNoteParams extends ObsidianBaseParams {
filename: string
content: string
operation: string
targetType: string
target: string
targetDelimiter?: string
trimTargetWhitespace?: boolean
}
export interface ObsidianPatchNoteResponse extends ToolResponse {
output: {
filename: string
patched: boolean
}
}
export interface ObsidianDeleteNoteParams extends ObsidianBaseParams {
filename: string
}
export interface ObsidianDeleteNoteResponse extends ToolResponse {
output: {
filename: string
deleted: boolean
}
}
export interface ObsidianSearchParams extends ObsidianBaseParams {
query: string
contextLength?: number
}
export interface ObsidianSearchResponse extends ToolResponse {
output: {
results: Array<{
filename: string
score: number
matches: Array<{
context: string
}>
}>
}
}
export interface ObsidianGetActiveParams extends ObsidianBaseParams {}
export interface ObsidianGetActiveResponse extends ToolResponse {
output: {
content: string
filename: string | null
}
}
export interface ObsidianAppendActiveParams extends ObsidianBaseParams {
content: string
}
export interface ObsidianAppendActiveResponse extends ToolResponse {
output: {
appended: boolean
}
}
export interface ObsidianPatchActiveParams extends ObsidianBaseParams {
content: string
operation: string
targetType: string
target: string
targetDelimiter?: string
trimTargetWhitespace?: boolean
}
export interface ObsidianPatchActiveResponse extends ToolResponse {
output: {
patched: boolean
}
}
export interface ObsidianListCommandsParams extends ObsidianBaseParams {}
export interface ObsidianListCommandsResponse extends ToolResponse {
output: {
commands: Array<{
id: string
name: string
}>
}
}
export interface ObsidianExecuteCommandParams extends ObsidianBaseParams {
commandId: string
}
export interface ObsidianExecuteCommandResponse extends ToolResponse {
output: {
commandId: string
executed: boolean
}
}
export interface ObsidianOpenFileParams extends ObsidianBaseParams {
filename: string
newLeaf?: boolean
}
export interface ObsidianOpenFileResponse extends ToolResponse {
output: {
filename: string
opened: boolean
}
}
export interface ObsidianGetPeriodicNoteParams extends ObsidianBaseParams {
period: string
}
export interface ObsidianGetPeriodicNoteResponse extends ToolResponse {
output: {
content: string
period: string
}
}
export interface ObsidianAppendPeriodicNoteParams extends ObsidianBaseParams {
period: string
content: string
}
export interface ObsidianAppendPeriodicNoteResponse extends ToolResponse {
output: {
period: string
appended: boolean
}
}

View File

@@ -426,19 +426,6 @@ import {
enrichSearchSimilarCompaniesTool,
enrichVerifyEmailTool,
} from '@/tools/enrich'
import {
evernoteCopyNoteTool,
evernoteCreateNotebookTool,
evernoteCreateNoteTool,
evernoteCreateTagTool,
evernoteDeleteNoteTool,
evernoteGetNotebookTool,
evernoteGetNoteTool,
evernoteListNotebooksTool,
evernoteListTagsTool,
evernoteSearchNotesTool,
evernoteUpdateNoteTool,
} from '@/tools/evernote'
import {
exaAnswerTool,
exaFindSimilarLinksTool,
@@ -1098,7 +1085,6 @@ import {
jiraRemoveWatcherTool,
jiraRetrieveTool,
jiraSearchIssuesTool,
jiraSearchUsersTool,
jiraTransitionIssueTool,
jiraUpdateCommentTool,
jiraUpdateTool,
@@ -1463,23 +1449,6 @@ import {
notionWriteTool,
notionWriteV2Tool,
} from '@/tools/notion'
import {
obsidianAppendActiveTool,
obsidianAppendNoteTool,
obsidianAppendPeriodicNoteTool,
obsidianCreateNoteTool,
obsidianDeleteNoteTool,
obsidianExecuteCommandTool,
obsidianGetActiveTool,
obsidianGetNoteTool,
obsidianGetPeriodicNoteTool,
obsidianListCommandsTool,
obsidianListFilesTool,
obsidianOpenFileTool,
obsidianPatchActiveTool,
obsidianPatchNoteTool,
obsidianSearchTool,
} from '@/tools/obsidian'
import {
onedriveCreateFolderTool,
onedriveDeleteTool,
@@ -2567,7 +2536,6 @@ export const tools: Record<string, ToolConfig> = {
jira_add_watcher: jiraAddWatcherTool,
jira_remove_watcher: jiraRemoveWatcherTool,
jira_get_users: jiraGetUsersTool,
jira_search_users: jiraSearchUsersTool,
jsm_get_service_desks: jsmGetServiceDesksTool,
jsm_get_request_types: jsmGetRequestTypesTool,
jsm_get_request_type_fields: jsmGetRequestTypeFieldsTool,
@@ -2769,21 +2737,6 @@ export const tools: Record<string, ToolConfig> = {
notion_create_database_v2: notionCreateDatabaseV2Tool,
notion_update_page_v2: notionUpdatePageV2Tool,
notion_add_database_row_v2: notionAddDatabaseRowTool,
obsidian_append_active: obsidianAppendActiveTool,
obsidian_append_note: obsidianAppendNoteTool,
obsidian_append_periodic_note: obsidianAppendPeriodicNoteTool,
obsidian_create_note: obsidianCreateNoteTool,
obsidian_delete_note: obsidianDeleteNoteTool,
obsidian_execute_command: obsidianExecuteCommandTool,
obsidian_get_active: obsidianGetActiveTool,
obsidian_get_note: obsidianGetNoteTool,
obsidian_get_periodic_note: obsidianGetPeriodicNoteTool,
obsidian_list_commands: obsidianListCommandsTool,
obsidian_list_files: obsidianListFilesTool,
obsidian_open_file: obsidianOpenFileTool,
obsidian_patch_active: obsidianPatchActiveTool,
obsidian_patch_note: obsidianPatchNoteTool,
obsidian_search: obsidianSearchTool,
onepassword_list_vaults: onepasswordListVaultsTool,
onepassword_get_vault: onepasswordGetVaultTool,
onepassword_list_items: onepasswordListItemsTool,
@@ -3167,17 +3120,6 @@ 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,