mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
52 Commits
improvemen
...
v0.5.113
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c12914d35 | ||
|
|
d90f828e88 | ||
|
|
a8bbab2d21 | ||
|
|
72bb7e6945 | ||
|
|
4cb0f4a2b0 | ||
|
|
fdd587d6af | ||
|
|
e9bdc57616 | ||
|
|
e7b4da2689 | ||
|
|
aa0101c666 | ||
|
|
c939f8a76e | ||
|
|
0b19ad0013 | ||
|
|
3d5141d852 | ||
|
|
75832ca007 | ||
|
|
97f78c60b4 | ||
|
|
9295499405 | ||
|
|
6bcbd15ee6 | ||
|
|
36612ae42a | ||
|
|
68d207df94 | ||
|
|
d5502d602b | ||
|
|
37d524bb0a | ||
|
|
1c2c2c65d4 | ||
|
|
19ef526886 | ||
|
|
ff2a1527ab | ||
|
|
2e1c639a81 | ||
|
|
ecd3536a72 | ||
|
|
635179d696 | ||
|
|
f88926a6a8 | ||
|
|
690b47a0bf | ||
|
|
158d5236bc | ||
|
|
1ba1bc8edb | ||
|
|
53fd92a30a | ||
|
|
8c0a2e04b1 | ||
|
|
0a52b09deb | ||
|
|
1d36b80172 | ||
|
|
e6a5e7f4e4 | ||
|
|
6586c5ce40 | ||
|
|
3ce947566d | ||
|
|
70c36cb7aa | ||
|
|
f1ec5fe824 | ||
|
|
e07e3c34cc | ||
|
|
0d2e6ff31d | ||
|
|
4fd0989264 | ||
|
|
67f8a687f6 | ||
|
|
af592349d3 | ||
|
|
0d86ea01f0 | ||
|
|
115f04e989 | ||
|
|
34d92fae89 | ||
|
|
67aa4bb332 | ||
|
|
15ace5e63f | ||
|
|
fdca73679d | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -710,6 +710,155 @@ 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}>
|
||||
@@ -1806,6 +1955,14 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
|
||||
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1822,6 +1979,24 @@ export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function FathomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000' fill='none'>
|
||||
<path
|
||||
d='M0,668.7v205.78c0,53.97,34.24,102.88,85.8,119.08,87.48,27.49,167.88-36.99,167.88-120.22v-77.45L0,668.7Z'
|
||||
fill='#007299'
|
||||
/>
|
||||
<path
|
||||
d='M873.72,626.07c-19.05,0-38.38-4.3-56.58-13.38L72.78,241.43C11.15,210.69-17.51,136.6,11.18,74.05,41.2,8.59,119.26-18.53,183.23,13.38l744.25,371.21c62.45,31.15,91,109.08,59.79,171.43-22.22,44.38-67.02,70.05-113.55,70.05Z'
|
||||
fill='#00beff'
|
||||
/>
|
||||
<path
|
||||
d='M500.09,813.66c-19.05,0-38.38-4.3-56.58-13.38l-370.72-184.9c-61.63-30.74-90.29-104.82-61.61-167.37,30.02-65.46,108.08-92.59,172.06-60.68l370.62,184.85c62.45,31.15,91,109.08,59.79,171.43-22.22,44.38-67.02,70.05-113.55,70.05Z'
|
||||
fill='#00beff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function LinkupIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 154 107' fill='none'>
|
||||
@@ -3397,6 +3572,27 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleAdsIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<g transform='matrix(.257748 0 0 .257745 -.361416 2.515516)'>
|
||||
<path
|
||||
d='M85.9 28.6c2.4-6.3 5.7-12.1 10.6-16.8 19.6-19.1 52-14.3 65.3 9.7 10 18.2 20.6 36 30.9 54l51.6 89.8c14.3 25.1-1.2 56.8-29.6 61.1-17.4 2.6-33.7-5.4-42.7-21l-45.4-78.8c-.3-.6-.7-1.1-1.1-1.6-1.6-1.3-2.3-3.2-3.3-4.9L88.8 62.2c-3.9-6.8-5.7-14.2-5.5-22 .3-4 .8-8 2.6-11.6'
|
||||
fill='#3c8bd9'
|
||||
/>
|
||||
<path
|
||||
d='M85.9 28.6c-.9 3.6-1.7 7.2-1.9 11-.3 8.4 1.8 16.2 6 23.5l32.9 56.9c1 1.7 1.8 3.4 2.8 5l-18.1 31.1-25.3 43.6c-.4 0-.5-.2-.6-.5-.1-.8.2-1.5.4-2.3 4.1-15 .7-28.3-9.6-39.7-6.3-6.9-14.3-10.8-23.5-12.1-12-1.7-22.6 1.4-32.1 8.9-1.7 1.3-2.8 3.2-4.8 4.2-.4 0-.6-.2-.7-.5l14.3-24.9L85.2 29.7c.2-.4.5-.7.7-1.1'
|
||||
fill='#fabc04'
|
||||
/>
|
||||
<path
|
||||
d='M11.8 158l5.7-5.1c24.3-19.2 60.8-5.3 66.1 25.1 1.3 7.3.6 14.3-1.6 21.3-.1.6-.2 1.1-.4 1.7-.9 1.6-1.7 3.3-2.7 4.9-8.9 14.7-22 22-39.2 20.9C20 225.4 4.5 210.6 1.8 191c-1.3-9.5.6-18.4 5.5-26.6 1-1.8 2.2-3.4 3.3-5.2.5-.4.3-1.2 1.2-1.2'
|
||||
fill='#34a852'
|
||||
/>
|
||||
<path d='M11.8 158c-.4.4-.4 1.1-1.1 1.2-.1-.7.3-1.1.7-1.6l.4.4' fill='#fabc04' />
|
||||
<path d='M81.6 201c-.4-.7 0-1.2.4-1.7l.4.4-.8 1.3' fill='#e1c025' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<path
|
||||
|
||||
@@ -40,8 +40,10 @@ import {
|
||||
ElasticsearchIcon,
|
||||
ElevenLabsIcon,
|
||||
EnrichSoIcon,
|
||||
EvernoteIcon,
|
||||
ExaAIIcon,
|
||||
EyeIcon,
|
||||
FathomIcon,
|
||||
FirecrawlIcon,
|
||||
FirefliesIcon,
|
||||
GammaIcon,
|
||||
@@ -49,6 +51,7 @@ import {
|
||||
GitLabIcon,
|
||||
GmailIcon,
|
||||
GongIcon,
|
||||
GoogleAdsIcon,
|
||||
GoogleBigQueryIcon,
|
||||
GoogleBooksIcon,
|
||||
GoogleCalendarIcon,
|
||||
@@ -103,6 +106,7 @@ import {
|
||||
MySQLIcon,
|
||||
Neo4jIcon,
|
||||
NotionIcon,
|
||||
ObsidianIcon,
|
||||
OnePasswordIcon,
|
||||
OpenAIIcon,
|
||||
OutlookIcon,
|
||||
@@ -202,7 +206,9 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
elevenlabs: ElevenLabsIcon,
|
||||
enrich: EnrichSoIcon,
|
||||
evernote: EvernoteIcon,
|
||||
exa: ExaAIIcon,
|
||||
fathom: FathomIcon,
|
||||
file_v3: DocumentIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
fireflies_v2: FirefliesIcon,
|
||||
@@ -211,6 +217,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
gitlab: GitLabIcon,
|
||||
gmail_v2: GmailIcon,
|
||||
gong: GongIcon,
|
||||
google_ads: GoogleAdsIcon,
|
||||
google_bigquery: GoogleBigQueryIcon,
|
||||
google_books: GoogleBooksIcon,
|
||||
google_calendar_v2: GoogleCalendarIcon,
|
||||
@@ -265,6 +272,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mysql: MySQLIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
notion_v2: NotionIcon,
|
||||
obsidian: ObsidianIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
onepassword: OnePasswordIcon,
|
||||
openai: OpenAIIcon,
|
||||
|
||||
@@ -22,6 +22,8 @@ With Ashby, you can:
|
||||
- **List and view jobs**: Browse all open, closed, and archived job postings with location and department info
|
||||
- **List applications**: View all applications across your organization with candidate and job details, status tracking, and pagination
|
||||
|
||||
The Ashby block also supports **webhook triggers** that automatically start workflows in response to Ashby events. Available triggers include Application Submitted, Candidate Stage Change, Candidate Hired, Candidate Deleted, Job Created, and Offer Created. Webhooks are fully managed — Sim automatically creates the webhook in Ashby when you save the trigger and deletes it when you remove it, so there's no manual webhook configuration needed. Just provide your Ashby API key (with `apiKeysWrite` permission) and select the event type.
|
||||
|
||||
In Sim, the Ashby integration enables your agents to programmatically manage your recruiting pipeline. Agents can search for candidates, create new candidate records, add notes after interviews, and monitor applications across jobs. This allows you to automate recruiting workflows like candidate intake, interview follow-ups, pipeline reporting, and cross-referencing candidates across roles.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
282
apps/docs/content/docs/en/tools/evernote.mdx
Normal file
282
apps/docs/content/docs/en/tools/evernote.mdx
Normal file
@@ -0,0 +1,282 @@
|
||||
---
|
||||
title: Evernote
|
||||
description: Manage notes, notebooks, and tags in Evernote
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="evernote"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Evernote](https://evernote.com/) is a note-taking and organization platform that helps individuals and teams capture ideas, manage projects, and store information across devices. With notebooks, tags, and powerful search, Evernote serves as a central hub for knowledge management.
|
||||
|
||||
With the Sim Evernote integration, you can:
|
||||
|
||||
- **Create and update notes**: Programmatically create new notes with content and tags, or update existing notes in any notebook.
|
||||
- **Search and retrieve notes**: Use Evernote's search grammar to find notes by keyword, tag, notebook, or other criteria, and retrieve full note content.
|
||||
- **Organize with notebooks and tags**: Create notebooks and tags, list existing ones, and move or copy notes between notebooks.
|
||||
- **Delete and manage notes**: Move notes to trash or copy them to different notebooks as part of automated workflows.
|
||||
|
||||
**How it works in Sim:**
|
||||
Add an Evernote block to your workflow and select an operation (e.g., create note, search notes, list notebooks). Provide your Evernote developer token and any required parameters. The block calls the Evernote API and returns structured data you can pass to downstream blocks — for example, searching for meeting notes and sending summaries to Slack, or creating notes from AI-generated content.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## 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 |
|
||||
|
||||
|
||||
150
apps/docs/content/docs/en/tools/fathom.mdx
Normal file
150
apps/docs/content/docs/en/tools/fathom.mdx
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Fathom
|
||||
description: Access meeting recordings, transcripts, and summaries
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="fathom"
|
||||
color="#181C1E"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Fathom](https://fathom.video/) is an AI meeting assistant that automatically records, transcribes, and summarizes your video calls. It works across platforms like Zoom, Google Meet, and Microsoft Teams, generating highlights and action items so your team can stay focused during meetings and catch up quickly afterward.
|
||||
|
||||
With the Sim Fathom integration, you can:
|
||||
|
||||
- **List and filter meetings**: Retrieve recent meetings recorded by you or shared with your team, with optional filters by date range, recorder, or team.
|
||||
- **Get meeting summaries**: Pull structured, markdown-formatted summaries for any recorded meeting to quickly review key discussion points.
|
||||
- **Access full transcripts**: Retrieve complete transcripts with speaker attribution and timestamps for detailed review or downstream processing.
|
||||
- **Manage teams and members**: List teams in your Fathom organization and view team member details to coordinate meeting workflows.
|
||||
|
||||
**How it works in Sim:**
|
||||
Add a Fathom block to your workflow and select an operation. Provide your Fathom API key and any required parameters (such as a recording ID for summaries and transcripts). The block calls the Fathom API and returns structured data you can pass to downstream blocks — for example, sending a summary to Slack or extracting action items with an AI agent.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Fathom AI Notetaker into your workflow. List meetings, get transcripts and summaries, and manage team members and teams. Can also trigger workflows when new meeting content is ready.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `fathom_list_meetings`
|
||||
|
||||
List recent meetings recorded by the user or shared to their team.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Fathom API Key |
|
||||
| `includeSummary` | string | No | Include meeting summary \(true/false\) |
|
||||
| `includeTranscript` | string | No | Include meeting transcript \(true/false\) |
|
||||
| `includeActionItems` | string | No | Include action items \(true/false\) |
|
||||
| `includeCrmMatches` | string | No | Include linked CRM matches \(true/false\) |
|
||||
| `createdAfter` | string | No | Filter meetings created after this ISO 8601 timestamp |
|
||||
| `createdBefore` | string | No | Filter meetings created before this ISO 8601 timestamp |
|
||||
| `recordedBy` | string | No | Filter by recorder email address |
|
||||
| `teams` | string | No | Filter by team name |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `meetings` | array | List of meetings |
|
||||
| ↳ `title` | string | Meeting title |
|
||||
| ↳ `recording_id` | number | Unique recording ID |
|
||||
| ↳ `url` | string | URL to view the meeting |
|
||||
| ↳ `share_url` | string | Shareable URL |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
| ↳ `transcript_language` | string | Transcript language |
|
||||
| `next_cursor` | string | Pagination cursor for next page |
|
||||
|
||||
### `fathom_get_summary`
|
||||
|
||||
Get the call summary for a specific meeting recording.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Fathom API Key |
|
||||
| `recordingId` | string | Yes | The recording ID of the meeting |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `template_name` | string | Name of the summary template used |
|
||||
| `markdown_formatted` | string | Markdown-formatted summary text |
|
||||
|
||||
### `fathom_get_transcript`
|
||||
|
||||
Get the full transcript for a specific meeting recording.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Fathom API Key |
|
||||
| `recordingId` | string | Yes | The recording ID of the meeting |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `transcript` | array | Array of transcript entries with speaker, text, and timestamp |
|
||||
| ↳ `speaker` | object | Speaker information |
|
||||
| ↳ `display_name` | string | Speaker display name |
|
||||
| ↳ `matched_calendar_invitee_email` | string | Matched calendar invitee email |
|
||||
| ↳ `text` | string | Transcript text |
|
||||
| ↳ `timestamp` | string | Timestamp \(HH:MM:SS\) |
|
||||
|
||||
### `fathom_list_team_members`
|
||||
|
||||
List team members in your Fathom organization.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Fathom API Key |
|
||||
| `teams` | string | No | Team name to filter by |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `members` | array | List of team members |
|
||||
| ↳ `name` | string | Team member name |
|
||||
| ↳ `email` | string | Team member email |
|
||||
| ↳ `created_at` | string | Date the member was added |
|
||||
| `next_cursor` | string | Pagination cursor for next page |
|
||||
|
||||
### `fathom_list_teams`
|
||||
|
||||
List teams in your Fathom organization.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Fathom API Key |
|
||||
| `cursor` | string | No | Pagination cursor from a previous response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `teams` | array | List of teams |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `created_at` | string | Date the team was created |
|
||||
| `next_cursor` | string | Pagination cursor for next page |
|
||||
|
||||
|
||||
192
apps/docs/content/docs/en/tools/google_ads.mdx
Normal file
192
apps/docs/content/docs/en/tools/google_ads.mdx
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Google Ads
|
||||
description: Query campaigns, ad groups, and performance metrics
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="google_ads"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Google Ads](https://ads.google.com) is Google's online advertising platform that lets businesses create ads to reach customers across Google Search, YouTube, Gmail, and millions of partner websites. It supports campaign types including Search, Display, Video, Shopping, and Performance Max, with detailed targeting, bidding strategies, and performance analytics.
|
||||
|
||||
In Sim, the Google Ads integration enables your agents to query campaign data, monitor ad group performance, and pull detailed metrics using the Google Ads Query Language (GAQL). This supports use cases such as automated performance reporting, budget monitoring, campaign health checks, and data-driven optimization workflows. By connecting Sim with Google Ads, your agents can retrieve real-time advertising data and act on insights without manual dashboard navigation.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to Google Ads to list accessible accounts, list campaigns, view ad group details, get performance metrics, and run custom GAQL queries.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_ads_list_customers`
|
||||
|
||||
List all Google Ads customer accounts accessible by the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `developerToken` | string | Yes | Google Ads API developer token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `customerIds` | array | List of accessible customer IDs |
|
||||
| `totalCount` | number | Total number of accessible customer accounts |
|
||||
|
||||
### `google_ads_search`
|
||||
|
||||
Run a custom Google Ads Query Language (GAQL) query
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
|
||||
| `developerToken` | string | Yes | Google Ads API developer token |
|
||||
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
|
||||
| `query` | string | Yes | GAQL query to execute |
|
||||
| `pageToken` | string | No | Page token for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Array of result objects from the GAQL query |
|
||||
| `totalResultsCount` | number | Total number of matching results |
|
||||
| `nextPageToken` | string | Token for the next page of results |
|
||||
|
||||
### `google_ads_list_campaigns`
|
||||
|
||||
List campaigns in a Google Ads account with optional status filtering
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
|
||||
| `developerToken` | string | Yes | Google Ads API developer token |
|
||||
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
|
||||
| `status` | string | No | Filter by campaign status \(ENABLED, PAUSED, REMOVED\) |
|
||||
| `limit` | number | No | Maximum number of campaigns to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `campaigns` | array | List of campaigns in the account |
|
||||
| ↳ `id` | string | Campaign ID |
|
||||
| ↳ `name` | string | Campaign name |
|
||||
| ↳ `status` | string | Campaign status \(ENABLED, PAUSED, REMOVED\) |
|
||||
| ↳ `channelType` | string | Advertising channel type \(SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX\) |
|
||||
| ↳ `startDate` | string | Campaign start date \(YYYY-MM-DD\) |
|
||||
| ↳ `endDate` | string | Campaign end date \(YYYY-MM-DD\) |
|
||||
| ↳ `budgetAmountMicros` | string | Daily budget in micros \(divide by 1,000,000 for currency value\) |
|
||||
| `totalCount` | number | Total number of campaigns returned |
|
||||
|
||||
### `google_ads_campaign_performance`
|
||||
|
||||
Get performance metrics for Google Ads campaigns over a date range
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
|
||||
| `developerToken` | string | Yes | Google Ads API developer token |
|
||||
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
|
||||
| `campaignId` | string | No | Filter by specific campaign ID |
|
||||
| `dateRange` | string | No | Predefined date range \(LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY\) |
|
||||
| `startDate` | string | No | Custom start date in YYYY-MM-DD format |
|
||||
| `endDate` | string | No | Custom end date in YYYY-MM-DD format |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `campaigns` | array | Campaign performance data broken down by date |
|
||||
| ↳ `id` | string | Campaign ID |
|
||||
| ↳ `name` | string | Campaign name |
|
||||
| ↳ `status` | string | Campaign status |
|
||||
| ↳ `impressions` | string | Number of impressions |
|
||||
| ↳ `clicks` | string | Number of clicks |
|
||||
| ↳ `costMicros` | string | Cost in micros \(divide by 1,000,000 for currency value\) |
|
||||
| ↳ `ctr` | number | Click-through rate \(0.0 to 1.0\) |
|
||||
| ↳ `conversions` | number | Number of conversions |
|
||||
| ↳ `date` | string | Date for this row \(YYYY-MM-DD\) |
|
||||
| `totalCount` | number | Total number of result rows |
|
||||
|
||||
### `google_ads_list_ad_groups`
|
||||
|
||||
List ad groups in a Google Ads campaign
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
|
||||
| `developerToken` | string | Yes | Google Ads API developer token |
|
||||
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
|
||||
| `campaignId` | string | Yes | Campaign ID to list ad groups for |
|
||||
| `status` | string | No | Filter by ad group status \(ENABLED, PAUSED, REMOVED\) |
|
||||
| `limit` | number | No | Maximum number of ad groups to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `adGroups` | array | List of ad groups in the campaign |
|
||||
| ↳ `id` | string | Ad group ID |
|
||||
| ↳ `name` | string | Ad group name |
|
||||
| ↳ `status` | string | Ad group status \(ENABLED, PAUSED, REMOVED\) |
|
||||
| ↳ `type` | string | Ad group type \(SEARCH_STANDARD, DISPLAY_STANDARD, SHOPPING_PRODUCT_ADS\) |
|
||||
| ↳ `campaignId` | string | Parent campaign ID |
|
||||
| ↳ `campaignName` | string | Parent campaign name |
|
||||
| `totalCount` | number | Total number of ad groups returned |
|
||||
|
||||
### `google_ads_ad_performance`
|
||||
|
||||
Get performance metrics for individual ads over a date range
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) |
|
||||
| `developerToken` | string | Yes | Google Ads API developer token |
|
||||
| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) |
|
||||
| `campaignId` | string | No | Filter by campaign ID |
|
||||
| `adGroupId` | string | No | Filter by ad group ID |
|
||||
| `dateRange` | string | No | Predefined date range \(LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY\) |
|
||||
| `startDate` | string | No | Custom start date in YYYY-MM-DD format |
|
||||
| `endDate` | string | No | Custom end date in YYYY-MM-DD format |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ads` | array | Ad performance data broken down by date |
|
||||
| ↳ `adId` | string | Ad ID |
|
||||
| ↳ `adGroupId` | string | Parent ad group ID |
|
||||
| ↳ `adGroupName` | string | Parent ad group name |
|
||||
| ↳ `campaignId` | string | Parent campaign ID |
|
||||
| ↳ `campaignName` | string | Parent campaign name |
|
||||
| ↳ `adType` | string | Ad type \(RESPONSIVE_SEARCH_AD, EXPANDED_TEXT_AD, etc.\) |
|
||||
| ↳ `impressions` | string | Number of impressions |
|
||||
| ↳ `clicks` | string | Number of clicks |
|
||||
| ↳ `costMicros` | string | Cost in micros \(divide by 1,000,000 for currency value\) |
|
||||
| ↳ `ctr` | number | Click-through rate \(0.0 to 1.0\) |
|
||||
| ↳ `conversions` | number | Number of conversions |
|
||||
| ↳ `date` | string | Date for this row \(YYYY-MM-DD\) |
|
||||
| `totalCount` | number | Total number of result rows |
|
||||
|
||||
|
||||
@@ -1014,4 +1014,36 @@ 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 |
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"elasticsearch",
|
||||
"elevenlabs",
|
||||
"enrich",
|
||||
"evernote",
|
||||
"exa",
|
||||
"fathom",
|
||||
"file",
|
||||
"firecrawl",
|
||||
"fireflies",
|
||||
@@ -44,6 +46,7 @@
|
||||
"gitlab",
|
||||
"gmail",
|
||||
"gong",
|
||||
"google_ads",
|
||||
"google_bigquery",
|
||||
"google_books",
|
||||
"google_calendar",
|
||||
@@ -98,6 +101,7 @@
|
||||
"mysql",
|
||||
"neo4j",
|
||||
"notion",
|
||||
"obsidian",
|
||||
"onedrive",
|
||||
"onepassword",
|
||||
"openai",
|
||||
|
||||
339
apps/docs/content/docs/en/tools/obsidian.mdx
Normal file
339
apps/docs/content/docs/en/tools/obsidian.mdx
Normal file
@@ -0,0 +1,339 @@
|
||||
---
|
||||
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"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Obsidian](https://obsidian.md/) is a powerful knowledge base and note-taking application that works on top of a local folder of plain-text Markdown files. With features like bidirectional linking, graph views, and a rich plugin ecosystem, Obsidian is widely used for personal knowledge management, research, and documentation.
|
||||
|
||||
With the Sim Obsidian integration, you can:
|
||||
|
||||
- **Read and create notes**: Retrieve note content from your vault or create new notes programmatically as part of automated workflows.
|
||||
- **Update and patch notes**: Modify existing notes in full or patch content at specific locations within a note.
|
||||
- **Search your vault**: Find notes by keyword or content across your entire Obsidian vault.
|
||||
- **Manage periodic notes**: Access and create daily or other periodic notes for journaling and task tracking.
|
||||
- **Execute commands**: Trigger Obsidian commands remotely to automate vault operations.
|
||||
|
||||
**How it works in Sim:**
|
||||
Add an Obsidian block to your workflow and select an operation. This integration requires the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin to be installed and running in your vault. Provide your API key and vault URL, along with any required parameters. The block communicates with your local Obsidian instance and returns structured data you can pass to downstream blocks — for example, searching your vault for research notes and feeding them into an AI agent for summarization.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## 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 |
|
||||
|
||||
|
||||
@@ -44,20 +44,24 @@ Search the web using Parallel AI. Provides comprehensive search results with int
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `objective` | string | Yes | The search objective or question to answer |
|
||||
| `search_queries` | string | No | Optional comma-separated list of search queries to execute |
|
||||
| `processor` | string | No | Processing method: base or pro \(default: base\) |
|
||||
| `max_results` | number | No | Maximum number of results to return \(default: 5\) |
|
||||
| `max_chars_per_result` | number | No | Maximum characters per result \(default: 1500\) |
|
||||
| `search_queries` | string | No | Comma-separated list of search queries to execute |
|
||||
| `mode` | string | No | Search mode: one-shot, agentic, or fast \(default: one-shot\) |
|
||||
| `max_results` | number | No | Maximum number of results to return \(default: 10\) |
|
||||
| `max_chars_per_result` | number | No | Maximum characters per result excerpt \(minimum: 1000\) |
|
||||
| `include_domains` | string | No | Comma-separated list of domains to restrict search results to |
|
||||
| `exclude_domains` | string | No | Comma-separated list of domains to exclude from search results |
|
||||
| `apiKey` | string | Yes | Parallel AI API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `search_id` | string | Unique identifier for this search request |
|
||||
| `results` | array | Search results with excerpts from relevant pages |
|
||||
| ↳ `url` | string | The URL of the search result |
|
||||
| ↳ `title` | string | The title of the search result |
|
||||
| ↳ `excerpts` | array | Text excerpts from the page |
|
||||
| ↳ `publish_date` | string | Publication date of the page \(YYYY-MM-DD\) |
|
||||
| ↳ `excerpts` | array | LLM-optimized excerpts from the page |
|
||||
|
||||
### `parallel_extract`
|
||||
|
||||
@@ -68,31 +72,33 @@ Extract targeted information from specific URLs using Parallel AI. Processes pro
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `urls` | string | Yes | Comma-separated list of URLs to extract information from |
|
||||
| `objective` | string | Yes | What information to extract from the provided URLs |
|
||||
| `excerpts` | boolean | Yes | Include relevant excerpts from the content |
|
||||
| `full_content` | boolean | Yes | Include full page content |
|
||||
| `objective` | string | No | What information to extract from the provided URLs |
|
||||
| `excerpts` | boolean | No | Include relevant excerpts from the content \(default: true\) |
|
||||
| `full_content` | boolean | No | Include full page content as markdown \(default: false\) |
|
||||
| `apiKey` | string | Yes | Parallel AI API Key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `extract_id` | string | Unique identifier for this extraction request |
|
||||
| `results` | array | Extracted information from the provided URLs |
|
||||
| ↳ `url` | string | The source URL |
|
||||
| ↳ `title` | string | The title of the page |
|
||||
| ↳ `content` | string | Extracted content |
|
||||
| ↳ `excerpts` | array | Relevant text excerpts |
|
||||
| ↳ `publish_date` | string | Publication date \(YYYY-MM-DD\) |
|
||||
| ↳ `excerpts` | array | Relevant text excerpts in markdown |
|
||||
| ↳ `full_content` | string | Full page content as markdown |
|
||||
|
||||
### `parallel_deep_research`
|
||||
|
||||
Conduct comprehensive deep research across the web using Parallel AI. Synthesizes information from multiple sources with citations. Can take up to 15 minutes to complete.
|
||||
Conduct comprehensive deep research across the web using Parallel AI. Synthesizes information from multiple sources with citations. Can take up to 45 minutes to complete.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `input` | string | Yes | Research query or question \(up to 15,000 characters\) |
|
||||
| `processor` | string | No | Compute level: base, lite, pro, ultra, ultra2x, ultra4x, ultra8x \(default: base\) |
|
||||
| `processor` | string | No | Processing tier: pro, ultra, pro-fast, ultra-fast \(default: pro\) |
|
||||
| `include_domains` | string | No | Comma-separated list of domains to restrict research to \(source policy\) |
|
||||
| `exclude_domains` | string | No | Comma-separated list of domains to exclude from research \(source policy\) |
|
||||
| `apiKey` | string | Yes | Parallel AI API Key |
|
||||
@@ -101,17 +107,17 @@ Conduct comprehensive deep research across the web using Parallel AI. Synthesize
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `status` | string | Task status \(completed, failed\) |
|
||||
| `status` | string | Task status \(completed, failed, running\) |
|
||||
| `run_id` | string | Unique ID for this research task |
|
||||
| `message` | string | Status message |
|
||||
| `content` | object | Research results \(structured based on output_schema\) |
|
||||
| `basis` | array | Citations and sources with reasoning and confidence levels |
|
||||
| ↳ `field` | string | Output field name |
|
||||
| ↳ `field` | string | Output field dot-notation path |
|
||||
| ↳ `reasoning` | string | Explanation for the result |
|
||||
| ↳ `citations` | array | Array of sources |
|
||||
| ↳ `url` | string | Source URL |
|
||||
| ↳ `title` | string | Source title |
|
||||
| ↳ `excerpts` | array | Relevant excerpts from the source |
|
||||
| ↳ `confidence` | string | Confidence level indicator |
|
||||
| ↳ `confidence` | string | Confidence level \(high, medium\) |
|
||||
|
||||
|
||||
|
||||
@@ -590,6 +590,7 @@ List all users in a Slack workspace. Returns user profiles with names and avatar
|
||||
| ↳ `name` | string | Username \(handle\) |
|
||||
| ↳ `real_name` | string | Full real name |
|
||||
| ↳ `display_name` | string | Display name shown in Slack |
|
||||
| ↳ `email` | string | Email address \(requires users:read.email scope\) |
|
||||
| ↳ `is_bot` | boolean | Whether the user is a bot |
|
||||
| ↳ `is_admin` | boolean | Whether the user is a workspace admin |
|
||||
| ↳ `is_owner` | boolean | Whether the user is the workspace owner |
|
||||
@@ -629,6 +630,7 @@ Get detailed information about a specific Slack user by their user ID.
|
||||
| ↳ `title` | string | Job title |
|
||||
| ↳ `phone` | string | Phone number |
|
||||
| ↳ `skype` | string | Skype handle |
|
||||
| ↳ `email` | string | Email address \(requires users:read.email scope\) |
|
||||
| ↳ `is_bot` | boolean | Whether the user is a bot |
|
||||
| ↳ `is_admin` | boolean | Whether the user is a workspace admin |
|
||||
| ↳ `is_owner` | boolean | Whether the user is the workspace owner |
|
||||
|
||||
@@ -13,13 +13,12 @@ import {
|
||||
isTerminalState,
|
||||
parseWorkflowSSEChunk,
|
||||
} from '@/lib/a2a/utils'
|
||||
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
|
||||
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 {
|
||||
@@ -243,9 +242,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
||||
|
||||
const { id, method, params: rpcParams } = body
|
||||
const requestApiKey = request.headers.get('X-API-Key')
|
||||
const apiKey = authenticatedAuthType === 'api_key' ? requestApiKey : null
|
||||
const apiKey = authenticatedAuthType === AuthType.API_KEY ? requestApiKey : null
|
||||
const isPersonalApiKeyCaller =
|
||||
authenticatedAuthType === 'api_key' && authenticatedApiKeyType === 'personal'
|
||||
authenticatedAuthType === AuthType.API_KEY && authenticatedApiKeyType === 'personal'
|
||||
const billedUserId = await getWorkspaceBilledAccountUserId(agent.workspaceId)
|
||||
if (!billedUserId) {
|
||||
logger.error('Unable to resolve workspace billed account for A2A execution', {
|
||||
@@ -631,11 +630,9 @@ 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 = {
|
||||
@@ -845,19 +842,10 @@ async function handleMessageStream(
|
||||
})
|
||||
} finally {
|
||||
await releaseLock(lockKey, lockValue)
|
||||
if (!messageStreamDecremented) {
|
||||
messageStreamDecremented = true
|
||||
decrementSSEConnections('a2a-message')
|
||||
}
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
if (!messageStreamDecremented) {
|
||||
messageStreamDecremented = true
|
||||
decrementSSEConnections('a2a-message')
|
||||
}
|
||||
},
|
||||
cancel() {},
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
@@ -1042,22 +1030,16 @@ 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 {
|
||||
|
||||
@@ -24,6 +24,7 @@ const { mockCheckSessionOrInternalAuth, mockLogger } = vi.hoisted(() => {
|
||||
})
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ vi.mock('@/lib/auth/credential-access', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkHybridAuth: vi.fn(),
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: vi.fn(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||
if (!auth.success || auth.authType !== AuthType.SESSION || !auth.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
|
||||
success: auth.success,
|
||||
authType: auth.authType,
|
||||
@@ -202,7 +202,7 @@ export async function GET(request: NextRequest) {
|
||||
credentialId,
|
||||
requireWorkflowIdForInternal: false,
|
||||
})
|
||||
if (!authz.ok || authz.authType !== 'session' || !authz.credentialOwnerUserId) {
|
||||
if (!authz.ok || authz.authType !== AuthType.SESSION || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ vi.mock('@/lib/auth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkHybridAuth: mocks.mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: mocks.mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: mocks.mockCheckInternalAuth,
|
||||
|
||||
@@ -106,6 +106,7 @@ vi.mock('@/lib/auth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkInternalAuth: mockCheckInternalAuth,
|
||||
checkHybridAuth: mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
|
||||
@@ -49,6 +49,7 @@ vi.mock('fs/promises', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ vi.mock('@/lib/auth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkHybridAuth: mocks.mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: mocks.mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: mocks.mockCheckInternalAuth,
|
||||
|
||||
@@ -18,6 +18,7 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkInternalAuth: mockCheckInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
||||
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
||||
@@ -25,7 +25,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
// For session auth, verify KB access. Internal JWT is trusted.
|
||||
if (auth.authType === 'session' && auth.userId) {
|
||||
if (auth.authType === AuthType.SESSION && auth.userId) {
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
@@ -62,7 +62,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
// For session auth, verify KB access. Internal JWT is trusted.
|
||||
if (auth.authType === 'session' && auth.userId) {
|
||||
if (auth.authType === AuthType.SESSION && auth.userId) {
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
|
||||
@@ -68,6 +68,7 @@ vi.mock('@sim/db', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ 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')
|
||||
@@ -50,14 +49,11 @@ 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 {
|
||||
|
||||
@@ -59,6 +59,7 @@ vi.mock('@sim/db/schema', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkHybridAuth: mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: vi.fn(),
|
||||
checkInternalAuth: vi.fn(),
|
||||
|
||||
@@ -19,7 +19,7 @@ import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { type AuthResult, AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateInternalToken } from '@/lib/auth/internal'
|
||||
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
|
||||
@@ -137,7 +137,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
||||
executeAuthContext = {
|
||||
authType: auth.authType,
|
||||
userId: auth.userId,
|
||||
apiKey: auth.authType === 'api_key' ? request.headers.get('X-API-Key') : null,
|
||||
apiKey: auth.authType === AuthType.API_KEY ? request.headers.get('X-API-Key') : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ async function handleToolsCall(
|
||||
const internalToken = await generateInternalToken(publicServerOwnerId)
|
||||
headers.Authorization = `Bearer ${internalToken}`
|
||||
} else if (executeAuthContext) {
|
||||
if (executeAuthContext.authType === 'api_key' && executeAuthContext.apiKey) {
|
||||
if (executeAuthContext.authType === AuthType.API_KEY && executeAuthContext.apiKey) {
|
||||
headers['X-API-Key'] = executeAuthContext.apiKey
|
||||
} else {
|
||||
const internalToken = await generateInternalToken(executeAuthContext.userId)
|
||||
|
||||
@@ -192,7 +192,8 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
)
|
||||
} catch (error) {
|
||||
connectionStatus = 'error'
|
||||
lastError = error instanceof Error ? error.message : 'Connection test failed'
|
||||
lastError =
|
||||
error instanceof Error ? error.message.split('\n')[0].slice(0, 200) : 'Connection failed'
|
||||
logger.warn(`[${requestId}] Failed to connect to server ${serverId}:`, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,20 @@ interface TestConnectionResult {
|
||||
warnings?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a user-friendly error message from connection errors.
|
||||
* Keeps diagnostic info (timeout, DNS, HTTP status) but strips
|
||||
* verbose internals (Zod details, full response bodies, stack traces).
|
||||
*/
|
||||
function sanitizeConnectionError(error: unknown): string {
|
||||
if (!(error instanceof Error)) {
|
||||
return 'Unknown connection error'
|
||||
}
|
||||
|
||||
const firstLine = error.message.split('\n')[0]
|
||||
return firstLine.length > 200 ? `${firstLine.slice(0, 200)}...` : firstLine
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Test connection to an MCP server before registering it
|
||||
*/
|
||||
@@ -137,8 +151,7 @@ export const POST = withMcpAuth('write')(
|
||||
} catch (toolError) {
|
||||
logger.warn(`[${requestId}] Connection established but could not list tools:`, toolError)
|
||||
result.success = false
|
||||
const errorMessage = toolError instanceof Error ? toolError.message : 'Unknown error'
|
||||
result.error = `Connection established but could not list tools: ${errorMessage}`
|
||||
result.error = 'Connection established but could not list tools'
|
||||
result.warnings = result.warnings || []
|
||||
result.warnings.push(
|
||||
'Server connected but tool listing failed - connection may be incomplete'
|
||||
@@ -163,11 +176,7 @@ export const POST = withMcpAuth('write')(
|
||||
logger.warn(`[${requestId}] MCP server test failed:`, error)
|
||||
|
||||
result.success = false
|
||||
if (error instanceof Error) {
|
||||
result.error = error.message
|
||||
} else {
|
||||
result.error = 'Unknown connection error'
|
||||
}
|
||||
result.error = sanitizeConnectionError(error)
|
||||
} finally {
|
||||
if (client) {
|
||||
try {
|
||||
|
||||
@@ -89,11 +89,12 @@ export const POST = withMcpAuth('read')(
|
||||
tool = tools.find((t) => t.name === toolName) ?? null
|
||||
|
||||
if (!tool) {
|
||||
logger.warn(`[${requestId}] Tool ${toolName} not found on server ${serverId}`, {
|
||||
availableTools: tools.map((t) => t.name),
|
||||
})
|
||||
return createMcpErrorResponse(
|
||||
new Error(
|
||||
`Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}`
|
||||
),
|
||||
'Tool not found',
|
||||
new Error('Tool not found'),
|
||||
'Tool not found on the specified server',
|
||||
404
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuthType } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
||||
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||
@@ -39,7 +40,7 @@ export async function POST(
|
||||
|
||||
const resumeInput = payload?.input ?? payload ?? {}
|
||||
const isPersonalApiKeyCaller =
|
||||
access.auth?.authType === 'api_key' && access.auth?.apiKeyType === 'personal'
|
||||
access.auth?.authType === AuthType.API_KEY && access.auth?.apiKeyType === 'personal'
|
||||
|
||||
let userId: string
|
||||
if (isPersonalApiKeyCaller && access.auth?.userId) {
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to cancel task',
|
||||
error: 'Failed to cancel task',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete push notification',
|
||||
error: 'Failed to delete push notification',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch Agent Card',
|
||||
error: 'Failed to fetch Agent Card',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -107,7 +107,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get push notification',
|
||||
error: 'Failed to get push notification',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to get task',
|
||||
error: 'Failed to get task',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -111,7 +111,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to resubscribe',
|
||||
error: 'Failed to resubscribe',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Failed to connect to agent: ${clientError instanceof Error ? clientError.message : 'Unknown error'}`,
|
||||
error: 'Failed to connect to agent',
|
||||
},
|
||||
{ status: 502 }
|
||||
)
|
||||
@@ -158,7 +158,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Failed to send message: ${sendError instanceof Error ? sendError.message : 'Unknown error'}`,
|
||||
error: 'Failed to send message to agent',
|
||||
},
|
||||
{ status: 502 }
|
||||
)
|
||||
@@ -218,7 +218,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Internal server error',
|
||||
error: 'Internal server error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to set push notification',
|
||||
error: 'Failed to set push notification',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
|
||||
@@ -182,6 +182,7 @@ vi.mock('@/lib/auth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
|
||||
}))
|
||||
|
||||
|
||||
38
apps/sim/app/api/tools/evernote/copy-note/route.ts
Normal file
38
apps/sim/app/api/tools/evernote/copy-note/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { copyNote } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteCopyNoteAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, noteGuid, toNotebookGuid } = body
|
||||
|
||||
if (!apiKey || !noteGuid || !toNotebookGuid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey, noteGuid, and toNotebookGuid are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const note = await copyNote(apiKey, noteGuid, toNotebookGuid)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { note },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to copy note', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
51
apps/sim/app/api/tools/evernote/create-note/route.ts
Normal file
51
apps/sim/app/api/tools/evernote/create-note/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createNote } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteCreateNoteAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, title, content, notebookGuid, tagNames } = body
|
||||
|
||||
if (!apiKey || !title || !content) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey, title, and content are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const parsedTags = tagNames
|
||||
? (() => {
|
||||
const tags =
|
||||
typeof tagNames === 'string'
|
||||
? tagNames
|
||||
.split(',')
|
||||
.map((t: string) => t.trim())
|
||||
.filter(Boolean)
|
||||
: tagNames
|
||||
return tags.length > 0 ? tags : undefined
|
||||
})()
|
||||
: undefined
|
||||
|
||||
const note = await createNote(apiKey, title, content, notebookGuid || undefined, parsedTags)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { note },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to create note', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
38
apps/sim/app/api/tools/evernote/create-notebook/route.ts
Normal file
38
apps/sim/app/api/tools/evernote/create-notebook/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createNotebook } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteCreateNotebookAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, name, stack } = body
|
||||
|
||||
if (!apiKey || !name) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and name are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const notebook = await createNotebook(apiKey, name, stack || undefined)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { notebook },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to create notebook', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
38
apps/sim/app/api/tools/evernote/create-tag/route.ts
Normal file
38
apps/sim/app/api/tools/evernote/create-tag/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { createTag } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteCreateTagAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, name, parentGuid } = body
|
||||
|
||||
if (!apiKey || !name) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and name are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const tag = await createTag(apiKey, name, parentGuid || undefined)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { tag },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to create tag', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
41
apps/sim/app/api/tools/evernote/delete-note/route.ts
Normal file
41
apps/sim/app/api/tools/evernote/delete-note/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { deleteNote } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteDeleteNoteAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, noteGuid } = body
|
||||
|
||||
if (!apiKey || !noteGuid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and noteGuid are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
await deleteNote(apiKey, noteGuid)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
noteGuid,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to delete note', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
38
apps/sim/app/api/tools/evernote/get-note/route.ts
Normal file
38
apps/sim/app/api/tools/evernote/get-note/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { getNote } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteGetNoteAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, noteGuid, withContent = true } = body
|
||||
|
||||
if (!apiKey || !noteGuid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and noteGuid are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const note = await getNote(apiKey, noteGuid, withContent)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { note },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to get note', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
38
apps/sim/app/api/tools/evernote/get-notebook/route.ts
Normal file
38
apps/sim/app/api/tools/evernote/get-notebook/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { getNotebook } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteGetNotebookAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, notebookGuid } = body
|
||||
|
||||
if (!apiKey || !notebookGuid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and notebookGuid are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const notebook = await getNotebook(apiKey, notebookGuid)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { notebook },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to get notebook', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
799
apps/sim/app/api/tools/evernote/lib/client.ts
Normal file
799
apps/sim/app/api/tools/evernote/lib/client.ts
Normal file
@@ -0,0 +1,799 @@
|
||||
/**
|
||||
* Evernote API client using Thrift binary protocol over HTTP.
|
||||
* Implements only the NoteStore methods needed for the integration.
|
||||
*/
|
||||
|
||||
import {
|
||||
ThriftReader,
|
||||
ThriftWriter,
|
||||
TYPE_BOOL,
|
||||
TYPE_I32,
|
||||
TYPE_I64,
|
||||
TYPE_LIST,
|
||||
TYPE_STRING,
|
||||
TYPE_STRUCT,
|
||||
} from './thrift'
|
||||
|
||||
export interface EvernoteNotebook {
|
||||
guid: string
|
||||
name: string
|
||||
defaultNotebook: boolean
|
||||
serviceCreated: number | null
|
||||
serviceUpdated: number | null
|
||||
stack: string | null
|
||||
}
|
||||
|
||||
export interface EvernoteNote {
|
||||
guid: string
|
||||
title: string
|
||||
content: string | null
|
||||
contentLength: number | null
|
||||
created: number | null
|
||||
updated: number | null
|
||||
deleted: number | null
|
||||
active: boolean
|
||||
notebookGuid: string | null
|
||||
tagGuids: string[]
|
||||
tagNames: string[]
|
||||
}
|
||||
|
||||
export interface EvernoteNoteMetadata {
|
||||
guid: string
|
||||
title: string | null
|
||||
contentLength: number | null
|
||||
created: number | null
|
||||
updated: number | null
|
||||
notebookGuid: string | null
|
||||
tagGuids: string[]
|
||||
}
|
||||
|
||||
export interface EvernoteTag {
|
||||
guid: string
|
||||
name: string
|
||||
parentGuid: string | null
|
||||
updateSequenceNum: number | null
|
||||
}
|
||||
|
||||
export interface EvernoteSearchResult {
|
||||
startIndex: number
|
||||
totalNotes: number
|
||||
notes: EvernoteNoteMetadata[]
|
||||
}
|
||||
|
||||
/** Extract shard ID from an Evernote developer token */
|
||||
function extractShardId(token: string): string {
|
||||
const match = token.match(/S=s(\d+)/)
|
||||
if (!match) {
|
||||
throw new Error('Invalid Evernote token format: cannot extract shard ID')
|
||||
}
|
||||
return `s${match[1]}`
|
||||
}
|
||||
|
||||
/** Get the NoteStore URL for the given token */
|
||||
function getNoteStoreUrl(token: string): string {
|
||||
const shardId = extractShardId(token)
|
||||
const host = token.includes(':Sandbox') ? 'sandbox.evernote.com' : 'www.evernote.com'
|
||||
return `https://${host}/shard/${shardId}/notestore`
|
||||
}
|
||||
|
||||
/** Make a Thrift RPC call to the NoteStore */
|
||||
async function callNoteStore(token: string, writer: ThriftWriter): Promise<ThriftReader> {
|
||||
const url = getNoteStoreUrl(token)
|
||||
const body = writer.toBuffer()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-thrift',
|
||||
Accept: 'application/x-thrift',
|
||||
},
|
||||
body: new Uint8Array(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Evernote API HTTP error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const reader = new ThriftReader(arrayBuffer)
|
||||
const msg = reader.readMessageBegin()
|
||||
|
||||
if (reader.isException(msg.type)) {
|
||||
const ex = reader.readException()
|
||||
throw new Error(`Evernote API error: ${ex.message}`)
|
||||
}
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
/** Check for Evernote-specific exceptions in the response struct. Returns true if handled. */
|
||||
function checkEvernoteException(reader: ThriftReader, fieldId: number, fieldType: number): boolean {
|
||||
if (fieldId === 1 && fieldType === TYPE_STRUCT) {
|
||||
let message = ''
|
||||
let errorCode = 0
|
||||
reader.readStruct((r, fid, ftype) => {
|
||||
if (fid === 1 && ftype === TYPE_I32) {
|
||||
errorCode = r.readI32()
|
||||
} else if (fid === 2 && ftype === TYPE_STRING) {
|
||||
message = r.readString()
|
||||
} else {
|
||||
r.skip(ftype)
|
||||
}
|
||||
})
|
||||
throw new Error(`Evernote error (${errorCode}): ${message}`)
|
||||
}
|
||||
if (fieldId === 2 && fieldType === TYPE_STRUCT) {
|
||||
let message = ''
|
||||
let errorCode = 0
|
||||
reader.readStruct((r, fid, ftype) => {
|
||||
if (fid === 1 && ftype === TYPE_I32) {
|
||||
errorCode = r.readI32()
|
||||
} else if (fid === 2 && ftype === TYPE_STRING) {
|
||||
message = r.readString()
|
||||
} else {
|
||||
r.skip(ftype)
|
||||
}
|
||||
})
|
||||
throw new Error(`Evernote system error (${errorCode}): ${message}`)
|
||||
}
|
||||
if (fieldId === 3 && fieldType === TYPE_STRUCT) {
|
||||
let identifier = ''
|
||||
let key = ''
|
||||
reader.readStruct((r, fid, ftype) => {
|
||||
if (fid === 1 && ftype === TYPE_STRING) {
|
||||
identifier = r.readString()
|
||||
} else if (fid === 2 && ftype === TYPE_STRING) {
|
||||
key = r.readString()
|
||||
} else {
|
||||
r.skip(ftype)
|
||||
}
|
||||
})
|
||||
throw new Error(`Evernote not found: ${identifier}${key ? ` (${key})` : ''}`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function readNotebook(reader: ThriftReader): EvernoteNotebook {
|
||||
const notebook: EvernoteNotebook = {
|
||||
guid: '',
|
||||
name: '',
|
||||
defaultNotebook: false,
|
||||
serviceCreated: null,
|
||||
serviceUpdated: null,
|
||||
stack: null,
|
||||
}
|
||||
|
||||
reader.readStruct((r, fieldId, fieldType) => {
|
||||
switch (fieldId) {
|
||||
case 1:
|
||||
if (fieldType === TYPE_STRING) notebook.guid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 2:
|
||||
if (fieldType === TYPE_STRING) notebook.name = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 4:
|
||||
if (fieldType === TYPE_BOOL) notebook.defaultNotebook = r.readBool()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 5:
|
||||
if (fieldType === TYPE_I64) notebook.serviceCreated = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 6:
|
||||
if (fieldType === TYPE_I64) notebook.serviceUpdated = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 9:
|
||||
if (fieldType === TYPE_STRING) notebook.stack = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
default:
|
||||
r.skip(fieldType)
|
||||
}
|
||||
})
|
||||
|
||||
return notebook
|
||||
}
|
||||
|
||||
function readNote(reader: ThriftReader): EvernoteNote {
|
||||
const note: EvernoteNote = {
|
||||
guid: '',
|
||||
title: '',
|
||||
content: null,
|
||||
contentLength: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
deleted: null,
|
||||
active: true,
|
||||
notebookGuid: null,
|
||||
tagGuids: [],
|
||||
tagNames: [],
|
||||
}
|
||||
|
||||
reader.readStruct((r, fieldId, fieldType) => {
|
||||
switch (fieldId) {
|
||||
case 1:
|
||||
if (fieldType === TYPE_STRING) note.guid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 2:
|
||||
if (fieldType === TYPE_STRING) note.title = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 3:
|
||||
if (fieldType === TYPE_STRING) note.content = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 5:
|
||||
if (fieldType === TYPE_I32) note.contentLength = r.readI32()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 6:
|
||||
if (fieldType === TYPE_I64) note.created = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 7:
|
||||
if (fieldType === TYPE_I64) note.updated = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 8:
|
||||
if (fieldType === TYPE_I64) note.deleted = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 9:
|
||||
if (fieldType === TYPE_BOOL) note.active = r.readBool()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 11:
|
||||
if (fieldType === TYPE_STRING) note.notebookGuid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 12:
|
||||
if (fieldType === TYPE_LIST) {
|
||||
const { size } = r.readListBegin()
|
||||
for (let i = 0; i < size; i++) {
|
||||
note.tagGuids.push(r.readString())
|
||||
}
|
||||
} else {
|
||||
r.skip(fieldType)
|
||||
}
|
||||
break
|
||||
case 15:
|
||||
if (fieldType === TYPE_LIST) {
|
||||
const { size } = r.readListBegin()
|
||||
for (let i = 0; i < size; i++) {
|
||||
note.tagNames.push(r.readString())
|
||||
}
|
||||
} else {
|
||||
r.skip(fieldType)
|
||||
}
|
||||
break
|
||||
default:
|
||||
r.skip(fieldType)
|
||||
}
|
||||
})
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
function readTag(reader: ThriftReader): EvernoteTag {
|
||||
const tag: EvernoteTag = {
|
||||
guid: '',
|
||||
name: '',
|
||||
parentGuid: null,
|
||||
updateSequenceNum: null,
|
||||
}
|
||||
|
||||
reader.readStruct((r, fieldId, fieldType) => {
|
||||
switch (fieldId) {
|
||||
case 1:
|
||||
if (fieldType === TYPE_STRING) tag.guid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 2:
|
||||
if (fieldType === TYPE_STRING) tag.name = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 3:
|
||||
if (fieldType === TYPE_STRING) tag.parentGuid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 4:
|
||||
if (fieldType === TYPE_I32) tag.updateSequenceNum = r.readI32()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
default:
|
||||
r.skip(fieldType)
|
||||
}
|
||||
})
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
function readNoteMetadata(reader: ThriftReader): EvernoteNoteMetadata {
|
||||
const meta: EvernoteNoteMetadata = {
|
||||
guid: '',
|
||||
title: null,
|
||||
contentLength: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
notebookGuid: null,
|
||||
tagGuids: [],
|
||||
}
|
||||
|
||||
reader.readStruct((r, fieldId, fieldType) => {
|
||||
switch (fieldId) {
|
||||
case 1:
|
||||
if (fieldType === TYPE_STRING) meta.guid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 2:
|
||||
if (fieldType === TYPE_STRING) meta.title = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 5:
|
||||
if (fieldType === TYPE_I32) meta.contentLength = r.readI32()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 6:
|
||||
if (fieldType === TYPE_I64) meta.created = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 7:
|
||||
if (fieldType === TYPE_I64) meta.updated = Number(r.readI64())
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 11:
|
||||
if (fieldType === TYPE_STRING) meta.notebookGuid = r.readString()
|
||||
else r.skip(fieldType)
|
||||
break
|
||||
case 12:
|
||||
if (fieldType === TYPE_LIST) {
|
||||
const { size } = r.readListBegin()
|
||||
for (let i = 0; i < size; i++) {
|
||||
meta.tagGuids.push(r.readString())
|
||||
}
|
||||
} else {
|
||||
r.skip(fieldType)
|
||||
}
|
||||
break
|
||||
default:
|
||||
r.skip(fieldType)
|
||||
}
|
||||
})
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
export async function listNotebooks(token: string): Promise<EvernoteNotebook[]> {
|
||||
const writer = new ThriftWriter()
|
||||
writer.writeMessageBegin('listNotebooks', 0)
|
||||
writer.writeStringField(1, token)
|
||||
writer.writeFieldStop()
|
||||
|
||||
const reader = await callNoteStore(token, writer)
|
||||
const notebooks: EvernoteNotebook[] = []
|
||||
|
||||
reader.readStruct((r, fieldId, fieldType) => {
|
||||
if (fieldId === 0 && fieldType === TYPE_LIST) {
|
||||
const { size } = r.readListBegin()
|
||||
for (let i = 0; i < size; i++) {
|
||||
notebooks.push(readNotebook(r))
|
||||
}
|
||||
} else {
|
||||
if (!checkEvernoteException(r, fieldId, fieldType)) {
|
||||
r.skip(fieldType)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return notebooks
|
||||
}
|
||||
|
||||
export async function getNote(
|
||||
token: string,
|
||||
guid: string,
|
||||
withContent = true
|
||||
): Promise<EvernoteNote> {
|
||||
const writer = new ThriftWriter()
|
||||
writer.writeMessageBegin('getNote', 0)
|
||||
writer.writeStringField(1, token)
|
||||
writer.writeStringField(2, guid)
|
||||
writer.writeBoolField(3, withContent)
|
||||
writer.writeBoolField(4, false)
|
||||
writer.writeBoolField(5, false)
|
||||
writer.writeBoolField(6, false)
|
||||
writer.writeFieldStop()
|
||||
|
||||
const reader = await callNoteStore(token, writer)
|
||||
let note: EvernoteNote | null = null
|
||||
|
||||
reader.readStruct((r, fieldId, fieldType) => {
|
||||
if (fieldId === 0 && fieldType === TYPE_STRUCT) {
|
||||
note = readNote(r)
|
||||
} else {
|
||||
if (!checkEvernoteException(r, fieldId, fieldType)) {
|
||||
r.skip(fieldType)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
throw new Error('No note returned from Evernote API')
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
/** Wrap content in ENML if it's not already */
|
||||
function wrapInEnml(content: string): string {
|
||||
if (content.includes('<!DOCTYPE en-note')) {
|
||||
return content
|
||||
}
|
||||
const escaped = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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
|
||||
}
|
||||
255
apps/sim/app/api/tools/evernote/lib/thrift.ts
Normal file
255
apps/sim/app/api/tools/evernote/lib/thrift.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Minimal Thrift binary protocol encoder/decoder for Evernote API.
|
||||
* Supports only the types needed for NoteStore operations.
|
||||
*/
|
||||
|
||||
const THRIFT_VERSION_1 = 0x80010000
|
||||
const MESSAGE_CALL = 1
|
||||
const MESSAGE_EXCEPTION = 3
|
||||
|
||||
const TYPE_STOP = 0
|
||||
const TYPE_BOOL = 2
|
||||
const TYPE_I32 = 8
|
||||
const TYPE_I64 = 10
|
||||
const TYPE_STRING = 11
|
||||
const TYPE_STRUCT = 12
|
||||
const TYPE_LIST = 15
|
||||
|
||||
export class ThriftWriter {
|
||||
private buffer: number[] = []
|
||||
|
||||
writeMessageBegin(name: string, seqId: number): void {
|
||||
this.writeI32(THRIFT_VERSION_1 | MESSAGE_CALL)
|
||||
this.writeString(name)
|
||||
this.writeI32(seqId)
|
||||
}
|
||||
|
||||
writeFieldBegin(type: number, id: number): void {
|
||||
this.buffer.push(type)
|
||||
this.writeI16(id)
|
||||
}
|
||||
|
||||
writeFieldStop(): void {
|
||||
this.buffer.push(TYPE_STOP)
|
||||
}
|
||||
|
||||
writeString(value: string): void {
|
||||
const encoded = new TextEncoder().encode(value)
|
||||
this.writeI32(encoded.length)
|
||||
for (const byte of encoded) {
|
||||
this.buffer.push(byte)
|
||||
}
|
||||
}
|
||||
|
||||
writeBool(value: boolean): void {
|
||||
this.buffer.push(value ? 1 : 0)
|
||||
}
|
||||
|
||||
writeI16(value: number): void {
|
||||
this.buffer.push((value >> 8) & 0xff)
|
||||
this.buffer.push(value & 0xff)
|
||||
}
|
||||
|
||||
writeI32(value: number): void {
|
||||
this.buffer.push((value >> 24) & 0xff)
|
||||
this.buffer.push((value >> 16) & 0xff)
|
||||
this.buffer.push((value >> 8) & 0xff)
|
||||
this.buffer.push(value & 0xff)
|
||||
}
|
||||
|
||||
writeI64(value: bigint): void {
|
||||
const buf = new ArrayBuffer(8)
|
||||
const view = new DataView(buf)
|
||||
view.setBigInt64(0, value, false)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
this.buffer.push(view.getUint8(i))
|
||||
}
|
||||
}
|
||||
|
||||
writeStringField(id: number, value: string): void {
|
||||
this.writeFieldBegin(TYPE_STRING, id)
|
||||
this.writeString(value)
|
||||
}
|
||||
|
||||
writeBoolField(id: number, value: boolean): void {
|
||||
this.writeFieldBegin(TYPE_BOOL, id)
|
||||
this.writeBool(value)
|
||||
}
|
||||
|
||||
writeI32Field(id: number, value: number): void {
|
||||
this.writeFieldBegin(TYPE_I32, id)
|
||||
this.writeI32(value)
|
||||
}
|
||||
|
||||
writeStringListField(id: number, values: string[]): void {
|
||||
this.writeFieldBegin(TYPE_LIST, id)
|
||||
this.buffer.push(TYPE_STRING)
|
||||
this.writeI32(values.length)
|
||||
for (const v of values) {
|
||||
this.writeString(v)
|
||||
}
|
||||
}
|
||||
|
||||
toBuffer(): Buffer {
|
||||
return Buffer.from(this.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
export class ThriftReader {
|
||||
private view: DataView
|
||||
private pos = 0
|
||||
|
||||
constructor(buffer: ArrayBuffer) {
|
||||
this.view = new DataView(buffer)
|
||||
}
|
||||
|
||||
readMessageBegin(): { name: string; type: number; seqId: number } {
|
||||
const versionAndType = this.readI32()
|
||||
const version = versionAndType & 0xffff0000
|
||||
if (version !== (THRIFT_VERSION_1 | 0)) {
|
||||
throw new Error(`Unsupported Thrift version: 0x${version.toString(16)}`)
|
||||
}
|
||||
const type = versionAndType & 0x000000ff
|
||||
const name = this.readString()
|
||||
const seqId = this.readI32()
|
||||
return { name, type, seqId }
|
||||
}
|
||||
|
||||
readFieldBegin(): { type: number; id: number } {
|
||||
const type = this.view.getUint8(this.pos++)
|
||||
if (type === TYPE_STOP) {
|
||||
return { type: TYPE_STOP, id: 0 }
|
||||
}
|
||||
const id = this.view.getInt16(this.pos, false)
|
||||
this.pos += 2
|
||||
return { type, id }
|
||||
}
|
||||
|
||||
readString(): string {
|
||||
const length = this.readI32()
|
||||
const bytes = new Uint8Array(this.view.buffer, this.pos, length)
|
||||
this.pos += length
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
|
||||
readBool(): boolean {
|
||||
return this.view.getUint8(this.pos++) !== 0
|
||||
}
|
||||
|
||||
readI32(): number {
|
||||
const value = this.view.getInt32(this.pos, false)
|
||||
this.pos += 4
|
||||
return value
|
||||
}
|
||||
|
||||
readI64(): bigint {
|
||||
const value = this.view.getBigInt64(this.pos, false)
|
||||
this.pos += 8
|
||||
return value
|
||||
}
|
||||
|
||||
readBinary(): Uint8Array {
|
||||
const length = this.readI32()
|
||||
const bytes = new Uint8Array(this.view.buffer, this.pos, length)
|
||||
this.pos += length
|
||||
return bytes
|
||||
}
|
||||
|
||||
readListBegin(): { elementType: number; size: number } {
|
||||
const elementType = this.view.getUint8(this.pos++)
|
||||
const size = this.readI32()
|
||||
return { elementType, size }
|
||||
}
|
||||
|
||||
/** Skip a value of the given Thrift type */
|
||||
skip(type: number): void {
|
||||
switch (type) {
|
||||
case TYPE_BOOL:
|
||||
this.pos += 1
|
||||
break
|
||||
case 6: // I16
|
||||
this.pos += 2
|
||||
break
|
||||
case 3: // BYTE
|
||||
this.pos += 1
|
||||
break
|
||||
case TYPE_I32:
|
||||
this.pos += 4
|
||||
break
|
||||
case TYPE_I64:
|
||||
case 4: // DOUBLE
|
||||
this.pos += 8
|
||||
break
|
||||
case TYPE_STRING: {
|
||||
const len = this.readI32()
|
||||
this.pos += len
|
||||
break
|
||||
}
|
||||
case TYPE_STRUCT:
|
||||
this.skipStruct()
|
||||
break
|
||||
case TYPE_LIST:
|
||||
case 14: {
|
||||
// SET
|
||||
const { elementType, size } = this.readListBegin()
|
||||
for (let i = 0; i < size; i++) {
|
||||
this.skip(elementType)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 13: {
|
||||
// MAP
|
||||
const keyType = this.view.getUint8(this.pos++)
|
||||
const valueType = this.view.getUint8(this.pos++)
|
||||
const count = this.readI32()
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.skip(keyType)
|
||||
this.skip(valueType)
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`Cannot skip unknown Thrift type: ${type}`)
|
||||
}
|
||||
}
|
||||
|
||||
private skipStruct(): void {
|
||||
for (;;) {
|
||||
const { type } = this.readFieldBegin()
|
||||
if (type === TYPE_STOP) break
|
||||
this.skip(type)
|
||||
}
|
||||
}
|
||||
|
||||
/** Read struct fields, calling the handler for each field */
|
||||
readStruct<T>(handler: (reader: ThriftReader, fieldId: number, fieldType: number) => void): void {
|
||||
for (;;) {
|
||||
const { type, id } = this.readFieldBegin()
|
||||
if (type === TYPE_STOP) break
|
||||
handler(this, id, type)
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if this is an exception response */
|
||||
isException(messageType: number): boolean {
|
||||
return messageType === MESSAGE_EXCEPTION
|
||||
}
|
||||
|
||||
/** Read a Thrift application exception */
|
||||
readException(): { message: string; type: number } {
|
||||
let message = ''
|
||||
let type = 0
|
||||
this.readStruct((reader, fieldId, fieldType) => {
|
||||
if (fieldId === 1 && fieldType === TYPE_STRING) {
|
||||
message = reader.readString()
|
||||
} else if (fieldId === 2 && fieldType === TYPE_I32) {
|
||||
type = reader.readI32()
|
||||
} else {
|
||||
reader.skip(fieldType)
|
||||
}
|
||||
})
|
||||
return { message, type }
|
||||
}
|
||||
}
|
||||
|
||||
export { TYPE_BOOL, TYPE_I32, TYPE_I64, TYPE_LIST, TYPE_STOP, TYPE_STRING, TYPE_STRUCT }
|
||||
35
apps/sim/app/api/tools/evernote/list-notebooks/route.ts
Normal file
35
apps/sim/app/api/tools/evernote/list-notebooks/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { listNotebooks } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteListNotebooksAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey } = body
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ success: false, error: 'apiKey is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const notebooks = await listNotebooks(apiKey)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { notebooks },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to list notebooks', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
35
apps/sim/app/api/tools/evernote/list-tags/route.ts
Normal file
35
apps/sim/app/api/tools/evernote/list-tags/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { listTags } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteListTagsAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey } = body
|
||||
|
||||
if (!apiKey) {
|
||||
return NextResponse.json({ success: false, error: 'apiKey is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const tags = await listTags(apiKey)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { tags },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to list tags', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
49
apps/sim/app/api/tools/evernote/search-notes/route.ts
Normal file
49
apps/sim/app/api/tools/evernote/search-notes/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { searchNotes } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteSearchNotesAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, query, notebookGuid, offset = 0, maxNotes = 25 } = body
|
||||
|
||||
if (!apiKey || !query) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and query are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const clampedMaxNotes = Math.min(Math.max(Number(maxNotes) || 25, 1), 250)
|
||||
|
||||
const result = await searchNotes(
|
||||
apiKey,
|
||||
query,
|
||||
notebookGuid || undefined,
|
||||
Number(offset),
|
||||
clampedMaxNotes
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
totalNotes: result.totalNotes,
|
||||
notes: result.notes,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to search notes', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
58
apps/sim/app/api/tools/evernote/update-note/route.ts
Normal file
58
apps/sim/app/api/tools/evernote/update-note/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { updateNote } from '@/app/api/tools/evernote/lib/client'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('EvernoteUpdateNoteAPI')
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { apiKey, noteGuid, title, content, notebookGuid, tagNames } = body
|
||||
|
||||
if (!apiKey || !noteGuid) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'apiKey and noteGuid are required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const parsedTags = tagNames
|
||||
? (() => {
|
||||
const tags =
|
||||
typeof tagNames === 'string'
|
||||
? tagNames
|
||||
.split(',')
|
||||
.map((t: string) => t.trim())
|
||||
.filter(Boolean)
|
||||
: tagNames
|
||||
return tags.length > 0 ? tags : undefined
|
||||
})()
|
||||
: undefined
|
||||
|
||||
const note = await updateNote(
|
||||
apiKey,
|
||||
noteGuid,
|
||||
title || undefined,
|
||||
content || undefined,
|
||||
notebookGuid || undefined,
|
||||
parsedTags
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { note },
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Failed to update note', { error: message })
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { MongoClient } from 'mongodb'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types'
|
||||
|
||||
export async function createMongoDBConnection(config: MongoDBConnectionConfig) {
|
||||
const hostValidation = await validateDatabaseHost(config.host, 'host')
|
||||
if (!hostValidation.isValid) {
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const credentials =
|
||||
config.username && config.password
|
||||
? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password)}@`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mysql from 'mysql2/promise'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
|
||||
export interface MySQLConnectionConfig {
|
||||
host: string
|
||||
@@ -10,6 +11,11 @@ export interface MySQLConnectionConfig {
|
||||
}
|
||||
|
||||
export async function createMySQLConnection(config: MySQLConnectionConfig) {
|
||||
const hostValidation = await validateDatabaseHost(config.host, 'host')
|
||||
if (!hostValidation.isValid) {
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const connectionConfig: mysql.ConnectionOptions = {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import neo4j from 'neo4j-driver'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
import type { Neo4jConnectionConfig } from '@/tools/neo4j/types'
|
||||
|
||||
export async function createNeo4jDriver(config: Neo4jConnectionConfig) {
|
||||
const hostValidation = await validateDatabaseHost(config.host, 'host')
|
||||
if (!hostValidation.isValid) {
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const isAuraHost =
|
||||
config.host === 'databases.neo4j.io' || config.host.endsWith('.databases.neo4j.io')
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Deleting data from ${params.table} on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const sql = createPostgresConnection({
|
||||
const sql = await createPostgresConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const sql = createPostgresConnection({
|
||||
const sql = await createPostgresConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Inserting data into ${params.table} on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const sql = createPostgresConnection({
|
||||
const sql = await createPostgresConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Introspecting PostgreSQL schema on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const sql = createPostgresConnection({
|
||||
const sql = await createPostgresConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Executing PostgreSQL query on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const sql = createPostgresConnection({
|
||||
const sql = await createPostgresConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Updating data in ${params.table} on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
const sql = createPostgresConnection({
|
||||
const sql = await createPostgresConnection({
|
||||
host: params.host,
|
||||
port: params.port,
|
||||
database: params.database,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import postgres from 'postgres'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
import type { PostgresConnectionConfig } from '@/tools/postgresql/types'
|
||||
|
||||
export function createPostgresConnection(config: PostgresConnectionConfig) {
|
||||
export async function createPostgresConnection(config: PostgresConnectionConfig) {
|
||||
const hostValidation = await validateDatabaseHost(config.host, 'host')
|
||||
if (!hostValidation.isValid) {
|
||||
throw new Error(hostValidation.error)
|
||||
}
|
||||
|
||||
const sslConfig =
|
||||
config.ssl === 'disabled'
|
||||
? false
|
||||
|
||||
@@ -3,6 +3,7 @@ import Redis from 'ioredis'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
|
||||
|
||||
const logger = createLogger('RedisAPI')
|
||||
|
||||
@@ -24,6 +25,16 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const { url, command, args } = RequestSchema.parse(body)
|
||||
|
||||
const parsedUrl = new URL(url)
|
||||
const hostname =
|
||||
parsedUrl.hostname.startsWith('[') && parsedUrl.hostname.endsWith(']')
|
||||
? parsedUrl.hostname.slice(1, -1)
|
||||
: parsedUrl.hostname
|
||||
const hostValidation = await validateDatabaseHost(hostname, 'host')
|
||||
if (!hostValidation.isValid) {
|
||||
return NextResponse.json({ error: hostValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
client = new Redis(url, {
|
||||
connectTimeout: 10000,
|
||||
commandTimeout: 10000,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { checkServerSideUsageLimits } from '@/lib/billing'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||
import { getEffectiveCurrentPeriodCost } from '@/lib/billing/core/usage'
|
||||
@@ -20,7 +20,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const userSubscription = await getHighestPrioritySubscription(authenticatedUserId)
|
||||
const rateLimiter = new RateLimiter()
|
||||
const triggerType = auth.authType === 'api_key' ? 'api' : 'manual'
|
||||
const triggerType = auth.authType === AuthType.API_KEY ? 'api' : 'manual'
|
||||
const [syncStatus, asyncStatus] = await Promise.all([
|
||||
rateLimiter.getRateLimitStatusWithSubscription(
|
||||
authenticatedUserId,
|
||||
|
||||
@@ -10,7 +10,6 @@ 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'
|
||||
@@ -331,14 +330,10 @@ 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
|
||||
}
|
||||
@@ -483,18 +478,9 @@ 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, {
|
||||
|
||||
@@ -367,9 +367,7 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Configure each new webhook (for providers that need configuration)
|
||||
const pollingProviders = ['gmail', 'outlook']
|
||||
const needsConfiguration = pollingProviders.includes(provider)
|
||||
const needsConfiguration = provider === 'gmail' || provider === 'outlook'
|
||||
|
||||
if (needsConfiguration) {
|
||||
const configureFunc =
|
||||
|
||||
@@ -268,6 +268,32 @@ vi.mock('@/lib/webhooks/processor', () => ({
|
||||
}
|
||||
}),
|
||||
handleProviderChallenges: vi.fn().mockResolvedValue(null),
|
||||
handlePreLookupWebhookVerification: vi
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async (
|
||||
method: string,
|
||||
body: Record<string, unknown> | undefined,
|
||||
_requestId: string,
|
||||
path: string
|
||||
) => {
|
||||
if (path !== 'pending-verification-path') {
|
||||
return null
|
||||
}
|
||||
|
||||
const isVerificationProbe =
|
||||
method === 'GET' ||
|
||||
method === 'HEAD' ||
|
||||
(method === 'POST' && (!body || Object.keys(body).length === 0 || !body.type))
|
||||
|
||||
if (!isVerificationProbe) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { NextResponse } = require('next/server')
|
||||
return NextResponse.json({ status: 'ok', message: 'Webhook endpoint verified' })
|
||||
}
|
||||
),
|
||||
handleProviderReachabilityTest: vi.fn().mockReturnValue(null),
|
||||
verifyProviderAuth: vi
|
||||
.fn()
|
||||
@@ -324,7 +350,9 @@ vi.mock('@/lib/webhooks/processor', () => ({
|
||||
return null
|
||||
}
|
||||
),
|
||||
checkWebhookPreprocessing: vi.fn().mockResolvedValue(null),
|
||||
checkWebhookPreprocessing: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ error: null, actorUserId: 'test-user-id' }),
|
||||
formatProviderErrorResponse: vi.fn().mockImplementation((_webhook, error, status) => {
|
||||
const { NextResponse } = require('next/server')
|
||||
return NextResponse.json({ error }, { status })
|
||||
@@ -351,7 +379,7 @@ vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
|
||||
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test'
|
||||
|
||||
import { POST } from '@/app/api/webhooks/trigger/[path]/route'
|
||||
import { GET, POST } from '@/app/api/webhooks/trigger/[path]/route'
|
||||
|
||||
describe('Webhook Trigger API Route', () => {
|
||||
beforeEach(() => {
|
||||
@@ -387,7 +415,7 @@ describe('Webhook Trigger API Route', () => {
|
||||
})
|
||||
|
||||
it('should handle 404 for non-existent webhooks', async () => {
|
||||
const req = createMockRequest('POST', { event: 'test' })
|
||||
const req = createMockRequest('POST', { type: 'event.test' })
|
||||
|
||||
const params = Promise.resolve({ path: 'non-existent-path' })
|
||||
|
||||
@@ -399,6 +427,72 @@ describe('Webhook Trigger API Route', () => {
|
||||
expect(text).toMatch(/not found/i)
|
||||
})
|
||||
|
||||
it('should return 405 for GET requests on unknown webhook paths', async () => {
|
||||
const req = createMockRequest(
|
||||
'GET',
|
||||
undefined,
|
||||
{},
|
||||
'http://localhost:3000/api/webhooks/trigger/non-existent-path'
|
||||
)
|
||||
|
||||
const params = Promise.resolve({ path: 'non-existent-path' })
|
||||
|
||||
const response = await GET(req as any, { params })
|
||||
|
||||
expect(response.status).toBe(405)
|
||||
})
|
||||
|
||||
it('should return 200 for GET verification probes on registered pending paths', async () => {
|
||||
const req = createMockRequest(
|
||||
'GET',
|
||||
undefined,
|
||||
{},
|
||||
'http://localhost:3000/api/webhooks/trigger/pending-verification-path'
|
||||
)
|
||||
|
||||
const params = Promise.resolve({ path: 'pending-verification-path' })
|
||||
|
||||
const response = await GET(req as any, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
status: 'ok',
|
||||
message: 'Webhook endpoint verified',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 200 for empty POST verification probes on registered pending paths', async () => {
|
||||
const req = createMockRequest(
|
||||
'POST',
|
||||
undefined,
|
||||
{},
|
||||
'http://localhost:3000/api/webhooks/trigger/pending-verification-path'
|
||||
)
|
||||
|
||||
const params = Promise.resolve({ path: 'pending-verification-path' })
|
||||
|
||||
const response = await POST(req as any, { params })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
status: 'ok',
|
||||
message: 'Webhook endpoint verified',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 404 for POST requests without type on unknown webhook paths', async () => {
|
||||
const req = createMockRequest('POST', { event: 'test' })
|
||||
|
||||
const params = Promise.resolve({ path: 'non-existent-path' })
|
||||
|
||||
const response = await POST(req as any, { params })
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
|
||||
const text = await response.text()
|
||||
expect(text).toMatch(/not found/i)
|
||||
})
|
||||
|
||||
describe('Generic Webhook Authentication', () => {
|
||||
it('should process generic webhook without authentication', async () => {
|
||||
testData.webhooks.push({
|
||||
|
||||
@@ -4,8 +4,8 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
checkWebhookPreprocessing,
|
||||
findAllWebhooksForPath,
|
||||
formatProviderErrorResponse,
|
||||
handlePreDeploymentVerification,
|
||||
handlePreLookupWebhookVerification,
|
||||
handleProviderChallenges,
|
||||
handleProviderReachabilityTest,
|
||||
parseWebhookBody,
|
||||
@@ -31,7 +31,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
return challengeResponse
|
||||
}
|
||||
|
||||
return new NextResponse('Method not allowed', { status: 405 })
|
||||
return (
|
||||
(await handlePreLookupWebhookVerification(request.method, undefined, requestId, path)) ||
|
||||
new NextResponse('Method not allowed', { status: 405 })
|
||||
)
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
@@ -65,6 +68,16 @@ export async function POST(
|
||||
const webhooksForPath = await findAllWebhooksForPath({ requestId, path })
|
||||
|
||||
if (webhooksForPath.length === 0) {
|
||||
const verificationResponse = await handlePreLookupWebhookVerification(
|
||||
request.method,
|
||||
body,
|
||||
requestId,
|
||||
path
|
||||
)
|
||||
if (verificationResponse) {
|
||||
return verificationResponse
|
||||
}
|
||||
|
||||
logger.warn(`[${requestId}] Webhook or workflow not found for path: ${path}`)
|
||||
return new NextResponse('Not Found', { status: 404 })
|
||||
}
|
||||
@@ -82,7 +95,6 @@ export async function POST(
|
||||
requestId
|
||||
)
|
||||
if (authError) {
|
||||
// For multi-webhook, log and continue to next webhook
|
||||
if (webhooksForPath.length > 1) {
|
||||
logger.warn(`[${requestId}] Auth failed for webhook ${foundWebhook.id}, continuing to next`)
|
||||
continue
|
||||
@@ -92,39 +104,18 @@ export async function POST(
|
||||
|
||||
const reachabilityResponse = handleProviderReachabilityTest(foundWebhook, body, requestId)
|
||||
if (reachabilityResponse) {
|
||||
// Reachability test should return immediately for the first webhook
|
||||
return reachabilityResponse
|
||||
}
|
||||
|
||||
let preprocessError: NextResponse | null = null
|
||||
try {
|
||||
preprocessError = await checkWebhookPreprocessing(foundWorkflow, foundWebhook, requestId)
|
||||
if (preprocessError) {
|
||||
if (webhooksForPath.length > 1) {
|
||||
logger.warn(
|
||||
`[${requestId}] Preprocessing failed for webhook ${foundWebhook.id}, continuing to next`
|
||||
)
|
||||
continue
|
||||
}
|
||||
return preprocessError
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Unexpected error during webhook preprocessing`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
webhookId: foundWebhook.id,
|
||||
workflowId: foundWorkflow.id,
|
||||
})
|
||||
|
||||
const preprocessResult = await checkWebhookPreprocessing(foundWorkflow, foundWebhook, requestId)
|
||||
if (preprocessResult.error) {
|
||||
if (webhooksForPath.length > 1) {
|
||||
logger.warn(
|
||||
`[${requestId}] Preprocessing failed for webhook ${foundWebhook.id}, continuing to next`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
return formatProviderErrorResponse(
|
||||
foundWebhook,
|
||||
'An unexpected error occurred during preprocessing',
|
||||
500
|
||||
)
|
||||
return preprocessResult.error
|
||||
}
|
||||
|
||||
if (foundWebhook.blockId) {
|
||||
@@ -152,6 +143,7 @@ export async function POST(
|
||||
const response = await queueWebhookExecution(foundWebhook, foundWorkflow, body, request, {
|
||||
requestId,
|
||||
path,
|
||||
actorUserId: preprocessResult.actorUserId,
|
||||
})
|
||||
responses.push(response)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ vi.mock('@sim/db/schema', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
115
apps/sim/app/api/workflows/[id]/execute/response-block.test.ts
Normal file
115
apps/sim/app/api/workflows/[id]/execute/response-block.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Tests that internal JWT callers receive the standard response format
|
||||
* even when the child workflow has a Response block.
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { AuthType } from '@/lib/auth/hybrid'
|
||||
import type { ExecutionResult } from '@/lib/workflows/types'
|
||||
import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils'
|
||||
|
||||
function buildExecutionResult(overrides: Partial<ExecutionResult> = {}): ExecutionResult {
|
||||
return {
|
||||
success: true,
|
||||
output: { data: { issues: [] }, status: 200, headers: {} },
|
||||
logs: [
|
||||
{
|
||||
blockId: 'response-1',
|
||||
blockType: 'response',
|
||||
blockName: 'Response',
|
||||
success: true,
|
||||
output: { data: { issues: [] }, status: 200, headers: {} },
|
||||
startedAt: '2026-01-01T00:00:00Z',
|
||||
endedAt: '2026-01-01T00:00:01Z',
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
duration: 500,
|
||||
startTime: '2026-01-01T00:00:00Z',
|
||||
endTime: '2026-01-01T00:00:01Z',
|
||||
},
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Response block gating by auth type', () => {
|
||||
let resultWithResponseBlock: ExecutionResult
|
||||
|
||||
beforeEach(() => {
|
||||
resultWithResponseBlock = buildExecutionResult()
|
||||
})
|
||||
|
||||
it('should detect a Response block in execution result', () => {
|
||||
expect(workflowHasResponseBlock(resultWithResponseBlock)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not detect a Response block when none exists', () => {
|
||||
const resultWithoutResponseBlock = buildExecutionResult({
|
||||
output: { result: 'hello' },
|
||||
logs: [
|
||||
{
|
||||
blockId: 'agent-1',
|
||||
blockType: 'agent',
|
||||
blockName: 'Agent',
|
||||
success: true,
|
||||
output: { result: 'hello' },
|
||||
startedAt: '2026-01-01T00:00:00Z',
|
||||
endedAt: '2026-01-01T00:00:01Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(workflowHasResponseBlock(resultWithoutResponseBlock)).toBe(false)
|
||||
})
|
||||
|
||||
it('should skip Response block formatting for internal JWT callers', () => {
|
||||
const authType = AuthType.INTERNAL_JWT
|
||||
const hasResponseBlock = workflowHasResponseBlock(resultWithResponseBlock)
|
||||
|
||||
expect(hasResponseBlock).toBe(true)
|
||||
|
||||
// This mirrors the route.ts condition:
|
||||
// if (auth.authType !== AuthType.INTERNAL_JWT && workflowHasResponseBlock(...))
|
||||
const shouldFormatAsResponseBlock = authType !== AuthType.INTERNAL_JWT && hasResponseBlock
|
||||
expect(shouldFormatAsResponseBlock).toBe(false)
|
||||
})
|
||||
|
||||
it('should apply Response block formatting for API key callers', () => {
|
||||
const authType = AuthType.API_KEY
|
||||
const hasResponseBlock = workflowHasResponseBlock(resultWithResponseBlock)
|
||||
|
||||
const shouldFormatAsResponseBlock = authType !== AuthType.INTERNAL_JWT && hasResponseBlock
|
||||
expect(shouldFormatAsResponseBlock).toBe(true)
|
||||
|
||||
const response = createHttpResponseFromBlock(resultWithResponseBlock)
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
|
||||
it('should apply Response block formatting for session callers', () => {
|
||||
const authType = AuthType.SESSION
|
||||
const hasResponseBlock = workflowHasResponseBlock(resultWithResponseBlock)
|
||||
|
||||
const shouldFormatAsResponseBlock = authType !== AuthType.INTERNAL_JWT && hasResponseBlock
|
||||
expect(shouldFormatAsResponseBlock).toBe(true)
|
||||
})
|
||||
|
||||
it('should return raw user data via createHttpResponseFromBlock', async () => {
|
||||
const response = createHttpResponseFromBlock(resultWithResponseBlock)
|
||||
const body = await response.json()
|
||||
|
||||
// Response block returns the user-defined data directly (no success/executionId wrapper)
|
||||
expect(body).toEqual({ issues: [] })
|
||||
expect(body.success).toBeUndefined()
|
||||
expect(body.executionId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should respect custom status codes from Response block', () => {
|
||||
const result = buildExecutionResult({
|
||||
output: { data: { error: 'Not found' }, status: 404, headers: {} },
|
||||
})
|
||||
|
||||
const response = createHttpResponseFromBlock(result)
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs'
|
||||
import {
|
||||
createTimeoutAbortController,
|
||||
@@ -22,7 +22,6 @@ 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,
|
||||
@@ -323,7 +322,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
)
|
||||
}
|
||||
|
||||
const defaultTriggerType = isPublicApiAccess || auth.authType === 'api_key' ? 'api' : 'manual'
|
||||
const defaultTriggerType =
|
||||
isPublicApiAccess || auth.authType === AuthType.API_KEY ? 'api' : 'manual'
|
||||
|
||||
const {
|
||||
selectedOutputs,
|
||||
@@ -382,7 +382,9 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
// For API key and internal JWT auth, the entire body is the input (except for our control fields)
|
||||
// For session auth, the input is explicitly provided in the input field
|
||||
const input =
|
||||
isPublicApiAccess || auth.authType === 'api_key' || auth.authType === 'internal_jwt'
|
||||
isPublicApiAccess ||
|
||||
auth.authType === AuthType.API_KEY ||
|
||||
auth.authType === AuthType.INTERNAL_JWT
|
||||
? (() => {
|
||||
const {
|
||||
selectedOutputs,
|
||||
@@ -408,7 +410,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
// Public API callers always execute the deployed state, never the draft.
|
||||
const shouldUseDraftState = isPublicApiAccess
|
||||
? false
|
||||
: (useDraftState ?? auth.authType === 'session')
|
||||
: (useDraftState ?? auth.authType === AuthType.SESSION)
|
||||
const streamHeader = req.headers.get('X-Stream-Response') === 'true'
|
||||
const enableSSE = streamHeader || streamParam === true
|
||||
const executionModeHeader = req.headers.get('X-Execution-Mode')
|
||||
@@ -441,7 +443,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
// Client-side sessions and personal API keys bill/permission-check the
|
||||
// authenticated user, not the workspace billed account.
|
||||
const useAuthenticatedUserAsActor =
|
||||
isClientSession || (auth.authType === 'api_key' && auth.apiKeyType === 'personal')
|
||||
isClientSession || (auth.authType === AuthType.API_KEY && auth.apiKeyType === 'personal')
|
||||
|
||||
// Authorization fetches the full workflow record and checks workspace permissions.
|
||||
// Run it first so we can pass the record to preprocessing (eliminates a duplicate DB query).
|
||||
@@ -671,8 +673,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
const resultWithBase64 = { ...result, output: outputWithBase64 }
|
||||
|
||||
const hasResponseBlock = workflowHasResponseBlock(resultWithBase64)
|
||||
if (hasResponseBlock) {
|
||||
if (auth.authType !== AuthType.INTERNAL_JWT && workflowHasResponseBlock(resultWithBase64)) {
|
||||
return createHttpResponseFromBlock(resultWithBase64)
|
||||
}
|
||||
|
||||
@@ -764,7 +765,6 @@ 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, {
|
||||
@@ -775,7 +775,6 @@ 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) => {
|
||||
@@ -1159,10 +1158,6 @@ 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'))
|
||||
@@ -1174,10 +1169,6 @@ 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')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ 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'
|
||||
|
||||
@@ -74,10 +73,8 @@ 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
|
||||
|
||||
@@ -145,20 +142,11 @@ 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')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ vi.mock('@sim/db/schema', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ vi.mock('@/lib/auth', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkHybridAuth: (...args: unknown[]) => mockCheckHybridAuth(...args),
|
||||
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
|
||||
}))
|
||||
|
||||
@@ -5,7 +5,7 @@ import { and, eq, isNull, ne } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { AuthType, checkHybridAuth, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { PlatformEvents } from '@/lib/core/telemetry'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
@@ -39,7 +39,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const isInternalCall = auth.authType === 'internal_jwt'
|
||||
const isInternalCall = auth.authType === AuthType.INTERNAL_JWT
|
||||
const userId = auth.userId || null
|
||||
|
||||
let workflowData = await getWorkflowById(workflowId)
|
||||
|
||||
@@ -18,6 +18,7 @@ const { mockCheckSessionOrInternalAuth, mockAuthorizeWorkflowByWorkspacePermissi
|
||||
vi.mock('@/lib/audit/log', () => auditMock)
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
}))
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ vi.mock('@/lib/audit/log', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/auth/hybrid', () => ({
|
||||
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
|
||||
checkHybridAuth: vi.fn(),
|
||||
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: vi.fn(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/components/emails'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { decryptSecret } from '@/lib/core/security/encryption'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
@@ -135,18 +136,18 @@ async function testWebhook(subscription: typeof workspaceNotificationSubscriptio
|
||||
headers['sim-signature'] = `t=${timestamp},v1=${signature}`
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||
|
||||
try {
|
||||
const response = await fetch(webhookConfig.url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
const response = await secureFetchWithValidation(
|
||||
webhookConfig.url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
timeout: 10000,
|
||||
allowHttp: true,
|
||||
},
|
||||
'webhookUrl'
|
||||
)
|
||||
const responseBody = await response.text().catch(() => '')
|
||||
|
||||
return {
|
||||
@@ -157,12 +158,10 @@ async function testWebhook(subscription: typeof workspaceNotificationSubscriptio
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
clearTimeout(timeoutId)
|
||||
const err = error as Error & { name?: string }
|
||||
if (err.name === 'AbortError') {
|
||||
return { success: false, error: 'Request timeout after 10 seconds' }
|
||||
}
|
||||
return { success: false, error: err.message }
|
||||
logger.warn('Webhook test failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
return { success: false, error: 'Failed to deliver webhook' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,13 +267,15 @@ async function testSlack(
|
||||
|
||||
return {
|
||||
success: result.ok,
|
||||
error: result.error,
|
||||
error: result.ok ? undefined : `Slack error: ${result.error || 'unknown'}`,
|
||||
channel: result.channel,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error
|
||||
return { success: false, error: err.message }
|
||||
logger.warn('Slack test notification failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
return { success: false, error: 'Failed to send Slack notification' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { extractEnvVarName, isEnvVarReference, isReference } from '@/executor/constants'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
@@ -14,8 +15,7 @@ import { useDependsOnGate } from './use-depends-on-gate'
|
||||
*
|
||||
* Builds a `SelectorContext` by mapping each `dependsOn` entry through the
|
||||
* canonical index to its `canonicalParamId`, which maps directly to
|
||||
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `collectionId`).
|
||||
* The one special case is `oauthCredential` which maps to `credentialId`.
|
||||
* `SelectorContext` field names (e.g. `siteId`, `teamId`, `oauthCredential`).
|
||||
*
|
||||
* @param blockId - The block containing the selector sub-block
|
||||
* @param subBlock - The sub-block config (must have `selectorKey` set)
|
||||
@@ -70,11 +70,8 @@ export function useSelectorSetup(
|
||||
if (isReference(strValue)) continue
|
||||
|
||||
const canonicalParamId = canonicalIndex.canonicalIdBySubBlockId[depKey] ?? depKey
|
||||
|
||||
if (canonicalParamId === 'oauthCredential') {
|
||||
context.credentialId = strValue
|
||||
} else if (canonicalParamId in CONTEXT_FIELD_SET) {
|
||||
;(context as Record<string, unknown>)[canonicalParamId] = strValue
|
||||
if (SELECTOR_CONTEXT_FIELDS.has(canonicalParamId as keyof SelectorContext)) {
|
||||
context[canonicalParamId as keyof SelectorContext] = strValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,19 +86,3 @@ export function useSelectorSetup(
|
||||
dependencyValues: resolvedDependencyValues,
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXT_FIELD_SET: Record<string, true> = {
|
||||
credentialId: true,
|
||||
domain: true,
|
||||
teamId: true,
|
||||
projectId: true,
|
||||
knowledgeBaseId: true,
|
||||
planId: true,
|
||||
siteId: true,
|
||||
collectionId: true,
|
||||
spreadsheetId: true,
|
||||
fileId: true,
|
||||
baseId: true,
|
||||
datasetId: true,
|
||||
serviceDeskId: true,
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management'
|
||||
const SLACK_OVERRIDES: SelectorOverrides = {
|
||||
transformContext: (context, deps) => {
|
||||
const authMethod = deps.authMethod as string
|
||||
const credentialId =
|
||||
const oauthCredential =
|
||||
authMethod === 'bot_token' ? String(deps.botToken ?? '') : String(deps.credential ?? '')
|
||||
return { ...context, credentialId }
|
||||
return { ...context, oauthCredential }
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ const SubBlockRow = memo(function SubBlockRow({
|
||||
subBlock,
|
||||
value: rawValue,
|
||||
workflowId,
|
||||
credentialId: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
oauthCredential: typeof credentialId === 'string' ? credentialId : undefined,
|
||||
knowledgeBaseId: typeof knowledgeBaseId === 'string' ? knowledgeBaseId : undefined,
|
||||
domain: domainValue,
|
||||
teamId: teamIdValue,
|
||||
|
||||
@@ -12,7 +12,7 @@ interface UseShiftSelectionLockResult {
|
||||
/** Computed ReactFlow props based on current selection state */
|
||||
selectionProps: {
|
||||
selectionOnDrag: boolean
|
||||
panOnDrag: [number, number] | false
|
||||
panOnDrag: number[]
|
||||
selectionKeyCode: string | null
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function useShiftSelectionLock({
|
||||
|
||||
const selectionProps = {
|
||||
selectionOnDrag: !isHandMode || isShiftSelecting,
|
||||
panOnDrag: (isHandMode && !isShiftSelecting ? [0, 1] : false) as [number, number] | false,
|
||||
panOnDrag: isHandMode && !isShiftSelecting ? [0, 1] : [1],
|
||||
selectionKeyCode: isShiftSelecting ? null : 'Shift',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { db } from '@sim/db'
|
||||
import { webhook, workflow as workflowTable } from '@sim/db/schema'
|
||||
import { account, webhook } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { task } from '@trigger.dev/sdk'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing'
|
||||
import {
|
||||
createTimeoutAbortController,
|
||||
getExecutionTimeout,
|
||||
getTimeoutErrorMessage,
|
||||
} from '@/lib/core/execution-limits'
|
||||
import { createTimeoutAbortController, getTimeoutErrorMessage } from '@/lib/core/execution-limits'
|
||||
import { IdempotencyService, webhookIdempotency } from '@/lib/core/idempotency'
|
||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
||||
import { processExecutionFiles } from '@/lib/execution/files'
|
||||
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { WebhookAttachmentProcessor } from '@/lib/webhooks/attachment-processor'
|
||||
@@ -20,7 +15,7 @@ import { fetchAndProcessAirtablePayloads, formatWebhookInput } from '@/lib/webho
|
||||
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||
import { loadDeployedWorkflowState } from '@/lib/workflows/persistence/utils'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import { resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionMetadata } from '@/executor/execution/types'
|
||||
@@ -109,8 +104,8 @@ export type WebhookExecutionPayload = {
|
||||
headers: Record<string, string>
|
||||
path: string
|
||||
blockId?: string
|
||||
workspaceId?: string
|
||||
credentialId?: string
|
||||
credentialAccountUserId?: string
|
||||
}
|
||||
|
||||
export async function executeWebhookJob(payload: WebhookExecutionPayload) {
|
||||
@@ -143,6 +138,22 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the account userId for a credential
|
||||
*/
|
||||
async function resolveCredentialAccountUserId(credentialId: string): Promise<string | undefined> {
|
||||
const resolved = await resolveOAuthAccountId(credentialId)
|
||||
if (!resolved) {
|
||||
return undefined
|
||||
}
|
||||
const [credentialRecord] = await db
|
||||
.select({ userId: account.userId })
|
||||
.from(account)
|
||||
.where(eq(account.id, resolved.accountId))
|
||||
.limit(1)
|
||||
return credentialRecord?.userId
|
||||
}
|
||||
|
||||
async function executeWebhookJobInternal(
|
||||
payload: WebhookExecutionPayload,
|
||||
executionId: string,
|
||||
@@ -155,17 +166,56 @@ async function executeWebhookJobInternal(
|
||||
requestId
|
||||
)
|
||||
|
||||
const userSubscription = await getHighestPrioritySubscription(payload.userId)
|
||||
const asyncTimeout = getExecutionTimeout(
|
||||
userSubscription?.plan as SubscriptionPlan | undefined,
|
||||
'async'
|
||||
)
|
||||
// Resolve workflow record, billing actor, subscription, and timeout
|
||||
const preprocessResult = await preprocessExecution({
|
||||
workflowId: payload.workflowId,
|
||||
userId: payload.userId,
|
||||
triggerType: 'webhook',
|
||||
executionId,
|
||||
requestId,
|
||||
checkRateLimit: false,
|
||||
checkDeployment: false,
|
||||
skipUsageLimits: true,
|
||||
workspaceId: payload.workspaceId,
|
||||
loggingSession,
|
||||
})
|
||||
|
||||
if (!preprocessResult.success) {
|
||||
throw new Error(preprocessResult.error?.message || 'Preprocessing failed in background job')
|
||||
}
|
||||
|
||||
const { workflowRecord, executionTimeout } = preprocessResult
|
||||
if (!workflowRecord) {
|
||||
throw new Error(`Workflow ${payload.workflowId} not found during preprocessing`)
|
||||
}
|
||||
|
||||
const workspaceId = workflowRecord.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error(`Workflow ${payload.workflowId} has no associated workspace`)
|
||||
}
|
||||
|
||||
const workflowVariables = (workflowRecord.variables as Record<string, any>) || {}
|
||||
const asyncTimeout = executionTimeout?.async ?? 120_000
|
||||
const timeoutController = createTimeoutAbortController(asyncTimeout)
|
||||
|
||||
let deploymentVersionId: string | undefined
|
||||
|
||||
try {
|
||||
const workflowData = await loadDeployedWorkflowState(payload.workflowId)
|
||||
// Parallelize workflow state, webhook record, and credential resolution
|
||||
const [workflowData, webhookRows, resolvedCredentialUserId] = await Promise.all([
|
||||
loadDeployedWorkflowState(payload.workflowId, workspaceId),
|
||||
db.select().from(webhook).where(eq(webhook.id, payload.webhookId)).limit(1),
|
||||
payload.credentialId
|
||||
? resolveCredentialAccountUserId(payload.credentialId)
|
||||
: Promise.resolve(undefined),
|
||||
])
|
||||
const credentialAccountUserId = resolvedCredentialUserId
|
||||
if (payload.credentialId && !credentialAccountUserId) {
|
||||
logger.warn(
|
||||
`[${requestId}] Failed to resolve credential account for credential ${payload.credentialId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!workflowData) {
|
||||
throw new Error(
|
||||
'Workflow state not found. The workflow may not be deployed or the deployment data may be corrupted.'
|
||||
@@ -178,28 +228,11 @@ async function executeWebhookJobInternal(
|
||||
? (workflowData.deploymentVersionId as string)
|
||||
: undefined
|
||||
|
||||
const wfRows = await db
|
||||
.select({ workspaceId: workflowTable.workspaceId, variables: workflowTable.variables })
|
||||
.from(workflowTable)
|
||||
.where(eq(workflowTable.id, payload.workflowId))
|
||||
.limit(1)
|
||||
const workspaceId = wfRows[0]?.workspaceId
|
||||
if (!workspaceId) {
|
||||
throw new Error(`Workflow ${payload.workflowId} has no associated workspace`)
|
||||
}
|
||||
const workflowVariables = (wfRows[0]?.variables as Record<string, any>) || {}
|
||||
|
||||
// Handle special Airtable case
|
||||
if (payload.provider === 'airtable') {
|
||||
logger.info(`[${requestId}] Processing Airtable webhook via fetchAndProcessAirtablePayloads`)
|
||||
|
||||
// Load the actual webhook record from database to get providerConfig
|
||||
const [webhookRecord] = await db
|
||||
.select()
|
||||
.from(webhook)
|
||||
.where(eq(webhook.id, payload.webhookId))
|
||||
.limit(1)
|
||||
|
||||
const webhookRecord = webhookRows[0]
|
||||
if (!webhookRecord) {
|
||||
throw new Error(`Webhook record not found: ${payload.webhookId}`)
|
||||
}
|
||||
@@ -210,29 +243,20 @@ async function executeWebhookJobInternal(
|
||||
providerConfig: webhookRecord.providerConfig,
|
||||
}
|
||||
|
||||
// Create a mock workflow object for Airtable processing
|
||||
const mockWorkflow = {
|
||||
id: payload.workflowId,
|
||||
userId: payload.userId,
|
||||
}
|
||||
|
||||
// Get the processed Airtable input
|
||||
const airtableInput = await fetchAndProcessAirtablePayloads(
|
||||
webhookData,
|
||||
mockWorkflow,
|
||||
requestId
|
||||
)
|
||||
|
||||
// If we got input (changes), execute the workflow like other providers
|
||||
if (airtableInput) {
|
||||
logger.info(`[${requestId}] Executing workflow with Airtable changes`)
|
||||
|
||||
// Get workflow for core execution
|
||||
const workflow = await getWorkflowById(payload.workflowId)
|
||||
if (!workflow) {
|
||||
throw new Error(`Workflow ${payload.workflowId} not found`)
|
||||
}
|
||||
|
||||
const metadata: ExecutionMetadata = {
|
||||
requestId,
|
||||
executionId,
|
||||
@@ -240,13 +264,13 @@ async function executeWebhookJobInternal(
|
||||
workspaceId,
|
||||
userId: payload.userId,
|
||||
sessionUserId: undefined,
|
||||
workflowUserId: workflow.userId,
|
||||
workflowUserId: workflowRecord.userId,
|
||||
triggerType: payload.provider || 'webhook',
|
||||
triggerBlockId: payload.blockId,
|
||||
useDraftState: false,
|
||||
startTime: new Date().toISOString(),
|
||||
isClientSession: false,
|
||||
credentialAccountUserId: payload.credentialAccountUserId,
|
||||
credentialAccountUserId,
|
||||
workflowStateOverride: {
|
||||
blocks,
|
||||
edges,
|
||||
@@ -258,7 +282,7 @@ async function executeWebhookJobInternal(
|
||||
|
||||
const snapshot = new ExecutionSnapshot(
|
||||
metadata,
|
||||
workflow,
|
||||
workflowRecord,
|
||||
airtableInput,
|
||||
workflowVariables,
|
||||
[]
|
||||
@@ -329,7 +353,6 @@ async function executeWebhookJobInternal(
|
||||
// No changes to process
|
||||
logger.info(`[${requestId}] No Airtable changes to process`)
|
||||
|
||||
// Start logging session so the complete call has a log entry to update
|
||||
await loggingSession.safeStart({
|
||||
userId: payload.userId,
|
||||
workspaceId,
|
||||
@@ -357,13 +380,6 @@ async function executeWebhookJobInternal(
|
||||
}
|
||||
|
||||
// Format input for standard webhooks
|
||||
// Load the actual webhook to get providerConfig (needed for Teams credentialId)
|
||||
const webhookRows = await db
|
||||
.select()
|
||||
.from(webhook)
|
||||
.where(eq(webhook.id, payload.webhookId))
|
||||
.limit(1)
|
||||
|
||||
const actualWebhook =
|
||||
webhookRows.length > 0
|
||||
? webhookRows[0]
|
||||
@@ -386,7 +402,6 @@ async function executeWebhookJobInternal(
|
||||
if (!input && payload.provider === 'whatsapp') {
|
||||
logger.info(`[${requestId}] No messages in WhatsApp payload, skipping execution`)
|
||||
|
||||
// Start logging session so the complete call has a log entry to update
|
||||
await loggingSession.safeStart({
|
||||
userId: payload.userId,
|
||||
workspaceId,
|
||||
@@ -452,7 +467,6 @@ async function executeWebhookJobInternal(
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error processing trigger file outputs:`, error)
|
||||
// Continue without processing attachments rather than failing execution
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,18 +513,11 @@ async function executeWebhookJobInternal(
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error processing generic webhook files:`, error)
|
||||
// Continue without processing files rather than failing execution
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Executing workflow for ${payload.provider} webhook`)
|
||||
|
||||
// Get workflow for core execution
|
||||
const workflow = await getWorkflowById(payload.workflowId)
|
||||
if (!workflow) {
|
||||
throw new Error(`Workflow ${payload.workflowId} not found`)
|
||||
}
|
||||
|
||||
const metadata: ExecutionMetadata = {
|
||||
requestId,
|
||||
executionId,
|
||||
@@ -518,13 +525,13 @@ async function executeWebhookJobInternal(
|
||||
workspaceId,
|
||||
userId: payload.userId,
|
||||
sessionUserId: undefined,
|
||||
workflowUserId: workflow.userId,
|
||||
workflowUserId: workflowRecord.userId,
|
||||
triggerType: payload.provider || 'webhook',
|
||||
triggerBlockId: payload.blockId,
|
||||
useDraftState: false,
|
||||
startTime: new Date().toISOString(),
|
||||
isClientSession: false,
|
||||
credentialAccountUserId: payload.credentialAccountUserId,
|
||||
credentialAccountUserId,
|
||||
workflowStateOverride: {
|
||||
blocks,
|
||||
edges,
|
||||
@@ -536,7 +543,13 @@ async function executeWebhookJobInternal(
|
||||
|
||||
const triggerInput = input || {}
|
||||
|
||||
const snapshot = new ExecutionSnapshot(metadata, workflow, triggerInput, workflowVariables, [])
|
||||
const snapshot = new ExecutionSnapshot(
|
||||
metadata,
|
||||
workflowRecord,
|
||||
triggerInput,
|
||||
workflowVariables,
|
||||
[]
|
||||
)
|
||||
|
||||
const executionResult = await executeWorkflowCore({
|
||||
snapshot,
|
||||
@@ -611,23 +624,9 @@ async function executeWebhookJobInternal(
|
||||
})
|
||||
|
||||
try {
|
||||
const wfRow = await db
|
||||
.select({ workspaceId: workflowTable.workspaceId })
|
||||
.from(workflowTable)
|
||||
.where(eq(workflowTable.id, payload.workflowId))
|
||||
.limit(1)
|
||||
const errorWorkspaceId = wfRow[0]?.workspaceId
|
||||
|
||||
if (!errorWorkspaceId) {
|
||||
logger.warn(
|
||||
`[${requestId}] Cannot log error: workflow ${payload.workflowId} has no workspace`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
||||
await loggingSession.safeStart({
|
||||
userId: payload.userId,
|
||||
workspaceId: errorWorkspaceId,
|
||||
workspaceId,
|
||||
variables: {},
|
||||
triggerData: {
|
||||
isTest: false,
|
||||
|
||||
@@ -19,6 +19,7 @@ import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||
import { RateLimiter } from '@/lib/core/rate-limiter'
|
||||
import { decryptSecret } from '@/lib/core/security/encryption'
|
||||
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
|
||||
import { formatDuration } from '@/lib/core/utils/formatting'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
||||
@@ -207,18 +208,18 @@ async function deliverWebhook(
|
||||
headers['sim-signature'] = `t=${payload.timestamp},v1=${signature}`
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000)
|
||||
|
||||
try {
|
||||
const response = await fetch(webhookConfig.url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
const response = await secureFetchWithValidation(
|
||||
webhookConfig.url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
timeout: 30000,
|
||||
allowHttp: true,
|
||||
},
|
||||
'webhookUrl'
|
||||
)
|
||||
|
||||
return {
|
||||
success: response.ok,
|
||||
@@ -226,11 +227,13 @@ async function deliverWebhook(
|
||||
error: response.ok ? undefined : `HTTP ${response.status}`,
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
clearTimeout(timeoutId)
|
||||
const err = error as Error & { name?: string }
|
||||
logger.warn('Webhook delivery failed', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
webhookUrl: webhookConfig.url,
|
||||
})
|
||||
return {
|
||||
success: false,
|
||||
error: err.name === 'AbortError' ? 'Request timeout' : err.message,
|
||||
error: 'Failed to deliver webhook',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AshbyIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const AshbyBlock: BlockConfig = {
|
||||
type: 'ashby',
|
||||
@@ -13,6 +14,18 @@ export const AshbyBlock: BlockConfig = {
|
||||
icon: AshbyIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'ashby_application_submit',
|
||||
'ashby_candidate_stage_change',
|
||||
'ashby_candidate_hire',
|
||||
'ashby_candidate_delete',
|
||||
'ashby_job_create',
|
||||
'ashby_offer_create',
|
||||
],
|
||||
},
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
@@ -366,6 +379,14 @@ Output only the ISO 8601 timestamp string, nothing else.`,
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Trigger subBlocks
|
||||
...getTrigger('ashby_application_submit').subBlocks,
|
||||
...getTrigger('ashby_candidate_stage_change').subBlocks,
|
||||
...getTrigger('ashby_candidate_hire').subBlocks,
|
||||
...getTrigger('ashby_candidate_delete').subBlocks,
|
||||
...getTrigger('ashby_job_create').subBlocks,
|
||||
...getTrigger('ashby_offer_create').subBlocks,
|
||||
],
|
||||
|
||||
tools: {
|
||||
|
||||
308
apps/sim/blocks/blocks/evernote.ts
Normal file
308
apps/sim/blocks/blocks/evernote.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { EvernoteIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const EvernoteBlock: BlockConfig = {
|
||||
type: 'evernote',
|
||||
name: 'Evernote',
|
||||
description: 'Manage notes, notebooks, and tags in Evernote',
|
||||
longDescription:
|
||||
'Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.',
|
||||
docsLink: 'https://docs.sim.ai/tools/evernote',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: EvernoteIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Create Note', id: 'create_note' },
|
||||
{ label: 'Get Note', id: 'get_note' },
|
||||
{ label: 'Update Note', id: 'update_note' },
|
||||
{ label: 'Delete Note', id: 'delete_note' },
|
||||
{ label: 'Copy Note', id: 'copy_note' },
|
||||
{ label: 'Search Notes', id: 'search_notes' },
|
||||
{ label: 'Get Notebook', id: 'get_notebook' },
|
||||
{ label: 'Create Notebook', id: 'create_notebook' },
|
||||
{ label: 'List Notebooks', id: 'list_notebooks' },
|
||||
{ label: 'Create Tag', id: 'create_tag' },
|
||||
{ label: 'List Tags', id: 'list_tags' },
|
||||
],
|
||||
value: () => 'create_note',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'Developer Token',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Enter your Evernote developer token',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Note title',
|
||||
condition: { field: 'operation', value: 'create_note' },
|
||||
required: { field: 'operation', value: 'create_note' },
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
title: 'Content',
|
||||
type: 'long-input',
|
||||
placeholder: 'Note content (plain text or ENML)',
|
||||
condition: { field: 'operation', value: 'create_note' },
|
||||
required: { field: 'operation', value: 'create_note' },
|
||||
},
|
||||
{
|
||||
id: 'noteGuid',
|
||||
title: 'Note GUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter the note GUID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_note', 'update_note', 'delete_note', 'copy_note'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['get_note', 'update_note', 'delete_note', 'copy_note'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updateTitle',
|
||||
title: 'New Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'New title (leave empty to keep current)',
|
||||
condition: { field: 'operation', value: 'update_note' },
|
||||
},
|
||||
{
|
||||
id: 'updateContent',
|
||||
title: 'New Content',
|
||||
type: 'long-input',
|
||||
placeholder: 'New content (leave empty to keep current)',
|
||||
condition: { field: 'operation', value: 'update_note' },
|
||||
},
|
||||
{
|
||||
id: 'toNotebookGuid',
|
||||
title: 'Destination Notebook GUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'GUID of the destination notebook',
|
||||
condition: { field: 'operation', value: 'copy_note' },
|
||||
required: { field: 'operation', value: 'copy_note' },
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., "tag:work intitle:meeting"',
|
||||
condition: { field: 'operation', value: 'search_notes' },
|
||||
required: { field: 'operation', value: 'search_notes' },
|
||||
},
|
||||
{
|
||||
id: 'notebookGuid',
|
||||
title: 'Notebook GUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Notebook GUID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_note', 'update_note', 'search_notes', 'get_notebook'],
|
||||
},
|
||||
required: { field: 'operation', value: 'get_notebook' },
|
||||
},
|
||||
{
|
||||
id: 'notebookName',
|
||||
title: 'Notebook Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Name for the new notebook',
|
||||
condition: { field: 'operation', value: 'create_notebook' },
|
||||
required: { field: 'operation', value: 'create_notebook' },
|
||||
},
|
||||
{
|
||||
id: 'stack',
|
||||
title: 'Stack',
|
||||
type: 'short-input',
|
||||
placeholder: 'Stack name (optional)',
|
||||
condition: { field: 'operation', value: 'create_notebook' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'tagName',
|
||||
title: 'Tag Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Name for the new tag',
|
||||
condition: { field: 'operation', value: 'create_tag' },
|
||||
required: { field: 'operation', value: 'create_tag' },
|
||||
},
|
||||
{
|
||||
id: 'parentGuid',
|
||||
title: 'Parent Tag GUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Parent tag GUID (optional)',
|
||||
condition: { field: 'operation', value: 'create_tag' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'tagNames',
|
||||
title: 'Tags',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated tags (e.g., "work, meeting, urgent")',
|
||||
condition: { field: 'operation', value: ['create_note', 'update_note'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'maxNotes',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: '25',
|
||||
condition: { field: 'operation', value: 'search_notes' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'offset',
|
||||
title: 'Offset',
|
||||
type: 'short-input',
|
||||
placeholder: '0',
|
||||
condition: { field: 'operation', value: 'search_notes' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'withContent',
|
||||
title: 'Include Content',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Yes', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
value: () => 'true',
|
||||
condition: { field: 'operation', value: 'get_note' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'evernote_copy_note',
|
||||
'evernote_create_note',
|
||||
'evernote_create_notebook',
|
||||
'evernote_create_tag',
|
||||
'evernote_delete_note',
|
||||
'evernote_get_note',
|
||||
'evernote_get_notebook',
|
||||
'evernote_list_notebooks',
|
||||
'evernote_list_tags',
|
||||
'evernote_search_notes',
|
||||
'evernote_update_note',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `evernote_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { operation, apiKey, ...rest } = params
|
||||
|
||||
switch (operation) {
|
||||
case 'create_note':
|
||||
return {
|
||||
apiKey,
|
||||
title: rest.title,
|
||||
content: rest.content,
|
||||
notebookGuid: rest.notebookGuid || undefined,
|
||||
tagNames: rest.tagNames || undefined,
|
||||
}
|
||||
case 'get_note':
|
||||
return {
|
||||
apiKey,
|
||||
noteGuid: rest.noteGuid,
|
||||
withContent: rest.withContent !== 'false',
|
||||
}
|
||||
case 'update_note':
|
||||
return {
|
||||
apiKey,
|
||||
noteGuid: rest.noteGuid,
|
||||
title: rest.updateTitle || undefined,
|
||||
content: rest.updateContent || undefined,
|
||||
notebookGuid: rest.notebookGuid || undefined,
|
||||
tagNames: rest.tagNames || undefined,
|
||||
}
|
||||
case 'delete_note':
|
||||
return {
|
||||
apiKey,
|
||||
noteGuid: rest.noteGuid,
|
||||
}
|
||||
case 'copy_note':
|
||||
return {
|
||||
apiKey,
|
||||
noteGuid: rest.noteGuid,
|
||||
toNotebookGuid: rest.toNotebookGuid,
|
||||
}
|
||||
case 'search_notes':
|
||||
return {
|
||||
apiKey,
|
||||
query: rest.query,
|
||||
notebookGuid: rest.notebookGuid || undefined,
|
||||
offset: rest.offset ? Number(rest.offset) : 0,
|
||||
maxNotes: rest.maxNotes ? Number(rest.maxNotes) : 25,
|
||||
}
|
||||
case 'get_notebook':
|
||||
return {
|
||||
apiKey,
|
||||
notebookGuid: rest.notebookGuid,
|
||||
}
|
||||
case 'create_notebook':
|
||||
return {
|
||||
apiKey,
|
||||
name: rest.notebookName,
|
||||
stack: rest.stack || undefined,
|
||||
}
|
||||
case 'list_notebooks':
|
||||
return { apiKey }
|
||||
case 'create_tag':
|
||||
return {
|
||||
apiKey,
|
||||
name: rest.tagName,
|
||||
parentGuid: rest.parentGuid || undefined,
|
||||
}
|
||||
case 'list_tags':
|
||||
return { apiKey }
|
||||
default:
|
||||
return { apiKey }
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
apiKey: { type: 'string', description: 'Evernote developer token' },
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
title: { type: 'string', description: 'Note title' },
|
||||
content: { type: 'string', description: 'Note content' },
|
||||
noteGuid: { type: 'string', description: 'Note GUID' },
|
||||
updateTitle: { type: 'string', description: 'New note title' },
|
||||
updateContent: { type: 'string', description: 'New note content' },
|
||||
toNotebookGuid: { type: 'string', description: 'Destination notebook GUID' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
notebookGuid: { type: 'string', description: 'Notebook GUID' },
|
||||
notebookName: { type: 'string', description: 'Notebook name' },
|
||||
stack: { type: 'string', description: 'Notebook stack name' },
|
||||
tagName: { type: 'string', description: 'Tag name' },
|
||||
parentGuid: { type: 'string', description: 'Parent tag GUID' },
|
||||
tagNames: { type: 'string', description: 'Comma-separated tag names' },
|
||||
maxNotes: { type: 'string', description: 'Maximum number of results' },
|
||||
offset: { type: 'string', description: 'Starting index for results' },
|
||||
withContent: { type: 'string', description: 'Whether to include note content' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
note: { type: 'json', description: 'Note data' },
|
||||
notebook: { type: 'json', description: 'Notebook data' },
|
||||
notebooks: { type: 'json', description: 'List of notebooks' },
|
||||
tag: { type: 'json', description: 'Tag data' },
|
||||
tags: { type: 'json', description: 'List of tags' },
|
||||
totalNotes: { type: 'number', description: 'Total number of matching notes' },
|
||||
notes: { type: 'json', description: 'List of note metadata' },
|
||||
success: { type: 'boolean', description: 'Whether the operation succeeded' },
|
||||
noteGuid: { type: 'string', description: 'GUID of the affected note' },
|
||||
},
|
||||
}
|
||||
211
apps/sim/blocks/blocks/fathom.ts
Normal file
211
apps/sim/blocks/blocks/fathom.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { FathomIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { FathomResponse } from '@/tools/fathom/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
import { fathomTriggerOptions } from '@/triggers/fathom/utils'
|
||||
|
||||
export const FathomBlock: BlockConfig<FathomResponse> = {
|
||||
type: 'fathom',
|
||||
name: 'Fathom',
|
||||
description: 'Access meeting recordings, transcripts, and summaries',
|
||||
authMode: AuthMode.ApiKey,
|
||||
triggerAllowed: true,
|
||||
longDescription:
|
||||
'Integrate Fathom AI Notetaker into your workflow. List meetings, get transcripts and summaries, and manage team members and teams. Can also trigger workflows when new meeting content is ready.',
|
||||
docsLink: 'https://docs.sim.ai/tools/fathom',
|
||||
category: 'tools',
|
||||
bgColor: '#181C1E',
|
||||
icon: FathomIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'List Meetings', id: 'fathom_list_meetings' },
|
||||
{ label: 'Get Summary', id: 'fathom_get_summary' },
|
||||
{ label: 'Get Transcript', id: 'fathom_get_transcript' },
|
||||
{ label: 'List Team Members', id: 'fathom_list_team_members' },
|
||||
{ label: 'List Teams', id: 'fathom_list_teams' },
|
||||
],
|
||||
value: () => 'fathom_list_meetings',
|
||||
},
|
||||
{
|
||||
id: 'recordingId',
|
||||
title: 'Recording ID',
|
||||
type: 'short-input',
|
||||
required: { field: 'operation', value: ['fathom_get_summary', 'fathom_get_transcript'] },
|
||||
placeholder: 'Enter the recording ID',
|
||||
condition: { field: 'operation', value: ['fathom_get_summary', 'fathom_get_transcript'] },
|
||||
},
|
||||
{
|
||||
id: 'includeSummary',
|
||||
title: 'Include Summary',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
},
|
||||
{
|
||||
id: 'includeTranscript',
|
||||
title: 'Include Transcript',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
},
|
||||
{
|
||||
id: 'includeActionItems',
|
||||
title: 'Include Action Items',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
},
|
||||
{
|
||||
id: 'includeCrmMatches',
|
||||
title: 'Include CRM Matches',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
},
|
||||
{
|
||||
id: 'createdAfter',
|
||||
title: 'Created After',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 timestamp (e.g., 2025-01-01T00:00:00Z)',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'createdBefore',
|
||||
title: 'Created Before',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 timestamp (e.g., 2025-12-31T23:59:59Z)',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string.',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recordedBy',
|
||||
title: 'Recorded By',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by recorder email',
|
||||
condition: { field: 'operation', value: 'fathom_list_meetings' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'teams',
|
||||
title: 'Team',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by team name',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['fathom_list_meetings', 'fathom_list_team_members'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'cursor',
|
||||
title: 'Pagination Cursor',
|
||||
type: 'short-input',
|
||||
placeholder: 'Cursor from a previous response',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['fathom_list_meetings', 'fathom_list_team_members', 'fathom_list_teams'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'Enter your Fathom API key',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 'selectedTriggerId',
|
||||
title: 'Trigger Type',
|
||||
type: 'dropdown',
|
||||
mode: 'trigger',
|
||||
options: fathomTriggerOptions,
|
||||
value: () => 'fathom_new_meeting',
|
||||
required: true,
|
||||
},
|
||||
...getTrigger('fathom_new_meeting').subBlocks,
|
||||
...getTrigger('fathom_webhook').subBlocks,
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'fathom_list_meetings',
|
||||
'fathom_get_summary',
|
||||
'fathom_get_transcript',
|
||||
'fathom_list_team_members',
|
||||
'fathom_list_teams',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
return params.operation || 'fathom_list_meetings'
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'Fathom API key' },
|
||||
recordingId: { type: 'string', description: 'Recording ID for summary or transcript' },
|
||||
includeSummary: { type: 'string', description: 'Include summary in meetings response' },
|
||||
includeTranscript: { type: 'string', description: 'Include transcript in meetings response' },
|
||||
includeActionItems: {
|
||||
type: 'string',
|
||||
description: 'Include action items in meetings response',
|
||||
},
|
||||
includeCrmMatches: {
|
||||
type: 'string',
|
||||
description: 'Include linked CRM matches in meetings response',
|
||||
},
|
||||
createdAfter: { type: 'string', description: 'Filter meetings created after this timestamp' },
|
||||
createdBefore: {
|
||||
type: 'string',
|
||||
description: 'Filter meetings created before this timestamp',
|
||||
},
|
||||
recordedBy: { type: 'string', description: 'Filter by recorder email' },
|
||||
teams: { type: 'string', description: 'Filter by team name' },
|
||||
cursor: { type: 'string', description: 'Pagination cursor for next page' },
|
||||
},
|
||||
outputs: {
|
||||
meetings: { type: 'json', description: 'List of meetings' },
|
||||
template_name: { type: 'string', description: 'Summary template name' },
|
||||
markdown_formatted: { type: 'string', description: 'Markdown-formatted summary' },
|
||||
transcript: { type: 'json', description: 'Meeting transcript entries' },
|
||||
members: { type: 'json', description: 'List of team members' },
|
||||
teams: { type: 'json', description: 'List of teams' },
|
||||
next_cursor: { type: 'string', description: 'Pagination cursor' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['fathom_new_meeting', 'fathom_webhook'],
|
||||
},
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export const GenericWebhookBlock: BlockConfig = {
|
||||
bestPractices: `
|
||||
- You can test the webhook by sending a request to the webhook URL. E.g. depending on authorization: curl -X POST http://localhost:3000/api/webhooks/trigger/d8abcf0d-1ee5-4b77-bb07-b1e8142ea4e9 -H "Content-Type: application/json" -H "X-Sim-Secret: 1234" -d '{"message": "Test webhook trigger", "data": {"key": "v"}}'
|
||||
- Continuing example above, the body can be accessed in downstream block using dot notation. E.g. <webhook1.message> and <webhook1.data.key>
|
||||
- To deduplicate incoming events, set the Deduplication Field to a dot-notation path of a unique field in the payload (e.g. "event.id"). Duplicate values within 7 days will be skipped.
|
||||
- Only use when there's no existing integration for the service with triggerAllowed flag set to true.
|
||||
`,
|
||||
subBlocks: [...getTrigger('generic_webhook').subBlocks],
|
||||
|
||||
294
apps/sim/blocks/blocks/google_ads.ts
Normal file
294
apps/sim/blocks/blocks/google_ads.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { GoogleAdsIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const GoogleAdsBlock: BlockConfig = {
|
||||
type: 'google_ads',
|
||||
name: 'Google Ads',
|
||||
description: 'Query campaigns, ad groups, and performance metrics',
|
||||
longDescription:
|
||||
'Connect to Google Ads to list accessible accounts, list campaigns, view ad group details, get performance metrics, and run custom GAQL queries.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_ads',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleAdsIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'List Customers', id: 'list_customers' },
|
||||
{ label: 'List Campaigns', id: 'list_campaigns' },
|
||||
{ label: 'Campaign Performance', id: 'campaign_performance' },
|
||||
{ label: 'List Ad Groups', id: 'list_ad_groups' },
|
||||
{ label: 'Ad Performance', id: 'ad_performance' },
|
||||
{ label: 'Custom Query (GAQL)', id: 'search' },
|
||||
],
|
||||
value: () => 'list_campaigns',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Ads Account',
|
||||
type: 'oauth-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'basic',
|
||||
required: true,
|
||||
serviceId: 'google-ads',
|
||||
requiredScopes: getScopesForService('google-ads'),
|
||||
placeholder: 'Select Google Ads account',
|
||||
},
|
||||
{
|
||||
id: 'manualCredential',
|
||||
title: 'Google Ads Account',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'oauthCredential',
|
||||
mode: 'advanced',
|
||||
placeholder: 'Enter credential ID',
|
||||
required: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'developerToken',
|
||||
title: 'Developer Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Google Ads API developer token',
|
||||
required: true,
|
||||
password: true,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'customerId',
|
||||
title: 'Customer ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Google Ads customer ID (no dashes)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'list_customers',
|
||||
not: true,
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: 'list_customers',
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'managerCustomerId',
|
||||
title: 'Manager Customer ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Manager account ID (optional)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'list_customers',
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'query',
|
||||
title: 'GAQL Query',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
"SELECT campaign.id, campaign.name, metrics.impressions FROM campaign WHERE campaign.status = 'ENABLED'",
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
required: { field: 'operation', value: 'search' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a Google Ads Query Language (GAQL) query based on the user's description.
|
||||
The query should:
|
||||
- Use valid GAQL syntax
|
||||
- Include relevant metrics when asking about performance
|
||||
- Include segments.date with a date range when using metrics
|
||||
- Be efficient and well-formatted
|
||||
|
||||
Common resources: campaign, ad_group, ad_group_ad, keyword_view, search_term_view
|
||||
Common metrics: metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions
|
||||
Date ranges: LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, YESTERDAY
|
||||
|
||||
Examples:
|
||||
- "active campaigns" -> SELECT campaign.id, campaign.name, campaign.status FROM campaign WHERE campaign.status = 'ENABLED'
|
||||
- "campaign spend last week" -> SELECT campaign.name, metrics.cost_micros, segments.date FROM campaign WHERE segments.date DURING LAST_7_DAYS AND campaign.status != 'REMOVED'
|
||||
|
||||
Return ONLY the GAQL query - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the query you want to run...',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'campaignId',
|
||||
title: 'Campaign ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Campaign ID to filter by',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['campaign_performance', 'list_ad_groups', 'ad_performance'],
|
||||
},
|
||||
required: { field: 'operation', value: 'list_ad_groups' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'adGroupId',
|
||||
title: 'Ad Group ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Ad group ID to filter by',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'ad_performance' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'status',
|
||||
title: 'Status Filter',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All (except removed)', id: '' },
|
||||
{ label: 'Enabled', id: 'ENABLED' },
|
||||
{ label: 'Paused', id: 'PAUSED' },
|
||||
],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['list_campaigns', 'list_ad_groups'] },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'dateRange',
|
||||
title: 'Date Range',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Last 30 Days', id: 'LAST_30_DAYS' },
|
||||
{ label: 'Last 7 Days', id: 'LAST_7_DAYS' },
|
||||
{ label: 'Today', id: 'TODAY' },
|
||||
{ label: 'Yesterday', id: 'YESTERDAY' },
|
||||
{ label: 'This Month', id: 'THIS_MONTH' },
|
||||
{ label: 'Last Month', id: 'LAST_MONTH' },
|
||||
{ label: 'Custom', id: 'CUSTOM' },
|
||||
],
|
||||
condition: { field: 'operation', value: ['campaign_performance', 'ad_performance'] },
|
||||
value: () => 'LAST_30_DAYS',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'startDate',
|
||||
title: 'Start Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'dateRange', value: 'CUSTOM' },
|
||||
required: { field: 'dateRange', value: 'CUSTOM' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'endDate',
|
||||
title: 'End Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
condition: { field: 'dateRange', value: 'CUSTOM' },
|
||||
required: { field: 'dateRange', value: 'CUSTOM' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'pageToken',
|
||||
title: 'Page Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Pagination token',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Maximum results to return',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_campaigns', 'list_ad_groups', 'ad_performance'],
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'google_ads_list_customers',
|
||||
'google_ads_search',
|
||||
'google_ads_list_campaigns',
|
||||
'google_ads_campaign_performance',
|
||||
'google_ads_list_ad_groups',
|
||||
'google_ads_ad_performance',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `google_ads_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { oauthCredential, dateRange, limit, ...rest } = params
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
...rest,
|
||||
oauthCredential,
|
||||
}
|
||||
|
||||
if (dateRange && dateRange !== 'CUSTOM') {
|
||||
result.dateRange = dateRange
|
||||
}
|
||||
|
||||
if (limit !== undefined && limit !== '') {
|
||||
result.limit = Number(limit)
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
oauthCredential: { type: 'string', description: 'Google Ads OAuth credential' },
|
||||
developerToken: { type: 'string', description: 'Google Ads API developer token' },
|
||||
customerId: { type: 'string', description: 'Google Ads customer ID (numeric, no dashes)' },
|
||||
managerCustomerId: { type: 'string', description: 'Manager account customer ID' },
|
||||
query: { type: 'string', description: 'GAQL query to execute' },
|
||||
campaignId: { type: 'string', description: 'Campaign ID to filter by' },
|
||||
adGroupId: { type: 'string', description: 'Ad group ID to filter by' },
|
||||
status: { type: 'string', description: 'Status filter (ENABLED, PAUSED)' },
|
||||
dateRange: { type: 'string', description: 'Date range for performance queries' },
|
||||
startDate: { type: 'string', description: 'Custom start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'Custom end date (YYYY-MM-DD)' },
|
||||
pageToken: { type: 'string', description: 'Pagination token' },
|
||||
limit: { type: 'number', description: 'Maximum results to return' },
|
||||
},
|
||||
outputs: {
|
||||
customerIds: {
|
||||
type: 'json',
|
||||
description: 'List of accessible customer IDs (list_customers)',
|
||||
},
|
||||
results: {
|
||||
type: 'json',
|
||||
description: 'Query results (search)',
|
||||
},
|
||||
campaigns: {
|
||||
type: 'json',
|
||||
description: 'Campaign data (list_campaigns, campaign_performance)',
|
||||
},
|
||||
adGroups: {
|
||||
type: 'json',
|
||||
description: 'Ad group data (list_ad_groups)',
|
||||
},
|
||||
ads: {
|
||||
type: 'json',
|
||||
description: 'Ad performance data (ad_performance)',
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of results',
|
||||
},
|
||||
totalResultsCount: {
|
||||
type: 'number',
|
||||
description: 'Total results count (search)',
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for next page of results',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export const GrainBlock: BlockConfig = {
|
||||
{ label: 'List Recordings', id: 'grain_list_recordings' },
|
||||
{ label: 'Get Recording', id: 'grain_get_recording' },
|
||||
{ label: 'Get Transcript', id: 'grain_get_transcript' },
|
||||
{ label: 'List Views', id: 'grain_list_views' },
|
||||
{ label: 'List Teams', id: 'grain_list_teams' },
|
||||
{ label: 'List Meeting Types', id: 'grain_list_meeting_types' },
|
||||
{ label: 'Create Webhook', id: 'grain_create_hook' },
|
||||
@@ -72,7 +73,7 @@ export const GrainBlock: BlockConfig = {
|
||||
placeholder: 'ISO8601 timestamp (e.g., 2024-01-01T00:00:00Z)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings'],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -96,7 +97,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'ISO8601 timestamp (e.g., 2024-01-01T00:00:00Z)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings'],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -125,7 +126,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
value: () => '',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings'],
|
||||
},
|
||||
},
|
||||
// Title search
|
||||
@@ -162,7 +163,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Filter by team UUID (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings'],
|
||||
},
|
||||
},
|
||||
// Meeting type ID filter
|
||||
@@ -173,7 +174,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Filter by meeting type UUID (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings'],
|
||||
},
|
||||
},
|
||||
// Include highlights
|
||||
@@ -183,7 +184,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
type: 'switch',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings', 'grain_get_recording'],
|
||||
},
|
||||
},
|
||||
// Include participants
|
||||
@@ -193,7 +194,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
type: 'switch',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings', 'grain_get_recording'],
|
||||
},
|
||||
},
|
||||
// Include AI summary
|
||||
@@ -203,7 +204,18 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
type: 'switch',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_list_recordings', 'grain_get_recording', 'grain_create_hook'],
|
||||
value: ['grain_list_recordings', 'grain_get_recording'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'viewId',
|
||||
title: 'View ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Grain view UUID',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['grain_create_hook'],
|
||||
},
|
||||
},
|
||||
// Include calendar event (get_recording only)
|
||||
@@ -271,6 +283,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
'grain_list_recordings',
|
||||
'grain_get_recording',
|
||||
'grain_get_transcript',
|
||||
'grain_list_views',
|
||||
'grain_list_teams',
|
||||
'grain_list_meeting_types',
|
||||
'grain_create_hook',
|
||||
@@ -327,6 +340,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
|
||||
case 'grain_list_teams':
|
||||
case 'grain_list_meeting_types':
|
||||
case 'grain_list_views':
|
||||
case 'grain_list_hooks':
|
||||
return baseParams
|
||||
|
||||
@@ -334,17 +348,13 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
if (!params.hookUrl?.trim()) {
|
||||
throw new Error('Webhook URL is required.')
|
||||
}
|
||||
if (!params.viewId?.trim()) {
|
||||
throw new Error('View ID is required.')
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
hookUrl: params.hookUrl.trim(),
|
||||
filterBeforeDatetime: params.beforeDatetime || undefined,
|
||||
filterAfterDatetime: params.afterDatetime || undefined,
|
||||
filterParticipantScope: params.participantScope || undefined,
|
||||
filterTeamId: params.teamId || undefined,
|
||||
filterMeetingTypeId: params.meetingTypeId || undefined,
|
||||
includeHighlights: params.includeHighlights || false,
|
||||
includeParticipants: params.includeParticipants || false,
|
||||
includeAiSummary: params.includeAiSummary || false,
|
||||
viewId: params.viewId.trim(),
|
||||
}
|
||||
|
||||
case 'grain_delete_hook':
|
||||
@@ -367,6 +377,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
apiKey: { type: 'string', description: 'Grain API key (Personal Access Token)' },
|
||||
recordingId: { type: 'string', description: 'Recording UUID' },
|
||||
cursor: { type: 'string', description: 'Pagination cursor' },
|
||||
viewId: { type: 'string', description: 'Grain view UUID for webhook subscriptions' },
|
||||
beforeDatetime: {
|
||||
type: 'string',
|
||||
description: 'Filter recordings before this ISO8601 timestamp',
|
||||
@@ -416,6 +427,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
teamsList: { type: 'json', description: 'Array of team objects' },
|
||||
// Meeting type outputs
|
||||
meetingTypes: { type: 'json', description: 'Array of meeting type objects' },
|
||||
views: { type: 'json', description: 'Array of Grain views' },
|
||||
// Hook outputs
|
||||
hooks: { type: 'json', description: 'Array of webhook objects' },
|
||||
hook: { type: 'json', description: 'Created webhook data' },
|
||||
|
||||
@@ -47,6 +47,7 @@ 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',
|
||||
},
|
||||
@@ -164,7 +165,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'issueKey',
|
||||
placeholder: 'Enter Jira issue key',
|
||||
dependsOn: ['credential', 'domain', 'projectId', 'manualProjectId'],
|
||||
dependsOn: ['credential', 'domain'],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
@@ -673,6 +674,31 @@ 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,
|
||||
@@ -707,6 +733,7 @@ Return ONLY the comment text - no explanations.`,
|
||||
'jira_add_watcher',
|
||||
'jira_remove_watcher',
|
||||
'jira_get_users',
|
||||
'jira_search_users',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -767,6 +794,8 @@ 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'
|
||||
}
|
||||
@@ -1023,6 +1052,18 @@ 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
|
||||
}
|
||||
@@ -1102,6 +1143,13 @@ 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
|
||||
|
||||
270
apps/sim/blocks/blocks/obsidian.ts
Normal file
270
apps/sim/blocks/blocks/obsidian.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
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' },
|
||||
},
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Parallel AI into the workflow. Can search the web, extract information from URLs, and conduct deep research.',
|
||||
docsLink: 'https://docs.parallel.ai/',
|
||||
docsLink: 'https://docs.sim.ai/tools/parallel-ai',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ParallelIcon,
|
||||
@@ -56,7 +56,7 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
title: 'Extract Objective',
|
||||
type: 'long-input',
|
||||
placeholder: 'What information to extract from the URLs?',
|
||||
required: true,
|
||||
required: false,
|
||||
condition: { field: 'operation', value: 'extract' },
|
||||
},
|
||||
{
|
||||
@@ -89,6 +89,37 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'deep_research' },
|
||||
},
|
||||
{
|
||||
id: 'search_mode',
|
||||
title: 'Search Mode',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'One-Shot', id: 'one-shot' },
|
||||
{ label: 'Agentic', id: 'agentic' },
|
||||
{ label: 'Fast', id: 'fast' },
|
||||
],
|
||||
value: () => 'one-shot',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'search_include_domains',
|
||||
title: 'Include Domains',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated domains to include (e.g., .edu, example.com)',
|
||||
required: false,
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'search_exclude_domains',
|
||||
title: 'Exclude Domains',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated domains to exclude',
|
||||
required: false,
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'include_domains',
|
||||
title: 'Include Domains',
|
||||
@@ -96,6 +127,7 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
placeholder: 'Comma-separated domains to include',
|
||||
required: false,
|
||||
condition: { field: 'operation', value: 'deep_research' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'exclude_domains',
|
||||
@@ -104,37 +136,37 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
placeholder: 'Comma-separated domains to exclude',
|
||||
required: false,
|
||||
condition: { field: 'operation', value: 'deep_research' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'processor',
|
||||
title: 'Processor',
|
||||
title: 'Research Processor',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Lite', id: 'lite' },
|
||||
{ label: 'Base', id: 'base' },
|
||||
{ label: 'Core', id: 'core' },
|
||||
{ label: 'Core 2x', id: 'core2x' },
|
||||
{ label: 'Pro', id: 'pro' },
|
||||
{ label: 'Ultra', id: 'ultra' },
|
||||
{ label: 'Ultra 2x', id: 'ultra2x' },
|
||||
{ label: 'Ultra 4x', id: 'ultra4x' },
|
||||
{ label: 'Pro Fast', id: 'pro-fast' },
|
||||
{ label: 'Ultra Fast', id: 'ultra-fast' },
|
||||
],
|
||||
value: () => 'base',
|
||||
condition: { field: 'operation', value: ['search', 'deep_research'] },
|
||||
value: () => 'pro',
|
||||
condition: { field: 'operation', value: 'deep_research' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'max_results',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: '5',
|
||||
placeholder: '10',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'max_chars_per_result',
|
||||
title: 'Max Chars',
|
||||
title: 'Max Chars Per Result',
|
||||
type: 'short-input',
|
||||
placeholder: '1500',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
@@ -149,8 +181,6 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
access: ['parallel_search', 'parallel_extract', 'parallel_deep_research'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
if (params.extract_objective) params.objective = params.extract_objective
|
||||
if (params.research_input) params.input = params.research_input
|
||||
switch (params.operation) {
|
||||
case 'search':
|
||||
return 'parallel_search'
|
||||
@@ -174,21 +204,30 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
.filter((query: string) => query.length > 0)
|
||||
if (queries.length > 0) {
|
||||
result.search_queries = queries
|
||||
} else {
|
||||
result.search_queries = undefined
|
||||
}
|
||||
}
|
||||
if (params.search_mode && params.search_mode !== 'one-shot') {
|
||||
result.mode = params.search_mode
|
||||
}
|
||||
if (params.max_results) result.max_results = Number(params.max_results)
|
||||
if (params.max_chars_per_result) {
|
||||
result.max_chars_per_result = Number(params.max_chars_per_result)
|
||||
}
|
||||
result.include_domains = params.search_include_domains || undefined
|
||||
result.exclude_domains = params.search_exclude_domains || undefined
|
||||
}
|
||||
|
||||
if (operation === 'extract') {
|
||||
if (params.extract_objective) result.objective = params.extract_objective
|
||||
result.excerpts = !(params.excerpts === 'false' || params.excerpts === false)
|
||||
result.full_content = params.full_content === 'true' || params.full_content === true
|
||||
}
|
||||
|
||||
if (operation === 'deep_research') {
|
||||
if (params.research_input) result.input = params.research_input
|
||||
if (params.processor) result.processor = params.processor
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
@@ -202,29 +241,34 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
excerpts: { type: 'boolean', description: 'Include excerpts' },
|
||||
full_content: { type: 'boolean', description: 'Include full content' },
|
||||
research_input: { type: 'string', description: 'Deep research query' },
|
||||
include_domains: { type: 'string', description: 'Domains to include' },
|
||||
exclude_domains: { type: 'string', description: 'Domains to exclude' },
|
||||
processor: { type: 'string', description: 'Processing method' },
|
||||
include_domains: { type: 'string', description: 'Domains to include (deep research)' },
|
||||
exclude_domains: { type: 'string', description: 'Domains to exclude (deep research)' },
|
||||
search_include_domains: { type: 'string', description: 'Domains to include (search)' },
|
||||
search_exclude_domains: { type: 'string', description: 'Domains to exclude (search)' },
|
||||
search_mode: { type: 'string', description: 'Search mode (one-shot, agentic, fast)' },
|
||||
processor: { type: 'string', description: 'Research processing tier' },
|
||||
max_results: { type: 'number', description: 'Maximum number of results' },
|
||||
max_chars_per_result: { type: 'number', description: 'Maximum characters per result' },
|
||||
apiKey: { type: 'string', description: 'Parallel AI API key' },
|
||||
},
|
||||
outputs: {
|
||||
results: { type: 'string', description: 'Search or extract results (JSON stringified)' },
|
||||
results: {
|
||||
type: 'json',
|
||||
description: 'Search or extract results (array of url, title, excerpts)',
|
||||
},
|
||||
search_id: { type: 'string', description: 'Search request ID (for search)' },
|
||||
extract_id: { type: 'string', description: 'Extract request ID (for extract)' },
|
||||
status: { type: 'string', description: 'Task status (for deep research)' },
|
||||
run_id: { type: 'string', description: 'Task run ID (for deep research)' },
|
||||
message: { type: 'string', description: 'Status message (for deep research)' },
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Research content (for deep research, JSON stringified)',
|
||||
type: 'json',
|
||||
description: 'Research content (for deep research, structured based on output_schema)',
|
||||
},
|
||||
basis: {
|
||||
type: 'string',
|
||||
description: 'Citations and sources (for deep research, JSON stringified)',
|
||||
},
|
||||
metadata: {
|
||||
type: 'string',
|
||||
description: 'Task metadata (for deep research, JSON stringified)',
|
||||
type: 'json',
|
||||
description:
|
||||
'Citations and sources with field, reasoning, citations, confidence (for deep research)',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@ 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 { FathomBlock } from '@/blocks/blocks/fathom'
|
||||
import { FileBlock, FileV2Block, FileV3Block } from '@/blocks/blocks/file'
|
||||
import { FirecrawlBlock } from '@/blocks/blocks/firecrawl'
|
||||
import { FirefliesBlock, FirefliesV2Block } from '@/blocks/blocks/fireflies'
|
||||
@@ -50,6 +52,7 @@ import { GitLabBlock } from '@/blocks/blocks/gitlab'
|
||||
import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail'
|
||||
import { GongBlock } from '@/blocks/blocks/gong'
|
||||
import { GoogleSearchBlock } from '@/blocks/blocks/google'
|
||||
import { GoogleAdsBlock } from '@/blocks/blocks/google_ads'
|
||||
import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery'
|
||||
import { GoogleBooksBlock } from '@/blocks/blocks/google_books'
|
||||
import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar'
|
||||
@@ -113,6 +116,7 @@ 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'
|
||||
@@ -233,7 +237,9 @@ export const registry: Record<string, BlockConfig> = {
|
||||
dynamodb: DynamoDBBlock,
|
||||
elasticsearch: ElasticsearchBlock,
|
||||
elevenlabs: ElevenLabsBlock,
|
||||
fathom: FathomBlock,
|
||||
enrich: EnrichBlock,
|
||||
evernote: EvernoteBlock,
|
||||
evaluator: EvaluatorBlock,
|
||||
exa: ExaBlock,
|
||||
file: FileBlock,
|
||||
@@ -252,6 +258,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
gmail_v2: GmailV2Block,
|
||||
google_calendar: GoogleCalendarBlock,
|
||||
google_calendar_v2: GoogleCalendarV2Block,
|
||||
google_ads: GoogleAdsBlock,
|
||||
google_books: GoogleBooksBlock,
|
||||
google_contacts: GoogleContactsBlock,
|
||||
google_docs: GoogleDocsBlock,
|
||||
@@ -320,6 +327,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
note: NoteBlock,
|
||||
notion: NotionBlock,
|
||||
notion_v2: NotionV2Block,
|
||||
obsidian: ObsidianBlock,
|
||||
onepassword: OnePasswordBlock,
|
||||
onedrive: OneDriveBlock,
|
||||
openai: OpenAIBlock,
|
||||
|
||||
@@ -710,6 +710,155 @@ 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}>
|
||||
@@ -1806,6 +1955,14 @@ export function Mem0Icon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function EvernoteIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='#7fce2c'>
|
||||
<path d='M29.343 16.818c.1 1.695-.08 3.368-.305 5.045-.225 1.712-.508 3.416-.964 5.084-.3 1.067-.673 2.1-1.202 3.074-.65 1.192-1.635 1.87-2.992 1.924l-3.832.036c-.636-.017-1.278-.146-1.9-.297-1.192-.3-1.862-1.1-2.06-2.3-.186-1.08-.173-2.187.04-3.264.252-1.23 1-1.96 2.234-2.103.817-.1 1.65-.077 2.476-.1.205-.007.275.098.203.287-.196.53-.236 1.07-.098 1.623.053.207-.023.307-.26.305a7.77 7.77 0 0 0-1.123.053c-.636.086-.96.47-.96 1.112 0 .205.026.416.066.622.103.507.45.78.944.837 1.123.127 2.247.138 3.37-.05.675-.114 1.08-.54 1.16-1.208.152-1.3.155-2.587-.228-3.845-.33-1.092-1.006-1.565-2.134-1.7l-3.36-.54c-1.06-.193-1.7-.887-1.92-1.9-.13-.572-.14-1.17-.214-1.757-.013-.106-.074-.208-.1-.3-.04.1-.106.212-.117.326-.066.68-.053 1.373-.185 2.04-.16.8-.404 1.566-.67 2.33-.185.535-.616.837-1.205.8a37.76 37.76 0 0 1-7.123-1.353l-.64-.207c-.927-.26-1.487-.903-1.74-1.787l-1-3.853-.74-4.3c-.115-.755-.2-1.523-.083-2.293.154-1.112.914-1.903 2.04-1.964l3.558-.062c.127 0 .254.003.373-.026a1.23 1.23 0 0 0 1.01-1.255l-.05-3.036c-.048-1.576.8-2.38 2.156-2.622a10.58 10.58 0 0 1 4.91.26c.933.275 1.467.923 1.715 1.83.058.22.146.3.37.287l2.582.01 3.333.37c.686.095 1.364.25 2.032.42 1.165.298 1.793 1.112 1.962 2.256l.357 3.355.3 5.577.01 2.277zm-4.534-1.155c-.02-.666-.07-1.267-.444-1.784a1.66 1.66 0 0 0-2.469-.15c-.364.4-.494.88-.564 1.4-.008.034.106.126.16.126l.8-.053c.768.007 1.523.113 2.25.393.066.026.136.04.265.077zM8.787 1.154a3.82 3.82 0 0 0-.278 1.592l.05 2.934c.005.357-.075.45-.433.45L5.1 6.156c-.583 0-1.143.1-1.554.278l5.2-5.332c.02.013.04.033.06.053z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
@@ -1822,6 +1979,24 @@ export function ElevenLabsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function FathomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 1000' fill='none'>
|
||||
<path
|
||||
d='M0,668.7v205.78c0,53.97,34.24,102.88,85.8,119.08,87.48,27.49,167.88-36.99,167.88-120.22v-77.45L0,668.7Z'
|
||||
fill='#007299'
|
||||
/>
|
||||
<path
|
||||
d='M873.72,626.07c-19.05,0-38.38-4.3-56.58-13.38L72.78,241.43C11.15,210.69-17.51,136.6,11.18,74.05,41.2,8.59,119.26-18.53,183.23,13.38l744.25,371.21c62.45,31.15,91,109.08,59.79,171.43-22.22,44.38-67.02,70.05-113.55,70.05Z'
|
||||
fill='#00beff'
|
||||
/>
|
||||
<path
|
||||
d='M500.09,813.66c-19.05,0-38.38-4.3-56.58-13.38l-370.72-184.9c-61.63-30.74-90.29-104.82-61.61-167.37,30.02-65.46,108.08-92.59,172.06-60.68l370.62,184.85c62.45,31.15,91,109.08,59.79,171.43-22.22,44.38-67.02,70.05-113.55,70.05Z'
|
||||
fill='#00beff'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export function LinkupIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 154 107' fill='none'>
|
||||
@@ -3397,6 +3572,27 @@ export const ResendIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleAdsIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<g transform='matrix(.257748 0 0 .257745 -.361416 2.515516)'>
|
||||
<path
|
||||
d='M85.9 28.6c2.4-6.3 5.7-12.1 10.6-16.8 19.6-19.1 52-14.3 65.3 9.7 10 18.2 20.6 36 30.9 54l51.6 89.8c14.3 25.1-1.2 56.8-29.6 61.1-17.4 2.6-33.7-5.4-42.7-21l-45.4-78.8c-.3-.6-.7-1.1-1.1-1.6-1.6-1.3-2.3-3.2-3.3-4.9L88.8 62.2c-3.9-6.8-5.7-14.2-5.5-22 .3-4 .8-8 2.6-11.6'
|
||||
fill='#3c8bd9'
|
||||
/>
|
||||
<path
|
||||
d='M85.9 28.6c-.9 3.6-1.7 7.2-1.9 11-.3 8.4 1.8 16.2 6 23.5l32.9 56.9c1 1.7 1.8 3.4 2.8 5l-18.1 31.1-25.3 43.6c-.4 0-.5-.2-.6-.5-.1-.8.2-1.5.4-2.3 4.1-15 .7-28.3-9.6-39.7-6.3-6.9-14.3-10.8-23.5-12.1-12-1.7-22.6 1.4-32.1 8.9-1.7 1.3-2.8 3.2-4.8 4.2-.4 0-.6-.2-.7-.5l14.3-24.9L85.2 29.7c.2-.4.5-.7.7-1.1'
|
||||
fill='#fabc04'
|
||||
/>
|
||||
<path
|
||||
d='M11.8 158l5.7-5.1c24.3-19.2 60.8-5.3 66.1 25.1 1.3 7.3.6 14.3-1.6 21.3-.1.6-.2 1.1-.4 1.7-.9 1.6-1.7 3.3-2.7 4.9-8.9 14.7-22 22-39.2 20.9C20 225.4 4.5 210.6 1.8 191c-1.3-9.5.6-18.4 5.5-26.6 1-1.8 2.2-3.4 3.3-5.2.5-.4.3-1.2 1.2-1.2'
|
||||
fill='#34a852'
|
||||
/>
|
||||
<path d='M11.8 158c-.4.4-.4 1.1-1.1 1.2-.1-.7.3-1.1.7-1.6l.4.4' fill='#fabc04' />
|
||||
<path d='M81.6 201c-.4-.7 0-1.2.4-1.7l.4.4-.8 1.3' fill='#e1c025' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const GoogleBigQueryIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>
|
||||
<path
|
||||
|
||||
@@ -166,7 +166,8 @@ export class ConditionBlockHandler implements BlockHandler {
|
||||
if (!output || typeof output !== 'object') {
|
||||
return output
|
||||
}
|
||||
const { _pauseMetadata, error, ...rest } = output
|
||||
const { _pauseMetadata, error, providerTiming, tokens, toolCalls, model, cost, ...rest } =
|
||||
output
|
||||
return rest
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export class TriggerBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
const existingState = ctx.blockStates.get(block.id)
|
||||
if (existingState?.output && Object.keys(existingState.output).length > 0) {
|
||||
if (existingState?.output) {
|
||||
return existingState.output
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,10 @@ type FolderResponse = { id: string; name: string }
|
||||
type PlannerTask = { id: string; title: string }
|
||||
|
||||
const ensureCredential = (context: SelectorContext, key: SelectorKey): string => {
|
||||
if (!context.credentialId) {
|
||||
if (!context.oauthCredential) {
|
||||
throw new Error(`Missing credential for selector ${key}`)
|
||||
}
|
||||
return context.credentialId
|
||||
return context.oauthCredential
|
||||
}
|
||||
|
||||
const ensureDomain = (context: SelectorContext, key: SelectorKey): string => {
|
||||
@@ -66,9 +66,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'airtable.bases',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'airtable.bases')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -104,10 +104,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'airtable.tables',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.baseId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.baseId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.baseId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'airtable.tables')
|
||||
if (!context.baseId) {
|
||||
@@ -151,9 +151,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'asana.workspaces',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'asana.workspaces')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -182,9 +182,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'attio.objects',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'attio.objects')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -216,9 +216,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'attio.lists',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'attio.lists')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -250,10 +250,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'bigquery.datasets',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.projectId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.projectId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'bigquery.datasets')
|
||||
if (!context.projectId) throw new Error('Missing project ID for bigquery.datasets selector')
|
||||
@@ -298,12 +298,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'bigquery.tables',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
context.datasetId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.credentialId && context.projectId && context.datasetId),
|
||||
Boolean(context.oauthCredential && context.projectId && context.datasetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'bigquery.tables')
|
||||
if (!context.projectId) throw new Error('Missing project ID for bigquery.tables selector')
|
||||
@@ -347,9 +347,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'calcom.eventTypes',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'calcom.eventTypes')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -381,9 +381,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'calcom.schedules',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'calcom.schedules')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -415,10 +415,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'confluence.spaces',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'confluence.spaces')
|
||||
const domain = ensureDomain(context, 'confluence.spaces')
|
||||
@@ -460,10 +460,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jsm.serviceDesks',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jsm.serviceDesks')
|
||||
const domain = ensureDomain(context, 'jsm.serviceDesks')
|
||||
@@ -505,12 +505,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jsm.requestTypes',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
context.serviceDeskId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) =>
|
||||
Boolean(context.credentialId && context.domain && context.serviceDeskId),
|
||||
Boolean(context.oauthCredential && context.domain && context.serviceDeskId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jsm.requestTypes')
|
||||
const domain = ensureDomain(context, 'jsm.requestTypes')
|
||||
@@ -556,9 +556,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.tasks.lists',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.tasks.lists')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -587,9 +587,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.planner.plans',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner.plans')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -618,9 +618,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'notion.databases',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'notion.databases')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -652,9 +652,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'notion.pages',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'notion.pages')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -686,9 +686,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'pipedrive.pipelines',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'pipedrive.pipelines')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -720,10 +720,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'sharepoint.lists',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'sharepoint.lists')
|
||||
if (!context.siteId) throw new Error('Missing site ID for sharepoint.lists selector')
|
||||
@@ -761,9 +761,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'trello.boards',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'trello.boards')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -794,9 +794,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'zoom.meetings',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'zoom.meetings')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -828,12 +828,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'slack.channels',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
credential: context.credentialId,
|
||||
credential: context.oauthCredential,
|
||||
workflowId: context.workflowId,
|
||||
})
|
||||
const data = await fetchJson<{ channels: SlackChannel[] }>('/api/tools/slack/channels', {
|
||||
@@ -852,12 +852,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'slack.users',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
credential: context.credentialId,
|
||||
credential: context.oauthCredential,
|
||||
workflowId: context.workflowId,
|
||||
})
|
||||
const data = await fetchJson<{ users: SlackUser[] }>('/api/tools/slack/users', {
|
||||
@@ -876,12 +876,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'gmail.labels',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ labels: FolderResponse[] }>('/api/tools/gmail/labels', {
|
||||
searchParams: { credentialId: context.credentialId },
|
||||
searchParams: { credentialId: context.oauthCredential },
|
||||
})
|
||||
return (data.labels || []).map((label) => ({
|
||||
id: label.id,
|
||||
@@ -895,12 +895,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'outlook.folders',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ folders: FolderResponse[] }>('/api/tools/outlook/folders', {
|
||||
searchParams: { credentialId: context.credentialId },
|
||||
searchParams: { credentialId: context.oauthCredential },
|
||||
})
|
||||
return (data.folders || []).map((folder) => ({
|
||||
id: folder.id,
|
||||
@@ -914,13 +914,13 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.calendar',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ calendars: { id: string; summary: string }[] }>(
|
||||
'/api/tools/google_calendar/calendars',
|
||||
{ searchParams: { credentialId: context.credentialId } }
|
||||
{ searchParams: { credentialId: context.oauthCredential } }
|
||||
)
|
||||
return (data.calendars || []).map((calendar) => ({
|
||||
id: calendar.id,
|
||||
@@ -934,11 +934,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.teams',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({ credential: context.credentialId })
|
||||
const body = JSON.stringify({ credential: context.oauthCredential })
|
||||
const data = await fetchJson<{ teams: { id: string; displayName: string }[] }>(
|
||||
'/api/tools/microsoft-teams/teams',
|
||||
{ method: 'POST', body }
|
||||
@@ -955,11 +955,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.chats',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({ credential: context.credentialId })
|
||||
const body = JSON.stringify({ credential: context.oauthCredential })
|
||||
const data = await fetchJson<{ chats: { id: string; displayName: string }[] }>(
|
||||
'/api/tools/microsoft-teams/chats',
|
||||
{ method: 'POST', body }
|
||||
@@ -976,13 +976,13 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.channels',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.teamId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const body = JSON.stringify({
|
||||
credential: context.credentialId,
|
||||
credential: context.oauthCredential,
|
||||
teamId: context.teamId,
|
||||
})
|
||||
const data = await fetchJson<{ channels: { id: string; displayName: string }[] }>(
|
||||
@@ -1001,14 +1001,14 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'wealthbox.contacts',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const data = await fetchJson<{ items: { id: string; name: string }[] }>(
|
||||
'/api/tools/wealthbox/items',
|
||||
{
|
||||
searchParams: { credentialId: context.credentialId, type: 'contact' },
|
||||
searchParams: { credentialId: context.oauthCredential, type: 'contact' },
|
||||
}
|
||||
)
|
||||
return (data.items || []).map((item) => ({
|
||||
@@ -1023,9 +1023,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'sharepoint.sites',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'sharepoint.sites')
|
||||
const body = JSON.stringify({
|
||||
@@ -1069,10 +1069,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.planner',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.planId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.planId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.planId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.planner')
|
||||
const body = JSON.stringify({
|
||||
@@ -1112,11 +1112,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jira.projects',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jira.projects')
|
||||
const domain = ensureDomain(context, 'jira.projects')
|
||||
@@ -1171,12 +1171,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'jira.issues',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
context.projectId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'jira.issues')
|
||||
const domain = ensureDomain(context, 'jira.issues')
|
||||
@@ -1235,9 +1235,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'linear.teams',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'linear.teams')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -1260,10 +1260,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'linear.projects',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.teamId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.teamId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.teamId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'linear.projects')
|
||||
const body = JSON.stringify({
|
||||
@@ -1290,11 +1290,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'confluence.pages',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.domain ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.domain),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.domain),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'confluence.pages')
|
||||
const domain = ensureDomain(context, 'confluence.pages')
|
||||
@@ -1343,9 +1343,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'onedrive.files',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'onedrive.files')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1366,9 +1366,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'onedrive.folders',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'onedrive.folders')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1389,12 +1389,12 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.drive',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.mimeType ?? 'any',
|
||||
context.fileId ?? 'root',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.drive')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1438,10 +1438,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'google.sheets',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'google.sheets')
|
||||
if (!context.spreadsheetId) {
|
||||
@@ -1469,10 +1469,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel.sheets',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.spreadsheetId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.spreadsheetId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel.sheets')
|
||||
if (!context.spreadsheetId) {
|
||||
@@ -1500,10 +1500,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.excel',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.excel')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1528,10 +1528,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'microsoft.word',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'microsoft.word')
|
||||
const data = await fetchJson<{ files: { id: string; name: string }[] }>(
|
||||
@@ -1596,9 +1596,9 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.sites',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.sites')
|
||||
const body = JSON.stringify({ credential: credentialId, workflowId: context.workflowId })
|
||||
@@ -1621,10 +1621,10 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.collections',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.siteId ?? 'none',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.siteId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId),
|
||||
fetchList: async ({ context }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.collections')
|
||||
if (!context.siteId) {
|
||||
@@ -1654,11 +1654,11 @@ const registry: Record<SelectorKey, SelectorDefinition> = {
|
||||
getQueryKey: ({ context, search }: SelectorQueryArgs) => [
|
||||
'selectors',
|
||||
'webflow.items',
|
||||
context.credentialId ?? 'none',
|
||||
context.oauthCredential ?? 'none',
|
||||
context.collectionId ?? 'none',
|
||||
search ?? '',
|
||||
],
|
||||
enabled: ({ context }) => Boolean(context.credentialId && context.collectionId),
|
||||
enabled: ({ context }) => Boolean(context.oauthCredential && context.collectionId),
|
||||
fetchList: async ({ context, search }: SelectorQueryArgs) => {
|
||||
const credentialId = ensureCredential(context, 'webflow.items')
|
||||
if (!context.collectionId) {
|
||||
|
||||
@@ -7,46 +7,16 @@ export interface SelectorResolution {
|
||||
allowSearch: boolean
|
||||
}
|
||||
|
||||
export interface SelectorResolutionArgs {
|
||||
workflowId?: string
|
||||
credentialId?: string
|
||||
domain?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
teamId?: string
|
||||
knowledgeBaseId?: string
|
||||
siteId?: string
|
||||
collectionId?: string
|
||||
spreadsheetId?: string
|
||||
fileId?: string
|
||||
baseId?: string
|
||||
datasetId?: string
|
||||
serviceDeskId?: string
|
||||
}
|
||||
|
||||
export function resolveSelectorForSubBlock(
|
||||
subBlock: SubBlockConfig,
|
||||
args: SelectorResolutionArgs
|
||||
context: SelectorContext
|
||||
): SelectorResolution | null {
|
||||
if (!subBlock.selectorKey) return null
|
||||
return {
|
||||
key: subBlock.selectorKey,
|
||||
context: {
|
||||
workflowId: args.workflowId,
|
||||
credentialId: args.credentialId,
|
||||
domain: args.domain,
|
||||
projectId: args.projectId,
|
||||
planId: args.planId,
|
||||
teamId: args.teamId,
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
siteId: args.siteId,
|
||||
collectionId: args.collectionId,
|
||||
spreadsheetId: args.spreadsheetId,
|
||||
fileId: args.fileId,
|
||||
baseId: args.baseId,
|
||||
datasetId: args.datasetId,
|
||||
serviceDeskId: args.serviceDeskId,
|
||||
mimeType: subBlock.mimeType,
|
||||
...context,
|
||||
mimeType: subBlock.mimeType ?? context.mimeType,
|
||||
},
|
||||
allowSearch: subBlock.selectorAllowSearch ?? true,
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface SelectorOption {
|
||||
export interface SelectorContext {
|
||||
workspaceId?: string
|
||||
workflowId?: string
|
||||
credentialId?: string
|
||||
oauthCredential?: string
|
||||
serviceId?: string
|
||||
domain?: string
|
||||
teamId?: string
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SelectorDisplayNameArgs {
|
||||
subBlock?: SubBlockConfig
|
||||
value: unknown
|
||||
workflowId?: string
|
||||
credentialId?: string
|
||||
oauthCredential?: string
|
||||
domain?: string
|
||||
projectId?: string
|
||||
planId?: string
|
||||
@@ -31,7 +31,7 @@ export function useSelectorDisplayName({
|
||||
subBlock,
|
||||
value,
|
||||
workflowId,
|
||||
credentialId,
|
||||
oauthCredential,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
@@ -51,7 +51,7 @@ export function useSelectorDisplayName({
|
||||
if (!subBlock || !detailId) return null
|
||||
return resolveSelectorForSubBlock(subBlock, {
|
||||
workflowId,
|
||||
credentialId,
|
||||
oauthCredential,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
@@ -69,7 +69,7 @@ export function useSelectorDisplayName({
|
||||
subBlock,
|
||||
detailId,
|
||||
workflowId,
|
||||
credentialId,
|
||||
oauthCredential,
|
||||
domain,
|
||||
projectId,
|
||||
planId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user