fix(integrations): repair corrupt icons, backfill missing block metas, restore scroll on back-nav (#5342)

* fix(integrations): repair corrupt icons, backfill missing block metas, restore scroll on back-nav

- Restore 7 brand icons (Google, Outlook, MongoDB, Postgres, OpenRouter, Groq, Cerebras) whose SVG path data was corrupted by a past bulk reformat, flooding the integrations page console with <path> parse errors; add a check:icon-paths CI gate that validates every icon d attribute (operand counts + arc flags).
- Backfill BlockMeta (tags/url/templates/skills) for postgresql, mysql, ssh, sftp, smtp — previously catalog integrations with empty detail pages; add an integration meta-coverage CI check so every catalog block must have a meta.
- Add scroll-position restoration for the integrations index/detail inner scroll containers so browser Back returns to where you were.
- Remove the error digest pill from the shared workspace ErrorShell (kept in logs, dropped from UI).

* fix(integrations): make scroll restoration robust — value-based echo detection + Back/Forward-only gate

Addresses review: replace the racy programmatic-scroll flag with value comparison (a restore's echo equals lastApplied and is ignored, so a stuck flag can never drop the first user scroll or overwrite the saved target), and gate restoration on popstate history traversals so fresh push navigations open at the top instead of jumping mid-list. TSDoc-only comments.

* fix(ci): attribute icon-path errors for export-const icons too

Greptile review: iconNameAt only matched 'export function', so a malformed path inside an 'export const XxxIcon = (...)' arrow-function icon would be misattributed to the preceding function-declared icon. Match both forms (mirrors check-bare-icons indexIconBodies).
This commit is contained in:
Waleed
2026-07-01 16:58:45 -07:00
committed by GitHub
parent 4e8b88f0af
commit 507cee1187
17 changed files with 1043 additions and 74 deletions
+3
View File
@@ -128,6 +128,9 @@ jobs:
- name: Bare-icon theme-safety audit
run: bun run check:bare-icons
- name: Icon SVG path validity audit
run: bun run check:icon-paths
- name: Verify realtime prune graph
run: bun run check:realtime-prune
+31 -31
View File
@@ -1142,7 +1142,7 @@ export function NotionIcon(props: SVGProps<SVGSVGElement>) {
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1em' height='1em' {...props}>
<path
d='M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z'
fill='#000000'
fill='currentColor'
/>
</svg>
)
@@ -1357,7 +1357,7 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
/>
<path
fill='#1565c0'
d='M43.61,20.08L43.6,20L42,20H24v8h11.3c-0.79,2.24-2.23,4.17-4.09,5.57 c00,00,00l6.19,5.24C36.97,39.21,44,34,44,24C44,22.66,43.86,21.35,43.61,20.08z'
d='M43.61,20.08L43.6,20L42,20H24v8h11.3c-0.79,2.24-2.23,4.17-4.09,5.57 c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.24C36.97,39.21,44,34,44,24C44,22.66,43.86,21.35,43.61,20.08z'
/>
</svg>
)
@@ -1980,7 +1980,7 @@ export function SquareIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 501.42 501.42' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#fff'
fill='currentColor'
d='M501.42,83.79v333.84c0,46.27-37.5,83.79-83.79,83.79H83.79c-46.28,0-83.79-37.5-83.79-83.79V83.79C0,37.51,37.52,0,83.79,0h333.84c46.29,0,83.79,37.5,83.79,83.79h0ZM410.22,117.64c0-14.61-11.85-26.45-26.45-26.45H117.62c-14.61,0-26.45,11.84-26.45,26.45v266.19c0,14.61,11.84,26.45,26.45,26.45h266.17c14.61,0,26.45-11.85,26.45-26.45V117.64h-.02ZM182.31,197.59c0-8.43,6.79-15.26,15.17-15.26h106.4c8.39,0,15.17,6.84,15.17,15.26v106.24c0,8.43-6.75,15.26-15.17,15.26h-106.4c-8.39,0-15.17-6.84-15.17-15.26v-106.24Z'
/>
</svg>
@@ -2086,7 +2086,7 @@ export function ContextDevIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 772.32 531.2' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#000000'
fill='currentColor'
d='M260.5,27.06v240.41c-29-28.17-69.08-44.96-113.07-43.26C69.5,227.24,5.16,289.98.31,367.81c-5.56,89.26,65.18,163.38,153.24,163.38,41.58,0,79.3-16.51,106.95-43.37h0c0,13.26,10.75,24.01,24.01,24.01h129.54c-51.07,0-96.29-24.98-124.23-63.34,2.81-5.4,5.32-10.98,7.47-16.76,0,0,0,0,0,0,.02-.12.06-.23.12-.35.46-1.2.89-2.4,1.3-3.6.41-1.22.81-2.42,1.2-3.64,0-.01,0-.02.01-.03.53-1.64,1.05-3.31,1.5-4.99,0,0,0,0,0,0,.02-.12.06-.23.08-.35,0,0,0-.01,0-.02.33-1.08.61-2.18.87-3.27,0-.02,0-.03.01-.04.12-.47.23-.95.35-1.43.27-1.08.52-2.15.74-3.23.06-.25.1-.5.17-.74.06-.29.12-.58.19-.89.04-.19.08-.37.13-.54.16-.85.33-1.7.47-2.55.04-.12.06-.25.08-.37.02-.1.04-.21.06-.33.12-.7.25-1.41.37-2.11.1-.58.19-1.18.29-1.76.02-.12.04-.23.04-.35.25-1.53.46-3.06.64-4.61.08-.72.17-1.45.25-2.17.04-.23.06-.45.08-.68h0c.06-.54.1-1.1.14-1.63h0c.02-.06.02-.13.02-.17.02-.19.04-.35.04-.54.1-.97.19-1.95.23-2.92.02-.1.04-.21.02-.31.06-.52.08-1.03.1-1.55.02-.1.02-.21.02-.31.04-.52.06-1.01.08-1.53h0c.04-.62.06-1.24.06-1.86h0c.02-.15.02-.27.02-.39.06-1.59.08-3.19.08-4.78h0c0-1.05-.02-2.11-.04-3.14,0-.39,0-.79-.02-1.16,0-.35-.02-.7-.04-1.06,0-.56-.02-1.12-.06-1.68,0-.29-.02-.58-.04-.87,0-.12,0-.23-.02-.33,0-.21-.02-.41-.04-.6,0-.19,0-.37-.02-.56,0-.08,0-.17-.02-.25-.02-.37-.04-.74-.06-1.1-.02-.37-.06-.72-.08-1.08,0-.08,0-.16-.02-.23-.04-.77-.12-1.55-.21-2.34-.02-.41-.06-.81-.1-1.2-.02-.14-.02-.29-.04-.41-.12-1.2-.27-2.38-.41-3.56-.06-.48-.12-.95-.19-1.43-.08-.6-.17-1.18-.25-1.76-.06-.37-.12-.72-.17-1.08-.06-.35-.1-.7-.17-1.03-.14-.85-.29-1.7-.43-2.52-.02-.12-.04-.25-.06-.37-.19-1.01-.39-2.03-.6-3.04-.08-.39-.15-.79-.25-1.16-.12-.58-.25-1.18-.37-1.76-.12-.54-.25-1.06-.37-1.59-.1-.46-.21-.91-.31-1.35-.15-.56-.29-1.1-.43-1.66-.1-.5-.25-.97-.37-1.47-.25-.89-.5-1.8-.74-2.71-.04-.1-.06-.21-.1-.31-.12-.43-.25-.89-.39-1.32-.27-.93-.56-1.84-.87-2.77-.12-.43-.27-.89-.41-1.32-.12-.35-.23-.68-.35-1.03-.1-.29-.19-.58-.29-.87-.1-.31-.23-.62-.33-.93-.33-.93-.66-1.86-1.01-2.79-.43-1.16-.89-2.3-1.34-3.44-.48-1.18-.95-2.36-1.47-3.52-.19-.43-.37-.87-.56-1.32-.33-.72-.66-1.43-.99-2.15-.1-.23-.21-.46-.33-.68-.1-.23-.23-.43-.33-.68-.85-1.78-1.72-3.58-2.65-5.34-.02-.08-.06-.14-.1-.23-.56-1.08-1.14-2.13-1.74-3.21-.56-1.03-1.16-2.05-1.76-3.06-.02-.06-.06-.14-.1-.21-.41-.72-.85-1.43-1.3-2.15-.89-1.49-1.82-2.94-2.77-4.39-.66-.99-1.32-2.01-2.03-3-.17-.23-.31-.48-.5-.7-.33-.48-.66-.97-1.03-1.47-.02-.02-.04-.06-.08-.1-.14-.23-.29-.43-.45-.66v-.02l-.1-.1c-.27-.37-.56-.77-.83-1.14-.17-.23-.33-.43-.5-.66-.31-.39-.62-.81-.93-1.2-.64-.85-1.3-1.7-1.99-2.55-.27-.35-.56-.7-.87-1.06-.6-.77-1.22-1.51-1.86-2.26-.83-.97-1.66-1.95-2.5-2.9-.52-.58-1.03-1.16-1.57-1.72-.58-.64-1.18-1.28-1.8-1.92-.25-.29-.52-.56-.81-.85h0c.29.29.56.56.81.85.62.64,1.22,1.28,1.8,1.92.54.56,1.05,1.14,1.57,1.72.85.95,1.68,1.92,2.5,2.9.64.74,1.26,1.49,1.86,2.26.31.35.6.7.87,1.06.68.85,1.34,1.7,1.99,2.55.31.39.62.81.93,1.2.17.23.33.44.5.66.27.37.56.77.83,1.14l.1.1c26.35-50.54,79.92-84.63,141.29-82.65,78.35,2.52,143.32,65.42,148.19,143.66,5.55,89.25-65.19,163.36-153.24,163.36h331.21c14.95,0,27.06-12.12,27.06-27.06V27.06c0-14.95-12.12-27.06-27.06-27.06h-457.7c-14.95,0-27.06,12.12-27.06,27.06ZM286.24,454.89s.01-.02.02-.03c0,0,0,.02-.01.02h0ZM286.62,454.23c.69-1.19,1.34-2.41,2-3.63-.66,1.21-1.31,2.44-1.99,3.62,0,0,0,0,0,.01Z'
/>
</svg>
@@ -2328,7 +2328,7 @@ export function BrexIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 223 179.3' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
fill='#FFFFFF'
fill='currentColor'
d='M144.9,14.3c-8.7,11.6-10.8,15.5-19.2,15.5H0v149.4h49.3c11.1,0,21.9-5.4,28.9-14.3c9-12,10.2-15.5,18.9-15.5 H223V0h-49.6C162.3,0,151.5,5.4,144.9,14.3L144.9,14.3z M183.9,110.9h-52.6c-11.4,0-21.9,4.8-28.9,14c-9,12-10.8,15.5-19.2,15.5 H38.8V68.7h52.6c11.4,0,21.9-5.4,28.9-14.3c9-11.6,11.4-15.2,19.5-15.2h44.2V110.9z'
/>
</svg>
@@ -2584,7 +2584,7 @@ export function LinkupIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 154 107' fill='none'>
<path
d='M150.68 72.71C146.61 70.25 137.91 69.54 124.79 70.61C128.99 57.68 133.76 35.39 121.32 25.15C115.89 20.67 107.47 19.04 97.62 20.56C94.68 21.01 91.58 21.74 88.39 22.73C78.87 8.28 66.3 0 53.86 0C39.43 0 26.13 9.34 16.41 26.29C5.68 45.01 0 71.96 0 104.23V104.53L3.6 106.94L3.88 106.83C30.58 95.56 67.58 85.07 100.59 79.4C101.6 87.64 102.12 95.99 102.12 104.24V104.52L105.49 107L105.76 106.91C106.26 106.75 155.16 90.88 153.98 77.59C153.86 76.2 153.18 74.23 150.68 72.71ZM148.41 78.09C148.72 81.54 133.24 91.06 111.84 98.89C115.97 92.1 119.82 84.17 122.78 76.36C135.66 75.14 144.53 75.55 147.79 77.53C148.38 77.88 148.41 78.09 148.41 78.09ZM116.67 77.01C114.08 83.38 110.95 89.63 107.54 95.25C107.33 89.51 106.91 83.88 106.3 78.46C109.92 77.9 113.41 77.41 116.67 77.01ZM117.77 29.5C125.38 35.76 125.78 51.32 118.87 71.18C114.75 71.63 110.28 72.24 105.6 72.98C103.05 55.17 98.28 39.97 91.42 27.75C94.57 26.82 96.97 26.35 98.46 26.11C106.72 24.84 113.58 26.04 117.77 29.5ZM53.86 5.62C65.06 5.62 74.89 12 83.09 24.59C57.7 34.54 30.32 59.41 5.78 94.8C7.43 51.48 23.03 5.62 53.86 5.62M10.19 98.24C40.75 53.93 68.2 36.44 86.07 29.59C92.45 41.24 97.2 56.55 99.84 73.93C70.52 79.03 35.64 88.5 10.19 98.24Z'
fill='#000000'
fill='currentColor'
/>
</svg>
)
@@ -2665,7 +2665,7 @@ export function LangsmithIcon(props: SVGProps<SVGSVGElement>) {
export function LatexIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 500' fill='#000000'>
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 500' fill='currentColor'>
<path
d='m5.46 4.23h-.25c-.1 1.02-.24 2.26-2 2.26h-.81c-.47 0-.49-.07-.49-.4v-5.31c0-.34 0-.48.94-.48h.33v-.3c-.36.03-1.26.03-1.67.03-.39 0-1.17 0-1.51-.03v.3h.23c.77 0 .79.11.79.47v5.25c0 .36-.02.47-.79.47h-.23v.31h5.19z'
transform='matrix(45 0 0 45 40 47.65)'
@@ -2763,7 +2763,7 @@ export function ThriveIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} viewBox='0 0 27.583 27' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M18.7559 0C21.8453 0 23.3909 0.000524727 24.5703 0.588867C25.6078 1.10712 26.452 1.93264 26.9814 2.94824C27.5826 4.10418 27.583 5.61553 27.583 8.63965V18.3604C27.583 21.3845 27.5826 22.8973 26.9814 24.0518C26.452 25.0674 25.6078 25.8929 24.5703 26.4111C23.3894 26.9995 21.8453 27 18.7559 27H8.82617C5.73674 27 4.19109 26.9996 3.01172 26.4111C1.97438 25.8929 1.13096 25.0672 0.601562 24.0518C0.000441481 22.8973 1.23549e-09 21.3845 0 18.3604V8.63965C1.23384e-09 5.61553 0.000441463 4.10418 0.601562 2.94824C1.13096 1.9328 1.97438 1.1071 3.01172 0.588867C4.19109 0.000438224 5.73674 7.53417e-10 8.82617 0H18.7559ZM7.16992 7.29004C7.16999 8.03525 7.78752 8.63965 8.54883 8.63965H11.584V20.251C11.5841 20.9961 12.2016 21.6006 12.9629 21.6006H15.998V11.3408C15.998 9.85028 14.7629 8.63965 13.2402 8.63965H20.4102V6.75098C20.4102 6.00586 19.7924 5.40065 19.0312 5.40039H7.16992V7.29004Z'
fill='#000000'
fill='currentColor'
/>
</svg>
)
@@ -3107,7 +3107,7 @@ export function OutlookIcon(props: SVGProps<SVGSVGElement>) {
/>
<path
fill='#1490DF'
d='M1811.58,927.59l-0.81,0.47l-634.49,356.85c-2.77,1.7-5.58,3.32-8.52,4.77 c-24.64,12.04-53.46,12.04-78.1,0c-2.92-1.44-5.76-3.04-8.52-4.77L446.66,928.06l-0.77-0.47 c-12.25-6.64-19.93-19.41-20.06-33.34v722.38c0.31,48.19,39.62,87,87.8,86.7c0,0,0,0,0,0h1229.64 c48.19,0.31,87.5-38.51,87.81-86.7c00,00,00V894.25C1831.07,908.07,1823.62,920.81,1811.58,927.59z'
d='M1811.58,927.59l-0.81,0.47l-634.49,356.85c-2.77,1.7-5.58,3.32-8.52,4.77 c-24.64,12.04-53.46,12.04-78.1,0c-2.92-1.44-5.76-3.04-8.52-4.77L446.66,928.06l-0.77-0.47 c-12.25-6.64-19.93-19.41-20.06-33.34v722.38c0.31,48.19,39.62,87,87.8,86.7c0,0,0,0,0,0h1229.64 c48.19,0.31,87.5-38.51,87.81-86.7c0-0.001,0-0.002,0-0.004V894.25C1831.07,908.07,1823.62,920.81,1811.58,927.59z'
/>
<path
opacity='0.1'
@@ -3436,7 +3436,7 @@ export const GroqIcon = (props: SVGProps<SVGSVGElement>) => (
xmlns='http://www.w3.org/2000/svg'
>
<title>Groq</title>
<path d='M12.04 2c-3.85-.035-7 3-7.04 6.78-.035 3.78 3.06 6.87 6.91 6.91h2.42v-2.57h-2.29c-2.41.028-4.38-1.87-4.41-4.23-.029-2.36 1.9-4.3 4.31-4.33h.1c2.41 0 4.36 1.92 4.37 4.28v6.31c0 2.34-1.94 4.25-4.32 4.28a4.38 4.38 0 01-3.03-1.25l-1.85 1.82A7 7 0 12.03 22h.092c3.8-.056 6.86-3.08 6.88-6.82v-6.5C18.91 4.96 15.82 2 12.04 2z' />
<path d='M12.04 2c-3.85-.035-7 3-7.04 6.78-.035 3.78 3.06 6.87 6.91 6.91h2.42v-2.57h-2.29c-2.41.028-4.38-1.87-4.41-4.23-.029-2.36 1.9-4.3 4.31-4.33h.1c2.41 0 4.36 1.92 4.37 4.28v6.31c0 2.34-1.94 4.25-4.32 4.28a4.38 4.38 0 01-3.03-1.25l-1.85 1.82A7 7 0 0012.03 22h.092c3.8-.056 6.86-3.08 6.88-6.82v-6.5C18.91 4.96 15.82 2 12.04 2z' />
</svg>
)
@@ -3444,7 +3444,7 @@ export const DeepseekIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} height='1em' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'>
<title>DeepSeek</title>
<path
d='M23.75 4.48c-.254-.124-.364.11-.512.23-.51.04-.094.09-.137.14-.372.4-.806.66-1.37.626-.829-.046-1.54.214-2.16.848-.133-.782-.575-1.25-1.25-1.55-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.14-.356.28-.313.57-.434 1.2-.422 1.84.03 1.44.633 2.58 1.84 3.39.137.09.172.19.129.32-.082.28-.18.55-.266.83-.55.18-.137.22-.329.14a5.53 5.53 0 01-1.74-1.18c-.857-.828-1.63-1.74-2.6-2.46a11.37 11.37 0 00-.689-.471c-.985-.957.13-1.74.388-1.84.27-.98.09-.432-.779-.428-.872-1.67.3-2.69.684a3.06 3.06 0 01-.465.14 9.6 9.6 0 00-2.88-.102c-1.88.21-3.39 1.1-4.5 2.62C.082 8.61-.231 10.68.152 12.85c.403 2.28 1.57 4.18 3.36 5.65 1.86 1.53 4 2.28 6.44 2.14 1.48-.085 3.13-.284 4.99-1.86.47.23.962.33 1.78.4.63.06 1.24-.03 1.71-.128.74-.156.68-.837.42-.961-2.15-1-1.68-.595-2.11-.926 1.1-1.3 2.75-2.64 3.39-7.05-.347.01-.565 0-.845-.004-.17.04-.237.23-.256a4.17 4.17 0 1.55-.475c1.4-.763 1.96-2.01 2.09-3.52.02-.23-.004-.467-.247-.588zM11.58 18c-2.09-1.64-3.1-2.18-3.52-2.16-.392.02-.321.47-.235.76.9.29.207.49.371.74.114.17.192.42-.113.6-.673.42-1.84-.14-1.9-.167-1.36-.802-2.5-1.86-3.3-3.31-.774-1.39-1.22-2.89-1.3-4.48-.02-.386.09-.522.48-.592a4.7 4.7 0 11.53-.039c2.13.312 3.95 1.26 5.47 2.77.868.86 1.53 1.89 2.2 2.89.72 1.07 1.49 2.08 2.48 2.91.348.29.625.51.891.68-.802.09-2.14.11-3.05-.614zm1-6.44a.306.31 0 1.42-.287.3.302 0 01.2.29.306.31 0 01-.31.31.303.3 0 01-.304-.308zm3.11 1.6c-.2.08-.399.15-.59.16a1.25 1.25 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 1.02-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.56 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.03-.054.16-.186.19-.21.36-.202.77-.136 1.15.16.35.144.62.408 1.782.39.451.46.576.68.914.18.265.34.537.45.848.07.195-.19.35-.25.45z'
d='M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45'
fill='#4D6BFE'
/>
</svg>
@@ -3545,7 +3545,7 @@ export const CerebrasIcon = (props: SVGProps<SVGSVGElement>) => (
<title>Cerebras</title>
<path
clipRule='evenodd'
d='M14.12 2.7a9.3 9.3 0 000 18.6V22.7c-5.91 0-10.7-4.79-10.7-10.7S8.21 1.3 14.12 1.3V2.7zm4.75 3.68A7.35 7.35 0 109.42 17.64l-.901 1.07a8.75 8.75 0 01-1.08-12.33 8.76 8.76 0 112.34-1.08l-.901 1.08zm-2.25.844a5.41 5.41 0 00-5.05 9.56l-.656 1.24a6.81 6.81 0 16.36-12.04l-.654 1.24zM14.12 8.54a3.46 3.46 0 100 6.92v1.4a4.86 4.86 0 010-9.73v1.4z'
d='M14.12 2.7a9.3 9.3 0 000 18.6V22.7c-5.91 0-10.7-4.79-10.7-10.7S8.21 1.3 14.12 1.3V2.7zm4.75 3.68A7.35 7.35 0 109.42 17.64l-.901 1.07a8.75 8.75 0 01-1.08-12.33 8.76 8.76 0 112.34-1.08l-.901 1.08zm-2.25.844a5.41 5.41 0 00-5.05 9.56l-.656 1.24a6.81 6.81 0 016.36-12.04l-.654 1.24zM14.12 8.54a3.46 3.46 0 100 6.92v1.4a4.86 4.86 0 010-9.73v1.4z'
fill='#F15A29'
fillRule='evenodd'
/>
@@ -4175,7 +4175,7 @@ export function PostgresIcon(props: SVGProps<SVGSVGElement>) {
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMinYMin meet'
>
<path d='M255.01 158.09c-1.53-4.65-5.56-7.89-10.76-8.66-2.45-.366-5.26-.21-8.58.475-5.79 1.2-10.09 1.65-13.22 1.74 11.84-19.98 21.46-42.77 27-64.23 8.96-34.69 4.17-50.49-1.42-57.64C233.22 10.85 211.61.683 185.55.372c-13.9-.17-26.11 2.58-32.47 4.55-5.93-1.05-12.3-1.63-18.99-1.74-12.54-.2-23.61 2.53-33.08 8.15-5.24-1.77-13.65-4.27-23.36-5.86-22.84-3.75-41.25-.828-54.72 8.69C6.62 25.67-.937 45.68.461 73.63c.444 8.87 5.41 35.87 13.22 61.48 4.49 14.72 9.28 26.94 14.24 36.33 7.03 13.32 14.55 21.16 22.99 23.97 4.73 1.58 13.33 2.68 22.37-4.85 1.15 1.39 2.68 2.77 4.7 4.05 2.58 1.63 5.73 2.95 8.88 3.74 11.34 2.84 21.96 2.13 31.03-1.85.056 1.61.099 3.15.135 4.48.06 2.16.12 4.27.199 6.25.54 13.37 1.45 23.77 4.14 31.05.148.4.35 1.1.56 1.66 1.35 4.12 3.59 11.01 9.32 16.41 5.93 5.59 13.09 7.31 19.66 7.31 3.29 0 6.43-.432 9.19-1.02 9.82-2.1 20.97-5.31 29.04-16.8 7.63-10.86 11.34-27.22 12.01-52.99.09-.729.17-1.42.244-2.09l.16-1.36 1.8.158.46.031c10.456 22.23-1.66 29.74-5.15 5.94-2.75 24.95-12.79 20.48-26.35' />
<path d='M255.01 158.09c-1.53-4.65-5.56-7.89-10.76-8.66-2.45-.366-5.26-.21-8.58.475-5.79 1.2-10.09 1.65-13.22 1.74 11.84-19.98 21.46-42.77 27-64.23 8.96-34.69 4.17-50.49-1.42-57.64C233.22 10.85 211.61.683 185.55.372c-13.9-.17-26.11 2.58-32.47 4.55-5.93-1.05-12.3-1.63-18.99-1.74-12.54-.2-23.61 2.53-33.08 8.15-5.24-1.77-13.65-4.27-23.36-5.86-22.84-3.75-41.25-.828-54.72 8.69C6.62 25.67-.937 45.68.461 73.63c.444 8.87 5.41 35.87 13.22 61.48 4.49 14.72 9.28 26.94 14.24 36.33 7.03 13.32 14.55 21.16 22.99 23.97 4.73 1.58 13.33 2.68 22.37-4.85 1.15 1.39 2.68 2.77 4.7 4.05 2.58 1.63 5.73 2.95 8.88 3.74 11.34 2.84 21.96 2.13 31.03-1.85.056 1.61.099 3.15.135 4.48.06 2.16.12 4.27.199 6.25.54 13.37 1.45 23.77 4.14 31.05.148.4.35 1.1.56 1.66 1.35 4.12 3.59 11.01 9.32 16.41 5.93 5.59 13.09 7.31 19.66 7.31 3.29 0 6.43-.432 9.19-1.02 9.82-2.1 20.97-5.31 29.04-16.8 7.63-10.86 11.34-27.22 12.01-52.99.09-.729.17-1.42.244-2.09l.16-1.36 1.8.158.46.031c10.002.456 22.232-1.665 29.743-5.154 5.935-2.754 24.954-12.795 20.476-26.351' />
<path
d='M237.91 160.72c-29.74 6.14-31.78-3.93-31.78-3.93 31.4-46.59 44.53-105.74 33.2-120.21-30.9-39.48-84.4-20.81-85.29-20.33l-.287.05c-5.88-1.22-12.45-1.95-19.84-2.07-13.46-.22-23.66 3.53-31.41 9.4 0 0-95.43-39.31-90.99 49.44.944 18.88 27.06 142.87 58.22 105.42 11.39-13.69 22.39-25.27 22.39-25.27 5.46 3.63 12.01 5.48 18.86 4.82l.533-.452c-.166 1.7-.09 3.36.213 5.33-8.03 8.97-5.67 10.54-21.71 13.84-16.23 3.35-6.7 9.3-.471 10.86 7.55 1.89 25.01 4.56 36.81-11.96l-.47 1.89c3.14 2.52 5.35 16.38 4.98 28.95-.37 12.57-.617 21.2 1.86 27.94 2.48 6.74 4.95 21.91 26.04 17.39 17.62-3.78 26.76-13.56 28.03-29.89.9-11.61 2.94-9.89 3.07-20.27l1.64-4.91c1.89-15.73.3-20.81 11.16-18.45l2.64.23c7.99.36 18.45-1.29 24.59-4.14 13.22-6.13 21.06-16.38 8.02-13.69h.002'
fill='#336791'
@@ -4241,7 +4241,7 @@ export function OpenRouterIcon(props: SVGProps<SVGSVGElement>) {
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M16.8 1.96l7.22 4.11v.087L16.73 10.21l.017-2.12-.821-.03c-1.06-.028-1.61.002-2.27.11-1.06.175-2.04.577-3.15 1.35L8.35 11.03c-.284.2-.495.34-.68.46l-.515.32-.397.23.385.23.53.34c.476.31 1.17.8 2.7 1.87 1.11.78 2.08 1.18 3.15 1.35l.3.05c.694.09 1.38.094 2.83.033l.022-2.16 7.22 4.11v.087L16.59 22l.014-1.86-.635.02c-1.39.042-2.14.002-3.14-.162-1.69-.28-3.26-.926-4.88-2.06l-2.16-1.5a22 22 0 00-.755-.498l-.467-.28a55.93 55.93 0 00-.76-.43C2.91 14.73.56 14.12 0 14.12V9.89l.14c.564-.007 2.91-.622 3.81-1.12l1.02-.58.44-.274c.428-.28 1.07-.726 2.69-1.85 1.62-1.13 3.19-1.78 4.88-2.06 1.15-.19 1.97-.213 3.81-.138l.02-1.91z' />
<path d='M16.8 1.96l7.22 4.11v.087L16.73 10.21l.017-2.12-.821-.03c-1.06-.028-1.61.002-2.27.11-1.06.175-2.04.577-3.15 1.35L8.35 11.03c-.284.2-.495.34-.68.46l-.515.32-.397.23.385.23.53.34c.476.31 1.17.8 2.7 1.87 1.11.78 2.08 1.18 3.15 1.35l.3.05c.694.09 1.38.094 2.83.033l.022-2.16 7.22 4.11v.087L16.59 22l.014-1.86-.635.02c-1.39.042-2.14.002-3.14-.162-1.69-.28-3.26-.926-4.88-2.06l-2.16-1.5a22 22 0 00-.755-.498l-.467-.28a55.93 55.93 0 00-.76-.43C2.91 14.73.56 14.12 0 14.12V9.89l.14.004c.564-.007 2.91-.622 3.81-1.12l1.02-.58.44-.274c.428-.28 1.07-.726 2.69-1.85 1.62-1.13 3.19-1.78 4.88-2.06 1.15-.19 1.97-.213 3.81-.138l.02-1.91z' />
</svg>
)
}
@@ -4311,7 +4311,7 @@ export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
clipRule='evenodd'
fill='currentColor'
d='M88.04 42.81c1.61 4.64 2.76 9.38 3.14 14.3.472 6.1.256 12.15-1.03 18.14-.35.17-.109.32-.164.48-.403-.814-.049-1.21.012-3.33.523-6.65 1.07-9.98 1.6-3.44.557-6.88 1.09-10.31 1.69-1.22.21-2.72-.041-3.21 1.64-.14.05-.154.05-.235.08l.166-10.05-.169-24.25 1.6-.275c2.62-.429 5.24-.864 7.86-1.28 3.13-.497 6.26-.98 9.39-1.46 1.38-.215 2.76-.412 4.15-.618z'
d='M88.038 42.812c1.605 4.643 2.761 9.383 3.141 14.296.472 6.095.256 12.147-1.029 18.142-.035.165-.109.32-.164.48-.403.001-.814-.049-1.208.012-3.329.523-6.655 1.065-9.981 1.604-3.438.557-6.881 1.092-10.313 1.687-1.216.21-2.721-.041-3.212 1.641-.014.046-.154.054-.235.08l.166-10.051-.169-24.252 1.602-.275c2.62-.429 5.24-.864 7.862-1.281 3.129-.497 6.261-.98 9.392-1.465 1.381-.215 2.764-.412 4.148-.618z'
/>
<path
fillRule='evenodd'
@@ -4329,13 +4329,13 @@ export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
clipRule='evenodd'
fill='#409433'
d='M65.04 80.75c.081-.26.22-.34.24-.8.49-1.68 2-1.43 3.21-1.64 3.43-.594 6.88-1.13 10.31-1.69 3.33-.539 6.65-1.08 9.98-1.6.394-.62.81-.011 1.21-.012-.622 2.22-1.11 4.49-1.9 6.65-.896 2.45-1.98 4.84-3.13 7.18a49.14 49.14 0 01-6.35 9.76c-1.92 2.31-4.06 4.44-6.2 6.55-1.18 1.17-2.58 2.11-3.88 3.16l-.337-.23-1.21-1.04-1.26-2.75a41.4 41.4 0 01-1.39-9.84l.023-.561.17-2.43c.057-.828.13-1.65.168-2.48.129-2.98.241-5.96.359-8.95z'
d='M65.036 80.753c.081-.026.222-.034.235-.08.491-1.682 1.996-1.431 3.212-1.641 3.432-.594 6.875-1.13 10.313-1.687 3.326-.539 6.652-1.081 9.981-1.604.394-.062.805-.011 1.208-.012-.622 2.22-1.112 4.488-1.901 6.647-.896 2.449-1.98 4.839-3.131 7.182a49.142 49.142 0 01-6.353 9.763c-1.919 2.308-4.058 4.441-6.202 6.548-1.185 1.165-2.582 2.114-3.882 3.161l-.337-.23-1.214-1.038-1.256-2.753a41.402 41.402 0 01-1.394-9.838l.023-.561.171-2.426c.057-.828.133-1.655.168-2.485.129-2.982.241-5.964.359-8.946z'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#4FAA41'
d='M65.04 80.75c-.118 2.98-.23 5.96-.357 8.95-.035.83-.111 1.66-.168 2.49l-.765.29c-1.7-5-3.4-9.95-5.06-14.91-2.75-8.21-5.47-16.43-8.21-24.64a4498.89 4498.89 0 00-6.7-19.87c-.105-.31-.407-.552-.617-.826l4.9-9c.168.29.39.57.496.88a6167.48 6167.48 0 16.77 20.12c2.92 8.73 5.81 17.47 8.73 26.2.116.35.308.67.491 1.06l.67-.78-.167 10.05z'
d='M65.036 80.753c-.118 2.982-.23 5.964-.357 8.947-.035.83-.111 1.657-.168 2.485l-.765.289c-1.699-5.002-3.399-9.951-5.062-14.913-2.75-8.209-5.467-16.431-8.213-24.642a4498.887 4498.887 0 00-6.7-19.867c-.105-.31-.407-.552-.617-.826l4.896-9.002c.168.292.39.565.496.879a6167.476 6167.476 0 016.768 20.118c2.916 8.73 5.814 17.467 8.728 26.198.116.349.308.671.491 1.062l.67-.78-.167 10.052z'
/>
<path
fillRule='evenodd'
@@ -4359,7 +4359,7 @@ export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
clipRule='evenodd'
fill='#A9AA88'
d='M62.6 107.09c.263-1.31.609-2.62.77-3.95.325-2.65.548-5.31.814-7.97l.066-.1.07.011a41.4 41.4 0 1.39 9.84c-.176.23-.425.44-.518.7-.727 2.05-1.41 4.12-2.14 6.17-.1.28-.378.5-.574.74l-.747-2.57.87-2.97z'
d='M62.598 107.085c.263-1.315.609-2.62.772-3.947.325-2.649.548-5.312.814-7.968l.066-.01.066.011a41.402 41.402 0 001.394 9.838c-.176.232-.425.439-.518.701-.727 2.05-1.412 4.116-2.143 6.166-.1.28-.378.498-.574.744l-.747-2.566.87-2.969z'
/>
<path
fillRule='evenodd'
@@ -5099,7 +5099,7 @@ export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
<path
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
fill='#000000'
fill='currentColor'
/>
</svg>
)
@@ -5119,7 +5119,7 @@ export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
<g>
<path
d='M221.66,144.56 C221.66,146.9 220.76,149.15 219.17,150.8 C217.57,152.46 215.4,153.39 213.15,153.39 C210.89,153.39 208.72,152.46 207.12,150.8 C205.53,149.15 204.63,146.9 204.63,144.56 L204.63,65.68 C204.63,63.34 205.53,61.09 207.12,59.43 C208.72,57.78 210.89,56.85 213.15,56.85 C215.4,56.85 217.57,57.78 219.17,59.43 C220.76,61.09 221.66,63.34 221.66,65.68 L221.66,144.56 Z M218.7,199.22 C217.58,200.39 185.91,227.75 127.86,227.75 C69.81,227.75 38.35,200.53 37.01,199.36 C36.17,198.63 35.48,197.73 34.97,196.71 C34.47,195.69 34.16,194.59 34.07,193.45 C33.98,192.31 34.1,191.16 34.44,190.07 C34.77,188.98 35.32,187.98 36.03,187.1 C37.5,185.34 39.58,184.25 41.82,184.07 C44.06,183.9 46.28,184.64 47.99,186.16 C48.48,186.52 76.77,210.31 127.79,210.31 C178.81,210.31 207.3,186.37 207.59,186.16 C209.32,184.66 211.54,183.92 213.79,184.1 C216.04,184.27 218.13,185.35 219.62,187.1 C221.06,188.84 221.77,191.1 221.61,193.38 C221.46,195.66 220.44,197.78 218.78,199.29 L218.7,199.22 Z M33.99,65.68 C34.12,63.33 35.13,61.13 36.82,59.55 C38.51,57.98 40.73,57.17 43,57.29 C45.1,57.41 47.08,58.33 48.56,59.88 C50.04,61.43 50.92,63.5 51.02,65.68 L51.02,144.42 C51.02,146.76 50.12,149 48.52,150.66 C46.93,152.31 44.76,153.24 42.5,153.24 C40.24,153.24 38.08,152.31 36.48,150.66 C34.88,149 33.99,146.76 33.99,144.42 L33.99,65.68 Z M76.7,48.16 C76.83,45.81 77.85,43.61 79.54,42.04 C81.22,40.47 83.44,39.65 85.71,39.77 C87.81,39.9 89.79,40.82 91.27,42.37 C92.75,43.92 93.63,45.98 93.73,48.16 L93.73,164.92 C93.73,167.26 92.83,169.51 91.24,171.16 C89.64,172.82 87.47,173.75 85.22,173.75 C82.96,173.75 80.79,172.82 79.2,171.16 C77.6,169.51 76.7,167.26 76.7,164.92 L76.7,48.16 Z M119.63,43.79 C119.63,41.44 120.52,39.2 122.12,37.54 C123.72,35.89 125.88,34.96 128.14,34.96 C130.4,34.96 132.56,35.89 134.16,37.54 C135.76,39.2 136.66,41.44 136.66,43.79 L136.66,170.76 C136.66,173.1 135.76,175.35 134.16,177 C132.56,178.66 130.4,179.59 128.14,179.59 C125.88,179.59 123.72,178.66 122.12,177 C120.52,175.35 119.63,173.1 119.63,170.76 L119.63,43.79 Z M161.85,48.16 C161.85,45.82 162.74,43.58 164.34,41.92 C165.94,40.26 168.1,39.33 170.36,39.33 C172.62,39.33 174.79,40.26 176.38,41.92 C177.98,43.58 178.88,45.82 178.88,48.16 L178.88,164.92 C178.88,167.26 177.98,169.51 176.38,171.16 C174.79,172.82 172.62,173.75 170.36,173.75 C168.1,173.75 165.94,172.82 164.34,171.16 C162.74,169.51 161.85,167.26 161.85,164.92 L161.85,48.16 Z M223.98,0 L32.02,0 C27.83,-0.03 23.69,0.8 19.81,2.44 C15.94,4.07 12.41,6.48 9.44,9.53 C6.47,12.58 4.1,16.21 2.48,20.21 C0.86,24.21 0.02,28.5 0,32.84 L0,229.87 C0.02,234.2 0.86,238.5 2.48,242.49 C4.1,246.49 6.47,250.12 9.44,253.17 C12.41,256.22 15.94,258.64 19.81,260.27 C23.69,261.91 27.83,262.73 32.02,262.71 L223.98,262.71 C228.16,262.73 232.3,261.91 236.17,260.28 C240.04,258.65 243.56,256.24 246.53,253.2 C249.51,250.16 251.87,246.54 253.5,242.55 C255.12,238.55 255.97,234.27 256,229.94 L256,32.84 C255.98,28.51 255.14,24.22 253.52,20.23 C251.91,16.23 249.55,12.61 246.58,9.56 C243.62,6.51 240.1,4.1 236.24,2.46 C232.37,0.82 228.23,-0.02 224.05,0'
fill='#000000'
fill='currentColor'
/>
</g>
</svg>
@@ -5160,7 +5160,7 @@ export function MailchimpIcon(props: SVGProps<SVGSVGElement>) {
y='0px'
viewBox='0 0 230.81 244.96'
xmlSpace='preserve'
fill='#000000'
fill='currentColor'
>
<g>
<g>
@@ -5957,7 +5957,7 @@ export function TemporalIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} viewBox='390 391 388 388' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M651.14,517.35C642.02,449.03,618.94,392,583.49,392s-58.53,57.03-67.65,125.35c-68.32,9.12-125.35,32.2-125.35,67.65s57.04,58.53,125.35,67.65c9.12,68.31,32.2,125.35,67.65,125.35s58.53-57.04,67.65-125.35c68.32-9.12,125.35-32.2,125.35-67.65S719.45,526.47,651.14,517.35z M513.61,632.75c-65.43-9.45-103.59-31.08-103.59-47.75s38.16-38.3,103.59-47.75c-1.44,15.75-2.19,31.83-2.19,47.75C511.42,600.92,512.17,617.01,513.61,632.75z M583.49,411.53c16.67,0,38.3,38.16,47.75,103.59c-15.74-1.44-31.83-2.19-47.75-2.19s-32.01,0.75-47.75,2.19C545.19,449.69,566.82,411.53,583.49,411.53z M653.37,632.75c-3.22,0.47-16.43,2.02-19.77,2.35c-0.33,3.35-1.89,16.55-2.35,19.77c-9.45,65.43-31.08,103.59-47.75,103.59s-38.3-38.16-47.75-103.59c-0.46-3.22-2.02-16.43-2.35-19.77c-1.52-15.51-2.44-32.17-2.44-50.1s0.92-34.59,2.44-50.11c15.51-1.52,32.17-2.44,50.1-2.44s34.59,0.92,50.1,2.44c3.35,0.33,16.55,1.89,19.77,2.35c65.43,9.45,103.6,31.09,103.6,47.75S718.8,623.3,653.37,632.75z'
fill='#F2F2F2'
fill='currentColor'
/>
</svg>
)
@@ -5989,31 +5989,31 @@ export function DatadogIcon(props: SVGProps<SVGSVGElement>) {
export function DaytonaIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 69 73' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='3.63965' y='49.2356' width='24.9293' height='8.54716' fill='#000000' />
<rect x='37.1162' y='19.3206' width='28.4906' height='8.54716' fill='#000000' />
<rect x='3.63965' y='49.2356' width='24.9293' height='8.54716' fill='currentColor' />
<rect x='37.1162' y='19.3206' width='28.4906' height='8.54716' fill='currentColor' />
<rect
width='29.9151'
height='8.54717'
transform='matrix(0.707107 -0.707106 0.707107 0.707106 22.1582 21.9543)'
fill='#000000'
fill='currentColor'
/>
<rect
width='22.9746'
height='8.54717'
transform='matrix(-0.707107 -0.707106 -0.707107 0.707106 22.2891 43.3223)'
fill='#000000'
fill='currentColor'
/>
<rect
width='24.217'
height='8.54717'
transform='matrix(-0.707107 0.707106 -0.707107 -0.707106 43.6572 55.2791)'
fill='#000000'
fill='currentColor'
/>
<rect
width='27.066'
height='8.54717'
transform='matrix(0.707107 0.707106 0.707107 -0.707106 43.5264 33.9111)'
fill='#000000'
fill='currentColor'
/>
<rect
x='22.1582'
@@ -6021,7 +6021,7 @@ export function DaytonaIcon(props: SVGProps<SVGSVGElement>) {
width='20.6556'
height='8.54718'
transform='rotate(90 22.1582 12.9094)'
fill='#000000'
fill='currentColor'
/>
<rect
x='52.0732'
@@ -6029,7 +6029,7 @@ export function DaytonaIcon(props: SVGProps<SVGSVGElement>) {
width='25.6415'
height='8.54718'
transform='rotate(90 52.0732 42.825)'
fill='#000000'
fill='currentColor'
/>
</svg>
)
@@ -6213,7 +6213,7 @@ export function KetchIcon(props: SVGProps<SVGSVGElement>) {
<svg {...props} viewBox='0 0 224 213' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='m133.1 43.92 28.76-29.48h-70.38l-77.79 78.08 77.64 76.26-30.05 29.13h70.98l77.64-77.58-76.8-76.41z'
fill='#fff'
fill='currentColor'
/>
<path d='m91.71 86.79v81.44l40.38-41.7v-82.02l-40.38 42.28z' fill='#9B5CFF' />
</svg>
@@ -25,16 +25,15 @@ interface ErrorShellProps {
title: string
description: string
icon?: ReactNode
digest?: string
children: ReactNode
}
/**
* Centered layout shared by the workspace error boundary and not-found page.
* Renders a framed glyph, serif headline, supporting paragraph, optional
* digest pill, and a row of action buttons.
* Renders a framed glyph, serif headline, supporting paragraph, and a row of
* action buttons.
*/
export function ErrorShell({ title, description, icon, digest, children }: ErrorShellProps) {
export function ErrorShell({ title, description, icon, children }: ErrorShellProps) {
return (
<div className='flex h-full flex-1 items-center justify-center bg-[var(--bg)] px-6 py-12'>
<div className='flex w-full max-w-[420px] flex-col items-center gap-5 text-center'>
@@ -51,12 +50,6 @@ export function ErrorShell({ title, description, icon, digest, children }: Error
{description}
</p>
</div>
{digest && (
<span className='inline-flex max-w-full items-center gap-1.5 rounded-full border border-[var(--border-1)] bg-[var(--surface-5)] px-2.5 py-1 font-mono text-[11px]'>
<span className='text-[var(--text-muted)]'>digest</span>
<span className='truncate text-[var(--text-body)]'>{digest}</span>
</span>
)}
<div className='flex flex-wrap items-center justify-center gap-2 pt-1'>{children}</div>
</div>
</div>
@@ -85,7 +78,7 @@ export function ErrorState({
}, [error.message, error.digest, loggerName])
return (
<ErrorShell title={title} description={description} icon={icon} digest={error.digest}>
<ErrorShell title={title} description={description} icon={icon}>
{children}
<Button variant='primary' size='md' onClick={reset}>
Refresh
@@ -19,6 +19,7 @@ import { ConnectServiceAccountModal } from '@/app/workspace/[workspaceId]/integr
import { IntegrationSection } from '@/app/workspace/[workspaceId]/integrations/components/integration-section'
import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
import { CONNECT_MODE } from '@/app/workspace/[workspaceId]/integrations/connect-route'
import { useScrollRestoration } from '@/app/workspace/[workspaceId]/integrations/hooks/use-scroll-restoration'
import { getTileIconColorClass } from '@/blocks/icon-color'
import { storeCuratedPrompt } from '@/blocks/integration-matcher'
import {
@@ -44,6 +45,7 @@ interface IntegrationBlockDetailProps {
}
export function IntegrationBlockDetail({ integration, workspaceId }: IntegrationBlockDetailProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null)
useOAuthReturnRouter()
const router = useRouter()
const [connectMode, setConnectMode] = useQueryState(connectParam.key, connectParam.parser)
@@ -53,11 +55,13 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
const oauthService = resolveOAuthServiceForIntegration(integration)
const [oauthOpen, setOAuthOpen] = useState(false)
const { data: credentials = [] } = useWorkspaceCredentials({
const { data: credentials = [], isPending: credentialsLoading } = useWorkspaceCredentials({
workspaceId,
enabled: Boolean(workspaceId),
})
useScrollRestoration(scrollContainerRef, { ready: !credentialsLoading })
const connectedCredentials = useMemo(() => {
if (!oauthService) return []
return credentials.filter(
@@ -170,7 +174,10 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
serviceIcon={oauthService.serviceIcon}
/>
)}
<div className='min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]'>
<div
ref={scrollContainerRef}
className='min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]'
>
<div className='mx-auto flex max-w-[48rem] flex-col gap-7 pb-3'>
<div className='flex flex-col gap-3'>
{Icon ? (
@@ -0,0 +1,147 @@
'use client'
import { type RefObject, useEffect, useLayoutEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
/** Namespace prefix so restoration keys never collide with other tab state. */
const STORAGE_PREFIX = 'integrations-scroll:' as const
/**
* True when the most recent navigation was a Back/Forward history traversal.
*
* A single module-level `popstate` listener flips this before the destination
* page mounts; each restore reads it once and clears it. Fresh push navigations
* (a sidebar link, a typed URL) never fire `popstate`, so the flag stays false
* and those visits open at the top instead of jumping to a stale position.
* Registered at module load — a per-hook listener would attach too late to see
* the `popstate` that triggered its own mount.
*/
let lastNavWasTraversal = false
if (typeof window !== 'undefined') {
window.addEventListener('popstate', () => {
lastNavWasTraversal = true
})
}
interface UseScrollRestorationOptions {
/**
* Flag that flips to `true` once the async content the scroll height depends
* on has settled (e.g. the credentials query is no longer pending). A late
* restore is re-attempted when this transitions so we do not clamp against a
* too-short container while data is still loading.
*/
ready?: boolean
}
/**
* Restores the scroll position of an inner scroll container across browser
* Back/Forward navigation within the same tab.
*
* Next.js App Router only restores WINDOW scroll, so a page whose content
* scrolls inside a nested `overflow-y-auto` element loses its position on Back.
* This hook persists `scrollTop` in `sessionStorage` (per-pathname, tab-scoped)
* and re-applies it — only on history traversals — once the container has laid
* out.
*
* Programmatic vs. user scrolls are told apart by VALUE, not a one-shot event
* flag: a restore assigns `scrollTop === lastAppliedRef`, so the echoed scroll
* event compares equal and is ignored (it is never persisted, so a clamped
* value cannot overwrite the saved target). This avoids the race where the
* programmatic scroll event fires before the listener attaches and a stuck flag
* drops the user's first real scroll. The restore itself latches only on a full
* (non-clamped) apply, so a late `ready` retry can complete a position that was
* clamped against still-loading content, and it stops the moment the user
* scrolls away from the last applied value so it never fights them.
*
* @param containerRef Ref to the scrollable container element.
* @param options `ready` marks async content as settled for a late retry.
*/
export function useScrollRestoration(
containerRef: RefObject<HTMLDivElement | null>,
{ ready = true }: UseScrollRestorationOptions = {}
): void {
const pathname = usePathname()
const storageKey = `${STORAGE_PREFIX}${pathname}`
const storageKeyRef = useRef(storageKey)
const hasRestoredRef = useRef(false)
/** Last `scrollTop` this hook assigned, so its echo scroll event is ignored. */
const lastAppliedRef = useRef(-1)
/** Latest user-initiated `scrollTop`, flushed to storage on unmount. */
const latestUserScrollRef = useRef<number | null>(null)
const rafRef = useRef<number | null>(null)
/** Captured once per mount: did we arrive here via Back/Forward? */
const shouldRestoreRef = useRef<boolean | null>(null)
useEffect(() => {
storageKeyRef.current = storageKey
}, [storageKey])
useEffect(() => {
const el = containerRef.current
if (!el) return
const persist = (value: number) => {
try {
sessionStorage.setItem(storageKeyRef.current, String(value))
} catch {}
}
const onScroll = () => {
if (el.scrollTop === lastAppliedRef.current) return
latestUserScrollRef.current = el.scrollTop
if (rafRef.current !== null) return
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null
persist(el.scrollTop)
})
}
el.addEventListener('scroll', onScroll, { passive: true })
return () => {
el.removeEventListener('scroll', onScroll)
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current)
rafRef.current = null
}
if (latestUserScrollRef.current !== null) persist(latestUserScrollRef.current)
}
}, [containerRef])
useLayoutEffect(() => {
const el = containerRef.current
if (!el || hasRestoredRef.current) return
if (shouldRestoreRef.current === null) {
shouldRestoreRef.current = lastNavWasTraversal
lastNavWasTraversal = false
}
if (!shouldRestoreRef.current) {
hasRestoredRef.current = true
return
}
if (lastAppliedRef.current !== -1 && el.scrollTop !== lastAppliedRef.current) {
hasRestoredRef.current = true
return
}
let target = 0
try {
const raw = sessionStorage.getItem(storageKeyRef.current)
target = raw ? Number(raw) : 0
} catch {}
if (!Number.isFinite(target) || target <= 0) {
hasRestoredRef.current = true
return
}
const maxScroll = el.scrollHeight - el.clientHeight
if (maxScroll <= 0) return
lastAppliedRef.current = Math.min(target, maxScroll)
el.scrollTop = lastAppliedRef.current
if (maxScroll >= target) hasRestoredRef.current = true
}, [containerRef, ready])
}
@@ -1,6 +1,6 @@
'use client'
import { type ComponentType, useCallback, useMemo } from 'react'
import { type ComponentType, useCallback, useMemo, useRef } from 'react'
import {
ArrowRight,
ChevronDown,
@@ -26,6 +26,7 @@ import { IntegrationSection } from '@/app/workspace/[workspaceId]/integrations/c
import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header'
import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
import { ShowcaseWithExplore } from '@/app/workspace/[workspaceId]/integrations/components/showcase-with-explore'
import { useScrollRestoration } from '@/app/workspace/[workspaceId]/integrations/hooks/use-scroll-restoration'
import {
ALL_CATEGORY,
CONNECTED_LABEL,
@@ -135,6 +136,7 @@ function ConnectedItem({ href, blockType, name, description, icon: Icon }: Conne
}
export function Integrations() {
const scrollContainerRef = useRef<HTMLDivElement>(null)
const params = useParams()
const workspaceId = (params?.workspaceId as string) || ''
@@ -163,6 +165,8 @@ export function Integrations() {
enabled: Boolean(workspaceId),
})
useScrollRestoration(scrollContainerRef, { ready: !credentialsLoading })
const oauthCredentials = useMemo(
() => credentials.filter((c) => c.type === 'oauth' || c.type === 'service_account'),
[credentials]
@@ -289,7 +293,10 @@ export function Integrations() {
return (
<div className='flex h-full flex-col bg-[var(--bg)]'>
<IntegrationTabsHeader active='integrations' workspaceId={workspaceId} />
<div className='min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]'>
<div
ref={scrollContainerRef}
className='min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]'
>
<div className='mx-auto flex max-w-[48rem] flex-col gap-7 pb-3'>
<ShowcaseWithExplore prompt='Explain the integrations in Sim and what I should connect.' />
<div className='flex items-center gap-2'>
+109 -1
View File
@@ -1,6 +1,7 @@
import { ClipboardList, Database, Search, Server, Table, Wrench } from '@sim/emcn/icons'
import { getErrorMessage } from '@sim/utils/errors'
import { MySQLIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { MySQLResponse } from '@/tools/mysql/types'
@@ -382,3 +383,110 @@ Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
},
},
}
export const MySQLBlockMeta = {
tags: ['data-analytics'],
url: 'https://www.mysql.com',
templates: [
{
icon: Search,
title: 'Ask your MySQL database in plain English',
prompt:
'Build a workflow that takes a plain-English question, has an agent translate it into a MySQL SELECT against the known schema, runs the query, and returns the rows as a readable answer.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'database'],
},
{
icon: ClipboardList,
title: 'Daily MySQL metrics report to Slack',
prompt:
'Create a scheduled workflow that runs a MySQL query for yesterdays key metrics, has an agent summarize the rows into a short update, and posts the digest to a Slack channel every morning.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['reporting', 'analytics'],
alsoIntegrations: ['slack'],
},
{
icon: Table,
title: 'Document a MySQL schema',
prompt:
'Build a workflow that introspects a MySQL database schema, has an agent write plain-English descriptions of each table and column, and saves the generated data dictionary as a file.',
modules: ['files', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'documentation'],
},
{
icon: Database,
title: 'Sync records from a Sim table into MySQL',
prompt:
'Create a workflow that reads rows from a Sim table and upserts each one into a MySQL table — updating the row when a matching key exists and inserting it otherwise — so the two stay in sync.',
modules: ['tables', 'agent', 'workflows'],
category: 'operations',
tags: ['automation', 'database'],
},
{
icon: Search,
title: 'Nightly MySQL data-quality audit',
prompt:
'Build a scheduled workflow that runs MySQL queries checking for nulls, duplicates, and orphaned foreign keys, has an agent describe any issues found, and alerts the team in Slack when the counts are non-zero.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['data-quality', 'monitoring'],
alsoIntegrations: ['slack'],
},
{
icon: Wrench,
title: 'Scheduled MySQL cleanup job',
prompt:
'Create a scheduled workflow that deletes stale or soft-deleted rows from a MySQL table older than a retention window, reports the row count removed, and keeps the table lean over time.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['maintenance', 'database'],
},
{
icon: Database,
title: 'Capture incoming records into MySQL',
prompt:
'Build a workflow that takes an incoming payload, validates the fields with an agent, and inserts a new row into the right MySQL table so submissions are persisted for later querying.',
modules: ['agent', 'workflows'],
category: 'operations',
tags: ['automation', 'database'],
},
{
icon: Server,
title: 'Export a MySQL query result to a file',
prompt:
'Create a scheduled workflow that runs a MySQL query for a snapshot of a table, writes the returned rows to a file, and keeps a dated export available for backups and downstream analysis.',
modules: ['scheduled', 'files', 'agent', 'workflows'],
category: 'engineering',
tags: ['export', 'backup'],
},
],
skills: [
{
name: 'nl-to-sql-query',
description: 'Translate a plain-English question into a MySQL SELECT and return the rows.',
content:
'# Natural Language To SQL\n\nTurn a question into a MySQL query and answer from the rows.\n\n## Steps\n1. Introspect the schema so the agent knows the tables and columns.\n2. Have the agent write a single MySQL SELECT that answers the question, with a LIMIT for safety.\n3. Run the query operation.\n4. Summarize the returned rows into a readable answer.\n\n## Output\nReturn the generated SQL, the rows, and a short natural-language summary of the result.',
},
{
name: 'scheduled-sql-report',
description: 'Run a MySQL query on a schedule and post a summarized report.',
content:
'# Scheduled SQL Report\n\nProduce a recurring report from MySQL data.\n\n## Steps\n1. Run a query for the reporting window (for example, DATE_SUB(NOW(), INTERVAL 1 DAY)).\n2. Read rowCount and rows to gather the metrics.\n3. Have an agent summarize the numbers into a short update.\n4. Deliver the summary to the target channel or file.\n\n## Output\nReport the key metrics, the row count, and the delivered summary.',
},
{
name: 'document-schema',
description: 'Introspect a MySQL schema and generate a plain-English data dictionary.',
content:
'# Document Schema\n\nGenerate documentation from a live MySQL schema.\n\n## Steps\n1. Run the introspect operation to list tables, columns, and types.\n2. Have an agent describe each table and column in plain English.\n3. Assemble the descriptions into a structured data dictionary.\n4. Save the result as a file for the team.\n\n## Output\nReturn the data dictionary covering every table and column, and confirm the saved file.',
},
{
name: 'upsert-records',
description: 'Insert new rows or update existing ones in MySQL based on a key match.',
content:
"# Upsert Records\n\nKeep a MySQL table in sync with a source of records.\n\n## Preferred: native MySQL upsert\nWhen the target table has a PRIMARY KEY or UNIQUE index on the match key, run a single execute with `INSERT ... ON DUPLICATE KEY UPDATE` — MySQL inserts a new row or updates the existing one in one statement (do NOT use Postgres `ON CONFLICT`, which MySQL does not support):\n\n```sql\nINSERT INTO users (id, name, email)\nVALUES (1, 'Jane', 'jane@example.com')\nON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email);\n```\n\n## Fallback: check-then-write\nIf there is no unique key to rely on:\n1. For each record, run a query to check whether a row with the matching key exists.\n2. If it exists, run update with a WHERE condition on the key.\n3. If it does not, run insert with the record data.\n4. Track how many rows were inserted versus updated.\n\n## Output\nReturn the counts of inserted and updated rows and confirm the table is in sync.",
},
],
} as const satisfies BlockMeta
+108 -1
View File
@@ -1,6 +1,6 @@
import { getErrorMessage } from '@sim/utils/errors'
import { PostgresIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { PostgresResponse } from '@/tools/postgresql/types'
@@ -402,3 +402,110 @@ Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
},
},
}
export const PostgreSQLBlockMeta = {
tags: ['data-analytics'],
url: 'https://www.postgresql.org',
templates: [
{
icon: PostgresIcon,
title: 'Ask your database in plain English',
prompt:
'Build a workflow that takes a natural-language question, has an agent turn it into a PostgreSQL SELECT query, runs it against the database, and returns the resulting rows as a readable answer.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['database', 'reporting'],
},
{
icon: PostgresIcon,
title: 'Daily metrics digest to Slack',
prompt:
'Create a scheduled workflow that queries key business metrics from PostgreSQL each morning, has an agent summarize the numbers and notable changes, and posts the digest to a Slack channel.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['reporting', 'automation'],
alsoIntegrations: ['slack'],
},
{
icon: PostgresIcon,
title: 'Document the schema',
prompt:
'Build a workflow that introspects a PostgreSQL schema to list its tables, columns, keys, and indexes, then has an agent write plain-English documentation describing what each table holds.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['database', 'documentation'],
},
{
icon: PostgresIcon,
title: 'Upsert records into a table',
prompt:
'Create a workflow that takes incoming records, checks PostgreSQL for an existing row by key, then inserts a new row or updates the existing one so the table stays in sync without duplicates.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['database', 'sync'],
},
{
icon: PostgresIcon,
title: 'Nightly stale-data cleanup',
prompt:
'Build a scheduled workflow that deletes rows in PostgreSQL older than a retention cutoff and reports how many rows were removed, so tables stay lean on an interval.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['database', 'automation'],
},
{
icon: PostgresIcon,
title: 'Sync query results into a Sim table',
prompt:
'Create a scheduled workflow that runs a PostgreSQL query for the latest records and writes each row into a Sim table, so the data is available for downstream blocks without a live database call.',
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['database', 'sync'],
},
{
icon: PostgresIcon,
title: 'Alert on threshold breach',
prompt:
'Build a scheduled workflow that queries a PostgreSQL count or aggregate, compares it to a threshold, and posts a Slack alert only when the value crosses the limit so the team hears about problems early.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['monitoring', 'automation'],
alsoIntegrations: ['slack'],
},
{
icon: PostgresIcon,
title: 'Log form submissions to Postgres',
prompt:
'Create a workflow that takes an incoming payload, validates the fields, and inserts a new row into a PostgreSQL table so every submission is durably recorded.',
modules: ['agent', 'workflows'],
category: 'productivity',
tags: ['database', 'automation'],
},
],
skills: [
{
name: 'query-to-answer',
description: 'Turn a natural-language question into a PostgreSQL SELECT and return the rows.',
content:
'# Query To Answer\n\nAnswer a question by generating and running a PostgreSQL SELECT.\n\n## Steps\n1. Introspect the relevant schema first so the agent knows the real table and column names.\n2. Have the agent write a single SELECT with an explicit LIMIT for large result sets.\n3. Run the query and inspect rows and rowCount.\n4. Summarize the rows back to the caller in plain language.\n\n## Output\nReturn the answer text, the underlying rows, and the rowCount.',
},
{
name: 'upsert-records',
description: 'Insert new rows or update existing ones by key so a table stays deduplicated.',
content:
"# Upsert Records\n\nKeep a table in sync by inserting new rows or updating existing ones on a key.\n\n## Preferred: atomic upsert with execute\nUse the execute operation to run a single `INSERT ... ON CONFLICT ... DO UPDATE`, which is atomic and avoids the race between a separate read and write. This requires a unique or primary-key constraint on the conflict target.\n\n```sql\nINSERT INTO users (id, name, email)\nVALUES ('u1', 'Jane', 'jane@example.com')\nON CONFLICT (id)\nDO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email;\n```\n\n`EXCLUDED` refers to the row proposed for insertion, so `DO UPDATE SET col = EXCLUDED.col` applies the incoming values.\n\n## Fallback: read then write\nIf the key has no unique constraint, query the table for an existing row by key, then run insert when none exists or update with a WHERE condition on the key when one does.\n\n## Steps\n1. Ensure the conflict target has a unique or primary-key constraint.\n2. Run execute with `INSERT ... ON CONFLICT (key) DO UPDATE SET ... = EXCLUDED. ...` for each record.\n3. Inspect rowCount to confirm the write landed.\n\n## Output\nReport how many records were upserted and the total rowCount changed.",
},
{
name: 'document-schema',
description: 'Introspect a schema and produce plain-English documentation of its tables.',
content:
'# Document Schema\n\nDescribe a PostgreSQL schema in readable terms.\n\n## Steps\n1. Run introspect on the target schema to get tables, columns, keys, and indexes.\n2. Have an agent describe what each table stores and how they relate via foreign keys.\n3. Note primary keys and indexes that hint at common access patterns.\n4. Assemble the descriptions into a single document.\n\n## Output\nReturn the list of tables with a short description of each and the raw introspection result.',
},
{
name: 'retention-cleanup',
description: 'Delete rows older than a retention cutoff and report the count removed.',
content:
'# Retention Cleanup\n\nRemove stale rows on a schedule.\n\n## Steps\n1. Compute the retention cutoff timestamp for the run.\n2. Optionally query a count of rows older than the cutoff to preview the impact.\n3. Run delete with a WHERE condition comparing the timestamp column to the cutoff.\n4. Capture the rowCount removed for the run.\n\n## Output\nReport how many rows were deleted and the cutoff that was applied.',
},
],
} as const satisfies BlockMeta
+107 -1
View File
@@ -1,5 +1,6 @@
import { ClipboardList, Download, File, Search, Server, Trash, Upload } from '@sim/emcn/icons'
import { SftpIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { SftpUploadResult } from '@/tools/sftp/types'
@@ -311,3 +312,108 @@ export const SftpBlock: BlockConfig<SftpUploadResult> = {
error: { type: 'string', description: 'Error message if operation failed' },
},
}
export const SftpBlockMeta = {
tags: ['document-processing', 'automation'],
templates: [
{
icon: Download,
title: 'Pull new files from a remote drop folder',
prompt:
'Build a workflow that runs on a schedule, lists a remote SFTP drop folder, downloads any new files, and passes their contents into the workflow for processing.',
modules: ['scheduled', 'files', 'workflows'],
category: 'operations',
tags: ['files', 'automation', 'sftp'],
},
{
icon: Upload,
title: 'Push a generated report to a partner SFTP',
prompt:
'Build a workflow that generates a report file and uploads it to a partner SFTP server under a dated remote path.',
modules: ['files', 'workflows'],
category: 'operations',
tags: ['files', 'upload', 'sftp'],
},
{
icon: Server,
title: 'Mirror a local export to a remote directory',
prompt:
'Build a workflow that lists a remote SFTP directory, compares it to the files produced by an earlier block, and uploads any missing files to keep the remote directory in sync.',
modules: ['files', 'scheduled', 'workflows'],
category: 'operations',
tags: ['files', 'sync', 'sftp'],
},
{
icon: Trash,
title: 'Archive and delete old files on a schedule',
prompt:
'Build a workflow that runs nightly, lists an SFTP directory, downloads files older than a threshold to archive them, and then deletes the originals from the remote server.',
modules: ['scheduled', 'files', 'workflows'],
category: 'operations',
tags: ['files', 'cleanup', 'sftp'],
},
{
icon: ClipboardList,
title: 'Notify Slack when new files land on SFTP',
prompt:
'Build a workflow that checks an SFTP inbox folder on a schedule and sends a Slack message listing any new files that have arrived since the last run.',
modules: ['scheduled', 'files', 'agent'],
category: 'operations',
tags: ['files', 'notifications', 'sftp'],
alsoIntegrations: ['slack'],
},
{
icon: Search,
title: 'Inventory a remote directory listing',
prompt:
'Build a workflow that lists a remote SFTP directory with detailed metadata and records the file names, sizes, and timestamps for auditing.',
modules: ['files', 'workflows', 'tables'],
category: 'operations',
tags: ['files', 'audit', 'sftp'],
},
{
icon: File,
title: 'Write a manifest file to a remote server',
prompt:
'Build a workflow that assembles a manifest of processed records and creates a manifest.txt file on a remote SFTP path so downstream systems know the batch is ready.',
modules: ['files', 'workflows'],
category: 'operations',
tags: ['files', 'manifest', 'sftp'],
},
{
icon: Download,
title: 'Download a remote file and summarize it',
prompt:
'Build a workflow that downloads a file from an SFTP server and passes its contents to an agent that summarizes the key points.',
modules: ['files', 'agent', 'workflows'],
category: 'operations',
tags: ['files', 'summarize', 'sftp'],
},
],
skills: [
{
name: 'pull-remote-drop-folder',
description: 'Poll a remote SFTP drop folder on a schedule and ingest any new files.',
content:
'# Pull Remote Drop Folder\n\nPeriodically fetch newly arrived files from a remote SFTP directory into a workflow.\n\n## Steps\n1. Use the List Directory operation to read the remote drop folder and inspect `entries`.\n2. Filter for files newer than the last processed timestamp.\n3. For each new file, use the Download File operation and read `file`/`content`.\n4. Hand the contents to downstream blocks for parsing.\n\n## Output\nNew remote files are downloaded and their contents are available for processing each run.',
},
{
name: 'push-report-to-partner',
description: 'Upload a generated report to a partner SFTP server under a dated path.',
content:
'# Push Report To Partner\n\nDeliver a generated file to an external partner over SFTP.\n\n## Steps\n1. Produce the report file in an earlier block.\n2. Use the Upload Files operation with the partner `remotePath` (e.g. `/outbound/2026-07-01/`).\n3. Enable overwrite only if same-day re-delivery is expected.\n4. Confirm delivery from `uploadedFiles`.\n\n## Output\nThe report lands on the partner SFTP server at the intended remote path.',
},
{
name: 'archive-and-cleanup',
description: 'Download aging remote files to archive them, then delete the originals.',
content:
'# Archive And Cleanup\n\nKeep a remote SFTP directory tidy by archiving and removing old files.\n\n## Steps\n1. List the target directory and identify files past the retention window.\n2. Download each stale file to archive it elsewhere.\n3. Use the Delete File/Directory operation to remove the original and check `deletedPath`.\n4. Schedule the workflow to run nightly.\n\n## Output\nAging files are preserved in an archive and cleared from the live remote directory.',
},
{
name: 'remote-directory-inventory',
description: 'Capture a detailed listing of a remote SFTP directory for auditing.',
content:
'# Remote Directory Inventory\n\nRecord what currently exists in a remote SFTP path.\n\n## Steps\n1. Use the List Directory operation with detailed info enabled.\n2. Read `entries` for names, sizes, and timestamps.\n3. Store the inventory in a table or log for auditing.\n\n## Output\nA point-in-time inventory of the remote directory is captured.',
},
],
} as const satisfies BlockMeta
+97 -1
View File
@@ -1,5 +1,5 @@
import { SmtpIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { SmtpSendMailResult } from '@/tools/smtp/types'
@@ -209,3 +209,99 @@ export const SmtpBlock: BlockConfig<SmtpSendMailResult> = {
error: { type: 'string', description: 'Error message if sending failed' },
},
}
export const SmtpBlockMeta = {
tags: ['messaging', 'automation'],
templates: [
{
icon: SmtpIcon,
title: 'Scheduled daily digest email',
prompt:
'Build a scheduled workflow that gathers the days key updates, has an agent write a clean HTML summary, and sends it as a daily digest email to the team over SMTP.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['email', 'reporting', 'automation'],
},
{
icon: SmtpIcon,
title: 'Alert email when a condition is met',
prompt:
'Create a workflow that checks a metric or status, and when it crosses a threshold, sends an alert email over SMTP with the details and a clear subject so the on-call person notices immediately.',
modules: ['agent', 'workflows'],
category: 'operations',
tags: ['email', 'alerting', 'automation'],
},
{
icon: SmtpIcon,
title: 'Email a generated report as an attachment',
prompt:
'Build a workflow that generates a report file, then sends it as an email attachment over SMTP to the requested recipients with a short summary in the body.',
modules: ['files', 'agent', 'workflows'],
category: 'operations',
tags: ['email', 'attachments', 'reporting'],
},
{
icon: SmtpIcon,
title: 'Personalized outreach from a recipient table',
prompt:
'Create a workflow that reads a table of recipients, has an agent draft a personalized message for each row, and sends an individual email to every contact over SMTP with their name and details filled in.',
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['email', 'outreach', 'personalization'],
},
{
icon: SmtpIcon,
title: 'Form-submission auto-reply',
prompt:
'Build a workflow that triggers on a new form submission and sends an automatic confirmation email over SMTP to the submitter, using their address as the recipient and a friendly reply-to.',
modules: ['agent', 'workflows'],
category: 'support',
tags: ['email', 'auto-reply', 'automation'],
},
{
icon: SmtpIcon,
title: 'Incident notification email',
prompt:
'Create a workflow that, when an incident is detected, sends a notification email over SMTP to a distribution list with the severity, impact, and next steps, ccing the stakeholders who need to be looped in.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['email', 'incident', 'notification'],
},
{
icon: SmtpIcon,
title: 'Weekly summary email',
prompt:
'Build a scheduled workflow that compiles the weeks results into an HTML summary with an agent and emails it over SMTP to the team every Friday, with a link-friendly reply-to for follow-ups.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['email', 'summary', 'reporting'],
},
],
skills: [
{
name: 'send-html-email',
description:
'Send a formatted HTML email over SMTP with subject, recipients, and a reply-to.',
content:
'# Send HTML Email\n\nSend a well-formatted email through any SMTP server.\n\n## Steps\n1. Set the from, to, and subject fields, adding cc or bcc recipients when needed.\n2. Compose the body as HTML and set the content type to html.\n3. Set a reply-to address so responses route to the right inbox.\n4. Send the email and capture the returned message id.\n\n## Output\nReport whether the email was sent, the recipient, the subject, and the message id.',
},
{
name: 'email-with-attachment',
description: 'Attach a generated file to an email and send it over SMTP.',
content:
'# Email With Attachment\n\nSend a report or document as an email attachment.\n\n## Steps\n1. Generate or reference the file from a previous block.\n2. Pass the file into the attachments field of the SMTP send.\n3. Write a short body summarizing what the attachment contains.\n4. Send to the recipients and record the message id.\n\n## Output\nConfirm the email was sent with the attachment, and return the recipient and message id.',
},
{
name: 'personalized-batch-email',
description: 'Send an individual, personalized email to each recipient in a table.',
content:
'# Personalized Batch Email\n\nSend one tailored email per recipient from a table of contacts.\n\n## Steps\n1. Read the recipient rows, each with an email address and personalization fields.\n2. For each row, draft a personalized subject and body.\n3. Send an individual email over SMTP so recipients do not see one another.\n4. Track successes and failures per recipient.\n\n## Output\nReturn the count of emails sent, and list any recipients that failed with the error.',
},
{
name: 'scheduled-digest-email',
description: 'Compile updates on a schedule and send them as a recurring digest email.',
content:
'# Scheduled Digest Email\n\nSend a recurring summary email on a fixed cadence.\n\n## Steps\n1. On the schedule, gather the relevant updates for the period.\n2. Have an agent build a concise HTML summary.\n3. Set the subject with the date or period and send over SMTP to the distribution list.\n4. Use bcc for large lists to protect recipient privacy.\n\n## Output\nConfirm the digest was sent, the number of recipients, and the message id.',
},
],
} as const satisfies BlockMeta
+113 -1
View File
@@ -1,5 +1,14 @@
import {
ClipboardList,
Download,
File,
Search,
Server,
TerminalWindow,
Wrench,
} from '@sim/emcn/icons'
import { SshIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { SSHResponse } from '@/tools/ssh/types'
@@ -601,3 +610,106 @@ Examples:
message: { type: 'string', description: 'Operation status message' },
},
}
export const SSHBlockMeta = {
tags: ['automation'],
url: 'https://www.openssh.com',
templates: [
{
icon: TerminalWindow,
title: 'Scheduled server health check to Slack',
prompt:
'Every morning, SSH into the production server and run a health check command (uptime, load average, and free memory). Summarize stdout with an agent and post the result to the #ops Slack channel so the team starts the day knowing the box is healthy.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['devops', 'automation', 'monitoring'],
alsoIntegrations: ['slack'],
},
{
icon: Server,
title: 'Disk-usage alert with SSH',
prompt:
"On a schedule, SSH into the server and execute 'df -h /' to read disk usage. Parse the stdout, and if the used percentage crosses 85%, send an alert to Slack with the current usage; otherwise stay silent.",
modules: ['scheduled', 'workflows'],
category: 'operations',
tags: ['devops', 'monitoring', 'alerts'],
alsoIntegrations: ['slack'],
},
{
icon: Wrench,
title: 'Run a deploy script over SSH',
prompt:
'Trigger a deployment by SSHing into the app server and executing a maintenance script that pulls the latest code, installs dependencies, rebuilds, and restarts the service. Check the exitCode and success outputs, then report stdout and stderr back so failures are visible.',
modules: ['workflows', 'agent'],
category: 'engineering',
tags: ['devops', 'deployment', 'automation'],
},
{
icon: ClipboardList,
title: 'Tail and summarize a remote log',
prompt:
'SSH into the server and read the last 200 lines of /var/log/app.log, then hand the content to an agent that summarizes recent errors and warnings into a short digest. Post the digest so on-call engineers can scan incidents at a glance.',
modules: ['workflows', 'agent'],
category: 'engineering',
tags: ['devops', 'logs', 'observability'],
alsoIntegrations: ['slack'],
},
{
icon: Download,
title: 'Fetch a remote report file into the workflow',
prompt:
'Download a generated report file from the remote server via SSH and pass the resulting file output into the next step for processing or storage, so a nightly export on the box flows straight into the workflow.',
modules: ['scheduled', 'files', 'workflows'],
category: 'operations',
tags: ['devops', 'files', 'automation'],
},
{
icon: Search,
title: 'Verify a command exists before running it',
prompt:
'Before executing a maintenance step, use Check Command Exists over SSH to confirm a tool like docker or git is installed on the remote host. If the exists output is true, run the command; if not, report a clear message that the dependency is missing.',
modules: ['workflows'],
category: 'engineering',
tags: ['devops', 'validation', 'automation'],
},
{
icon: File,
title: 'Push a config file and restart a service',
prompt:
'Upload a rendered configuration file to the remote server via SSH, write it to the target path, then execute a command to restart the affected service. Confirm success from the exitCode and stderr outputs before finishing.',
modules: ['workflows', 'agent'],
category: 'operations',
tags: ['devops', 'configuration', 'automation'],
},
],
skills: [
{
name: 'remote-health-check',
description:
'Run a lightweight health check over SSH and turn the output into a readable status line.',
content:
'# Remote Health Check\n\nRun a quick SSH health probe and summarize the result.\n\n## Steps\n1. Use the Execute Command operation with `uptime && free -m && df -h /`.\n2. Read `stdout` for load, memory, and disk figures.\n3. Confirm `exitCode` is 0 and `success` is true before trusting the output.\n4. Summarize into one status line (healthy / degraded) for posting downstream.\n\n## Output\nA short human-readable status derived from `stdout`, gated on `success`.',
},
{
name: 'guarded-command-run',
description:
'Check a command exists over SSH before executing it, so missing dependencies fail loudly.',
content:
'# Guarded Command Run\n\nAvoid silent failures by verifying a dependency first.\n\n## Steps\n1. Use Check Command Exists with the target `commandName` (e.g. `docker`).\n2. Branch on the `exists` output.\n3. When `exists` is true, run the real command with Execute Command.\n4. When false, stop and report a clear "dependency missing" message.\n\n## Output\nEither the command `stdout`/`exitCode`, or an explicit missing-dependency message.',
},
{
name: 'remote-log-digest',
description:
'Pull the tail of a remote log over SSH and summarize errors and warnings into a digest.',
content:
'# Remote Log Digest\n\nTurn a noisy remote log into a scannable summary.\n\n## Steps\n1. Use Read File Content on the log path (e.g. `/var/log/app.log`) with a sensible max size.\n2. Take the returned `content` and pass it to an agent.\n3. Ask the agent to extract recent errors, warnings, and their counts.\n4. Emit a short digest for Slack or a report.\n\n## Output\nA concise digest of notable log lines derived from the file `content`.',
},
{
name: 'safe-config-deploy',
description:
'Upload a config file over SSH, then restart the service and verify the exit code.',
content:
'# Safe Config Deploy\n\nShip a config change and confirm the service came back healthy.\n\n## Steps\n1. Use Upload File (or Write File Content) to place the config at its target path.\n2. Execute the restart command for the affected service.\n3. Inspect `exitCode` and `stderr` to detect a failed restart.\n4. Report `success` plus any `stderr` so a bad deploy is visible immediately.\n\n## Output\nA pass/fail result built from `exitCode`, `success`, and `stderr`.',
},
],
} as const satisfies BlockMeta
+10 -5
View File
@@ -204,7 +204,7 @@ import {
import { MondayBlock, MondayBlockMeta } from '@/blocks/blocks/monday'
import { MongoDBBlock, MongoDBBlockMeta } from '@/blocks/blocks/mongodb'
import { MothershipBlock } from '@/blocks/blocks/mothership'
import { MySQLBlock } from '@/blocks/blocks/mysql'
import { MySQLBlock, MySQLBlockMeta } from '@/blocks/blocks/mysql'
import { Neo4jBlock, Neo4jBlockMeta } from '@/blocks/blocks/neo4j'
import { NeverBounceBlock, NeverBounceBlockMeta } from '@/blocks/blocks/neverbounce'
import { NewRelicBlock, NewRelicBlockMeta } from '@/blocks/blocks/new_relic'
@@ -230,7 +230,7 @@ import { PiBlock } from '@/blocks/blocks/pi'
import { PineconeBlock, PineconeBlockMeta } from '@/blocks/blocks/pinecone'
import { PipedriveBlock, PipedriveBlockMeta } from '@/blocks/blocks/pipedrive'
import { PolymarketBlock, PolymarketBlockMeta } from '@/blocks/blocks/polymarket'
import { PostgreSQLBlock } from '@/blocks/blocks/postgresql'
import { PostgreSQLBlock, PostgreSQLBlockMeta } from '@/blocks/blocks/postgresql'
import { PostHogBlock, PostHogBlockMeta } from '@/blocks/blocks/posthog'
import { ProfoundBlock, ProfoundBlockMeta } from '@/blocks/blocks/profound'
import { ProspeoBlock, ProspeoBlockMeta } from '@/blocks/blocks/prospeo'
@@ -264,19 +264,19 @@ import { SentryBlock, SentryBlockMeta } from '@/blocks/blocks/sentry'
import { SerperBlock, SerperBlockMeta } from '@/blocks/blocks/serper'
import { ServiceNowBlock, ServiceNowBlockMeta } from '@/blocks/blocks/servicenow'
import { SESBlock, SESBlockMeta } from '@/blocks/blocks/ses'
import { SftpBlock } from '@/blocks/blocks/sftp'
import { SftpBlock, SftpBlockMeta } from '@/blocks/blocks/sftp'
import { SharepointBlock, SharepointBlockMeta, SharepointV2Block } from '@/blocks/blocks/sharepoint'
import { ShopifyBlock, ShopifyBlockMeta } from '@/blocks/blocks/shopify'
import { SimWorkspaceEventBlock } from '@/blocks/blocks/sim_workspace_event'
import { SimilarwebBlock, SimilarwebBlockMeta } from '@/blocks/blocks/similarweb'
import { SixtyfourBlock, SixtyfourBlockMeta } from '@/blocks/blocks/sixtyfour'
import { SlackBlock, SlackBlockMeta } from '@/blocks/blocks/slack'
import { SmtpBlock } from '@/blocks/blocks/smtp'
import { SmtpBlock, SmtpBlockMeta } from '@/blocks/blocks/smtp'
import { SportmonksBlock, SportmonksBlockMeta } from '@/blocks/blocks/sportmonks'
import { SpotifyBlock, SpotifyBlockMeta } from '@/blocks/blocks/spotify'
import { SQSBlock, SQSBlockMeta } from '@/blocks/blocks/sqs'
import { SquareBlock, SquareBlockMeta } from '@/blocks/blocks/square'
import { SSHBlock } from '@/blocks/blocks/ssh'
import { SSHBlock, SSHBlockMeta } from '@/blocks/blocks/ssh'
import { StagehandBlock, StagehandBlockMeta } from '@/blocks/blocks/stagehand'
import { StartTriggerBlock } from '@/blocks/blocks/start_trigger'
import { StarterBlock } from '@/blocks/blocks/starter'
@@ -793,6 +793,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
mistral_parse: MistralParseBlockMeta,
monday: MondayBlockMeta,
mongodb: MongoDBBlockMeta,
mysql: MySQLBlockMeta,
neo4j: Neo4jBlockMeta,
neverbounce: NeverBounceBlockMeta,
new_relic: NewRelicBlockMeta,
@@ -812,6 +813,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
pinecone: PineconeBlockMeta,
pipedrive: PipedriveBlockMeta,
polymarket: PolymarketBlockMeta,
postgresql: PostgreSQLBlockMeta,
posthog: PostHogBlockMeta,
profound: ProfoundBlockMeta,
prospeo: ProspeoBlockMeta,
@@ -841,15 +843,18 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
serper: SerperBlockMeta,
servicenow: ServiceNowBlockMeta,
ses: SESBlockMeta,
sftp: SftpBlockMeta,
sharepoint: SharepointBlockMeta,
shopify: ShopifyBlockMeta,
similarweb: SimilarwebBlockMeta,
sixtyfour: SixtyfourBlockMeta,
slack: SlackBlockMeta,
smtp: SmtpBlockMeta,
sportmonks: SportmonksBlockMeta,
spotify: SpotifyBlockMeta,
sqs: SQSBlockMeta,
square: SquareBlockMeta,
ssh: SSHBlockMeta,
stagehand: StagehandBlockMeta,
stripe: StripeBlockMeta,
sts: STSBlockMeta,
+10 -10
View File
@@ -1357,7 +1357,7 @@ export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
/>
<path
fill='#1565c0'
d='M43.61,20.08L43.6,20L42,20H24v8h11.3c-0.79,2.24-2.23,4.17-4.09,5.57 c00,00,00l6.19,5.24C36.97,39.21,44,34,44,24C44,22.66,43.86,21.35,43.61,20.08z'
d='M43.61,20.08L43.6,20L42,20H24v8h11.3c-0.79,2.24-2.23,4.17-4.09,5.57 c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.24C36.97,39.21,44,34,44,24C44,22.66,43.86,21.35,43.61,20.08z'
/>
</svg>
)
@@ -3107,7 +3107,7 @@ export function OutlookIcon(props: SVGProps<SVGSVGElement>) {
/>
<path
fill='#1490DF'
d='M1811.58,927.59l-0.81,0.47l-634.49,356.85c-2.77,1.7-5.58,3.32-8.52,4.77 c-24.64,12.04-53.46,12.04-78.1,0c-2.92-1.44-5.76-3.04-8.52-4.77L446.66,928.06l-0.77-0.47 c-12.25-6.64-19.93-19.41-20.06-33.34v722.38c0.31,48.19,39.62,87,87.8,86.7c0,0,0,0,0,0h1229.64 c48.19,0.31,87.5-38.51,87.81-86.7c00,00,00V894.25C1831.07,908.07,1823.62,920.81,1811.58,927.59z'
d='M1811.58,927.59l-0.81,0.47l-634.49,356.85c-2.77,1.7-5.58,3.32-8.52,4.77 c-24.64,12.04-53.46,12.04-78.1,0c-2.92-1.44-5.76-3.04-8.52-4.77L446.66,928.06l-0.77-0.47 c-12.25-6.64-19.93-19.41-20.06-33.34v722.38c0.31,48.19,39.62,87,87.8,86.7c0,0,0,0,0,0h1229.64 c48.19,0.31,87.5-38.51,87.81-86.7c0-0.001,0-0.002,0-0.004V894.25C1831.07,908.07,1823.62,920.81,1811.58,927.59z'
/>
<path
opacity='0.1'
@@ -3436,7 +3436,7 @@ export const GroqIcon = (props: SVGProps<SVGSVGElement>) => (
xmlns='http://www.w3.org/2000/svg'
>
<title>Groq</title>
<path d='M12.04 2c-3.85-.035-7 3-7.04 6.78-.035 3.78 3.06 6.87 6.91 6.91h2.42v-2.57h-2.29c-2.41.028-4.38-1.87-4.41-4.23-.029-2.36 1.9-4.3 4.31-4.33h.1c2.41 0 4.36 1.92 4.37 4.28v6.31c0 2.34-1.94 4.25-4.32 4.28a4.38 4.38 0 01-3.03-1.25l-1.85 1.82A7 7 0 12.03 22h.092c3.8-.056 6.86-3.08 6.88-6.82v-6.5C18.91 4.96 15.82 2 12.04 2z' />
<path d='M12.04 2c-3.85-.035-7 3-7.04 6.78-.035 3.78 3.06 6.87 6.91 6.91h2.42v-2.57h-2.29c-2.41.028-4.38-1.87-4.41-4.23-.029-2.36 1.9-4.3 4.31-4.33h.1c2.41 0 4.36 1.92 4.37 4.28v6.31c0 2.34-1.94 4.25-4.32 4.28a4.38 4.38 0 01-3.03-1.25l-1.85 1.82A7 7 0 0012.03 22h.092c3.8-.056 6.86-3.08 6.88-6.82v-6.5C18.91 4.96 15.82 2 12.04 2z' />
</svg>
)
@@ -3545,7 +3545,7 @@ export const CerebrasIcon = (props: SVGProps<SVGSVGElement>) => (
<title>Cerebras</title>
<path
clipRule='evenodd'
d='M14.12 2.7a9.3 9.3 0 000 18.6V22.7c-5.91 0-10.7-4.79-10.7-10.7S8.21 1.3 14.12 1.3V2.7zm4.75 3.68A7.35 7.35 0 109.42 17.64l-.901 1.07a8.75 8.75 0 01-1.08-12.33 8.76 8.76 0 112.34-1.08l-.901 1.08zm-2.25.844a5.41 5.41 0 00-5.05 9.56l-.656 1.24a6.81 6.81 0 16.36-12.04l-.654 1.24zM14.12 8.54a3.46 3.46 0 100 6.92v1.4a4.86 4.86 0 010-9.73v1.4z'
d='M14.12 2.7a9.3 9.3 0 000 18.6V22.7c-5.91 0-10.7-4.79-10.7-10.7S8.21 1.3 14.12 1.3V2.7zm4.75 3.68A7.35 7.35 0 109.42 17.64l-.901 1.07a8.75 8.75 0 01-1.08-12.33 8.76 8.76 0 112.34-1.08l-.901 1.08zm-2.25.844a5.41 5.41 0 00-5.05 9.56l-.656 1.24a6.81 6.81 0 016.36-12.04l-.654 1.24zM14.12 8.54a3.46 3.46 0 100 6.92v1.4a4.86 4.86 0 010-9.73v1.4z'
fill='#F15A29'
fillRule='evenodd'
/>
@@ -4175,7 +4175,7 @@ export function PostgresIcon(props: SVGProps<SVGSVGElement>) {
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMinYMin meet'
>
<path d='M255.01 158.09c-1.53-4.65-5.56-7.89-10.76-8.66-2.45-.366-5.26-.21-8.58.475-5.79 1.2-10.09 1.65-13.22 1.74 11.84-19.98 21.46-42.77 27-64.23 8.96-34.69 4.17-50.49-1.42-57.64C233.22 10.85 211.61.683 185.55.372c-13.9-.17-26.11 2.58-32.47 4.55-5.93-1.05-12.3-1.63-18.99-1.74-12.54-.2-23.61 2.53-33.08 8.15-5.24-1.77-13.65-4.27-23.36-5.86-22.84-3.75-41.25-.828-54.72 8.69C6.62 25.67-.937 45.68.461 73.63c.444 8.87 5.41 35.87 13.22 61.48 4.49 14.72 9.28 26.94 14.24 36.33 7.03 13.32 14.55 21.16 22.99 23.97 4.73 1.58 13.33 2.68 22.37-4.85 1.15 1.39 2.68 2.77 4.7 4.05 2.58 1.63 5.73 2.95 8.88 3.74 11.34 2.84 21.96 2.13 31.03-1.85.056 1.61.099 3.15.135 4.48.06 2.16.12 4.27.199 6.25.54 13.37 1.45 23.77 4.14 31.05.148.4.35 1.1.56 1.66 1.35 4.12 3.59 11.01 9.32 16.41 5.93 5.59 13.09 7.31 19.66 7.31 3.29 0 6.43-.432 9.19-1.02 9.82-2.1 20.97-5.31 29.04-16.8 7.63-10.86 11.34-27.22 12.01-52.99.09-.729.17-1.42.244-2.09l.16-1.36 1.8.158.46.031c10.456 22.23-1.66 29.74-5.15 5.94-2.75 24.95-12.79 20.48-26.35' />
<path d='M255.01 158.09c-1.53-4.65-5.56-7.89-10.76-8.66-2.45-.366-5.26-.21-8.58.475-5.79 1.2-10.09 1.65-13.22 1.74 11.84-19.98 21.46-42.77 27-64.23 8.96-34.69 4.17-50.49-1.42-57.64C233.22 10.85 211.61.683 185.55.372c-13.9-.17-26.11 2.58-32.47 4.55-5.93-1.05-12.3-1.63-18.99-1.74-12.54-.2-23.61 2.53-33.08 8.15-5.24-1.77-13.65-4.27-23.36-5.86-22.84-3.75-41.25-.828-54.72 8.69C6.62 25.67-.937 45.68.461 73.63c.444 8.87 5.41 35.87 13.22 61.48 4.49 14.72 9.28 26.94 14.24 36.33 7.03 13.32 14.55 21.16 22.99 23.97 4.73 1.58 13.33 2.68 22.37-4.85 1.15 1.39 2.68 2.77 4.7 4.05 2.58 1.63 5.73 2.95 8.88 3.74 11.34 2.84 21.96 2.13 31.03-1.85.056 1.61.099 3.15.135 4.48.06 2.16.12 4.27.199 6.25.54 13.37 1.45 23.77 4.14 31.05.148.4.35 1.1.56 1.66 1.35 4.12 3.59 11.01 9.32 16.41 5.93 5.59 13.09 7.31 19.66 7.31 3.29 0 6.43-.432 9.19-1.02 9.82-2.1 20.97-5.31 29.04-16.8 7.63-10.86 11.34-27.22 12.01-52.99.09-.729.17-1.42.244-2.09l.16-1.36 1.8.158.46.031c10.002.456 22.232-1.665 29.743-5.154 5.935-2.754 24.954-12.795 20.476-26.351' />
<path
d='M237.91 160.72c-29.74 6.14-31.78-3.93-31.78-3.93 31.4-46.59 44.53-105.74 33.2-120.21-30.9-39.48-84.4-20.81-85.29-20.33l-.287.05c-5.88-1.22-12.45-1.95-19.84-2.07-13.46-.22-23.66 3.53-31.41 9.4 0 0-95.43-39.31-90.99 49.44.944 18.88 27.06 142.87 58.22 105.42 11.39-13.69 22.39-25.27 22.39-25.27 5.46 3.63 12.01 5.48 18.86 4.82l.533-.452c-.166 1.7-.09 3.36.213 5.33-8.03 8.97-5.67 10.54-21.71 13.84-16.23 3.35-6.7 9.3-.471 10.86 7.55 1.89 25.01 4.56 36.81-11.96l-.47 1.89c3.14 2.52 5.35 16.38 4.98 28.95-.37 12.57-.617 21.2 1.86 27.94 2.48 6.74 4.95 21.91 26.04 17.39 17.62-3.78 26.76-13.56 28.03-29.89.9-11.61 2.94-9.89 3.07-20.27l1.64-4.91c1.89-15.73.3-20.81 11.16-18.45l2.64.23c7.99.36 18.45-1.29 24.59-4.14 13.22-6.13 21.06-16.38 8.02-13.69h.002'
fill='#336791'
@@ -4241,7 +4241,7 @@ export function OpenRouterIcon(props: SVGProps<SVGSVGElement>) {
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M16.8 1.96l7.22 4.11v.087L16.73 10.21l.017-2.12-.821-.03c-1.06-.028-1.61.002-2.27.11-1.06.175-2.04.577-3.15 1.35L8.35 11.03c-.284.2-.495.34-.68.46l-.515.32-.397.23.385.23.53.34c.476.31 1.17.8 2.7 1.87 1.11.78 2.08 1.18 3.15 1.35l.3.05c.694.09 1.38.094 2.83.033l.022-2.16 7.22 4.11v.087L16.59 22l.014-1.86-.635.02c-1.39.042-2.14.002-3.14-.162-1.69-.28-3.26-.926-4.88-2.06l-2.16-1.5a22 22 0 00-.755-.498l-.467-.28a55.93 55.93 0 00-.76-.43C2.91 14.73.56 14.12 0 14.12V9.89l.14c.564-.007 2.91-.622 3.81-1.12l1.02-.58.44-.274c.428-.28 1.07-.726 2.69-1.85 1.62-1.13 3.19-1.78 4.88-2.06 1.15-.19 1.97-.213 3.81-.138l.02-1.91z' />
<path d='M16.8 1.96l7.22 4.11v.087L16.73 10.21l.017-2.12-.821-.03c-1.06-.028-1.61.002-2.27.11-1.06.175-2.04.577-3.15 1.35L8.35 11.03c-.284.2-.495.34-.68.46l-.515.32-.397.23.385.23.53.34c.476.31 1.17.8 2.7 1.87 1.11.78 2.08 1.18 3.15 1.35l.3.05c.694.09 1.38.094 2.83.033l.022-2.16 7.22 4.11v.087L16.59 22l.014-1.86-.635.02c-1.39.042-2.14.002-3.14-.162-1.69-.28-3.26-.926-4.88-2.06l-2.16-1.5a22 22 0 00-.755-.498l-.467-.28a55.93 55.93 0 00-.76-.43C2.91 14.73.56 14.12 0 14.12V9.89l.14.004c.564-.007 2.91-.622 3.81-1.12l1.02-.58.44-.274c.428-.28 1.07-.726 2.69-1.85 1.62-1.13 3.19-1.78 4.88-2.06 1.15-.19 1.97-.213 3.81-.138l.02-1.91z' />
</svg>
)
}
@@ -4311,7 +4311,7 @@ export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
clipRule='evenodd'
fill='currentColor'
d='M88.04 42.81c1.61 4.64 2.76 9.38 3.14 14.3.472 6.1.256 12.15-1.03 18.14-.35.17-.109.32-.164.48-.403-.814-.049-1.21.012-3.33.523-6.65 1.07-9.98 1.6-3.44.557-6.88 1.09-10.31 1.69-1.22.21-2.72-.041-3.21 1.64-.14.05-.154.05-.235.08l.166-10.05-.169-24.25 1.6-.275c2.62-.429 5.24-.864 7.86-1.28 3.13-.497 6.26-.98 9.39-1.46 1.38-.215 2.76-.412 4.15-.618z'
d='M88.038 42.812c1.605 4.643 2.761 9.383 3.141 14.296.472 6.095.256 12.147-1.029 18.142-.035.165-.109.32-.164.48-.403.001-.814-.049-1.208.012-3.329.523-6.655 1.065-9.981 1.604-3.438.557-6.881 1.092-10.313 1.687-1.216.21-2.721-.041-3.212 1.641-.014.046-.154.054-.235.08l.166-10.051-.169-24.252 1.602-.275c2.62-.429 5.24-.864 7.862-1.281 3.129-.497 6.261-.98 9.392-1.465 1.381-.215 2.764-.412 4.148-.618z'
/>
<path
fillRule='evenodd'
@@ -4329,13 +4329,13 @@ export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
clipRule='evenodd'
fill='#409433'
d='M65.04 80.75c.081-.26.22-.34.24-.8.49-1.68 2-1.43 3.21-1.64 3.43-.594 6.88-1.13 10.31-1.69 3.33-.539 6.65-1.08 9.98-1.6.394-.62.81-.011 1.21-.012-.622 2.22-1.11 4.49-1.9 6.65-.896 2.45-1.98 4.84-3.13 7.18a49.14 49.14 0 01-6.35 9.76c-1.92 2.31-4.06 4.44-6.2 6.55-1.18 1.17-2.58 2.11-3.88 3.16l-.337-.23-1.21-1.04-1.26-2.75a41.4 41.4 0 01-1.39-9.84l.023-.561.17-2.43c.057-.828.13-1.65.168-2.48.129-2.98.241-5.96.359-8.95z'
d='M65.036 80.753c.081-.026.222-.034.235-.08.491-1.682 1.996-1.431 3.212-1.641 3.432-.594 6.875-1.13 10.313-1.687 3.326-.539 6.652-1.081 9.981-1.604.394-.062.805-.011 1.208-.012-.622 2.22-1.112 4.488-1.901 6.647-.896 2.449-1.98 4.839-3.131 7.182a49.142 49.142 0 01-6.353 9.763c-1.919 2.308-4.058 4.441-6.202 6.548-1.185 1.165-2.582 2.114-3.882 3.161l-.337-.23-1.214-1.038-1.256-2.753a41.402 41.402 0 01-1.394-9.838l.023-.561.171-2.426c.057-.828.133-1.655.168-2.485.129-2.982.241-5.964.359-8.946z'
/>
<path
fillRule='evenodd'
clipRule='evenodd'
fill='#4FAA41'
d='M65.04 80.75c-.118 2.98-.23 5.96-.357 8.95-.035.83-.111 1.66-.168 2.49l-.765.29c-1.7-5-3.4-9.95-5.06-14.91-2.75-8.21-5.47-16.43-8.21-24.64a4498.89 4498.89 0 00-6.7-19.87c-.105-.31-.407-.552-.617-.826l4.9-9c.168.29.39.57.496.88a6167.48 6167.48 0 16.77 20.12c2.92 8.73 5.81 17.47 8.73 26.2.116.35.308.67.491 1.06l.67-.78-.167 10.05z'
d='M65.036 80.753c-.118 2.982-.23 5.964-.357 8.947-.035.83-.111 1.657-.168 2.485l-.765.289c-1.699-5.002-3.399-9.951-5.062-14.913-2.75-8.209-5.467-16.431-8.213-24.642a4498.887 4498.887 0 00-6.7-19.867c-.105-.31-.407-.552-.617-.826l4.896-9.002c.168.292.39.565.496.879a6167.476 6167.476 0 016.768 20.118c2.916 8.73 5.814 17.467 8.728 26.198.116.349.308.671.491 1.062l.67-.78-.167 10.052z'
/>
<path
fillRule='evenodd'
@@ -4359,7 +4359,7 @@ export function MongoDBIcon(props: SVGProps<SVGSVGElement>) {
fillRule='evenodd'
clipRule='evenodd'
fill='#A9AA88'
d='M62.6 107.09c.263-1.31.609-2.62.77-3.95.325-2.65.548-5.31.814-7.97l.066-.1.07.011a41.4 41.4 0 1.39 9.84c-.176.23-.425.44-.518.7-.727 2.05-1.41 4.12-2.14 6.17-.1.28-.378.5-.574.74l-.747-2.57.87-2.97z'
d='M62.598 107.085c.263-1.315.609-2.62.772-3.947.325-2.649.548-5.312.814-7.968l.066-.01.066.011a41.402 41.402 0 001.394 9.838c-.176.232-.425.439-.518.701-.727 2.05-1.412 4.116-2.143 6.166-.1.28-.378.498-.574.744l-.747-2.566.87-2.969z'
/>
<path
fillRule='evenodd'
+11 -6
View File
@@ -1,5 +1,5 @@
{
"updatedAt": "2026-06-30",
"updatedAt": "2026-07-01",
"integrations": [
{
"type": "onepassword",
@@ -11269,7 +11269,8 @@
"triggerCount": 0,
"authType": "none",
"category": "tools",
"integrationType": "databases"
"integrationType": "databases",
"tags": ["data-analytics"]
},
{
"type": "neo4j",
@@ -12365,7 +12366,8 @@
"triggerCount": 0,
"authType": "none",
"category": "tools",
"integrationType": "databases"
"integrationType": "databases",
"tags": ["data-analytics"]
},
{
"type": "posthog",
@@ -15252,7 +15254,8 @@
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "documents"
"integrationType": "documents",
"tags": ["document-processing", "automation"]
},
{
"type": "sharepoint_v2",
@@ -15722,7 +15725,8 @@
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "email"
"integrationType": "email",
"tags": ["messaging", "automation"]
},
{
"type": "sportmonks",
@@ -16850,7 +16854,8 @@
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "devops"
"integrationType": "devops",
"tags": ["automation"]
},
{
"type": "stagehand",
+45 -2
View File
@@ -26,7 +26,7 @@
import { execSync } from 'child_process'
import { SUBBLOCK_ID_MIGRATIONS } from '@/lib/workflows/migrations/subblock-migrations'
import { getAllBlocks } from '@/blocks/registry'
import { getAllBlocks, getBlockMeta } from '@/blocks/registry'
import { tools as toolRegistry } from '@/tools/registry'
const baseRef = process.argv[2] || 'HEAD~1'
@@ -237,6 +237,42 @@ function checkCanonicalIdContract(): CheckResult {
return { kind: 'fail', errors }
}
/**
* Every catalog-visible integration block must expose a `BlockMeta` entry in
* `BLOCK_META_REGISTRY`. The integration detail pages
* (`getTemplatesForBlock`/`getSuggestedSkillsForBlock`) and the landing catalog
* read metas by block type; a `tools`-category block that ships in the toolbar
* without a meta renders an empty detail page (no templates, no skills).
*
* "Catalog-visible integration" mirrors the `isIntegrationBlock` predicate in
* `scripts/generate-docs.ts`: `category === 'tools' && !hideFromToolbar`. Core
* primitives (`agent`/`api`/`function`/…), hidden/legacy base versions, and
* first-party `blocks`-category blocks intentionally carry no meta and are not
* checked. `getBlockMeta` resolves through the version suffix, so a versioned
* block (e.g. `github_v2`) passes via its base meta.
*/
function checkIntegrationMetaCoverage(): CheckResult {
const errors: string[] = []
for (const block of getAllBlocks()) {
const isCatalogIntegration = block.category === 'tools' && !block.hideFromToolbar
if (!isCatalogIntegration) continue
if (!getBlockMeta(block.type)) {
errors.push(
`Block "${block.type}" is a catalog integration (category: 'tools', not hidden) but has no BlockMeta.\n` +
` → Export a \`${block.type}\`-keyed \`{Service}BlockMeta\` (tags + templates + skills) from its block file\n` +
' and register it in BLOCK_META_REGISTRY (apps/sim/blocks/registry-maps.ts), alphabetically.'
)
}
}
if (errors.length === 0) {
return { kind: 'pass', message: 'Integration meta coverage check passed' }
}
return { kind: 'fail', errors }
}
function reportResult(label: string, failureHeader: string, result: CheckResult): boolean {
if (result.kind === 'pass') {
console.log(`${result.message}`)
@@ -256,6 +292,7 @@ function reportResult(label: string, failureHeader: string, result: CheckResult)
const stabilityResult = checkSubblockIdStability()
const canonicalResult = checkCanonicalIdContract()
const metaCoverageResult = checkIntegrationMetaCoverage()
const stabilityOk = reportResult(
'Subblock ID stability check',
@@ -269,4 +306,10 @@ const canonicalOk = reportResult(
canonicalResult
)
process.exit(stabilityOk && canonicalOk ? 0 : 1)
const metaCoverageOk = reportResult(
'Integration meta coverage check',
'Catalog integrations without a BlockMeta render empty detail pages (no templates, no skills).',
metaCoverageResult
)
process.exit(stabilityOk && canonicalOk && metaCoverageOk ? 0 : 1)
+1
View File
@@ -32,6 +32,7 @@
"check:client-boundary": "bun run scripts/check-client-boundary-imports.ts --check",
"check:utils": "bun run scripts/check-utils-enforcement.ts",
"check:bare-icons": "bun run scripts/check-bare-icons.ts",
"check:icon-paths": "bun run scripts/check-icon-paths.ts",
"check:migrations": "bun run scripts/check-migrations-safety.ts",
"mship-contracts:generate": "bun run scripts/sync-mothership-stream-contract.ts",
"mship-contracts:check": "bun run scripts/sync-mothership-stream-contract.ts --check",
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env bun
/**
* Validates that every `<path d='…'>` in `apps/sim/components/icons.tsx` is
* syntactically well-formed SVG path data.
*
* Malformed path data does not crash the build or fail TypeScript — the browser
* silently drops the bad segment and logs `<path> attribute d: Expected number`
* / `Expected arc flag` to the console. A bulk icon reformat once corrupted
* several brand icons this way (dropped arc-flag digits, mangled cubic operands
* like `c00,00,00`), flooding the integrations page with dozens of console
* errors that no existing check caught. This script is that missing gate.
*
* It walks each `d` string against the SVG path grammar and fails if any
* command has the wrong operand count or an arc flag that is not `0`/`1`.
* Number scanning handles the compact forms the spec allows (packed decimals
* `.5.5`, sign-delimited `1-2`, exponents, and arc flags packed against
* neighbours like `001.39`), so valid minified paths pass unflagged.
*
* Run: `bun run scripts/check-icon-paths.ts`
*/
import { readFile } from 'node:fs/promises'
import path from 'node:path'
const ROOT = path.resolve(import.meta.dir, '..')
/** Defaults to the shared icon module; overridable via argv for testing. */
const ICONS_FILE = process.argv[2]
? path.resolve(process.argv[2])
: path.join(ROOT, 'apps/sim/components/icons.tsx')
/** Operand count per path command; arc (`a`) is handled specially for flags. */
const OPERANDS: Record<string, number> = {
m: 2,
l: 2,
h: 1,
v: 1,
c: 6,
s: 4,
q: 4,
t: 2,
a: 7,
z: 0,
}
const isWsp = (ch: string) => ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f'
/** Skip whitespace and comma separators; returns the next significant index. */
function skipSep(d: string, i: number): number {
while (i < d.length && (isWsp(d[i]) || d[i] === ',')) i++
return i
}
/**
* Scan one number starting at `i` (after separators are skipped). Returns the
* index past the number, or `-1` if no valid number begins here. Mirrors the
* SVG number grammar: optional sign, integer/fraction, optional exponent.
*/
function scanNumber(d: string, i: number): number {
const start = i
if (d[i] === '+' || d[i] === '-') i++
let digits = 0
while (i < d.length && d[i] >= '0' && d[i] <= '9') {
i++
digits++
}
if (d[i] === '.') {
i++
while (i < d.length && d[i] >= '0' && d[i] <= '9') {
i++
digits++
}
}
if (digits === 0) return -1
if (d[i] === 'e' || d[i] === 'E') {
let j = i + 1
if (d[j] === '+' || d[j] === '-') j++
let expDigits = 0
while (j < d.length && d[j] >= '0' && d[j] <= '9') {
j++
expDigits++
}
if (expDigits > 0) i = j
}
return i > start ? i : -1
}
interface PathError {
reason: string
/** 0-based offset into the `d` string where parsing failed. */
offset: number
}
/**
* Validate a single `d` string. Returns the first structural error, or `null`
* when the path is well-formed.
*/
function validatePath(d: string): PathError | null {
let i = skipSep(d, 0)
if (i >= d.length) return { reason: 'empty path data', offset: 0 }
let cmd = ''
// The first command must be a moveto.
if (d[i] !== 'M' && d[i] !== 'm') return { reason: 'path must start with M/m', offset: i }
while (i < d.length) {
i = skipSep(d, i)
if (i >= d.length) break
const ch = d[i]
if (/[a-zA-Z]/.test(ch)) {
if (!(ch.toLowerCase() in OPERANDS)) {
return { reason: `unknown command '${ch}'`, offset: i }
}
cmd = ch
i++
if (cmd === 'z' || cmd === 'Z') continue
} else if (cmd === '' || cmd === 'z' || cmd === 'Z') {
return { reason: `expected a command, found '${ch}'`, offset: i }
}
// After an explicit M/m, repeated operand groups are implicit L/l.
const effective = cmd === 'M' ? 'L' : cmd === 'm' ? 'l' : cmd
const key = effective.toLowerCase()
if (key === 'a') {
const err = scanArcGroup(d, i)
if (typeof err === 'string') return { reason: err, offset: i }
i = err
} else {
const count = OPERANDS[key]
for (let n = 0; n < count; n++) {
const before = skipSep(d, i)
const next = scanNumber(d, before)
if (next < 0) {
return { reason: `expected number for '${cmd}' command`, offset: before }
}
i = next
}
}
}
return null
}
/**
* Scan one 7-operand arc group: rx ry x-axis-rotation large-arc-flag
* sweep-flag x y. Flags are a single `0`/`1` that may be packed against the
* next token (e.g. `001.39`). Returns the next index, or an error string.
*/
function scanArcGroup(d: string, i: number): number | string {
for (let n = 0; n < 3; n++) {
const before = skipSep(d, i)
const next = scanNumber(d, before)
if (next < 0) return `expected number in arc command`
i = next
}
for (let n = 0; n < 2; n++) {
const before = skipSep(d, i)
if (d[before] !== '0' && d[before] !== '1') {
return `expected arc flag ('0' or '1')`
}
i = before + 1
}
for (let n = 0; n < 2; n++) {
const before = skipSep(d, i)
const next = scanNumber(d, before)
if (next < 0) return `expected number in arc command`
i = next
}
return i
}
interface Finding {
icon: string
line: number
reason: string
snippet: string
}
/**
* Find the nearest preceding icon export for a source offset. Matches both
* `export function XxxIcon` and `export const XxxIcon =` forms so arrow-function
* icons are attributed correctly.
*/
function iconNameAt(src: string, offset: number): string {
const before = src.slice(0, offset)
const matches = [...before.matchAll(/export (?:function|const) (\w+)\s*[=(]/g)]
return matches.length ? matches[matches.length - 1][1] : '<unknown>'
}
async function main() {
const src = await readFile(ICONS_FILE, 'utf8')
const findings: Finding[] = []
for (const m of src.matchAll(/\bd=(?:'([^']*)'|"([^"]*)")/g)) {
const d = m[1] ?? m[2] ?? ''
if (!d.trim()) continue
const err = validatePath(d)
if (!err) continue
// Offset of the `d` value within the file → the corrupt char.
const valueStart = m.index + m[0].indexOf(d)
const fileOffset = valueStart + err.offset
const line = src.slice(0, fileOffset).split('\n').length
const around = d.slice(Math.max(0, err.offset - 20), err.offset + 20)
findings.push({
icon: iconNameAt(src, m.index),
line,
reason: err.reason,
snippet: `${around}`,
})
}
if (findings.length === 0) {
console.log('✓ All icon <path> data in components/icons.tsx is valid SVG.')
process.exit(0)
}
console.error(`\nFound ${findings.length} malformed icon path(s) in components/icons.tsx:\n`)
for (const f of findings) {
console.error(` ${f.icon} (icons.tsx:${f.line}) — ${f.reason}`)
console.error(` near: ${f.snippet}`)
console.error(
' Malformed path data renders nothing and floods the console with SVG parse errors.'
)
console.error(' Fix the d attribute (correct operand counts; arc flags must be 0 or 1).\n')
}
process.exit(1)
}
main()