mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-19 02:34:37 -05:00
Compare commits
3 Commits
feat/datav
...
v0.5.93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdca73679d | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -4407,161 +4407,6 @@ export function DatadogIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MicrosoftDataverseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const clip0 = `dataverse_clip0_${id}`
|
||||
const clip1 = `dataverse_clip1_${id}`
|
||||
const clip2 = `dataverse_clip2_${id}`
|
||||
const paint0 = `dataverse_paint0_${id}`
|
||||
const paint1 = `dataverse_paint1_${id}`
|
||||
const paint2 = `dataverse_paint2_${id}`
|
||||
const paint3 = `dataverse_paint3_${id}`
|
||||
const paint4 = `dataverse_paint4_${id}`
|
||||
const paint5 = `dataverse_paint5_${id}`
|
||||
const paint6 = `dataverse_paint6_${id}`
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='96'
|
||||
height='96'
|
||||
viewBox='0 0 96 96'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<g clipPath={`url(#${clip0})`}>
|
||||
<g clipPath={`url(#${clip1})`}>
|
||||
<g clipPath={`url(#${clip2})`}>
|
||||
<path
|
||||
d='M13.8776 21.8242C29.1033 8.13791 49.7501 8.1861 62.955 18.9134C74.9816 28.6836 77.4697 44.3159 70.851 55.7801C64.2321 67.2443 52.5277 70.1455 39.5011 62.6247L31.7286 76.087L31.7234 76.0862C27.4181 83.5324 17.8937 86.0828 10.4437 81.7817C7.45394 80.0556 5.25322 77.4879 3.96665 74.551L3.96096 74.5511C-4.07832 55.7804 0.200745 34.1184 13.8776 21.8242Z'
|
||||
fill={`url(#${paint0})`}
|
||||
/>
|
||||
<path
|
||||
d='M13.8776 21.8242C29.1033 8.13791 49.7501 8.1861 62.955 18.9134C74.9816 28.6836 77.4697 44.3159 70.851 55.7801C64.2321 67.2443 52.5277 70.1455 39.5011 62.6247L31.7286 76.087L31.7234 76.0862C27.4181 83.5324 17.8937 86.0828 10.4437 81.7817C7.45394 80.0556 5.25322 77.4879 3.96665 74.551L3.96096 74.5511C-4.07832 55.7804 0.200745 34.1184 13.8776 21.8242Z'
|
||||
fill={`url(#${paint1})`}
|
||||
fillOpacity='0.8'
|
||||
/>
|
||||
<path
|
||||
d='M85.4327 14.2231C88.4528 15.9668 90.6686 18.569 91.9494 21.5433L91.9533 21.5444C99.9406 40.2943 95.6533 61.9068 81.9983 74.1814C66.7726 87.8677 46.1257 87.8196 32.9209 77.0923C20.8945 67.3221 18.4062 51.6897 25.0249 40.2256C31.6438 28.7614 43.3482 25.8601 56.3748 33.381L64.1434 19.9255L64.1482 19.9249C68.4516 12.4736 77.9805 9.92084 85.4327 14.2231Z'
|
||||
fill={`url(#${paint2})`}
|
||||
/>
|
||||
<path
|
||||
d='M85.4327 14.2231C88.4528 15.9668 90.6686 18.569 91.9494 21.5433L91.9533 21.5444C99.9406 40.2943 95.6533 61.9068 81.9983 74.1814C66.7726 87.8677 46.1257 87.8196 32.9209 77.0923C20.8945 67.3221 18.4062 51.6897 25.0249 40.2256C31.6438 28.7614 43.3482 25.8601 56.3748 33.381L64.1434 19.9255L64.1482 19.9249C68.4516 12.4736 77.9805 9.92084 85.4327 14.2231Z'
|
||||
fill={`url(#${paint3})`}
|
||||
fillOpacity='0.9'
|
||||
/>
|
||||
<path
|
||||
d='M39.5041 62.6261C52.5307 70.1469 64.2352 67.2456 70.8541 55.7814C77.2488 44.7055 75.1426 29.7389 64.147 19.9271L56.3791 33.3814L39.5041 62.6261Z'
|
||||
fill={`url(#${paint4})`}
|
||||
/>
|
||||
<path
|
||||
d='M56.3794 33.3815C43.3528 25.8607 31.6482 28.762 25.0294 40.2262C18.6347 51.3021 20.7409 66.2687 31.7364 76.0806L39.5043 62.6262L56.3794 33.3815Z'
|
||||
fill={`url(#${paint5})`}
|
||||
/>
|
||||
<path
|
||||
d='M33.3215 56.4453C37.9837 64.5204 48.3094 67.2872 56.3846 62.625C64.4598 57.9628 67.2266 47.6371 62.5643 39.5619C57.9021 31.4867 47.5764 28.72 39.5013 33.3822C31.4261 38.0444 28.6593 48.3701 33.3215 56.4453Z'
|
||||
fill={`url(#${paint6})`}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id={paint0}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(46.0001 49.4996) rotate(-148.717) scale(46.2195 47.5359)'
|
||||
>
|
||||
<stop offset='0.465088' stopColor='#09442A' />
|
||||
<stop offset='0.70088' stopColor='#136C6C' />
|
||||
<stop offset='1' stopColor='#22918B' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint1}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(50.0001 32.4996) rotate(123.57) scale(66.0095 46.5498)'
|
||||
>
|
||||
<stop offset='0.718705' stopColor='#1A7F7C' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#16BBDA' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint2}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(50.4999 44.5001) rotate(30.75) scale(45.9618 44.5095)'
|
||||
>
|
||||
<stop offset='0.358097' stopColor='#136C6C' />
|
||||
<stop offset='0.789474' stopColor='#42B870' />
|
||||
<stop offset='1' stopColor='#76D45E' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint3}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(42.5 -36.0002 31.1824 36.8127 49.4998 55.5001)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0.583166' stopColor='#76D45E' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#C8F5B7' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint4}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(47.5 48) rotate(-58.9042) scale(32.6898)'
|
||||
>
|
||||
<stop offset='0.486266' stopColor='#22918B' />
|
||||
<stop offset='0.729599' stopColor='#42B870' />
|
||||
<stop offset='1' stopColor='#43E5CA' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint5}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(47.3833 49.0077) rotate(119.859) scale(31.1328 29.4032)'
|
||||
>
|
||||
<stop offset='0.459553' stopColor='#08494E' />
|
||||
<stop offset='0.742242' stopColor='#1A7F7C' />
|
||||
<stop offset='1' stopColor='#309C61' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint6}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(52.5 40) rotate(120.784) scale(27.3542)'
|
||||
>
|
||||
<stop stopColor='#C8F5B7' />
|
||||
<stop offset='0.24583' stopColor='#98F0B0' />
|
||||
<stop offset='0.643961' stopColor='#52D17C' />
|
||||
<stop offset='1' stopColor='#119FC5' />
|
||||
</radialGradient>
|
||||
<clipPath id={clip0}>
|
||||
<rect width='96' height='96' fill='white' />
|
||||
</clipPath>
|
||||
<clipPath id={clip1}>
|
||||
<rect width='96' height='96' fill='white' />
|
||||
</clipPath>
|
||||
<clipPath id={clip2}>
|
||||
<rect width='95.9998' height='96' fill='white' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function KalshiIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 78 20' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -5687,33 +5532,3 @@ export function OnePasswordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function VercelIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 256 222'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid'
|
||||
>
|
||||
<g transform='translate(19.2 16.63) scale(0.85)'>
|
||||
<polygon fill='#fafafa' points='128 0 256 221.705007 0 221.705007' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudflareIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
|
||||
<path
|
||||
fill='#f38020'
|
||||
d='M331 326c11-26-4-38-19-38l-148-2c-4 0-4-6 1-7l150-2c17-1 37-15 43-33 0 0 10-21 9-24a97 97 0 0 0-187-11c-38-25-78 9-69 46-48 3-65 46-60 72 0 1 1 2 3 2h274c1 0 3-1 3-3z'
|
||||
/>
|
||||
<path
|
||||
fill='#faae40'
|
||||
d='M381 224c-4 0-6-1-7 1l-5 21c-5 16 3 30 20 31l32 2c4 0 4 6-1 7l-33 1c-36 4-46 39-46 39 0 2 0 3 2 3h113l3-2a81 81 0 0 0-78-103'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
CloudflareIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatadogIcon,
|
||||
@@ -72,7 +71,6 @@ import {
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
Mem0Icon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftOneDriveIcon,
|
||||
MicrosoftPlannerIcon,
|
||||
@@ -127,7 +125,6 @@ import {
|
||||
TTSIcon,
|
||||
TwilioIcon,
|
||||
TypeformIcon,
|
||||
VercelIcon,
|
||||
VideoIcon,
|
||||
WealthboxIcon,
|
||||
WebflowIcon,
|
||||
@@ -158,7 +155,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
circleback: CirclebackIcon,
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
cloudflare: CloudflareIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
datadog: DatadogIcon,
|
||||
@@ -212,7 +208,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
mailgun: MailgunIcon,
|
||||
mem0: Mem0Icon,
|
||||
memory: BrainIcon,
|
||||
microsoft_dataverse: MicrosoftDataverseIcon,
|
||||
microsoft_excel_v2: MicrosoftExcelIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
microsoft_teams: MicrosoftTeamsIcon,
|
||||
@@ -267,7 +262,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
twilio_sms: TwilioIcon,
|
||||
twilio_voice: TwilioIcon,
|
||||
typeform: TypeformIcon,
|
||||
vercel: VercelIcon,
|
||||
video_generator_v2: VideoIcon,
|
||||
vision_v2: EyeIcon,
|
||||
wealthbox: WealthboxIcon,
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
---
|
||||
title: Cloudflare
|
||||
description: Manage DNS, domains, certificates, and cache
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="cloudflare"
|
||||
color="#F5F6FA"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Cloudflare](https://cloudflare.com/) is a global cloud platform that provides content delivery, domain management, cybersecurity, and performance services for websites and applications.
|
||||
|
||||
In Sim, the Cloudflare integration empowers your agents to automate the management of DNS records, SSL/TLS certificates, domains (zones), cache, zone settings, and more through easy-to-use API tools. Agents can securely list and edit domains, update DNS records, monitor analytics, and manage security and performance—all as part of your automated workflows.
|
||||
|
||||
With Cloudflare, you can:
|
||||
|
||||
- **Manage DNS and Domains**: List all your domains (zones), view zone details, and fully control DNS records from your automated agent workflows.
|
||||
- **Handle SSL/TLS Certificates and Settings**: Issue, renew, or list certificates and adjust security and performance settings for your sites.
|
||||
- **Purge Cache and Analyze Traffic**: Instantly purge edge cache and review real-time DNS analytics directly within your Sim agent processes.
|
||||
- **Automate Security and Operations**: Use agents to programmatically manage zones, update settings, and streamline repetitive Cloudflare tasks.
|
||||
|
||||
This integration enables streamlined, secure management of your site's infrastructure from within Sim. Your agents can integrate Cloudflare operations directly into processes—keeping DNS records up-to-date, responding to security events, improving site performance, and automating large-scale site and account administration.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Cloudflare into the workflow. Manage zones (domains), DNS records, SSL/TLS certificates, zone settings, DNS analytics, and cache purging via the Cloudflare API.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `cloudflare_list_zones`
|
||||
|
||||
Lists all zones (domains) in the Cloudflare account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | No | Filter zones by domain name \(e.g., "example.com"\) |
|
||||
| `status` | string | No | Filter by zone status: "initializing", "pending", "active", or "moved" |
|
||||
| `page` | number | No | Page number for pagination \(default: 1\) |
|
||||
| `per_page` | number | No | Number of zones per page \(default: 20, max: 50\) |
|
||||
| `accountId` | string | No | Filter zones by account ID |
|
||||
| `order` | string | No | Sort field \(name, status, account.id, account.name\) |
|
||||
| `direction` | string | No | Sort direction \(asc, desc\) |
|
||||
| `match` | string | No | Match logic for filters \(any, all\). Default: all |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `zones` | array | List of zones/domains |
|
||||
| ↳ `id` | string | Zone ID |
|
||||
| ↳ `name` | string | Domain name |
|
||||
| ↳ `status` | string | Zone status \(initializing, pending, active, moved\) |
|
||||
| ↳ `paused` | boolean | Whether the zone is paused |
|
||||
| ↳ `type` | string | Zone type \(full, partial, or secondary\) |
|
||||
| ↳ `name_servers` | array | Assigned Cloudflare name servers |
|
||||
| ↳ `original_name_servers` | array | Original name servers before moving to Cloudflare |
|
||||
| ↳ `created_on` | string | ISO 8601 date when the zone was created |
|
||||
| ↳ `modified_on` | string | ISO 8601 date when the zone was last modified |
|
||||
| ↳ `activated_on` | string | ISO 8601 date when the zone was activated |
|
||||
| ↳ `development_mode` | number | Seconds remaining in development mode \(0 = off\) |
|
||||
| ↳ `plan` | object | Zone plan information |
|
||||
| ↳ `id` | string | Plan identifier |
|
||||
| ↳ `name` | string | Plan name |
|
||||
| ↳ `price` | number | Plan price |
|
||||
| ↳ `is_subscribed` | boolean | Whether the zone is subscribed to the plan |
|
||||
| ↳ `frequency` | string | Plan billing frequency |
|
||||
| ↳ `currency` | string | Plan currency |
|
||||
| ↳ `legacy_id` | string | Legacy plan identifier |
|
||||
| ↳ `account` | object | Account the zone belongs to |
|
||||
| ↳ `id` | string | Account identifier |
|
||||
| ↳ `name` | string | Account name |
|
||||
| ↳ `owner` | object | Zone owner information |
|
||||
| ↳ `id` | string | Owner identifier |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `type` | string | Owner type |
|
||||
| ↳ `meta` | object | Zone metadata |
|
||||
| ↳ `cdn_only` | boolean | Whether the zone is CDN only |
|
||||
| ↳ `custom_certificate_quota` | number | Custom certificate quota |
|
||||
| ↳ `dns_only` | boolean | Whether the zone is DNS only |
|
||||
| ↳ `foundation_dns` | boolean | Whether foundation DNS is enabled |
|
||||
| ↳ `page_rule_quota` | number | Page rule quota |
|
||||
| ↳ `phishing_detected` | boolean | Whether phishing was detected |
|
||||
| ↳ `step` | number | Current setup step |
|
||||
| ↳ `vanity_name_servers` | array | Custom vanity name servers |
|
||||
| ↳ `permissions` | array | User permissions for the zone |
|
||||
| `total_count` | number | Total number of zones matching the query |
|
||||
|
||||
### `cloudflare_get_zone`
|
||||
|
||||
Gets details for a specific zone (domain) by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to retrieve details for |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Zone ID |
|
||||
| `name` | string | Domain name |
|
||||
| `status` | string | Zone status \(initializing, pending, active, moved\) |
|
||||
| `paused` | boolean | Whether the zone is paused |
|
||||
| `type` | string | Zone type \(full, partial, or secondary\) |
|
||||
| `name_servers` | array | Assigned Cloudflare name servers |
|
||||
| `original_name_servers` | array | Original name servers before moving to Cloudflare |
|
||||
| `created_on` | string | ISO 8601 date when the zone was created |
|
||||
| `modified_on` | string | ISO 8601 date when the zone was last modified |
|
||||
| `activated_on` | string | ISO 8601 date when the zone was activated |
|
||||
| `development_mode` | number | Seconds remaining in development mode \(0 = off\) |
|
||||
| `plan` | object | Zone plan information |
|
||||
| ↳ `id` | string | Plan identifier |
|
||||
| ↳ `name` | string | Plan name |
|
||||
| ↳ `price` | number | Plan price |
|
||||
| ↳ `is_subscribed` | boolean | Whether the zone is subscribed to the plan |
|
||||
| ↳ `frequency` | string | Plan billing frequency |
|
||||
| ↳ `currency` | string | Plan currency |
|
||||
| ↳ `legacy_id` | string | Legacy plan identifier |
|
||||
| `account` | object | Account the zone belongs to |
|
||||
| ↳ `id` | string | Account identifier |
|
||||
| ↳ `name` | string | Account name |
|
||||
| `owner` | object | Zone owner information |
|
||||
| ↳ `id` | string | Owner identifier |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `type` | string | Owner type |
|
||||
| `meta` | object | Zone metadata |
|
||||
| ↳ `cdn_only` | boolean | Whether the zone is CDN only |
|
||||
| ↳ `custom_certificate_quota` | number | Custom certificate quota |
|
||||
| ↳ `dns_only` | boolean | Whether the zone is DNS only |
|
||||
| ↳ `foundation_dns` | boolean | Whether foundation DNS is enabled |
|
||||
| ↳ `page_rule_quota` | number | Page rule quota |
|
||||
| ↳ `phishing_detected` | boolean | Whether phishing was detected |
|
||||
| ↳ `step` | number | Current setup step |
|
||||
| `vanity_name_servers` | array | Custom vanity name servers |
|
||||
| `permissions` | array | User permissions for the zone |
|
||||
|
||||
### `cloudflare_create_zone`
|
||||
|
||||
Adds a new zone (domain) to the Cloudflare account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | Yes | The domain name to add \(e.g., "example.com"\) |
|
||||
| `accountId` | string | Yes | The Cloudflare account ID |
|
||||
| `type` | string | No | Zone type: "full" \(Cloudflare manages DNS\), "partial" \(CNAME setup\), or "secondary" \(secondary DNS\) |
|
||||
| `jump_start` | boolean | No | Automatically attempt to fetch existing DNS records when creating the zone |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created zone ID |
|
||||
| `name` | string | Domain name |
|
||||
| `status` | string | Zone status \(initializing, pending, active, moved\) |
|
||||
| `paused` | boolean | Whether the zone is paused |
|
||||
| `type` | string | Zone type \(full, partial, or secondary\) |
|
||||
| `name_servers` | array | Assigned Cloudflare name servers |
|
||||
| `original_name_servers` | array | Original name servers before moving to Cloudflare |
|
||||
| `created_on` | string | ISO 8601 date when the zone was created |
|
||||
| `modified_on` | string | ISO 8601 date when the zone was last modified |
|
||||
| `activated_on` | string | ISO 8601 date when the zone was activated |
|
||||
| `development_mode` | number | Seconds remaining in development mode \(0 = off\) |
|
||||
| `plan` | object | Zone plan information |
|
||||
| ↳ `id` | string | Plan identifier |
|
||||
| ↳ `name` | string | Plan name |
|
||||
| ↳ `price` | number | Plan price |
|
||||
| ↳ `is_subscribed` | boolean | Whether the zone is subscribed to the plan |
|
||||
| ↳ `frequency` | string | Plan billing frequency |
|
||||
| ↳ `currency` | string | Plan currency |
|
||||
| ↳ `legacy_id` | string | Legacy plan identifier |
|
||||
| `account` | object | Account the zone belongs to |
|
||||
| ↳ `id` | string | Account identifier |
|
||||
| ↳ `name` | string | Account name |
|
||||
| `owner` | object | Zone owner information |
|
||||
| ↳ `id` | string | Owner identifier |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `type` | string | Owner type |
|
||||
| `meta` | object | Zone metadata |
|
||||
| ↳ `cdn_only` | boolean | Whether the zone is CDN only |
|
||||
| ↳ `custom_certificate_quota` | number | Custom certificate quota |
|
||||
| ↳ `dns_only` | boolean | Whether the zone is DNS only |
|
||||
| ↳ `foundation_dns` | boolean | Whether foundation DNS is enabled |
|
||||
| ↳ `page_rule_quota` | number | Page rule quota |
|
||||
| ↳ `phishing_detected` | boolean | Whether phishing was detected |
|
||||
| ↳ `step` | number | Current setup step |
|
||||
| `vanity_name_servers` | array | Custom vanity name servers |
|
||||
| `permissions` | array | User permissions for the zone |
|
||||
|
||||
### `cloudflare_delete_zone`
|
||||
|
||||
Deletes a zone (domain) from the Cloudflare account.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to delete |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Deleted zone ID |
|
||||
|
||||
### `cloudflare_list_dns_records`
|
||||
|
||||
Lists DNS records for a specific zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to list DNS records for |
|
||||
| `type` | string | No | Filter by record type \(e.g., "A", "AAAA", "CNAME", "MX", "TXT"\) |
|
||||
| `name` | string | No | Filter by record name \(exact match\) |
|
||||
| `content` | string | No | Filter by record content \(exact match\) |
|
||||
| `page` | number | No | Page number for pagination \(default: 1\) |
|
||||
| `per_page` | number | No | Number of records per page \(default: 100, max: 5000000\) |
|
||||
| `direction` | string | No | Sort direction \(asc or desc\) |
|
||||
| `match` | string | No | Match logic for filters: any or all \(default: all\) |
|
||||
| `order` | string | No | Sort field \(type, name, content, ttl, proxied\) |
|
||||
| `proxied` | boolean | No | Filter by proxy status |
|
||||
| `search` | string | No | Free-text search across record name, content, and value |
|
||||
| `tag` | string | No | Filter by tags \(comma-separated\) |
|
||||
| `tag_match` | string | No | Tag filter match logic: any or all |
|
||||
| `commentFilter` | string | No | Filter records by comment content \(substring match\) |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | List of DNS records |
|
||||
| ↳ `id` | string | Unique identifier for the DNS record |
|
||||
| ↳ `zone_id` | string | The ID of the zone the record belongs to |
|
||||
| ↳ `zone_name` | string | The name of the zone |
|
||||
| ↳ `type` | string | Record type \(A, AAAA, CNAME, MX, TXT, etc.\) |
|
||||
| ↳ `name` | string | Record name \(e.g., example.com\) |
|
||||
| ↳ `content` | string | Record content \(e.g., IP address\) |
|
||||
| ↳ `proxiable` | boolean | Whether the record can be proxied |
|
||||
| ↳ `proxied` | boolean | Whether Cloudflare proxy is enabled |
|
||||
| ↳ `ttl` | number | TTL in seconds \(1 = automatic\) |
|
||||
| ↳ `locked` | boolean | Whether the record is locked |
|
||||
| ↳ `priority` | number | MX/SRV record priority |
|
||||
| ↳ `comment` | string | Comment associated with the record |
|
||||
| ↳ `tags` | array | Tags associated with the record |
|
||||
| ↳ `comment_modified_on` | string | ISO 8601 timestamp when the comment was last modified |
|
||||
| ↳ `tags_modified_on` | string | ISO 8601 timestamp when tags were last modified |
|
||||
| ↳ `meta` | object | Record metadata |
|
||||
| ↳ `source` | string | Source of the DNS record |
|
||||
| ↳ `created_on` | string | ISO 8601 timestamp when the record was created |
|
||||
| ↳ `modified_on` | string | ISO 8601 timestamp when the record was last modified |
|
||||
| `total_count` | number | Total number of DNS records matching the query |
|
||||
|
||||
### `cloudflare_create_dns_record`
|
||||
|
||||
Creates a new DNS record for a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to create the DNS record in |
|
||||
| `type` | string | Yes | DNS record type \(e.g., "A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV"\) |
|
||||
| `name` | string | Yes | DNS record name \(e.g., "example.com" or "subdomain.example.com"\) |
|
||||
| `content` | string | Yes | DNS record content \(e.g., IP address for A records, target for CNAME\) |
|
||||
| `ttl` | number | No | Time to live in seconds \(1 = automatic, default: 1\) |
|
||||
| `proxied` | boolean | No | Whether to enable Cloudflare proxy \(default: false\) |
|
||||
| `priority` | number | No | Priority for MX and SRV records |
|
||||
| `comment` | string | No | Comment for the DNS record |
|
||||
| `tags` | string | No | Comma-separated tags for the DNS record |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Unique identifier for the created DNS record |
|
||||
| `zone_id` | string | The ID of the zone the record belongs to |
|
||||
| `zone_name` | string | The name of the zone |
|
||||
| `type` | string | DNS record type \(A, AAAA, CNAME, MX, TXT, etc.\) |
|
||||
| `name` | string | DNS record hostname |
|
||||
| `content` | string | DNS record value \(e.g., IP address, target hostname\) |
|
||||
| `proxiable` | boolean | Whether the record can be proxied through Cloudflare |
|
||||
| `proxied` | boolean | Whether Cloudflare proxy is enabled |
|
||||
| `ttl` | number | Time to live in seconds \(1 = automatic\) |
|
||||
| `locked` | boolean | Whether the record is locked |
|
||||
| `priority` | number | Priority for MX and SRV records |
|
||||
| `comment` | string | Comment associated with the record |
|
||||
| `tags` | array | Tags associated with the record |
|
||||
| `comment_modified_on` | string | ISO 8601 timestamp when the comment was last modified |
|
||||
| `tags_modified_on` | string | ISO 8601 timestamp when tags were last modified |
|
||||
| `meta` | object | Record metadata |
|
||||
| ↳ `source` | string | Source of the DNS record |
|
||||
| `created_on` | string | ISO 8601 timestamp when the record was created |
|
||||
| `modified_on` | string | ISO 8601 timestamp when the record was last modified |
|
||||
|
||||
### `cloudflare_update_dns_record`
|
||||
|
||||
Updates an existing DNS record for a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID containing the DNS record |
|
||||
| `recordId` | string | Yes | The DNS record ID to update |
|
||||
| `type` | string | No | DNS record type \(e.g., "A", "AAAA", "CNAME", "MX", "TXT"\) |
|
||||
| `name` | string | No | DNS record name |
|
||||
| `content` | string | No | DNS record content \(e.g., IP address\) |
|
||||
| `ttl` | number | No | Time to live in seconds \(1 = automatic\) |
|
||||
| `proxied` | boolean | No | Whether to enable Cloudflare proxy |
|
||||
| `priority` | number | No | Priority for MX and SRV records |
|
||||
| `comment` | string | No | Comment for the DNS record |
|
||||
| `tags` | string | No | Comma-separated tags for the DNS record |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Unique identifier for the updated DNS record |
|
||||
| `zone_id` | string | The ID of the zone the record belongs to |
|
||||
| `zone_name` | string | The name of the zone |
|
||||
| `type` | string | DNS record type \(A, AAAA, CNAME, MX, TXT, etc.\) |
|
||||
| `name` | string | DNS record hostname |
|
||||
| `content` | string | DNS record value \(e.g., IP address, target hostname\) |
|
||||
| `proxiable` | boolean | Whether the record can be proxied through Cloudflare |
|
||||
| `proxied` | boolean | Whether Cloudflare proxy is enabled |
|
||||
| `ttl` | number | Time to live in seconds \(1 = automatic\) |
|
||||
| `locked` | boolean | Whether the record is locked |
|
||||
| `priority` | number | Priority for MX and SRV records |
|
||||
| `comment` | string | Comment associated with the record |
|
||||
| `tags` | array | Tags associated with the record |
|
||||
| `comment_modified_on` | string | ISO 8601 timestamp when the comment was last modified |
|
||||
| `tags_modified_on` | string | ISO 8601 timestamp when tags were last modified |
|
||||
| `meta` | object | Record metadata |
|
||||
| ↳ `source` | string | Source of the DNS record |
|
||||
| `created_on` | string | ISO 8601 timestamp when the record was created |
|
||||
| `modified_on` | string | ISO 8601 timestamp when the record was last modified |
|
||||
|
||||
### `cloudflare_delete_dns_record`
|
||||
|
||||
Deletes a DNS record from a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID containing the DNS record |
|
||||
| `recordId` | string | Yes | The DNS record ID to delete |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Deleted record ID |
|
||||
|
||||
### `cloudflare_list_certificates`
|
||||
|
||||
Lists SSL/TLS certificate packs for a zone.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to list certificates for |
|
||||
| `status` | string | No | Filter certificate packs by status \(e.g., "all", "active", "pending"\) |
|
||||
| `page` | number | No | Page number of paginated results \(default: 1\) |
|
||||
| `per_page` | number | No | Number of certificate packs per page \(default: 20, min: 5, max: 50\) |
|
||||
| `deploy` | string | No | Filter by deployment environment: "staging" or "production" |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `certificates` | array | List of SSL/TLS certificate packs |
|
||||
| ↳ `id` | string | Certificate pack ID |
|
||||
| ↳ `type` | string | Certificate type \(e.g., "universal", "advanced"\) |
|
||||
| ↳ `hosts` | array | Hostnames covered by this certificate pack |
|
||||
| ↳ `primary_certificate` | string | ID of the primary certificate in the pack |
|
||||
| ↳ `status` | string | Certificate pack status \(e.g., "active", "pending"\) |
|
||||
| ↳ `certificates` | array | Individual certificates within the pack |
|
||||
| ↳ `id` | string | Certificate ID |
|
||||
| ↳ `hosts` | array | Hostnames covered by this certificate |
|
||||
| ↳ `issuer` | string | Certificate issuer |
|
||||
| ↳ `signature` | string | Signature algorithm \(e.g., "ECDSAWithSHA256"\) |
|
||||
| ↳ `status` | string | Certificate status |
|
||||
| ↳ `bundle_method` | string | Bundle method \(e.g., "ubiquitous"\) |
|
||||
| ↳ `zone_id` | string | Zone ID the certificate belongs to |
|
||||
| ↳ `uploaded_on` | string | Upload date \(ISO 8601\) |
|
||||
| ↳ `modified_on` | string | Last modified date \(ISO 8601\) |
|
||||
| ↳ `expires_on` | string | Expiration date \(ISO 8601\) |
|
||||
| ↳ `priority` | number | Certificate priority order |
|
||||
| ↳ `geo_restrictions` | object | Geographic restrictions for the certificate |
|
||||
| ↳ `label` | string | Geographic restriction label |
|
||||
| ↳ `cloudflare_branding` | boolean | Whether Cloudflare branding is enabled on the certificate |
|
||||
| ↳ `validation_method` | string | Validation method \(e.g., "txt", "http", "cname"\) |
|
||||
| ↳ `validity_days` | number | Validity period in days |
|
||||
| ↳ `certificate_authority` | string | Certificate authority \(e.g., "lets_encrypt", "google"\) |
|
||||
| ↳ `validation_errors` | array | Validation issues for the certificate pack |
|
||||
| ↳ `message` | string | Validation error message |
|
||||
| ↳ `validation_records` | array | Validation records for the certificate pack |
|
||||
| ↳ `cname` | string | CNAME record name |
|
||||
| ↳ `cname_target` | string | CNAME record target |
|
||||
| ↳ `emails` | array | Email addresses for validation |
|
||||
| ↳ `http_body` | string | HTTP validation body content |
|
||||
| ↳ `http_url` | string | HTTP validation URL |
|
||||
| ↳ `status` | string | Validation record status |
|
||||
| ↳ `txt_name` | string | TXT record name |
|
||||
| ↳ `txt_value` | string | TXT record value |
|
||||
| ↳ `dcv_delegation_records` | array | Domain control validation delegation records |
|
||||
| ↳ `cname` | string | CNAME record name |
|
||||
| ↳ `cname_target` | string | CNAME record target |
|
||||
| ↳ `emails` | array | Email addresses for validation |
|
||||
| ↳ `http_body` | string | HTTP validation body content |
|
||||
| ↳ `http_url` | string | HTTP validation URL |
|
||||
| ↳ `status` | string | Delegation record status |
|
||||
| ↳ `txt_name` | string | TXT record name |
|
||||
| ↳ `txt_value` | string | TXT record value |
|
||||
| `total_count` | number | Total number of certificate packs |
|
||||
|
||||
### `cloudflare_get_zone_settings`
|
||||
|
||||
Gets all settings for a zone including SSL mode, minification, caching level, and security settings.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to get settings for |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `settings` | array | List of zone settings |
|
||||
| ↳ `id` | string | Setting identifier \(e.g., ssl, minify, cache_level, security_level, always_use_https\) |
|
||||
| ↳ `value` | string | Setting value as a string. Simple values returned as-is \(e.g., "full", "on"\). Complex values are JSON-stringified \(e.g., \ |
|
||||
| ↳ `editable` | boolean | Whether the setting can be modified for the current zone plan |
|
||||
| ↳ `modified_on` | string | ISO 8601 timestamp when the setting was last modified |
|
||||
| ↳ `time_remaining` | number | Seconds remaining until the setting can be modified again \(only present for rate-limited settings\) |
|
||||
|
||||
### `cloudflare_update_zone_setting`
|
||||
|
||||
Updates a specific zone setting such as SSL mode, security level, cache level, minification, or other configuration.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to update settings for |
|
||||
| `settingId` | string | Yes | Setting to update \(e.g., "ssl", "security_level", "cache_level", "minify", "always_use_https", "browser_cache_ttl", "http3", "min_tls_version", "ciphers"\) |
|
||||
| `value` | string | Yes | New value for the setting as a string or JSON string for complex values \(e.g., "full" for SSL, "medium" for security_level, "aggressive" for cache_level, \'\{"css":"on","html":"on","js":"on"\}\' for minify, \'\["ECDHE-RSA-AES128-GCM-SHA256"\]\' for ciphers\) |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Setting identifier \(e.g., ssl, minify, cache_level\) |
|
||||
| `value` | string | Updated setting value as a string. Simple values returned as-is \(e.g., "full", "on"\). Complex values are JSON-stringified. |
|
||||
| `editable` | boolean | Whether the setting can be modified for the current zone plan |
|
||||
| `modified_on` | string | ISO 8601 timestamp when the setting was last modified |
|
||||
| `time_remaining` | number | Seconds remaining until the setting can be modified again \(only present for rate-limited settings\) |
|
||||
|
||||
### `cloudflare_dns_analytics`
|
||||
|
||||
Gets DNS analytics report for a zone including query counts and trends.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to get DNS analytics for |
|
||||
| `since` | string | No | Start date for analytics \(ISO 8601, e.g., "2024-01-01T00:00:00Z"\) or relative \(e.g., "-6h"\) |
|
||||
| `until` | string | No | End date for analytics \(ISO 8601, e.g., "2024-01-31T23:59:59Z"\) or relative \(e.g., "now"\) |
|
||||
| `metrics` | string | Yes | Comma-separated metrics to retrieve \(e.g., "queryCount,uncachedCount,staleCount,responseTimeAvg,responseTimeMedian,responseTime90th,responseTime99th"\) |
|
||||
| `dimensions` | string | No | Comma-separated dimensions to group by \(e.g., "queryName,queryType,responseCode,responseCached,coloName,origin,dayOfWeek,tcp,ipVersion,querySizeBucket,responseSizeBucket"\) |
|
||||
| `filters` | string | No | Filters to apply to the data \(e.g., "queryType==A"\) |
|
||||
| `sort` | string | No | Sort order for the result set. Fields must be included in metrics or dimensions \(e.g., "+queryCount" or "-responseTimeAvg"\) |
|
||||
| `limit` | number | No | Maximum number of results to return |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `totals` | object | Aggregate DNS analytics totals for the entire queried period |
|
||||
| ↳ `queryCount` | number | Total number of DNS queries |
|
||||
| ↳ `uncachedCount` | number | Number of uncached DNS queries |
|
||||
| ↳ `staleCount` | number | Number of stale DNS queries |
|
||||
| ↳ `responseTimeAvg` | number | Average response time in milliseconds |
|
||||
| ↳ `responseTimeMedian` | number | Median response time in milliseconds |
|
||||
| ↳ `responseTime90th` | number | 90th percentile response time in milliseconds |
|
||||
| ↳ `responseTime99th` | number | 99th percentile response time in milliseconds |
|
||||
| `min` | object | Minimum values across the analytics period |
|
||||
| ↳ `queryCount` | number | Minimum number of DNS queries |
|
||||
| ↳ `uncachedCount` | number | Minimum number of uncached DNS queries |
|
||||
| ↳ `staleCount` | number | Minimum number of stale DNS queries |
|
||||
| ↳ `responseTimeAvg` | number | Minimum average response time in milliseconds |
|
||||
| ↳ `responseTimeMedian` | number | Minimum median response time in milliseconds |
|
||||
| ↳ `responseTime90th` | number | Minimum 90th percentile response time in milliseconds |
|
||||
| ↳ `responseTime99th` | number | Minimum 99th percentile response time in milliseconds |
|
||||
| `max` | object | Maximum values across the analytics period |
|
||||
| ↳ `queryCount` | number | Maximum number of DNS queries |
|
||||
| ↳ `uncachedCount` | number | Maximum number of uncached DNS queries |
|
||||
| ↳ `staleCount` | number | Maximum number of stale DNS queries |
|
||||
| ↳ `responseTimeAvg` | number | Maximum average response time in milliseconds |
|
||||
| ↳ `responseTimeMedian` | number | Maximum median response time in milliseconds |
|
||||
| ↳ `responseTime90th` | number | Maximum 90th percentile response time in milliseconds |
|
||||
| ↳ `responseTime99th` | number | Maximum 99th percentile response time in milliseconds |
|
||||
| `data` | array | Raw analytics data rows returned by the Cloudflare DNS analytics report |
|
||||
| ↳ `dimensions` | array | Dimension values for this data row, parallel to the requested dimensions list |
|
||||
| ↳ `metrics` | array | Metric values for this data row, parallel to the requested metrics list |
|
||||
| `data_lag` | number | Processing lag in seconds before analytics data becomes available |
|
||||
| `rows` | number | Total number of rows in the result set |
|
||||
| `query` | object | Echo of the query parameters sent to the API |
|
||||
| ↳ `since` | string | Start date of the analytics query |
|
||||
| ↳ `until` | string | End date of the analytics query |
|
||||
| ↳ `metrics` | array | Metrics requested in the query |
|
||||
| ↳ `dimensions` | array | Dimensions requested in the query |
|
||||
| ↳ `filters` | string | Filters applied to the query |
|
||||
| ↳ `sort` | array | Sort order applied to the query |
|
||||
| ↳ `limit` | number | Maximum number of results requested |
|
||||
|
||||
### `cloudflare_purge_cache`
|
||||
|
||||
Purges cached content for a zone. Can purge everything or specific files/tags/hosts/prefixes.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `zoneId` | string | Yes | The zone ID to purge cache for |
|
||||
| `purge_everything` | boolean | No | Set to true to purge all cached content. Mutually exclusive with files, tags, hosts, and prefixes |
|
||||
| `files` | string | No | Comma-separated list of URLs to purge from cache |
|
||||
| `tags` | string | No | Comma-separated list of cache tags to purge \(Enterprise only\) |
|
||||
| `hosts` | string | No | Comma-separated list of hostnames to purge \(Enterprise only\) |
|
||||
| `prefixes` | string | No | Comma-separated list of URL prefixes to purge \(Enterprise only\) |
|
||||
| `apiKey` | string | Yes | Cloudflare API Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Purge request identifier returned by Cloudflare |
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"circleback",
|
||||
"clay",
|
||||
"clerk",
|
||||
"cloudflare",
|
||||
"confluence",
|
||||
"cursor",
|
||||
"datadog",
|
||||
@@ -69,7 +68,6 @@
|
||||
"mailgun",
|
||||
"mem0",
|
||||
"memory",
|
||||
"microsoft_dataverse",
|
||||
"microsoft_excel",
|
||||
"microsoft_planner",
|
||||
"microsoft_teams",
|
||||
@@ -124,7 +122,6 @@
|
||||
"twilio_sms",
|
||||
"twilio_voice",
|
||||
"typeform",
|
||||
"vercel",
|
||||
"video_generator",
|
||||
"vision",
|
||||
"wealthbox",
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
---
|
||||
title: Microsoft Dataverse
|
||||
description: Manage records in Microsoft Dataverse tables
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="microsoft_dataverse"
|
||||
color="#E0E0E0"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Microsoft Dataverse](https://learn.microsoft.com/en-us/power-apps/maker/data-platform/data-platform-intro) is a powerful cloud data platform for securely storing, managing, and interacting with structured business data. The Microsoft Dataverse integration enables you to programmatically create, read, update, delete, and link records in Dataverse tables as part of your workflow and automation needs.
|
||||
|
||||
With Microsoft Dataverse integration, you can:
|
||||
|
||||
- **List and query records:** Access lists of records or query with advanced filters to find the data you need from any Dataverse table.
|
||||
- **Create and update records:** Add new records or update existing ones in any table for use across Power Platform, Dynamics 365, and custom apps.
|
||||
- **Delete and manage records:** Remove records as part of data lifecycle management directly from your automation flows.
|
||||
- **Associate and disassociate records:** Link related items together or remove associations using entity relationships and navigation properties—essential for reflecting complex business processes.
|
||||
- **Work with any Dataverse environment:** Connect to your organization’s environments, including production, sandbox, or Dynamics 365 tenants, for maximum flexibility.
|
||||
- **Integrate with Power Platform and Dynamics 365:** Automate tasks ranging from sales and marketing data updates to custom app workflows—all powered by Dataverse's security and governance.
|
||||
|
||||
The Dataverse integration empowers solution builders and business users to automate business processes, maintain accurate and up-to-date information, create system integrations, trigger actions, and drive insights—all with robust security and governance.
|
||||
|
||||
Connect Microsoft Dataverse to your automations to unlock sophisticated data management, orchestration, and business logic across your apps, teams, and cloud services.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `microsoft_dataverse_associate`
|
||||
|
||||
Associate two records in Microsoft Dataverse via a navigation property. Creates a relationship between a source record and a target record. Supports both collection-valued (POST) and single-valued (PUT) navigation properties.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Source entity set name \(e.g., accounts\) |
|
||||
| `recordId` | string | Yes | Source record GUID |
|
||||
| `navigationProperty` | string | Yes | Navigation property name \(e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued\) |
|
||||
| `targetEntitySetName` | string | Yes | Target entity set name \(e.g., contacts\) |
|
||||
| `targetRecordId` | string | Yes | Target record GUID to associate |
|
||||
| `navigationType` | string | No | Type of navigation property: "collection" \(default, uses POST\) or "single" \(uses PUT for lookup fields\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the association was created successfully |
|
||||
| `entitySetName` | string | Source entity set name used in the association |
|
||||
| `recordId` | string | Source record GUID that was associated |
|
||||
| `navigationProperty` | string | Navigation property used for the association |
|
||||
| `targetEntitySetName` | string | Target entity set name used in the association |
|
||||
| `targetRecordId` | string | Target record GUID that was associated |
|
||||
|
||||
### `microsoft_dataverse_create_multiple`
|
||||
|
||||
Create multiple records of the same table type in a single request. Each record in the Targets array must include an @odata.type annotation. Recommended batch size: 100-1000 records for standard tables.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `entityLogicalName` | string | Yes | Table logical name for @odata.type annotation \(e.g., account, contact\). Used to set Microsoft.Dynamics.CRM.\{entityLogicalName\} on each record. |
|
||||
| `records` | object | Yes | Array of record objects to create. Each record should contain column logical names as keys. The @odata.type annotation is added automatically. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ids` | array | Array of GUIDs for the created records |
|
||||
| `count` | number | Number of records created |
|
||||
| `success` | boolean | Whether all records were created successfully |
|
||||
|
||||
### `microsoft_dataverse_create_record`
|
||||
|
||||
Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `data` | object | Yes | Record data as a JSON object with column names as keys |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the created record |
|
||||
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
|
||||
| `success` | boolean | Whether the record was created successfully |
|
||||
|
||||
### `microsoft_dataverse_delete_record`
|
||||
|
||||
Delete a record from a Microsoft Dataverse table by its ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the deleted record |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_disassociate`
|
||||
|
||||
Remove an association between two records in Microsoft Dataverse. For collection-valued navigation properties, provide the target record ID. For single-valued navigation properties, only the navigation property name is needed.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Source entity set name \(e.g., accounts\) |
|
||||
| `recordId` | string | Yes | Source record GUID |
|
||||
| `navigationProperty` | string | Yes | Navigation property name \(e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued\) |
|
||||
| `targetRecordId` | string | No | Target record GUID \(required for collection-valued navigation properties, omit for single-valued\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the disassociation was completed successfully |
|
||||
| `entitySetName` | string | Source entity set name used in the disassociation |
|
||||
| `recordId` | string | Source record GUID that was disassociated |
|
||||
| `navigationProperty` | string | Navigation property used for the disassociation |
|
||||
| `targetRecordId` | string | Target record GUID that was disassociated |
|
||||
|
||||
### `microsoft_dataverse_download_file`
|
||||
|
||||
Download a file from a file or image column on a Dataverse record. Returns the file content as a base64-encoded string along with file metadata from response headers.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | Record GUID to download the file from |
|
||||
| `fileColumn` | string | Yes | File or image column logical name \(e.g., entityimage, cr_document\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `fileContent` | string | Base64-encoded file content |
|
||||
| `fileName` | string | Name of the downloaded file |
|
||||
| `fileSize` | number | File size in bytes |
|
||||
| `mimeType` | string | MIME type of the file |
|
||||
| `success` | boolean | Whether the file was downloaded successfully |
|
||||
|
||||
### `microsoft_dataverse_execute_action`
|
||||
|
||||
Execute a bound or unbound Dataverse action. Actions perform operations with side effects (e.g., Merge, GrantAccess, SendEmail, QualifyLead). For bound actions, provide the entity set name and record ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `actionName` | string | Yes | Action name \(e.g., Merge, GrantAccess, SendEmail\). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound actions. |
|
||||
| `entitySetName` | string | No | Entity set name for bound actions \(e.g., accounts\). Leave empty for unbound actions. |
|
||||
| `recordId` | string | No | Record GUID for bound actions. Leave empty for unbound or collection-bound actions. |
|
||||
| `parameters` | object | No | Action parameters as a JSON object. For entity references, include @odata.type annotation \(e.g., \{"Target": \{"@odata.type": "Microsoft.Dynamics.CRM.account", "accountid": "..."\}\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `result` | object | Action response data. Structure varies by action. Null for actions that return 204 No Content. |
|
||||
| `success` | boolean | Whether the action executed successfully |
|
||||
|
||||
### `microsoft_dataverse_execute_function`
|
||||
|
||||
Execute a bound or unbound Dataverse function. Functions are read-only operations (e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount, InitializeFrom). For bound functions, provide the entity set name and record ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `functionName` | string | Yes | Function name \(e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount\). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound functions. |
|
||||
| `entitySetName` | string | No | Entity set name for bound functions \(e.g., systemusers\). Leave empty for unbound functions. |
|
||||
| `recordId` | string | No | Record GUID for bound functions. Leave empty for unbound functions. |
|
||||
| `parameters` | string | No | Function parameters as a comma-separated list of name=value pairs for the URL \(e.g., "LocalizedStandardName=\'Pacific Standard Time\ |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `result` | object | Function response data. Structure varies by function. |
|
||||
| `success` | boolean | Whether the function executed successfully |
|
||||
|
||||
### `microsoft_dataverse_fetchxml_query`
|
||||
|
||||
Execute a FetchXML query against a Microsoft Dataverse table. FetchXML supports aggregation, grouping, linked-entity joins, and complex filtering beyond OData capabilities.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `fetchXml` | string | Yes | FetchXML query string. Must include <fetch> root element and <entity> child element matching the table logical name. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array of Dataverse records. Each record has dynamic columns based on the table schema. |
|
||||
| `count` | number | Number of records returned in the current page |
|
||||
| `fetchXmlPagingCookie` | string | Paging cookie for retrieving the next page of results |
|
||||
| `moreRecords` | boolean | Whether more records are available beyond the current page |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_get_record`
|
||||
|
||||
Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to retrieve |
|
||||
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
|
||||
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
|
||||
| `recordId` | string | The record primary key ID \(auto-detected from response\) |
|
||||
| `success` | boolean | Whether the record was retrieved successfully |
|
||||
|
||||
### `microsoft_dataverse_list_records`
|
||||
|
||||
Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `select` | string | No | Comma-separated list of columns to return \(OData $select\) |
|
||||
| `filter` | string | No | OData $filter expression \(e.g., statecode eq 0\) |
|
||||
| `orderBy` | string | No | OData $orderby expression \(e.g., name asc, createdon desc\) |
|
||||
| `top` | number | No | Maximum number of records to return \(OData $top\) |
|
||||
| `expand` | string | No | Navigation properties to expand \(OData $expand\) |
|
||||
| `count` | string | No | Set to "true" to include total record count in response \(OData $count\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array of Dataverse records. Each record has dynamic columns based on the table schema. |
|
||||
| `count` | number | Number of records returned in the current page |
|
||||
| `totalCount` | number | Total number of matching records server-side \(requires $count=true\) |
|
||||
| `nextLink` | string | URL for the next page of results |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_search`
|
||||
|
||||
Perform a full-text relevance search across Microsoft Dataverse tables. Requires Dataverse Search to be enabled on the environment. Supports simple and Lucene query syntax.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `searchTerm` | string | Yes | Search text \(1-100 chars\). Supports simple syntax: + \(AND\), \| \(OR\), - \(NOT\), * \(wildcard\), "exact phrase" |
|
||||
| `entities` | string | No | JSON array of search entity configs. Each object: \{"Name":"account","SelectColumns":\["name"\],"SearchColumns":\["name"\],"Filter":"statecode eq 0"\} |
|
||||
| `filter` | string | No | Global OData filter applied across all entities \(e.g., "createdon gt 2024-01-01"\) |
|
||||
| `facets` | string | No | JSON array of facet specifications \(e.g., \["entityname,count:100","ownerid,count:100"\]\) |
|
||||
| `top` | number | No | Maximum number of results \(default: 50, max: 100\) |
|
||||
| `skip` | number | No | Number of results to skip for pagination |
|
||||
| `orderBy` | string | No | JSON array of sort expressions \(e.g., \["createdon desc"\]\) |
|
||||
| `searchMode` | string | No | Search mode: "any" \(default, match any term\) or "all" \(match all terms\) |
|
||||
| `searchType` | string | No | Query type: "simple" \(default\) or "lucene" \(enables regex, fuzzy, proximity, boosting\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Array of search result objects |
|
||||
| ↳ `Id` | string | Record GUID |
|
||||
| ↳ `EntityName` | string | Table logical name \(e.g., account, contact\) |
|
||||
| ↳ `ObjectTypeCode` | number | Entity type code |
|
||||
| ↳ `Attributes` | object | Record attributes matching the search. Keys are column logical names. |
|
||||
| ↳ `Highlights` | object | Highlighted search matches. Keys are column names, values are arrays of strings with \{crmhit\}/\{/crmhit\} markers. |
|
||||
| ↳ `Score` | number | Relevance score for this result |
|
||||
| `totalCount` | number | Total number of matching records across all tables |
|
||||
| `count` | number | Number of results returned in this page |
|
||||
| `facets` | object | Facet results when facets were requested. Keys are facet names, values are arrays of facet value objects with count and value properties. |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_update_multiple`
|
||||
|
||||
Update multiple records of the same table type in a single request. Each record must include its primary key. Only include columns that need to be changed. Recommended batch size: 100-1000 records.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `entityLogicalName` | string | Yes | Table logical name for @odata.type annotation \(e.g., account, contact\). Used to set Microsoft.Dynamics.CRM.\{entityLogicalName\} on each record. |
|
||||
| `records` | object | Yes | Array of record objects to update. Each record must include its primary key \(e.g., accountid\) and only the columns being changed. The @odata.type annotation is added automatically. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether all records were updated successfully |
|
||||
|
||||
### `microsoft_dataverse_update_record`
|
||||
|
||||
Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to update |
|
||||
| `data` | object | Yes | Record data to update as a JSON object with column names as keys |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the updated record |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_upload_file`
|
||||
|
||||
Upload a file to a file or image column on a Dataverse record. Supports single-request upload for files up to 128 MB. The file content must be provided as a base64-encoded string.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | Record GUID to upload the file to |
|
||||
| `fileColumn` | string | Yes | File or image column logical name \(e.g., entityimage, cr_document\) |
|
||||
| `fileName` | string | Yes | Name of the file being uploaded \(e.g., document.pdf\) |
|
||||
| `file` | file | No | File to upload \(UserFile object\) |
|
||||
| `fileContent` | string | No | Base64-encoded file content \(legacy\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | Record GUID the file was uploaded to |
|
||||
| `fileColumn` | string | File column the file was uploaded to |
|
||||
| `fileName` | string | Name of the uploaded file |
|
||||
| `success` | boolean | Whether the file was uploaded successfully |
|
||||
|
||||
### `microsoft_dataverse_upsert_record`
|
||||
|
||||
Create or update a record in a Microsoft Dataverse table. If a record with the given ID exists, it is updated; otherwise, a new record is created.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
| `entitySetName` | string | Yes | Entity set name \(plural table name, e.g., accounts, contacts\) |
|
||||
| `recordId` | string | Yes | The unique identifier \(GUID\) of the record to upsert |
|
||||
| `data` | object | Yes | Record data as a JSON object with column names as keys |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recordId` | string | The ID of the upserted record |
|
||||
| `created` | boolean | True if the record was created, false if updated |
|
||||
| `record` | object | Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields. |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `microsoft_dataverse_whoami`
|
||||
|
||||
Retrieve the current authenticated user information from Microsoft Dataverse. Useful for testing connectivity and getting the user ID, business unit ID, and organization ID.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | The authenticated user ID |
|
||||
| `businessUnitId` | string | The business unit ID |
|
||||
| `organizationId` | string | The organization ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@
|
||||
"fumadocs-mdx": "14.1.0",
|
||||
"fumadocs-ui": "16.2.3",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "16.1.6",
|
||||
"next": "16.1.0-canary.21",
|
||||
"next-themes": "^0.4.6",
|
||||
"postgres": "^3.4.5",
|
||||
"react": "19.2.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull, min } from 'drizzle-orm'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
@@ -37,6 +37,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
logger.info(`[${requestId}] Duplicating folder ${sourceFolderId} for user ${session.user.id}`)
|
||||
|
||||
// Verify the source folder exists
|
||||
const sourceFolder = await db
|
||||
.select()
|
||||
.from(workflowFolder)
|
||||
@@ -47,6 +48,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
throw new Error('Source folder not found')
|
||||
}
|
||||
|
||||
// Check if user has permission to access the source folder
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
@@ -59,51 +61,26 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
|
||||
const targetWorkspaceId = workspaceId || sourceFolder.workspaceId
|
||||
|
||||
// Step 1: Duplicate folder structure
|
||||
const { newFolderId, folderMapping } = await db.transaction(async (tx) => {
|
||||
const newFolderId = crypto.randomUUID()
|
||||
const now = new Date()
|
||||
const targetParentId = parentId ?? sourceFolder.parentId
|
||||
|
||||
const folderParentCondition = targetParentId
|
||||
? eq(workflowFolder.parentId, targetParentId)
|
||||
: isNull(workflowFolder.parentId)
|
||||
const workflowParentCondition = targetParentId
|
||||
? eq(workflow.folderId, targetParentId)
|
||||
: isNull(workflow.folderId)
|
||||
|
||||
const [[folderResult], [workflowResult]] = await Promise.all([
|
||||
tx
|
||||
.select({ minSortOrder: min(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
|
||||
tx
|
||||
.select({ minSortOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition)),
|
||||
])
|
||||
|
||||
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
|
||||
number | null
|
||||
>((currentMin, candidate) => {
|
||||
if (candidate == null) return currentMin
|
||||
if (currentMin == null) return candidate
|
||||
return Math.min(currentMin, candidate)
|
||||
}, null)
|
||||
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
|
||||
|
||||
// Create the new root folder
|
||||
await tx.insert(workflowFolder).values({
|
||||
id: newFolderId,
|
||||
userId: session.user.id,
|
||||
workspaceId: targetWorkspaceId,
|
||||
name,
|
||||
color: color || sourceFolder.color,
|
||||
parentId: targetParentId,
|
||||
sortOrder,
|
||||
parentId: parentId || sourceFolder.parentId,
|
||||
sortOrder: sourceFolder.sortOrder,
|
||||
isExpanded: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
// Recursively duplicate child folders
|
||||
const folderMapping = new Map<string, string>([[sourceFolderId, newFolderId]])
|
||||
await duplicateFolderStructure(
|
||||
tx,
|
||||
@@ -119,6 +96,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
return { newFolderId, folderMapping }
|
||||
})
|
||||
|
||||
// Step 2: Duplicate workflows
|
||||
const workflowStats = await duplicateWorkflowsInFolderTree(
|
||||
sourceFolder.workspaceId,
|
||||
targetWorkspaceId,
|
||||
@@ -195,6 +173,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to recursively duplicate folder structure
|
||||
async function duplicateFolderStructure(
|
||||
tx: any,
|
||||
sourceFolderId: string,
|
||||
@@ -205,6 +184,7 @@ async function duplicateFolderStructure(
|
||||
timestamp: Date,
|
||||
folderMapping: Map<string, string>
|
||||
): Promise<void> {
|
||||
// Get all child folders
|
||||
const childFolders = await tx
|
||||
.select()
|
||||
.from(workflowFolder)
|
||||
@@ -215,6 +195,7 @@ async function duplicateFolderStructure(
|
||||
)
|
||||
)
|
||||
|
||||
// Create each child folder and recurse
|
||||
for (const childFolder of childFolders) {
|
||||
const newChildFolderId = crypto.randomUUID()
|
||||
folderMapping.set(childFolder.id, newChildFolderId)
|
||||
@@ -232,6 +213,7 @@ async function duplicateFolderStructure(
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
|
||||
// Recurse for this child's children
|
||||
await duplicateFolderStructure(
|
||||
tx,
|
||||
childFolder.id,
|
||||
@@ -245,6 +227,7 @@ async function duplicateFolderStructure(
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to duplicate all workflows in a folder tree
|
||||
async function duplicateWorkflowsInFolderTree(
|
||||
sourceWorkspaceId: string,
|
||||
targetWorkspaceId: string,
|
||||
@@ -254,7 +237,9 @@ async function duplicateWorkflowsInFolderTree(
|
||||
): Promise<{ total: number; succeeded: number; failed: number }> {
|
||||
const stats = { total: 0, succeeded: 0, failed: 0 }
|
||||
|
||||
// Process each folder in the mapping
|
||||
for (const [oldFolderId, newFolderId] of folderMapping.entries()) {
|
||||
// Get workflows in this folder
|
||||
const workflowsInFolder = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
@@ -262,6 +247,7 @@ async function duplicateWorkflowsInFolderTree(
|
||||
|
||||
stats.total += workflowsInFolder.length
|
||||
|
||||
// Duplicate each workflow
|
||||
for (const sourceWorkflow of workflowsInFolder) {
|
||||
try {
|
||||
await duplicateWorkflow({
|
||||
|
||||
@@ -10,14 +10,9 @@ import {
|
||||
mockConsoleLogger,
|
||||
setupCommonApiMocks,
|
||||
} from '@sim/testing'
|
||||
import { drizzleOrmMock } from '@sim/testing/mocks'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/audit/log', () => auditMock)
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
...drizzleOrmMock,
|
||||
min: vi.fn((field) => ({ type: 'min', field })),
|
||||
}))
|
||||
|
||||
interface CapturedFolderValues {
|
||||
name?: string
|
||||
@@ -29,35 +24,29 @@ interface CapturedFolderValues {
|
||||
}
|
||||
|
||||
function createMockTransaction(mockData: {
|
||||
selectResults?: Array<Array<{ [key: string]: unknown }>>
|
||||
selectData?: Array<{ id: string; [key: string]: unknown }>
|
||||
insertResult?: Array<{ id: string; [key: string]: unknown }>
|
||||
onInsertValues?: (values: CapturedFolderValues) => void
|
||||
}) {
|
||||
const { selectResults = [[], []], insertResult = [], onInsertValues } = mockData
|
||||
return async (callback: (tx: unknown) => Promise<unknown>) => {
|
||||
const where = vi.fn()
|
||||
for (const result of selectResults) {
|
||||
where.mockReturnValueOnce(result)
|
||||
}
|
||||
where.mockReturnValue([])
|
||||
|
||||
const { selectData = [], insertResult = [] } = mockData
|
||||
return vi.fn().mockImplementation(async (callback: (tx: unknown) => Promise<unknown>) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where,
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue(selectData),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values: CapturedFolderValues) => {
|
||||
onInsertValues?.(values)
|
||||
return {
|
||||
returning: vi.fn().mockReturnValue(insertResult),
|
||||
}
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue(insertResult),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Folders API Route', () => {
|
||||
@@ -268,12 +257,25 @@ describe('Folders API Route', () => {
|
||||
it('should create a new folder successfully', async () => {
|
||||
mockAuthenticatedUser()
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
})
|
||||
)
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]), // No existing folders
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'New Test Folder',
|
||||
@@ -283,11 +285,12 @@ describe('Folders API Route', () => {
|
||||
|
||||
const { POST } = await import('@/app/api/folders/route')
|
||||
const response = await POST(req)
|
||||
const responseBody = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(responseBody).toHaveProperty('folder')
|
||||
expect(responseBody.folder).toMatchObject({
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toHaveProperty('folder')
|
||||
expect(data.folder).toMatchObject({
|
||||
id: 'folder-1',
|
||||
name: 'Test Folder 1',
|
||||
workspaceId: 'workspace-123',
|
||||
@@ -296,17 +299,26 @@ describe('Folders API Route', () => {
|
||||
|
||||
it('should create folder with correct sort order', async () => {
|
||||
mockAuthenticatedUser()
|
||||
let capturedValues: CapturedFolderValues | null = null
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[{ minSortOrder: 5 }], [{ minSortOrder: 2 }]],
|
||||
insertResult: [{ ...mockFolders[0], sortOrder: 1 }],
|
||||
onInsertValues: (values) => {
|
||||
capturedValues = values
|
||||
},
|
||||
})
|
||||
)
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([{ sortOrder: 5 }]), // Existing folder with sort order 5
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([{ ...mockFolders[0], sortOrder: 6 }]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'New Test Folder',
|
||||
@@ -320,10 +332,8 @@ describe('Folders API Route', () => {
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.folder).toMatchObject({
|
||||
sortOrder: 1,
|
||||
sortOrder: 6,
|
||||
})
|
||||
expect(capturedValues).not.toBeNull()
|
||||
expect(capturedValues!.sortOrder).toBe(1)
|
||||
})
|
||||
|
||||
it('should create subfolder with parent reference', async () => {
|
||||
@@ -331,7 +341,7 @@ describe('Folders API Route', () => {
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
selectData: [], // No existing folders
|
||||
insertResult: [{ ...mockFolders[1] }],
|
||||
})
|
||||
)
|
||||
@@ -392,12 +402,25 @@ describe('Folders API Route', () => {
|
||||
mockAuthenticatedUser()
|
||||
mockGetUserEntityPermissions.mockResolvedValue('write') // Write permissions
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
})
|
||||
)
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]), // No existing folders
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'Test Folder',
|
||||
@@ -417,12 +440,25 @@ describe('Folders API Route', () => {
|
||||
mockAuthenticatedUser()
|
||||
mockGetUserEntityPermissions.mockResolvedValue('admin') // Admin permissions
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
})
|
||||
)
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]), // No existing folders
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'Test Folder',
|
||||
@@ -491,15 +527,28 @@ describe('Folders API Route', () => {
|
||||
|
||||
let capturedValues: CapturedFolderValues | null = null
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
onInsertValues: (values) => {
|
||||
capturedValues = values
|
||||
},
|
||||
})
|
||||
)
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values) => {
|
||||
capturedValues = values
|
||||
return {
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: ' Test Folder With Spaces ',
|
||||
@@ -518,15 +567,28 @@ describe('Folders API Route', () => {
|
||||
|
||||
let capturedValues: CapturedFolderValues | null = null
|
||||
|
||||
mockTransaction.mockImplementationOnce(
|
||||
createMockTransaction({
|
||||
selectResults: [[], []],
|
||||
insertResult: [mockFolders[0]],
|
||||
onInsertValues: (values) => {
|
||||
capturedValues = values
|
||||
},
|
||||
})
|
||||
)
|
||||
mockTransaction.mockImplementationOnce(async (callback: any) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
orderBy: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values) => {
|
||||
capturedValues = values
|
||||
return {
|
||||
returning: vi.fn().mockReturnValue([mockFolders[0]]),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'Test Folder',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, eq, isNull, min } from 'drizzle-orm'
|
||||
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -87,33 +87,19 @@ export async function POST(request: NextRequest) {
|
||||
if (providedSortOrder !== undefined) {
|
||||
sortOrder = providedSortOrder
|
||||
} else {
|
||||
const folderParentCondition = parentId
|
||||
? eq(workflowFolder.parentId, parentId)
|
||||
: isNull(workflowFolder.parentId)
|
||||
const workflowParentCondition = parentId
|
||||
? eq(workflow.folderId, parentId)
|
||||
: isNull(workflow.folderId)
|
||||
const existingFolders = await tx
|
||||
.select({ sortOrder: workflowFolder.sortOrder })
|
||||
.from(workflowFolder)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowFolder.workspaceId, workspaceId),
|
||||
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
|
||||
)
|
||||
)
|
||||
.orderBy(desc(workflowFolder.sortOrder))
|
||||
.limit(1)
|
||||
|
||||
const [[folderResult], [workflowResult]] = await Promise.all([
|
||||
tx
|
||||
.select({ minSortOrder: min(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
|
||||
tx
|
||||
.select({ minSortOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, workspaceId), workflowParentCondition)),
|
||||
])
|
||||
|
||||
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
|
||||
number | null
|
||||
>((currentMin, candidate) => {
|
||||
if (candidate == null) return currentMin
|
||||
if (currentMin == null) return candidate
|
||||
return Math.min(currentMin, candidate)
|
||||
}, null)
|
||||
|
||||
sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
|
||||
sortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
|
||||
}
|
||||
|
||||
const [folder] = await tx
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('DataverseUploadFileAPI')
|
||||
|
||||
const DataverseUploadFileSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
environmentUrl: z.string().min(1, 'Environment URL is required'),
|
||||
entitySetName: z.string().min(1, 'Entity set name is required'),
|
||||
recordId: z.string().min(1, 'Record ID is required'),
|
||||
fileColumn: z.string().min(1, 'File column is required'),
|
||||
fileName: z.string().min(1, 'File name is required'),
|
||||
file: RawFileInputSchema.optional().nullable(),
|
||||
fileContent: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Dataverse upload attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Dataverse upload request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
}
|
||||
)
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = DataverseUploadFileSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Uploading file to Dataverse`, {
|
||||
entitySetName: validatedData.entitySetName,
|
||||
recordId: validatedData.recordId,
|
||||
fileColumn: validatedData.fileColumn,
|
||||
fileName: validatedData.fileName,
|
||||
hasFile: !!validatedData.file,
|
||||
hasFileContent: !!validatedData.fileContent,
|
||||
})
|
||||
|
||||
let fileBuffer: Buffer
|
||||
|
||||
if (validatedData.file) {
|
||||
const rawFile = validatedData.file
|
||||
logger.info(`[${requestId}] Processing UserFile upload: ${rawFile.name}`)
|
||||
|
||||
let userFile
|
||||
try {
|
||||
userFile = processSingleFileToUserFile(rawFile, requestId, logger)
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to process file',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
} else if (validatedData.fileContent) {
|
||||
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Either file or fileContent must be provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const baseUrl = validatedData.environmentUrl.replace(/\/$/, '')
|
||||
const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName}(${validatedData.recordId})/${validatedData.fileColumn}`
|
||||
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
'x-ms-file-name': validatedData.fileName,
|
||||
},
|
||||
body: new Uint8Array(fileBuffer),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error(`[${requestId}] Dataverse upload file failed`, {
|
||||
errorData,
|
||||
status: response.status,
|
||||
})
|
||||
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] File uploaded to Dataverse successfully`, {
|
||||
entitySetName: validatedData.entitySetName,
|
||||
recordId: validatedData.recordId,
|
||||
fileColumn: validatedData.fileColumn,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
recordId: validatedData.recordId,
|
||||
fileColumn: validatedData.fileColumn,
|
||||
fileName: validatedData.fileName,
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error uploading file to Dataverse:`, error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { auditMock, createMockRequest, mockConsoleLogger, setupCommonApiMocks } from '@sim/testing'
|
||||
import { drizzleOrmMock } from '@sim/testing/mocks'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||
const mockGetUserEntityPermissions = vi.fn()
|
||||
const mockDbSelect = vi.fn()
|
||||
const mockDbInsert = vi.fn()
|
||||
const mockWorkflowCreated = vi.fn()
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
...drizzleOrmMock,
|
||||
min: vi.fn((field) => ({ type: 'min', field })),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/audit/log', () => auditMock)
|
||||
|
||||
describe('Workflows API Route - POST ordering', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
|
||||
setupCommonApiMocks()
|
||||
mockConsoleLogger()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue('workflow-new-id'),
|
||||
})
|
||||
|
||||
mockCheckSessionOrInternalAuth.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-123',
|
||||
userName: 'Test User',
|
||||
userEmail: 'test@example.com',
|
||||
})
|
||||
mockGetUserEntityPermissions.mockResolvedValue('write')
|
||||
|
||||
vi.doMock('@sim/db', () => ({
|
||||
db: {
|
||||
select: (...args: unknown[]) => mockDbSelect(...args),
|
||||
insert: (...args: unknown[]) => mockDbInsert(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
|
||||
getUserEntityPermissions: (...args: unknown[]) => mockGetUserEntityPermissions(...args),
|
||||
workspaceExists: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.doMock('@/app/api/workflows/utils', () => ({
|
||||
verifyWorkspaceMembership: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/telemetry', () => ({
|
||||
PlatformEvents: {
|
||||
workflowCreated: (...args: unknown[]) => mockWorkflowCreated(...args),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('uses top insertion against mixed siblings (folders + workflows)', async () => {
|
||||
const minResultsQueue: Array<Array<{ minOrder: number }>> = [
|
||||
[{ minOrder: 5 }],
|
||||
[{ minOrder: 2 }],
|
||||
]
|
||||
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockImplementation(() => Promise.resolve(minResultsQueue.shift() ?? [])),
|
||||
}),
|
||||
}))
|
||||
|
||||
let insertedValues: Record<string, unknown> | null = null
|
||||
mockDbInsert.mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values: Record<string, unknown>) => {
|
||||
insertedValues = values
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'New Workflow',
|
||||
description: 'desc',
|
||||
color: '#3972F6',
|
||||
workspaceId: 'workspace-123',
|
||||
folderId: null,
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/workflows/route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.sortOrder).toBe(1)
|
||||
expect(insertedValues).not.toBeNull()
|
||||
expect(insertedValues?.sortOrder).toBe(1)
|
||||
})
|
||||
|
||||
it('defaults to sortOrder 0 when there are no siblings', async () => {
|
||||
const minResultsQueue: Array<Array<{ minOrder: number }>> = [[], []]
|
||||
|
||||
mockDbSelect.mockImplementation(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockImplementation(() => Promise.resolve(minResultsQueue.shift() ?? [])),
|
||||
}),
|
||||
}))
|
||||
|
||||
let insertedValues: Record<string, unknown> | null = null
|
||||
mockDbInsert.mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values: Record<string, unknown>) => {
|
||||
insertedValues = values
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
name: 'New Workflow',
|
||||
description: 'desc',
|
||||
color: '#3972F6',
|
||||
workspaceId: 'workspace-123',
|
||||
folderId: null,
|
||||
})
|
||||
|
||||
const { POST } = await import('@/app/api/workflows/route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.sortOrder).toBe(0)
|
||||
expect(insertedValues?.sortOrder).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { permissions, workflow } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, eq, inArray, isNull, min } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
@@ -162,33 +162,12 @@ export async function POST(req: NextRequest) {
|
||||
if (providedSortOrder !== undefined) {
|
||||
sortOrder = providedSortOrder
|
||||
} else {
|
||||
const workflowParentCondition = folderId
|
||||
? eq(workflow.folderId, folderId)
|
||||
: isNull(workflow.folderId)
|
||||
const folderParentCondition = folderId
|
||||
? eq(workflowFolder.parentId, folderId)
|
||||
: isNull(workflowFolder.parentId)
|
||||
|
||||
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
|
||||
db
|
||||
.select({ minOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, workspaceId), workflowParentCondition)),
|
||||
db
|
||||
.select({ minOrder: min(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
|
||||
])
|
||||
|
||||
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
|
||||
number | null
|
||||
>((currentMin, candidate) => {
|
||||
if (candidate == null) return currentMin
|
||||
if (currentMin == null) return candidate
|
||||
return Math.min(currentMin, candidate)
|
||||
}, null)
|
||||
|
||||
sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
|
||||
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
|
||||
const [minResult] = await db
|
||||
.select({ minOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, workspaceId), folderCondition))
|
||||
sortOrder = (minResult?.minOrder ?? 1) - 1
|
||||
}
|
||||
|
||||
await db.insert(workflow).values({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,597 +0,0 @@
|
||||
import { MicrosoftDataverseIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { DataverseResponse } from '@/tools/microsoft_dataverse/types'
|
||||
|
||||
export const MicrosoftDataverseBlock: BlockConfig<DataverseResponse> = {
|
||||
type: 'microsoft_dataverse',
|
||||
name: 'Microsoft Dataverse',
|
||||
description: 'Manage records in Microsoft Dataverse tables',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments.',
|
||||
docsLink: 'https://docs.sim.ai/tools/microsoft_dataverse',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: MicrosoftDataverseIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'List Records', id: 'list_records' },
|
||||
{ label: 'Get Record', id: 'get_record' },
|
||||
{ label: 'Create Record', id: 'create_record' },
|
||||
{ label: 'Update Record', id: 'update_record' },
|
||||
{ label: 'Upsert Record', id: 'upsert_record' },
|
||||
{ label: 'Delete Record', id: 'delete_record' },
|
||||
{ label: 'Create Multiple', id: 'create_multiple' },
|
||||
{ label: 'Update Multiple', id: 'update_multiple' },
|
||||
{ label: 'FetchXML Query', id: 'fetchxml_query' },
|
||||
{ label: 'Search', id: 'search' },
|
||||
{ label: 'Execute Action', id: 'execute_action' },
|
||||
{ label: 'Execute Function', id: 'execute_function' },
|
||||
{ label: 'Upload File', id: 'upload_file' },
|
||||
{ label: 'Download File', id: 'download_file' },
|
||||
{ label: 'Associate Records', id: 'associate' },
|
||||
{ label: 'Disassociate Records', id: 'disassociate' },
|
||||
{ label: 'WhoAmI', id: 'whoami' },
|
||||
],
|
||||
value: () => 'list_records',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'microsoft-dataverse',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'environmentUrl',
|
||||
title: 'Environment URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://myorg.crm.dynamics.com',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'entitySetName',
|
||||
title: 'Entity Set Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Plural table name (e.g., accounts, contacts)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['whoami', 'search'],
|
||||
not: true,
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['whoami', 'search', 'execute_action', 'execute_function'],
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recordId',
|
||||
title: 'Record ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Record GUID (e.g., 00000000-0000-0000-0000-000000000000)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_record',
|
||||
'update_record',
|
||||
'upsert_record',
|
||||
'delete_record',
|
||||
'associate',
|
||||
'disassociate',
|
||||
'upload_file',
|
||||
'download_file',
|
||||
'execute_action',
|
||||
'execute_function',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'get_record',
|
||||
'update_record',
|
||||
'upsert_record',
|
||||
'delete_record',
|
||||
'associate',
|
||||
'disassociate',
|
||||
'upload_file',
|
||||
'download_file',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
title: 'Record Data',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON object with column values (e.g., {"name": "Contoso", "telephone1": "555-0100"})',
|
||||
condition: { field: 'operation', value: ['create_record', 'update_record', 'upsert_record'] },
|
||||
required: { field: 'operation', value: ['create_record', 'update_record', 'upsert_record'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a Dataverse record JSON object based on the user's description.
|
||||
The JSON should contain column logical names as keys and appropriate values.
|
||||
Common Dataverse column naming conventions:
|
||||
- Text: "name", "description", "emailaddress1", "telephone1"
|
||||
- Lookup: "_primarycontactid_value" (read-only), use "primarycontactid@odata.bind": "/contacts(guid)" for setting
|
||||
- Choice/OptionSet: integer values (e.g., "statecode": 0, "statuscode": 1)
|
||||
- Date: ISO 8601 format (e.g., "createdon": "2024-01-15T00:00:00Z")
|
||||
- Currency: decimal numbers (e.g., "revenue": 1000000.00)
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the record data you want to create or update...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// FetchXML Query
|
||||
{
|
||||
id: 'fetchXml',
|
||||
title: 'FetchXML',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'<fetch top="50"><entity name="account"><attribute name="name"/><filter><condition attribute="statecode" operator="eq" value="0"/></filter></entity></fetch>',
|
||||
condition: { field: 'operation', value: 'fetchxml_query' },
|
||||
required: { field: 'operation', value: 'fetchxml_query' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a FetchXML query for the Microsoft Dataverse Web API based on the user's description.
|
||||
FetchXML structure:
|
||||
- Root: <fetch top="N" aggregate="true|false" distinct="true|false">
|
||||
- Entity: <entity name="logical_name"> (singular table name, e.g., "account")
|
||||
- Attributes: <attribute name="column"/> or <all-attributes/>
|
||||
- Filter: <filter type="and|or"><condition attribute="name" operator="eq" value="val"/></filter>
|
||||
- Order: <order attribute="name" descending="true|false"/>
|
||||
- Link-entity: <link-entity name="contact" from="parentcustomerid" to="accountid" alias="c">
|
||||
- Aggregation: <attribute name="revenue" aggregate="sum" alias="total"/>
|
||||
|
||||
Operators: eq, ne, gt, ge, lt, le, like, not-like, in, not-in, null, not-null, between, not-between, contains, not-contain
|
||||
|
||||
Return ONLY valid FetchXML - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the query you want to run...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Search
|
||||
{
|
||||
id: 'searchTerm',
|
||||
title: 'Search Term',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search text (e.g., Contoso)',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
required: { field: 'operation', value: 'search' },
|
||||
},
|
||||
{
|
||||
id: 'searchEntities',
|
||||
title: 'Search Entities',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON array of entity configs (e.g., [{"Name":"account","SelectColumns":["name"],"SearchColumns":["name"]}])',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'searchMode',
|
||||
title: 'Search Mode',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Any (match any term)', id: 'any' },
|
||||
{ label: 'All (match all terms)', id: 'all' },
|
||||
],
|
||||
value: () => 'any',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'searchType',
|
||||
title: 'Query Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Simple (default)', id: 'simple' },
|
||||
{ label: 'Lucene (regex, fuzzy, proximity)', id: 'lucene' },
|
||||
],
|
||||
value: () => 'simple',
|
||||
condition: { field: 'operation', value: 'search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Execute Action
|
||||
{
|
||||
id: 'actionName',
|
||||
title: 'Action Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., Merge, GrantAccess, SendEmail',
|
||||
condition: { field: 'operation', value: 'execute_action' },
|
||||
required: { field: 'operation', value: 'execute_action' },
|
||||
},
|
||||
// Execute Function
|
||||
{
|
||||
id: 'functionName',
|
||||
title: 'Function Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount',
|
||||
condition: { field: 'operation', value: 'execute_function' },
|
||||
required: { field: 'operation', value: 'execute_function' },
|
||||
},
|
||||
{
|
||||
id: 'functionParameters',
|
||||
title: 'Function Parameters',
|
||||
type: 'short-input',
|
||||
placeholder: "e.g., LocalizedStandardName='Pacific Standard Time',LocaleId=1033",
|
||||
condition: { field: 'operation', value: 'execute_function' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Action/Function parameters (shared JSON body for actions)
|
||||
{
|
||||
id: 'parameters',
|
||||
title: 'Action Parameters',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON object with action parameters (e.g., {"Target": {"@odata.type": "Microsoft.Dynamics.CRM.account", "accountid": "..."}})',
|
||||
condition: { field: 'operation', value: 'execute_action' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON object containing parameters for a Microsoft Dataverse action based on the user's description.
|
||||
For entity references, include @odata.type annotations:
|
||||
- {"Target": {"@odata.type": "Microsoft.Dynamics.CRM.account", "accountid": "guid"}}
|
||||
- {"EntityMoniker": {"@odata.type": "Microsoft.Dynamics.CRM.contact", "contactid": "guid"}}
|
||||
For simple values, just use the parameter name and value.
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the action parameters...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Bulk operations
|
||||
{
|
||||
id: 'entityLogicalName',
|
||||
title: 'Table Logical Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Singular table name (e.g., account, contact)',
|
||||
condition: { field: 'operation', value: ['create_multiple', 'update_multiple'] },
|
||||
required: { field: 'operation', value: ['create_multiple', 'update_multiple'] },
|
||||
},
|
||||
{
|
||||
id: 'records',
|
||||
title: 'Records',
|
||||
type: 'long-input',
|
||||
placeholder: 'JSON array of records (e.g., [{"name": "Contoso"}, {"name": "Fabrikam"}])',
|
||||
condition: { field: 'operation', value: ['create_multiple', 'update_multiple'] },
|
||||
required: { field: 'operation', value: ['create_multiple', 'update_multiple'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON array of Dataverse records based on the user's description.
|
||||
Each record should be an object with column logical names as keys.
|
||||
For UpdateMultiple, each record must include its primary key (e.g., accountid).
|
||||
Common column naming conventions:
|
||||
- Text: "name", "description", "emailaddress1", "telephone1"
|
||||
- Choice/OptionSet: integer values (e.g., "statecode": 0)
|
||||
- Date: ISO 8601 format (e.g., "2024-01-15T00:00:00Z")
|
||||
|
||||
Return ONLY a valid JSON array - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the records you want to create or update...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// File operations
|
||||
{
|
||||
id: 'fileColumn',
|
||||
title: 'File Column',
|
||||
type: 'short-input',
|
||||
placeholder: 'File column logical name (e.g., entityimage, cr_document)',
|
||||
condition: { field: 'operation', value: ['upload_file', 'download_file'] },
|
||||
required: { field: 'operation', value: ['upload_file', 'download_file'] },
|
||||
},
|
||||
{
|
||||
id: 'fileName',
|
||||
title: 'File Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., document.pdf',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
required: { field: 'operation', value: 'upload_file' },
|
||||
},
|
||||
{
|
||||
id: 'uploadFile',
|
||||
title: 'File',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'file',
|
||||
placeholder: 'Upload a file',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
mode: 'basic',
|
||||
multiple: false,
|
||||
required: { field: 'operation', value: 'upload_file' },
|
||||
},
|
||||
{
|
||||
id: 'fileReference',
|
||||
title: 'File',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'file',
|
||||
placeholder: 'Reference a file from previous blocks (e.g., {{block_1.output.file}})',
|
||||
condition: { field: 'operation', value: 'upload_file' },
|
||||
mode: 'advanced',
|
||||
required: { field: 'operation', value: 'upload_file' },
|
||||
},
|
||||
// OData query options (list_records)
|
||||
{
|
||||
id: 'select',
|
||||
title: 'Select Columns',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated columns (e.g., name,telephone1,emailaddress1)',
|
||||
condition: { field: 'operation', value: ['list_records', 'get_record'] },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a comma-separated list of Dataverse column logical names based on the user's description.
|
||||
Use lowercase logical names without spaces.
|
||||
Common columns by table:
|
||||
- Accounts: name, accountnumber, telephone1, emailaddress1, address1_city, revenue, industrycode
|
||||
- Contacts: firstname, lastname, fullname, emailaddress1, telephone1, jobtitle, birthdate
|
||||
- General: statecode, statuscode, createdon, modifiedon, ownerid, createdby
|
||||
|
||||
Return ONLY the comma-separated column names - no explanations.`,
|
||||
placeholder: 'Describe which columns you want to retrieve...',
|
||||
generationType: 'odata-expression',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'filter',
|
||||
title: 'Filter',
|
||||
type: 'short-input',
|
||||
placeholder: "OData filter (e.g., statecode eq 0 and contains(name,'Contoso'))",
|
||||
condition: { field: 'operation', value: ['list_records', 'search'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an OData $filter expression for the Dataverse Web API based on the user's description.
|
||||
OData filter syntax:
|
||||
- Comparison: eq, ne, gt, ge, lt, le (e.g., "revenue gt 1000000")
|
||||
- Logical: and, or, not (e.g., "statecode eq 0 and revenue gt 1000000")
|
||||
- String functions: contains(name,'value'), startswith(name,'value'), endswith(name,'value')
|
||||
- Date functions: year(createdon) eq 2024, month(createdon) eq 1
|
||||
- Null check: fieldname eq null, fieldname ne null
|
||||
- Status: statecode eq 0 (active), statecode eq 1 (inactive)
|
||||
|
||||
Return ONLY the filter expression - no $filter= prefix, no explanations.`,
|
||||
placeholder: 'Describe which records you want to filter for...',
|
||||
generationType: 'odata-expression',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'orderBy',
|
||||
title: 'Order By',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., name asc, createdon desc',
|
||||
condition: { field: 'operation', value: ['list_records', 'search'] },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an OData $orderby expression for sorting Dataverse records based on the user's description.
|
||||
Format: column_name asc|desc, separated by commas for multi-column sort.
|
||||
Examples:
|
||||
- "name asc" - Sort by name alphabetically
|
||||
- "createdon desc" - Sort by creation date, newest first
|
||||
- "name asc, createdon desc" - Sort by name, then by date
|
||||
|
||||
Return ONLY the orderby expression - no $orderby= prefix, no explanations.`,
|
||||
placeholder: 'Describe how you want to sort the results...',
|
||||
generationType: 'odata-expression',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'top',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: 'Maximum number of records (default: 5000)',
|
||||
condition: { field: 'operation', value: ['list_records', 'search'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'expand',
|
||||
title: 'Expand',
|
||||
type: 'short-input',
|
||||
placeholder: 'Navigation properties to expand (e.g., primarycontactid)',
|
||||
condition: { field: 'operation', value: ['list_records', 'get_record'] },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an OData $expand expression for the Dataverse Web API based on the user's description.
|
||||
$expand retrieves related records through navigation properties.
|
||||
Examples:
|
||||
- "primarycontactid" - Expand the primary contact lookup
|
||||
- "contact_customer_accounts" - Expand related contacts for an account
|
||||
- "primarycontactid($select=fullname,emailaddress1)" - Expand with selected columns
|
||||
- "contact_customer_accounts($select=fullname;$top=5;$orderby=fullname asc)" - Expand with query options
|
||||
|
||||
Return ONLY the expand expression - no $expand= prefix, no explanations.`,
|
||||
placeholder: 'Describe which related records you want to include...',
|
||||
generationType: 'odata-expression',
|
||||
},
|
||||
},
|
||||
// Associate/Disassociate
|
||||
{
|
||||
id: 'navigationProperty',
|
||||
title: 'Navigation Property',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., contact_customer_accounts',
|
||||
condition: { field: 'operation', value: ['associate', 'disassociate'] },
|
||||
required: { field: 'operation', value: ['associate', 'disassociate'] },
|
||||
},
|
||||
{
|
||||
id: 'navigationType',
|
||||
title: 'Navigation Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Collection-valued (default)', id: 'collection' },
|
||||
{ label: 'Single-valued (lookup)', id: 'single' },
|
||||
],
|
||||
value: () => 'collection',
|
||||
condition: { field: 'operation', value: 'associate' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'targetEntitySetName',
|
||||
title: 'Target Entity Set',
|
||||
type: 'short-input',
|
||||
placeholder: 'Target table name (e.g., contacts)',
|
||||
condition: { field: 'operation', value: 'associate' },
|
||||
required: { field: 'operation', value: 'associate' },
|
||||
},
|
||||
{
|
||||
id: 'targetRecordId',
|
||||
title: 'Target Record ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Target record GUID',
|
||||
condition: { field: 'operation', value: ['associate', 'disassociate'] },
|
||||
required: { field: 'operation', value: 'associate' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'microsoft_dataverse_associate',
|
||||
'microsoft_dataverse_create_multiple',
|
||||
'microsoft_dataverse_create_record',
|
||||
'microsoft_dataverse_delete_record',
|
||||
'microsoft_dataverse_disassociate',
|
||||
'microsoft_dataverse_download_file',
|
||||
'microsoft_dataverse_execute_action',
|
||||
'microsoft_dataverse_execute_function',
|
||||
'microsoft_dataverse_fetchxml_query',
|
||||
'microsoft_dataverse_get_record',
|
||||
'microsoft_dataverse_list_records',
|
||||
'microsoft_dataverse_search',
|
||||
'microsoft_dataverse_update_multiple',
|
||||
'microsoft_dataverse_update_record',
|
||||
'microsoft_dataverse_upload_file',
|
||||
'microsoft_dataverse_upsert_record',
|
||||
'microsoft_dataverse_whoami',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => `microsoft_dataverse_${params.operation}`,
|
||||
params: (params) => {
|
||||
const { credential, operation, file, ...rest } = params
|
||||
|
||||
const cleanParams: Record<string, unknown> = {
|
||||
credential,
|
||||
}
|
||||
|
||||
// Normalize file input from basic (uploadFile) or advanced (fileReference) mode
|
||||
const normalizedFile = normalizeFileInput(file, { single: true })
|
||||
if (normalizedFile) {
|
||||
cleanParams.file = normalizedFile
|
||||
}
|
||||
|
||||
// Map block subBlock IDs to tool param names where they differ
|
||||
if (operation === 'search' && rest.searchEntities) {
|
||||
cleanParams.entities = rest.searchEntities
|
||||
rest.searchEntities = undefined
|
||||
}
|
||||
if (operation === 'execute_function' && rest.functionParameters) {
|
||||
cleanParams.parameters = rest.functionParameters
|
||||
rest.functionParameters = undefined
|
||||
// Prevent stale action parameters from overwriting mapped function parameters
|
||||
rest.parameters = undefined
|
||||
}
|
||||
// Always clean up mapped subBlock IDs so they don't leak through the loop below
|
||||
rest.searchEntities = undefined
|
||||
rest.functionParameters = undefined
|
||||
|
||||
Object.entries(rest).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
cleanParams[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
return cleanParams
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Microsoft Dataverse OAuth credential' },
|
||||
environmentUrl: { type: 'string', description: 'Dataverse environment URL' },
|
||||
entitySetName: { type: 'string', description: 'Entity set name (plural table name)' },
|
||||
recordId: { type: 'string', description: 'Record GUID' },
|
||||
data: { type: 'json', description: 'Record data as JSON object' },
|
||||
select: { type: 'string', description: 'Columns to return (comma-separated)' },
|
||||
filter: { type: 'string', description: 'OData $filter expression' },
|
||||
orderBy: { type: 'string', description: 'OData $orderby expression' },
|
||||
top: { type: 'string', description: 'Maximum number of records' },
|
||||
expand: { type: 'string', description: 'Navigation properties to expand' },
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
description: 'Navigation property name for associations',
|
||||
},
|
||||
navigationType: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Navigation property type: "collection" (default) or "single" (for lookup fields)',
|
||||
},
|
||||
targetEntitySetName: { type: 'string', description: 'Target entity set for association' },
|
||||
targetRecordId: { type: 'string', description: 'Target record GUID for association' },
|
||||
fetchXml: { type: 'string', description: 'FetchXML query string' },
|
||||
searchTerm: { type: 'string', description: 'Search text for relevance search' },
|
||||
searchEntities: { type: 'string', description: 'JSON array of search entity configurations' },
|
||||
searchMode: { type: 'string', description: 'Search mode: "any" or "all"' },
|
||||
searchType: { type: 'string', description: 'Query type: "simple" or "lucene"' },
|
||||
actionName: { type: 'string', description: 'Dataverse action name to execute' },
|
||||
functionName: { type: 'string', description: 'Dataverse function name to execute' },
|
||||
functionParameters: {
|
||||
type: 'string',
|
||||
description: 'Function parameters as URL-encoded string',
|
||||
},
|
||||
parameters: { type: 'json', description: 'Action parameters as JSON object' },
|
||||
entityLogicalName: { type: 'string', description: 'Table logical name for @odata.type' },
|
||||
records: { type: 'json', description: 'Array of record objects for bulk operations' },
|
||||
fileColumn: { type: 'string', description: 'File or image column logical name' },
|
||||
fileName: { type: 'string', description: 'Name of the file to upload' },
|
||||
file: { type: 'json', description: 'File to upload (canonical param)' },
|
||||
},
|
||||
outputs: {
|
||||
records: { type: 'json', description: 'Array of records (list/fetchxml/search)' },
|
||||
record: { type: 'json', description: 'Single record data' },
|
||||
recordId: { type: 'string', description: 'Record ID' },
|
||||
count: { type: 'number', description: 'Number of records returned in the current page' },
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total matching records server-side',
|
||||
},
|
||||
nextLink: { type: 'string', description: 'URL for next page of results' },
|
||||
created: { type: 'boolean', description: 'Whether a new record was created (upsert)' },
|
||||
userId: { type: 'string', description: 'Authenticated user ID (WhoAmI)' },
|
||||
businessUnitId: { type: 'string', description: 'Business unit ID (WhoAmI)' },
|
||||
organizationId: { type: 'string', description: 'Organization ID (WhoAmI)' },
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
description: 'Source entity set name (associate/disassociate)',
|
||||
},
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
description: 'Navigation property used (associate/disassociate)',
|
||||
},
|
||||
targetEntitySetName: { type: 'string', description: 'Target entity set name (associate)' },
|
||||
targetRecordId: { type: 'string', description: 'Target record GUID (associate/disassociate)' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
result: { type: 'json', description: 'Action/function result data' },
|
||||
ids: { type: 'json', description: 'Array of created record IDs (create multiple)' },
|
||||
fetchXmlPagingCookie: { type: 'string', description: 'Paging cookie for FetchXML pagination' },
|
||||
moreRecords: { type: 'boolean', description: 'Whether more records are available (FetchXML)' },
|
||||
results: { type: 'json', description: 'Search results array' },
|
||||
facets: { type: 'json', description: 'Facet results for search (when facets requested)' },
|
||||
fileContent: { type: 'string', description: 'Base64-encoded downloaded file content' },
|
||||
fileName: { type: 'string', description: 'Downloaded file name' },
|
||||
fileSize: { type: 'number', description: 'File size in bytes' },
|
||||
mimeType: { type: 'string', description: 'File MIME type' },
|
||||
fileColumn: { type: 'string', description: 'File column name' },
|
||||
},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
||||
step: 1,
|
||||
integer: true,
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
@@ -58,7 +57,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination (from nextPageToken)',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'channelId',
|
||||
@@ -66,7 +64,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter results to a specific channel',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'eventType',
|
||||
@@ -80,7 +77,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'publishedAfter',
|
||||
@@ -88,7 +84,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: '2024-01-01T00:00:00Z',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp based on the user's description.
|
||||
@@ -111,7 +106,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: '2024-12-31T23:59:59Z',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp based on the user's description.
|
||||
@@ -140,7 +134,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'any',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
@@ -155,7 +148,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'relevance',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'videoCategoryId',
|
||||
@@ -163,7 +155,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Use Get Video Categories to find IDs',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'videoDefinition',
|
||||
@@ -176,7 +167,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'any',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'videoCaption',
|
||||
@@ -189,7 +179,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'any',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'regionCode',
|
||||
@@ -200,7 +189,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
field: 'operation',
|
||||
value: ['youtube_search', 'youtube_trending', 'youtube_video_categories'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'relevanceLanguage',
|
||||
@@ -208,7 +196,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'en, es, fr',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'safeSearch',
|
||||
@@ -221,7 +208,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'moderate',
|
||||
condition: { field: 'operation', value: 'youtube_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Trending Videos operation inputs
|
||||
{
|
||||
@@ -240,7 +226,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Use Get Video Categories to find IDs',
|
||||
condition: { field: 'operation', value: 'youtube_trending' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
@@ -248,7 +233,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination (from nextPageToken)',
|
||||
condition: { field: 'operation', value: 'youtube_trending' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Video Details operation inputs
|
||||
{
|
||||
@@ -266,7 +250,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'en, es, fr (for category names)',
|
||||
condition: { field: 'operation', value: 'youtube_video_categories' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Channel Info operation inputs
|
||||
{
|
||||
@@ -315,7 +298,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'date',
|
||||
condition: { field: 'operation', value: 'youtube_channel_videos' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
@@ -323,7 +305,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination (from nextPageToken)',
|
||||
condition: { field: 'operation', value: 'youtube_channel_videos' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Channel Playlists operation inputs
|
||||
{
|
||||
@@ -350,7 +331,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination (from nextPageToken)',
|
||||
condition: { field: 'operation', value: 'youtube_channel_playlists' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Playlist Items operation inputs
|
||||
{
|
||||
@@ -377,7 +357,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination (from nextPageToken)',
|
||||
condition: { field: 'operation', value: 'youtube_playlist_items' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Video Comments operation inputs
|
||||
{
|
||||
@@ -408,7 +387,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
value: () => 'relevance',
|
||||
condition: { field: 'operation', value: 'youtube_comments' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
@@ -416,7 +394,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'Token for pagination (from nextPageToken)',
|
||||
condition: { field: 'operation', value: 'youtube_comments' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// API Key (common to all operations)
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@ import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
|
||||
import { CirclebackBlock } from '@/blocks/blocks/circleback'
|
||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||
import { ClerkBlock } from '@/blocks/blocks/clerk'
|
||||
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
||||
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
|
||||
@@ -79,7 +78,6 @@ import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
|
||||
import { McpBlock } from '@/blocks/blocks/mcp'
|
||||
import { Mem0Block } from '@/blocks/blocks/mem0'
|
||||
import { MemoryBlock } from '@/blocks/blocks/memory'
|
||||
import { MicrosoftDataverseBlock } from '@/blocks/blocks/microsoft_dataverse'
|
||||
import { MicrosoftExcelBlock, MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel'
|
||||
import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner'
|
||||
import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams'
|
||||
@@ -148,7 +146,6 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio'
|
||||
import { TwilioVoiceBlock } from '@/blocks/blocks/twilio_voice'
|
||||
import { TypeformBlock } from '@/blocks/blocks/typeform'
|
||||
import { VariablesBlock } from '@/blocks/blocks/variables'
|
||||
import { VercelBlock } from '@/blocks/blocks/vercel'
|
||||
import { VideoGeneratorBlock, VideoGeneratorV2Block } from '@/blocks/blocks/video_generator'
|
||||
import { VisionBlock, VisionV2Block } from '@/blocks/blocks/vision'
|
||||
import { WaitBlock } from '@/blocks/blocks/wait'
|
||||
@@ -185,7 +182,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
calendly: CalendlyBlock,
|
||||
chat_trigger: ChatTriggerBlock,
|
||||
circleback: CirclebackBlock,
|
||||
cloudflare: CloudflareBlock,
|
||||
clay: ClayBlock,
|
||||
clerk: ClerkBlock,
|
||||
condition: ConditionBlock,
|
||||
@@ -262,7 +258,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
mcp: McpBlock,
|
||||
mem0: Mem0Block,
|
||||
memory: MemoryBlock,
|
||||
microsoft_dataverse: MicrosoftDataverseBlock,
|
||||
microsoft_excel: MicrosoftExcelBlock,
|
||||
microsoft_excel_v2: MicrosoftExcelV2Block,
|
||||
microsoft_planner: MicrosoftPlannerBlock,
|
||||
@@ -335,7 +330,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
twilio_sms: TwilioSMSBlock,
|
||||
twilio_voice: TwilioVoiceBlock,
|
||||
typeform: TypeformBlock,
|
||||
vercel: VercelBlock,
|
||||
variables: VariablesBlock,
|
||||
video_generator: VideoGeneratorBlock,
|
||||
video_generator_v2: VideoGeneratorV2Block,
|
||||
|
||||
@@ -41,7 +41,6 @@ export type GenerationType =
|
||||
| 'timestamp'
|
||||
| 'timezone'
|
||||
| 'cron-expression'
|
||||
| 'odata-expression'
|
||||
|
||||
export type SubBlockType =
|
||||
| 'short-input' // Single line input
|
||||
|
||||
@@ -4407,161 +4407,6 @@ export function DatadogIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function MicrosoftDataverseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
const id = useId()
|
||||
const clip0 = `dataverse_clip0_${id}`
|
||||
const clip1 = `dataverse_clip1_${id}`
|
||||
const clip2 = `dataverse_clip2_${id}`
|
||||
const paint0 = `dataverse_paint0_${id}`
|
||||
const paint1 = `dataverse_paint1_${id}`
|
||||
const paint2 = `dataverse_paint2_${id}`
|
||||
const paint3 = `dataverse_paint3_${id}`
|
||||
const paint4 = `dataverse_paint4_${id}`
|
||||
const paint5 = `dataverse_paint5_${id}`
|
||||
const paint6 = `dataverse_paint6_${id}`
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width='96'
|
||||
height='96'
|
||||
viewBox='0 0 96 96'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<g clipPath={`url(#${clip0})`}>
|
||||
<g clipPath={`url(#${clip1})`}>
|
||||
<g clipPath={`url(#${clip2})`}>
|
||||
<path
|
||||
d='M13.8776 21.8242C29.1033 8.13791 49.7501 8.1861 62.955 18.9134C74.9816 28.6836 77.4697 44.3159 70.851 55.7801C64.2321 67.2443 52.5277 70.1455 39.5011 62.6247L31.7286 76.087L31.7234 76.0862C27.4181 83.5324 17.8937 86.0828 10.4437 81.7817C7.45394 80.0556 5.25322 77.4879 3.96665 74.551L3.96096 74.5511C-4.07832 55.7804 0.200745 34.1184 13.8776 21.8242Z'
|
||||
fill={`url(#${paint0})`}
|
||||
/>
|
||||
<path
|
||||
d='M13.8776 21.8242C29.1033 8.13791 49.7501 8.1861 62.955 18.9134C74.9816 28.6836 77.4697 44.3159 70.851 55.7801C64.2321 67.2443 52.5277 70.1455 39.5011 62.6247L31.7286 76.087L31.7234 76.0862C27.4181 83.5324 17.8937 86.0828 10.4437 81.7817C7.45394 80.0556 5.25322 77.4879 3.96665 74.551L3.96096 74.5511C-4.07832 55.7804 0.200745 34.1184 13.8776 21.8242Z'
|
||||
fill={`url(#${paint1})`}
|
||||
fillOpacity='0.8'
|
||||
/>
|
||||
<path
|
||||
d='M85.4327 14.2231C88.4528 15.9668 90.6686 18.569 91.9494 21.5433L91.9533 21.5444C99.9406 40.2943 95.6533 61.9068 81.9983 74.1814C66.7726 87.8677 46.1257 87.8196 32.9209 77.0923C20.8945 67.3221 18.4062 51.6897 25.0249 40.2256C31.6438 28.7614 43.3482 25.8601 56.3748 33.381L64.1434 19.9255L64.1482 19.9249C68.4516 12.4736 77.9805 9.92084 85.4327 14.2231Z'
|
||||
fill={`url(#${paint2})`}
|
||||
/>
|
||||
<path
|
||||
d='M85.4327 14.2231C88.4528 15.9668 90.6686 18.569 91.9494 21.5433L91.9533 21.5444C99.9406 40.2943 95.6533 61.9068 81.9983 74.1814C66.7726 87.8677 46.1257 87.8196 32.9209 77.0923C20.8945 67.3221 18.4062 51.6897 25.0249 40.2256C31.6438 28.7614 43.3482 25.8601 56.3748 33.381L64.1434 19.9255L64.1482 19.9249C68.4516 12.4736 77.9805 9.92084 85.4327 14.2231Z'
|
||||
fill={`url(#${paint3})`}
|
||||
fillOpacity='0.9'
|
||||
/>
|
||||
<path
|
||||
d='M39.5041 62.6261C52.5307 70.1469 64.2352 67.2456 70.8541 55.7814C77.2488 44.7055 75.1426 29.7389 64.147 19.9271L56.3791 33.3814L39.5041 62.6261Z'
|
||||
fill={`url(#${paint4})`}
|
||||
/>
|
||||
<path
|
||||
d='M56.3794 33.3815C43.3528 25.8607 31.6482 28.762 25.0294 40.2262C18.6347 51.3021 20.7409 66.2687 31.7364 76.0806L39.5043 62.6262L56.3794 33.3815Z'
|
||||
fill={`url(#${paint5})`}
|
||||
/>
|
||||
<path
|
||||
d='M33.3215 56.4453C37.9837 64.5204 48.3094 67.2872 56.3846 62.625C64.4598 57.9628 67.2266 47.6371 62.5643 39.5619C57.9021 31.4867 47.5764 28.72 39.5013 33.3822C31.4261 38.0444 28.6593 48.3701 33.3215 56.4453Z'
|
||||
fill={`url(#${paint6})`}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id={paint0}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(46.0001 49.4996) rotate(-148.717) scale(46.2195 47.5359)'
|
||||
>
|
||||
<stop offset='0.465088' stopColor='#09442A' />
|
||||
<stop offset='0.70088' stopColor='#136C6C' />
|
||||
<stop offset='1' stopColor='#22918B' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint1}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(50.0001 32.4996) rotate(123.57) scale(66.0095 46.5498)'
|
||||
>
|
||||
<stop offset='0.718705' stopColor='#1A7F7C' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#16BBDA' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint2}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(50.4999 44.5001) rotate(30.75) scale(45.9618 44.5095)'
|
||||
>
|
||||
<stop offset='0.358097' stopColor='#136C6C' />
|
||||
<stop offset='0.789474' stopColor='#42B870' />
|
||||
<stop offset='1' stopColor='#76D45E' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint3}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientTransform='matrix(42.5 -36.0002 31.1824 36.8127 49.4998 55.5001)'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
>
|
||||
<stop offset='0.583166' stopColor='#76D45E' stopOpacity='0' />
|
||||
<stop offset='1' stopColor='#C8F5B7' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint4}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(47.5 48) rotate(-58.9042) scale(32.6898)'
|
||||
>
|
||||
<stop offset='0.486266' stopColor='#22918B' />
|
||||
<stop offset='0.729599' stopColor='#42B870' />
|
||||
<stop offset='1' stopColor='#43E5CA' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint5}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(47.3833 49.0077) rotate(119.859) scale(31.1328 29.4032)'
|
||||
>
|
||||
<stop offset='0.459553' stopColor='#08494E' />
|
||||
<stop offset='0.742242' stopColor='#1A7F7C' />
|
||||
<stop offset='1' stopColor='#309C61' />
|
||||
</radialGradient>
|
||||
<radialGradient
|
||||
id={paint6}
|
||||
cx='0'
|
||||
cy='0'
|
||||
r='1'
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientTransform='translate(52.5 40) rotate(120.784) scale(27.3542)'
|
||||
>
|
||||
<stop stopColor='#C8F5B7' />
|
||||
<stop offset='0.24583' stopColor='#98F0B0' />
|
||||
<stop offset='0.643961' stopColor='#52D17C' />
|
||||
<stop offset='1' stopColor='#119FC5' />
|
||||
</radialGradient>
|
||||
<clipPath id={clip0}>
|
||||
<rect width='96' height='96' fill='white' />
|
||||
</clipPath>
|
||||
<clipPath id={clip1}>
|
||||
<rect width='96' height='96' fill='white' />
|
||||
</clipPath>
|
||||
<clipPath id={clip2}>
|
||||
<rect width='95.9998' height='96' fill='white' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function KalshiIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 78 20' fill='currentColor' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -5687,33 +5532,3 @@ export function OnePasswordIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function VercelIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='0 0 256 222'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
preserveAspectRatio='xMidYMid'
|
||||
>
|
||||
<g transform='translate(19.2 16.63) scale(0.85)'>
|
||||
<polygon fill='#fafafa' points='128 0 256 221.705007 0 221.705007' />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function CloudflareIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>
|
||||
<path
|
||||
fill='#f38020'
|
||||
d='M331 326c11-26-4-38-19-38l-148-2c-4 0-4-6 1-7l150-2c17-1 37-15 43-33 0 0 10-21 9-24a97 97 0 0 0-187-11c-38-25-78 9-69 46-48 3-65 46-60 72 0 1 1 2 3 2h274c1 0 3-1 3-3z'
|
||||
/>
|
||||
<path
|
||||
fill='#faae40'
|
||||
d='M381 224c-4 0-6-1-7 1l-5 21c-5 16 3 30 20 31l32 2c4 0 4 6-1 7l-33 1c-36 4-46 39-46 39 0 2 0 3 2 3h113l3-2a81 81 0 0 0-78-103'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { mockLogger, queryClient, useFolderStoreMock, useWorkflowRegistryMock } = vi.hoisted(() => ({
|
||||
mockLogger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
queryClient: {
|
||||
cancelQueries: vi.fn().mockResolvedValue(undefined),
|
||||
invalidateQueries: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
useFolderStoreMock: Object.assign(vi.fn(), {
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(),
|
||||
}),
|
||||
useWorkflowRegistryMock: Object.assign(vi.fn(), {
|
||||
getState: vi.fn(),
|
||||
setState: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
let folderState: {
|
||||
folders: Record<string, any>
|
||||
}
|
||||
|
||||
let workflowRegistryState: {
|
||||
workflows: Record<string, any>
|
||||
}
|
||||
|
||||
vi.mock('@sim/logger', () => ({
|
||||
createLogger: vi.fn(() => mockLogger),
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
keepPreviousData: {},
|
||||
useQuery: vi.fn(),
|
||||
useQueryClient: vi.fn(() => queryClient),
|
||||
useMutation: vi.fn((options) => options),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/folders/store', () => ({
|
||||
useFolderStore: useFolderStoreMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workflows/registry/store', () => ({
|
||||
useWorkflowRegistry: useWorkflowRegistryMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/queries/workflows', () => ({
|
||||
workflowKeys: {
|
||||
list: (workspaceId: string | undefined) => ['workflows', 'list', workspaceId ?? ''],
|
||||
},
|
||||
}))
|
||||
|
||||
import { useCreateFolder, useDuplicateFolderMutation } from '@/hooks/queries/folders'
|
||||
|
||||
function getOptimisticFolderByName(name: string) {
|
||||
return Object.values(folderState.folders).find((folder: any) => folder.name === name) as
|
||||
| { sortOrder: number }
|
||||
| undefined
|
||||
}
|
||||
|
||||
describe('folder optimistic top insertion ordering', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useFolderStoreMock.getState.mockImplementation(() => folderState)
|
||||
useFolderStoreMock.setState.mockImplementation((updater: any) => {
|
||||
if (typeof updater === 'function') {
|
||||
const next = updater(folderState)
|
||||
if (next) {
|
||||
folderState = { ...folderState, ...next }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
folderState = { ...folderState, ...updater }
|
||||
})
|
||||
useWorkflowRegistryMock.getState.mockImplementation(() => workflowRegistryState)
|
||||
|
||||
folderState = {
|
||||
folders: {
|
||||
'folder-parent-match': {
|
||||
id: 'folder-parent-match',
|
||||
name: 'Existing sibling folder',
|
||||
userId: 'user-1',
|
||||
workspaceId: 'ws-1',
|
||||
parentId: 'parent-1',
|
||||
color: '#808080',
|
||||
isExpanded: false,
|
||||
sortOrder: 5,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
'folder-other-parent': {
|
||||
id: 'folder-other-parent',
|
||||
name: 'Other parent folder',
|
||||
userId: 'user-1',
|
||||
workspaceId: 'ws-1',
|
||||
parentId: 'parent-2',
|
||||
color: '#808080',
|
||||
isExpanded: false,
|
||||
sortOrder: -100,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
workflowRegistryState = {
|
||||
workflows: {
|
||||
'workflow-parent-match': {
|
||||
id: 'workflow-parent-match',
|
||||
name: 'Existing sibling workflow',
|
||||
workspaceId: 'ws-1',
|
||||
folderId: 'parent-1',
|
||||
sortOrder: 2,
|
||||
},
|
||||
'workflow-other-parent': {
|
||||
id: 'workflow-other-parent',
|
||||
name: 'Other parent workflow',
|
||||
workspaceId: 'ws-1',
|
||||
folderId: 'parent-2',
|
||||
sortOrder: -50,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('creates folders at top of mixed non-root siblings', async () => {
|
||||
const mutation = useCreateFolder()
|
||||
|
||||
await mutation.onMutate({
|
||||
workspaceId: 'ws-1',
|
||||
name: 'New child folder',
|
||||
parentId: 'parent-1',
|
||||
})
|
||||
|
||||
const optimisticFolder = getOptimisticFolderByName('New child folder')
|
||||
expect(optimisticFolder).toBeDefined()
|
||||
expect(optimisticFolder?.sortOrder).toBe(1)
|
||||
})
|
||||
|
||||
it('duplicates folders at top of mixed non-root siblings', async () => {
|
||||
const mutation = useDuplicateFolderMutation()
|
||||
|
||||
await mutation.onMutate({
|
||||
workspaceId: 'ws-1',
|
||||
id: 'folder-parent-match',
|
||||
name: 'Duplicated child folder',
|
||||
parentId: 'parent-1',
|
||||
})
|
||||
|
||||
const optimisticFolder = getOptimisticFolderByName('Duplicated child folder')
|
||||
expect(optimisticFolder).toBeDefined()
|
||||
expect(optimisticFolder?.sortOrder).toBe(1)
|
||||
})
|
||||
|
||||
it('uses source parent scope when duplicate parentId is undefined', async () => {
|
||||
const mutation = useDuplicateFolderMutation()
|
||||
|
||||
await mutation.onMutate({
|
||||
workspaceId: 'ws-1',
|
||||
id: 'folder-parent-match',
|
||||
name: 'Duplicated with inherited parent',
|
||||
// parentId intentionally omitted to mirror duplicate fallback behavior
|
||||
})
|
||||
|
||||
const optimisticFolder = getOptimisticFolderByName('Duplicated with inherited parent') as
|
||||
| { parentId: string | null; sortOrder: number }
|
||||
| undefined
|
||||
expect(optimisticFolder).toBeDefined()
|
||||
expect(optimisticFolder?.parentId).toBe('parent-1')
|
||||
expect(optimisticFolder?.sortOrder).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -5,11 +5,9 @@ import {
|
||||
createOptimisticMutationHandlers,
|
||||
generateTempId,
|
||||
} from '@/hooks/queries/utils/optimistic-mutation'
|
||||
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
|
||||
import { workflowKeys } from '@/hooks/queries/workflows'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import type { WorkflowFolder } from '@/stores/folders/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('FolderQueries')
|
||||
|
||||
@@ -135,35 +133,40 @@ function createFolderMutationHandlers<TVariables extends { workspaceId: string }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next sort order for a folder in a given parent
|
||||
*/
|
||||
function getNextSortOrder(
|
||||
folders: Record<string, WorkflowFolder>,
|
||||
workspaceId: string,
|
||||
parentId: string | null | undefined
|
||||
): number {
|
||||
const siblingFolders = Object.values(folders).filter(
|
||||
(f) => f.workspaceId === workspaceId && f.parentId === (parentId || null)
|
||||
)
|
||||
return siblingFolders.reduce((max, f) => Math.max(max, f.sortOrder), -1) + 1
|
||||
}
|
||||
|
||||
export function useCreateFolder() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const handlers = createFolderMutationHandlers<CreateFolderVariables>(
|
||||
queryClient,
|
||||
'CreateFolder',
|
||||
(variables, tempId, previousFolders) => {
|
||||
const currentWorkflows = useWorkflowRegistry.getState().workflows
|
||||
|
||||
return {
|
||||
id: tempId,
|
||||
name: variables.name,
|
||||
userId: '',
|
||||
workspaceId: variables.workspaceId,
|
||||
parentId: variables.parentId || null,
|
||||
color: variables.color || '#808080',
|
||||
isExpanded: false,
|
||||
sortOrder:
|
||||
variables.sortOrder ??
|
||||
getTopInsertionSortOrder(
|
||||
currentWorkflows,
|
||||
previousFolders,
|
||||
variables.workspaceId,
|
||||
variables.parentId
|
||||
),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
(variables, tempId, previousFolders) => ({
|
||||
id: tempId,
|
||||
name: variables.name,
|
||||
userId: '',
|
||||
workspaceId: variables.workspaceId,
|
||||
parentId: variables.parentId || null,
|
||||
color: variables.color || '#808080',
|
||||
isExpanded: false,
|
||||
sortOrder:
|
||||
variables.sortOrder ??
|
||||
getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
)
|
||||
|
||||
return useMutation({
|
||||
@@ -239,25 +242,17 @@ export function useDuplicateFolderMutation() {
|
||||
queryClient,
|
||||
'DuplicateFolder',
|
||||
(variables, tempId, previousFolders) => {
|
||||
const currentWorkflows = useWorkflowRegistry.getState().workflows
|
||||
|
||||
// Get source folder info if available
|
||||
const sourceFolder = previousFolders[variables.id]
|
||||
const targetParentId = variables.parentId ?? sourceFolder?.parentId ?? null
|
||||
return {
|
||||
id: tempId,
|
||||
name: variables.name,
|
||||
userId: sourceFolder?.userId || '',
|
||||
workspaceId: variables.workspaceId,
|
||||
parentId: targetParentId,
|
||||
parentId: variables.parentId ?? sourceFolder?.parentId ?? null,
|
||||
color: variables.color || sourceFolder?.color || '#808080',
|
||||
isExpanded: false,
|
||||
sortOrder: getTopInsertionSortOrder(
|
||||
currentWorkflows,
|
||||
previousFolders,
|
||||
variables.workspaceId,
|
||||
targetParentId
|
||||
),
|
||||
sortOrder: getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
interface SortableWorkflow {
|
||||
workspaceId?: string
|
||||
folderId?: string | null
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
interface SortableFolder {
|
||||
workspaceId?: string
|
||||
parentId?: string | null
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the insertion sort order that places a new item at the top of a
|
||||
* mixed list of folders and workflows within the same parent scope.
|
||||
*/
|
||||
export function getTopInsertionSortOrder(
|
||||
workflows: Record<string, SortableWorkflow>,
|
||||
folders: Record<string, SortableFolder>,
|
||||
workspaceId: string,
|
||||
parentId: string | null | undefined
|
||||
): number {
|
||||
const normalizedParentId = parentId ?? null
|
||||
|
||||
const siblingWorkflows = Object.values(workflows).filter(
|
||||
(workflow) =>
|
||||
workflow.workspaceId === workspaceId && (workflow.folderId ?? null) === normalizedParentId
|
||||
)
|
||||
const siblingFolders = Object.values(folders).filter(
|
||||
(folder) =>
|
||||
folder.workspaceId === workspaceId && (folder.parentId ?? null) === normalizedParentId
|
||||
)
|
||||
|
||||
const siblingOrders = [
|
||||
...siblingWorkflows.map((workflow) => workflow.sortOrder ?? 0),
|
||||
...siblingFolders.map((folder) => folder.sortOrder),
|
||||
]
|
||||
|
||||
if (siblingOrders.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Math.min(...siblingOrders) - 1
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
createOptimisticMutationHandlers,
|
||||
generateTempId,
|
||||
} from '@/hooks/queries/utils/optimistic-mutation'
|
||||
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
||||
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
|
||||
@@ -225,13 +223,11 @@ export function useCreateWorkflow() {
|
||||
sortOrder = variables.sortOrder
|
||||
} else {
|
||||
const currentWorkflows = useWorkflowRegistry.getState().workflows
|
||||
const currentFolders = useFolderStore.getState().folders
|
||||
sortOrder = getTopInsertionSortOrder(
|
||||
currentWorkflows,
|
||||
currentFolders,
|
||||
variables.workspaceId,
|
||||
variables.folderId
|
||||
const targetFolderId = variables.folderId || null
|
||||
const workflowsInFolder = Object.values(currentWorkflows).filter(
|
||||
(w) => w.folderId === targetFolderId
|
||||
)
|
||||
sortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1) - 1
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -327,8 +323,11 @@ export function useDuplicateWorkflowMutation() {
|
||||
'DuplicateWorkflow',
|
||||
(variables, tempId) => {
|
||||
const currentWorkflows = useWorkflowRegistry.getState().workflows
|
||||
const currentFolders = useFolderStore.getState().folders
|
||||
const targetFolderId = variables.folderId ?? null
|
||||
const targetFolderId = variables.folderId || null
|
||||
const workflowsInFolder = Object.values(currentWorkflows).filter(
|
||||
(w) => w.folderId === targetFolderId
|
||||
)
|
||||
const minSortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1)
|
||||
|
||||
return {
|
||||
id: tempId,
|
||||
@@ -339,12 +338,7 @@ export function useDuplicateWorkflowMutation() {
|
||||
color: variables.color,
|
||||
workspaceId: variables.workspaceId,
|
||||
folderId: targetFolderId,
|
||||
sortOrder: getTopInsertionSortOrder(
|
||||
currentWorkflows,
|
||||
currentFolders,
|
||||
variables.workspaceId,
|
||||
targetFolderId
|
||||
),
|
||||
sortOrder: minSortOrder - 1,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -395,7 +395,6 @@ export const auth = betterAuth({
|
||||
'google-groups',
|
||||
'vertex-ai',
|
||||
'github-repo',
|
||||
'microsoft-dataverse',
|
||||
'microsoft-teams',
|
||||
'microsoft-excel',
|
||||
'microsoft-planner',
|
||||
@@ -1154,54 +1153,6 @@ export const auth = betterAuth({
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
providerId: 'microsoft-dataverse',
|
||||
clientId: env.MICROSOFT_CLIENT_ID as string,
|
||||
clientSecret: env.MICROSOFT_CLIENT_SECRET as string,
|
||||
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
|
||||
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
|
||||
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
responseType: 'code',
|
||||
accessType: 'offline',
|
||||
authentication: 'basic',
|
||||
pkce: true,
|
||||
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/microsoft-dataverse`,
|
||||
getUserInfo: async (tokens) => {
|
||||
// Dataverse access tokens target dynamics.microsoft.com, not graph.microsoft.com,
|
||||
// so we cannot call the Graph API /me endpoint. Instead, we decode the ID token JWT
|
||||
// which is always returned when the openid scope is requested.
|
||||
const idToken = (tokens as Record<string, unknown>).idToken as string | undefined
|
||||
if (!idToken) {
|
||||
logger.error(
|
||||
'Microsoft Dataverse OAuth: no ID token received. Ensure openid scope is requested.'
|
||||
)
|
||||
throw new Error('Microsoft Dataverse OAuth requires an ID token (openid scope)')
|
||||
}
|
||||
|
||||
const parts = idToken.split('.')
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Microsoft Dataverse OAuth: malformed ID token')
|
||||
}
|
||||
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'))
|
||||
const now = new Date()
|
||||
return {
|
||||
id: `${payload.oid || payload.sub}-${crypto.randomUUID()}`,
|
||||
name: payload.name || 'Microsoft User',
|
||||
email: payload.preferred_username || payload.email || payload.upn,
|
||||
emailVerified: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
providerId: 'microsoft-planner',
|
||||
clientId: env.MICROSOFT_CLIENT_ID as string,
|
||||
|
||||
@@ -2,7 +2,6 @@ export const MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS = 90
|
||||
export const PROACTIVE_REFRESH_THRESHOLD_DAYS = 7
|
||||
|
||||
export const MICROSOFT_PROVIDERS = new Set([
|
||||
'microsoft-dataverse',
|
||||
'microsoft-excel',
|
||||
'microsoft-planner',
|
||||
'microsoft-teams',
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
JiraIcon,
|
||||
LinearIcon,
|
||||
LinkedInIcon,
|
||||
MicrosoftDataverseIcon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftIcon,
|
||||
MicrosoftOneDriveIcon,
|
||||
@@ -155,20 +154,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
name: 'Microsoft',
|
||||
icon: MicrosoftIcon,
|
||||
services: {
|
||||
'microsoft-dataverse': {
|
||||
name: 'Microsoft Dataverse',
|
||||
description: 'Connect to Microsoft Dataverse and manage records.',
|
||||
providerId: 'microsoft-dataverse',
|
||||
icon: MicrosoftDataverseIcon,
|
||||
baseProviderIcon: MicrosoftIcon,
|
||||
scopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'https://dynamics.microsoft.com/user_impersonation',
|
||||
'offline_access',
|
||||
],
|
||||
},
|
||||
'microsoft-excel': {
|
||||
name: 'Microsoft Excel',
|
||||
description: 'Connect to Microsoft Excel and manage spreadsheets.',
|
||||
|
||||
@@ -20,7 +20,6 @@ export type OAuthProvider =
|
||||
| 'jira'
|
||||
| 'dropbox'
|
||||
| 'microsoft'
|
||||
| 'microsoft-dataverse'
|
||||
| 'microsoft-excel'
|
||||
| 'microsoft-planner'
|
||||
| 'microsoft-teams'
|
||||
@@ -62,7 +61,6 @@ export type OAuthService =
|
||||
| 'notion'
|
||||
| 'jira'
|
||||
| 'dropbox'
|
||||
| 'microsoft-dataverse'
|
||||
| 'microsoft-excel'
|
||||
| 'microsoft-teams'
|
||||
| 'microsoft-planner'
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { mockConsoleLogger, setupCommonApiMocks } from '@sim/testing'
|
||||
import { drizzleOrmMock } from '@sim/testing/mocks'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mockAuthorizeWorkflowByWorkspacePermission = vi.fn()
|
||||
const mockGetUserEntityPermissions = vi.fn()
|
||||
|
||||
const { mockDb } = vi.hoisted(() => ({
|
||||
mockDb: {
|
||||
transaction: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
...drizzleOrmMock,
|
||||
min: vi.fn((field) => ({ type: 'min', field })),
|
||||
}))
|
||||
vi.mock('@/lib/workflows/utils', () => ({
|
||||
authorizeWorkflowByWorkspacePermission: (...args: unknown[]) =>
|
||||
mockAuthorizeWorkflowByWorkspacePermission(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/workspaces/permissions/utils', () => ({
|
||||
getUserEntityPermissions: (...args: unknown[]) => mockGetUserEntityPermissions(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db/schema', () => ({
|
||||
workflow: {
|
||||
id: 'id',
|
||||
workspaceId: 'workspaceId',
|
||||
folderId: 'folderId',
|
||||
sortOrder: 'sortOrder',
|
||||
variables: 'variables',
|
||||
},
|
||||
workflowFolder: {
|
||||
workspaceId: 'workspaceId',
|
||||
parentId: 'parentId',
|
||||
sortOrder: 'sortOrder',
|
||||
},
|
||||
workflowBlocks: {
|
||||
workflowId: 'workflowId',
|
||||
},
|
||||
workflowEdges: {
|
||||
workflowId: 'workflowId',
|
||||
},
|
||||
workflowSubflows: {
|
||||
workflowId: 'workflowId',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@sim/db', () => ({
|
||||
db: mockDb,
|
||||
}))
|
||||
|
||||
import { duplicateWorkflow } from './duplicate'
|
||||
|
||||
function createMockTx(
|
||||
selectResults: unknown[],
|
||||
onWorkflowInsert?: (values: Record<string, unknown>) => void
|
||||
) {
|
||||
let selectCallCount = 0
|
||||
|
||||
const select = vi.fn().mockImplementation(() => ({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockImplementation(() => {
|
||||
const result = selectResults[selectCallCount++] ?? []
|
||||
if (selectCallCount === 1) {
|
||||
return {
|
||||
limit: vi.fn().mockResolvedValue(result),
|
||||
}
|
||||
}
|
||||
return Promise.resolve(result)
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
const insert = vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((values: Record<string, unknown>) => {
|
||||
onWorkflowInsert?.(values)
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
})
|
||||
|
||||
const update = vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
})
|
||||
|
||||
return {
|
||||
select,
|
||||
insert,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
describe('duplicateWorkflow ordering', () => {
|
||||
beforeEach(() => {
|
||||
setupCommonApiMocks()
|
||||
mockConsoleLogger()
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue('new-workflow-id'),
|
||||
})
|
||||
|
||||
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true })
|
||||
mockGetUserEntityPermissions.mockResolvedValue('write')
|
||||
})
|
||||
|
||||
it('uses mixed-sibling top insertion sort order', async () => {
|
||||
let insertedWorkflowValues: Record<string, unknown> | null = null
|
||||
const tx = createMockTx(
|
||||
[
|
||||
[
|
||||
{
|
||||
id: 'source-workflow-id',
|
||||
workspaceId: 'workspace-123',
|
||||
folderId: null,
|
||||
description: 'source',
|
||||
color: '#000000',
|
||||
variables: {},
|
||||
},
|
||||
],
|
||||
[{ minOrder: 5 }],
|
||||
[{ minOrder: 2 }],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
],
|
||||
(values) => {
|
||||
insertedWorkflowValues = values
|
||||
}
|
||||
)
|
||||
|
||||
mockDb.transaction.mockImplementation(async (callback: (txArg: unknown) => Promise<unknown>) =>
|
||||
callback(tx)
|
||||
)
|
||||
|
||||
const result = await duplicateWorkflow({
|
||||
sourceWorkflowId: 'source-workflow-id',
|
||||
userId: 'user-123',
|
||||
name: 'Duplicated',
|
||||
workspaceId: 'workspace-123',
|
||||
folderId: null,
|
||||
requestId: 'req-1',
|
||||
})
|
||||
|
||||
expect(result.sortOrder).toBe(1)
|
||||
expect(insertedWorkflowValues?.sortOrder).toBe(1)
|
||||
})
|
||||
|
||||
it('defaults to sortOrder 0 when target has no siblings', async () => {
|
||||
let insertedWorkflowValues: Record<string, unknown> | null = null
|
||||
const tx = createMockTx(
|
||||
[
|
||||
[
|
||||
{
|
||||
id: 'source-workflow-id',
|
||||
workspaceId: 'workspace-123',
|
||||
folderId: null,
|
||||
description: 'source',
|
||||
color: '#000000',
|
||||
variables: {},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
],
|
||||
(values) => {
|
||||
insertedWorkflowValues = values
|
||||
}
|
||||
)
|
||||
|
||||
mockDb.transaction.mockImplementation(async (callback: (txArg: unknown) => Promise<unknown>) =>
|
||||
callback(tx)
|
||||
)
|
||||
|
||||
const result = await duplicateWorkflow({
|
||||
sourceWorkflowId: 'source-workflow-id',
|
||||
userId: 'user-123',
|
||||
name: 'Duplicated',
|
||||
workspaceId: 'workspace-123',
|
||||
folderId: null,
|
||||
requestId: 'req-2',
|
||||
})
|
||||
|
||||
expect(result.sortOrder).toBe(0)
|
||||
expect(insertedWorkflowValues?.sortOrder).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import {
|
||||
workflow,
|
||||
workflowBlocks,
|
||||
workflowEdges,
|
||||
workflowFolder,
|
||||
workflowSubflows,
|
||||
} from '@sim/db/schema'
|
||||
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull, min } from 'drizzle-orm'
|
||||
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
|
||||
@@ -138,31 +132,15 @@ export async function duplicateWorkflow(
|
||||
throw new Error('Write or admin access required for target workspace')
|
||||
}
|
||||
const targetFolderId = folderId !== undefined ? folderId : source.folderId
|
||||
const workflowParentCondition = targetFolderId
|
||||
const folderCondition = targetFolderId
|
||||
? eq(workflow.folderId, targetFolderId)
|
||||
: isNull(workflow.folderId)
|
||||
const folderParentCondition = targetFolderId
|
||||
? eq(workflowFolder.parentId, targetFolderId)
|
||||
: isNull(workflowFolder.parentId)
|
||||
|
||||
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
|
||||
tx
|
||||
.select({ minOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition)),
|
||||
tx
|
||||
.select({ minOrder: min(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
|
||||
])
|
||||
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
|
||||
number | null
|
||||
>((currentMin, candidate) => {
|
||||
if (candidate == null) return currentMin
|
||||
if (currentMin == null) return candidate
|
||||
return Math.min(currentMin, candidate)
|
||||
}, null)
|
||||
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
|
||||
const [minResult] = await tx
|
||||
.select({ minOrder: min(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, targetWorkspaceId), folderCondition))
|
||||
const sortOrder = (minResult?.minOrder ?? 1) - 1
|
||||
|
||||
// Mapping from old variable IDs to new variable IDs (populated during variable duplication)
|
||||
const varIdMapping = new Map<string, string>()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextConfig } from 'next'
|
||||
import { env, getEnv, isTruthy } from './lib/core/config/env'
|
||||
import { isDev } from './lib/core/config/feature-flags'
|
||||
import { isDev, isHosted } from './lib/core/config/feature-flags'
|
||||
import {
|
||||
getFormEmbedCSPPolicy,
|
||||
getMainCSPPolicy,
|
||||
@@ -306,15 +306,34 @@ const nextConfig: NextConfig = {
|
||||
}
|
||||
)
|
||||
|
||||
return redirects
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
// Only enable domain redirects for the hosted version
|
||||
if (isHosted) {
|
||||
redirects.push(
|
||||
{
|
||||
source: '/((?!api|_next|_vercel|favicon|static|ingest|.*\\..*).*)',
|
||||
destination: 'https://www.sim.ai/$1',
|
||||
permanent: true,
|
||||
has: [{ type: 'host' as const, value: 'simstudio.ai' }],
|
||||
},
|
||||
{
|
||||
source: '/((?!api|_next|_vercel|favicon|static|ingest|.*\\..*).*)',
|
||||
destination: 'https://www.sim.ai/$1',
|
||||
permanent: true,
|
||||
has: [{ type: 'host' as const, value: 'www.simstudio.ai' }],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Beluga campaign short link tracking
|
||||
if (isHosted) {
|
||||
redirects.push({
|
||||
source: '/r/:shortCode',
|
||||
destination: 'https://go.trybeluga.ai/:shortCode',
|
||||
},
|
||||
]
|
||||
permanent: false,
|
||||
})
|
||||
}
|
||||
|
||||
return redirects
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"mysql2": "3.14.3",
|
||||
"nanoid": "^3.3.7",
|
||||
"neo4j-driver": "6.0.1",
|
||||
"next": "16.1.6",
|
||||
"next": "16.1.0-canary.21",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"next-runtime-env": "3.3.0",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -208,8 +208,8 @@
|
||||
"sharp"
|
||||
],
|
||||
"overrides": {
|
||||
"next": "16.1.6",
|
||||
"@next/env": "16.1.6",
|
||||
"next": "16.1.0-canary.21",
|
||||
"@next/env": "16.1.0-canary.21",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"postgres": "^3.4.5"
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import type {
|
||||
CloudflareCreateDnsRecordParams,
|
||||
CloudflareCreateDnsRecordResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createDnsRecordTool: ToolConfig<
|
||||
CloudflareCreateDnsRecordParams,
|
||||
CloudflareCreateDnsRecordResponse
|
||||
> = {
|
||||
id: 'cloudflare_create_dns_record',
|
||||
name: 'Cloudflare Create DNS Record',
|
||||
description: 'Creates a new DNS record for a zone.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to create the DNS record in',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record type (e.g., "A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV")',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record name (e.g., "example.com" or "subdomain.example.com")',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record content (e.g., IP address for A records, target for CNAME)',
|
||||
},
|
||||
ttl: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Time to live in seconds (1 = automatic, default: 1)',
|
||||
},
|
||||
proxied: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to enable Cloudflare proxy (default: false)',
|
||||
},
|
||||
priority: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority for MX and SRV records',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comment for the DNS record',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tags for the DNS record',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
type: params.type,
|
||||
name: params.name,
|
||||
content: params.content,
|
||||
}
|
||||
if (params.ttl !== undefined) body.ttl = Number(params.ttl)
|
||||
if (params.proxied !== undefined) body.proxied = params.proxied
|
||||
if (params.priority !== undefined) body.priority = Number(params.priority)
|
||||
if (params.comment) body.comment = params.comment
|
||||
if (params.tags) {
|
||||
const tagList = String(params.tags)
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
if (tagList.length > 0) body.tags = tagList
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
zone_id: '',
|
||||
zone_name: '',
|
||||
type: '',
|
||||
name: '',
|
||||
content: '',
|
||||
proxiable: false,
|
||||
proxied: false,
|
||||
ttl: 0,
|
||||
locked: false,
|
||||
priority: undefined,
|
||||
comment: null,
|
||||
tags: [],
|
||||
comment_modified_on: null,
|
||||
tags_modified_on: null,
|
||||
meta: null,
|
||||
created_on: '',
|
||||
modified_on: '',
|
||||
},
|
||||
error: data.errors?.[0]?.message ?? 'Failed to create DNS record',
|
||||
}
|
||||
}
|
||||
|
||||
const record = data.result
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: record?.id ?? '',
|
||||
zone_id: record?.zone_id ?? '',
|
||||
zone_name: record?.zone_name ?? '',
|
||||
type: record?.type ?? '',
|
||||
name: record?.name ?? '',
|
||||
content: record?.content ?? '',
|
||||
proxiable: record?.proxiable ?? false,
|
||||
proxied: record?.proxied ?? false,
|
||||
ttl: record?.ttl ?? 0,
|
||||
locked: record?.locked ?? false,
|
||||
priority: record?.priority ?? null,
|
||||
comment: record?.comment ?? null,
|
||||
tags: record?.tags ?? [],
|
||||
comment_modified_on: record?.comment_modified_on ?? null,
|
||||
tags_modified_on: record?.tags_modified_on ?? null,
|
||||
meta: record?.meta ?? null,
|
||||
created_on: record?.created_on ?? '',
|
||||
modified_on: record?.modified_on ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique identifier for the created DNS record' },
|
||||
zone_id: { type: 'string', description: 'The ID of the zone the record belongs to' },
|
||||
zone_name: { type: 'string', description: 'The name of the zone' },
|
||||
type: { type: 'string', description: 'DNS record type (A, AAAA, CNAME, MX, TXT, etc.)' },
|
||||
name: { type: 'string', description: 'DNS record hostname' },
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'DNS record value (e.g., IP address, target hostname)',
|
||||
},
|
||||
proxiable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the record can be proxied through Cloudflare',
|
||||
},
|
||||
proxied: { type: 'boolean', description: 'Whether Cloudflare proxy is enabled' },
|
||||
ttl: { type: 'number', description: 'Time to live in seconds (1 = automatic)' },
|
||||
locked: { type: 'boolean', description: 'Whether the record is locked' },
|
||||
priority: { type: 'number', description: 'Priority for MX and SRV records', optional: true },
|
||||
comment: { type: 'string', description: 'Comment associated with the record', optional: true },
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Tags associated with the record',
|
||||
items: { type: 'string', description: 'Tag value' },
|
||||
},
|
||||
comment_modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the comment was last modified',
|
||||
optional: true,
|
||||
},
|
||||
tags_modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when tags were last modified',
|
||||
optional: true,
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Record metadata',
|
||||
optional: true,
|
||||
properties: {
|
||||
source: { type: 'string', description: 'Source of the DNS record' },
|
||||
},
|
||||
},
|
||||
created_on: { type: 'string', description: 'ISO 8601 timestamp when the record was created' },
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the record was last modified',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
import type {
|
||||
CloudflareCreateZoneParams,
|
||||
CloudflareCreateZoneResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createZoneTool: ToolConfig<CloudflareCreateZoneParams, CloudflareCreateZoneResponse> =
|
||||
{
|
||||
id: 'cloudflare_create_zone',
|
||||
name: 'Cloudflare Create Zone',
|
||||
description: 'Adds a new zone (domain) to the Cloudflare account.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name to add (e.g., "example.com")',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The Cloudflare account ID',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Zone type: "full" (Cloudflare manages DNS), "partial" (CNAME setup), or "secondary" (secondary DNS)',
|
||||
},
|
||||
jump_start: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Automatically attempt to fetch existing DNS records when creating the zone',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://api.cloudflare.com/client/v4/zones',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {
|
||||
name: params.name,
|
||||
account: { id: params.accountId },
|
||||
}
|
||||
if (params.type) body.type = params.type
|
||||
if (params.jump_start !== undefined) body.jump_start = params.jump_start
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
name: '',
|
||||
status: '',
|
||||
paused: false,
|
||||
type: '',
|
||||
name_servers: [],
|
||||
original_name_servers: [],
|
||||
created_on: '',
|
||||
modified_on: '',
|
||||
activated_on: '',
|
||||
development_mode: 0,
|
||||
plan: {
|
||||
id: '',
|
||||
name: '',
|
||||
price: 0,
|
||||
is_subscribed: false,
|
||||
frequency: '',
|
||||
currency: '',
|
||||
legacy_id: '',
|
||||
},
|
||||
account: { id: '', name: '' },
|
||||
owner: { id: '', name: '', type: '' },
|
||||
meta: {
|
||||
cdn_only: false,
|
||||
custom_certificate_quota: 0,
|
||||
dns_only: false,
|
||||
foundation_dns: false,
|
||||
page_rule_quota: 0,
|
||||
phishing_detected: false,
|
||||
step: 0,
|
||||
},
|
||||
vanity_name_servers: [],
|
||||
permissions: [],
|
||||
},
|
||||
error: data.errors?.[0]?.message ?? 'Failed to create zone',
|
||||
}
|
||||
}
|
||||
|
||||
const zone = data.result
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: zone?.id ?? '',
|
||||
name: zone?.name ?? '',
|
||||
status: zone?.status ?? '',
|
||||
paused: zone?.paused ?? false,
|
||||
type: zone?.type ?? '',
|
||||
name_servers: zone?.name_servers ?? [],
|
||||
original_name_servers: zone?.original_name_servers ?? [],
|
||||
created_on: zone?.created_on ?? '',
|
||||
modified_on: zone?.modified_on ?? '',
|
||||
activated_on: zone?.activated_on ?? '',
|
||||
development_mode: zone?.development_mode ?? 0,
|
||||
plan: {
|
||||
id: zone?.plan?.id ?? '',
|
||||
name: zone?.plan?.name ?? '',
|
||||
price: zone?.plan?.price ?? 0,
|
||||
is_subscribed: zone?.plan?.is_subscribed ?? false,
|
||||
frequency: zone?.plan?.frequency ?? '',
|
||||
currency: zone?.plan?.currency ?? '',
|
||||
legacy_id: zone?.plan?.legacy_id ?? '',
|
||||
},
|
||||
account: {
|
||||
id: zone?.account?.id ?? '',
|
||||
name: zone?.account?.name ?? '',
|
||||
},
|
||||
owner: {
|
||||
id: zone?.owner?.id ?? '',
|
||||
name: zone?.owner?.name ?? '',
|
||||
type: zone?.owner?.type ?? '',
|
||||
},
|
||||
meta: {
|
||||
cdn_only: zone?.meta?.cdn_only ?? false,
|
||||
custom_certificate_quota: zone?.meta?.custom_certificate_quota ?? 0,
|
||||
dns_only: zone?.meta?.dns_only ?? false,
|
||||
foundation_dns: zone?.meta?.foundation_dns ?? false,
|
||||
page_rule_quota: zone?.meta?.page_rule_quota ?? 0,
|
||||
phishing_detected: zone?.meta?.phishing_detected ?? false,
|
||||
step: zone?.meta?.step ?? 0,
|
||||
},
|
||||
vanity_name_servers: zone?.vanity_name_servers ?? [],
|
||||
permissions: zone?.permissions ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Created zone ID' },
|
||||
name: { type: 'string', description: 'Domain name' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Zone status (initializing, pending, active, moved)',
|
||||
},
|
||||
paused: { type: 'boolean', description: 'Whether the zone is paused' },
|
||||
type: { type: 'string', description: 'Zone type (full, partial, or secondary)' },
|
||||
name_servers: {
|
||||
type: 'array',
|
||||
description: 'Assigned Cloudflare name servers',
|
||||
items: { type: 'string', description: 'Name server hostname' },
|
||||
},
|
||||
original_name_servers: {
|
||||
type: 'array',
|
||||
description: 'Original name servers before moving to Cloudflare',
|
||||
items: { type: 'string', description: 'Name server hostname' },
|
||||
optional: true,
|
||||
},
|
||||
created_on: { type: 'string', description: 'ISO 8601 date when the zone was created' },
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when the zone was last modified',
|
||||
},
|
||||
activated_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when the zone was activated',
|
||||
optional: true,
|
||||
},
|
||||
development_mode: {
|
||||
type: 'number',
|
||||
description: 'Seconds remaining in development mode (0 = off)',
|
||||
},
|
||||
plan: {
|
||||
type: 'object',
|
||||
description: 'Zone plan information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Plan identifier' },
|
||||
name: { type: 'string', description: 'Plan name' },
|
||||
price: { type: 'number', description: 'Plan price' },
|
||||
is_subscribed: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the zone is subscribed to the plan',
|
||||
},
|
||||
frequency: { type: 'string', description: 'Plan billing frequency' },
|
||||
currency: { type: 'string', description: 'Plan currency' },
|
||||
legacy_id: { type: 'string', description: 'Legacy plan identifier' },
|
||||
},
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
description: 'Account the zone belongs to',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Account identifier' },
|
||||
name: { type: 'string', description: 'Account name' },
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Zone owner information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Owner identifier' },
|
||||
name: { type: 'string', description: 'Owner name' },
|
||||
type: { type: 'string', description: 'Owner type' },
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Zone metadata',
|
||||
properties: {
|
||||
cdn_only: { type: 'boolean', description: 'Whether the zone is CDN only' },
|
||||
custom_certificate_quota: { type: 'number', description: 'Custom certificate quota' },
|
||||
dns_only: { type: 'boolean', description: 'Whether the zone is DNS only' },
|
||||
foundation_dns: { type: 'boolean', description: 'Whether foundation DNS is enabled' },
|
||||
page_rule_quota: { type: 'number', description: 'Page rule quota' },
|
||||
phishing_detected: { type: 'boolean', description: 'Whether phishing was detected' },
|
||||
step: { type: 'number', description: 'Current setup step' },
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
vanity_name_servers: {
|
||||
type: 'array',
|
||||
description: 'Custom vanity name servers',
|
||||
items: { type: 'string', description: 'Vanity name server hostname' },
|
||||
optional: true,
|
||||
},
|
||||
permissions: {
|
||||
type: 'array',
|
||||
description: 'User permissions for the zone',
|
||||
items: { type: 'string', description: 'Permission string' },
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import type {
|
||||
CloudflareDeleteDnsRecordParams,
|
||||
CloudflareDeleteDnsRecordResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteDnsRecordTool: ToolConfig<
|
||||
CloudflareDeleteDnsRecordParams,
|
||||
CloudflareDeleteDnsRecordResponse
|
||||
> = {
|
||||
id: 'cloudflare_delete_dns_record',
|
||||
name: 'Cloudflare Delete DNS Record',
|
||||
description: 'Deletes a DNS record from a zone.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID containing the DNS record',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The DNS record ID to delete',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records/${params.recordId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { id: '' },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to delete DNS record',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.result?.id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Deleted record ID' },
|
||||
},
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type {
|
||||
CloudflareDeleteZoneParams,
|
||||
CloudflareDeleteZoneResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteZoneTool: ToolConfig<CloudflareDeleteZoneParams, CloudflareDeleteZoneResponse> =
|
||||
{
|
||||
id: 'cloudflare_delete_zone',
|
||||
name: 'Cloudflare Delete Zone',
|
||||
description: 'Deletes a zone (domain) from the Cloudflare account.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to delete',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { id: '' },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to delete zone',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.result?.id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Deleted zone ID' },
|
||||
},
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
import type {
|
||||
CloudflareDnsAnalyticsParams,
|
||||
CloudflareDnsAnalyticsResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const dnsAnalyticsTool: ToolConfig<
|
||||
CloudflareDnsAnalyticsParams,
|
||||
CloudflareDnsAnalyticsResponse
|
||||
> = {
|
||||
id: 'cloudflare_dns_analytics',
|
||||
name: 'Cloudflare DNS Analytics',
|
||||
description: 'Gets DNS analytics report for a zone including query counts and trends.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to get DNS analytics for',
|
||||
},
|
||||
since: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start date for analytics (ISO 8601, e.g., "2024-01-01T00:00:00Z") or relative (e.g., "-6h")',
|
||||
},
|
||||
until: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'End date for analytics (ISO 8601, e.g., "2024-01-31T23:59:59Z") or relative (e.g., "now")',
|
||||
},
|
||||
metrics: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated metrics to retrieve (e.g., "queryCount,uncachedCount,staleCount,responseTimeAvg,responseTimeMedian,responseTime90th,responseTime99th")',
|
||||
},
|
||||
dimensions: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated dimensions to group by (e.g., "queryName,queryType,responseCode,responseCached,coloName,origin,dayOfWeek,tcp,ipVersion,querySizeBucket,responseSizeBucket")',
|
||||
},
|
||||
filters: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filters to apply to the data (e.g., "queryType==A")',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort order for the result set. Fields must be included in metrics or dimensions (e.g., "+queryCount" or "-responseTimeAvg")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results to return',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_analytics/report`
|
||||
)
|
||||
if (params.since) url.searchParams.append('since', params.since)
|
||||
if (params.until) url.searchParams.append('until', params.until)
|
||||
if (params.metrics) url.searchParams.append('metrics', params.metrics)
|
||||
if (params.dimensions) url.searchParams.append('dimensions', params.dimensions)
|
||||
if (params.filters) url.searchParams.append('filters', params.filters)
|
||||
if (params.sort) url.searchParams.append('sort', params.sort)
|
||||
if (params.limit) url.searchParams.append('limit', String(params.limit))
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
totals: {
|
||||
queryCount: 0,
|
||||
uncachedCount: 0,
|
||||
staleCount: 0,
|
||||
responseTimeAvg: 0,
|
||||
responseTimeMedian: 0,
|
||||
responseTime90th: 0,
|
||||
responseTime99th: 0,
|
||||
},
|
||||
min: {
|
||||
queryCount: 0,
|
||||
uncachedCount: 0,
|
||||
staleCount: 0,
|
||||
responseTimeAvg: 0,
|
||||
responseTimeMedian: 0,
|
||||
responseTime90th: 0,
|
||||
responseTime99th: 0,
|
||||
},
|
||||
max: {
|
||||
queryCount: 0,
|
||||
uncachedCount: 0,
|
||||
staleCount: 0,
|
||||
responseTimeAvg: 0,
|
||||
responseTimeMedian: 0,
|
||||
responseTime90th: 0,
|
||||
responseTime99th: 0,
|
||||
},
|
||||
data: [],
|
||||
data_lag: 0,
|
||||
rows: 0,
|
||||
query: {
|
||||
since: '',
|
||||
until: '',
|
||||
metrics: [],
|
||||
dimensions: [],
|
||||
filters: '',
|
||||
sort: [],
|
||||
limit: 0,
|
||||
},
|
||||
},
|
||||
error: data.errors?.[0]?.message ?? 'Failed to get DNS analytics',
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.result
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
totals: {
|
||||
queryCount: result?.totals?.queryCount ?? 0,
|
||||
uncachedCount: result?.totals?.uncachedCount ?? 0,
|
||||
staleCount: result?.totals?.staleCount ?? 0,
|
||||
responseTimeAvg: result?.totals?.responseTimeAvg ?? 0,
|
||||
responseTimeMedian: result?.totals?.responseTimeMedian ?? 0,
|
||||
responseTime90th: result?.totals?.responseTime90th ?? 0,
|
||||
responseTime99th: result?.totals?.responseTime99th ?? 0,
|
||||
},
|
||||
min: {
|
||||
queryCount: result?.min?.queryCount ?? 0,
|
||||
uncachedCount: result?.min?.uncachedCount ?? 0,
|
||||
staleCount: result?.min?.staleCount ?? 0,
|
||||
responseTimeAvg: result?.min?.responseTimeAvg ?? 0,
|
||||
responseTimeMedian: result?.min?.responseTimeMedian ?? 0,
|
||||
responseTime90th: result?.min?.responseTime90th ?? 0,
|
||||
responseTime99th: result?.min?.responseTime99th ?? 0,
|
||||
},
|
||||
max: {
|
||||
queryCount: result?.max?.queryCount ?? 0,
|
||||
uncachedCount: result?.max?.uncachedCount ?? 0,
|
||||
staleCount: result?.max?.staleCount ?? 0,
|
||||
responseTimeAvg: result?.max?.responseTimeAvg ?? 0,
|
||||
responseTimeMedian: result?.max?.responseTimeMedian ?? 0,
|
||||
responseTime90th: result?.max?.responseTime90th ?? 0,
|
||||
responseTime99th: result?.max?.responseTime99th ?? 0,
|
||||
},
|
||||
data:
|
||||
result?.data?.map((entry: any) => ({
|
||||
dimensions: entry.dimensions ?? [],
|
||||
metrics: entry.metrics ?? [],
|
||||
})) ?? [],
|
||||
data_lag: result?.data_lag ?? 0,
|
||||
rows: result?.rows ?? 0,
|
||||
query: {
|
||||
since: result?.query?.since ?? '',
|
||||
until: result?.query?.until ?? '',
|
||||
metrics: result?.query?.metrics ?? [],
|
||||
dimensions: result?.query?.dimensions ?? [],
|
||||
filters: result?.query?.filters ?? '',
|
||||
sort: result?.query?.sort ?? [],
|
||||
limit: result?.query?.limit ?? 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
totals: {
|
||||
type: 'object',
|
||||
description: 'Aggregate DNS analytics totals for the entire queried period',
|
||||
properties: {
|
||||
queryCount: { type: 'number', description: 'Total number of DNS queries' },
|
||||
uncachedCount: { type: 'number', description: 'Number of uncached DNS queries' },
|
||||
staleCount: { type: 'number', description: 'Number of stale DNS queries' },
|
||||
responseTimeAvg: {
|
||||
type: 'number',
|
||||
description: 'Average response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTimeMedian: {
|
||||
type: 'number',
|
||||
description: 'Median response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTime90th: {
|
||||
type: 'number',
|
||||
description: '90th percentile response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTime99th: {
|
||||
type: 'number',
|
||||
description: '99th percentile response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
min: {
|
||||
type: 'object',
|
||||
description: 'Minimum values across the analytics period',
|
||||
optional: true,
|
||||
properties: {
|
||||
queryCount: { type: 'number', description: 'Minimum number of DNS queries' },
|
||||
uncachedCount: { type: 'number', description: 'Minimum number of uncached DNS queries' },
|
||||
staleCount: { type: 'number', description: 'Minimum number of stale DNS queries' },
|
||||
responseTimeAvg: {
|
||||
type: 'number',
|
||||
description: 'Minimum average response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTimeMedian: {
|
||||
type: 'number',
|
||||
description: 'Minimum median response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTime90th: {
|
||||
type: 'number',
|
||||
description: 'Minimum 90th percentile response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTime99th: {
|
||||
type: 'number',
|
||||
description: 'Minimum 99th percentile response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
max: {
|
||||
type: 'object',
|
||||
description: 'Maximum values across the analytics period',
|
||||
optional: true,
|
||||
properties: {
|
||||
queryCount: { type: 'number', description: 'Maximum number of DNS queries' },
|
||||
uncachedCount: { type: 'number', description: 'Maximum number of uncached DNS queries' },
|
||||
staleCount: { type: 'number', description: 'Maximum number of stale DNS queries' },
|
||||
responseTimeAvg: {
|
||||
type: 'number',
|
||||
description: 'Maximum average response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTimeMedian: {
|
||||
type: 'number',
|
||||
description: 'Maximum median response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTime90th: {
|
||||
type: 'number',
|
||||
description: 'Maximum 90th percentile response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
responseTime99th: {
|
||||
type: 'number',
|
||||
description: 'Maximum 99th percentile response time in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
type: 'array',
|
||||
description: 'Raw analytics data rows returned by the Cloudflare DNS analytics report',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
dimensions: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Dimension values for this data row, parallel to the requested dimensions list',
|
||||
items: { type: 'string', description: 'Dimension value' },
|
||||
},
|
||||
metrics: {
|
||||
type: 'array',
|
||||
description: 'Metric values for this data row, parallel to the requested metrics list',
|
||||
items: { type: 'number', description: 'Metric value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data_lag: {
|
||||
type: 'number',
|
||||
description: 'Processing lag in seconds before analytics data becomes available',
|
||||
},
|
||||
rows: {
|
||||
type: 'number',
|
||||
description: 'Total number of rows in the result set',
|
||||
},
|
||||
query: {
|
||||
type: 'object',
|
||||
description: 'Echo of the query parameters sent to the API',
|
||||
optional: true,
|
||||
properties: {
|
||||
since: { type: 'string', description: 'Start date of the analytics query' },
|
||||
until: { type: 'string', description: 'End date of the analytics query' },
|
||||
metrics: {
|
||||
type: 'array',
|
||||
description: 'Metrics requested in the query',
|
||||
items: { type: 'string', description: 'Metric name' },
|
||||
},
|
||||
dimensions: {
|
||||
type: 'array',
|
||||
description: 'Dimensions requested in the query',
|
||||
items: { type: 'string', description: 'Dimension name' },
|
||||
},
|
||||
filters: { type: 'string', description: 'Filters applied to the query' },
|
||||
sort: {
|
||||
type: 'array',
|
||||
description: 'Sort order applied to the query',
|
||||
items: { type: 'string', description: 'Sort field with direction prefix' },
|
||||
},
|
||||
limit: { type: 'number', description: 'Maximum number of results requested' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import type { CloudflareGetZoneParams, CloudflareGetZoneResponse } from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getZoneTool: ToolConfig<CloudflareGetZoneParams, CloudflareGetZoneResponse> = {
|
||||
id: 'cloudflare_get_zone',
|
||||
name: 'Cloudflare Get Zone',
|
||||
description: 'Gets details for a specific zone (domain) by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to retrieve details for',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
name: '',
|
||||
status: '',
|
||||
paused: false,
|
||||
type: '',
|
||||
name_servers: [],
|
||||
original_name_servers: [],
|
||||
created_on: '',
|
||||
modified_on: '',
|
||||
activated_on: '',
|
||||
development_mode: 0,
|
||||
plan: {
|
||||
id: '',
|
||||
name: '',
|
||||
price: 0,
|
||||
is_subscribed: false,
|
||||
frequency: '',
|
||||
currency: '',
|
||||
legacy_id: '',
|
||||
},
|
||||
account: { id: '', name: '' },
|
||||
owner: { id: '', name: '', type: '' },
|
||||
meta: {
|
||||
cdn_only: false,
|
||||
custom_certificate_quota: 0,
|
||||
dns_only: false,
|
||||
foundation_dns: false,
|
||||
page_rule_quota: 0,
|
||||
phishing_detected: false,
|
||||
step: 0,
|
||||
},
|
||||
vanity_name_servers: [],
|
||||
permissions: [],
|
||||
},
|
||||
error: data.errors?.[0]?.message ?? 'Failed to get zone',
|
||||
}
|
||||
}
|
||||
|
||||
const zone = data.result
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: zone?.id ?? '',
|
||||
name: zone?.name ?? '',
|
||||
status: zone?.status ?? '',
|
||||
paused: zone?.paused ?? false,
|
||||
type: zone?.type ?? '',
|
||||
name_servers: zone?.name_servers ?? [],
|
||||
original_name_servers: zone?.original_name_servers ?? [],
|
||||
created_on: zone?.created_on ?? '',
|
||||
modified_on: zone?.modified_on ?? '',
|
||||
activated_on: zone?.activated_on ?? '',
|
||||
development_mode: zone?.development_mode ?? 0,
|
||||
plan: {
|
||||
id: zone?.plan?.id ?? '',
|
||||
name: zone?.plan?.name ?? '',
|
||||
price: zone?.plan?.price ?? 0,
|
||||
is_subscribed: zone?.plan?.is_subscribed ?? false,
|
||||
frequency: zone?.plan?.frequency ?? '',
|
||||
currency: zone?.plan?.currency ?? '',
|
||||
legacy_id: zone?.plan?.legacy_id ?? '',
|
||||
},
|
||||
account: {
|
||||
id: zone?.account?.id ?? '',
|
||||
name: zone?.account?.name ?? '',
|
||||
},
|
||||
owner: {
|
||||
id: zone?.owner?.id ?? '',
|
||||
name: zone?.owner?.name ?? '',
|
||||
type: zone?.owner?.type ?? '',
|
||||
},
|
||||
meta: {
|
||||
cdn_only: zone?.meta?.cdn_only ?? false,
|
||||
custom_certificate_quota: zone?.meta?.custom_certificate_quota ?? 0,
|
||||
dns_only: zone?.meta?.dns_only ?? false,
|
||||
foundation_dns: zone?.meta?.foundation_dns ?? false,
|
||||
page_rule_quota: zone?.meta?.page_rule_quota ?? 0,
|
||||
phishing_detected: zone?.meta?.phishing_detected ?? false,
|
||||
step: zone?.meta?.step ?? 0,
|
||||
},
|
||||
vanity_name_servers: zone?.vanity_name_servers ?? [],
|
||||
permissions: zone?.permissions ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Zone ID' },
|
||||
name: { type: 'string', description: 'Domain name' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Zone status (initializing, pending, active, moved)',
|
||||
},
|
||||
paused: { type: 'boolean', description: 'Whether the zone is paused' },
|
||||
type: { type: 'string', description: 'Zone type (full, partial, or secondary)' },
|
||||
name_servers: {
|
||||
type: 'array',
|
||||
description: 'Assigned Cloudflare name servers',
|
||||
items: { type: 'string', description: 'Name server hostname' },
|
||||
},
|
||||
original_name_servers: {
|
||||
type: 'array',
|
||||
description: 'Original name servers before moving to Cloudflare',
|
||||
items: { type: 'string', description: 'Name server hostname' },
|
||||
optional: true,
|
||||
},
|
||||
created_on: { type: 'string', description: 'ISO 8601 date when the zone was created' },
|
||||
modified_on: { type: 'string', description: 'ISO 8601 date when the zone was last modified' },
|
||||
activated_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when the zone was activated',
|
||||
optional: true,
|
||||
},
|
||||
development_mode: {
|
||||
type: 'number',
|
||||
description: 'Seconds remaining in development mode (0 = off)',
|
||||
},
|
||||
plan: {
|
||||
type: 'object',
|
||||
description: 'Zone plan information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Plan identifier' },
|
||||
name: { type: 'string', description: 'Plan name' },
|
||||
price: { type: 'number', description: 'Plan price' },
|
||||
is_subscribed: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the zone is subscribed to the plan',
|
||||
},
|
||||
frequency: { type: 'string', description: 'Plan billing frequency' },
|
||||
currency: { type: 'string', description: 'Plan currency' },
|
||||
legacy_id: { type: 'string', description: 'Legacy plan identifier' },
|
||||
},
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
description: 'Account the zone belongs to',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Account identifier' },
|
||||
name: { type: 'string', description: 'Account name' },
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Zone owner information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Owner identifier' },
|
||||
name: { type: 'string', description: 'Owner name' },
|
||||
type: { type: 'string', description: 'Owner type' },
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Zone metadata',
|
||||
properties: {
|
||||
cdn_only: { type: 'boolean', description: 'Whether the zone is CDN only' },
|
||||
custom_certificate_quota: { type: 'number', description: 'Custom certificate quota' },
|
||||
dns_only: { type: 'boolean', description: 'Whether the zone is DNS only' },
|
||||
foundation_dns: { type: 'boolean', description: 'Whether foundation DNS is enabled' },
|
||||
page_rule_quota: { type: 'number', description: 'Page rule quota' },
|
||||
phishing_detected: { type: 'boolean', description: 'Whether phishing was detected' },
|
||||
step: { type: 'number', description: 'Current setup step' },
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
vanity_name_servers: {
|
||||
type: 'array',
|
||||
description: 'Custom vanity name servers',
|
||||
items: { type: 'string', description: 'Vanity name server hostname' },
|
||||
optional: true,
|
||||
},
|
||||
permissions: {
|
||||
type: 'array',
|
||||
description: 'User permissions for the zone',
|
||||
items: { type: 'string', description: 'Permission string' },
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type {
|
||||
CloudflareGetZoneSettingsParams,
|
||||
CloudflareGetZoneSettingsResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getZoneSettingsTool: ToolConfig<
|
||||
CloudflareGetZoneSettingsParams,
|
||||
CloudflareGetZoneSettingsResponse
|
||||
> = {
|
||||
id: 'cloudflare_get_zone_settings',
|
||||
name: 'Cloudflare Get Zone Settings',
|
||||
description:
|
||||
'Gets all settings for a zone including SSL mode, minification, caching level, and security settings.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to get settings for',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}/settings`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { settings: [] },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to get zone settings',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
settings:
|
||||
data.result?.map((setting: Record<string, unknown>) => ({
|
||||
id: (setting.id as string) ?? '',
|
||||
value:
|
||||
typeof setting.value === 'object' && setting.value !== null
|
||||
? JSON.stringify(setting.value)
|
||||
: String(setting.value ?? ''),
|
||||
editable: (setting.editable as boolean) ?? false,
|
||||
modified_on: (setting.modified_on as string) ?? '',
|
||||
...(setting.time_remaining != null
|
||||
? { time_remaining: setting.time_remaining as number }
|
||||
: {}),
|
||||
})) ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
settings: {
|
||||
type: 'array',
|
||||
description: 'List of zone settings',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Setting identifier (e.g., ssl, minify, cache_level, security_level, always_use_https)',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Setting value as a string. Simple values returned as-is (e.g., "full", "on"). Complex values are JSON-stringified (e.g., \'{"css":"on","html":"on","js":"on"}\').',
|
||||
},
|
||||
editable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the setting can be modified for the current zone plan',
|
||||
},
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the setting was last modified',
|
||||
},
|
||||
time_remaining: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Seconds remaining until the setting can be modified again (only present for rate-limited settings)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { createDnsRecordTool } from '@/tools/cloudflare/create_dns_record'
|
||||
import { createZoneTool } from '@/tools/cloudflare/create_zone'
|
||||
import { deleteDnsRecordTool } from '@/tools/cloudflare/delete_dns_record'
|
||||
import { deleteZoneTool } from '@/tools/cloudflare/delete_zone'
|
||||
import { dnsAnalyticsTool } from '@/tools/cloudflare/dns_analytics'
|
||||
import { getZoneTool } from '@/tools/cloudflare/get_zone'
|
||||
import { getZoneSettingsTool } from '@/tools/cloudflare/get_zone_settings'
|
||||
import { listCertificatesTool } from '@/tools/cloudflare/list_certificates'
|
||||
import { listDnsRecordsTool } from '@/tools/cloudflare/list_dns_records'
|
||||
import { listZonesTool } from '@/tools/cloudflare/list_zones'
|
||||
import { purgeCacheTool } from '@/tools/cloudflare/purge_cache'
|
||||
import { updateDnsRecordTool } from '@/tools/cloudflare/update_dns_record'
|
||||
import { updateZoneSettingTool } from '@/tools/cloudflare/update_zone_setting'
|
||||
|
||||
export const cloudflareCreateDnsRecordTool = createDnsRecordTool
|
||||
export const cloudflareCreateZoneTool = createZoneTool
|
||||
export const cloudflareDeleteDnsRecordTool = deleteDnsRecordTool
|
||||
export const cloudflareDeleteZoneTool = deleteZoneTool
|
||||
export const cloudflareDnsAnalyticsTool = dnsAnalyticsTool
|
||||
export const cloudflareGetZoneTool = getZoneTool
|
||||
export const cloudflareGetZoneSettingsTool = getZoneSettingsTool
|
||||
export const cloudflareListCertificatesTool = listCertificatesTool
|
||||
export const cloudflareListDnsRecordsTool = listDnsRecordsTool
|
||||
export const cloudflareListZonesTool = listZonesTool
|
||||
export const cloudflarePurgeCacheTool = purgeCacheTool
|
||||
export const cloudflareUpdateDnsRecordTool = updateDnsRecordTool
|
||||
export const cloudflareUpdateZoneSettingTool = updateZoneSettingTool
|
||||
@@ -1,302 +0,0 @@
|
||||
import type {
|
||||
CloudflareListCertificatesParams,
|
||||
CloudflareListCertificatesResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listCertificatesTool: ToolConfig<
|
||||
CloudflareListCertificatesParams,
|
||||
CloudflareListCertificatesResponse
|
||||
> = {
|
||||
id: 'cloudflare_list_certificates',
|
||||
name: 'Cloudflare List Certificates',
|
||||
description: 'Lists SSL/TLS certificate packs for a zone.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to list certificates for',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter certificate packs by status (e.g., "all", "active", "pending")',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number of paginated results (default: 1)',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of certificate packs per page (default: 20, min: 5, max: 50)',
|
||||
},
|
||||
deploy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by deployment environment: "staging" or "production"',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/ssl/certificate_packs`
|
||||
)
|
||||
if (params.status) url.searchParams.append('status', params.status)
|
||||
if (params.page) url.searchParams.append('page', String(params.page))
|
||||
if (params.per_page) url.searchParams.append('per_page', String(params.per_page))
|
||||
if (params.deploy) url.searchParams.append('deploy', params.deploy)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { certificates: [], total_count: 0 },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to list certificates',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
certificates:
|
||||
data.result?.map((cert: any) => ({
|
||||
id: cert.id ?? '',
|
||||
type: cert.type ?? '',
|
||||
hosts: cert.hosts ?? [],
|
||||
primary_certificate: cert.primary_certificate ?? '',
|
||||
status: cert.status ?? '',
|
||||
certificates:
|
||||
cert.certificates?.map((c: any) => ({
|
||||
id: c.id ?? '',
|
||||
hosts: c.hosts ?? [],
|
||||
issuer: c.issuer ?? '',
|
||||
signature: c.signature ?? '',
|
||||
status: c.status ?? '',
|
||||
bundle_method: c.bundle_method ?? '',
|
||||
zone_id: c.zone_id ?? '',
|
||||
uploaded_on: c.uploaded_on ?? '',
|
||||
modified_on: c.modified_on ?? '',
|
||||
expires_on: c.expires_on ?? '',
|
||||
priority: c.priority ?? 0,
|
||||
geo_restrictions: c.geo_restrictions ?? undefined,
|
||||
})) ?? [],
|
||||
cloudflare_branding: cert.cloudflare_branding ?? false,
|
||||
validation_method: cert.validation_method ?? '',
|
||||
validity_days: cert.validity_days ?? 0,
|
||||
certificate_authority: cert.certificate_authority ?? '',
|
||||
validation_errors:
|
||||
cert.validation_errors?.map((e: any) => ({
|
||||
message: e.message ?? '',
|
||||
})) ?? [],
|
||||
validation_records:
|
||||
cert.validation_records?.map((r: any) => ({
|
||||
cname: r.cname ?? '',
|
||||
cname_target: r.cname_target ?? '',
|
||||
emails: r.emails ?? [],
|
||||
http_body: r.http_body ?? '',
|
||||
http_url: r.http_url ?? '',
|
||||
status: r.status ?? '',
|
||||
txt_name: r.txt_name ?? '',
|
||||
txt_value: r.txt_value ?? '',
|
||||
})) ?? [],
|
||||
dcv_delegation_records:
|
||||
cert.dcv_delegation_records?.map((r: any) => ({
|
||||
cname: r.cname ?? '',
|
||||
cname_target: r.cname_target ?? '',
|
||||
emails: r.emails ?? [],
|
||||
http_body: r.http_body ?? '',
|
||||
http_url: r.http_url ?? '',
|
||||
status: r.status ?? '',
|
||||
txt_name: r.txt_name ?? '',
|
||||
txt_value: r.txt_value ?? '',
|
||||
})) ?? [],
|
||||
})) ?? [],
|
||||
total_count: data.result_info?.total_count ?? data.result?.length ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
certificates: {
|
||||
type: 'array',
|
||||
description: 'List of SSL/TLS certificate packs',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Certificate pack ID' },
|
||||
type: { type: 'string', description: 'Certificate type (e.g., "universal", "advanced")' },
|
||||
hosts: {
|
||||
type: 'array',
|
||||
description: 'Hostnames covered by this certificate pack',
|
||||
items: {
|
||||
type: 'string',
|
||||
description: 'Hostname',
|
||||
},
|
||||
},
|
||||
primary_certificate: {
|
||||
type: 'string',
|
||||
description: 'ID of the primary certificate in the pack',
|
||||
optional: true,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Certificate pack status (e.g., "active", "pending")',
|
||||
},
|
||||
certificates: {
|
||||
type: 'array',
|
||||
description: 'Individual certificates within the pack',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Certificate ID' },
|
||||
hosts: {
|
||||
type: 'array',
|
||||
description: 'Hostnames covered by this certificate',
|
||||
items: { type: 'string', description: 'Hostname' },
|
||||
},
|
||||
issuer: { type: 'string', description: 'Certificate issuer' },
|
||||
signature: {
|
||||
type: 'string',
|
||||
description: 'Signature algorithm (e.g., "ECDSAWithSHA256")',
|
||||
},
|
||||
status: { type: 'string', description: 'Certificate status' },
|
||||
bundle_method: {
|
||||
type: 'string',
|
||||
description: 'Bundle method (e.g., "ubiquitous")',
|
||||
},
|
||||
zone_id: { type: 'string', description: 'Zone ID the certificate belongs to' },
|
||||
uploaded_on: { type: 'string', description: 'Upload date (ISO 8601)' },
|
||||
modified_on: { type: 'string', description: 'Last modified date (ISO 8601)' },
|
||||
expires_on: { type: 'string', description: 'Expiration date (ISO 8601)' },
|
||||
priority: {
|
||||
type: 'number',
|
||||
description: 'Certificate priority order',
|
||||
optional: true,
|
||||
},
|
||||
geo_restrictions: {
|
||||
type: 'object',
|
||||
description: 'Geographic restrictions for the certificate',
|
||||
optional: true,
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
description: 'Geographic restriction label',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cloudflare_branding: {
|
||||
type: 'boolean',
|
||||
description: 'Whether Cloudflare branding is enabled on the certificate',
|
||||
optional: true,
|
||||
},
|
||||
validation_method: {
|
||||
type: 'string',
|
||||
description: 'Validation method (e.g., "txt", "http", "cname")',
|
||||
optional: true,
|
||||
},
|
||||
validity_days: {
|
||||
type: 'number',
|
||||
description: 'Validity period in days',
|
||||
optional: true,
|
||||
},
|
||||
certificate_authority: {
|
||||
type: 'string',
|
||||
description: 'Certificate authority (e.g., "lets_encrypt", "google")',
|
||||
optional: true,
|
||||
},
|
||||
validation_errors: {
|
||||
type: 'array',
|
||||
description: 'Validation issues for the certificate pack',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Validation error message',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validation_records: {
|
||||
type: 'array',
|
||||
description: 'Validation records for the certificate pack',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cname: { type: 'string', description: 'CNAME record name' },
|
||||
cname_target: { type: 'string', description: 'CNAME record target' },
|
||||
emails: {
|
||||
type: 'array',
|
||||
description: 'Email addresses for validation',
|
||||
items: { type: 'string', description: 'Email address' },
|
||||
},
|
||||
http_body: { type: 'string', description: 'HTTP validation body content' },
|
||||
http_url: { type: 'string', description: 'HTTP validation URL' },
|
||||
status: { type: 'string', description: 'Validation record status' },
|
||||
txt_name: { type: 'string', description: 'TXT record name' },
|
||||
txt_value: { type: 'string', description: 'TXT record value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
dcv_delegation_records: {
|
||||
type: 'array',
|
||||
description: 'Domain control validation delegation records',
|
||||
optional: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cname: { type: 'string', description: 'CNAME record name' },
|
||||
cname_target: { type: 'string', description: 'CNAME record target' },
|
||||
emails: {
|
||||
type: 'array',
|
||||
description: 'Email addresses for validation',
|
||||
items: { type: 'string', description: 'Email address' },
|
||||
},
|
||||
http_body: { type: 'string', description: 'HTTP validation body content' },
|
||||
http_url: { type: 'string', description: 'HTTP validation URL' },
|
||||
status: { type: 'string', description: 'Delegation record status' },
|
||||
txt_name: { type: 'string', description: 'TXT record name' },
|
||||
txt_value: { type: 'string', description: 'TXT record value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total_count: {
|
||||
type: 'number',
|
||||
description: 'Total number of certificate packs',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import type {
|
||||
CloudflareListDnsRecordsParams,
|
||||
CloudflareListDnsRecordsResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listDnsRecordsTool: ToolConfig<
|
||||
CloudflareListDnsRecordsParams,
|
||||
CloudflareListDnsRecordsResponse
|
||||
> = {
|
||||
id: 'cloudflare_list_dns_records',
|
||||
name: 'Cloudflare List DNS Records',
|
||||
description: 'Lists DNS records for a specific zone.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to list DNS records for',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by record type (e.g., "A", "AAAA", "CNAME", "MX", "TXT")',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by record name (exact match)',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by record content (exact match)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination (default: 1)',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of records per page (default: 100, max: 5000000)',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc or desc)',
|
||||
},
|
||||
match: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Match logic for filters: any or all (default: all)',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort field (type, name, content, ttl, proxied)',
|
||||
},
|
||||
proxied: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by proxy status',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Free-text search across record name, content, and value',
|
||||
},
|
||||
tag: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by tags (comma-separated)',
|
||||
},
|
||||
tag_match: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Tag filter match logic: any or all',
|
||||
},
|
||||
commentFilter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter records by comment content (substring match)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records`)
|
||||
if (params.type) url.searchParams.append('type', params.type)
|
||||
if (params.name) url.searchParams.append('name', params.name)
|
||||
if (params.content) url.searchParams.append('content', params.content)
|
||||
if (params.page) url.searchParams.append('page', String(params.page))
|
||||
if (params.per_page) url.searchParams.append('per_page', String(params.per_page))
|
||||
if (params.direction) url.searchParams.append('direction', params.direction)
|
||||
if (params.match) url.searchParams.append('match', params.match)
|
||||
if (params.order) url.searchParams.append('order', params.order)
|
||||
if (params.proxied !== undefined) url.searchParams.append('proxied', String(params.proxied))
|
||||
if (params.search) url.searchParams.append('search', params.search)
|
||||
if (params.tag) url.searchParams.append('tag', params.tag)
|
||||
if (params.tag_match) url.searchParams.append('tag_match', params.tag_match)
|
||||
if (params.commentFilter) url.searchParams.append('comment.contains', params.commentFilter)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { records: [], total_count: 0 },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to list DNS records',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records:
|
||||
data.result?.map((record: any) => ({
|
||||
id: record.id ?? '',
|
||||
zone_id: record.zone_id ?? '',
|
||||
zone_name: record.zone_name ?? '',
|
||||
type: record.type ?? '',
|
||||
name: record.name ?? '',
|
||||
content: record.content ?? '',
|
||||
proxiable: record.proxiable ?? false,
|
||||
proxied: record.proxied ?? false,
|
||||
ttl: record.ttl ?? 0,
|
||||
locked: record.locked ?? false,
|
||||
priority: record.priority ?? null,
|
||||
comment: record.comment ?? null,
|
||||
tags: record.tags ?? [],
|
||||
comment_modified_on: record.comment_modified_on ?? null,
|
||||
tags_modified_on: record.tags_modified_on ?? null,
|
||||
meta: record.meta ?? null,
|
||||
created_on: record.created_on ?? '',
|
||||
modified_on: record.modified_on ?? '',
|
||||
})) ?? [],
|
||||
total_count: data.result_info?.total_count ?? data.result?.length ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'List of DNS records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Unique identifier for the DNS record' },
|
||||
zone_id: { type: 'string', description: 'The ID of the zone the record belongs to' },
|
||||
zone_name: { type: 'string', description: 'The name of the zone' },
|
||||
type: { type: 'string', description: 'Record type (A, AAAA, CNAME, MX, TXT, etc.)' },
|
||||
name: { type: 'string', description: 'Record name (e.g., example.com)' },
|
||||
content: { type: 'string', description: 'Record content (e.g., IP address)' },
|
||||
proxiable: { type: 'boolean', description: 'Whether the record can be proxied' },
|
||||
proxied: { type: 'boolean', description: 'Whether Cloudflare proxy is enabled' },
|
||||
ttl: { type: 'number', description: 'TTL in seconds (1 = automatic)' },
|
||||
locked: { type: 'boolean', description: 'Whether the record is locked' },
|
||||
priority: { type: 'number', description: 'MX/SRV record priority', optional: true },
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Comment associated with the record',
|
||||
optional: true,
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Tags associated with the record',
|
||||
items: { type: 'string', description: 'Tag value' },
|
||||
},
|
||||
comment_modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the comment was last modified',
|
||||
optional: true,
|
||||
},
|
||||
tags_modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when tags were last modified',
|
||||
optional: true,
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Record metadata',
|
||||
optional: true,
|
||||
properties: {
|
||||
source: { type: 'string', description: 'Source of the DNS record' },
|
||||
},
|
||||
},
|
||||
created_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the record was created',
|
||||
},
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the record was last modified',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total_count: {
|
||||
type: 'number',
|
||||
description: 'Total number of DNS records matching the query',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import type {
|
||||
CloudflareListZonesParams,
|
||||
CloudflareListZonesResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listZonesTool: ToolConfig<CloudflareListZonesParams, CloudflareListZonesResponse> = {
|
||||
id: 'cloudflare_list_zones',
|
||||
name: 'Cloudflare List Zones',
|
||||
description: 'Lists all zones (domains) in the Cloudflare account.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter zones by domain name (e.g., "example.com")',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by zone status: "initializing", "pending", "active", or "moved"',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page number for pagination (default: 1)',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of zones per page (default: 20, max: 50)',
|
||||
},
|
||||
accountId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter zones by account ID',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort field (name, status, account.id, account.name)',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction (asc, desc)',
|
||||
},
|
||||
match: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Match logic for filters (any, all). Default: all',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.cloudflare.com/client/v4/zones')
|
||||
if (params.name) url.searchParams.append('name', params.name)
|
||||
if (params.status) url.searchParams.append('status', params.status)
|
||||
if (params.page) url.searchParams.append('page', String(params.page))
|
||||
if (params.per_page) url.searchParams.append('per_page', String(params.per_page))
|
||||
if (params.accountId) url.searchParams.append('account.id', params.accountId)
|
||||
if (params.order) url.searchParams.append('order', params.order)
|
||||
if (params.direction) url.searchParams.append('direction', params.direction)
|
||||
if (params.match) url.searchParams.append('match', params.match)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { zones: [], total_count: 0 },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to list zones',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
zones:
|
||||
data.result?.map((zone: any) => ({
|
||||
id: zone.id ?? '',
|
||||
name: zone.name ?? '',
|
||||
status: zone.status ?? '',
|
||||
paused: zone.paused ?? false,
|
||||
type: zone.type ?? '',
|
||||
name_servers: zone.name_servers ?? [],
|
||||
original_name_servers: zone.original_name_servers ?? [],
|
||||
created_on: zone.created_on ?? '',
|
||||
modified_on: zone.modified_on ?? '',
|
||||
activated_on: zone.activated_on ?? '',
|
||||
development_mode: zone.development_mode ?? 0,
|
||||
plan: {
|
||||
id: zone.plan?.id ?? '',
|
||||
name: zone.plan?.name ?? '',
|
||||
price: zone.plan?.price ?? 0,
|
||||
is_subscribed: zone.plan?.is_subscribed ?? false,
|
||||
frequency: zone.plan?.frequency ?? '',
|
||||
currency: zone.plan?.currency ?? '',
|
||||
legacy_id: zone.plan?.legacy_id ?? '',
|
||||
},
|
||||
account: {
|
||||
id: zone.account?.id ?? '',
|
||||
name: zone.account?.name ?? '',
|
||||
},
|
||||
owner: {
|
||||
id: zone.owner?.id ?? '',
|
||||
name: zone.owner?.name ?? '',
|
||||
type: zone.owner?.type ?? '',
|
||||
},
|
||||
meta: {
|
||||
cdn_only: zone.meta?.cdn_only ?? false,
|
||||
custom_certificate_quota: zone.meta?.custom_certificate_quota ?? 0,
|
||||
dns_only: zone.meta?.dns_only ?? false,
|
||||
foundation_dns: zone.meta?.foundation_dns ?? false,
|
||||
page_rule_quota: zone.meta?.page_rule_quota ?? 0,
|
||||
phishing_detected: zone.meta?.phishing_detected ?? false,
|
||||
step: zone.meta?.step ?? 0,
|
||||
},
|
||||
vanity_name_servers: zone.vanity_name_servers ?? [],
|
||||
permissions: zone.permissions ?? [],
|
||||
})) ?? [],
|
||||
total_count: data.result_info?.total_count ?? data.result?.length ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
zones: {
|
||||
type: 'array',
|
||||
description: 'List of zones/domains',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Zone ID' },
|
||||
name: { type: 'string', description: 'Domain name' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Zone status (initializing, pending, active, moved)',
|
||||
},
|
||||
paused: { type: 'boolean', description: 'Whether the zone is paused' },
|
||||
type: { type: 'string', description: 'Zone type (full, partial, or secondary)' },
|
||||
name_servers: {
|
||||
type: 'array',
|
||||
description: 'Assigned Cloudflare name servers',
|
||||
items: { type: 'string', description: 'Name server hostname' },
|
||||
},
|
||||
original_name_servers: {
|
||||
type: 'array',
|
||||
description: 'Original name servers before moving to Cloudflare',
|
||||
items: { type: 'string', description: 'Name server hostname' },
|
||||
optional: true,
|
||||
},
|
||||
created_on: { type: 'string', description: 'ISO 8601 date when the zone was created' },
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when the zone was last modified',
|
||||
},
|
||||
activated_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 date when the zone was activated',
|
||||
optional: true,
|
||||
},
|
||||
development_mode: {
|
||||
type: 'number',
|
||||
description: 'Seconds remaining in development mode (0 = off)',
|
||||
},
|
||||
plan: {
|
||||
type: 'object',
|
||||
description: 'Zone plan information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Plan identifier' },
|
||||
name: { type: 'string', description: 'Plan name' },
|
||||
price: { type: 'number', description: 'Plan price' },
|
||||
is_subscribed: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the zone is subscribed to the plan',
|
||||
},
|
||||
frequency: { type: 'string', description: 'Plan billing frequency' },
|
||||
currency: { type: 'string', description: 'Plan currency' },
|
||||
legacy_id: { type: 'string', description: 'Legacy plan identifier' },
|
||||
},
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
description: 'Account the zone belongs to',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Account identifier' },
|
||||
name: { type: 'string', description: 'Account name' },
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Zone owner information',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Owner identifier' },
|
||||
name: { type: 'string', description: 'Owner name' },
|
||||
type: { type: 'string', description: 'Owner type' },
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Zone metadata',
|
||||
properties: {
|
||||
cdn_only: { type: 'boolean', description: 'Whether the zone is CDN only' },
|
||||
custom_certificate_quota: { type: 'number', description: 'Custom certificate quota' },
|
||||
dns_only: { type: 'boolean', description: 'Whether the zone is DNS only' },
|
||||
foundation_dns: { type: 'boolean', description: 'Whether foundation DNS is enabled' },
|
||||
page_rule_quota: { type: 'number', description: 'Page rule quota' },
|
||||
phishing_detected: { type: 'boolean', description: 'Whether phishing was detected' },
|
||||
step: { type: 'number', description: 'Current setup step' },
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
vanity_name_servers: {
|
||||
type: 'array',
|
||||
description: 'Custom vanity name servers',
|
||||
items: { type: 'string', description: 'Vanity name server hostname' },
|
||||
optional: true,
|
||||
},
|
||||
permissions: {
|
||||
type: 'array',
|
||||
description: 'User permissions for the zone',
|
||||
items: { type: 'string', description: 'Permission string' },
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
total_count: {
|
||||
type: 'number',
|
||||
description: 'Total number of zones matching the query',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import type {
|
||||
CloudflarePurgeCacheParams,
|
||||
CloudflarePurgeCacheResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const purgeCacheTool: ToolConfig<CloudflarePurgeCacheParams, CloudflarePurgeCacheResponse> =
|
||||
{
|
||||
id: 'cloudflare_purge_cache',
|
||||
name: 'Cloudflare Purge Cache',
|
||||
description:
|
||||
'Purges cached content for a zone. Can purge everything or specific files/tags/hosts/prefixes.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to purge cache for',
|
||||
},
|
||||
purge_everything: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Set to true to purge all cached content. Mutually exclusive with files, tags, hosts, and prefixes',
|
||||
},
|
||||
files: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of URLs to purge from cache',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of cache tags to purge (Enterprise only)',
|
||||
},
|
||||
hosts: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of hostnames to purge (Enterprise only)',
|
||||
},
|
||||
prefixes: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of URL prefixes to purge (Enterprise only)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.cloudflare.com/client/v4/zones/${params.zoneId}/purge_cache`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
if (params.purge_everything) {
|
||||
return { purge_everything: true }
|
||||
}
|
||||
|
||||
const body: Record<string, string[]> = {}
|
||||
if (params.files) {
|
||||
const fileList = String(params.files)
|
||||
.split(',')
|
||||
.map((f) => f.trim())
|
||||
.filter(Boolean)
|
||||
if (fileList.length > 0) body.files = fileList
|
||||
}
|
||||
if (params.tags) {
|
||||
const tagList = String(params.tags)
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
if (tagList.length > 0) body.tags = tagList
|
||||
}
|
||||
if (params.hosts) {
|
||||
const hostList = String(params.hosts)
|
||||
.split(',')
|
||||
.map((h) => h.trim())
|
||||
.filter(Boolean)
|
||||
if (hostList.length > 0) body.hosts = hostList
|
||||
}
|
||||
if (params.prefixes) {
|
||||
const prefixList = String(params.prefixes)
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
if (prefixList.length > 0) body.prefixes = prefixList
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
throw new Error(
|
||||
'No purge targets specified. Provide at least one of: files, tags, hosts, or prefixes, or set purge_everything to true.'
|
||||
)
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { id: '' },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to purge cache',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.result?.id ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Purge request identifier returned by Cloudflare' },
|
||||
},
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface CloudflareBaseParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface CloudflareListZonesParams extends CloudflareBaseParams {
|
||||
name?: string
|
||||
status?: string
|
||||
page?: number
|
||||
per_page?: number
|
||||
accountId?: string
|
||||
order?: string
|
||||
direction?: string
|
||||
match?: string
|
||||
}
|
||||
|
||||
export interface CloudflareZonePlan {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
is_subscribed: boolean
|
||||
frequency: string
|
||||
currency: string
|
||||
legacy_id: string
|
||||
}
|
||||
|
||||
export interface CloudflareZoneMeta {
|
||||
cdn_only: boolean
|
||||
custom_certificate_quota: number
|
||||
dns_only: boolean
|
||||
foundation_dns: boolean
|
||||
page_rule_quota: number
|
||||
phishing_detected: boolean
|
||||
step: number
|
||||
}
|
||||
|
||||
export interface CloudflareZone {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
paused: boolean
|
||||
type: string
|
||||
name_servers: string[]
|
||||
original_name_servers?: string[]
|
||||
created_on: string
|
||||
modified_on: string
|
||||
activated_on?: string
|
||||
development_mode?: number
|
||||
plan?: CloudflareZonePlan
|
||||
account?: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
owner?: {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
meta?: CloudflareZoneMeta
|
||||
vanity_name_servers?: string[]
|
||||
permissions?: string[]
|
||||
}
|
||||
|
||||
export interface CloudflareListZonesResponse extends ToolResponse {
|
||||
output: {
|
||||
zones: CloudflareZone[]
|
||||
total_count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareGetZoneParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
}
|
||||
|
||||
export interface CloudflareGetZoneResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
paused: boolean
|
||||
type: string
|
||||
name_servers: string[]
|
||||
original_name_servers: string[]
|
||||
created_on: string
|
||||
modified_on: string
|
||||
activated_on: string
|
||||
development_mode: number
|
||||
plan: CloudflareZonePlan
|
||||
account: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
owner: {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
meta: CloudflareZoneMeta
|
||||
vanity_name_servers: string[]
|
||||
permissions: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareCreateZoneParams extends CloudflareBaseParams {
|
||||
name: string
|
||||
accountId: string
|
||||
type?: string
|
||||
jump_start?: boolean
|
||||
}
|
||||
|
||||
export interface CloudflareCreateZoneResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
paused: boolean
|
||||
type: string
|
||||
name_servers: string[]
|
||||
original_name_servers: string[]
|
||||
created_on: string
|
||||
modified_on: string
|
||||
activated_on: string
|
||||
development_mode: number
|
||||
plan: CloudflareZonePlan
|
||||
account: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
owner: {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
meta: CloudflareZoneMeta
|
||||
vanity_name_servers: string[]
|
||||
permissions: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareDeleteZoneParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
}
|
||||
|
||||
export interface CloudflareDeleteZoneResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareListDnsRecordsParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
type?: string
|
||||
name?: string
|
||||
content?: string
|
||||
page?: number
|
||||
per_page?: number
|
||||
direction?: string
|
||||
match?: string
|
||||
order?: string
|
||||
proxied?: boolean
|
||||
search?: string
|
||||
tag?: string
|
||||
tag_match?: string
|
||||
commentFilter?: string
|
||||
}
|
||||
|
||||
export interface CloudflareDnsRecordMeta {
|
||||
source: string
|
||||
}
|
||||
|
||||
export interface CloudflareDnsRecord {
|
||||
id: string
|
||||
zone_id: string
|
||||
zone_name: string
|
||||
type: string
|
||||
name: string
|
||||
content: string
|
||||
proxiable: boolean
|
||||
proxied: boolean
|
||||
ttl: number
|
||||
locked: boolean
|
||||
priority?: number
|
||||
comment?: string | null
|
||||
tags: string[]
|
||||
comment_modified_on?: string | null
|
||||
tags_modified_on?: string | null
|
||||
meta?: CloudflareDnsRecordMeta | null
|
||||
created_on: string
|
||||
modified_on: string
|
||||
}
|
||||
|
||||
export interface CloudflareListDnsRecordsResponse extends ToolResponse {
|
||||
output: {
|
||||
records: CloudflareDnsRecord[]
|
||||
total_count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareCreateDnsRecordParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
type: string
|
||||
name: string
|
||||
content: string
|
||||
ttl?: number
|
||||
proxied?: boolean
|
||||
priority?: number
|
||||
comment?: string
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export interface CloudflareCreateDnsRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
zone_id: string
|
||||
zone_name: string
|
||||
type: string
|
||||
name: string
|
||||
content: string
|
||||
proxiable: boolean
|
||||
proxied: boolean
|
||||
ttl: number
|
||||
locked: boolean
|
||||
priority?: number
|
||||
comment?: string | null
|
||||
tags: string[]
|
||||
comment_modified_on?: string | null
|
||||
tags_modified_on?: string | null
|
||||
meta?: CloudflareDnsRecordMeta | null
|
||||
created_on: string
|
||||
modified_on: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareUpdateDnsRecordParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
recordId: string
|
||||
type?: string
|
||||
name?: string
|
||||
content?: string
|
||||
ttl?: number
|
||||
proxied?: boolean
|
||||
priority?: number
|
||||
comment?: string
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export interface CloudflareUpdateDnsRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
zone_id: string
|
||||
zone_name: string
|
||||
type: string
|
||||
name: string
|
||||
content: string
|
||||
proxiable: boolean
|
||||
proxied: boolean
|
||||
ttl: number
|
||||
locked: boolean
|
||||
priority?: number
|
||||
comment?: string | null
|
||||
tags: string[]
|
||||
comment_modified_on?: string | null
|
||||
tags_modified_on?: string | null
|
||||
meta?: CloudflareDnsRecordMeta | null
|
||||
created_on: string
|
||||
modified_on: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareDeleteDnsRecordParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
recordId: string
|
||||
}
|
||||
|
||||
export interface CloudflareDeleteDnsRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareListCertificatesParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
status?: string
|
||||
page?: number
|
||||
per_page?: number
|
||||
deploy?: string
|
||||
}
|
||||
|
||||
export interface CloudflareCertificateGeoRestrictions {
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface CloudflareCertificate {
|
||||
id: string
|
||||
hosts: string[]
|
||||
issuer: string
|
||||
signature: string
|
||||
status: string
|
||||
bundle_method: string
|
||||
zone_id: string
|
||||
uploaded_on: string
|
||||
modified_on: string
|
||||
expires_on: string
|
||||
priority?: number
|
||||
geo_restrictions?: CloudflareCertificateGeoRestrictions
|
||||
}
|
||||
|
||||
export interface CloudflareCertificateValidationError {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface CloudflareDcvDelegationRecord {
|
||||
cname: string
|
||||
cname_target: string
|
||||
emails: string[]
|
||||
http_body: string
|
||||
http_url: string
|
||||
status: string
|
||||
txt_name: string
|
||||
txt_value: string
|
||||
}
|
||||
|
||||
export interface CloudflareCertificatePack {
|
||||
id: string
|
||||
type: string
|
||||
hosts: string[]
|
||||
primary_certificate: string
|
||||
status: string
|
||||
certificates: CloudflareCertificate[]
|
||||
cloudflare_branding?: boolean
|
||||
validation_method?: string
|
||||
validity_days?: number
|
||||
certificate_authority?: string
|
||||
validation_errors?: CloudflareCertificateValidationError[]
|
||||
validation_records?: CloudflareDcvDelegationRecord[]
|
||||
dcv_delegation_records?: CloudflareDcvDelegationRecord[]
|
||||
}
|
||||
|
||||
export interface CloudflareListCertificatesResponse extends ToolResponse {
|
||||
output: {
|
||||
certificates: CloudflareCertificatePack[]
|
||||
total_count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflarePurgeCacheParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
purge_everything?: boolean
|
||||
files?: string
|
||||
tags?: string
|
||||
hosts?: string
|
||||
prefixes?: string
|
||||
}
|
||||
|
||||
export interface CloudflarePurgeCacheResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareDnsAnalyticsParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
since?: string
|
||||
until?: string
|
||||
metrics: string
|
||||
dimensions?: string
|
||||
filters?: string
|
||||
sort?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CloudflareDnsAnalyticsTotals {
|
||||
queryCount: number
|
||||
uncachedCount: number
|
||||
staleCount: number
|
||||
responseTimeAvg?: number
|
||||
responseTimeMedian?: number
|
||||
responseTime90th?: number
|
||||
responseTime99th?: number
|
||||
}
|
||||
|
||||
export interface CloudflareDnsAnalyticsQuery {
|
||||
since: string
|
||||
until: string
|
||||
metrics: string[]
|
||||
dimensions: string[]
|
||||
filters: string
|
||||
sort: string[]
|
||||
limit: number
|
||||
}
|
||||
|
||||
export interface CloudflareDnsAnalyticsResponse extends ToolResponse {
|
||||
output: {
|
||||
totals: CloudflareDnsAnalyticsTotals
|
||||
min: CloudflareDnsAnalyticsTotals
|
||||
max: CloudflareDnsAnalyticsTotals
|
||||
data: Array<{
|
||||
dimensions: string[]
|
||||
metrics: number[]
|
||||
}>
|
||||
data_lag: number
|
||||
rows: number
|
||||
query: CloudflareDnsAnalyticsQuery
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareGetZoneSettingsParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
}
|
||||
|
||||
export interface CloudflareZoneSetting {
|
||||
id: string
|
||||
value: string
|
||||
editable: boolean
|
||||
modified_on: string
|
||||
time_remaining?: number
|
||||
}
|
||||
|
||||
export interface CloudflareGetZoneSettingsResponse extends ToolResponse {
|
||||
output: {
|
||||
settings: CloudflareZoneSetting[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface CloudflareUpdateZoneSettingParams extends CloudflareBaseParams {
|
||||
zoneId: string
|
||||
settingId: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface CloudflareUpdateZoneSettingResponse extends ToolResponse {
|
||||
output: {
|
||||
id: string
|
||||
value: string
|
||||
editable: boolean
|
||||
modified_on: string
|
||||
time_remaining?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type CloudflareResponse =
|
||||
| CloudflareListZonesResponse
|
||||
| CloudflareGetZoneResponse
|
||||
| CloudflareCreateZoneResponse
|
||||
| CloudflareDeleteZoneResponse
|
||||
| CloudflareListDnsRecordsResponse
|
||||
| CloudflareCreateDnsRecordResponse
|
||||
| CloudflareUpdateDnsRecordResponse
|
||||
| CloudflareDeleteDnsRecordResponse
|
||||
| CloudflareListCertificatesResponse
|
||||
| CloudflarePurgeCacheResponse
|
||||
| CloudflareDnsAnalyticsResponse
|
||||
| CloudflareGetZoneSettingsResponse
|
||||
| CloudflareUpdateZoneSettingResponse
|
||||
@@ -1,217 +0,0 @@
|
||||
import type {
|
||||
CloudflareUpdateDnsRecordParams,
|
||||
CloudflareUpdateDnsRecordResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateDnsRecordTool: ToolConfig<
|
||||
CloudflareUpdateDnsRecordParams,
|
||||
CloudflareUpdateDnsRecordResponse
|
||||
> = {
|
||||
id: 'cloudflare_update_dns_record',
|
||||
name: 'Cloudflare Update DNS Record',
|
||||
description: 'Updates an existing DNS record for a zone.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID containing the DNS record',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The DNS record ID to update',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record type (e.g., "A", "AAAA", "CNAME", "MX", "TXT")',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record name',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record content (e.g., IP address)',
|
||||
},
|
||||
ttl: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Time to live in seconds (1 = automatic)',
|
||||
},
|
||||
proxied: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to enable Cloudflare proxy',
|
||||
},
|
||||
priority: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority for MX and SRV records',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comment for the DNS record',
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated tags for the DNS record',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/dns_records/${params.recordId}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, any> = {}
|
||||
if (params.type !== undefined) body.type = params.type
|
||||
if (params.name !== undefined) body.name = params.name
|
||||
if (params.content !== undefined) body.content = params.content
|
||||
if (params.ttl !== undefined) body.ttl = Number(params.ttl)
|
||||
if (params.proxied !== undefined) body.proxied = params.proxied
|
||||
if (params.priority !== undefined) body.priority = Number(params.priority)
|
||||
if (params.comment !== undefined) body.comment = params.comment
|
||||
if (params.tags) {
|
||||
const tagList = String(params.tags)
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
if (tagList.length > 0) body.tags = tagList
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
id: '',
|
||||
zone_id: '',
|
||||
zone_name: '',
|
||||
type: '',
|
||||
name: '',
|
||||
content: '',
|
||||
proxiable: false,
|
||||
proxied: false,
|
||||
ttl: 0,
|
||||
locked: false,
|
||||
priority: undefined,
|
||||
comment: null,
|
||||
tags: [],
|
||||
comment_modified_on: null,
|
||||
tags_modified_on: null,
|
||||
meta: null,
|
||||
created_on: '',
|
||||
modified_on: '',
|
||||
},
|
||||
error: data.errors?.[0]?.message ?? 'Failed to update DNS record',
|
||||
}
|
||||
}
|
||||
|
||||
const record = data.result
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: record?.id ?? '',
|
||||
zone_id: record?.zone_id ?? '',
|
||||
zone_name: record?.zone_name ?? '',
|
||||
type: record?.type ?? '',
|
||||
name: record?.name ?? '',
|
||||
content: record?.content ?? '',
|
||||
proxiable: record?.proxiable ?? false,
|
||||
proxied: record?.proxied ?? false,
|
||||
ttl: record?.ttl ?? 0,
|
||||
locked: record?.locked ?? false,
|
||||
priority: record?.priority ?? null,
|
||||
comment: record?.comment ?? null,
|
||||
tags: record?.tags ?? [],
|
||||
comment_modified_on: record?.comment_modified_on ?? null,
|
||||
tags_modified_on: record?.tags_modified_on ?? null,
|
||||
meta: record?.meta ?? null,
|
||||
created_on: record?.created_on ?? '',
|
||||
modified_on: record?.modified_on ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Unique identifier for the updated DNS record' },
|
||||
zone_id: { type: 'string', description: 'The ID of the zone the record belongs to' },
|
||||
zone_name: { type: 'string', description: 'The name of the zone' },
|
||||
type: { type: 'string', description: 'DNS record type (A, AAAA, CNAME, MX, TXT, etc.)' },
|
||||
name: { type: 'string', description: 'DNS record hostname' },
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'DNS record value (e.g., IP address, target hostname)',
|
||||
},
|
||||
proxiable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the record can be proxied through Cloudflare',
|
||||
},
|
||||
proxied: { type: 'boolean', description: 'Whether Cloudflare proxy is enabled' },
|
||||
ttl: { type: 'number', description: 'Time to live in seconds (1 = automatic)' },
|
||||
locked: { type: 'boolean', description: 'Whether the record is locked' },
|
||||
priority: { type: 'number', description: 'Priority for MX and SRV records', optional: true },
|
||||
comment: { type: 'string', description: 'Comment associated with the record', optional: true },
|
||||
tags: {
|
||||
type: 'array',
|
||||
description: 'Tags associated with the record',
|
||||
items: { type: 'string', description: 'Tag value' },
|
||||
},
|
||||
comment_modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the comment was last modified',
|
||||
optional: true,
|
||||
},
|
||||
tags_modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when tags were last modified',
|
||||
optional: true,
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Record metadata',
|
||||
optional: true,
|
||||
properties: {
|
||||
source: { type: 'string', description: 'Source of the DNS record' },
|
||||
},
|
||||
},
|
||||
created_on: { type: 'string', description: 'ISO 8601 timestamp when the record was created' },
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the record was last modified',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import type {
|
||||
CloudflareUpdateZoneSettingParams,
|
||||
CloudflareUpdateZoneSettingResponse,
|
||||
} from '@/tools/cloudflare/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateZoneSettingTool: ToolConfig<
|
||||
CloudflareUpdateZoneSettingParams,
|
||||
CloudflareUpdateZoneSettingResponse
|
||||
> = {
|
||||
id: 'cloudflare_update_zone_setting',
|
||||
name: 'Cloudflare Update Zone Setting',
|
||||
description:
|
||||
'Updates a specific zone setting such as SSL mode, security level, cache level, minification, or other configuration.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The zone ID to update settings for',
|
||||
},
|
||||
settingId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Setting to update (e.g., "ssl", "security_level", "cache_level", "minify", "always_use_https", "browser_cache_ttl", "http3", "min_tls_version", "ciphers")',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'New value for the setting as a string or JSON string for complex values (e.g., "full" for SSL, "medium" for security_level, "aggressive" for cache_level, \'{"css":"on","html":"on","js":"on"}\' for minify, \'["ECDHE-RSA-AES128-GCM-SHA256"]\' for ciphers)',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cloudflare API Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.cloudflare.com/client/v4/zones/${params.zoneId}/settings/${params.settingId}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
try {
|
||||
return { value: JSON.parse(params.value) }
|
||||
} catch {
|
||||
return { value: params.value }
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
output: { id: '', value: '', editable: false, modified_on: '' },
|
||||
error: data.errors?.[0]?.message ?? 'Failed to update zone setting',
|
||||
}
|
||||
}
|
||||
|
||||
const setting = data.result
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: setting?.id ?? '',
|
||||
value:
|
||||
typeof setting?.value === 'object' && setting?.value !== null
|
||||
? JSON.stringify(setting.value)
|
||||
: String(setting?.value ?? ''),
|
||||
editable: setting?.editable ?? false,
|
||||
modified_on: setting?.modified_on ?? '',
|
||||
...(setting?.time_remaining != null
|
||||
? { time_remaining: setting.time_remaining as number }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Setting identifier (e.g., ssl, minify, cache_level)',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Updated setting value as a string. Simple values returned as-is (e.g., "full", "on"). Complex values are JSON-stringified.',
|
||||
},
|
||||
editable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the setting can be modified for the current zone plan',
|
||||
},
|
||||
modified_on: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp when the setting was last modified',
|
||||
},
|
||||
time_remaining: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Seconds remaining until the setting can be modified again (only present for rate-limited settings)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseAssociateParams,
|
||||
DataverseAssociateResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseAssociate')
|
||||
|
||||
export const dataverseAssociateTool: ToolConfig<
|
||||
DataverseAssociateParams,
|
||||
DataverseAssociateResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_associate',
|
||||
name: 'Associate Microsoft Dataverse Records',
|
||||
description:
|
||||
'Associate two records in Microsoft Dataverse via a navigation property. Creates a relationship between a source record and a target record. Supports both collection-valued (POST) and single-valued (PUT) navigation properties.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source entity set name (e.g., accounts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source record GUID',
|
||||
},
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Navigation property name (e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued)',
|
||||
},
|
||||
targetEntitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target entity set name (e.g., contacts)',
|
||||
},
|
||||
targetRecordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target record GUID to associate',
|
||||
},
|
||||
navigationType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Type of navigation property: "collection" (default, uses POST) or "single" (uses PUT for lookup fields)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref`
|
||||
},
|
||||
method: (params) => (params.navigationType === 'single' ? 'PUT' : 'POST'),
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return {
|
||||
'@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName}(${params.targetRecordId})`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseAssociateParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse associate failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
entitySetName: params?.entitySetName ?? '',
|
||||
recordId: params?.recordId ?? '',
|
||||
navigationProperty: params?.navigationProperty ?? '',
|
||||
targetEntitySetName: params?.targetEntitySetName ?? '',
|
||||
targetRecordId: params?.targetRecordId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the association was created successfully' },
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
description: 'Source entity set name used in the association',
|
||||
},
|
||||
recordId: { type: 'string', description: 'Source record GUID that was associated' },
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
description: 'Navigation property used for the association',
|
||||
},
|
||||
targetEntitySetName: {
|
||||
type: 'string',
|
||||
description: 'Target entity set name used in the association',
|
||||
},
|
||||
targetRecordId: { type: 'string', description: 'Target record GUID that was associated' },
|
||||
},
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseCreateMultipleParams,
|
||||
DataverseCreateMultipleResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseCreateMultiple')
|
||||
|
||||
export const dataverseCreateMultipleTool: ToolConfig<
|
||||
DataverseCreateMultipleParams,
|
||||
DataverseCreateMultipleResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_create_multiple',
|
||||
name: 'Create Multiple Microsoft Dataverse Records',
|
||||
description:
|
||||
'Create multiple records of the same table type in a single request. Each record in the Targets array must include an @odata.type annotation. Recommended batch size: 100-1000 records for standard tables.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
entityLogicalName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Table logical name for @odata.type annotation (e.g., account, contact). Used to set Microsoft.Dynamics.CRM.{entityLogicalName} on each record.',
|
||||
},
|
||||
records: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of record objects to create. Each record should contain column logical names as keys. The @odata.type annotation is added automatically.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.CreateMultiple`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let records = params.records
|
||||
if (typeof records === 'string') {
|
||||
try {
|
||||
records = JSON.parse(records)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for records array')
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(records)) {
|
||||
throw new Error('Records must be an array of objects')
|
||||
}
|
||||
const targets = records.map((record: Record<string, unknown>) => ({
|
||||
...record,
|
||||
'@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`,
|
||||
}))
|
||||
return { Targets: targets }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse create multiple failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
const ids = data?.Ids ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ids,
|
||||
count: ids.length,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ids: {
|
||||
type: 'array',
|
||||
description: 'Array of GUIDs for the created records',
|
||||
items: {
|
||||
type: 'string',
|
||||
description: 'GUID of a created record',
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Number of records created' },
|
||||
success: { type: 'boolean', description: 'Whether all records were created successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseCreateRecordParams,
|
||||
DataverseCreateRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseCreateRecord')
|
||||
|
||||
export const dataverseCreateRecordTool: ToolConfig<
|
||||
DataverseCreateRecordParams,
|
||||
DataverseCreateRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_create_record',
|
||||
name: 'Create Microsoft Dataverse Record',
|
||||
description:
|
||||
'Create a new record in a Microsoft Dataverse table. Requires the entity set name (plural table name) and record data as a JSON object.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record data as a JSON object with column names as keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
}),
|
||||
body: (params) => {
|
||||
let data = params.data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for record data')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse create record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
let recordId = ''
|
||||
if (data) {
|
||||
const idKey = Object.keys(data).find((k) => k.endsWith('id') && !k.startsWith('@'))
|
||||
recordId = idKey ? String(data[idKey]) : ''
|
||||
}
|
||||
|
||||
if (!recordId) {
|
||||
const entityIdHeader = response.headers.get('OData-EntityId')
|
||||
if (entityIdHeader) {
|
||||
const match = entityIdHeader.match(/\(([^)]+)\)/)
|
||||
if (match) {
|
||||
recordId = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId,
|
||||
record: data ?? {},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the created record', optional: true },
|
||||
record: { ...DATAVERSE_RECORD_OUTPUT, optional: true },
|
||||
success: { type: 'boolean', description: 'Whether the record was created successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseDeleteRecordParams,
|
||||
DataverseDeleteRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseDeleteRecord')
|
||||
|
||||
export const dataverseDeleteRecordTool: ToolConfig<
|
||||
DataverseDeleteRecordParams,
|
||||
DataverseDeleteRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_delete_record',
|
||||
name: 'Delete Microsoft Dataverse Record',
|
||||
description: 'Delete a record from a Microsoft Dataverse table by its ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseDeleteRecordParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse delete record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: params?.recordId ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the deleted record' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseDisassociateParams,
|
||||
DataverseDisassociateResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseDisassociate')
|
||||
|
||||
export const dataverseDisassociateTool: ToolConfig<
|
||||
DataverseDisassociateParams,
|
||||
DataverseDisassociateResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_disassociate',
|
||||
name: 'Disassociate Microsoft Dataverse Records',
|
||||
description:
|
||||
'Remove an association between two records in Microsoft Dataverse. For collection-valued navigation properties, provide the target record ID. For single-valued navigation properties, only the navigation property name is needed.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source entity set name (e.g., accounts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Source record GUID',
|
||||
},
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Navigation property name (e.g., contact_customer_accounts for collection-valued, or parentcustomerid_account for single-valued)',
|
||||
},
|
||||
targetRecordId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Target record GUID (required for collection-valued navigation properties, omit for single-valued)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
if (params.targetRecordId) {
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}(${params.targetRecordId})/$ref`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseDisassociateParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse disassociate failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
entitySetName: params?.entitySetName ?? '',
|
||||
recordId: params?.recordId ?? '',
|
||||
navigationProperty: params?.navigationProperty ?? '',
|
||||
...(params?.targetRecordId ? { targetRecordId: params.targetRecordId } : {}),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the disassociation was completed successfully',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
description: 'Source entity set name used in the disassociation',
|
||||
},
|
||||
recordId: { type: 'string', description: 'Source record GUID that was disassociated' },
|
||||
navigationProperty: {
|
||||
type: 'string',
|
||||
description: 'Navigation property used for the disassociation',
|
||||
},
|
||||
targetRecordId: {
|
||||
type: 'string',
|
||||
description: 'Target record GUID that was disassociated',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseDownloadFileParams,
|
||||
DataverseDownloadFileResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseDownloadFile')
|
||||
|
||||
export const dataverseDownloadFileTool: ToolConfig<
|
||||
DataverseDownloadFileParams,
|
||||
DataverseDownloadFileResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_download_file',
|
||||
name: 'Download File from Microsoft Dataverse',
|
||||
description:
|
||||
'Download a file from a file or image column on a Dataverse record. Returns the file content as a base64-encoded string along with file metadata from response headers.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record GUID to download the file from',
|
||||
},
|
||||
fileColumn: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File or image column logical name (e.g., entityimage, cr_document)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.fileColumn}/$value`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse download file failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const fileName = response.headers.get('x-ms-file-name') ?? ''
|
||||
const fileSize = response.headers.get('x-ms-file-size') ?? ''
|
||||
const mimeType = response.headers.get('mimetype') ?? response.headers.get('content-type') ?? ''
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const base64Content = Buffer.from(buffer).toString('base64')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
fileContent: base64Content,
|
||||
fileName,
|
||||
fileSize: fileSize ? Number.parseInt(fileSize, 10) : buffer.byteLength,
|
||||
mimeType,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
fileContent: { type: 'string', description: 'Base64-encoded file content' },
|
||||
fileName: { type: 'string', description: 'Name of the downloaded file', optional: true },
|
||||
fileSize: { type: 'number', description: 'File size in bytes' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the file', optional: true },
|
||||
success: { type: 'boolean', description: 'Whether the file was downloaded successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseExecuteActionParams,
|
||||
DataverseExecuteActionResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseExecuteAction')
|
||||
|
||||
export const dataverseExecuteActionTool: ToolConfig<
|
||||
DataverseExecuteActionParams,
|
||||
DataverseExecuteActionResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_execute_action',
|
||||
name: 'Execute Microsoft Dataverse Action',
|
||||
description:
|
||||
'Execute a bound or unbound Dataverse action. Actions perform operations with side effects (e.g., Merge, GrantAccess, SendEmail, QualifyLead). For bound actions, provide the entity set name and record ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
actionName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Action name (e.g., Merge, GrantAccess, SendEmail). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound actions.',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity set name for bound actions (e.g., accounts). Leave empty for unbound actions.',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Record GUID for bound actions. Leave empty for unbound or collection-bound actions.',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Action parameters as a JSON object. For entity references, include @odata.type annotation (e.g., {"Target": {"@odata.type": "Microsoft.Dynamics.CRM.account", "accountid": "..."}})',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
if (params.entitySetName) {
|
||||
if (params.recordId) {
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.actionName}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.actionName}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${params.actionName}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
if (!params.parameters) return {}
|
||||
let data = params.parameters
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for action parameters')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse execute action failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = response.status === 204 ? null : await response.json().catch(() => null)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Action response data. Structure varies by action. Null for actions that return 204 No Content.',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the action executed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseExecuteFunctionParams,
|
||||
DataverseExecuteFunctionResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseExecuteFunction')
|
||||
|
||||
export const dataverseExecuteFunctionTool: ToolConfig<
|
||||
DataverseExecuteFunctionParams,
|
||||
DataverseExecuteFunctionResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_execute_function',
|
||||
name: 'Execute Microsoft Dataverse Function',
|
||||
description:
|
||||
'Execute a bound or unbound Dataverse function. Functions are read-only operations (e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount, InitializeFrom). For bound functions, provide the entity set name and record ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
functionName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Function name (e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound functions.',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Entity set name for bound functions (e.g., systemusers). Leave empty for unbound functions.',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record GUID for bound functions. Leave empty for unbound functions.',
|
||||
},
|
||||
parameters: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Function parameters as a comma-separated list of name=value pairs for the URL (e.g., "LocalizedStandardName=\'Pacific Standard Time\',LocaleId=1033"). Use @p1,@p2 aliases for complex values.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
const paramStr = params.parameters ? `(${params.parameters})` : '()'
|
||||
if (params.entitySetName) {
|
||||
if (params.recordId) {
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}`
|
||||
}
|
||||
return `${baseUrl}/api/data/v9.2/${params.functionName}${paramStr}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse execute function failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Function response data. Structure varies by function.',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the function executed successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseFetchXmlQueryParams,
|
||||
DataverseFetchXmlQueryResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORDS_ARRAY_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseFetchXmlQuery')
|
||||
|
||||
export const dataverseFetchXmlQueryTool: ToolConfig<
|
||||
DataverseFetchXmlQueryParams,
|
||||
DataverseFetchXmlQueryResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_fetchxml_query',
|
||||
name: 'FetchXML Query Microsoft Dataverse',
|
||||
description:
|
||||
'Execute a FetchXML query against a Microsoft Dataverse table. FetchXML supports aggregation, grouping, linked-entity joins, and complex filtering beyond OData capabilities.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
fetchXml: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'FetchXML query string. Must include <fetch> root element and <entity> child element matching the table logical name.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
const encodedFetchXml = encodeURIComponent(params.fetchXml)
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}?fetchXml=${encodedFetchXml}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'odata.include-annotations="*"',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse FetchXML query failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const records = data.value ?? []
|
||||
const fetchXmlPagingCookie = data['@Microsoft.Dynamics.CRM.fetchxmlpagingcookie'] ?? null
|
||||
const moreRecords = data['@Microsoft.Dynamics.CRM.morerecords'] ?? false
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
count: records.length,
|
||||
fetchXmlPagingCookie,
|
||||
moreRecords,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: DATAVERSE_RECORDS_ARRAY_OUTPUT,
|
||||
count: { type: 'number', description: 'Number of records returned in the current page' },
|
||||
fetchXmlPagingCookie: {
|
||||
type: 'string',
|
||||
description: 'Paging cookie for retrieving the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
moreRecords: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more records are available beyond the current page',
|
||||
},
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseGetRecordParams,
|
||||
DataverseGetRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseGetRecord')
|
||||
|
||||
export const dataverseGetRecordTool: ToolConfig<
|
||||
DataverseGetRecordParams,
|
||||
DataverseGetRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_get_record',
|
||||
name: 'Get Microsoft Dataverse Record',
|
||||
description:
|
||||
'Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to retrieve',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of columns to return (OData $select)',
|
||||
},
|
||||
expand: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Navigation properties to expand (OData $expand)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
const queryParts: string[] = []
|
||||
if (params.select) queryParts.push(`$select=${params.select}`)
|
||||
if (params.expand) queryParts.push(`$expand=${params.expand}`)
|
||||
const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})${query}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'odata.include-annotations="*"',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse get record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const idKey = Object.keys(data).find((k) => k.endsWith('id') && !k.startsWith('@'))
|
||||
const recordId = idKey ? String(data[idKey]) : ''
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record: data,
|
||||
recordId,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: DATAVERSE_RECORD_OUTPUT,
|
||||
recordId: {
|
||||
type: 'string',
|
||||
description: 'The record primary key ID (auto-detected from response)',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Whether the record was retrieved successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export { dataverseAssociateTool } from './associate'
|
||||
export { dataverseCreateMultipleTool } from './create_multiple'
|
||||
export { dataverseCreateRecordTool } from './create_record'
|
||||
export { dataverseDeleteRecordTool } from './delete_record'
|
||||
export { dataverseDisassociateTool } from './disassociate'
|
||||
export { dataverseDownloadFileTool } from './download_file'
|
||||
export { dataverseExecuteActionTool } from './execute_action'
|
||||
export { dataverseExecuteFunctionTool } from './execute_function'
|
||||
export { dataverseFetchXmlQueryTool } from './fetchxml_query'
|
||||
export { dataverseGetRecordTool } from './get_record'
|
||||
export { dataverseListRecordsTool } from './list_records'
|
||||
export { dataverseSearchTool } from './search'
|
||||
export { dataverseUpdateMultipleTool } from './update_multiple'
|
||||
export { dataverseUpdateRecordTool } from './update_record'
|
||||
export { dataverseUploadFileTool } from './upload_file'
|
||||
export { dataverseUpsertRecordTool } from './upsert_record'
|
||||
export { dataverseWhoAmITool } from './whoami'
|
||||
@@ -1,146 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseListRecordsParams,
|
||||
DataverseListRecordsResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORDS_ARRAY_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseListRecords')
|
||||
|
||||
export const dataverseListRecordsTool: ToolConfig<
|
||||
DataverseListRecordsParams,
|
||||
DataverseListRecordsResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_list_records',
|
||||
name: 'List Microsoft Dataverse Records',
|
||||
description:
|
||||
'Query and list records from a Microsoft Dataverse table. Supports OData query options for filtering, selecting columns, ordering, and pagination.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of columns to return (OData $select)',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'OData $filter expression (e.g., statecode eq 0)',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'OData $orderby expression (e.g., name asc, createdon desc)',
|
||||
},
|
||||
top: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of records to return (OData $top)',
|
||||
},
|
||||
expand: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Navigation properties to expand (OData $expand)',
|
||||
},
|
||||
count: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Set to "true" to include total record count in response (OData $count)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
const queryParts: string[] = []
|
||||
if (params.select) queryParts.push(`$select=${params.select}`)
|
||||
if (params.filter) queryParts.push(`$filter=${params.filter}`)
|
||||
if (params.orderBy) queryParts.push(`$orderby=${params.orderBy}`)
|
||||
if (params.top) queryParts.push(`$top=${params.top}`)
|
||||
if (params.expand) queryParts.push(`$expand=${params.expand}`)
|
||||
if (params.count) queryParts.push(`$count=${params.count}`)
|
||||
const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : ''
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}${query}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'odata.include-annotations="*",odata.maxpagesize=100',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse list records failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const records = data.value ?? []
|
||||
const nextLink = data['@odata.nextLink'] ?? null
|
||||
const totalCount = data['@odata.count'] ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
count: records.length,
|
||||
totalCount,
|
||||
nextLink,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: DATAVERSE_RECORDS_ARRAY_OUTPUT,
|
||||
count: { type: 'number', description: 'Number of records returned in the current page' },
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records server-side (requires $count=true)',
|
||||
optional: true,
|
||||
},
|
||||
nextLink: {
|
||||
type: 'string',
|
||||
description: 'URL for the next page of results',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseSearchParams,
|
||||
DataverseSearchResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseSearch')
|
||||
|
||||
export const dataverseSearchTool: ToolConfig<DataverseSearchParams, DataverseSearchResponse> = {
|
||||
id: 'microsoft_dataverse_search',
|
||||
name: 'Search Microsoft Dataverse',
|
||||
description:
|
||||
'Perform a full-text relevance search across Microsoft Dataverse tables. Requires Dataverse Search to be enabled on the environment. Supports simple and Lucene query syntax.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
searchTerm: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search text (1-100 chars). Supports simple syntax: + (AND), | (OR), - (NOT), * (wildcard), "exact phrase"',
|
||||
},
|
||||
entities: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of search entity configs. Each object: {"Name":"account","SelectColumns":["name"],"SearchColumns":["name"],"Filter":"statecode eq 0"}',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Global OData filter applied across all entities (e.g., "createdon gt 2024-01-01")',
|
||||
},
|
||||
facets: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON array of facet specifications (e.g., ["entityname,count:100","ownerid,count:100"])',
|
||||
},
|
||||
top: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of results (default: 50, max: 100)',
|
||||
},
|
||||
skip: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to skip for pagination',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'JSON array of sort expressions (e.g., ["createdon desc"])',
|
||||
},
|
||||
searchMode: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Search mode: "any" (default, match any term) or "all" (match all terms)',
|
||||
},
|
||||
searchType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Query type: "simple" (default) or "lucene" (enables regex, fuzzy, proximity, boosting)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/searchquery`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
search: params.searchTerm,
|
||||
count: true,
|
||||
}
|
||||
if (params.entities) body.entities = params.entities
|
||||
if (params.filter) body.filter = params.filter
|
||||
if (params.facets) body.facets = params.facets
|
||||
if (params.top) body.top = params.top
|
||||
if (params.skip) body.skip = params.skip
|
||||
if (params.orderBy) body.orderby = params.orderBy
|
||||
|
||||
const options: Record<string, string> = {}
|
||||
if (params.searchMode) options.searchmode = params.searchMode
|
||||
if (params.searchType) options.querytype = params.searchType
|
||||
if (Object.keys(options).length > 0) {
|
||||
body.options = JSON.stringify(options).replace(/"/g, "'")
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse search failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
let parsedResponse = data.response
|
||||
if (typeof parsedResponse === 'string') {
|
||||
try {
|
||||
parsedResponse = JSON.parse(parsedResponse)
|
||||
} catch {
|
||||
parsedResponse = {}
|
||||
}
|
||||
}
|
||||
|
||||
const results = parsedResponse?.Value ?? []
|
||||
const totalCount = parsedResponse?.Count ?? 0
|
||||
const facets = parsedResponse?.Facets ?? null
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
results,
|
||||
totalCount,
|
||||
count: results.length,
|
||||
facets,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Array of search result objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
Id: { type: 'string', description: 'Record GUID' },
|
||||
EntityName: {
|
||||
type: 'string',
|
||||
description: 'Table logical name (e.g., account, contact)',
|
||||
},
|
||||
ObjectTypeCode: { type: 'number', description: 'Entity type code' },
|
||||
Attributes: {
|
||||
type: 'object',
|
||||
description: 'Record attributes matching the search. Keys are column logical names.',
|
||||
},
|
||||
Highlights: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Highlighted search matches. Keys are column names, values are arrays of strings with {crmhit}/{/crmhit} markers.',
|
||||
optional: true,
|
||||
},
|
||||
Score: { type: 'number', description: 'Relevance score for this result' },
|
||||
},
|
||||
},
|
||||
},
|
||||
totalCount: {
|
||||
type: 'number',
|
||||
description: 'Total number of matching records across all tables',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of results returned in this page' },
|
||||
facets: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Facet results when facets were requested. Keys are facet names, values are arrays of facet value objects with count and value properties.',
|
||||
optional: true,
|
||||
},
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Microsoft Dataverse Web API types.
|
||||
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dataverse record output definition.
|
||||
* Dataverse records are dynamic (user-defined tables), so columns vary by table.
|
||||
* Every record includes OData metadata fields such as `@odata.etag`.
|
||||
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/retrieve-entity-using-web-api
|
||||
*/
|
||||
export const DATAVERSE_RECORD_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description:
|
||||
'Dataverse record object. Contains dynamic columns based on the queried table, plus OData metadata fields.',
|
||||
properties: {
|
||||
'@odata.context': {
|
||||
type: 'string',
|
||||
description: 'OData context URL describing the entity type and properties returned',
|
||||
optional: true,
|
||||
},
|
||||
'@odata.etag': {
|
||||
type: 'string',
|
||||
description: 'OData entity tag for concurrency control (e.g., W/"12345")',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of Dataverse records output definition for list endpoints.
|
||||
* Each item mirrors `DATAVERSE_RECORD_OUTPUT`.
|
||||
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api
|
||||
*/
|
||||
export const DATAVERSE_RECORDS_ARRAY_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description:
|
||||
'Array of Dataverse records. Each record has dynamic columns based on the table schema.',
|
||||
items: {
|
||||
type: 'object',
|
||||
description: 'A single Dataverse record with dynamic columns based on the table schema',
|
||||
properties: {
|
||||
'@odata.etag': {
|
||||
type: 'string',
|
||||
description: 'OData entity tag for concurrency control (e.g., W/"12345")',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export interface DataverseCreateRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseGetRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
select?: string
|
||||
expand?: string
|
||||
}
|
||||
|
||||
export interface DataverseUpdateRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseDeleteRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
}
|
||||
|
||||
export interface DataverseListRecordsParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
select?: string
|
||||
filter?: string
|
||||
orderBy?: string
|
||||
top?: number
|
||||
expand?: string
|
||||
count?: string
|
||||
}
|
||||
|
||||
export interface DataverseCreateRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
record: Record<string, unknown>
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseGetRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
record: Record<string, unknown>
|
||||
recordId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUpdateRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseDeleteRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseListRecordsResponse extends ToolResponse {
|
||||
output: {
|
||||
records: Record<string, unknown>[]
|
||||
count: number
|
||||
totalCount: number | null
|
||||
nextLink: string | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUpsertRecordParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseUpsertRecordResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
created: boolean
|
||||
record: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseWhoAmIParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
}
|
||||
|
||||
export interface DataverseWhoAmIResponse extends ToolResponse {
|
||||
output: {
|
||||
userId: string
|
||||
businessUnitId: string
|
||||
organizationId: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseAssociateParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetEntitySetName: string
|
||||
targetRecordId: string
|
||||
navigationType?: 'collection' | 'single'
|
||||
}
|
||||
|
||||
export interface DataverseAssociateResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetEntitySetName: string
|
||||
targetRecordId: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseDisassociateParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetRecordId?: string
|
||||
}
|
||||
|
||||
export interface DataverseDisassociateResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
navigationProperty: string
|
||||
targetRecordId?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseFetchXmlQueryParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
fetchXml: string
|
||||
}
|
||||
|
||||
export interface DataverseFetchXmlQueryResponse extends ToolResponse {
|
||||
output: {
|
||||
records: Record<string, unknown>[]
|
||||
count: number
|
||||
fetchXmlPagingCookie: string | null
|
||||
moreRecords: boolean
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseExecuteActionParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
actionName: string
|
||||
entitySetName?: string
|
||||
recordId?: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DataverseExecuteActionResponse extends ToolResponse {
|
||||
output: {
|
||||
result: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseExecuteFunctionParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
functionName: string
|
||||
entitySetName?: string
|
||||
recordId?: string
|
||||
parameters?: string
|
||||
}
|
||||
|
||||
export interface DataverseExecuteFunctionResponse extends ToolResponse {
|
||||
output: {
|
||||
result: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseCreateMultipleParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
entityLogicalName: string
|
||||
records: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface DataverseCreateMultipleResponse extends ToolResponse {
|
||||
output: {
|
||||
ids: string[]
|
||||
count: number
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUpdateMultipleParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
entityLogicalName: string
|
||||
records: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface DataverseUpdateMultipleResponse extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseUploadFileParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
fileColumn: string
|
||||
fileName: string
|
||||
file?: unknown
|
||||
fileContent?: string
|
||||
}
|
||||
|
||||
export interface DataverseUploadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
recordId: string
|
||||
fileColumn: string
|
||||
fileName: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseDownloadFileParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
entitySetName: string
|
||||
recordId: string
|
||||
fileColumn: string
|
||||
}
|
||||
|
||||
export interface DataverseDownloadFileResponse extends ToolResponse {
|
||||
output: {
|
||||
fileContent: string
|
||||
fileName: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DataverseSearchParams {
|
||||
accessToken: string
|
||||
environmentUrl: string
|
||||
searchTerm: string
|
||||
entities?: string
|
||||
filter?: string
|
||||
facets?: string
|
||||
top?: number
|
||||
skip?: number
|
||||
orderBy?: string
|
||||
searchMode?: string
|
||||
searchType?: string
|
||||
}
|
||||
|
||||
export interface DataverseSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
results: Record<string, unknown>[]
|
||||
totalCount: number
|
||||
count: number
|
||||
facets: Record<string, unknown> | null
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type DataverseResponse =
|
||||
| DataverseCreateRecordResponse
|
||||
| DataverseGetRecordResponse
|
||||
| DataverseUpdateRecordResponse
|
||||
| DataverseDeleteRecordResponse
|
||||
| DataverseListRecordsResponse
|
||||
| DataverseUpsertRecordResponse
|
||||
| DataverseWhoAmIResponse
|
||||
| DataverseAssociateResponse
|
||||
| DataverseDisassociateResponse
|
||||
| DataverseFetchXmlQueryResponse
|
||||
| DataverseExecuteActionResponse
|
||||
| DataverseExecuteFunctionResponse
|
||||
| DataverseCreateMultipleResponse
|
||||
| DataverseUpdateMultipleResponse
|
||||
| DataverseUploadFileResponse
|
||||
| DataverseDownloadFileResponse
|
||||
| DataverseSearchResponse
|
||||
@@ -1,112 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseUpdateMultipleParams,
|
||||
DataverseUpdateMultipleResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseUpdateMultiple')
|
||||
|
||||
export const dataverseUpdateMultipleTool: ToolConfig<
|
||||
DataverseUpdateMultipleParams,
|
||||
DataverseUpdateMultipleResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_update_multiple',
|
||||
name: 'Update Multiple Microsoft Dataverse Records',
|
||||
description:
|
||||
'Update multiple records of the same table type in a single request. Each record must include its primary key. Only include columns that need to be changed. Recommended batch size: 100-1000 records.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
entityLogicalName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Table logical name for @odata.type annotation (e.g., account, contact). Used to set Microsoft.Dynamics.CRM.{entityLogicalName} on each record.',
|
||||
},
|
||||
records: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of record objects to update. Each record must include its primary key (e.g., accountid) and only the columns being changed. The @odata.type annotation is added automatically.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.UpdateMultiple`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
let records = params.records
|
||||
if (typeof records === 'string') {
|
||||
try {
|
||||
records = JSON.parse(records)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for records array')
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(records)) {
|
||||
throw new Error('Records must be an array of objects')
|
||||
}
|
||||
const targets = records.map((record: Record<string, unknown>) => ({
|
||||
...record,
|
||||
'@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`,
|
||||
}))
|
||||
return { Targets: targets }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse update multiple failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether all records were updated successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseUpdateRecordParams,
|
||||
DataverseUpdateRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseUpdateRecord')
|
||||
|
||||
export const dataverseUpdateRecordTool: ToolConfig<
|
||||
DataverseUpdateRecordParams,
|
||||
DataverseUpdateRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_update_record',
|
||||
name: 'Update Microsoft Dataverse Record',
|
||||
description:
|
||||
'Update an existing record in a Microsoft Dataverse table. Only send the columns you want to change.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to update',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record data to update as a JSON object with column names as keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
'If-Match': '*',
|
||||
}),
|
||||
body: (params) => {
|
||||
let data = params.data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for record data')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseUpdateRecordParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse update record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: params?.recordId ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the updated record' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type {
|
||||
DataverseUploadFileParams,
|
||||
DataverseUploadFileResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const dataverseUploadFileTool: ToolConfig<
|
||||
DataverseUploadFileParams,
|
||||
DataverseUploadFileResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_upload_file',
|
||||
name: 'Upload File to Microsoft Dataverse',
|
||||
description:
|
||||
'Upload a file to a file or image column on a Dataverse record. Supports single-request upload for files up to 128 MB. The file content must be provided as a base64-encoded string.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record GUID to upload the file to',
|
||||
},
|
||||
fileColumn: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'File or image column logical name (e.g., entityimage, cr_document)',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the file being uploaded (e.g., document.pdf)',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'File to upload (UserFile object)',
|
||||
},
|
||||
fileContent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Base64-encoded file content (legacy)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/microsoft-dataverse/upload-file',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
environmentUrl: params.environmentUrl,
|
||||
entitySetName: params.entitySetName,
|
||||
recordId: params.recordId,
|
||||
fileColumn: params.fileColumn,
|
||||
fileName: params.fileName,
|
||||
file: params.file,
|
||||
fileContent: params.fileContent,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Dataverse upload file failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data.output,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'Record GUID the file was uploaded to' },
|
||||
fileColumn: { type: 'string', description: 'File column the file was uploaded to' },
|
||||
fileName: { type: 'string', description: 'Name of the uploaded file' },
|
||||
success: { type: 'boolean', description: 'Whether the file was uploaded successfully' },
|
||||
},
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseUpsertRecordParams,
|
||||
DataverseUpsertRecordResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseUpsertRecord')
|
||||
|
||||
export const dataverseUpsertRecordTool: ToolConfig<
|
||||
DataverseUpsertRecordParams,
|
||||
DataverseUpsertRecordResponse
|
||||
> = {
|
||||
id: 'microsoft_dataverse_upsert_record',
|
||||
name: 'Upsert Microsoft Dataverse Record',
|
||||
description:
|
||||
'Create or update a record in a Microsoft Dataverse table. If a record with the given ID exists, it is updated; otherwise, a new record is created.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
entitySetName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Entity set name (plural table name, e.g., accounts, contacts)',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique identifier (GUID) of the record to upsert',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record data as a JSON object with column names as keys',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
Prefer: 'return=representation',
|
||||
}),
|
||||
body: (params) => {
|
||||
let data = params.data
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data)
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for record data')
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: DataverseUpsertRecordParams) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse upsert record failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const created = response.status === 201
|
||||
const data = await response.json().catch(() => null)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
recordId: params?.recordId ?? '',
|
||||
created,
|
||||
record: data,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
recordId: { type: 'string', description: 'The ID of the upserted record' },
|
||||
created: { type: 'boolean', description: 'True if the record was created, false if updated' },
|
||||
record: { ...DATAVERSE_RECORD_OUTPUT, optional: true },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
DataverseWhoAmIParams,
|
||||
DataverseWhoAmIResponse,
|
||||
} from '@/tools/microsoft_dataverse/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('DataverseWhoAmI')
|
||||
|
||||
export const dataverseWhoAmITool: ToolConfig<DataverseWhoAmIParams, DataverseWhoAmIResponse> = {
|
||||
id: 'microsoft_dataverse_whoami',
|
||||
name: 'Microsoft Dataverse WhoAmI',
|
||||
description:
|
||||
'Retrieve the current authenticated user information from Microsoft Dataverse. Useful for testing connectivity and getting the user ID, business unit ID, and organization ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: { required: true, provider: 'microsoft-dataverse' },
|
||||
errorExtractor: 'nested-error-object',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Microsoft Dataverse API',
|
||||
},
|
||||
environmentUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const baseUrl = params.environmentUrl.replace(/\/$/, '')
|
||||
return `${baseUrl}/api/data/v9.2/WhoAmI()`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'OData-MaxVersion': '4.0',
|
||||
'OData-Version': '4.0',
|
||||
Accept: 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
const errorMessage =
|
||||
errorData?.error?.message ??
|
||||
`Dataverse API error: ${response.status} ${response.statusText}`
|
||||
logger.error('Dataverse WhoAmI failed', { errorData, status: response.status })
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
userId: data.UserId ?? '',
|
||||
businessUnitId: data.BusinessUnitId ?? '',
|
||||
organizationId: data.OrganizationId ?? '',
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
userId: { type: 'string', description: 'The authenticated user ID' },
|
||||
businessUnitId: { type: 'string', description: 'The business unit ID' },
|
||||
organizationId: { type: 'string', description: 'The organization ID' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -110,21 +110,6 @@ import {
|
||||
clerkRevokeSessionTool,
|
||||
clerkUpdateUserTool,
|
||||
} from '@/tools/clerk'
|
||||
import {
|
||||
cloudflareCreateDnsRecordTool,
|
||||
cloudflareCreateZoneTool,
|
||||
cloudflareDeleteDnsRecordTool,
|
||||
cloudflareDeleteZoneTool,
|
||||
cloudflareDnsAnalyticsTool,
|
||||
cloudflareGetZoneSettingsTool,
|
||||
cloudflareGetZoneTool,
|
||||
cloudflareListCertificatesTool,
|
||||
cloudflareListDnsRecordsTool,
|
||||
cloudflareListZonesTool,
|
||||
cloudflarePurgeCacheTool,
|
||||
cloudflareUpdateDnsRecordTool,
|
||||
cloudflareUpdateZoneSettingTool,
|
||||
} from '@/tools/cloudflare'
|
||||
import {
|
||||
confluenceAddLabelTool,
|
||||
confluenceCreateBlogPostTool,
|
||||
@@ -1089,25 +1074,6 @@ import {
|
||||
} from '@/tools/mailgun'
|
||||
import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from '@/tools/mem0'
|
||||
import { memoryAddTool, memoryDeleteTool, memoryGetAllTool, memoryGetTool } from '@/tools/memory'
|
||||
import {
|
||||
dataverseAssociateTool,
|
||||
dataverseCreateMultipleTool,
|
||||
dataverseCreateRecordTool,
|
||||
dataverseDeleteRecordTool,
|
||||
dataverseDisassociateTool,
|
||||
dataverseDownloadFileTool,
|
||||
dataverseExecuteActionTool,
|
||||
dataverseExecuteFunctionTool,
|
||||
dataverseFetchXmlQueryTool,
|
||||
dataverseGetRecordTool,
|
||||
dataverseListRecordsTool,
|
||||
dataverseSearchTool,
|
||||
dataverseUpdateMultipleTool,
|
||||
dataverseUpdateRecordTool,
|
||||
dataverseUploadFileTool,
|
||||
dataverseUpsertRecordTool,
|
||||
dataverseWhoAmITool,
|
||||
} from '@/tools/microsoft_dataverse'
|
||||
import {
|
||||
microsoftExcelReadTool,
|
||||
microsoftExcelReadV2Tool,
|
||||
@@ -1727,58 +1693,6 @@ import {
|
||||
typeformUpdateFormTool,
|
||||
} from '@/tools/typeform'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import {
|
||||
vercelAddDomainTool,
|
||||
vercelAddProjectDomainTool,
|
||||
vercelCancelDeploymentTool,
|
||||
vercelCreateAliasTool,
|
||||
vercelCreateCheckTool,
|
||||
vercelCreateDeploymentTool,
|
||||
vercelCreateDnsRecordTool,
|
||||
vercelCreateEdgeConfigTool,
|
||||
vercelCreateEnvVarTool,
|
||||
vercelCreateProjectTool,
|
||||
vercelCreateWebhookTool,
|
||||
vercelDeleteAliasTool,
|
||||
vercelDeleteDeploymentTool,
|
||||
vercelDeleteDnsRecordTool,
|
||||
vercelDeleteDomainTool,
|
||||
vercelDeleteEnvVarTool,
|
||||
vercelDeleteProjectTool,
|
||||
vercelDeleteWebhookTool,
|
||||
vercelGetAliasTool,
|
||||
vercelGetCheckTool,
|
||||
vercelGetDeploymentEventsTool,
|
||||
vercelGetDeploymentTool,
|
||||
vercelGetDomainConfigTool,
|
||||
vercelGetDomainTool,
|
||||
vercelGetEdgeConfigItemsTool,
|
||||
vercelGetEdgeConfigTool,
|
||||
vercelGetEnvVarsTool,
|
||||
vercelGetProjectTool,
|
||||
vercelGetTeamTool,
|
||||
vercelGetUserTool,
|
||||
vercelListAliasesTool,
|
||||
vercelListChecksTool,
|
||||
vercelListDeploymentFilesTool,
|
||||
vercelListDeploymentsTool,
|
||||
vercelListDnsRecordsTool,
|
||||
vercelListDomainsTool,
|
||||
vercelListEdgeConfigsTool,
|
||||
vercelListProjectDomainsTool,
|
||||
vercelListProjectsTool,
|
||||
vercelListTeamMembersTool,
|
||||
vercelListTeamsTool,
|
||||
vercelListWebhooksTool,
|
||||
vercelPauseProjectTool,
|
||||
vercelRemoveProjectDomainTool,
|
||||
vercelRerequestCheckTool,
|
||||
vercelUnpauseProjectTool,
|
||||
vercelUpdateCheckTool,
|
||||
vercelUpdateEdgeConfigItemsTool,
|
||||
vercelUpdateEnvVarTool,
|
||||
vercelUpdateProjectTool,
|
||||
} from '@/tools/vercel'
|
||||
import {
|
||||
falaiVideoTool,
|
||||
lumaVideoTool,
|
||||
@@ -2786,66 +2700,6 @@ export const tools: Record<string, ToolConfig> = {
|
||||
trello_update_card: trelloUpdateCardTool,
|
||||
trello_get_actions: trelloGetActionsTool,
|
||||
trello_add_comment: trelloAddCommentTool,
|
||||
// Vercel - Deployments
|
||||
vercel_list_deployments: vercelListDeploymentsTool,
|
||||
vercel_get_deployment: vercelGetDeploymentTool,
|
||||
vercel_create_deployment: vercelCreateDeploymentTool,
|
||||
vercel_cancel_deployment: vercelCancelDeploymentTool,
|
||||
vercel_delete_deployment: vercelDeleteDeploymentTool,
|
||||
vercel_get_deployment_events: vercelGetDeploymentEventsTool,
|
||||
vercel_list_deployment_files: vercelListDeploymentFilesTool,
|
||||
// Vercel - Projects
|
||||
vercel_list_projects: vercelListProjectsTool,
|
||||
vercel_get_project: vercelGetProjectTool,
|
||||
vercel_create_project: vercelCreateProjectTool,
|
||||
vercel_update_project: vercelUpdateProjectTool,
|
||||
vercel_delete_project: vercelDeleteProjectTool,
|
||||
vercel_pause_project: vercelPauseProjectTool,
|
||||
vercel_unpause_project: vercelUnpauseProjectTool,
|
||||
vercel_list_project_domains: vercelListProjectDomainsTool,
|
||||
vercel_add_project_domain: vercelAddProjectDomainTool,
|
||||
vercel_remove_project_domain: vercelRemoveProjectDomainTool,
|
||||
// Vercel - Environment Variables
|
||||
vercel_get_env_vars: vercelGetEnvVarsTool,
|
||||
vercel_create_env_var: vercelCreateEnvVarTool,
|
||||
vercel_update_env_var: vercelUpdateEnvVarTool,
|
||||
vercel_delete_env_var: vercelDeleteEnvVarTool,
|
||||
// Vercel - Domains
|
||||
vercel_list_domains: vercelListDomainsTool,
|
||||
vercel_get_domain: vercelGetDomainTool,
|
||||
vercel_add_domain: vercelAddDomainTool,
|
||||
vercel_delete_domain: vercelDeleteDomainTool,
|
||||
vercel_get_domain_config: vercelGetDomainConfigTool,
|
||||
// Vercel - DNS
|
||||
vercel_list_dns_records: vercelListDnsRecordsTool,
|
||||
vercel_create_dns_record: vercelCreateDnsRecordTool,
|
||||
vercel_delete_dns_record: vercelDeleteDnsRecordTool,
|
||||
// Vercel - Aliases
|
||||
vercel_list_aliases: vercelListAliasesTool,
|
||||
vercel_get_alias: vercelGetAliasTool,
|
||||
vercel_create_alias: vercelCreateAliasTool,
|
||||
vercel_delete_alias: vercelDeleteAliasTool,
|
||||
// Vercel - Edge Config
|
||||
vercel_list_edge_configs: vercelListEdgeConfigsTool,
|
||||
vercel_get_edge_config: vercelGetEdgeConfigTool,
|
||||
vercel_create_edge_config: vercelCreateEdgeConfigTool,
|
||||
vercel_get_edge_config_items: vercelGetEdgeConfigItemsTool,
|
||||
vercel_update_edge_config_items: vercelUpdateEdgeConfigItemsTool,
|
||||
// Vercel - Teams & User
|
||||
vercel_list_teams: vercelListTeamsTool,
|
||||
vercel_get_team: vercelGetTeamTool,
|
||||
vercel_list_team_members: vercelListTeamMembersTool,
|
||||
vercel_get_user: vercelGetUserTool,
|
||||
// Webhooks
|
||||
vercel_list_webhooks: vercelListWebhooksTool,
|
||||
vercel_create_webhook: vercelCreateWebhookTool,
|
||||
vercel_delete_webhook: vercelDeleteWebhookTool,
|
||||
// Checks
|
||||
vercel_create_check: vercelCreateCheckTool,
|
||||
vercel_get_check: vercelGetCheckTool,
|
||||
vercel_list_checks: vercelListChecksTool,
|
||||
vercel_update_check: vercelUpdateCheckTool,
|
||||
vercel_rerequest_check: vercelRerequestCheckTool,
|
||||
twilio_send_sms: sendSMSTool,
|
||||
twilio_voice_make_call: makeCallTool,
|
||||
twilio_voice_list_calls: listCallsTool,
|
||||
@@ -2968,19 +2822,6 @@ export const tools: Record<string, ToolConfig> = {
|
||||
clerk_list_sessions: clerkListSessionsTool,
|
||||
clerk_get_session: clerkGetSessionTool,
|
||||
clerk_revoke_session: clerkRevokeSessionTool,
|
||||
cloudflare_list_zones: cloudflareListZonesTool,
|
||||
cloudflare_get_zone: cloudflareGetZoneTool,
|
||||
cloudflare_create_zone: cloudflareCreateZoneTool,
|
||||
cloudflare_delete_zone: cloudflareDeleteZoneTool,
|
||||
cloudflare_list_dns_records: cloudflareListDnsRecordsTool,
|
||||
cloudflare_create_dns_record: cloudflareCreateDnsRecordTool,
|
||||
cloudflare_update_dns_record: cloudflareUpdateDnsRecordTool,
|
||||
cloudflare_delete_dns_record: cloudflareDeleteDnsRecordTool,
|
||||
cloudflare_list_certificates: cloudflareListCertificatesTool,
|
||||
cloudflare_get_zone_settings: cloudflareGetZoneSettingsTool,
|
||||
cloudflare_update_zone_setting: cloudflareUpdateZoneSettingTool,
|
||||
cloudflare_dns_analytics: cloudflareDnsAnalyticsTool,
|
||||
cloudflare_purge_cache: cloudflarePurgeCacheTool,
|
||||
discord_send_message: discordSendMessageTool,
|
||||
discord_get_messages: discordGetMessagesTool,
|
||||
discord_get_server: discordGetServerTool,
|
||||
@@ -3161,23 +3002,6 @@ export const tools: Record<string, ToolConfig> = {
|
||||
onedrive_download: onedriveDownloadTool,
|
||||
onedrive_list: onedriveListTool,
|
||||
onedrive_upload: onedriveUploadTool,
|
||||
microsoft_dataverse_associate: dataverseAssociateTool,
|
||||
microsoft_dataverse_create_multiple: dataverseCreateMultipleTool,
|
||||
microsoft_dataverse_create_record: dataverseCreateRecordTool,
|
||||
microsoft_dataverse_delete_record: dataverseDeleteRecordTool,
|
||||
microsoft_dataverse_disassociate: dataverseDisassociateTool,
|
||||
microsoft_dataverse_download_file: dataverseDownloadFileTool,
|
||||
microsoft_dataverse_execute_action: dataverseExecuteActionTool,
|
||||
microsoft_dataverse_execute_function: dataverseExecuteFunctionTool,
|
||||
microsoft_dataverse_fetchxml_query: dataverseFetchXmlQueryTool,
|
||||
microsoft_dataverse_get_record: dataverseGetRecordTool,
|
||||
microsoft_dataverse_list_records: dataverseListRecordsTool,
|
||||
microsoft_dataverse_search: dataverseSearchTool,
|
||||
microsoft_dataverse_update_multiple: dataverseUpdateMultipleTool,
|
||||
microsoft_dataverse_update_record: dataverseUpdateRecordTool,
|
||||
microsoft_dataverse_upload_file: dataverseUploadFileTool,
|
||||
microsoft_dataverse_upsert_record: dataverseUpsertRecordTool,
|
||||
microsoft_dataverse_whoami: dataverseWhoAmITool,
|
||||
microsoft_excel_read: microsoftExcelReadTool,
|
||||
microsoft_excel_write: microsoftExcelWriteTool,
|
||||
microsoft_excel_table_add: microsoftExcelTableAddTool,
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelAddDomainParams, VercelAddDomainResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelAddDomainTool: ToolConfig<VercelAddDomainParams, VercelAddDomainResponse> = {
|
||||
id: 'vercel_add_domain',
|
||||
name: 'Vercel Add Domain',
|
||||
description: 'Add a new domain to a Vercel account or team',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name to add',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelAddDomainParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v7/domains${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelAddDomainParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelAddDomainParams) => ({
|
||||
method: 'add',
|
||||
name: params.name.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const d = data.domain ?? data
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: d.id ?? null,
|
||||
name: d.name ?? null,
|
||||
verified: d.verified ?? false,
|
||||
createdAt: d.createdAt ?? null,
|
||||
serviceType: d.serviceType ?? null,
|
||||
nameservers: d.nameservers ?? [],
|
||||
intendedNameservers: d.intendedNameservers ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Domain ID' },
|
||||
name: { type: 'string', description: 'Domain name' },
|
||||
verified: { type: 'boolean', description: 'Whether domain is verified' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
serviceType: { type: 'string', description: 'Service type (zeit.world, external, na)' },
|
||||
nameservers: {
|
||||
type: 'array',
|
||||
description: 'Current nameservers',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
intendedNameservers: {
|
||||
type: 'array',
|
||||
description: 'Intended nameservers',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelAddProjectDomainParams,
|
||||
VercelAddProjectDomainResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelAddProjectDomainTool: ToolConfig<
|
||||
VercelAddProjectDomainParams,
|
||||
VercelAddProjectDomainResponse
|
||||
> = {
|
||||
id: 'vercel_add_project_domain',
|
||||
name: 'Vercel Add Project Domain',
|
||||
description: 'Add a domain to a Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or name',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Domain name to add',
|
||||
},
|
||||
redirect: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target domain for redirect',
|
||||
},
|
||||
redirectStatusCode: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'HTTP status code for redirect (301, 302, 307, 308)',
|
||||
},
|
||||
gitBranch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Git branch to link the domain to',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelAddProjectDomainParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v10/projects/${params.projectId.trim()}/domains${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelAddProjectDomainParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelAddProjectDomainParams) => {
|
||||
const body: Record<string, unknown> = { name: params.domain.trim() }
|
||||
if (params.redirect) body.redirect = params.redirect.trim()
|
||||
if (params.redirectStatusCode) body.redirectStatusCode = params.redirectStatusCode
|
||||
if (params.gitBranch) body.gitBranch = params.gitBranch.trim()
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
name: data.name,
|
||||
apexName: data.apexName,
|
||||
verified: data.verified,
|
||||
gitBranch: data.gitBranch ?? null,
|
||||
redirect: data.redirect ?? null,
|
||||
redirectStatusCode: data.redirectStatusCode ?? null,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
name: { type: 'string', description: 'Domain name' },
|
||||
apexName: { type: 'string', description: 'Apex domain name' },
|
||||
verified: { type: 'boolean', description: 'Whether the domain is verified' },
|
||||
gitBranch: { type: 'string', description: 'Git branch for the domain', optional: true },
|
||||
redirect: { type: 'string', description: 'Redirect target domain', optional: true },
|
||||
redirectStatusCode: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code for redirect (301, 302, 307, 308)',
|
||||
optional: true,
|
||||
},
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelCancelDeploymentParams,
|
||||
VercelCancelDeploymentResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCancelDeploymentTool: ToolConfig<
|
||||
VercelCancelDeploymentParams,
|
||||
VercelCancelDeploymentResponse
|
||||
> = {
|
||||
id: 'vercel_cancel_deployment',
|
||||
name: 'Vercel Cancel Deployment',
|
||||
description: 'Cancel a running Vercel deployment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The deployment ID to cancel',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCancelDeploymentParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v12/deployments/${params.deploymentId.trim()}/cancel${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params: VercelCancelDeploymentParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? data.uid,
|
||||
name: data.name ?? null,
|
||||
state: data.readyState ?? data.state ?? 'CANCELED',
|
||||
url: data.url ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Deployment ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Deployment name',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
description: 'Deployment state after cancellation',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Deployment URL',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelCreateAliasParams, VercelCreateAliasResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateAliasTool: ToolConfig<VercelCreateAliasParams, VercelCreateAliasResponse> =
|
||||
{
|
||||
id: 'vercel_create_alias',
|
||||
name: 'Vercel Create Alias',
|
||||
description: 'Assign an alias (domain/subdomain) to a deployment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment ID to assign the alias to',
|
||||
},
|
||||
alias: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain or subdomain to assign as an alias',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateAliasParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v2/deployments/${params.deploymentId.trim()}/aliases${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateAliasParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateAliasParams) => ({
|
||||
alias: params.alias.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
uid: data.uid ?? null,
|
||||
alias: data.alias ?? null,
|
||||
created: data.created ?? null,
|
||||
oldDeploymentId: data.oldDeploymentId ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
uid: {
|
||||
type: 'string',
|
||||
description: 'Alias ID',
|
||||
},
|
||||
alias: {
|
||||
type: 'string',
|
||||
description: 'Alias hostname',
|
||||
},
|
||||
created: {
|
||||
type: 'string',
|
||||
description: 'Creation timestamp as ISO 8601 date-time string',
|
||||
},
|
||||
oldDeploymentId: {
|
||||
type: 'string',
|
||||
description: 'ID of the previously aliased deployment, if the alias was reassigned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelCheckResponse, VercelCreateCheckParams } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateCheckTool: ToolConfig<VercelCreateCheckParams, VercelCheckResponse> = {
|
||||
id: 'vercel_create_check',
|
||||
name: 'Vercel Create Check',
|
||||
description: 'Create a new deployment check',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment ID to create the check for',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the check (max 100 characters)',
|
||||
},
|
||||
blocking: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the check blocks the deployment',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Page path being checked',
|
||||
},
|
||||
detailsUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL with details about the check',
|
||||
},
|
||||
externalId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'External identifier for the check',
|
||||
},
|
||||
rerequestable: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether the check can be rerequested',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateCheckParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateCheckParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateCheckParams) => {
|
||||
const body: Record<string, unknown> = {
|
||||
name: params.name.trim(),
|
||||
blocking: params.blocking,
|
||||
}
|
||||
if (params.path) body.path = params.path
|
||||
if (params.detailsUrl) body.detailsUrl = params.detailsUrl
|
||||
if (params.externalId) body.externalId = params.externalId
|
||||
if (params.rerequestable !== undefined) body.rerequestable = params.rerequestable
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: data.status ?? 'registered',
|
||||
conclusion: data.conclusion ?? null,
|
||||
blocking: data.blocking ?? false,
|
||||
deploymentId: data.deploymentId,
|
||||
integrationId: data.integrationId ?? null,
|
||||
externalId: data.externalId ?? null,
|
||||
detailsUrl: data.detailsUrl ?? null,
|
||||
path: data.path ?? null,
|
||||
rerequestable: data.rerequestable ?? false,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
startedAt: data.startedAt ?? null,
|
||||
completedAt: data.completedAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Check ID' },
|
||||
name: { type: 'string', description: 'Check name' },
|
||||
status: { type: 'string', description: 'Check status: registered, running, or completed' },
|
||||
conclusion: {
|
||||
type: 'string',
|
||||
description: 'Check conclusion: canceled, failed, neutral, succeeded, skipped, or stale',
|
||||
optional: true,
|
||||
},
|
||||
blocking: { type: 'boolean', description: 'Whether the check blocks the deployment' },
|
||||
deploymentId: { type: 'string', description: 'Associated deployment ID' },
|
||||
integrationId: { type: 'string', description: 'Associated integration ID', optional: true },
|
||||
externalId: { type: 'string', description: 'External identifier', optional: true },
|
||||
detailsUrl: { type: 'string', description: 'URL with details about the check', optional: true },
|
||||
path: { type: 'string', description: 'Page path being checked', optional: true },
|
||||
rerequestable: { type: 'boolean', description: 'Whether the check can be rerequested' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
|
||||
startedAt: { type: 'number', description: 'Start timestamp in milliseconds', optional: true },
|
||||
completedAt: {
|
||||
type: 'number',
|
||||
description: 'Completion timestamp in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelCreateDeploymentParams,
|
||||
VercelCreateDeploymentResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateDeploymentTool: ToolConfig<
|
||||
VercelCreateDeploymentParams,
|
||||
VercelCreateDeploymentResponse
|
||||
> = {
|
||||
id: 'vercel_create_deployment',
|
||||
name: 'Vercel Create Deployment',
|
||||
description: 'Create a new deployment or redeploy an existing one',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project name for the deployment',
|
||||
},
|
||||
project: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID (overrides name for project lookup)',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Existing deployment ID to redeploy',
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Target environment: production, staging, or a custom environment identifier',
|
||||
},
|
||||
gitSource: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'JSON string defining the Git Repository source to deploy (e.g. {"type":"github","repo":"owner/repo","ref":"main"})',
|
||||
},
|
||||
forceNew: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Forces a new deployment even if there is a previous similar deployment (0 or 1)',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateDeploymentParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.forceNew) query.set('forceNew', params.forceNew)
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v13/deployments${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateDeploymentParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateDeploymentParams) => {
|
||||
const body: Record<string, any> = {
|
||||
name: params.name.trim(),
|
||||
}
|
||||
if (params.project) body.project = params.project.trim()
|
||||
if (params.deploymentId) body.deploymentId = params.deploymentId.trim()
|
||||
if (params.target) body.target = params.target
|
||||
if (params.gitSource) {
|
||||
try {
|
||||
body.gitSource = JSON.parse(params.gitSource)
|
||||
} catch {
|
||||
body.gitSource = params.gitSource
|
||||
}
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
url: data.url ?? '',
|
||||
readyState: data.readyState ?? 'QUEUED',
|
||||
projectId: data.projectId ?? '',
|
||||
createdAt: data.createdAt ?? data.created,
|
||||
alias: data.alias ?? [],
|
||||
target: data.target ?? null,
|
||||
inspectorUrl: data.inspectorUrl ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Deployment ID' },
|
||||
name: { type: 'string', description: 'Deployment name' },
|
||||
url: { type: 'string', description: 'Unique deployment URL' },
|
||||
readyState: {
|
||||
type: 'string',
|
||||
description: 'Deployment ready state: QUEUED, BUILDING, ERROR, INITIALIZING, READY, CANCELED',
|
||||
},
|
||||
projectId: { type: 'string', description: 'Associated project ID' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
alias: {
|
||||
type: 'array',
|
||||
description: 'Assigned aliases',
|
||||
items: { type: 'string', description: 'Alias domain' },
|
||||
},
|
||||
target: { type: 'string', description: 'Target environment', optional: true },
|
||||
inspectorUrl: { type: 'string', description: 'Vercel inspector URL' },
|
||||
},
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelCreateDnsRecordParams,
|
||||
VercelCreateDnsRecordResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateDnsRecordTool: ToolConfig<
|
||||
VercelCreateDnsRecordParams,
|
||||
VercelCreateDnsRecordResponse
|
||||
> = {
|
||||
id: 'vercel_create_dns_record',
|
||||
name: 'Vercel Create DNS Record',
|
||||
description: 'Create a DNS record for a domain in a Vercel account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name to create the record for',
|
||||
},
|
||||
recordName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The subdomain or record name',
|
||||
},
|
||||
recordType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'DNS record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, TXT, NS)',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The value of the DNS record',
|
||||
},
|
||||
ttl: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Time to live in seconds',
|
||||
},
|
||||
mxPriority: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Priority for MX records',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateDnsRecordParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v2/domains/${params.domain.trim()}/records${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateDnsRecordParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateDnsRecordParams) => {
|
||||
const body: Record<string, unknown> = {
|
||||
name: params.recordName.trim(),
|
||||
type: params.recordType.trim(),
|
||||
value: params.value.trim(),
|
||||
}
|
||||
if (params.ttl != null) body.ttl = params.ttl
|
||||
if (params.mxPriority != null) body.mxPriority = params.mxPriority
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const d = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
uid: d.uid ?? null,
|
||||
updated: d.updated ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
uid: { type: 'string', description: 'The DNS record ID' },
|
||||
updated: { type: 'number', description: 'Timestamp of the update' },
|
||||
},
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelCreateEdgeConfigParams,
|
||||
VercelCreateEdgeConfigResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateEdgeConfigTool: ToolConfig<
|
||||
VercelCreateEdgeConfigParams,
|
||||
VercelCreateEdgeConfigResponse
|
||||
> = {
|
||||
id: 'vercel_create_edge_config',
|
||||
name: 'Vercel Create Edge Config',
|
||||
description: 'Create a new Edge Config store',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name/slug for the new Edge Config',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateEdgeConfigParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/edge-config${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateEdgeConfigParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateEdgeConfigParams) => ({
|
||||
slug: params.slug.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
slug: data.slug ?? null,
|
||||
ownerId: data.ownerId ?? null,
|
||||
digest: data.digest ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
updatedAt: data.updatedAt ?? null,
|
||||
itemCount: data.itemCount ?? 0,
|
||||
sizeInBytes: data.sizeInBytes ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Edge Config ID',
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
description: 'Edge Config slug',
|
||||
},
|
||||
ownerId: {
|
||||
type: 'string',
|
||||
description: 'Owner ID',
|
||||
},
|
||||
digest: {
|
||||
type: 'string',
|
||||
description: 'Content digest hash',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'number',
|
||||
description: 'Creation timestamp',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'number',
|
||||
description: 'Last update timestamp',
|
||||
},
|
||||
itemCount: {
|
||||
type: 'number',
|
||||
description: 'Number of items',
|
||||
},
|
||||
sizeInBytes: {
|
||||
type: 'number',
|
||||
description: 'Size in bytes',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelCreateEnvVarParams, VercelCreateEnvVarResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateEnvVarTool: ToolConfig<
|
||||
VercelCreateEnvVarParams,
|
||||
VercelCreateEnvVarResponse
|
||||
> = {
|
||||
id: 'vercel_create_env_var',
|
||||
name: 'Vercel Create Environment Variable',
|
||||
description: 'Create an environment variable for a Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or name',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Environment variable name',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Environment variable value',
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of target environments (production, preview, development)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Variable type: system, secret, encrypted, plain, or sensitive (default: plain)',
|
||||
},
|
||||
gitBranch: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Git branch to associate with the variable (requires target to include preview)',
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comment to add context to the variable (max 500 characters)',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateEnvVarParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v10/projects/${params.projectId.trim()}/env${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateEnvVarParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateEnvVarParams) => {
|
||||
const body: Record<string, unknown> = {
|
||||
key: params.key,
|
||||
value: params.value,
|
||||
target: params.target.split(',').map((t) => t.trim()),
|
||||
type: params.type || 'plain',
|
||||
}
|
||||
if (params.gitBranch) body.gitBranch = params.gitBranch
|
||||
if (params.comment) body.comment = params.comment
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const env = data.created ?? data
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: env.id,
|
||||
key: env.key,
|
||||
value: env.value ?? '',
|
||||
type: env.type ?? 'plain',
|
||||
target: env.target ?? [],
|
||||
gitBranch: env.gitBranch ?? null,
|
||||
comment: env.comment ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Environment variable ID',
|
||||
},
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Variable name',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Variable value',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Variable type (secret, system, encrypted, plain, sensitive)',
|
||||
},
|
||||
target: {
|
||||
type: 'array',
|
||||
description: 'Target environments',
|
||||
items: { type: 'string', description: 'Environment name' },
|
||||
},
|
||||
gitBranch: {
|
||||
type: 'string',
|
||||
description: 'Git branch filter',
|
||||
optional: true,
|
||||
},
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Comment providing context for the variable',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelCreateProjectParams, VercelCreateProjectResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateProjectTool: ToolConfig<
|
||||
VercelCreateProjectParams,
|
||||
VercelCreateProjectResponse
|
||||
> = {
|
||||
id: 'vercel_create_project',
|
||||
name: 'Vercel Create Project',
|
||||
description: 'Create a new Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project name',
|
||||
},
|
||||
framework: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project framework (e.g. nextjs, remix, vite)',
|
||||
},
|
||||
gitRepository: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Git repository connection object with type and repo',
|
||||
},
|
||||
buildCommand: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom build command',
|
||||
},
|
||||
outputDirectory: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom output directory',
|
||||
},
|
||||
installCommand: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom install command',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateProjectParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v11/projects${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateProjectParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateProjectParams) => {
|
||||
const body: Record<string, unknown> = { name: params.name.trim() }
|
||||
if (params.framework) body.framework = params.framework.trim()
|
||||
if (params.gitRepository) body.gitRepository = params.gitRepository
|
||||
if (params.buildCommand) body.buildCommand = params.buildCommand.trim()
|
||||
if (params.outputDirectory) body.outputDirectory = params.outputDirectory.trim()
|
||||
if (params.installCommand) body.installCommand = params.installCommand.trim()
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
framework: data.framework ?? null,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Project ID' },
|
||||
name: { type: 'string', description: 'Project name' },
|
||||
framework: { type: 'string', description: 'Project framework', optional: true },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelCreateWebhookParams, VercelCreateWebhookResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelCreateWebhookTool: ToolConfig<
|
||||
VercelCreateWebhookParams,
|
||||
VercelCreateWebhookResponse
|
||||
> = {
|
||||
id: 'vercel_create_webhook',
|
||||
name: 'Vercel Create Webhook',
|
||||
description: 'Create a new webhook for a Vercel team',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Webhook URL (must be https)',
|
||||
},
|
||||
events: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated event names to subscribe to',
|
||||
},
|
||||
projectIds: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated project IDs to scope the webhook to',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to create the webhook for',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelCreateWebhookParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/webhooks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: VercelCreateWebhookParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: VercelCreateWebhookParams) => {
|
||||
const body: Record<string, any> = {
|
||||
url: params.url.trim(),
|
||||
events: params.events.split(',').map((e) => e.trim()),
|
||||
}
|
||||
if (params.projectIds) {
|
||||
body.projectIds = params.projectIds.split(',').map((p) => p.trim())
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
url: data.url ?? null,
|
||||
secret: data.secret ?? null,
|
||||
events: data.events ?? [],
|
||||
ownerId: data.ownerId ?? null,
|
||||
projectIds: data.projectIds ?? [],
|
||||
createdAt: data.createdAt ?? null,
|
||||
updatedAt: data.updatedAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Webhook ID' },
|
||||
url: { type: 'string', description: 'Webhook URL' },
|
||||
secret: { type: 'string', description: 'Webhook signing secret' },
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'Events the webhook listens to',
|
||||
items: { type: 'string', description: 'Event name' },
|
||||
},
|
||||
ownerId: { type: 'string', description: 'Owner ID' },
|
||||
projectIds: {
|
||||
type: 'array',
|
||||
description: 'Associated project IDs',
|
||||
items: { type: 'string', description: 'Project ID' },
|
||||
},
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last updated timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelDeleteAliasParams, VercelDeleteAliasResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteAliasTool: ToolConfig<VercelDeleteAliasParams, VercelDeleteAliasResponse> =
|
||||
{
|
||||
id: 'vercel_delete_alias',
|
||||
name: 'Vercel Delete Alias',
|
||||
description: 'Delete an alias by its ID',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
aliasId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Alias ID to delete',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteAliasParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v2/aliases/${params.aliasId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteAliasParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
status: data.status ?? 'SUCCESS',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Deletion status (SUCCESS)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelDeleteDeploymentParams,
|
||||
VercelDeleteDeploymentResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteDeploymentTool: ToolConfig<
|
||||
VercelDeleteDeploymentParams,
|
||||
VercelDeleteDeploymentResponse
|
||||
> = {
|
||||
id: 'vercel_delete_deployment',
|
||||
name: 'Vercel Delete Deployment',
|
||||
description: 'Delete a Vercel deployment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The deployment ID or URL to delete',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteDeploymentParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const id = params.deploymentId.trim()
|
||||
if (id.includes('.')) {
|
||||
query.set('url', id)
|
||||
}
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v13/deployments/${id}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteDeploymentParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
uid: data.uid ?? data.id ?? null,
|
||||
state: data.state ?? 'DELETED',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
uid: {
|
||||
type: 'string',
|
||||
description: 'The removed deployment ID',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
description: 'Deployment state after deletion (DELETED)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelDeleteDnsRecordParams,
|
||||
VercelDeleteDnsRecordResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteDnsRecordTool: ToolConfig<
|
||||
VercelDeleteDnsRecordParams,
|
||||
VercelDeleteDnsRecordResponse
|
||||
> = {
|
||||
id: 'vercel_delete_dns_record',
|
||||
name: 'Vercel Delete DNS Record',
|
||||
description: 'Delete a DNS record for a domain in a Vercel account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name the record belongs to',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the DNS record to delete',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteDnsRecordParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v2/domains/${params.domain.trim()}/records/${params.recordId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteDnsRecordParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async () => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
|
||||
},
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelDeleteDomainParams, VercelDeleteDomainResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteDomainTool: ToolConfig<
|
||||
VercelDeleteDomainParams,
|
||||
VercelDeleteDomainResponse
|
||||
> = {
|
||||
id: 'vercel_delete_domain',
|
||||
name: 'Vercel Delete Domain',
|
||||
description: 'Delete a domain from a Vercel account or team',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name to delete',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteDomainParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v6/domains/${params.domain.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteDomainParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const d = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
uid: d.uid ?? null,
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
uid: { type: 'string', description: 'The ID of the deleted domain' },
|
||||
deleted: { type: 'boolean', description: 'Whether the domain was deleted' },
|
||||
},
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelDeleteEnvVarParams, VercelDeleteEnvVarResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteEnvVarTool: ToolConfig<
|
||||
VercelDeleteEnvVarParams,
|
||||
VercelDeleteEnvVarResponse
|
||||
> = {
|
||||
id: 'vercel_delete_env_var',
|
||||
name: 'Vercel Delete Environment Variable',
|
||||
description: 'Delete an environment variable from a Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or name',
|
||||
},
|
||||
envId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Environment variable ID to delete',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteEnvVarParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}/env/${params.envId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteEnvVarParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async () => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the environment variable was successfully deleted',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelDeleteProjectParams, VercelDeleteProjectResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteProjectTool: ToolConfig<
|
||||
VercelDeleteProjectParams,
|
||||
VercelDeleteProjectResponse
|
||||
> = {
|
||||
id: 'vercel_delete_project',
|
||||
name: 'Vercel Delete Project',
|
||||
description: 'Delete a Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or name',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteProjectParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteProjectParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async () => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the project was successfully deleted' },
|
||||
},
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelDeleteWebhookParams, VercelDeleteWebhookResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelDeleteWebhookTool: ToolConfig<
|
||||
VercelDeleteWebhookParams,
|
||||
VercelDeleteWebhookResponse
|
||||
> = {
|
||||
id: 'vercel_delete_webhook',
|
||||
name: 'Vercel Delete Webhook',
|
||||
description: 'Delete a webhook from a Vercel team',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
webhookId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The webhook ID to delete',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelDeleteWebhookParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/webhooks/${params.webhookId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params: VercelDeleteWebhookParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async () => {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the webhook was successfully deleted',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetAliasParams, VercelGetAliasResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetAliasTool: ToolConfig<VercelGetAliasParams, VercelGetAliasResponse> = {
|
||||
id: 'vercel_get_alias',
|
||||
name: 'Vercel Get Alias',
|
||||
description: 'Get details about a specific alias by ID or hostname',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
aliasId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Alias ID or hostname to look up',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetAliasParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v4/aliases/${params.aliasId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetAliasParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
uid: data.uid ?? null,
|
||||
alias: data.alias ?? null,
|
||||
deploymentId: data.deploymentId ?? null,
|
||||
projectId: data.projectId ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
updatedAt: data.updatedAt ?? null,
|
||||
redirect: data.redirect ?? null,
|
||||
redirectStatusCode: data.redirectStatusCode ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
uid: {
|
||||
type: 'string',
|
||||
description: 'Alias ID',
|
||||
},
|
||||
alias: {
|
||||
type: 'string',
|
||||
description: 'Alias hostname',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
description: 'Associated deployment ID',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
description: 'Associated project ID',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'number',
|
||||
description: 'Creation timestamp in milliseconds',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'number',
|
||||
description: 'Last update timestamp in milliseconds',
|
||||
},
|
||||
redirect: {
|
||||
type: 'string',
|
||||
description: 'Target domain for redirect aliases',
|
||||
},
|
||||
redirectStatusCode: {
|
||||
type: 'number',
|
||||
description: 'HTTP status code for redirect (301, 302, 307, or 308)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelCheckResponse, VercelGetCheckParams } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetCheckTool: ToolConfig<VercelGetCheckParams, VercelCheckResponse> = {
|
||||
id: 'vercel_get_check',
|
||||
name: 'Vercel Get Check',
|
||||
description: 'Get details of a specific deployment check',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment ID the check belongs to',
|
||||
},
|
||||
checkId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Check ID to retrieve',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetCheckParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks/${params.checkId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetCheckParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: data.status ?? 'registered',
|
||||
conclusion: data.conclusion ?? null,
|
||||
blocking: data.blocking ?? false,
|
||||
deploymentId: data.deploymentId,
|
||||
integrationId: data.integrationId ?? null,
|
||||
externalId: data.externalId ?? null,
|
||||
detailsUrl: data.detailsUrl ?? null,
|
||||
path: data.path ?? null,
|
||||
rerequestable: data.rerequestable ?? false,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
startedAt: data.startedAt ?? null,
|
||||
completedAt: data.completedAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Check ID' },
|
||||
name: { type: 'string', description: 'Check name' },
|
||||
status: { type: 'string', description: 'Check status: registered, running, or completed' },
|
||||
conclusion: {
|
||||
type: 'string',
|
||||
description: 'Check conclusion: canceled, failed, neutral, succeeded, skipped, or stale',
|
||||
optional: true,
|
||||
},
|
||||
blocking: { type: 'boolean', description: 'Whether the check blocks the deployment' },
|
||||
deploymentId: { type: 'string', description: 'Associated deployment ID' },
|
||||
integrationId: { type: 'string', description: 'Associated integration ID', optional: true },
|
||||
externalId: { type: 'string', description: 'External identifier', optional: true },
|
||||
detailsUrl: { type: 'string', description: 'URL with details about the check', optional: true },
|
||||
path: { type: 'string', description: 'Page path being checked', optional: true },
|
||||
rerequestable: { type: 'boolean', description: 'Whether the check can be rerequested' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
|
||||
startedAt: { type: 'number', description: 'Start timestamp in milliseconds', optional: true },
|
||||
completedAt: {
|
||||
type: 'number',
|
||||
description: 'Completion timestamp in milliseconds',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetDeploymentParams, VercelGetDeploymentResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetDeploymentTool: ToolConfig<
|
||||
VercelGetDeploymentParams,
|
||||
VercelGetDeploymentResponse
|
||||
> = {
|
||||
id: 'vercel_get_deployment',
|
||||
name: 'Vercel Get Deployment',
|
||||
description: 'Get details of a specific Vercel deployment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique deployment identifier or hostname',
|
||||
},
|
||||
withGitRepoInfo: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to add in gitRepo information (true/false)',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetDeploymentParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.withGitRepoInfo) query.set('withGitRepoInfo', params.withGitRepoInfo)
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v13/deployments/${params.deploymentId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetDeploymentParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
url: data.url ?? '',
|
||||
readyState: data.readyState ?? 'UNKNOWN',
|
||||
status: data.status ?? data.readyState ?? 'UNKNOWN',
|
||||
target: data.target ?? null,
|
||||
createdAt: data.createdAt ?? data.created,
|
||||
buildingAt: data.buildingAt ?? null,
|
||||
ready: data.ready ?? null,
|
||||
source: data.source ?? '',
|
||||
alias: data.alias ?? [],
|
||||
regions: data.regions ?? [],
|
||||
inspectorUrl: data.inspectorUrl ?? '',
|
||||
projectId: data.projectId ?? '',
|
||||
creator: {
|
||||
uid: data.creator?.uid ?? '',
|
||||
username: data.creator?.username ?? '',
|
||||
},
|
||||
project: data.project
|
||||
? {
|
||||
id: data.project.id,
|
||||
name: data.project.name,
|
||||
framework: data.project.framework ?? null,
|
||||
}
|
||||
: null,
|
||||
meta: data.meta ?? {},
|
||||
gitSource: data.gitSource ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Deployment ID' },
|
||||
name: { type: 'string', description: 'Deployment name' },
|
||||
url: { type: 'string', description: 'Unique deployment URL' },
|
||||
readyState: {
|
||||
type: 'string',
|
||||
description: 'Deployment ready state: QUEUED, BUILDING, ERROR, INITIALIZING, READY, CANCELED',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Deployment status',
|
||||
},
|
||||
target: { type: 'string', description: 'Target environment', optional: true },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
buildingAt: { type: 'number', description: 'Build start timestamp', optional: true },
|
||||
ready: { type: 'number', description: 'Ready timestamp', optional: true },
|
||||
source: {
|
||||
type: 'string',
|
||||
description: 'Deployment source: cli, git, redeploy, import, v0-web, etc.',
|
||||
},
|
||||
alias: {
|
||||
type: 'array',
|
||||
description: 'Assigned aliases',
|
||||
items: { type: 'string', description: 'Alias domain' },
|
||||
},
|
||||
regions: {
|
||||
type: 'array',
|
||||
description: 'Deployment regions',
|
||||
items: { type: 'string', description: 'Region code' },
|
||||
},
|
||||
inspectorUrl: { type: 'string', description: 'Vercel inspector URL' },
|
||||
projectId: { type: 'string', description: 'Associated project ID' },
|
||||
creator: {
|
||||
type: 'object',
|
||||
description: 'Creator information',
|
||||
properties: {
|
||||
uid: { type: 'string', description: 'Creator user ID' },
|
||||
username: { type: 'string', description: 'Creator username' },
|
||||
},
|
||||
},
|
||||
project: {
|
||||
type: 'object',
|
||||
description: 'Associated project',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Project ID' },
|
||||
name: { type: 'string', description: 'Project name' },
|
||||
framework: { type: 'string', description: 'Project framework', optional: true },
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
description: 'Deployment metadata (key-value strings)',
|
||||
properties: {
|
||||
githubCommitSha: { type: 'string', description: 'GitHub commit SHA', optional: true },
|
||||
githubCommitMessage: {
|
||||
type: 'string',
|
||||
description: 'GitHub commit message',
|
||||
optional: true,
|
||||
},
|
||||
githubCommitRef: { type: 'string', description: 'GitHub branch/ref', optional: true },
|
||||
githubRepo: { type: 'string', description: 'GitHub repository', optional: true },
|
||||
githubOrg: { type: 'string', description: 'GitHub organization', optional: true },
|
||||
githubCommitAuthorName: {
|
||||
type: 'string',
|
||||
description: 'Commit author name',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
gitSource: {
|
||||
type: 'object',
|
||||
description: 'Git source information',
|
||||
optional: true,
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Git provider type (e.g., github, gitlab, bitbucket)',
|
||||
},
|
||||
ref: { type: 'string', description: 'Git ref (branch or tag)' },
|
||||
sha: { type: 'string', description: 'Git commit SHA' },
|
||||
repoId: { type: 'string', description: 'Repository ID', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelGetDeploymentEventsParams,
|
||||
VercelGetDeploymentEventsResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetDeploymentEventsTool: ToolConfig<
|
||||
VercelGetDeploymentEventsParams,
|
||||
VercelGetDeploymentEventsResponse
|
||||
> = {
|
||||
id: 'vercel_get_deployment_events',
|
||||
name: 'Vercel Get Deployment Events',
|
||||
description: 'Get build and runtime events for a Vercel deployment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The unique deployment identifier or hostname',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Order of events by timestamp: backward or forward (default: forward)',
|
||||
},
|
||||
follow: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'When set to 1, returns live events as they happen',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of events to return (-1 for all)',
|
||||
},
|
||||
since: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Timestamp to start pulling build logs from',
|
||||
},
|
||||
until: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Timestamp to stop pulling build logs at',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetDeploymentEventsParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.direction) query.set('direction', params.direction)
|
||||
if (params.follow !== undefined) query.set('follow', String(params.follow))
|
||||
if (params.limit !== undefined) query.set('limit', String(params.limit))
|
||||
if (params.since !== undefined) query.set('since', String(params.since))
|
||||
if (params.until !== undefined) query.set('until', String(params.until))
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v3/deployments/${params.deploymentId.trim()}/events${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetDeploymentEventsParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const events = (Array.isArray(data) ? data : (data.events ?? [])).map((e: any) => ({
|
||||
type: e.type ?? null,
|
||||
created: e.created ?? null,
|
||||
date: e.date ?? null,
|
||||
text: e.text ?? e.payload?.text ?? null,
|
||||
serial: e.serial ?? null,
|
||||
deploymentId: e.deploymentId ?? e.payload?.deploymentId ?? null,
|
||||
id: e.id ?? null,
|
||||
level: e.level ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
events,
|
||||
count: events.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'List of deployment events',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Event type: delimiter, command, stdout, stderr, exit, deployment-state, middleware, middleware-invocation, edge-function-invocation, metric, report, fatal',
|
||||
},
|
||||
created: { type: 'number', description: 'Event creation timestamp' },
|
||||
date: { type: 'number', description: 'Event date timestamp' },
|
||||
text: { type: 'string', description: 'Event text content' },
|
||||
serial: { type: 'string', description: 'Event serial identifier' },
|
||||
deploymentId: { type: 'string', description: 'Associated deployment ID' },
|
||||
id: { type: 'string', description: 'Event unique identifier' },
|
||||
level: { type: 'string', description: 'Event level: error or warning' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of events returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetDomainParams, VercelGetDomainResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetDomainTool: ToolConfig<VercelGetDomainParams, VercelGetDomainResponse> = {
|
||||
id: 'vercel_get_domain',
|
||||
name: 'Vercel Get Domain',
|
||||
description: 'Get information about a specific domain in a Vercel account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name to retrieve',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetDomainParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v5/domains/${params.domain.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetDomainParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const d = data.domain ?? data
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: d.id ?? null,
|
||||
name: d.name ?? null,
|
||||
verified: d.verified ?? false,
|
||||
createdAt: d.createdAt ?? null,
|
||||
expiresAt: d.expiresAt ?? null,
|
||||
serviceType: d.serviceType ?? null,
|
||||
nameservers: d.nameservers ?? [],
|
||||
intendedNameservers: d.intendedNameservers ?? [],
|
||||
customNameservers: d.customNameservers ?? [],
|
||||
renew: d.renew ?? false,
|
||||
boughtAt: d.boughtAt ?? null,
|
||||
transferredAt: d.transferredAt ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Domain ID' },
|
||||
name: { type: 'string', description: 'Domain name' },
|
||||
verified: { type: 'boolean', description: 'Whether domain is verified' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
expiresAt: { type: 'number', description: 'Expiration timestamp' },
|
||||
serviceType: { type: 'string', description: 'Service type (zeit.world, external, na)' },
|
||||
nameservers: {
|
||||
type: 'array',
|
||||
description: 'Current nameservers',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
intendedNameservers: {
|
||||
type: 'array',
|
||||
description: 'Intended nameservers',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
customNameservers: {
|
||||
type: 'array',
|
||||
description: 'Custom nameservers',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
renew: { type: 'boolean', description: 'Whether auto-renewal is enabled' },
|
||||
boughtAt: { type: 'number', description: 'Purchase timestamp' },
|
||||
transferredAt: { type: 'number', description: 'Transfer completion timestamp' },
|
||||
},
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelGetDomainConfigParams,
|
||||
VercelGetDomainConfigResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetDomainConfigTool: ToolConfig<
|
||||
VercelGetDomainConfigParams,
|
||||
VercelGetDomainConfigResponse
|
||||
> = {
|
||||
id: 'vercel_get_domain_config',
|
||||
name: 'Vercel Get Domain Config',
|
||||
description: 'Get the configuration for a domain in a Vercel account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The domain name to get configuration for',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetDomainConfigParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v6/domains/${params.domain.trim()}/config${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetDomainConfigParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const d = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
configuredBy: d.configuredBy ?? null,
|
||||
acceptedChallenges: d.acceptedChallenges ?? [],
|
||||
misconfigured: d.misconfigured ?? false,
|
||||
recommendedIPv4: d.recommendedIPv4 ?? [],
|
||||
recommendedCNAME: d.recommendedCNAME ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
configuredBy: {
|
||||
type: 'string',
|
||||
description: 'How the domain is configured (CNAME, A, http, dns-01, or null)',
|
||||
},
|
||||
acceptedChallenges: {
|
||||
type: 'array',
|
||||
description: 'Accepted challenge types for certificate issuance (dns-01, http-01)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
misconfigured: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the domain is misconfigured for TLS certificate generation',
|
||||
},
|
||||
recommendedIPv4: {
|
||||
type: 'array',
|
||||
description: 'Recommended IPv4 addresses with rank values',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
rank: { type: 'number', description: 'Priority rank (1 is preferred)' },
|
||||
value: {
|
||||
type: 'array',
|
||||
description: 'IPv4 addresses',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
recommendedCNAME: {
|
||||
type: 'array',
|
||||
description: 'Recommended CNAME records with rank values',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
rank: { type: 'number', description: 'Priority rank (1 is preferred)' },
|
||||
value: { type: 'string', description: 'CNAME value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetEdgeConfigParams, VercelGetEdgeConfigResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetEdgeConfigTool: ToolConfig<
|
||||
VercelGetEdgeConfigParams,
|
||||
VercelGetEdgeConfigResponse
|
||||
> = {
|
||||
id: 'vercel_get_edge_config',
|
||||
name: 'Vercel Get Edge Config',
|
||||
description: 'Get details about a specific Edge Config store',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
edgeConfigId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Edge Config ID to look up',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetEdgeConfigParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/edge-config/${params.edgeConfigId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetEdgeConfigParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id ?? null,
|
||||
slug: data.slug ?? null,
|
||||
ownerId: data.ownerId ?? null,
|
||||
digest: data.digest ?? null,
|
||||
createdAt: data.createdAt ?? null,
|
||||
updatedAt: data.updatedAt ?? null,
|
||||
itemCount: data.itemCount ?? 0,
|
||||
sizeInBytes: data.sizeInBytes ?? 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Edge Config ID',
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
description: 'Edge Config slug',
|
||||
},
|
||||
ownerId: {
|
||||
type: 'string',
|
||||
description: 'Owner ID',
|
||||
},
|
||||
digest: {
|
||||
type: 'string',
|
||||
description: 'Content digest hash',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'number',
|
||||
description: 'Creation timestamp',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'number',
|
||||
description: 'Last update timestamp',
|
||||
},
|
||||
itemCount: {
|
||||
type: 'number',
|
||||
description: 'Number of items',
|
||||
},
|
||||
sizeInBytes: {
|
||||
type: 'number',
|
||||
description: 'Size in bytes',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type {
|
||||
VercelGetEdgeConfigItemsParams,
|
||||
VercelGetEdgeConfigItemsResponse,
|
||||
} from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetEdgeConfigItemsTool: ToolConfig<
|
||||
VercelGetEdgeConfigItemsParams,
|
||||
VercelGetEdgeConfigItemsResponse
|
||||
> = {
|
||||
id: 'vercel_get_edge_config_items',
|
||||
name: 'Vercel Get Edge Config Items',
|
||||
description: 'Get all items in an Edge Config store',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
edgeConfigId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Edge Config ID to get items from',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetEdgeConfigItemsParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/edge-config/${params.edgeConfigId.trim()}/items${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetEdgeConfigItemsParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const rawItems = Array.isArray(data) ? data : (data.items ?? [])
|
||||
const items = rawItems.map((item: any) => ({
|
||||
key: item.key ?? null,
|
||||
value: item.value ?? null,
|
||||
description: item.description ?? null,
|
||||
edgeConfigId: item.edgeConfigId ?? null,
|
||||
createdAt: item.createdAt ?? null,
|
||||
updatedAt: item.updatedAt ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
items,
|
||||
count: items.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'List of Edge Config items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'string', description: 'Item key' },
|
||||
value: { type: 'json', description: 'Item value' },
|
||||
description: { type: 'string', description: 'Item description' },
|
||||
edgeConfigId: { type: 'string', description: 'Parent Edge Config ID' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of items returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetEnvVarsParams, VercelGetEnvVarsResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetEnvVarsTool: ToolConfig<VercelGetEnvVarsParams, VercelGetEnvVarsResponse> = {
|
||||
id: 'vercel_get_env_vars',
|
||||
name: 'Vercel Get Environment Variables',
|
||||
description: 'Retrieve environment variables for a Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or name',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetEnvVarsParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v10/projects/${params.projectId.trim()}/env${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetEnvVarsParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const envs = (data.envs ?? []).map((e: any) => ({
|
||||
id: e.id,
|
||||
key: e.key,
|
||||
value: e.value ?? '',
|
||||
type: e.type ?? 'plain',
|
||||
target: e.target ?? [],
|
||||
gitBranch: e.gitBranch ?? null,
|
||||
comment: e.comment ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
envs,
|
||||
count: envs.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
envs: {
|
||||
type: 'array',
|
||||
description: 'List of environment variables',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Environment variable ID' },
|
||||
key: { type: 'string', description: 'Variable name' },
|
||||
value: { type: 'string', description: 'Variable value' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Variable type (secret, system, encrypted, plain, sensitive)',
|
||||
},
|
||||
target: {
|
||||
type: 'array',
|
||||
description: 'Target environments',
|
||||
items: { type: 'string', description: 'Environment name' },
|
||||
},
|
||||
gitBranch: { type: 'string', description: 'Git branch filter', optional: true },
|
||||
comment: {
|
||||
type: 'string',
|
||||
description: 'Comment providing context for the variable',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of environment variables returned',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetProjectParams, VercelGetProjectResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetProjectTool: ToolConfig<VercelGetProjectParams, VercelGetProjectResponse> = {
|
||||
id: 'vercel_get_project',
|
||||
name: 'Vercel Get Project',
|
||||
description: 'Get details of a specific Vercel project',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID or name',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetProjectParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetProjectParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
framework: data.framework ?? null,
|
||||
createdAt: data.createdAt,
|
||||
updatedAt: data.updatedAt,
|
||||
domains: data.domains ?? [],
|
||||
link: data.link
|
||||
? {
|
||||
type: data.link.type,
|
||||
repo: data.link.repo,
|
||||
org: data.link.org,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Project ID' },
|
||||
name: { type: 'string', description: 'Project name' },
|
||||
framework: { type: 'string', description: 'Project framework', optional: true },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last updated timestamp' },
|
||||
domains: {
|
||||
type: 'array',
|
||||
description: 'Project domains',
|
||||
items: { type: 'string', description: 'Domain' },
|
||||
},
|
||||
link: {
|
||||
type: 'object',
|
||||
description: 'Git repository connection',
|
||||
optional: true,
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Repository type (github, gitlab, bitbucket)' },
|
||||
repo: { type: 'string', description: 'Repository name' },
|
||||
org: { type: 'string', description: 'Organization or owner' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetTeamParams, VercelGetTeamResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetTeamTool: ToolConfig<VercelGetTeamParams, VercelGetTeamResponse> = {
|
||||
id: 'vercel_get_team',
|
||||
name: 'Vercel Get Team',
|
||||
description: 'Get information about a specific Vercel team',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The team ID to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelGetTeamParams) => `https://api.vercel.com/v2/teams/${params.teamId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetTeamParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const d = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: d.id ?? null,
|
||||
slug: d.slug ?? null,
|
||||
name: d.name ?? null,
|
||||
avatar: d.avatar ?? null,
|
||||
description: d.description ?? null,
|
||||
createdAt: d.createdAt ?? null,
|
||||
updatedAt: d.updatedAt ?? null,
|
||||
creatorId: d.creatorId ?? null,
|
||||
membership: d.membership
|
||||
? {
|
||||
uid: d.membership.uid ?? null,
|
||||
teamId: d.membership.teamId ?? null,
|
||||
role: d.membership.role ?? null,
|
||||
confirmed: d.membership.confirmed ?? false,
|
||||
created: d.membership.created ?? null,
|
||||
createdAt: d.membership.createdAt ?? null,
|
||||
accessRequestedAt: d.membership.accessRequestedAt ?? null,
|
||||
teamRoles: d.membership.teamRoles ?? [],
|
||||
teamPermissions: d.membership.teamPermissions ?? [],
|
||||
}
|
||||
: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Team ID' },
|
||||
slug: { type: 'string', description: 'Team slug' },
|
||||
name: { type: 'string', description: 'Team name' },
|
||||
avatar: { type: 'string', description: 'Avatar file ID' },
|
||||
description: { type: 'string', description: 'Short team description' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
|
||||
creatorId: { type: 'string', description: 'User ID of team creator' },
|
||||
membership: {
|
||||
type: 'object',
|
||||
description: 'Current user membership details',
|
||||
properties: {
|
||||
uid: { type: 'string', description: 'User ID of the member' },
|
||||
teamId: { type: 'string', description: 'Team ID' },
|
||||
role: { type: 'string', description: 'Membership role' },
|
||||
confirmed: { type: 'boolean', description: 'Whether membership is confirmed' },
|
||||
created: { type: 'number', description: 'Membership creation timestamp' },
|
||||
createdAt: { type: 'number', description: 'Membership creation timestamp (milliseconds)' },
|
||||
accessRequestedAt: { type: 'number', description: 'When access was requested' },
|
||||
teamRoles: {
|
||||
type: 'array',
|
||||
description: 'Team role assignments',
|
||||
items: { type: 'string', description: 'Role name' },
|
||||
},
|
||||
teamPermissions: {
|
||||
type: 'array',
|
||||
description: 'Team permission assignments',
|
||||
items: { type: 'string', description: 'Permission name' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelGetUserParams, VercelGetUserResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelGetUserTool: ToolConfig<VercelGetUserParams, VercelGetUserResponse> = {
|
||||
id: 'vercel_get_user',
|
||||
name: 'Vercel Get User',
|
||||
description: 'Get information about the authenticated Vercel user',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.vercel.com/v2/user',
|
||||
method: 'GET',
|
||||
headers: (params: VercelGetUserParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const d = data.user ?? data
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: d.id ?? null,
|
||||
email: d.email ?? null,
|
||||
username: d.username ?? null,
|
||||
name: d.name ?? null,
|
||||
avatar: d.avatar ?? null,
|
||||
defaultTeamId: d.defaultTeamId ?? null,
|
||||
createdAt: d.createdAt ?? null,
|
||||
stagingPrefix: d.stagingPrefix ?? null,
|
||||
softBlock: d.softBlock
|
||||
? {
|
||||
blockedAt: d.softBlock.blockedAt ?? null,
|
||||
reason: d.softBlock.reason ?? null,
|
||||
}
|
||||
: null,
|
||||
hasTrialAvailable: d.hasTrialAvailable ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'User ID' },
|
||||
email: { type: 'string', description: 'User email' },
|
||||
username: { type: 'string', description: 'Username' },
|
||||
name: { type: 'string', description: 'Display name' },
|
||||
avatar: { type: 'string', description: 'SHA1 hash of the avatar' },
|
||||
defaultTeamId: { type: 'string', description: 'Default team ID' },
|
||||
createdAt: { type: 'number', description: 'Account creation timestamp in milliseconds' },
|
||||
stagingPrefix: { type: 'string', description: 'Prefix for preview deployment URLs' },
|
||||
softBlock: {
|
||||
type: 'object',
|
||||
description: 'Account restriction details if blocked',
|
||||
properties: {
|
||||
blockedAt: { type: 'number', description: 'When the account was blocked' },
|
||||
reason: { type: 'string', description: 'Reason for the block' },
|
||||
},
|
||||
},
|
||||
hasTrialAvailable: { type: 'boolean', description: 'Whether a trial is available' },
|
||||
},
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Deployment tools
|
||||
|
||||
// Domain tools
|
||||
import { vercelAddDomainTool } from '@/tools/vercel/add_domain'
|
||||
// Project tools
|
||||
import { vercelAddProjectDomainTool } from '@/tools/vercel/add_project_domain'
|
||||
import { vercelCancelDeploymentTool } from '@/tools/vercel/cancel_deployment'
|
||||
// Alias tools
|
||||
import { vercelCreateAliasTool } from '@/tools/vercel/create_alias'
|
||||
// Check tools
|
||||
import { vercelCreateCheckTool } from '@/tools/vercel/create_check'
|
||||
import { vercelCreateDeploymentTool } from '@/tools/vercel/create_deployment'
|
||||
// DNS tools
|
||||
import { vercelCreateDnsRecordTool } from '@/tools/vercel/create_dns_record'
|
||||
// Edge Config tools
|
||||
import { vercelCreateEdgeConfigTool } from '@/tools/vercel/create_edge_config'
|
||||
// Environment variable tools
|
||||
import { vercelCreateEnvVarTool } from '@/tools/vercel/create_env_var'
|
||||
import { vercelCreateProjectTool } from '@/tools/vercel/create_project'
|
||||
// Webhook tools
|
||||
import { vercelCreateWebhookTool } from '@/tools/vercel/create_webhook'
|
||||
import { vercelDeleteAliasTool } from '@/tools/vercel/delete_alias'
|
||||
import { vercelDeleteDeploymentTool } from '@/tools/vercel/delete_deployment'
|
||||
import { vercelDeleteDnsRecordTool } from '@/tools/vercel/delete_dns_record'
|
||||
import { vercelDeleteDomainTool } from '@/tools/vercel/delete_domain'
|
||||
import { vercelDeleteEnvVarTool } from '@/tools/vercel/delete_env_var'
|
||||
import { vercelDeleteProjectTool } from '@/tools/vercel/delete_project'
|
||||
import { vercelDeleteWebhookTool } from '@/tools/vercel/delete_webhook'
|
||||
import { vercelGetAliasTool } from '@/tools/vercel/get_alias'
|
||||
import { vercelGetCheckTool } from '@/tools/vercel/get_check'
|
||||
import { vercelGetDeploymentTool } from '@/tools/vercel/get_deployment'
|
||||
import { vercelGetDeploymentEventsTool } from '@/tools/vercel/get_deployment_events'
|
||||
import { vercelGetDomainTool } from '@/tools/vercel/get_domain'
|
||||
import { vercelGetDomainConfigTool } from '@/tools/vercel/get_domain_config'
|
||||
import { vercelGetEdgeConfigTool } from '@/tools/vercel/get_edge_config'
|
||||
import { vercelGetEdgeConfigItemsTool } from '@/tools/vercel/get_edge_config_items'
|
||||
import { vercelGetEnvVarsTool } from '@/tools/vercel/get_env_vars'
|
||||
import { vercelGetProjectTool } from '@/tools/vercel/get_project'
|
||||
// Team & User tools
|
||||
import { vercelGetTeamTool } from '@/tools/vercel/get_team'
|
||||
import { vercelGetUserTool } from '@/tools/vercel/get_user'
|
||||
import { vercelListAliasesTool } from '@/tools/vercel/list_aliases'
|
||||
import { vercelListChecksTool } from '@/tools/vercel/list_checks'
|
||||
import { vercelListDeploymentFilesTool } from '@/tools/vercel/list_deployment_files'
|
||||
import { vercelListDeploymentsTool } from '@/tools/vercel/list_deployments'
|
||||
import { vercelListDnsRecordsTool } from '@/tools/vercel/list_dns_records'
|
||||
import { vercelListDomainsTool } from '@/tools/vercel/list_domains'
|
||||
import { vercelListEdgeConfigsTool } from '@/tools/vercel/list_edge_configs'
|
||||
import { vercelListProjectDomainsTool } from '@/tools/vercel/list_project_domains'
|
||||
import { vercelListProjectsTool } from '@/tools/vercel/list_projects'
|
||||
import { vercelListTeamMembersTool } from '@/tools/vercel/list_team_members'
|
||||
import { vercelListTeamsTool } from '@/tools/vercel/list_teams'
|
||||
import { vercelListWebhooksTool } from '@/tools/vercel/list_webhooks'
|
||||
import { vercelPauseProjectTool } from '@/tools/vercel/pause_project'
|
||||
import { vercelRemoveProjectDomainTool } from '@/tools/vercel/remove_project_domain'
|
||||
import { vercelRerequestCheckTool } from '@/tools/vercel/rerequest_check'
|
||||
import { vercelUnpauseProjectTool } from '@/tools/vercel/unpause_project'
|
||||
import { vercelUpdateCheckTool } from '@/tools/vercel/update_check'
|
||||
import { vercelUpdateEdgeConfigItemsTool } from '@/tools/vercel/update_edge_config_items'
|
||||
import { vercelUpdateEnvVarTool } from '@/tools/vercel/update_env_var'
|
||||
import { vercelUpdateProjectTool } from '@/tools/vercel/update_project'
|
||||
|
||||
export {
|
||||
// Deployments
|
||||
vercelListDeploymentsTool,
|
||||
vercelGetDeploymentTool,
|
||||
vercelCreateDeploymentTool,
|
||||
vercelCancelDeploymentTool,
|
||||
vercelDeleteDeploymentTool,
|
||||
vercelGetDeploymentEventsTool,
|
||||
vercelListDeploymentFilesTool,
|
||||
// Projects
|
||||
vercelListProjectsTool,
|
||||
vercelGetProjectTool,
|
||||
vercelCreateProjectTool,
|
||||
vercelUpdateProjectTool,
|
||||
vercelDeleteProjectTool,
|
||||
vercelPauseProjectTool,
|
||||
vercelUnpauseProjectTool,
|
||||
vercelListProjectDomainsTool,
|
||||
vercelAddProjectDomainTool,
|
||||
vercelRemoveProjectDomainTool,
|
||||
// Environment Variables
|
||||
vercelGetEnvVarsTool,
|
||||
vercelCreateEnvVarTool,
|
||||
vercelUpdateEnvVarTool,
|
||||
vercelDeleteEnvVarTool,
|
||||
// Domains
|
||||
vercelListDomainsTool,
|
||||
vercelGetDomainTool,
|
||||
vercelAddDomainTool,
|
||||
vercelDeleteDomainTool,
|
||||
vercelGetDomainConfigTool,
|
||||
// DNS
|
||||
vercelListDnsRecordsTool,
|
||||
vercelCreateDnsRecordTool,
|
||||
vercelDeleteDnsRecordTool,
|
||||
// Aliases
|
||||
vercelListAliasesTool,
|
||||
vercelGetAliasTool,
|
||||
vercelCreateAliasTool,
|
||||
vercelDeleteAliasTool,
|
||||
// Edge Config
|
||||
vercelListEdgeConfigsTool,
|
||||
vercelGetEdgeConfigTool,
|
||||
vercelCreateEdgeConfigTool,
|
||||
vercelGetEdgeConfigItemsTool,
|
||||
vercelUpdateEdgeConfigItemsTool,
|
||||
// Teams & User
|
||||
vercelListTeamsTool,
|
||||
vercelGetTeamTool,
|
||||
vercelListTeamMembersTool,
|
||||
vercelGetUserTool,
|
||||
// Webhooks
|
||||
vercelListWebhooksTool,
|
||||
vercelCreateWebhookTool,
|
||||
vercelDeleteWebhookTool,
|
||||
// Checks
|
||||
vercelCreateCheckTool,
|
||||
vercelGetCheckTool,
|
||||
vercelListChecksTool,
|
||||
vercelUpdateCheckTool,
|
||||
vercelRerequestCheckTool,
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
@@ -1,107 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelListAliasesParams, VercelListAliasesResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelListAliasesTool: ToolConfig<VercelListAliasesParams, VercelListAliasesResponse> =
|
||||
{
|
||||
id: 'vercel_list_aliases',
|
||||
name: 'Vercel List Aliases',
|
||||
description: 'List aliases for a Vercel project or team',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter aliases by project ID',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter aliases by domain',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of aliases to return',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelListAliasesParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.projectId) query.set('projectId', params.projectId.trim())
|
||||
if (params.domain) query.set('domain', params.domain.trim())
|
||||
if (params.limit) query.set('limit', String(params.limit))
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v4/aliases${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelListAliasesParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const aliases = (data.aliases ?? []).map((a: any) => ({
|
||||
uid: a.uid ?? null,
|
||||
alias: a.alias ?? null,
|
||||
deploymentId: a.deploymentId ?? null,
|
||||
projectId: a.projectId ?? null,
|
||||
createdAt: a.createdAt ?? null,
|
||||
updatedAt: a.updatedAt ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
aliases,
|
||||
count: aliases.length,
|
||||
hasMore: data.pagination?.next != null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
aliases: {
|
||||
type: 'array',
|
||||
description: 'List of aliases',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uid: { type: 'string', description: 'Alias ID' },
|
||||
alias: { type: 'string', description: 'Alias hostname' },
|
||||
deploymentId: { type: 'string', description: 'Associated deployment ID' },
|
||||
projectId: { type: 'string', description: 'Associated project ID' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of aliases returned',
|
||||
},
|
||||
hasMore: {
|
||||
type: 'boolean',
|
||||
description: 'Whether more aliases are available',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import type { VercelListChecksParams, VercelListChecksResponse } from '@/tools/vercel/types'
|
||||
|
||||
export const vercelListChecksTool: ToolConfig<VercelListChecksParams, VercelListChecksResponse> = {
|
||||
id: 'vercel_list_checks',
|
||||
name: 'Vercel List Checks',
|
||||
description: 'List all checks for a deployment',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Vercel Access Token',
|
||||
},
|
||||
deploymentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Deployment ID to list checks for',
|
||||
},
|
||||
teamId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Team ID to scope the request',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: VercelListChecksParams) => {
|
||||
const query = new URLSearchParams()
|
||||
if (params.teamId) query.set('teamId', params.teamId.trim())
|
||||
const qs = query.toString()
|
||||
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: VercelListChecksParams) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const checks = (data.checks ?? []).map((check: Record<string, unknown>) => ({
|
||||
id: check.id,
|
||||
name: check.name,
|
||||
status: check.status ?? 'registered',
|
||||
conclusion: check.conclusion ?? null,
|
||||
blocking: check.blocking ?? false,
|
||||
deploymentId: check.deploymentId,
|
||||
integrationId: check.integrationId ?? null,
|
||||
externalId: check.externalId ?? null,
|
||||
detailsUrl: check.detailsUrl ?? null,
|
||||
path: check.path ?? null,
|
||||
rerequestable: check.rerequestable ?? false,
|
||||
createdAt: check.createdAt,
|
||||
updatedAt: check.updatedAt,
|
||||
startedAt: check.startedAt ?? null,
|
||||
completedAt: check.completedAt ?? null,
|
||||
}))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
checks,
|
||||
count: checks.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
checks: {
|
||||
type: 'array',
|
||||
description: 'List of deployment checks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Check ID' },
|
||||
name: { type: 'string', description: 'Check name' },
|
||||
status: { type: 'string', description: 'Check status' },
|
||||
conclusion: { type: 'string', description: 'Check conclusion' },
|
||||
blocking: { type: 'boolean', description: 'Whether the check blocks the deployment' },
|
||||
deploymentId: { type: 'string', description: 'Associated deployment ID' },
|
||||
integrationId: { type: 'string', description: 'Associated integration ID' },
|
||||
externalId: { type: 'string', description: 'External identifier' },
|
||||
detailsUrl: { type: 'string', description: 'URL with details about the check' },
|
||||
path: { type: 'string', description: 'Page path being checked' },
|
||||
rerequestable: { type: 'boolean', description: 'Whether the check can be rerequested' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp' },
|
||||
startedAt: { type: 'number', description: 'Start timestamp' },
|
||||
completedAt: { type: 'number', description: 'Completion timestamp' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: { type: 'number', description: 'Total number of checks' },
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user