feat(crms): added hubspot, asana, trello, salesforce, pipedrive tools and triggers (#1871)

* trello integration added

* added asana integration

* added pipedrive (need to finish testing)

* finished pipedrive

* finished hubspot, need to test more

* make oauth required modal scrollable

* edited layout and fixed merge conflicts

* salesforce working, need to add more operations

* added all salesforce tools

* hubspot triggers working, plus other fixes

* fixed payload and added hubspot triggers

* fixed test

* build fix

* rebase

* updated docs

* oauth required modal

* fixed icons

* fixed hubspot scopes parsing

* reduce scopes of salesforce oauth

* cleaned up scopes

* lint

* aligned oauth.ts, auth.ts, and block definitions for all 29 providers

* updated icons

* fixed logos and updated docs

* revert changes to unused file

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
Co-authored-by: waleed <waleed>
Co-authored-by: aadamgough <adam@sim.ai>
This commit is contained in:
Adam Gough
2025-11-10 17:46:42 -08:00
committed by GitHub
parent 0ed0a26b3b
commit 2f9224c166
131 changed files with 20220 additions and 126 deletions

View File

@@ -0,0 +1,170 @@
---
title: Asana
description: Interact with Asana
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="asana"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='781.361 0 944.893 873.377'
>
<radialGradient
id='asana_radial_gradient'
cx='943.992'
cy='1221.416'
r='.663'
gradientTransform='matrix(944.8934 0 0 -873.3772 -890717.875 1067234.75)'
gradientUnits='userSpaceOnUse'
>
<stop offset='0' stopColor='#ffb900' />
<stop offset='.6' stopColor='#f95d8f' />
<stop offset='.999' stopColor='#f95353' />
</radialGradient>
<path
fill='url(#asana_radial_gradient)'
d='M1520.766 462.371c-113.508 0-205.508 92-205.508 205.488 0 113.499 92 205.518 205.508 205.518 113.489 0 205.488-92.019 205.488-205.518 0-113.488-91.999-205.488-205.488-205.488zm-533.907.01c-113.489.01-205.498 91.99-205.498 205.488 0 113.489 92.009 205.498 205.498 205.498 113.498 0 205.508-92.009 205.508-205.498 0-113.499-92.01-205.488-205.518-205.488h.01zm472.447-256.883c0 113.489-91.999 205.518-205.488 205.518-113.508 0-205.508-92.029-205.508-205.518S1140.31 0 1253.817 0c113.489 0 205.479 92.009 205.479 205.498h.01z'
/>
</svg>`}
/>
## Usage Instructions
Integrate Asana into the workflow. Can read, write, and update tasks.
## Tools
### `asana_get_task`
Retrieve a single task by GID or get multiple tasks with filters
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskGid` | string | No | The globally unique identifier \(GID\) of the task. If not provided, will get multiple tasks. |
| `workspace` | string | No | Workspace GID to filter tasks \(required when not using taskGid\) |
| `project` | string | No | Project GID to filter tasks |
| `limit` | number | No | Maximum number of tasks to return \(default: 50\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Single task details or array of tasks, depending on whether taskGid was provided |
### `asana_create_task`
Create a new task in Asana
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `workspace` | string | Yes | Workspace GID where the task will be created |
| `name` | string | Yes | Name of the task |
| `notes` | string | No | Notes or description for the task |
| `assignee` | string | No | User GID to assign the task to |
| `due_on` | string | No | Due date in YYYY-MM-DD format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created task details with timestamp, gid, name, notes, and permalink |
### `asana_update_task`
Update an existing task in Asana
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskGid` | string | Yes | The globally unique identifier \(GID\) of the task to update |
| `name` | string | No | Updated name for the task |
| `notes` | string | No | Updated notes or description for the task |
| `assignee` | string | No | Updated assignee user GID |
| `completed` | boolean | No | Mark task as completed or not completed |
| `due_on` | string | No | Updated due date in YYYY-MM-DD format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated task details with timestamp, gid, name, notes, and modified timestamp |
### `asana_get_projects`
Retrieve all projects from an Asana workspace
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `workspace` | string | Yes | Workspace GID to retrieve projects from |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | List of projects with their gid, name, and resource type |
### `asana_search_tasks`
Search for tasks in an Asana workspace
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `workspace` | string | Yes | Workspace GID to search tasks in |
| `text` | string | No | Text to search for in task names |
| `assignee` | string | No | Filter tasks by assignee user GID |
| `projects` | array | No | Array of project GIDs to filter tasks by |
| `completed` | boolean | No | Filter by completion status |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | List of tasks matching the search criteria |
### `asana_add_comment`
Add a comment (story) to an Asana task
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `taskGid` | string | Yes | The globally unique identifier \(GID\) of the task |
| `text` | string | Yes | The text content of the comment |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Comment details including gid, text, created timestamp, and author |
## Notes
- Category: `tools`
- Type: `asana`

View File

@@ -0,0 +1,289 @@
---
title: HubSpot
description: Interact with HubSpot CRM or trigger workflows from HubSpot events
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="hubspot"
color="#FF7A59"
icon={true}
iconSvg={`<svg className="block-icon"
role='img'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
fill='currentColor'
>
<path d='M18.164 7.93V5.084a2.198 2.198 0 001.267-1.978v-.067A2.2 2.2 0 0017.238.845h-.067a2.2 2.2 0 00-2.193 2.193v.067a2.196 2.196 0 001.252 1.973l.013.006v2.852a6.22 6.22 0 00-2.969 1.31l.012-.01-7.828-6.095A2.497 2.497 0 104.3 4.656l-.012.006 7.697 5.991a6.176 6.176 0 00-1.038 3.446c0 1.343.425 2.588 1.147 3.607l-.013-.02-2.342 2.343a1.968 1.968 0 00-.58-.095h-.002a2.033 2.033 0 102.033 2.033 1.978 1.978 0 00-.1-.595l.005.014 2.317-2.317a6.247 6.247 0 104.782-11.134l-.036-.005zm-.964 9.378a3.206 3.206 0 113.215-3.207v.002a3.206 3.206 0 01-3.207 3.207z' />
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[HubSpot](https://www.hubspot.com) is a comprehensive CRM platform that provides a full suite of marketing, sales, and customer service tools to help businesses grow better. With its powerful automation capabilities and extensive API, HubSpot has become one of the world's leading CRM platforms, serving businesses of all sizes across industries.
HubSpot CRM offers a complete solution for managing customer relationships, from initial contact through to long-term customer success. The platform combines contact management, deal tracking, marketing automation, and customer service tools into a unified system that helps teams stay aligned and focused on customer success.
Key features of HubSpot CRM include:
- Contact & Company Management: Comprehensive database for storing and organizing customer and prospect information
- Deal Pipeline: Visual sales pipeline for tracking opportunities through customizable stages
- Marketing Events: Track and manage marketing campaigns and events with detailed attribution
- Ticket Management: Customer support ticketing system for tracking and resolving customer issues
- Quotes & Line Items: Create and manage sales quotes with detailed product line items
- User & Team Management: Organize teams, assign ownership, and track user activity across the platform
In Sim, the HubSpot integration enables your AI agents to seamlessly interact with your CRM data and automate key business processes. This creates powerful opportunities for intelligent lead qualification, automated contact enrichment, deal management, customer support automation, and data synchronization across your tech stack. The integration allows agents to create, retrieve, update, and search across all major HubSpot objects, enabling sophisticated workflows that can respond to CRM events, maintain data quality, and ensure your team has the most up-to-date customer information. By connecting Sim with HubSpot, you can build AI agents that automatically qualify leads, route support tickets, update deal stages based on customer interactions, generate quotes, and keep your CRM data synchronized with other business systems—ultimately increasing team productivity and improving customer experiences.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate HubSpot into your workflow. Manage contacts, companies, deals, tickets, and other CRM objects with powerful automation capabilities. Can be used in trigger mode to start workflows when contacts are created, deleted, or updated.
## Tools
### `hubspot_get_users`
Retrieve all users from HubSpot account
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Number of results to return \(default: 100\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Users data |
### `hubspot_list_contacts`
Retrieve all contacts from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
| `after` | string | No | Pagination cursor for next page of results |
| `properties` | string | No | Comma-separated list of properties to return \(e.g., "email,firstname,lastname"\) |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Contacts data |
### `hubspot_get_contact`
Retrieve a single contact by ID or email from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `contactId` | string | Yes | The ID or email of the contact to retrieve |
| `idProperty` | string | No | Property to use as unique identifier \(e.g., "email"\). If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of properties to return |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Contact data |
### `hubspot_create_contact`
Create a new contact in HubSpot. Requires at least one of: email, firstname, or lastname
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `properties` | object | Yes | Contact properties as JSON object. Must include at least one of: email, firstname, or lastname |
| `associations` | array | No | Array of associations to create with the contact \(e.g., companies, deals\). Each object should have "to" \(with "id"\) and "types" \(with "associationCategory" and "associationTypeId"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created contact data |
### `hubspot_update_contact`
Update an existing contact in HubSpot by ID or email
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `contactId` | string | Yes | The ID or email of the contact to update |
| `idProperty` | string | No | Property to use as unique identifier \(e.g., "email"\). If not specified, uses record ID |
| `properties` | object | Yes | Contact properties to update as JSON object |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated contact data |
### `hubspot_search_contacts`
Search for contacts in HubSpot using filters, sorting, and queries
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filterGroups` | array | No | Array of filter groups. Each group contains filters with propertyName, operator, and value |
| `sorts` | array | No | Array of sort objects with propertyName and direction \("ASCENDING" or "DESCENDING"\) |
| `query` | string | No | Search query string |
| `properties` | array | No | Array of property names to return |
| `limit` | number | No | Maximum number of results to return \(max 100\) |
| `after` | string | No | Pagination cursor for next page |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Search results |
### `hubspot_list_companies`
Retrieve all companies from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
| `after` | string | No | Pagination cursor for next page of results |
| `properties` | string | No | Comma-separated list of properties to return |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Companies data |
### `hubspot_get_company`
Retrieve a single company by ID or domain from HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `companyId` | string | Yes | The ID or domain of the company to retrieve |
| `idProperty` | string | No | Property to use as unique identifier \(e.g., "domain"\). If not specified, uses record ID |
| `properties` | string | No | Comma-separated list of properties to return |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Company data |
### `hubspot_create_company`
Create a new company in HubSpot
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `properties` | object | Yes | Company properties as JSON object \(e.g., name, domain, city, industry\) |
| `associations` | array | No | Array of associations to create with the company |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created company data |
### `hubspot_update_company`
Update an existing company in HubSpot by ID or domain
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `companyId` | string | Yes | The ID or domain of the company to update |
| `idProperty` | string | No | Property to use as unique identifier \(e.g., "domain"\). If not specified, uses record ID |
| `properties` | object | Yes | Company properties to update as JSON object |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated company data |
### `hubspot_search_companies`
Search for companies in HubSpot using filters, sorting, and queries
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filterGroups` | array | No | Array of filter groups. Each group contains filters with propertyName, operator, and value |
| `sorts` | array | No | Array of sort objects with propertyName and direction \("ASCENDING" or "DESCENDING"\) |
| `query` | string | No | Search query string |
| `properties` | array | No | Array of property names to return |
| `limit` | number | No | Maximum number of results to return \(max 100\) |
| `after` | string | No | Pagination cursor for next page |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Search results |
### `hubspot_list_deals`
Retrieve all deals from HubSpot account with pagination support
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `limit` | string | No | Maximum number of results per page \(max 100, default 100\) |
| `after` | string | No | Pagination cursor for next page of results |
| `properties` | string | No | Comma-separated list of properties to return |
| `associations` | string | No | Comma-separated list of object types to retrieve associated IDs for |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Deals data |
## Notes
- Category: `tools`
- Type: `hubspot`

View File

@@ -3,6 +3,7 @@
"index",
"airtable",
"arxiv",
"asana",
"browser_use",
"clay",
"confluence",
@@ -21,6 +22,7 @@
"google_search",
"google_sheets",
"google_vault",
"hubspot",
"huggingface",
"hunter",
"image_generator",
@@ -45,11 +47,13 @@
"parallel_ai",
"perplexity",
"pinecone",
"pipedrive",
"postgresql",
"qdrant",
"reddit",
"resend",
"s3",
"salesforce",
"schedule",
"serper",
"sharepoint",
@@ -63,6 +67,7 @@
"telegram",
"thinking",
"translate",
"trello",
"twilio_sms",
"twilio_voice",
"typeform",

View File

@@ -9,10 +9,42 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
type="microsoft_planner"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon" fill='currentColor' viewBox='-1 -1 27 27' xmlns='http://www.w3.org/2000/svg'>
iconSvg={`<svg className="block-icon"
xmlnsXlink='http://www.w3.org/1999/xlink'
viewBox='0 0 24 24'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<g clipPath='url(#msplanner_clip0)'>
<path
d='M8.25809 15.7412C7.22488 16.7744 5.54971 16.7744 4.5165 15.7412L0.774909 11.9996C-0.258303 10.9664 -0.258303 9.29129 0.774908 8.25809L4.5165 4.51655C5.54971 3.48335 7.22488 3.48335 8.25809 4.51655L11.9997 8.2581C13.0329 9.29129 13.0329 10.9664 11.9997 11.9996L8.25809 15.7412Z'
fill='url(#msplanner_paint0_linear)'
/>
<path
d='M8.25809 15.7412C7.22488 16.7744 5.54971 16.7744 4.5165 15.7412L0.774909 11.9996C-0.258303 10.9664 -0.258303 9.29129 0.774908 8.25809L4.5165 4.51655C5.54971 3.48335 7.22488 3.48335 8.25809 4.51655L11.9997 8.2581C13.0329 9.29129 13.0329 10.9664 11.9997 11.9996L8.25809 15.7412Z'
fill='url(#msplanner_paint1_linear)'
/>
<path
d='M0.774857 11.9999C1.80809 13.0331 3.48331 13.0331 4.51655 11.9999L15.7417 0.774926C16.7749 -0.258304 18.4501 -0.258309 19.4834 0.774914L23.225 4.51655C24.2583 5.54977 24.2583 7.22496 23.225 8.25819L11.9999 19.4832C10.9667 20.5164 9.29146 20.5164 8.25822 19.4832L0.774857 11.9999Z'
fill='url(#msplanner_paint2_linear)'
/>
<path
d='M0.774857 11.9999C1.80809 13.0331 3.48331 13.0331 4.51655 11.9999L15.7417 0.774926C16.7749 -0.258304 18.4501 -0.258309 19.4834 0.774914L23.225 4.51655C24.2583 5.54977 24.2583 7.22496 23.225 8.25819L11.9999 19.4832C10.9667 20.5164 9.29146 20.5164 8.25822 19.4832L0.774857 11.9999Z'
fill='url(#msplanner_paint3_linear)'
/>
<path
d='M4.51642 15.7413C5.54966 16.7746 7.22487 16.7746 8.25812 15.7413L15.7415 8.25803C16.7748 7.2248 18.45 7.2248 19.4832 8.25803L23.2249 11.9997C24.2582 13.0329 24.2582 14.7081 23.2249 15.7413L15.7415 23.2246C14.7083 24.2579 13.033 24.2579 11.9998 23.2246L4.51642 15.7413Z'
fill='url(#msplanner_paint4_linear)'
/>
<path
d='M4.51642 15.7413C5.54966 16.7746 7.22487 16.7746 8.25812 15.7413L15.7415 8.25803C16.7748 7.2248 18.45 7.2248 19.4832 8.25803L23.2249 11.9997C24.2582 13.0329 24.2582 14.7081 23.2249 15.7413L15.7415 23.2246C14.7083 24.2579 13.033 24.2579 11.9998 23.2246L4.51642 15.7413Z'
fill='url(#msplanner_paint5_linear)'
/>
</g>
<defs>
<linearGradient
id='paint0_linear_3984_11038'
id='msplanner_paint0_linear'
x1='6.38724'
y1='3.74167'
x2='2.15779'
@@ -23,7 +55,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#541278' />
</linearGradient>
<linearGradient
id='paint1_linear_3984_11038'
id='msplanner_paint1_linear'
x1='8.38032'
y1='11.0696'
x2='4.94062'
@@ -34,7 +66,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#7034B0' stopOpacity='0' />
</linearGradient>
<linearGradient
id='paint2_linear_3984_11038'
id='msplanner_paint2_linear'
x1='18.3701'
y1='-3.33385e-05'
x2='9.85717'
@@ -45,7 +77,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#6C0F71' />
</linearGradient>
<linearGradient
id='paint3_linear_3984_11038'
id='msplanner_paint3_linear'
x1='18.3701'
y1='-3.33385e-05'
x2='9.85717'
@@ -57,7 +89,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#8F28B3' />
</linearGradient>
<linearGradient
id='paint4_linear_3984_11038'
id='msplanner_paint4_linear'
x1='18.0002'
y1='7.49958'
x2='14.0004'
@@ -68,7 +100,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#00479E' />
</linearGradient>
<linearGradient
id='paint5_linear_3984_11038'
id='msplanner_paint5_linear'
x1='18.2164'
y1='7.92626'
x2='10.5237'
@@ -78,31 +110,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop stopColor='#3DCBFF' />
<stop offset='1' stopColor='#4A40D4' />
</linearGradient>
<clipPath id='msplanner_clip0'>
<rect fill='white' />
</clipPath>
</defs>
<path
d='M8.25809 15.7412C7.22488 16.7744 5.54971 16.7744 4.5165 15.7412L0.774909 11.9996C-0.258303 10.9664 -0.258303 9.29129 0.774908 8.25809L4.5165 4.51655C5.54971 3.48335 7.22488 3.48335 8.25809 4.51655L11.9997 8.2581C13.0329 9.29129 13.0329 10.9664 11.9997 11.9996L8.25809 15.7412Z'
fill='url(#paint0_linear_3984_11038)'
/>
<path
d='M8.25809 15.7412C7.22488 16.7744 5.54971 16.7744 4.5165 15.7412L0.774909 11.9996C-0.258303 10.9664 -0.258303 9.29129 0.774908 8.25809L4.5165 4.51655C5.54971 3.48335 7.22488 3.48335 8.25809 4.51655L11.9997 8.2581C13.0329 9.29129 13.0329 10.9664 11.9997 11.9996L8.25809 15.7412Z'
fill='url(#paint1_linear_3984_11038)'
/>
<path
d='M0.774857 11.9999C1.80809 13.0331 3.48331 13.0331 4.51655 11.9999L15.7417 0.774926C16.7749 -0.258304 18.4501 -0.258309 19.4834 0.774914L23.225 4.51655C24.2583 5.54977 24.2583 7.22496 23.225 8.25819L11.9999 19.4832C10.9667 20.5164 9.29146 20.5164 8.25822 19.4832L0.774857 11.9999Z'
fill='url(#paint2_linear_3984_11038)'
/>
<path
d='M0.774857 11.9999C1.80809 13.0331 3.48331 13.0331 4.51655 11.9999L15.7417 0.774926C16.7749 -0.258304 18.4501 -0.258309 19.4834 0.774914L23.225 4.51655C24.2583 5.54977 24.2583 7.22496 23.225 8.25819L11.9999 19.4832C10.9667 20.5164 9.29146 20.5164 8.25822 19.4832L0.774857 11.9999Z'
fill='url(#paint3_linear_3984_11038)'
/>
<path
d='M4.51642 15.7413C5.54966 16.7746 7.22487 16.7746 8.25812 15.7413L15.7415 8.25803C16.7748 7.2248 18.45 7.2248 19.4832 8.25803L23.2249 11.9997C24.2582 13.0329 24.2582 14.7081 23.2249 15.7413L15.7415 23.2246C14.7083 24.2579 13.033 24.2579 11.9998 23.2246L4.51642 15.7413Z'
fill='url(#paint4_linear_3984_11038)'
/>
<path
d='M4.51642 15.7413C5.54966 16.7746 7.22487 16.7746 8.25812 15.7413L15.7415 8.25803C16.7748 7.2248 18.45 7.2248 19.4832 8.25803L23.2249 11.9997C24.2582 13.0329 24.2582 14.7081 23.2249 15.7413L15.7415 23.2246C14.7083 24.2579 13.033 24.2579 11.9998 23.2246L4.51642 15.7413Z'
fill='url(#paint5_linear_3984_11038)'
/>
</svg>`}
/>

View File

@@ -53,7 +53,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
d='M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z'
/>
<linearGradient
id='a'
id='msteams_gradient_a'
gradientUnits='userSpaceOnUse'
x1='198.099'
y1='1683.0726'
@@ -69,7 +69,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#3940ab' />
</linearGradient>
<path
fill='url(#a)'
fill='url(#msteams_gradient_a)'
d='M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z'
/>
<path

View File

@@ -0,0 +1,452 @@
---
title: Pipedrive
description: Interact with Pipedrive CRM
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="pipedrive"
color="#2E6936"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 304 304'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
>
<defs>
<path
d='M59.6807,81.1772 C59.6807,101.5343 70.0078,123.4949 92.7336,123.4949 C109.5872,123.4949 126.6277,110.3374 126.6277,80.8785 C126.6277,55.0508 113.232,37.7119 93.2944,37.7119 C77.0483,37.7119 59.6807,49.1244 59.6807,81.1772 Z M101.3006,0 C142.0482,0 169.4469,32.2728 169.4469,80.3126 C169.4469,127.5978 140.584,160.60942 99.3224,160.60942 C79.6495,160.60942 67.0483,152.1836 60.4595,146.0843 C60.5063,147.5305 60.5374,149.1497 60.5374,150.8788 L60.5374,215 L18.32565,215 L18.32565,44.157 C18.32565,41.6732 17.53126,40.8873 15.07021,40.8873 L0.5531,40.8873 L0.5531,3.4741 L35.9736,3.4741 C52.282,3.4741 56.4564,11.7741 57.2508,18.1721 C63.8708,10.7524 77.5935,0 101.3006,0 Z'
id='path-1'
/>
</defs>
<g
id='Pipedrive_letter_logo_dark'
stroke='none'
strokeWidth='1'
fill='none'
fillRule='evenodd'
>
<g transform='translate(67.000000, 44.000000)'>
<mask id='mask-2' fill='white'>
<use href='#path-1' />
</mask>
<use id='Clip-5' fill='#FFFFFF' xlinkHref='#path-1' />
</g>
</g>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Pipedrive](https://www.pipedrive.com) is a powerful sales-focused CRM platform designed to help sales teams manage leads, track deals, and optimize their sales pipeline. Built with simplicity and effectiveness in mind, Pipedrive has become a favorite among sales professionals and growing businesses worldwide for its intuitive visual pipeline management and actionable sales insights.
Pipedrive provides a comprehensive suite of tools for managing the entire sales process from lead capture to deal closure. With its robust API and extensive integration capabilities, Pipedrive enables sales teams to automate repetitive tasks, maintain data consistency, and focus on what matters most—closing deals.
Key features of Pipedrive include:
- Visual Sales Pipeline: Intuitive drag-and-drop interface for managing deals through customizable sales stages
- Lead Management: Comprehensive lead inbox for capturing, qualifying, and converting potential opportunities
- Activity Tracking: Sophisticated system for scheduling and tracking calls, meetings, emails, and tasks
- Project Management: Built-in project tracking capabilities for post-sale customer success and delivery
- Email Integration: Native mailbox integration for seamless communication tracking within the CRM
In Sim, the Pipedrive integration allows your AI agents to seamlessly interact with your sales workflow. This creates opportunities for automated lead qualification, deal creation and updates, activity scheduling, and pipeline management as part of your AI-powered sales processes. The integration enables agents to create, retrieve, update, and manage deals, leads, activities, and projects programmatically, facilitating intelligent sales automation and ensuring that critical customer information is properly tracked and acted upon. By connecting Sim with Pipedrive, you can build AI agents that maintain sales pipeline visibility, automate routine CRM tasks, qualify leads intelligently, and ensure no opportunities slip through the cracks—enhancing sales team productivity and driving consistent revenue growth.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate Pipedrive into your workflow. Manage deals, contacts, sales pipeline, projects, activities, files, and communications with powerful CRM capabilities.
## Tools
### `pipedrive_get_all_deals`
Retrieve all deals from Pipedrive with optional filters
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `status` | string | No | Only fetch deals with a specific status. Values: open, won, lost. If omitted, all not deleted deals are returned |
| `person_id` | string | No | If supplied, only deals linked to the specified person are returned |
| `org_id` | string | No | If supplied, only deals linked to the specified organization are returned |
| `pipeline_id` | string | No | If supplied, only deals in the specified pipeline are returned |
| `updated_since` | string | No | If set, only deals updated after this time are returned. Format: 2025-01-01T10:20:00Z |
| `limit` | string | No | Number of results to return \(default: 100, max: 500\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Deals data and metadata |
### `pipedrive_get_deal`
Retrieve detailed information about a specific deal
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `deal_id` | string | Yes | The ID of the deal to retrieve |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Deal details |
### `pipedrive_create_deal`
Create a new deal in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | string | Yes | The title of the deal |
| `value` | string | No | The monetary value of the deal |
| `currency` | string | No | Currency code \(e.g., USD, EUR\) |
| `person_id` | string | No | ID of the person this deal is associated with |
| `org_id` | string | No | ID of the organization this deal is associated with |
| `pipeline_id` | string | No | ID of the pipeline this deal should be placed in |
| `stage_id` | string | No | ID of the stage this deal should be placed in |
| `status` | string | No | Status of the deal: open, won, lost |
| `expected_close_date` | string | No | Expected close date in YYYY-MM-DD format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created deal details |
### `pipedrive_update_deal`
Update an existing deal in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `deal_id` | string | Yes | The ID of the deal to update |
| `title` | string | No | New title for the deal |
| `value` | string | No | New monetary value for the deal |
| `status` | string | No | New status: open, won, lost |
| `stage_id` | string | No | New stage ID for the deal |
| `expected_close_date` | string | No | New expected close date in YYYY-MM-DD format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated deal details |
### `pipedrive_get_files`
Retrieve files from Pipedrive with optional filters
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `deal_id` | string | No | Filter files by deal ID |
| `person_id` | string | No | Filter files by person ID |
| `org_id` | string | No | Filter files by organization ID |
| `limit` | string | No | Number of results to return \(default: 100, max: 500\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Files data |
### `pipedrive_get_mail_messages`
Retrieve mail threads from Pipedrive mailbox
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `folder` | string | No | Filter by folder: inbox, drafts, sent, archive \(default: inbox\) |
| `limit` | string | No | Number of results to return \(default: 50\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Mail threads data |
### `pipedrive_get_mail_thread`
Retrieve all messages from a specific mail thread
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `thread_id` | string | Yes | The ID of the mail thread |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Mail thread messages data |
### `pipedrive_get_pipelines`
Retrieve all pipelines from Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `sort_by` | string | No | Field to sort by: id, update_time, add_time \(default: id\) |
| `sort_direction` | string | No | Sorting direction: asc, desc \(default: asc\) |
| `limit` | string | No | Number of results to return \(default: 100, max: 500\) |
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Pipelines data |
### `pipedrive_get_pipeline_deals`
Retrieve all deals in a specific pipeline
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `pipeline_id` | string | Yes | The ID of the pipeline |
| `stage_id` | string | No | Filter by specific stage within the pipeline |
| `status` | string | No | Filter by deal status: open, won, lost |
| `limit` | string | No | Number of results to return \(default: 100, max: 500\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Pipeline deals data |
### `pipedrive_get_projects`
Retrieve all projects or a specific project from Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `project_id` | string | No | Optional: ID of a specific project to retrieve |
| `status` | string | No | Filter by project status: open, completed, deleted \(only for listing all\) |
| `limit` | string | No | Number of results to return \(default: 100, max: 500, only for listing all\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Projects data or single project details |
### `pipedrive_create_project`
Create a new project in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | string | Yes | The title of the project |
| `description` | string | No | Description of the project |
| `start_date` | string | No | Project start date in YYYY-MM-DD format |
| `end_date` | string | No | Project end date in YYYY-MM-DD format |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created project details |
### `pipedrive_get_activities`
Retrieve activities (tasks) from Pipedrive with optional filters
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `deal_id` | string | No | Filter activities by deal ID |
| `person_id` | string | No | Filter activities by person ID |
| `org_id` | string | No | Filter activities by organization ID |
| `type` | string | No | Filter by activity type \(call, meeting, task, deadline, email, lunch\) |
| `done` | string | No | Filter by completion status: 0 for not done, 1 for done |
| `limit` | string | No | Number of results to return \(default: 100, max: 500\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Activities data |
### `pipedrive_create_activity`
Create a new activity (task) in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `subject` | string | Yes | The subject/title of the activity |
| `type` | string | Yes | Activity type: call, meeting, task, deadline, email, lunch |
| `due_date` | string | Yes | Due date in YYYY-MM-DD format |
| `due_time` | string | No | Due time in HH:MM format |
| `duration` | string | No | Duration in HH:MM format |
| `deal_id` | string | No | ID of the deal to associate with |
| `person_id` | string | No | ID of the person to associate with |
| `org_id` | string | No | ID of the organization to associate with |
| `note` | string | No | Notes for the activity |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created activity details |
### `pipedrive_update_activity`
Update an existing activity (task) in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `activity_id` | string | Yes | The ID of the activity to update |
| `subject` | string | No | New subject/title for the activity |
| `due_date` | string | No | New due date in YYYY-MM-DD format |
| `due_time` | string | No | New due time in HH:MM format |
| `duration` | string | No | New duration in HH:MM format |
| `done` | string | No | Mark as done: 0 for not done, 1 for done |
| `note` | string | No | New notes for the activity |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated activity details |
### `pipedrive_get_leads`
Retrieve all leads or a specific lead from Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `lead_id` | string | No | Optional: ID of a specific lead to retrieve |
| `archived` | string | No | Get archived leads instead of active ones |
| `owner_id` | string | No | Filter by owner user ID |
| `person_id` | string | No | Filter by person ID |
| `organization_id` | string | No | Filter by organization ID |
| `limit` | string | No | Number of results to return \(default: 100, max: 500\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Leads data or single lead details |
### `pipedrive_create_lead`
Create a new lead in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | string | Yes | The name of the lead |
| `person_id` | string | No | ID of the person \(REQUIRED unless organization_id is provided\) |
| `organization_id` | string | No | ID of the organization \(REQUIRED unless person_id is provided\) |
| `owner_id` | string | No | ID of the user who will own the lead |
| `value_amount` | string | No | Potential value amount |
| `value_currency` | string | No | Currency code \(e.g., USD, EUR\) |
| `expected_close_date` | string | No | Expected close date in YYYY-MM-DD format |
| `visible_to` | string | No | Visibility: 1 \(Owner & followers\), 3 \(Entire company\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created lead details |
### `pipedrive_update_lead`
Update an existing lead in Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `lead_id` | string | Yes | The ID of the lead to update |
| `title` | string | No | New name for the lead |
| `person_id` | string | No | New person ID |
| `organization_id` | string | No | New organization ID |
| `owner_id` | string | No | New owner user ID |
| `value_amount` | string | No | New value amount |
| `value_currency` | string | No | New currency code \(e.g., USD, EUR\) |
| `expected_close_date` | string | No | New expected close date in YYYY-MM-DD format |
| `is_archived` | string | No | Archive the lead: true or false |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated lead details |
### `pipedrive_delete_lead`
Delete a specific lead from Pipedrive
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `lead_id` | string | Yes | The ID of the lead to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Deletion result |
## Notes
- Category: `tools`
- Type: `pipedrive`

View File

@@ -10,7 +10,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
color="#1A223F"
icon={true}
iconSvg={`<svg className="block-icon" fill='none' viewBox='0 0 49 56' xmlns='http://www.w3.org/2000/svg'>
<g clipPath='url(#b)'>
<g clipPath='url(#qdrant_clippath_b)'>
<path
d='m38.489 51.477-1.1167-30.787-2.0223-8.1167 13.498 1.429v37.242l-8.2456 4.7589-2.1138-4.5259z'
clipRule='evenodd'
@@ -59,11 +59,14 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
fill='#DC244C'
fillRule='evenodd'
/>
<path d='m24.603 46.483v-9.5222l-7.7166-4.4411v9.5064l7.7166 4.4569z' fill='url(#a)' />
<path
d='m24.603 46.483v-9.5222l-7.7166-4.4411v9.5064l7.7166 4.4569z'
fill='url(#qdrant_gradient_a)'
/>
</g>
<defs>
<linearGradient
id='a'
id='qdrant_gradient_a'
x1='23.18'
x2='15.491'
y1='38.781'
@@ -73,7 +76,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop stopColor='#FF3364' offset='0' />
<stop stopColor='#C91540' stopOpacity='0' offset='1' />
</linearGradient>
<clipPath id='b'>
<clipPath id='qdrant_clippath_b'>
<rect transform='translate(.34961)' fill='#fff' />
</clipPath>
</defs>

File diff suppressed because one or more lines are too long

View File

@@ -9,14 +9,21 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
type="supabase"
color="#1C1C1C"
icon={true}
iconSvg={`<svg className="block-icon" viewBox='0 0 27 27' xmlns='http://www.w3.org/2000/svg'>
iconSvg={`<svg className="block-icon"
fill='currentColor'
viewBox='0 0 27 27'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M15.4057 26.2606C14.7241 27.1195 13.3394 26.649 13.3242 25.5519L13.083 9.50684H23.8724C25.8262 9.50684 26.9157 11.7636 25.7006 13.2933L15.4057 26.2606Z'
fill='url(#paint0_linear)'
fill='url(#supabase_paint0_linear)'
/>
<path
d='M15.4057 26.2606C14.7241 27.1195 13.3394 26.649 13.3242 25.5519L13.083 9.50684H23.8724C25.8262 9.50684 26.9157 11.7636 25.7006 13.2933L15.4057 26.2606Z'
fill='url(#paint1_linear)'
fill='url(#supabase_paint1_linear)'
fillOpacity='0.2'
/>
<path
@@ -25,7 +32,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
/>
<defs>
<linearGradient
id='paint0_linear'
id='supabase_paint0_linear'
x1='13.084'
y1='13.0655'
x2='22.6727'
@@ -36,7 +43,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<stop offset='1' stopColor='#3ECF8E' />
</linearGradient>
<linearGradient
id='paint1_linear'
id='supabase_paint1_linear'
x1='8.83277'
y1='7.24485'
x2='13.2057'

View File

@@ -0,0 +1,167 @@
---
title: Trello
description: Manage Trello boards and cards
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="trello"
color="#0052CC"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 256 256'
preserveAspectRatio='xMidYMid'
>
<rect fill='#0052CC' x='0' y='0' rx='32' />
<rect fill='#FFF' x='144.64' y='33.28' rx='12' />
<rect fill='#FFF' x='33.28' y='33.28' rx='12' />
</svg>`}
/>
## Usage Instructions
Integrate with Trello to manage boards and cards. List boards, list cards, create cards, update cards, get actions, and add comments.
## Tools
### `trello_list_lists`
List all lists on a Trello board
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `boardId` | string | Yes | ID of the board to list lists from |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation was successful |
| `lists` | array | Array of list objects with id, name, closed, pos, and idBoard |
| `count` | number | Number of lists returned |
| `error` | string | Error message if operation failed |
### `trello_list_cards`
List all cards on a Trello board
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `boardId` | string | Yes | ID of the board to list cards from |
| `listId` | string | No | Optional: Filter cards by list ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation was successful |
| `cards` | array | Array of card objects with id, name, desc, url, board/list IDs, labels, and due date |
| `count` | number | Number of cards returned |
| `error` | string | Error message if operation failed |
### `trello_create_card`
Create a new card on a Trello board
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `boardId` | string | Yes | ID of the board to create the card on |
| `listId` | string | Yes | ID of the list to create the card in |
| `name` | string | Yes | Name/title of the card |
| `desc` | string | No | Description of the card |
| `pos` | string | No | Position of the card \(top, bottom, or positive float\) |
| `due` | string | No | Due date \(ISO 8601 format\) |
| `labels` | string | No | Comma-separated list of label IDs |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the card was created successfully |
| `card` | object | The created card object with id, name, desc, url, and other properties |
| `error` | string | Error message if operation failed |
### `trello_update_card`
Update an existing card on Trello
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `cardId` | string | Yes | ID of the card to update |
| `name` | string | No | New name/title of the card |
| `desc` | string | No | New description of the card |
| `closed` | boolean | No | Archive/close the card \(true\) or reopen it \(false\) |
| `idList` | string | No | Move card to a different list |
| `due` | string | No | Due date \(ISO 8601 format\) |
| `dueComplete` | boolean | No | Mark the due date as complete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the card was updated successfully |
| `card` | object | The updated card object with id, name, desc, url, and other properties |
| `error` | string | Error message if operation failed |
### `trello_get_actions`
Get activity/actions from a board or card
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `boardId` | string | No | ID of the board to get actions from \(either boardId or cardId required\) |
| `cardId` | string | No | ID of the card to get actions from \(either boardId or cardId required\) |
| `filter` | string | No | Filter actions by type \(e.g., "commentCard,updateCard,createCard" or "all"\) |
| `limit` | number | No | Maximum number of actions to return \(default: 50, max: 1000\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation was successful |
| `actions` | array | Array of action objects with type, date, member, and data |
| `count` | number | Number of actions returned |
| `error` | string | Error message if operation failed |
### `trello_add_comment`
Add a comment to a Trello card
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `cardId` | string | Yes | ID of the card to comment on |
| `text` | string | Yes | Comment text |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the comment was added successfully |
| `comment` | object | The created comment object with id, text, date, and member creator |
| `error` | string | Error message if operation failed |
## Notes
- Category: `tools`
- Type: `trello`

View File

@@ -210,7 +210,8 @@ export async function GET(request: NextRequest) {
displayName = `${acc.accountId} (${baseProvider})`
}
const grantedScopes = acc.scope ? acc.scope.split(/[\s,]+/).filter(Boolean) : []
const storedScope = acc.scope?.trim()
const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : []
const scopeEvaluation = evaluateScopeCoverage(acc.providerId, grantedScopes)
return {

View File

@@ -200,7 +200,7 @@ describe('OAuth Token API Routes', () => {
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(response.status).toBe(404)
expect(data).toHaveProperty('error')
})

View File

@@ -71,10 +71,20 @@ export async function POST(request: NextRequest) {
// Fetch the credential as the owner to enforce ownership scoping
const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId)
if (!credential) {
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
try {
// Refresh the token if needed
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
return NextResponse.json({ accessToken }, { status: 200 })
return NextResponse.json(
{
accessToken,
idToken: credential.idToken || undefined,
},
{ status: 200 }
)
} catch (error) {
logger.error(`[${requestId}] Failed to refresh access token:`, error)
return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
@@ -137,7 +147,13 @@ export async function GET(request: NextRequest) {
try {
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
return NextResponse.json({ accessToken }, { status: 200 })
return NextResponse.json(
{
accessToken,
idToken: credential.idToken || undefined,
},
{ status: 200 }
)
} catch (_error) {
return NextResponse.json({ error: 'Failed to refresh access token' }, { status: 401 })
}

View File

@@ -0,0 +1,41 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
const logger = createLogger('TrelloAuthorize')
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const apiKey = env.TRELLO_API_KEY
if (!apiKey) {
logger.error('TRELLO_API_KEY not configured')
return NextResponse.json({ error: 'Trello API key not configured' }, { status: 500 })
}
const baseUrl = getBaseUrl()
const returnUrl = `${baseUrl}/api/auth/trello/callback`
const authUrl = new URL('https://trello.com/1/authorize')
authUrl.searchParams.set('key', apiKey)
authUrl.searchParams.set('name', 'Sim Studio')
authUrl.searchParams.set('expiration', 'never')
authUrl.searchParams.set('response_type', 'token')
authUrl.searchParams.set('scope', 'read,write')
authUrl.searchParams.set('return_url', returnUrl)
return NextResponse.redirect(authUrl.toString())
} catch (error) {
logger.error('Error initiating Trello authorization:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,130 @@
import { type NextRequest, NextResponse } from 'next/server'
import { getBaseUrl } from '@/lib/urls/utils'
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
const baseUrl = getBaseUrl()
return new NextResponse(
`<!DOCTYPE html>
<html>
<head>
<title>Connecting to Trello...</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #0052CC 0%, #0079BF 100%);
}
.container {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
text-align: center;
max-width: 400px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #0052CC;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #ef4444;
margin-top: 1rem;
}
h2 {
color: #111827;
margin: 0 0 0.5rem 0;
}
p {
color: #6b7280;
margin: 0;
}
</style>
</head>
<body>
<div class="container">
<div class="spinner"></div>
<h2>Connecting to Trello</h2>
<p id="status">Processing authorization...</p>
<p id="error" class="error" style="display:none;"></p>
</div>
<script>
(function() {
const statusEl = document.getElementById('status');
const errorEl = document.getElementById('error');
try {
const fragment = window.location.hash.substring(1);
const params = new URLSearchParams(fragment);
const token = params.get('token');
if (!token) {
throw new Error('No token received from Trello');
}
statusEl.textContent = 'Saving your connection...';
fetch('${baseUrl}/api/auth/trello/store', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token: token })
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusEl.textContent = 'Success! Redirecting...';
setTimeout(function() {
window.location.href = '${baseUrl}/workspace?trello_connected=true';
}, 500);
} else {
throw new Error(data.error || 'Failed to save connection');
}
})
.catch(error => {
errorEl.textContent = error.message || 'Failed to save connection';
errorEl.style.display = 'block';
statusEl.textContent = 'Connection failed';
setTimeout(function() {
window.location.href = '${baseUrl}/workspace?error=trello_failed';
}, 3000);
});
} catch (error) {
errorEl.textContent = error.message || 'Authorization failed';
errorEl.style.display = 'block';
statusEl.textContent = 'Connection failed';
setTimeout(function() {
window.location.href = '${baseUrl}/workspace?error=trello_auth_failed';
}, 3000);
}
})();
</script>
</body>
</html>`,
{
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate',
},
}
)
}

View File

@@ -0,0 +1,87 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { db } from '@/../../packages/db'
import { account } from '@/../../packages/db/schema'
const logger = createLogger('TrelloStore')
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest) {
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn('Unauthorized attempt to store Trello token')
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { token } = body
if (!token) {
return NextResponse.json({ success: false, error: 'Token required' }, { status: 400 })
}
const apiKey = env.TRELLO_API_KEY
if (!apiKey) {
logger.error('TRELLO_API_KEY not configured')
return NextResponse.json({ success: false, error: 'Trello not configured' }, { status: 500 })
}
const validationUrl = `https://api.trello.com/1/members/me?key=${apiKey}&token=${token}&fields=id,username,fullName,email`
const userResponse = await fetch(validationUrl, {
headers: { Accept: 'application/json' },
})
if (!userResponse.ok) {
const errorText = await userResponse.text()
logger.error('Invalid Trello token', {
status: userResponse.status,
error: errorText,
})
return NextResponse.json(
{ success: false, error: `Invalid Trello token: ${errorText}` },
{ status: 400 }
)
}
const trelloUser = await userResponse.json()
const existing = await db.query.account.findFirst({
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'trello')),
})
const now = new Date()
if (existing) {
await db
.update(account)
.set({
accessToken: token,
accountId: trelloUser.id,
scope: 'read,write',
updatedAt: now,
})
.where(eq(account.id, existing.id))
} else {
await db.insert(account).values({
id: `trello_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'trello',
accountId: trelloUser.id,
accessToken: token,
scope: 'read,write',
createdAt: now,
updatedAt: now,
})
}
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error storing Trello token:', error)
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,112 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAlphanumericId } from '@/lib/security/input-validation'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaAddCommentAPI')
export async function POST(request: Request) {
try {
const { accessToken, taskGid, text } = await request.json()
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!taskGid) {
logger.error('Missing task GID in request')
return NextResponse.json({ error: 'Task GID is required' }, { status: 400 })
}
if (!text) {
logger.error('Missing comment text in request')
return NextResponse.json({ error: 'Comment text is required' }, { status: 400 })
}
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/stories`
const body = {
data: {
text,
},
}
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const story = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
gid: story.gid,
text: story.text || '',
created_at: story.created_at,
created_by: story.created_by
? {
gid: story.created_by.gid,
name: story.created_by.name,
}
: undefined,
},
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to add comment to Asana task',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,126 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAlphanumericId } from '@/lib/security/input-validation'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaCreateTaskAPI')
export async function POST(request: Request) {
try {
const { accessToken, workspace, name, notes, assignee, due_on } = await request.json()
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!name) {
logger.error('Missing task name in request')
return NextResponse.json({ error: 'Task name is required' }, { status: 400 })
}
if (!workspace) {
logger.error('Missing workspace in request')
return NextResponse.json({ error: 'Workspace GID is required' }, { status: 400 })
}
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const url = 'https://app.asana.com/api/1.0/tasks'
const taskData: Record<string, any> = {
name,
workspace,
}
if (notes) {
taskData.notes = notes
}
if (assignee) {
taskData.assignee = assignee
}
if (due_on) {
taskData.due_on = due_on
}
const body = { data: taskData }
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
gid: task.gid,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
created_at: task.created_at,
permalink_url: task.permalink_url,
},
})
} catch (error: any) {
logger.error('Error creating Asana task:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,95 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAlphanumericId } from '@/lib/security/input-validation'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaGetProjectsAPI')
export async function POST(request: Request) {
try {
const { accessToken, workspace } = await request.json()
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!workspace) {
logger.error('Missing workspace in request')
return NextResponse.json({ error: 'Workspace is required' }, { status: 400 })
}
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/projects?workspace=${workspace}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const projects = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
projects: projects.map((project: any) => ({
gid: project.gid,
name: project.name,
resource_type: project.resource_type,
})),
},
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Asana projects',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,222 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAlphanumericId } from '@/lib/security/input-validation'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaGetTaskAPI')
export async function POST(request: Request) {
try {
const { accessToken, taskGid, workspace, project, limit } = await request.json()
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (taskGid) {
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}?opt_fields=gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
gid: task.gid,
resource_type: task.resource_type,
resource_subtype: task.resource_subtype,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
assignee: task.assignee
? {
gid: task.assignee.gid,
name: task.assignee.name,
}
: undefined,
created_by: task.created_by
? {
gid: task.created_by.gid,
resource_type: task.created_by.resource_type,
name: task.created_by.name,
}
: undefined,
due_on: task.due_on || undefined,
created_at: task.created_at,
modified_at: task.modified_at,
},
})
}
if (!workspace && !project) {
logger.error('Either taskGid or workspace/project must be provided')
return NextResponse.json(
{ error: 'Either taskGid or workspace/project must be provided' },
{ status: 400 }
)
}
const params = new URLSearchParams()
if (project) {
const projectValidation = validateAlphanumericId(project, 'project', 100)
if (!projectValidation.isValid) {
return NextResponse.json({ error: projectValidation.error }, { status: 400 })
}
params.append('project', project)
} else if (workspace) {
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
params.append('workspace', workspace)
}
if (limit) {
params.append('limit', String(limit))
} else {
params.append('limit', '50')
}
params.append(
'opt_fields',
'gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype'
)
const url = `https://app.asana.com/api/1.0/tasks?${params.toString()}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const tasks = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
tasks: tasks.map((task: any) => ({
gid: task.gid,
resource_type: task.resource_type,
resource_subtype: task.resource_subtype,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
assignee: task.assignee
? {
gid: task.assignee.gid,
name: task.assignee.name,
}
: undefined,
created_by: task.created_by
? {
gid: task.created_by.gid,
resource_type: task.created_by.resource_type,
name: task.created_by.name,
}
: undefined,
due_on: task.due_on || undefined,
created_at: task.created_at,
modified_at: task.modified_at,
})),
next_page: result.next_page,
},
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Asana task(s)',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,138 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAlphanumericId } from '@/lib/security/input-validation'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaSearchTasksAPI')
export async function POST(request: Request) {
try {
const { accessToken, workspace, text, assignee, projects, completed } = await request.json()
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!workspace) {
logger.error('Missing workspace in request')
return NextResponse.json({ error: 'Workspace is required' }, { status: 400 })
}
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const params = new URLSearchParams()
if (text) {
params.append('text', text)
}
if (assignee) {
params.append('assignee.any', assignee)
}
if (projects && Array.isArray(projects) && projects.length > 0) {
params.append('projects.any', projects.join(','))
}
if (completed !== undefined) {
params.append('completed', String(completed))
}
params.append(
'opt_fields',
'gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype'
)
const url = `https://app.asana.com/api/1.0/workspaces/${workspace}/tasks/search?${params.toString()}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const tasks = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
tasks: tasks.map((task: any) => ({
gid: task.gid,
resource_type: task.resource_type,
resource_subtype: task.resource_subtype,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
assignee: task.assignee
? {
gid: task.assignee.gid,
name: task.assignee.name,
}
: undefined,
created_by: task.created_by
? {
gid: task.created_by.gid,
resource_type: task.created_by.resource_type,
name: task.created_by.name,
}
: undefined,
due_on: task.due_on || undefined,
created_at: task.created_at,
modified_at: task.modified_at,
})),
next_page: result.next_page,
},
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to search Asana tasks',
details: (error as Error).message,
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,125 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateAlphanumericId } from '@/lib/security/input-validation'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaUpdateTaskAPI')
export async function PUT(request: Request) {
try {
const { accessToken, taskGid, name, notes, assignee, completed, due_on } = await request.json()
if (!accessToken) {
logger.error('Missing access token in request')
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
if (!taskGid) {
logger.error('Missing task GID in request')
return NextResponse.json({ error: 'Task GID is required' }, { status: 400 })
}
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}`
const taskData: Record<string, any> = {}
if (name !== undefined) {
taskData.name = name
}
if (notes !== undefined) {
taskData.notes = notes
}
if (assignee !== undefined) {
taskData.assignee = assignee
}
if (completed !== undefined) {
taskData.completed = completed
}
if (due_on !== undefined) {
taskData.due_on = due_on
}
const body = { data: taskData }
const response = await fetch(url, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
gid: task.gid,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
modified_at: task.modified_at,
},
})
} catch (error: any) {
logger.error('Error updating Asana task:', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
success: false,
},
{ status: 500 }
)
}
}

View File

@@ -52,6 +52,13 @@ export async function POST(
const requestId = generateRequestId()
const { path } = await params
// Log ALL incoming webhook requests for debugging
logger.info(`[${requestId}] Incoming webhook request`, {
path,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
})
// Handle Microsoft Graph subscription validation (some environments send POST with validationToken)
try {
const url = new URL(request.url)
@@ -91,6 +98,23 @@ export async function POST(
const { webhook: foundWebhook, workflow: foundWorkflow } = findResult
// Log HubSpot webhook details for debugging
if (foundWebhook.provider === 'hubspot') {
const events = Array.isArray(body) ? body : [body]
const firstEvent = events[0]
logger.info(`[${requestId}] HubSpot webhook received`, {
path,
subscriptionType: firstEvent?.subscriptionType,
objectId: firstEvent?.objectId,
portalId: firstEvent?.portalId,
webhookId: foundWebhook.id,
workflowId: foundWorkflow.id,
triggerId: foundWebhook.providerConfig?.triggerId,
eventCount: events.length,
})
}
const authError = await verifyProviderAuth(
foundWebhook,
foundWorkflow,

View File

@@ -51,6 +51,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'write:confluence-content': 'Create and edit Confluence pages',
'write:confluence-space': 'Manage Confluence spaces',
'write:confluence-file': 'Upload files to Confluence',
'read:content:confluence': 'Read Confluence content',
'read:page:confluence': 'View Confluence pages',
'write:page:confluence': 'Create and update Confluence pages',
'read:comment:confluence': 'View comments on Confluence pages',
@@ -189,6 +190,47 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'sites:write': 'Manage webhooks and site settings',
'cms:read': 'View your CMS content',
'cms:write': 'Manage your CMS content',
'crm.objects.contacts.read': 'Read your HubSpot contacts',
'crm.objects.contacts.write': 'Create and update HubSpot contacts',
'crm.objects.companies.read': 'Read your HubSpot companies',
'crm.objects.companies.write': 'Create and update HubSpot companies',
'crm.objects.deals.read': 'Read your HubSpot deals',
'crm.objects.deals.write': 'Create and update HubSpot deals',
'crm.objects.owners.read': 'Read HubSpot object owners',
'crm.objects.users.read': 'Read HubSpot users',
'crm.objects.users.write': 'Create and update HubSpot users',
'crm.objects.marketing_events.read': 'Read HubSpot marketing events',
'crm.objects.marketing_events.write': 'Create and update HubSpot marketing events',
'crm.objects.line_items.read': 'Read HubSpot line items',
'crm.objects.line_items.write': 'Create and update HubSpot line items',
'crm.objects.quotes.read': 'Read HubSpot quotes',
'crm.objects.quotes.write': 'Create and update HubSpot quotes',
'crm.objects.appointments.read': 'Read HubSpot appointments',
'crm.objects.appointments.write': 'Create and update HubSpot appointments',
'crm.objects.carts.read': 'Read HubSpot shopping carts',
'crm.objects.carts.write': 'Create and update HubSpot shopping carts',
'crm.import': 'Import data into HubSpot',
'crm.lists.read': 'Read HubSpot lists',
'crm.lists.write': 'Create and update HubSpot lists',
tickets: 'Manage HubSpot tickets',
api: 'Access Salesforce API',
refresh_token: 'Maintain long-term access to your Salesforce account',
default: 'Access your Asana workspace',
base: 'Basic access to your Pipedrive account',
'deals:read': 'Read your Pipedrive deals',
'deals:full': 'Full access to manage your Pipedrive deals',
'contacts:read': 'Read your Pipedrive contacts',
'contacts:full': 'Full access to manage your Pipedrive contacts',
'leads:read': 'Read your Pipedrive leads',
'leads:full': 'Full access to manage your Pipedrive leads',
'activities:read': 'Read your Pipedrive activities',
'activities:full': 'Full access to manage your Pipedrive activities',
'mail:read': 'Read your Pipedrive emails',
'mail:full': 'Full access to manage your Pipedrive emails',
'projects:read': 'Read your Pipedrive projects',
'projects:full': 'Full access to manage your Pipedrive projects',
'webhooks:read': 'Read your Pipedrive webhooks',
'webhooks:full': 'Full access to manage your Pipedrive webhooks',
}
function getScopeDescription(scope: string): string {
@@ -241,6 +283,11 @@ export function OAuthRequiredModal({
requiredScopes,
})
if (providerId === 'trello') {
window.location.href = '/api/auth/trello/authorize'
return
}
await client.oauth2.link({
providerId,
callbackURL: window.location.href,
@@ -278,7 +325,7 @@ export function OAuthRequiredModal({
<div className='border-b px-4 py-3'>
<h4 className='font-medium text-sm'>Permissions requested</h4>
</div>
<ul className='space-y-3 px-4 py-3'>
<ul className='max-h-[400px] space-y-3 overflow-y-auto px-4 py-3'>
{displayScopes.map((scope) => (
<li key={scope} className='flex items-start gap-2 text-sm'>
<div className='mt-1 rounded-full bg-muted p-0.5'>

View File

@@ -237,6 +237,11 @@ export function Credentials({ onOpenChange, registerCloseHandler }: CredentialsP
scopes: service.scopes,
})
if (service.providerId === 'trello') {
window.location.href = '/api/auth/trello/authorize'
return
}
await client.oauth2.link({
providerId: service.providerId,
callbackURL: window.location.href,

View File

@@ -34,7 +34,12 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
type: 'oauth-input',
provider: 'airtable',
serviceId: 'airtable',
requiredScopes: ['data.records:read', 'data.records:write'], // Keep both scopes
requiredScopes: [
'data.records:read',
'data.records:write',
'user.email:read',
'webhook:manage',
],
placeholder: 'Select Airtable account',
required: true,
},

View File

@@ -0,0 +1,293 @@
import { AsanaIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { AsanaResponse } from '@/tools/asana/types'
export const AsanaBlock: BlockConfig<AsanaResponse> = {
type: 'asana',
name: 'Asana',
description: 'Interact with Asana',
authMode: AuthMode.OAuth,
longDescription: 'Integrate Asana into the workflow. Can read, write, and update tasks.',
docsLink: 'https://docs.sim.ai/tools/asana',
category: 'tools',
bgColor: '#E0E0E0',
icon: AsanaIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get Task', id: 'get_task' },
{ label: 'Create Task', id: 'create_task' },
{ label: 'Update Task', id: 'update_task' },
{ label: 'Get Projects', id: 'get_projects' },
{ label: 'Search Tasks', id: 'search_tasks' },
{ label: 'Add Comment', id: 'add_comment' },
],
value: () => 'get_task',
},
{
id: 'credential',
title: 'Asana Account',
type: 'oauth-input',
required: true,
provider: 'asana',
serviceId: 'asana',
requiredScopes: ['default'],
placeholder: 'Select Asana account',
},
{
id: 'workspace',
title: 'Workspace GID',
type: 'short-input',
required: true,
placeholder: 'Enter Asana workspace GID',
condition: {
field: 'operation',
value: ['create_task', 'get_projects', 'search_tasks'],
},
},
{
id: 'taskGid',
title: 'Task GID',
type: 'short-input',
required: false,
placeholder: 'Leave empty to get all tasks with filters below',
condition: {
field: 'operation',
value: ['get_task'],
},
},
{
id: 'taskGid',
title: 'Task GID',
type: 'short-input',
required: true,
placeholder: 'Enter Asana task GID',
condition: {
field: 'operation',
value: ['update_task', 'add_comment'],
},
},
{
id: 'getTasks_workspace',
title: 'Workspace GID',
type: 'short-input',
placeholder: 'Enter workspace GID',
condition: {
field: 'operation',
value: ['get_task'],
},
},
{
id: 'getTasks_project',
title: 'Project GID',
type: 'short-input',
placeholder: 'Enter project GID',
condition: {
field: 'operation',
value: ['get_task'],
},
},
{
id: 'getTasks_limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Max tasks to return (default: 50)',
condition: {
field: 'operation',
value: ['get_task'],
},
},
{
id: 'name',
title: 'Task Name',
type: 'short-input',
required: true,
placeholder: 'Enter task name',
condition: {
field: 'operation',
value: ['create_task', 'update_task'],
},
},
{
id: 'notes',
title: 'Task Notes',
type: 'long-input',
placeholder: 'Enter task notes or description',
condition: {
field: 'operation',
value: ['create_task', 'update_task'],
},
},
{
id: 'assignee',
title: 'Assignee GID',
type: 'short-input',
placeholder: 'Enter assignee user GID',
condition: {
field: 'operation',
value: ['create_task', 'update_task', 'search_tasks'],
},
},
{
id: 'due_on',
title: 'Due Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: {
field: 'operation',
value: ['create_task', 'update_task'],
},
},
{
id: 'searchText',
title: 'Search Text',
type: 'short-input',
placeholder: 'Enter search text',
condition: {
field: 'operation',
value: ['search_tasks'],
},
},
{
id: 'commentText',
title: 'Comment Text',
type: 'long-input',
required: true,
placeholder: 'Enter comment text',
condition: {
field: 'operation',
value: ['add_comment'],
},
},
],
tools: {
access: [
'asana_get_task',
'asana_create_task',
'asana_update_task',
'asana_get_projects',
'asana_search_tasks',
'asana_add_comment',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'get_task':
return 'asana_get_task'
case 'create_task':
return 'asana_create_task'
case 'update_task':
return 'asana_update_task'
case 'get_projects':
return 'asana_get_projects'
case 'search_tasks':
return 'asana_search_tasks'
case 'add_comment':
return 'asana_add_comment'
default:
return 'asana_get_task'
}
},
params: (params) => {
const { credential, operation } = params
const projectsArray = params.projects
? params.projects
.split(',')
.map((p: string) => p.trim())
.filter((p: string) => p.length > 0)
: undefined
const baseParams = {
accessToken: credential?.accessToken,
}
switch (operation) {
case 'get_task':
return {
...baseParams,
taskGid: params.taskGid,
workspace: params.getTasks_workspace,
project: params.getTasks_project,
limit: params.getTasks_limit ? Number(params.getTasks_limit) : undefined,
}
case 'create_task':
return {
...baseParams,
workspace: params.workspace,
name: params.name,
notes: params.notes,
assignee: params.assignee,
due_on: params.due_on,
}
case 'update_task':
return {
...baseParams,
taskGid: params.taskGid,
name: params.name,
notes: params.notes,
assignee: params.assignee,
completed: params.completed?.includes('completed'),
due_on: params.due_on,
}
case 'get_projects':
return {
...baseParams,
workspace: params.workspace,
}
case 'search_tasks':
return {
...baseParams,
workspace: params.workspace,
text: params.searchText,
assignee: params.assignee,
projects: projectsArray,
completed: params.completed?.includes('completed'),
}
case 'add_comment':
return {
...baseParams,
taskGid: params.taskGid,
text: params.commentText,
}
default:
return baseParams
}
},
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
workspace: { type: 'string', description: 'Workspace GID' },
taskGid: { type: 'string', description: 'Task GID' },
getTasks_workspace: { type: 'string', description: 'Workspace GID for getting tasks' },
getTasks_project: { type: 'string', description: 'Project GID filter for getting tasks' },
getTasks_limit: { type: 'string', description: 'Limit for getting tasks' },
name: { type: 'string', description: 'Task name' },
notes: { type: 'string', description: 'Task notes' },
assignee: { type: 'string', description: 'Assignee user GID' },
due_on: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
projects: { type: 'string', description: 'Project GIDs' },
completed: { type: 'array', description: 'Completion status' },
searchText: { type: 'string', description: 'Search text' },
commentText: { type: 'string', description: 'Comment text' },
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: { type: 'string', description: 'Operation result (JSON)' },
},
}

View File

@@ -118,7 +118,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
id: 'threadId',
title: 'Thread ID',
type: 'short-input',
placeholder: 'Thread ID to reply to (for threading)',
condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] },
mode: 'advanced',
@@ -128,7 +127,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
id: 'replyToMessageId',
title: 'Reply to Message ID',
type: 'short-input',
placeholder: 'Gmail message ID (not RFC Message-ID) - use the "id" field from results',
condition: { field: 'operation', value: ['send_gmail', 'draft_gmail'] },
mode: 'advanced',
@@ -233,7 +231,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
id: 'destinationLabel',
title: 'Move To Label',
type: 'folder-selector',
canonicalParamId: 'addLabelIds',
provider: 'google-email',
serviceId: 'gmail',
@@ -249,7 +246,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
id: 'manualDestinationLabel',
title: 'Move To Label',
type: 'short-input',
canonicalParamId: 'addLabelIds',
placeholder: 'Enter label ID (e.g., INBOX, Label_123)',
mode: 'advanced',

View File

@@ -20,7 +20,11 @@ export const GoogleFormsBlock: BlockConfig = {
required: true,
provider: 'google-forms',
serviceId: 'google-forms',
requiredScopes: [],
requiredScopes: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/forms.responses.readonly',
],
placeholder: 'Select Google account',
},
{

View File

@@ -36,7 +36,10 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
required: true,
provider: 'google-sheets',
serviceId: 'google-sheets',
requiredScopes: [],
requiredScopes: [
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/drive.file',
],
placeholder: 'Select Google account',
},
// Spreadsheet Selector
@@ -47,7 +50,10 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
canonicalParamId: 'spreadsheetId',
provider: 'google-drive',
serviceId: 'google-drive',
requiredScopes: [],
requiredScopes: [
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/drive.file',
],
mimeType: 'application/vnd.google-apps.spreadsheet',
placeholder: 'Select a spreadsheet',
dependsOn: ['credential'],

View File

@@ -0,0 +1,981 @@
import { HubspotIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { HubSpotResponse } from '@/tools/hubspot/types'
import { getTrigger } from '@/triggers'
import { hubspotAllTriggerOptions } from '@/triggers/hubspot/utils'
export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
type: 'hubspot',
name: 'HubSpot',
description: 'Interact with HubSpot CRM or trigger workflows from HubSpot events',
authMode: AuthMode.OAuth,
longDescription:
'Integrate HubSpot into your workflow. Manage contacts, companies, deals, tickets, and other CRM objects with powerful automation capabilities. Can be used in trigger mode to start workflows when contacts are created, deleted, or updated.',
docsLink: 'https://docs.sim.ai/tools/hubspot',
category: 'tools',
bgColor: '#FF7A59',
icon: HubspotIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get Users', id: 'get_users' },
{ label: 'Get Contacts', id: 'get_contacts' },
{ label: 'Create Contact', id: 'create_contact' },
{ label: 'Update Contact', id: 'update_contact' },
{ label: 'Search Contacts', id: 'search_contacts' },
{ label: 'Get Companies', id: 'get_companies' },
{ label: 'Create Company', id: 'create_company' },
{ label: 'Update Company', id: 'update_company' },
{ label: 'Search Companies', id: 'search_companies' },
{ label: 'Get Deals', id: 'get_deals' },
],
value: () => 'get_contacts',
},
{
id: 'credential',
title: 'HubSpot Account',
type: 'oauth-input',
provider: 'hubspot',
serviceId: 'hubspot',
requiredScopes: [
'crm.objects.contacts.read',
'crm.objects.contacts.write',
'crm.objects.companies.read',
'crm.objects.companies.write',
'crm.objects.deals.read',
'crm.objects.deals.write',
'crm.objects.owners.read',
'crm.objects.users.read',
'crm.objects.users.write',
'crm.objects.marketing_events.read',
'crm.objects.marketing_events.write',
'crm.objects.line_items.read',
'crm.objects.line_items.write',
'crm.objects.quotes.read',
'crm.objects.quotes.write',
'crm.objects.appointments.read',
'crm.objects.appointments.write',
'crm.objects.carts.read',
'crm.objects.carts.write',
'crm.import',
'crm.lists.read',
'crm.lists.write',
'tickets',
],
placeholder: 'Select HubSpot account',
required: true,
},
{
id: 'contactId',
title: 'Contact ID or Email',
type: 'short-input',
placeholder: 'Optional - Leave empty to list all contacts',
condition: { field: 'operation', value: ['get_contacts', 'update_contact'] },
},
{
id: 'companyId',
title: 'Company ID or Domain',
type: 'short-input',
placeholder: 'Optional - Leave empty to list all companies',
condition: { field: 'operation', value: ['get_companies', 'update_company'] },
},
{
id: 'idProperty',
title: 'ID Property',
type: 'short-input',
placeholder: 'Optional - e.g., "email" for contacts, "domain" for companies',
condition: {
field: 'operation',
value: ['get_contacts', 'update_contact', 'get_companies', 'update_company'],
},
},
{
id: 'propertiesToSet',
title: 'Properties',
type: 'long-input',
placeholder:
'JSON object with properties (e.g., {"email": "test@example.com", "firstname": "John"})',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact', 'create_company', 'update_company'],
},
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert HubSpot CRM developer. Generate HubSpot property objects as JSON based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the JSON object with HubSpot properties. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw JSON object that can be used directly in HubSpot API create/update operations.
### HUBSPOT PROPERTIES STRUCTURE
HubSpot properties are defined as a flat JSON object with property names as keys and their values as the corresponding values. Property names must match HubSpot's internal property names (usually lowercase, snake_case or no spaces).
### COMMON CONTACT PROPERTIES
**Standard Properties**:
- **email**: Email address (required for most operations)
- **firstname**: First name
- **lastname**: Last name
- **phone**: Phone number
- **mobilephone**: Mobile phone number
- **company**: Company name
- **jobtitle**: Job title
- **website**: Website URL
- **address**: Street address
- **city**: City
- **state**: State/Region
- **zip**: Postal code
- **country**: Country
- **lifecyclestage**: Lifecycle stage (e.g., "lead", "customer", "subscriber", "opportunity")
- **hs_lead_status**: Lead status (e.g., "NEW", "OPEN", "IN_PROGRESS", "QUALIFIED")
**Additional Properties**:
- **salutation**: Salutation (e.g., "Mr.", "Ms.", "Dr.")
- **degree**: Degree
- **industry**: Industry
- **fax**: Fax number
- **numemployees**: Number of employees (for companies)
- **annualrevenue**: Annual revenue (for companies)
### COMMON COMPANY PROPERTIES
**Standard Properties**:
- **name**: Company name (required)
- **domain**: Company domain (e.g., "example.com")
- **city**: City
- **state**: State/Region
- **zip**: Postal code
- **country**: Country
- **phone**: Phone number
- **industry**: Industry
- **type**: Company type (e.g., "PROSPECT", "PARTNER", "RESELLER", "VENDOR", "OTHER")
- **description**: Company description
- **website**: Website URL
- **numberofemployees**: Number of employees
- **annualrevenue**: Annual revenue
**Additional Properties**:
- **timezone**: Timezone
- **linkedin_company_page**: LinkedIn URL
- **twitterhandle**: Twitter handle
- **facebook_company_page**: Facebook URL
- **founded_year**: Year founded
### EXAMPLES
**Simple Contact**: "Create contact with email john@example.com and name John Doe"
→ {
"email": "john@example.com",
"firstname": "John",
"lastname": "Doe"
}
**Complete Contact**: "Create a lead contact with full details"
→ {
"email": "jane.smith@acme.com",
"firstname": "Jane",
"lastname": "Smith",
"phone": "+1-555-123-4567",
"company": "Acme Corp",
"jobtitle": "Marketing Manager",
"website": "https://acme.com",
"city": "San Francisco",
"state": "California",
"country": "United States",
"lifecyclestage": "lead",
"hs_lead_status": "NEW"
}
**Simple Company**: "Create company Acme Corp with domain acme.com"
→ {
"name": "Acme Corp",
"domain": "acme.com"
}
**Complete Company**: "Create a technology company with full details"
→ {
"name": "TechStart Inc",
"domain": "techstart.io",
"industry": "TECHNOLOGY",
"phone": "+1-555-987-6543",
"city": "Austin",
"state": "Texas",
"country": "United States",
"website": "https://techstart.io",
"description": "Innovative software solutions",
"numberofemployees": 50,
"annualrevenue": 5000000,
"type": "PROSPECT"
}
**Update Contact**: "Update contact phone and job title"
→ {
"phone": "+1-555-999-8888",
"jobtitle": "Senior Manager"
}
### REMEMBER
Return ONLY the JSON object with properties - no explanations, no markdown, no extra text.`,
placeholder: 'Describe the properties you want to set...',
generationType: 'json-object',
},
},
{
id: 'properties',
title: 'Properties to Return',
type: 'short-input',
placeholder: 'Comma-separated list (e.g., "email,firstname,lastname")',
condition: { field: 'operation', value: ['get_contacts', 'get_companies', 'get_deals'] },
},
{
id: 'associations',
title: 'Associations',
type: 'short-input',
placeholder: 'Comma-separated object types (e.g., "companies,deals")',
condition: {
field: 'operation',
value: ['get_contacts', 'get_companies', 'get_deals', 'create_contact', 'create_company'],
},
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Max results (list: 100, search: 200)',
condition: {
field: 'operation',
value: [
'get_users',
'get_contacts',
'get_companies',
'get_deals',
'search_contacts',
'search_companies',
],
},
},
{
id: 'after',
title: 'After (Pagination)',
type: 'short-input',
placeholder: 'Pagination cursor from previous response',
condition: {
field: 'operation',
value: [
'get_contacts',
'get_companies',
'get_deals',
'search_contacts',
'search_companies',
],
},
},
{
id: 'query',
title: 'Search Query',
type: 'short-input',
placeholder: 'Search term (e.g., company name, contact email)',
condition: { field: 'operation', value: ['search_contacts', 'search_companies'] },
},
{
id: 'filterGroups',
title: 'Filter Groups',
type: 'long-input',
placeholder:
'JSON array of filter groups (e.g., [{"filters":[{"propertyName":"email","operator":"EQ","value":"test@example.com"}]}])',
condition: { field: 'operation', value: ['search_contacts', 'search_companies'] },
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert HubSpot CRM developer. Generate HubSpot filter groups as JSON arrays based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the JSON array of filter groups. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw JSON array that can be used directly in HubSpot API search operations.
### HUBSPOT FILTER GROUPS STRUCTURE
Filter groups are arrays of filter objects. Each filter group contains an array of filters. Multiple filter groups are combined with OR logic, while filters within a group are combined with AND logic.
Structure:
[
{
"filters": [
{
"propertyName": "property_name",
"operator": "OPERATOR",
"value": "value"
}
]
}
]
### FILTER OPERATORS
HubSpot supports the following operators:
**Comparison Operators**:
- **EQ**: Equals - exact match
- **NEQ**: Not equals
- **LT**: Less than (for numbers and dates)
- **LTE**: Less than or equal to
- **GT**: Greater than (for numbers and dates)
- **GTE**: Greater than or equal to
- **BETWEEN**: Between two values (requires "highValue" field)
**String Operators**:
- **CONTAINS_TOKEN**: Contains the token (word)
- **NOT_CONTAINS_TOKEN**: Does not contain the token
**Existence Operators**:
- **HAS_PROPERTY**: Property has any value (value can be "*")
- **NOT_HAS_PROPERTY**: Property has no value (value can be "*")
**Set Operators**:
- **IN**: Value is in the provided list (value is semicolon-separated)
- **NOT_IN**: Value is not in the provided list
### COMMON CONTACT PROPERTIES FOR FILTERING
- **email**: Email address
- **firstname**: First name
- **lastname**: Last name
- **lifecyclestage**: Lifecycle stage (lead, customer, subscriber, opportunity)
- **hs_lead_status**: Lead status (NEW, OPEN, IN_PROGRESS, QUALIFIED)
- **createdate**: Creation date (milliseconds timestamp)
- **lastmodifieddate**: Last modified date
- **phone**: Phone number
- **company**: Company name
- **jobtitle**: Job title
### COMMON COMPANY PROPERTIES FOR FILTERING
- **name**: Company name
- **domain**: Company domain
- **industry**: Industry
- **type**: Company type
- **city**: City
- **state**: State
- **country**: Country
- **numberofemployees**: Number of employees
- **annualrevenue**: Annual revenue
- **createdate**: Creation date
### EXAMPLES
**Simple Equality**: "Find contacts with email john@example.com"
→ [
{
"filters": [
{
"propertyName": "email",
"operator": "EQ",
"value": "john@example.com"
}
]
}
]
**Multiple Filters (AND)**: "Find lead contacts in San Francisco"
→ [
{
"filters": [
{
"propertyName": "lifecyclestage",
"operator": "EQ",
"value": "lead"
},
{
"propertyName": "city",
"operator": "EQ",
"value": "San Francisco"
}
]
}
]
**Multiple Filter Groups (OR)**: "Find contacts who are either leads or customers"
→ [
{
"filters": [
{
"propertyName": "lifecyclestage",
"operator": "EQ",
"value": "lead"
}
]
},
{
"filters": [
{
"propertyName": "lifecyclestage",
"operator": "EQ",
"value": "customer"
}
]
}
]
**Contains Text**: "Find contacts with Gmail addresses"
→ [
{
"filters": [
{
"propertyName": "email",
"operator": "CONTAINS_TOKEN",
"value": "@gmail.com"
}
]
}
]
**IN Operator**: "Find companies in tech or finance industries"
→ [
{
"filters": [
{
"propertyName": "industry",
"operator": "IN",
"value": "TECHNOLOGY;FINANCE"
}
]
}
]
**Has Property**: "Find contacts with phone numbers"
→ [
{
"filters": [
{
"propertyName": "phone",
"operator": "HAS_PROPERTY",
"value": "*"
}
]
}
]
**Range Filter**: "Find companies with 10 to 100 employees"
→ [
{
"filters": [
{
"propertyName": "numberofemployees",
"operator": "GTE",
"value": "10"
},
{
"propertyName": "numberofemployees",
"operator": "LTE",
"value": "100"
}
]
}
]
### REMEMBER
Return ONLY the JSON array of filter groups - no explanations, no markdown, no extra text.`,
placeholder: 'Describe the filters you want to apply...',
generationType: 'json-object',
},
},
{
id: 'sorts',
title: 'Sort Order',
type: 'long-input',
placeholder:
'JSON array of sort objects (e.g., [{"propertyName":"createdate","direction":"DESCENDING"}])',
condition: { field: 'operation', value: ['search_contacts', 'search_companies'] },
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert HubSpot CRM developer. Generate HubSpot sort arrays as JSON based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the JSON array of sort objects. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw JSON array that can be used directly in HubSpot API search operations.
### HUBSPOT SORT STRUCTURE
Sorts are defined as an array of objects, each containing a property name and a direction. Results will be sorted by the first sort object, then by the second if values are equal, and so on.
Structure:
[
{
"propertyName": "property_name",
"direction": "ASCENDING" | "DESCENDING"
}
]
### SORT DIRECTIONS
- **ASCENDING**: Sort from lowest to highest (A-Z, 0-9, oldest to newest)
- **DESCENDING**: Sort from highest to lowest (Z-A, 9-0, newest to oldest)
### COMMON SORTABLE PROPERTIES
**Contact Properties**:
- **createdate**: Creation date (when the contact was created)
- **lastmodifieddate**: Last modified date (when the contact was last updated)
- **firstname**: First name (alphabetical)
- **lastname**: Last name (alphabetical)
- **email**: Email address (alphabetical)
- **lifecyclestage**: Lifecycle stage
- **hs_lead_status**: Lead status
- **company**: Company name (alphabetical)
- **jobtitle**: Job title (alphabetical)
- **phone**: Phone number
**Company Properties**:
- **createdate**: Creation date
- **lastmodifieddate**: Last modified date
- **name**: Company name (alphabetical)
- **domain**: Domain (alphabetical)
- **industry**: Industry
- **city**: City (alphabetical)
- **state**: State (alphabetical)
- **numberofemployees**: Number of employees (numeric)
- **annualrevenue**: Annual revenue (numeric)
### EXAMPLES
**Simple Sort**: "Sort by creation date, newest first"
→ [
{
"propertyName": "createdate",
"direction": "DESCENDING"
}
]
**Alphabetical Sort**: "Sort contacts by last name A to Z"
→ [
{
"propertyName": "lastname",
"direction": "ASCENDING"
}
]
**Multiple Sorts**: "Sort by lifecycle stage, then by last name"
→ [
{
"propertyName": "lifecyclestage",
"direction": "ASCENDING"
},
{
"propertyName": "lastname",
"direction": "ASCENDING"
}
]
**Numeric Sort**: "Sort companies by revenue, highest first"
→ [
{
"propertyName": "annualrevenue",
"direction": "DESCENDING"
}
]
**Recent First**: "Show most recently updated contacts first"
→ [
{
"propertyName": "lastmodifieddate",
"direction": "DESCENDING"
}
]
**Name and Date**: "Sort by company name, then by creation date newest first"
→ [
{
"propertyName": "name",
"direction": "ASCENDING"
},
{
"propertyName": "createdate",
"direction": "DESCENDING"
}
]
### REMEMBER
Return ONLY the JSON array of sort objects - no explanations, no markdown, no extra text.`,
placeholder: 'Describe how you want to sort the results...',
generationType: 'json-object',
},
},
{
id: 'searchProperties',
title: 'Properties to Return',
type: 'long-input',
placeholder: 'JSON array of properties (e.g., ["email","firstname","lastname"])',
condition: { field: 'operation', value: ['search_contacts', 'search_companies'] },
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: `You are an expert HubSpot CRM developer. Generate HubSpot property arrays as JSON based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the JSON array of property names. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw JSON array of strings that can be used directly in HubSpot API search operations.
### HUBSPOT PROPERTIES ARRAY STRUCTURE
Properties to return are defined as a simple array of property name strings. These specify which fields should be included in the search results.
Structure:
["property1", "property2", "property3"]
### COMMON CONTACT PROPERTIES
**Basic Information**:
- **email**: Email address
- **firstname**: First name
- **lastname**: Last name
- **phone**: Phone number
- **mobilephone**: Mobile phone number
**Professional Information**:
- **company**: Company name
- **jobtitle**: Job title
- **industry**: Industry
- **department**: Department
- **seniority**: Seniority level
**Address Information**:
- **address**: Street address
- **city**: City
- **state**: State/Region
- **zip**: Postal code
- **country**: Country
**CRM Information**:
- **lifecyclestage**: Lifecycle stage
- **hs_lead_status**: Lead status
- **hubspot_owner_id**: Owner ID
- **hs_analytics_source**: Original source
**Dates**:
- **createdate**: Creation date
- **lastmodifieddate**: Last modified date
- **hs_lifecyclestage_lead_date**: Lead date
- **hs_lifecyclestage_customer_date**: Customer date
**Website & Social**:
- **website**: Website URL
- **linkedin_url**: LinkedIn profile URL
- **twitterhandle**: Twitter handle
### COMMON COMPANY PROPERTIES
**Basic Information**:
- **name**: Company name
- **domain**: Company domain
- **phone**: Phone number
- **industry**: Industry
- **type**: Company type
**Address Information**:
- **city**: City
- **state**: State/Region
- **zip**: Postal code
- **country**: Country
- **address**: Street address
**Business Information**:
- **numberofemployees**: Number of employees
- **annualrevenue**: Annual revenue
- **founded_year**: Year founded
- **description**: Company description
**Website & Social**:
- **website**: Website URL
- **linkedin_company_page**: LinkedIn company page
- **twitterhandle**: Twitter handle
- **facebook_company_page**: Facebook page
**CRM Information**:
- **hubspot_owner_id**: Owner ID
- **createdate**: Creation date
- **lastmodifieddate**: Last modified date
- **hs_lastmodifieddate**: Last modified date (detailed)
### EXAMPLES
**Basic Contact Fields**: "Return email, name, and phone"
→ ["email", "firstname", "lastname", "phone"]
**Complete Contact Profile**: "Return all contact details"
→ ["email", "firstname", "lastname", "phone", "mobilephone", "company", "jobtitle", "address", "city", "state", "zip", "country", "lifecyclestage", "hs_lead_status", "createdate"]
**Business Contact Info**: "Return professional information"
→ ["email", "firstname", "lastname", "company", "jobtitle", "phone", "industry"]
**Basic Company Fields**: "Return company name, domain, and industry"
→ ["name", "domain", "industry"]
**Complete Company Profile**: "Return all company information"
→ ["name", "domain", "industry", "phone", "city", "state", "country", "numberofemployees", "annualrevenue", "website", "description", "type", "createdate"]
**Contact with Dates**: "Return contact info with timestamps"
→ ["email", "firstname", "lastname", "createdate", "lastmodifieddate", "lifecyclestage"]
**Company Financial Info**: "Return company size and revenue"
→ ["name", "domain", "numberofemployees", "annualrevenue", "industry"]
**Social Media Properties**: "Return social media links"
→ ["email", "firstname", "lastname", "linkedin_url", "twitterhandle"]
**CRM Status Fields**: "Return lifecycle and owner information"
→ ["email", "firstname", "lastname", "lifecyclestage", "hs_lead_status", "hubspot_owner_id"]
### REMEMBER
Return ONLY the JSON array of property names - no explanations, no markdown, no extra text.`,
placeholder: 'Describe which properties you want to return...',
generationType: 'json-object',
},
},
{
id: 'selectedTriggerId',
title: 'Trigger Type',
type: 'dropdown',
mode: 'trigger',
options: hubspotAllTriggerOptions,
value: () => 'hubspot_contact_created',
required: true,
},
...getTrigger('hubspot_contact_created').subBlocks.slice(1),
...getTrigger('hubspot_contact_deleted').subBlocks.slice(1),
...getTrigger('hubspot_contact_privacy_deleted').subBlocks.slice(1),
...getTrigger('hubspot_contact_property_changed').subBlocks.slice(1),
...getTrigger('hubspot_company_created').subBlocks.slice(1),
...getTrigger('hubspot_company_deleted').subBlocks.slice(1),
...getTrigger('hubspot_company_property_changed').subBlocks.slice(1),
...getTrigger('hubspot_conversation_creation').subBlocks.slice(1),
...getTrigger('hubspot_conversation_deletion').subBlocks.slice(1),
...getTrigger('hubspot_conversation_new_message').subBlocks.slice(1),
...getTrigger('hubspot_conversation_privacy_deletion').subBlocks.slice(1),
...getTrigger('hubspot_conversation_property_changed').subBlocks.slice(1),
...getTrigger('hubspot_deal_created').subBlocks.slice(1),
...getTrigger('hubspot_deal_deleted').subBlocks.slice(1),
...getTrigger('hubspot_deal_property_changed').subBlocks.slice(1),
...getTrigger('hubspot_ticket_created').subBlocks.slice(1),
...getTrigger('hubspot_ticket_deleted').subBlocks.slice(1),
...getTrigger('hubspot_ticket_property_changed').subBlocks.slice(1),
],
tools: {
access: [
'hubspot_get_users',
'hubspot_list_contacts',
'hubspot_get_contact',
'hubspot_create_contact',
'hubspot_update_contact',
'hubspot_search_contacts',
'hubspot_list_companies',
'hubspot_get_company',
'hubspot_create_company',
'hubspot_update_company',
'hubspot_search_companies',
'hubspot_list_deals',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'get_users':
return 'hubspot_get_users'
case 'get_contacts':
return params.contactId ? 'hubspot_get_contact' : 'hubspot_list_contacts'
case 'create_contact':
return 'hubspot_create_contact'
case 'update_contact':
return 'hubspot_update_contact'
case 'search_contacts':
return 'hubspot_search_contacts'
case 'get_companies':
return params.companyId ? 'hubspot_get_company' : 'hubspot_list_companies'
case 'create_company':
return 'hubspot_create_company'
case 'update_company':
return 'hubspot_update_company'
case 'search_companies':
return 'hubspot_search_companies'
case 'get_deals':
return 'hubspot_list_deals'
default:
throw new Error(`Unknown operation: ${params.operation}`)
}
},
params: (params) => {
const {
credential,
operation,
propertiesToSet,
properties,
searchProperties,
filterGroups,
sorts,
associations,
...rest
} = params
const cleanParams: Record<string, any> = {
credential,
}
if (propertiesToSet) {
try {
cleanParams.properties =
typeof propertiesToSet === 'string' ? JSON.parse(propertiesToSet) : propertiesToSet
} catch (error) {
throw new Error('Invalid JSON in properties field')
}
}
if (properties && !searchProperties) {
cleanParams.properties = properties
}
if (searchProperties) {
try {
cleanParams.properties =
typeof searchProperties === 'string' ? JSON.parse(searchProperties) : searchProperties
} catch (error) {
throw new Error('Invalid JSON in searchProperties field')
}
}
if (filterGroups) {
try {
cleanParams.filterGroups =
typeof filterGroups === 'string' ? JSON.parse(filterGroups) : filterGroups
} catch (error) {
throw new Error('Invalid JSON in filterGroups field')
}
}
if (sorts) {
try {
cleanParams.sorts = typeof sorts === 'string' ? JSON.parse(sorts) : sorts
} catch (error) {
throw new Error('Invalid JSON in sorts field')
}
}
if (associations) {
cleanParams.associations = associations
}
// Add other params
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: 'HubSpot access token' },
contactId: { type: 'string', description: 'Contact ID or email' },
companyId: { type: 'string', description: 'Company ID or domain' },
idProperty: { type: 'string', description: 'Property name to use as unique identifier' },
propertiesToSet: { type: 'json', description: 'Properties to create/update (JSON object)' },
properties: {
type: 'string',
description: 'Comma-separated properties to return (for list/get)',
},
associations: { type: 'string', description: 'Comma-separated object types for associations' },
limit: { type: 'string', description: 'Maximum results (list: 100, search: 200)' },
after: { type: 'string', description: 'Pagination cursor' },
query: { type: 'string', description: 'Search query string' },
filterGroups: { type: 'json', description: 'Filter groups for search (JSON array)' },
sorts: { type: 'json', description: 'Sort order (JSON array of strings or objects)' },
searchProperties: { type: 'json', description: 'Properties to return in search (JSON array)' },
},
outputs: {
users: { type: 'json', description: 'Array of user objects' },
contacts: { type: 'json', description: 'Array of contact objects' },
contact: { type: 'json', description: 'Single contact object' },
companies: { type: 'json', description: 'Array of company objects' },
company: { type: 'json', description: 'Single company object' },
deals: { type: 'json', description: 'Array of deal objects' },
total: { type: 'number', description: 'Total number of matching results (for search)' },
paging: { type: 'json', description: 'Pagination info with next/prev cursors' },
metadata: { type: 'json', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success status' },
payload: {
type: 'json',
description: 'Full webhook payload array from HubSpot containing event details',
},
provider: {
type: 'string',
description: 'Provider name (hubspot)',
},
providerConfig: {
appId: {
type: 'string',
description: 'HubSpot App ID',
},
clientId: {
type: 'string',
description: 'HubSpot Client ID',
},
triggerId: {
type: 'string',
description: 'Trigger ID (e.g., hubspot_company_created)',
},
clientSecret: {
type: 'string',
description: 'HubSpot Client Secret',
},
developerApiKey: {
type: 'string',
description: 'HubSpot Developer API Key',
},
curlSetWebhookUrl: {
type: 'string',
description: 'curl command to set webhook URL',
},
curlCreateSubscription: {
type: 'string',
description: 'curl command to create subscription',
},
webhookUrlDisplay: {
type: 'string',
description: 'Webhook URL display value',
},
propertyName: {
type: 'string',
description: 'Optional property name filter (for property change triggers)',
},
},
} as any,
triggerAllowed: true,
triggers: {
enabled: true,
available: [
'hubspot_contact_created',
'hubspot_contact_deleted',
'hubspot_contact_privacy_deleted',
'hubspot_contact_property_changed',
'hubspot_company_created',
'hubspot_company_deleted',
'hubspot_company_property_changed',
'hubspot_conversation_creation',
'hubspot_conversation_deletion',
'hubspot_conversation_new_message',
'hubspot_conversation_privacy_deletion',
'hubspot_conversation_property_changed',
'hubspot_deal_created',
'hubspot_deal_deleted',
'hubspot_deal_property_changed',
'hubspot_ticket_created',
'hubspot_ticket_deleted',
'hubspot_ticket_property_changed',
],
},
}

View File

@@ -32,7 +32,14 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
type: 'oauth-input',
provider: 'microsoft-excel',
serviceId: 'microsoft-excel',
requiredScopes: [],
requiredScopes: [
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'offline_access',
],
placeholder: 'Select Microsoft account',
required: true,
},

View File

@@ -0,0 +1,733 @@
import { PipedriveIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { PipedriveResponse } from '@/tools/pipedrive/types'
export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
type: 'pipedrive',
name: 'Pipedrive',
description: 'Interact with Pipedrive CRM',
authMode: AuthMode.OAuth,
longDescription:
'Integrate Pipedrive into your workflow. Manage deals, contacts, sales pipeline, projects, activities, files, and communications with powerful CRM capabilities.',
docsLink: 'https://docs.sim.ai/tools/pipedrive',
category: 'tools',
bgColor: '#2E6936',
icon: PipedriveIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get All Deals', id: 'get_all_deals' },
{ label: 'Get Deal', id: 'get_deal' },
{ label: 'Create Deal', id: 'create_deal' },
{ label: 'Update Deal', id: 'update_deal' },
{ label: 'Get Files', id: 'get_files' },
{ label: 'Get Mail Threads', id: 'get_mail_messages' },
{ label: 'Get Mail Thread Messages', id: 'get_mail_thread' },
{ label: 'Get Pipelines', id: 'get_pipelines' },
{ label: 'Get Pipeline Deals', id: 'get_pipeline_deals' },
{ label: 'Get Projects', id: 'get_projects' },
{ label: 'Create Project', id: 'create_project' },
{ label: 'Get Activities', id: 'get_activities' },
{ label: 'Create Activity', id: 'create_activity' },
{ label: 'Update Activity', id: 'update_activity' },
{ label: 'Get Leads', id: 'get_leads' },
{ label: 'Create Lead', id: 'create_lead' },
{ label: 'Update Lead', id: 'update_lead' },
{ label: 'Delete Lead', id: 'delete_lead' },
],
value: () => 'get_all_deals',
},
{
id: 'credential',
title: 'Pipedrive Account',
type: 'oauth-input',
provider: 'pipedrive',
serviceId: 'pipedrive',
requiredScopes: [
'base',
'deals:full',
'contacts:full',
'leads:full',
'activities:full',
'mail:full',
'projects:full',
],
placeholder: 'Select Pipedrive account',
required: true,
},
{
id: 'status',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'All (not deleted)', id: '' },
{ label: 'Open', id: 'open' },
{ label: 'Won', id: 'won' },
{ label: 'Lost', id: 'lost' },
],
value: () => '',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Filter by person ID',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'org_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Filter by organization ID',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'pipeline_id',
title: 'Pipeline ID',
type: 'short-input',
placeholder: 'Filter by pipeline ID ',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'updated_since',
title: 'Updated Since',
type: 'short-input',
placeholder: 'Date (2025-01-01T10:20:00Z)',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
condition: { field: 'operation', value: ['get_all_deals'] },
},
{
id: 'deal_id',
title: 'Deal ID',
type: 'short-input',
placeholder: 'Enter deal ID',
required: true,
condition: { field: 'operation', value: ['get_deal', 'update_deal'] },
},
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Enter deal title',
required: true,
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'value',
title: 'Value',
type: 'short-input',
placeholder: 'Monetary value ',
condition: { field: 'operation', value: ['create_deal', 'update_deal'] },
},
{
id: 'currency',
title: 'Currency',
type: 'short-input',
placeholder: 'Currency code (e.g., USD, EUR)',
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Associated person ID ',
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'org_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Associated organization ID ',
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'pipeline_id',
title: 'Pipeline ID',
type: 'short-input',
placeholder: 'Pipeline ID ',
condition: { field: 'operation', value: ['create_deal'] },
},
{
id: 'stage_id',
title: 'Stage ID',
type: 'short-input',
placeholder: 'Stage ID ',
condition: { field: 'operation', value: ['create_deal', 'update_deal'] },
},
{
id: 'status',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'Open', id: 'open' },
{ label: 'Won', id: 'won' },
{ label: 'Lost', id: 'lost' },
],
value: () => 'open',
condition: { field: 'operation', value: ['create_deal', 'update_deal'] },
},
{
id: 'expected_close_date',
title: 'Expected Close Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD ',
condition: { field: 'operation', value: ['create_deal', 'update_deal'] },
},
{
id: 'title',
title: 'New Title',
type: 'short-input',
placeholder: 'New deal title ',
condition: { field: 'operation', value: ['update_deal'] },
},
{
id: 'deal_id',
title: 'Deal ID',
type: 'short-input',
placeholder: 'Filter by deal ID ',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Filter by person ID ',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'org_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Filter by organization ID ',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'folder',
title: 'Folder',
type: 'dropdown',
options: [
{ label: 'Inbox', id: 'inbox' },
{ label: 'Drafts', id: 'drafts' },
{ label: 'Sent', id: 'sent' },
{ label: 'Archive', id: 'archive' },
],
value: () => 'inbox',
condition: { field: 'operation', value: ['get_mail_messages'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 50)',
condition: { field: 'operation', value: ['get_mail_messages'] },
},
{
id: 'thread_id',
title: 'Thread ID',
type: 'short-input',
placeholder: 'Enter mail thread ID',
required: true,
condition: { field: 'operation', value: ['get_mail_thread'] },
},
{
id: 'sort_by',
title: 'Sort By',
type: 'dropdown',
options: [
{ label: 'ID', id: 'id' },
{ label: 'Update Time', id: 'update_time' },
{ label: 'Add Time', id: 'add_time' },
],
value: () => 'id',
condition: { field: 'operation', value: ['get_pipelines'] },
},
{
id: 'sort_direction',
title: 'Sort Direction',
type: 'dropdown',
options: [
{ label: 'Ascending', id: 'asc' },
{ label: 'Descending', id: 'desc' },
],
value: () => 'asc',
condition: { field: 'operation', value: ['get_pipelines'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
condition: { field: 'operation', value: ['get_pipelines'] },
},
{
id: 'cursor',
title: 'Cursor',
type: 'short-input',
placeholder: 'Pagination cursor (optional)',
condition: { field: 'operation', value: ['get_pipelines'] },
},
{
id: 'pipeline_id',
title: 'Pipeline ID',
type: 'short-input',
placeholder: 'Enter pipeline ID',
required: true,
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'stage_id',
title: 'Stage ID',
type: 'short-input',
placeholder: 'Filter by stage ID ',
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'status',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Open', id: 'open' },
{ label: 'Won', id: 'won' },
{ label: 'Lost', id: 'lost' },
],
value: () => '',
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'project_id',
title: 'Project ID',
type: 'short-input',
placeholder: 'Project ID',
condition: { field: 'operation', value: ['get_projects'] },
},
{
id: 'status',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Open', id: 'open' },
{ label: 'Completed', id: 'completed' },
],
value: () => '',
condition: { field: 'operation', value: ['get_projects'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
condition: { field: 'operation', value: ['get_projects'] },
},
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Enter project title',
required: true,
condition: { field: 'operation', value: ['create_project'] },
},
{
id: 'description',
title: 'Description',
type: 'long-input',
placeholder: 'Project description ',
condition: { field: 'operation', value: ['create_project'] },
},
{
id: 'start_date',
title: 'Start Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD ',
condition: { field: 'operation', value: ['create_project'] },
},
{
id: 'end_date',
title: 'End Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD ',
condition: { field: 'operation', value: ['create_project'] },
},
{
id: 'deal_id',
title: 'Deal ID',
type: 'short-input',
placeholder: 'Filter by deal ID ',
condition: { field: 'operation', value: ['get_activities', 'create_activity'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Filter by person ID ',
condition: { field: 'operation', value: ['get_activities', 'create_activity'] },
},
{
id: 'org_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Filter by organization ID ',
condition: { field: 'operation', value: ['get_activities', 'create_activity'] },
},
{
id: 'type',
title: 'Activity Type',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Call', id: 'call' },
{ label: 'Meeting', id: 'meeting' },
{ label: 'Task', id: 'task' },
{ label: 'Deadline', id: 'deadline' },
{ label: 'Email', id: 'email' },
{ label: 'Lunch', id: 'lunch' },
],
value: () => '',
condition: { field: 'operation', value: ['get_activities'] },
},
{
id: 'done',
title: 'Completion Status',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Not Done', id: '0' },
{ label: 'Done', id: '1' },
],
value: () => '',
condition: { field: 'operation', value: ['get_activities'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
condition: { field: 'operation', value: ['get_activities'] },
},
{
id: 'subject',
title: 'Subject',
type: 'short-input',
placeholder: 'Activity subject/title',
required: true,
condition: { field: 'operation', value: ['create_activity', 'update_activity'] },
},
{
id: 'type',
title: 'Activity Type',
type: 'dropdown',
options: [
{ label: 'Call', id: 'call' },
{ label: 'Meeting', id: 'meeting' },
{ label: 'Task', id: 'task' },
{ label: 'Deadline', id: 'deadline' },
{ label: 'Email', id: 'email' },
{ label: 'Lunch', id: 'lunch' },
],
value: () => 'task',
required: true,
condition: { field: 'operation', value: ['create_activity'] },
},
{
id: 'due_date',
title: 'Due Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
required: true,
condition: { field: 'operation', value: ['create_activity', 'update_activity'] },
},
{
id: 'due_time',
title: 'Due Time',
type: 'short-input',
placeholder: 'HH:MM ',
condition: { field: 'operation', value: ['create_activity', 'update_activity'] },
},
{
id: 'duration',
title: 'Duration',
type: 'short-input',
placeholder: 'HH:MM ',
condition: { field: 'operation', value: ['create_activity', 'update_activity'] },
},
{
id: 'note',
title: 'Notes',
type: 'long-input',
placeholder: 'Activity notes ',
condition: { field: 'operation', value: ['create_activity', 'update_activity'] },
},
{
id: 'activity_id',
title: 'Activity ID',
type: 'short-input',
placeholder: 'Enter activity ID',
required: true,
condition: { field: 'operation', value: ['update_activity'] },
},
{
id: 'done',
title: 'Mark as Done',
type: 'dropdown',
options: [
{ label: 'Not Done', id: '0' },
{ label: 'Done', id: '1' },
],
value: () => '0',
condition: { field: 'operation', value: ['update_activity'] },
},
{
id: 'lead_id',
title: 'Lead ID',
type: 'short-input',
placeholder: 'Lead ID',
condition: { field: 'operation', value: ['get_leads', 'update_lead', 'delete_lead'] },
},
{
id: 'archived',
title: 'Archived',
type: 'dropdown',
options: [
{ label: 'Active Leads', id: 'false' },
{ label: 'Archived Leads', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: ['get_leads'] },
},
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Enter lead title',
required: true,
condition: { field: 'operation', value: ['create_lead'] },
},
{
id: 'title',
title: 'New Title',
type: 'short-input',
placeholder: 'New lead title',
condition: { field: 'operation', value: ['update_lead'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Person ID to link lead to',
condition: { field: 'operation', value: ['create_lead', 'update_lead', 'get_leads'] },
},
{
id: 'organization_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Organization ID to link lead to',
condition: { field: 'operation', value: ['create_lead', 'update_lead', 'get_leads'] },
},
{
id: 'owner_id',
title: 'Owner ID',
type: 'short-input',
placeholder: 'Owner user ID',
condition: { field: 'operation', value: ['create_lead', 'update_lead', 'get_leads'] },
},
{
id: 'value_amount',
title: 'Value Amount',
type: 'short-input',
placeholder: 'Potential value amount',
condition: { field: 'operation', value: ['create_lead', 'update_lead'] },
},
{
id: 'value_currency',
title: 'Value Currency',
type: 'short-input',
placeholder: 'Currency code (e.g., USD, EUR)',
condition: { field: 'operation', value: ['create_lead', 'update_lead'] },
},
{
id: 'expected_close_date',
title: 'Expected Close Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: { field: 'operation', value: ['create_lead', 'update_lead'] },
},
{
id: 'is_archived',
title: 'Archive Lead',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: ['update_lead'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100)',
condition: { field: 'operation', value: ['get_leads'] },
},
],
tools: {
access: [
'pipedrive_get_all_deals',
'pipedrive_get_deal',
'pipedrive_create_deal',
'pipedrive_update_deal',
'pipedrive_get_files',
'pipedrive_get_mail_messages',
'pipedrive_get_mail_thread',
'pipedrive_get_pipelines',
'pipedrive_get_pipeline_deals',
'pipedrive_get_projects',
'pipedrive_create_project',
'pipedrive_get_activities',
'pipedrive_create_activity',
'pipedrive_update_activity',
'pipedrive_get_leads',
'pipedrive_create_lead',
'pipedrive_update_lead',
'pipedrive_delete_lead',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'get_all_deals':
return 'pipedrive_get_all_deals'
case 'get_deal':
return 'pipedrive_get_deal'
case 'create_deal':
return 'pipedrive_create_deal'
case 'update_deal':
return 'pipedrive_update_deal'
case 'get_files':
return 'pipedrive_get_files'
case 'get_mail_messages':
return 'pipedrive_get_mail_messages'
case 'get_mail_thread':
return 'pipedrive_get_mail_thread'
case 'get_pipelines':
return 'pipedrive_get_pipelines'
case 'get_pipeline_deals':
return 'pipedrive_get_pipeline_deals'
case 'get_projects':
return 'pipedrive_get_projects'
case 'create_project':
return 'pipedrive_create_project'
case 'get_activities':
return 'pipedrive_get_activities'
case 'create_activity':
return 'pipedrive_create_activity'
case 'update_activity':
return 'pipedrive_update_activity'
case 'get_leads':
return 'pipedrive_get_leads'
case 'create_lead':
return 'pipedrive_create_lead'
case 'update_lead':
return 'pipedrive_update_lead'
case 'delete_lead':
return 'pipedrive_delete_lead'
default:
throw new Error(`Unknown operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, operation, ...rest } = params
const cleanParams: Record<string, any> = {
credential,
}
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: 'Pipedrive access token' },
deal_id: { type: 'string', description: 'Deal ID' },
title: { type: 'string', description: 'Title' },
value: { type: 'string', description: 'Monetary value' },
currency: { type: 'string', description: 'Currency code' },
person_id: { type: 'string', description: 'Person ID' },
org_id: { type: 'string', description: 'Organization ID' },
pipeline_id: { type: 'string', description: 'Pipeline ID' },
stage_id: { type: 'string', description: 'Stage ID' },
status: { type: 'string', description: 'Status' },
expected_close_date: { type: 'string', description: 'Expected close date' },
updated_since: { type: 'string', description: 'Updated since timestamp' },
limit: { type: 'string', description: 'Result limit' },
folder: { type: 'string', description: 'Mail folder' },
thread_id: { type: 'string', description: 'Mail thread ID' },
sort_by: { type: 'string', description: 'Field to sort by' },
sort_direction: { type: 'string', description: 'Sorting direction' },
cursor: { type: 'string', description: 'Pagination cursor' },
project_id: { type: 'string', description: 'Project ID' },
description: { type: 'string', description: 'Description' },
start_date: { type: 'string', description: 'Start date' },
end_date: { type: 'string', description: 'End date' },
activity_id: { type: 'string', description: 'Activity ID' },
subject: { type: 'string', description: 'Activity subject' },
type: { type: 'string', description: 'Activity type' },
due_date: { type: 'string', description: 'Due date' },
due_time: { type: 'string', description: 'Due time' },
duration: { type: 'string', description: 'Duration' },
done: { type: 'string', description: 'Completion status' },
note: { type: 'string', description: 'Notes' },
lead_id: { type: 'string', description: 'Lead ID' },
archived: { type: 'string', description: 'Archived status' },
value_amount: { type: 'string', description: 'Value amount' },
value_currency: { type: 'string', description: 'Value currency' },
is_archived: { type: 'string', description: 'Archive status' },
},
outputs: {
deals: { type: 'json', description: 'Array of deal objects' },
deal: { type: 'json', description: 'Single deal object' },
files: { type: 'json', description: 'Array of file objects' },
messages: { type: 'json', description: 'Array of mail message objects' },
pipelines: { type: 'json', description: 'Array of pipeline objects' },
projects: { type: 'json', description: 'Array of project objects' },
project: { type: 'json', description: 'Single project object' },
activities: { type: 'json', description: 'Array of activity objects' },
activity: { type: 'json', description: 'Single activity object' },
leads: { type: 'json', description: 'Array of lead objects' },
lead: { type: 'json', description: 'Single lead object' },
metadata: { type: 'json', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -0,0 +1,498 @@
import { SalesforceIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { SalesforceResponse } from '@/tools/salesforce/types'
export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
type: 'salesforce',
name: 'Salesforce',
description: 'Interact with Salesforce CRM',
authMode: AuthMode.OAuth,
longDescription:
'Integrate Salesforce into your workflow. Manage accounts, contacts, leads, opportunities, cases, and tasks with powerful automation capabilities.',
docsLink: 'https://docs.sim.ai/tools/salesforce',
category: 'tools',
bgColor: '#E0E0E0',
icon: SalesforceIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get Accounts', id: 'get_accounts' },
{ label: 'Create Account', id: 'create_account' },
{ label: 'Update Account', id: 'update_account' },
{ label: 'Delete Account', id: 'delete_account' },
{ label: 'Get Contacts', id: 'get_contacts' },
{ label: 'Create Contact', id: 'create_contact' },
{ label: 'Update Contact', id: 'update_contact' },
{ label: 'Delete Contact', id: 'delete_contact' },
{ label: 'Get Leads', id: 'get_leads' },
{ label: 'Create Lead', id: 'create_lead' },
{ label: 'Update Lead', id: 'update_lead' },
{ label: 'Delete Lead', id: 'delete_lead' },
{ label: 'Get Opportunities', id: 'get_opportunities' },
{ label: 'Create Opportunity', id: 'create_opportunity' },
{ label: 'Update Opportunity', id: 'update_opportunity' },
{ label: 'Delete Opportunity', id: 'delete_opportunity' },
{ label: 'Get Cases', id: 'get_cases' },
{ label: 'Create Case', id: 'create_case' },
{ label: 'Update Case', id: 'update_case' },
{ label: 'Delete Case', id: 'delete_case' },
{ label: 'Get Tasks', id: 'get_tasks' },
{ label: 'Create Task', id: 'create_task' },
{ label: 'Update Task', id: 'update_task' },
{ label: 'Delete Task', id: 'delete_task' },
],
value: () => 'get_accounts',
},
{
id: 'credential',
title: 'Salesforce Account',
type: 'oauth-input',
provider: 'salesforce',
serviceId: 'salesforce',
requiredScopes: ['api', 'refresh_token', 'openid'],
placeholder: 'Select Salesforce account',
required: true,
},
// Common fields for GET operations
{
id: 'fields',
title: 'Fields to Return',
type: 'short-input',
placeholder: 'Comma-separated fields',
condition: {
field: 'operation',
value: [
'get_accounts',
'get_contacts',
'get_leads',
'get_opportunities',
'get_cases',
'get_tasks',
],
},
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Max results (default: 100)',
condition: {
field: 'operation',
value: [
'get_accounts',
'get_contacts',
'get_leads',
'get_opportunities',
'get_cases',
'get_tasks',
],
},
},
{
id: 'orderBy',
title: 'Order By',
type: 'short-input',
placeholder: 'Field and direction (e.g., "Name ASC")',
condition: {
field: 'operation',
value: [
'get_accounts',
'get_contacts',
'get_leads',
'get_opportunities',
'get_cases',
'get_tasks',
],
},
},
// Account fields
{
id: 'accountId',
title: 'Account ID',
type: 'short-input',
placeholder: 'Salesforce Account ID',
condition: {
field: 'operation',
value: [
'update_account',
'delete_account',
'create_contact',
'update_contact',
'create_case',
],
},
},
{
id: 'name',
title: 'Name',
type: 'short-input',
placeholder: 'Name',
condition: {
field: 'operation',
value: ['create_account', 'update_account', 'create_opportunity', 'update_opportunity'],
},
},
{
id: 'type',
title: 'Type',
type: 'short-input',
placeholder: 'Type',
condition: { field: 'operation', value: ['create_account', 'update_account'] },
},
{
id: 'industry',
title: 'Industry',
type: 'short-input',
placeholder: 'Industry',
condition: { field: 'operation', value: ['create_account', 'update_account'] },
},
{
id: 'phone',
title: 'Phone',
type: 'short-input',
placeholder: 'Phone',
condition: {
field: 'operation',
value: [
'create_account',
'update_account',
'create_contact',
'update_contact',
'create_lead',
'update_lead',
],
},
},
{
id: 'website',
title: 'Website',
type: 'short-input',
placeholder: 'Website',
condition: { field: 'operation', value: ['create_account', 'update_account'] },
},
// Contact fields
{
id: 'contactId',
title: 'Contact ID',
type: 'short-input',
placeholder: 'Contact ID',
condition: {
field: 'operation',
value: ['get_contacts', 'update_contact', 'delete_contact', 'create_case'],
},
},
{
id: 'lastName',
title: 'Last Name',
type: 'short-input',
placeholder: 'Last name',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact', 'create_lead', 'update_lead'],
},
},
{
id: 'firstName',
title: 'First Name',
type: 'short-input',
placeholder: 'First name',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact', 'create_lead', 'update_lead'],
},
},
{
id: 'email',
title: 'Email',
type: 'short-input',
placeholder: 'Email',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact', 'create_lead', 'update_lead'],
},
},
{
id: 'title',
title: 'Job Title',
type: 'short-input',
placeholder: 'Job title',
condition: {
field: 'operation',
value: ['create_contact', 'update_contact', 'create_lead', 'update_lead'],
},
},
// Lead fields
{
id: 'leadId',
title: 'Lead ID',
type: 'short-input',
placeholder: 'Lead ID',
condition: { field: 'operation', value: ['get_leads', 'update_lead', 'delete_lead'] },
},
{
id: 'company',
title: 'Company',
type: 'short-input',
placeholder: 'Company name',
condition: { field: 'operation', value: ['create_lead', 'update_lead'] },
},
{
id: 'status',
title: 'Status',
type: 'short-input',
placeholder: 'Status',
condition: {
field: 'operation',
value: [
'create_lead',
'update_lead',
'create_case',
'update_case',
'create_task',
'update_task',
],
},
},
{
id: 'leadSource',
title: 'Lead Source',
type: 'short-input',
placeholder: 'Lead source',
condition: { field: 'operation', value: ['create_lead', 'update_lead'] },
},
// Opportunity fields
{
id: 'opportunityId',
title: 'Opportunity ID',
type: 'short-input',
placeholder: 'Opportunity ID',
condition: {
field: 'operation',
value: ['get_opportunities', 'update_opportunity', 'delete_opportunity'],
},
},
{
id: 'stageName',
title: 'Stage Name',
type: 'short-input',
placeholder: 'Stage name',
condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] },
},
{
id: 'closeDate',
title: 'Close Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD (required for create)',
condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] },
required: true,
},
{
id: 'amount',
title: 'Amount',
type: 'short-input',
placeholder: 'Deal amount',
condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] },
},
{
id: 'probability',
title: 'Probability',
type: 'short-input',
placeholder: 'Win probability (0-100)',
condition: { field: 'operation', value: ['create_opportunity', 'update_opportunity'] },
},
// Case fields
{
id: 'caseId',
title: 'Case ID',
type: 'short-input',
placeholder: 'Case ID',
condition: { field: 'operation', value: ['get_cases', 'update_case', 'delete_case'] },
},
{
id: 'subject',
title: 'Subject',
type: 'short-input',
placeholder: 'Subject',
condition: {
field: 'operation',
value: ['create_case', 'update_case', 'create_task', 'update_task'],
},
},
{
id: 'priority',
title: 'Priority',
type: 'short-input',
placeholder: 'Priority',
condition: {
field: 'operation',
value: ['create_case', 'update_case', 'create_task', 'update_task'],
},
},
{
id: 'origin',
title: 'Origin',
type: 'short-input',
placeholder: 'Origin (e.g., Phone, Email, Web)',
condition: { field: 'operation', value: ['create_case'] },
},
// Task fields
{
id: 'taskId',
title: 'Task ID',
type: 'short-input',
placeholder: 'Task ID',
condition: { field: 'operation', value: ['get_tasks', 'update_task', 'delete_task'] },
},
{
id: 'activityDate',
title: 'Due Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD',
condition: { field: 'operation', value: ['create_task', 'update_task'] },
},
{
id: 'whoId',
title: 'Related Contact/Lead ID',
type: 'short-input',
placeholder: 'Contact or Lead ID',
condition: { field: 'operation', value: ['create_task'] },
},
{
id: 'whatId',
title: 'Related Account/Opportunity ID',
type: 'short-input',
placeholder: 'Account or Opportunity ID',
condition: { field: 'operation', value: ['create_task'] },
},
// Long-input fields at the bottom
{
id: 'description',
title: 'Description',
type: 'long-input',
placeholder: 'Description',
condition: {
field: 'operation',
value: [
'create_account',
'update_account',
'create_contact',
'update_contact',
'create_lead',
'update_lead',
'create_opportunity',
'update_opportunity',
'create_case',
'update_case',
'create_task',
'update_task',
],
},
},
],
tools: {
access: [
'salesforce_get_accounts',
'salesforce_create_account',
'salesforce_update_account',
'salesforce_delete_account',
'salesforce_get_contacts',
'salesforce_create_contact',
'salesforce_update_contact',
'salesforce_delete_contact',
'salesforce_get_leads',
'salesforce_create_lead',
'salesforce_update_lead',
'salesforce_delete_lead',
'salesforce_get_opportunities',
'salesforce_create_opportunity',
'salesforce_update_opportunity',
'salesforce_delete_opportunity',
'salesforce_get_cases',
'salesforce_create_case',
'salesforce_update_case',
'salesforce_delete_case',
'salesforce_get_tasks',
'salesforce_create_task',
'salesforce_update_task',
'salesforce_delete_task',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'get_accounts':
return 'salesforce_get_accounts'
case 'create_account':
return 'salesforce_create_account'
case 'update_account':
return 'salesforce_update_account'
case 'delete_account':
return 'salesforce_delete_account'
case 'get_contacts':
return 'salesforce_get_contacts'
case 'create_contact':
return 'salesforce_create_contact'
case 'update_contact':
return 'salesforce_update_contact'
case 'delete_contact':
return 'salesforce_delete_contact'
case 'get_leads':
return 'salesforce_get_leads'
case 'create_lead':
return 'salesforce_create_lead'
case 'update_lead':
return 'salesforce_update_lead'
case 'delete_lead':
return 'salesforce_delete_lead'
case 'get_opportunities':
return 'salesforce_get_opportunities'
case 'create_opportunity':
return 'salesforce_create_opportunity'
case 'update_opportunity':
return 'salesforce_update_opportunity'
case 'delete_opportunity':
return 'salesforce_delete_opportunity'
case 'get_cases':
return 'salesforce_get_cases'
case 'create_case':
return 'salesforce_create_case'
case 'update_case':
return 'salesforce_update_case'
case 'delete_case':
return 'salesforce_delete_case'
case 'get_tasks':
return 'salesforce_get_tasks'
case 'create_task':
return 'salesforce_create_task'
case 'update_task':
return 'salesforce_update_task'
case 'delete_task':
return 'salesforce_delete_task'
default:
throw new Error(`Unknown operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, operation, ...rest } = params
const cleanParams: Record<string, any> = { credential }
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: 'Salesforce credential' },
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: { type: 'json', description: 'Operation result data' },
},
}

View File

@@ -43,10 +43,9 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
'openid',
'profile',
'email',
'Files.Read',
'Files.ReadWrite',
'Sites.Read.All',
'Sites.ReadWrite.All',
'Sites.Manage.All',
'offline_access',
],
placeholder: 'Select Microsoft account',

View File

@@ -59,7 +59,9 @@ export const SlackBlock: BlockConfig<SlackResponse> = {
'chat:write.public',
'users:read',
'files:write',
'files:read',
'canvases:write',
'reactions:write',
],
placeholder: 'Select Slack workspace',
condition: {

View File

@@ -0,0 +1,415 @@
import { TrelloIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types'
/**
* Trello Block
*
* Note: Trello uses OAuth 1.0a authentication with a unique credential ID format
* (non-UUID strings like CUID2). This is different from most OAuth 2.0 providers
* that use UUID-based credential IDs. The OAuth credentials API has been updated
* to accept both UUID and non-UUID credential ID formats to support Trello.
*/
export const TrelloBlock: BlockConfig<ToolResponse> = {
type: 'trello',
name: 'Trello',
description: 'Manage Trello boards and cards',
authMode: AuthMode.OAuth,
longDescription:
'Integrate with Trello to manage boards and cards. List boards, list cards, create cards, update cards, get actions, and add comments.',
docsLink: 'https://docs.sim.ai/tools/trello',
category: 'tools',
bgColor: '#0052CC',
icon: TrelloIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Get Lists', id: 'trello_list_lists' },
{ label: 'List Cards', id: 'trello_list_cards' },
{ label: 'Create Card', id: 'trello_create_card' },
{ label: 'Update Card', id: 'trello_update_card' },
{ label: 'Get Actions', id: 'trello_get_actions' },
{ label: 'Add Comment', id: 'trello_add_comment' },
],
value: () => 'trello_list_lists',
},
{
id: 'credential',
title: 'Trello Account',
type: 'oauth-input',
provider: 'trello',
serviceId: 'trello',
requiredScopes: ['read', 'write'],
placeholder: 'Select Trello account',
required: true,
},
{
id: 'boardId',
title: 'Board',
type: 'short-input',
placeholder: 'Enter board ID',
condition: {
field: 'operation',
value: 'trello_list_lists',
},
required: true,
},
{
id: 'boardId',
title: 'Board',
type: 'short-input',
placeholder: 'Enter board ID or search for a board',
condition: {
field: 'operation',
value: 'trello_list_cards',
},
required: true,
},
{
id: 'listId',
title: 'List (Optional)',
type: 'short-input',
placeholder: 'Enter list ID to filter cards by list',
condition: {
field: 'operation',
value: 'trello_list_cards',
},
},
{
id: 'boardId',
title: 'Board',
type: 'short-input',
placeholder: 'Enter board ID or search for a board',
condition: {
field: 'operation',
value: 'trello_create_card',
},
required: true,
},
{
id: 'listId',
title: 'List',
type: 'short-input',
placeholder: 'Enter list ID or search for a list',
condition: {
field: 'operation',
value: 'trello_create_card',
},
required: true,
},
{
id: 'name',
title: 'Card Name',
type: 'short-input',
placeholder: 'Enter card name/title',
condition: {
field: 'operation',
value: 'trello_create_card',
},
required: true,
},
{
id: 'desc',
title: 'Description',
type: 'long-input',
placeholder: 'Enter card description (optional)',
condition: {
field: 'operation',
value: 'trello_create_card',
},
},
{
id: 'pos',
title: 'Position',
type: 'dropdown',
options: [
{ label: 'Top', id: 'top' },
{ label: 'Bottom', id: 'bottom' },
],
condition: {
field: 'operation',
value: 'trello_create_card',
},
},
{
id: 'due',
title: 'Due Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD or ISO 8601',
condition: {
field: 'operation',
value: 'trello_create_card',
},
},
{
id: 'labels',
title: 'Labels',
type: 'short-input',
placeholder: 'Comma-separated label IDs (optional)',
condition: {
field: 'operation',
value: 'trello_create_card',
},
},
{
id: 'cardId',
title: 'Card',
type: 'short-input',
placeholder: 'Enter card ID or search for a card',
condition: {
field: 'operation',
value: 'trello_update_card',
},
required: true,
},
{
id: 'name',
title: 'New Card Name',
type: 'short-input',
placeholder: 'Enter new card name (leave empty to keep current)',
condition: {
field: 'operation',
value: 'trello_update_card',
},
},
{
id: 'desc',
title: 'New Description',
type: 'long-input',
placeholder: 'Enter new description (leave empty to keep current)',
condition: {
field: 'operation',
value: 'trello_update_card',
},
},
{
id: 'closed',
title: 'Archive Card',
type: 'switch',
condition: {
field: 'operation',
value: 'trello_update_card',
},
},
{
id: 'dueComplete',
title: 'Mark Due Date Complete',
type: 'switch',
condition: {
field: 'operation',
value: 'trello_update_card',
},
},
{
id: 'idList',
title: 'Move to List',
type: 'short-input',
placeholder: 'Enter list ID to move card',
condition: {
field: 'operation',
value: 'trello_update_card',
},
},
{
id: 'due',
title: 'Due Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD or ISO 8601',
condition: {
field: 'operation',
value: 'trello_update_card',
},
},
{
id: 'boardId',
title: 'Board ID',
type: 'short-input',
placeholder: 'Enter board ID to get board actions',
condition: {
field: 'operation',
value: 'trello_get_actions',
},
},
{
id: 'cardId',
title: 'Card ID',
type: 'short-input',
placeholder: 'Enter card ID to get card actions',
condition: {
field: 'operation',
value: 'trello_get_actions',
},
},
{
id: 'filter',
title: 'Action Filter',
type: 'short-input',
placeholder: 'e.g., commentCard,updateCard',
condition: {
field: 'operation',
value: 'trello_get_actions',
},
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: '50',
condition: {
field: 'operation',
value: 'trello_get_actions',
},
},
{
id: 'cardId',
title: 'Card',
type: 'short-input',
placeholder: 'Enter card ID or search for a card',
condition: {
field: 'operation',
value: 'trello_add_comment',
},
required: true,
},
{
id: 'text',
title: 'Comment',
type: 'long-input',
placeholder: 'Enter your comment',
condition: {
field: 'operation',
value: 'trello_add_comment',
},
required: true,
},
],
tools: {
access: [
'trello_list_lists',
'trello_list_cards',
'trello_create_card',
'trello_update_card',
'trello_get_actions',
'trello_add_comment',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'trello_list_lists':
return 'trello_list_lists'
case 'trello_list_cards':
return 'trello_list_cards'
case 'trello_create_card':
return 'trello_create_card'
case 'trello_update_card':
return 'trello_update_card'
case 'trello_get_actions':
return 'trello_get_actions'
case 'trello_add_comment':
return 'trello_add_comment'
default:
return 'trello_list_lists'
}
},
params: (params) => {
const { operation, limit, closed, dueComplete, ...rest } = params
const result: Record<string, any> = { ...rest }
if (limit && operation === 'trello_get_actions') {
result.limit = Number.parseInt(limit, 10)
}
if (closed !== undefined && operation === 'trello_update_card') {
if (typeof closed === 'string') {
result.closed = closed.toLowerCase() === 'true' || closed === '1'
} else if (typeof closed === 'number') {
result.closed = closed !== 0
} else {
result.closed = Boolean(closed)
}
}
if (dueComplete !== undefined && operation === 'trello_update_card') {
if (typeof dueComplete === 'string') {
result.dueComplete = dueComplete.toLowerCase() === 'true' || dueComplete === '1'
} else if (typeof dueComplete === 'number') {
result.dueComplete = dueComplete !== 0
} else {
result.dueComplete = Boolean(dueComplete)
}
}
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Trello operation to perform' },
credential: { type: 'string', description: 'Trello OAuth credential' },
boardId: { type: 'string', description: 'Board ID' },
listId: { type: 'string', description: 'List ID' },
cardId: { type: 'string', description: 'Card ID' },
name: { type: 'string', description: 'Card name/title' },
desc: { type: 'string', description: 'Card or board description' },
pos: { type: 'string', description: 'Card position (top, bottom, or number)' },
due: { type: 'string', description: 'Due date in ISO 8601 format' },
labels: { type: 'string', description: 'Comma-separated label IDs' },
closed: { type: 'boolean', description: 'Archive/close status' },
idList: { type: 'string', description: 'ID of list to move card to' },
dueComplete: { type: 'boolean', description: 'Mark due date as complete' },
filter: { type: 'string', description: 'Action type filter' },
limit: { type: 'number', description: 'Maximum number of results' },
text: { type: 'string', description: 'Comment text' },
},
outputs: {
success: { type: 'boolean', description: 'Whether the operation was successful' },
lists: {
type: 'array',
description: 'Array of list objects (for list_lists operation)',
},
cards: {
type: 'array',
description: 'Array of card objects (for list_cards operation)',
},
card: {
type: 'json',
description: 'Card object (for create_card and update_card operations)',
},
actions: {
type: 'array',
description: 'Array of action objects (for get_actions operation)',
},
comment: {
type: 'json',
description: 'Comment object (for add_comment operation)',
},
count: {
type: 'number',
description: 'Number of items returned (boards, cards, actions)',
},
error: {
type: 'string',
description: 'Error message if operation failed',
},
},
}

View File

@@ -33,7 +33,7 @@ export const XBlock: BlockConfig<XResponse> = {
type: 'oauth-input',
provider: 'x',
serviceId: 'x',
requiredScopes: ['tweet.read', 'tweet.write', 'users.read'],
requiredScopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
placeholder: 'Select X account',
},
{

View File

@@ -3,6 +3,7 @@ import { AirtableBlock } from '@/blocks/blocks/airtable'
import { ApiBlock } from '@/blocks/blocks/api'
import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger'
import { ArxivBlock } from '@/blocks/blocks/arxiv'
import { AsanaBlock } from '@/blocks/blocks/asana'
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
import { ClayBlock } from '@/blocks/blocks/clay'
@@ -26,6 +27,7 @@ import { GoogleFormsBlock } from '@/blocks/blocks/google_form'
import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets'
import { GoogleVaultBlock } from '@/blocks/blocks/google_vault'
import { GuardrailsBlock } from '@/blocks/blocks/guardrails'
import { HubSpotBlock } from '@/blocks/blocks/hubspot'
import { HuggingFaceBlock } from '@/blocks/blocks/huggingface'
import { HunterBlock } from '@/blocks/blocks/hunter'
import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator'
@@ -53,6 +55,7 @@ import { ParallelBlock } from '@/blocks/blocks/parallel'
import { PauseResumeBlock } from '@/blocks/blocks/pause_resume'
import { PerplexityBlock } from '@/blocks/blocks/perplexity'
import { PineconeBlock } from '@/blocks/blocks/pinecone'
import { PipedriveBlock } from '@/blocks/blocks/pipedrive'
import { PostgreSQLBlock } from '@/blocks/blocks/postgresql'
import { QdrantBlock } from '@/blocks/blocks/qdrant'
import { RedditBlock } from '@/blocks/blocks/reddit'
@@ -60,6 +63,7 @@ import { ResendBlock } from '@/blocks/blocks/resend'
import { ResponseBlock } from '@/blocks/blocks/response'
import { RouterBlock } from '@/blocks/blocks/router'
import { S3Block } from '@/blocks/blocks/s3'
import { SalesforceBlock } from '@/blocks/blocks/salesforce'
import { ScheduleBlock } from '@/blocks/blocks/schedule'
import { SerperBlock } from '@/blocks/blocks/serper'
import { SharepointBlock } from '@/blocks/blocks/sharepoint'
@@ -74,6 +78,7 @@ import { TavilyBlock } from '@/blocks/blocks/tavily'
import { TelegramBlock } from '@/blocks/blocks/telegram'
import { ThinkingBlock } from '@/blocks/blocks/thinking'
import { TranslateBlock } from '@/blocks/blocks/translate'
import { TrelloBlock } from '@/blocks/blocks/trello'
import { TwilioSMSBlock } from '@/blocks/blocks/twilio'
import { TwilioVoiceBlock } from '@/blocks/blocks/twilio_voice'
import { TypeformBlock } from '@/blocks/blocks/typeform'
@@ -99,6 +104,7 @@ export const registry: Record<string, BlockConfig> = {
api: ApiBlock,
approval: PauseResumeBlock,
arxiv: ArxivBlock,
asana: AsanaBlock,
browser_use: BrowserUseBlock,
clay: ClayBlock,
condition: ConditionBlock,
@@ -121,6 +127,7 @@ export const registry: Record<string, BlockConfig> = {
google_search: GoogleSearchBlock,
google_sheets: GoogleSheetsBlock,
google_vault: GoogleVaultBlock,
hubspot: HubSpotBlock,
huggingface: HuggingFaceBlock,
hunter: HunterBlock,
image_generator: ImageGeneratorBlock,
@@ -145,6 +152,7 @@ export const registry: Record<string, BlockConfig> = {
parallel_ai: ParallelBlock,
perplexity: PerplexityBlock,
pinecone: PineconeBlock,
pipedrive: PipedriveBlock,
postgresql: PostgreSQLBlock,
qdrant: QdrantBlock,
resend: ResendBlock,
@@ -154,6 +162,7 @@ export const registry: Record<string, BlockConfig> = {
router: RouterBlock,
schedule: ScheduleBlock,
s3: S3Block,
salesforce: SalesforceBlock,
serper: SerperBlock,
sharepoint: SharepointBlock,
// sms: SMSBlock,
@@ -172,6 +181,7 @@ export const registry: Record<string, BlockConfig> = {
telegram: TelegramBlock,
thinking: ThinkingBlock,
translate: TranslateBlock,
trello: TrelloBlock,
twilio_sms: TwilioSMSBlock,
twilio_voice: TwilioVoiceBlock,
typeform: TypeformBlock,

File diff suppressed because one or more lines are too long

View File

@@ -153,6 +153,9 @@ export const auth = betterAuth({
'slack',
'reddit',
'webflow',
'asana',
'pipedrive',
'hubspot',
// Common SSO provider patterns
...SSO_TRUSTED_PROVIDERS,
@@ -661,6 +664,208 @@ export const auth = betterAuth({
},
},
{
providerId: 'pipedrive',
clientId: env.PIPEDRIVE_CLIENT_ID as string,
clientSecret: env.PIPEDRIVE_CLIENT_SECRET as string,
authorizationUrl: 'https://oauth.pipedrive.com/oauth/authorize',
tokenUrl: 'https://oauth.pipedrive.com/oauth/token',
userInfoUrl: 'https://api.pipedrive.com/v1/users/me',
prompt: 'consent',
scopes: [
'base',
'deals:full',
'contacts:full',
'leads:full',
'activities:full',
'mail:full',
'projects:full',
],
responseType: 'code',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/pipedrive`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching Pipedrive user profile')
const response = await fetch('https://api.pipedrive.com/v1/users/me', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
logger.error('Failed to fetch Pipedrive user info', {
status: response.status,
})
throw new Error('Failed to fetch user info')
}
const data = await response.json()
const user = data.data
return {
id: user.id.toString(),
name: user.name,
email: user.email,
emailVerified: user.activated,
image: user.icon_url,
createdAt: new Date(),
updatedAt: new Date(),
}
} catch (error) {
logger.error('Error creating Pipedrive user profile:', { error })
return null
}
},
},
// HubSpot provider
{
providerId: 'hubspot',
clientId: env.HUBSPOT_CLIENT_ID as string,
clientSecret: env.HUBSPOT_CLIENT_SECRET as string,
authorizationUrl: 'https://app.hubspot.com/oauth/authorize',
tokenUrl: 'https://api.hubapi.com/oauth/v1/token',
userInfoUrl: 'https://api.hubapi.com/oauth/v1/access-tokens',
prompt: 'consent',
scopes: [
'crm.objects.contacts.read',
'crm.objects.contacts.write',
'crm.objects.companies.read',
'crm.objects.companies.write',
'crm.objects.deals.read',
'crm.objects.deals.write',
'crm.objects.owners.read',
'crm.objects.users.read',
'crm.objects.users.write',
'crm.objects.marketing_events.read',
'crm.objects.marketing_events.write',
'crm.objects.line_items.read',
'crm.objects.line_items.write',
'crm.objects.quotes.read',
'crm.objects.quotes.write',
'crm.objects.appointments.read',
'crm.objects.appointments.write',
'crm.objects.carts.read',
'crm.objects.carts.write',
'crm.import',
'crm.lists.read',
'crm.lists.write',
'tickets',
],
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/hubspot`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching HubSpot user profile')
const response = await fetch(
`https://api.hubapi.com/oauth/v1/access-tokens/${tokens.accessToken}`
)
if (!response.ok) {
let errorBody: string | undefined
try {
errorBody = await response.text()
} catch {
// ignore
}
logger.error('Failed to fetch HubSpot user info', {
status: response.status,
statusText: response.statusText,
body: errorBody?.slice(0, 500),
})
throw new Error('Failed to fetch user info')
}
const rawText = await response.text()
const data = JSON.parse(rawText)
const scopesArray = Array.isArray((data as any)?.scopes) ? (data as any).scopes : []
if (Array.isArray(scopesArray) && scopesArray.length > 0) {
tokens.scopes = scopesArray
} else if (typeof (data as any)?.scope === 'string') {
tokens.scopes = (data as any).scope.split(/\s+/).filter(Boolean)
}
logger.info('HubSpot token metadata response:', {
hasScopes: !!data.scopes,
scopesType: typeof data.scopes,
scopesIsArray: Array.isArray(data.scopes),
scopesValue: data.scopes,
fullResponse: data,
})
return {
id: data.user_id || data.hub_id.toString(),
name: data.user || 'HubSpot User',
email: data.user || `hubspot-${data.hub_id}@hubspot.com`,
emailVerified: true,
image: undefined,
createdAt: new Date(),
updatedAt: new Date(),
// Extract scopes from HubSpot's response and convert array to space-delimited string
// Use 'scope' (singular) as that's what better-auth expects for the account table
...(data.scopes && Array.isArray(data.scopes)
? { scope: data.scopes.join(' ') }
: {}),
}
} catch (error) {
logger.error('Error creating HubSpot user profile:', { error })
return null
}
},
},
// Salesforce provider
{
providerId: 'salesforce',
clientId: env.SALESFORCE_CLIENT_ID as string,
clientSecret: env.SALESFORCE_CLIENT_SECRET as string,
authorizationUrl: 'https://login.salesforce.com/services/oauth2/authorize',
tokenUrl: 'https://login.salesforce.com/services/oauth2/token',
userInfoUrl: 'https://login.salesforce.com/services/oauth2/userinfo',
scopes: ['api', 'refresh_token', 'openid'],
pkce: true,
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/salesforce`,
getUserInfo: async (tokens) => {
try {
logger.info('Fetching Salesforce user profile')
const response = await fetch(
'https://login.salesforce.com/services/oauth2/userinfo',
{
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
}
)
if (!response.ok) {
logger.error('Failed to fetch Salesforce user info', {
status: response.status,
})
throw new Error('Failed to fetch user info')
}
const data = await response.json()
return {
id: data.user_id || data.sub,
name: data.name || 'Salesforce User',
email: data.email || `salesforce-${data.user_id}@salesforce.com`,
emailVerified: data.email_verified || true,
image: data.picture || undefined,
createdAt: new Date(),
updatedAt: new Date(),
}
} catch (error) {
logger.error('Error creating Salesforce user profile:', { error })
return null
}
},
},
// Supabase provider
{
providerId: 'supabase',
@@ -1216,6 +1421,57 @@ export const auth = betterAuth({
},
},
{
providerId: 'asana',
clientId: env.ASANA_CLIENT_ID as string,
clientSecret: env.ASANA_CLIENT_SECRET as string,
authorizationUrl: 'https://app.asana.com/-/oauth_authorize',
tokenUrl: 'https://app.asana.com/-/oauth_token',
userInfoUrl: 'https://app.asana.com/api/1.0/users/me',
scopes: ['default'],
responseType: 'code',
pkce: false,
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/asana`,
getUserInfo: async (tokens) => {
try {
const response = await fetch('https://app.asana.com/api/1.0/users/me', {
headers: {
Authorization: `Bearer ${tokens.accessToken}`,
},
})
if (!response.ok) {
logger.error('Error fetching Asana user info:', {
status: response.status,
statusText: response.statusText,
})
return null
}
const result = await response.json()
const profile = result.data
const now = new Date()
return {
id: profile.gid,
name: profile.name || 'Asana User',
email: profile.email || `${profile.gid}@asana.user`,
image: profile.photo?.image_128x128 || undefined,
emailVerified: !!profile.email,
createdAt: now,
updatedAt: now,
}
} catch (error) {
logger.error('Error in Asana getUserInfo:', { error })
return null
}
},
},
// Slack provider
{
providerId: 'slack',

View File

@@ -181,6 +181,8 @@ export const env = createEnv({
CONFLUENCE_CLIENT_SECRET: z.string().optional(), // Atlassian Confluence OAuth client secret
JIRA_CLIENT_ID: z.string().optional(), // Atlassian Jira OAuth client ID
JIRA_CLIENT_SECRET: z.string().optional(), // Atlassian Jira OAuth client secret
ASANA_CLIENT_ID: z.string().optional(), // Asana OAuth client ID
ASANA_CLIENT_SECRET: z.string().optional(), // Asana OAuth client secret
AIRTABLE_CLIENT_ID: z.string().optional(), // Airtable OAuth client ID
AIRTABLE_CLIENT_SECRET: z.string().optional(), // Airtable OAuth client secret
SUPABASE_CLIENT_ID: z.string().optional(), // Supabase OAuth client ID
@@ -193,8 +195,12 @@ export const env = createEnv({
MICROSOFT_CLIENT_SECRET: z.string().optional(), // Microsoft OAuth client secret
HUBSPOT_CLIENT_ID: z.string().optional(), // HubSpot OAuth client ID
HUBSPOT_CLIENT_SECRET: z.string().optional(), // HubSpot OAuth client secret
SALESFORCE_CLIENT_ID: z.string().optional(), // Salesforce OAuth client ID
SALESFORCE_CLIENT_SECRET: z.string().optional(), // Salesforce OAuth client secret
WEALTHBOX_CLIENT_ID: z.string().optional(), // WealthBox OAuth client ID
WEALTHBOX_CLIENT_SECRET: z.string().optional(), // WealthBox OAuth client secret
PIPEDRIVE_CLIENT_ID: z.string().optional(), // Pipedrive OAuth client ID
PIPEDRIVE_CLIENT_SECRET: z.string().optional(), // Pipedrive OAuth client secret
LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID
LINEAR_CLIENT_SECRET: z.string().optional(), // Linear OAuth client secret
SLACK_CLIENT_ID: z.string().optional(), // Slack OAuth client ID
@@ -203,6 +209,7 @@ export const env = createEnv({
REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret
WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID
WEBFLOW_CLIENT_SECRET: z.string().optional(), // Webflow OAuth client secret
TRELLO_API_KEY: z.string().optional(), // Trello API Key
// E2B Remote Code Execution
E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution
@@ -249,7 +256,7 @@ export const env = createEnv({
client: {
// Core Application URLs - Required for frontend functionality
NEXT_PUBLIC_APP_URL: z.string().url(), // Base URL of the application (e.g., https://app.sim.ai)
NEXT_PUBLIC_APP_URL: z.string().url(), // Base URL of the application (e.g., https://www.sim.ai)
// Client-side Services
NEXT_PUBLIC_SOCKET_URL: z.string().url().optional(), // WebSocket server URL for real-time features

View File

@@ -1,6 +1,7 @@
import type { ReactNode } from 'react'
import {
AirtableIcon,
AsanaIcon,
ConfluenceIcon,
DiscordIcon,
GithubIcon,
@@ -11,6 +12,7 @@ import {
GoogleFormsIcon,
GoogleIcon,
GoogleSheetsIcon,
HubspotIcon,
JiraIcon,
LinearIcon,
MicrosoftExcelIcon,
@@ -21,9 +23,12 @@ import {
MicrosoftTeamsIcon,
NotionIcon,
OutlookIcon,
PipedriveIcon,
RedditIcon,
SalesforceIcon,
SlackIcon,
SupabaseIcon,
TrelloIcon,
WealthboxIcon,
WebflowIcon,
xIcon,
@@ -47,8 +52,13 @@ export type OAuthProvider =
| 'linear'
| 'slack'
| 'reddit'
| 'trello'
| 'wealthbox'
| 'webflow'
| 'asana'
| 'pipedrive'
| 'hubspot'
| 'salesforce'
| string
export type OAuthService =
@@ -79,7 +89,11 @@ export type OAuthService =
| 'wealthbox'
| 'onedrive'
| 'webflow'
| 'trello'
| 'asana'
| 'pipedrive'
| 'hubspot'
| 'salesforce'
export interface OAuthProviderConfig {
id: OAuthProvider
name: string
@@ -244,17 +258,19 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'Chat.Read',
'Chat.ReadWrite',
'Chat.ReadBasic',
'ChatMessage.Send',
'Channel.ReadBasic.All',
'ChannelMessage.Send',
'ChannelMessage.Read.All',
'ChannelMessage.ReadWrite',
'ChannelMember.Read.All',
'Group.Read.All',
'Group.ReadWrite.All',
'Team.ReadBasic.All',
'TeamMember.Read.All',
'offline_access',
'Files.Read',
'Sites.Read.All',
'TeamMember.Read.All',
],
},
outlook: {
@@ -383,12 +399,10 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'delete:attachment:confluence',
'read:content:confluence',
'delete:page:confluence',
'write:label:confluence',
'read:label:confluence',
'write:label:confluence',
'read:attachment:confluence',
'write:attachment:confluence',
'read:label:confluence',
'write:label:confluence',
'search:confluence',
'read:me',
'offline_access',
@@ -465,7 +479,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
providerId: 'airtable',
icon: (props) => AirtableIcon(props),
baseProviderIcon: (props) => AirtableIcon(props),
scopes: ['data.records:read', 'data.records:write'],
scopes: ['data.records:read', 'data.records:write', 'user.email:read', 'webhook:manage'],
},
},
defaultService: 'airtable',
@@ -618,6 +632,123 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
},
defaultService: 'webflow',
},
trello: {
id: 'trello',
name: 'Trello',
icon: (props) => TrelloIcon(props),
services: {
trello: {
id: 'trello',
name: 'Trello',
description: 'Manage Trello boards, cards, and workflows.',
providerId: 'trello',
icon: (props) => TrelloIcon(props),
baseProviderIcon: (props) => TrelloIcon(props),
scopes: ['read', 'write'],
},
},
defaultService: 'trello',
},
asana: {
id: 'asana',
name: 'Asana',
icon: (props) => AsanaIcon(props),
services: {
asana: {
id: 'asana',
name: 'Asana',
description: 'Manage Asana projects, tasks, and workflows.',
providerId: 'asana',
icon: (props) => AsanaIcon(props),
baseProviderIcon: (props) => AsanaIcon(props),
scopes: ['default'],
},
},
defaultService: 'asana',
},
pipedrive: {
id: 'pipedrive',
name: 'Pipedrive',
icon: (props) => PipedriveIcon(props),
services: {
pipedrive: {
id: 'pipedrive',
name: 'Pipedrive',
description: 'Manage deals, contacts, and sales pipeline in Pipedrive CRM.',
providerId: 'pipedrive',
icon: (props) => PipedriveIcon(props),
baseProviderIcon: (props) => PipedriveIcon(props),
scopes: [
'base',
'deals:full',
'contacts:full',
'leads:full',
'activities:full',
'mail:full',
'projects:full',
],
},
},
defaultService: 'pipedrive',
},
hubspot: {
id: 'hubspot',
name: 'HubSpot',
icon: (props) => HubspotIcon(props),
services: {
hubspot: {
id: 'hubspot',
name: 'HubSpot',
description: 'Access and manage your HubSpot CRM data.',
providerId: 'hubspot',
icon: (props) => HubspotIcon(props),
baseProviderIcon: (props) => HubspotIcon(props),
scopes: [
'crm.objects.contacts.read',
'crm.objects.contacts.write',
'crm.objects.companies.read',
'crm.objects.companies.write',
'crm.objects.deals.read',
'crm.objects.deals.write',
'crm.objects.owners.read',
'crm.objects.users.read',
'crm.objects.users.write',
'crm.objects.marketing_events.read',
'crm.objects.marketing_events.write',
'crm.objects.line_items.read',
'crm.objects.line_items.write',
'crm.objects.quotes.read',
'crm.objects.quotes.write',
'crm.objects.appointments.read',
'crm.objects.appointments.write',
'crm.objects.carts.read',
'crm.objects.carts.write',
'crm.import',
'crm.lists.read',
'crm.lists.write',
'tickets',
],
},
},
defaultService: 'hubspot',
},
salesforce: {
id: 'salesforce',
name: 'Salesforce',
icon: (props) => SalesforceIcon(props),
services: {
salesforce: {
id: 'salesforce',
name: 'Salesforce',
description: 'Access and manage your Salesforce CRM data.',
providerId: 'salesforce',
icon: (props) => SalesforceIcon(props),
baseProviderIcon: (props) => SalesforceIcon(props),
scopes: ['api', 'refresh_token', 'openid'],
},
},
defaultService: 'salesforce',
},
}
export function getServiceByProviderAndId(
@@ -1031,6 +1162,58 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
supportsRefreshTokenRotation: false,
}
}
case 'asana': {
const { clientId, clientSecret } = getCredentials(
env.ASANA_CLIENT_ID,
env.ASANA_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://app.asana.com/-/oauth_token',
clientId,
clientSecret,
useBasicAuth: true,
supportsRefreshTokenRotation: true,
}
}
case 'pipedrive': {
const { clientId, clientSecret } = getCredentials(
env.PIPEDRIVE_CLIENT_ID,
env.PIPEDRIVE_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://oauth.pipedrive.com/oauth/token',
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: true,
}
}
case 'hubspot': {
const { clientId, clientSecret } = getCredentials(
env.HUBSPOT_CLIENT_ID,
env.HUBSPOT_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://api.hubapi.com/oauth/v1/token',
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: true,
}
}
case 'salesforce': {
const { clientId, clientSecret } = getCredentials(
env.SALESFORCE_CLIENT_ID,
env.SALESFORCE_CLIENT_SECRET
)
return {
tokenEndpoint: 'https://login.salesforce.com/services/oauth2/token',
clientId,
clientSecret,
useBasicAuth: false,
supportsRefreshTokenRotation: false,
}
}
default:
throw new Error(`Unsupported provider: ${provider}`)
}

View File

@@ -738,6 +738,47 @@ export async function queueWebhookExecution(
}
}
if (foundWebhook.provider === 'hubspot') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined
if (triggerId?.startsWith('hubspot_')) {
const events = Array.isArray(body) ? body : [body]
const firstEvent = events[0]
const subscriptionType = firstEvent?.subscriptionType as string | undefined
const { isHubSpotContactEventMatch } = await import('@/triggers/hubspot/utils')
if (!isHubSpotContactEventMatch(triggerId, subscriptionType || '')) {
logger.debug(
`[${options.requestId}] HubSpot event mismatch for trigger ${triggerId}. Event: ${subscriptionType}. Skipping execution.`,
{
webhookId: foundWebhook.id,
workflowId: foundWorkflow.id,
triggerId,
receivedEvent: subscriptionType,
}
)
// Return 200 OK to prevent HubSpot from retrying
return NextResponse.json({
message: 'Event type does not match trigger configuration. Ignoring.',
})
}
logger.info(
`[${options.requestId}] HubSpot event match confirmed for trigger ${triggerId}. Event: ${subscriptionType}`,
{
webhookId: foundWebhook.id,
workflowId: foundWorkflow.id,
triggerId,
receivedEvent: subscriptionType,
}
)
}
}
const headers = Object.fromEntries(request.headers.entries())
// For Microsoft Teams Graph notifications, extract unique identifiers for idempotency

View File

@@ -783,18 +783,41 @@ export async function formatWebhookInput(
if (foundWebhook.provider === 'gmail') {
if (body && typeof body === 'object' && 'email' in body) {
return body // { email: {...}, timestamp: ... }
return body
}
return body
}
if (foundWebhook.provider === 'outlook') {
if (body && typeof body === 'object' && 'email' in body) {
return body // { email: {...}, timestamp: ... }
return body
}
return body
}
if (foundWebhook.provider === 'hubspot') {
const events = Array.isArray(body) ? body : [body]
const event = events[0]
if (!event) {
logger.warn('HubSpot webhook received with empty payload')
return null
}
logger.info('Formatting HubSpot webhook input', {
subscriptionType: event.subscriptionType,
objectId: event.objectId,
portalId: event.portalId,
})
return {
payload: body,
provider: 'hubspot',
providerConfig: foundWebhook.providerConfig,
workflowId: foundWorkflow.id,
}
}
if (foundWebhook.provider === 'microsoftteams') {
// Check if this is a Microsoft Graph change notification
if (body?.value && Array.isArray(body.value) && body.value.length > 0) {

View File

@@ -23,7 +23,7 @@ const createMockHeaders = (customHeaders: Record<string, string> = {}) => {
'Accept-Encoding': 'gzip, deflate, br',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Referer: 'https://app.simstudio.dev',
Referer: 'https://www.simstudio.dev',
'Sec-Ch-Ua': 'Chromium;v=91, Not-A.Brand;v=99',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"macOS"',

View File

@@ -0,0 +1,83 @@
import type { AsanaAddCommentParams, AsanaAddCommentResponse } from '@/tools/asana/types'
import type { ToolConfig } from '@/tools/types'
export const asanaAddCommentTool: ToolConfig<AsanaAddCommentParams, AsanaAddCommentResponse> = {
id: 'asana_add_comment',
name: 'Asana Add Comment',
description: 'Add a comment (story) to an Asana task',
version: '1.0.0',
oauth: {
required: true,
provider: 'asana',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Asana',
},
taskGid: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The globally unique identifier (GID) of the task',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text content of the comment',
},
},
request: {
url: '/api/tools/asana/add-comment',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
taskGid: params.taskGid,
text: params.text,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
error: 'Empty response from Asana',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || null,
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description: 'Comment details including gid, text, created timestamp, and author',
},
},
}

View File

@@ -0,0 +1,120 @@
import type { AsanaCreateTaskParams, AsanaCreateTaskResponse } from '@/tools/asana/types'
import type { ToolConfig } from '@/tools/types'
export const asanaCreateTaskTool: ToolConfig<AsanaCreateTaskParams, AsanaCreateTaskResponse> = {
id: 'asana_create_task',
name: 'Asana Create Task',
description: 'Create a new task in Asana',
version: '1.0.0',
oauth: {
required: true,
provider: 'asana',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Asana',
},
workspace: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Workspace GID where the task will be created',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the task',
},
notes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Notes or description for the task',
},
assignee: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'User GID to assign the task to',
},
due_on: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Due date in YYYY-MM-DD format',
},
},
request: {
url: '/api/tools/asana/create-task',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
workspace: params.workspace,
name: params.name,
notes: params.notes,
assignee: params.assignee,
due_on: params.due_on,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: true,
output: {
ts: new Date().toISOString(),
gid: 'unknown',
name: 'Task created successfully',
notes: '',
completed: false,
created_at: new Date().toISOString(),
permalink_url: '',
},
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
gid: 'unknown',
name: 'Task creation failed',
notes: '',
completed: false,
created_at: new Date().toISOString(),
permalink_url: '',
},
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description: 'Created task details with timestamp, gid, name, notes, and permalink',
},
},
}

View File

@@ -0,0 +1,76 @@
import type { AsanaGetProjectsParams, AsanaGetProjectsResponse } from '@/tools/asana/types'
import type { ToolConfig } from '@/tools/types'
export const asanaGetProjectsTool: ToolConfig<AsanaGetProjectsParams, AsanaGetProjectsResponse> = {
id: 'asana_get_projects',
name: 'Asana Get Projects',
description: 'Retrieve all projects from an Asana workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'asana',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Asana',
},
workspace: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Workspace GID to retrieve projects from',
},
},
request: {
url: '/api/tools/asana/get-projects',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
workspace: params.workspace,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
error: 'Empty response from Asana',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), projects: [] },
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description: 'List of projects with their gid, name, and resource type',
},
},
}

View File

@@ -0,0 +1,99 @@
import type { AsanaGetTaskParams, AsanaGetTaskResponse } from '@/tools/asana/types'
import type { ToolConfig } from '@/tools/types'
export const asanaGetTaskTool: ToolConfig<AsanaGetTaskParams, AsanaGetTaskResponse> = {
id: 'asana_get_task',
name: 'Asana Get Task',
description: 'Retrieve a single task by GID or get multiple tasks with filters',
version: '1.0.0',
oauth: {
required: true,
provider: 'asana',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Asana',
},
taskGid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The globally unique identifier (GID) of the task. If not provided, will get multiple tasks.',
},
workspace: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Workspace GID to filter tasks (required when not using taskGid)',
},
project: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Project GID to filter tasks',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of tasks to return (default: 50)',
},
},
request: {
url: '/api/tools/asana/get-task',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
taskGid: params.taskGid,
workspace: params.workspace,
project: params.project,
limit: params.limit,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
error: 'Empty response from Asana',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || null,
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description:
'Single task details or array of tasks, depending on whether taskGid was provided',
},
},
}

View File

@@ -0,0 +1,13 @@
import { asanaAddCommentTool } from '@/tools/asana/add_comment'
import { asanaCreateTaskTool } from '@/tools/asana/create_task'
import { asanaGetProjectsTool } from '@/tools/asana/get_projects'
import { asanaGetTaskTool } from '@/tools/asana/get_task'
import { asanaSearchTasksTool } from '@/tools/asana/search_tasks'
import { asanaUpdateTaskTool } from '@/tools/asana/update_task'
export { asanaGetTaskTool }
export { asanaCreateTaskTool }
export { asanaUpdateTaskTool }
export { asanaGetProjectsTool }
export { asanaSearchTasksTool }
export { asanaAddCommentTool }

View File

@@ -0,0 +1,104 @@
import type { AsanaSearchTasksParams, AsanaSearchTasksResponse } from '@/tools/asana/types'
import type { ToolConfig } from '@/tools/types'
export const asanaSearchTasksTool: ToolConfig<AsanaSearchTasksParams, AsanaSearchTasksResponse> = {
id: 'asana_search_tasks',
name: 'Asana Search Tasks',
description: 'Search for tasks in an Asana workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'asana',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Asana',
},
workspace: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Workspace GID to search tasks in',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Text to search for in task names',
},
assignee: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter tasks by assignee user GID',
},
projects: {
type: 'array',
required: false,
visibility: 'user-only',
description: 'Array of project GIDs to filter tasks by',
},
completed: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Filter by completion status',
},
},
request: {
url: '/api/tools/asana/search-tasks',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
workspace: params.workspace,
text: params.text,
assignee: params.assignee,
projects: params.projects,
completed: params.completed,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
error: 'Empty response from Asana',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), tasks: [] },
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description: 'List of tasks matching the search criteria',
},
},
}

View File

@@ -0,0 +1,214 @@
import type { ToolResponse } from '@/tools/types'
export interface AsanaGetTaskParams {
accessToken: string
taskGid?: string
workspace?: string
project?: string
limit?: number
}
export interface AsanaGetTaskResponse extends ToolResponse {
output:
| {
ts: string
gid: string
resource_type: string
resource_subtype: string
name: string
notes: string
completed: boolean
assignee?: {
gid: string
name: string
}
created_by?: {
gid: string
resource_type: string
name: string
}
due_on?: string
created_at: string
modified_at: string
}
| {
ts: string
tasks: Array<{
gid: string
resource_type: string
resource_subtype: string
name: string
notes?: string
completed: boolean
assignee?: {
gid: string
name: string
}
created_by?: {
gid: string
resource_type: string
name: string
}
due_on?: string
created_at: string
modified_at: string
}>
next_page?: {
offset: string
path: string
uri: string
}
}
}
export interface AsanaCreateTaskParams {
accessToken: string
workspace: string
name: string
notes?: string
assignee?: string
due_on?: string
}
export interface AsanaCreateTaskResponse extends ToolResponse {
output: {
ts: string
gid: string
name: string
notes: string
completed: boolean
created_at: string
permalink_url: string
}
}
export interface AsanaUpdateTaskParams {
accessToken: string
taskGid: string
name?: string
notes?: string
assignee?: string
completed?: boolean
due_on?: string
}
export interface AsanaUpdateTaskResponse extends ToolResponse {
output: {
ts: string
gid: string
name: string
notes: string
completed: boolean
modified_at: string
}
}
export interface AsanaGetProjectsParams {
accessToken: string
workspace: string
}
export interface AsanaGetProjectsResponse extends ToolResponse {
output: {
ts: string
projects: Array<{
gid: string
name: string
resource_type: string
}>
}
}
export interface AsanaSearchTasksParams {
accessToken: string
workspace: string
text?: string
assignee?: string
projects?: string[]
completed?: boolean
}
export interface AsanaSearchTasksResponse extends ToolResponse {
output: {
ts: string
tasks: Array<{
gid: string
resource_type: string
resource_subtype: string
name: string
notes?: string
completed: boolean
assignee?: {
gid: string
name: string
}
created_by?: {
gid: string
resource_type: string
name: string
}
due_on?: string
created_at: string
modified_at: string
}>
next_page?: {
offset: string
path: string
uri: string
}
}
}
export interface AsanaTask {
gid: string
resource_type: string
resource_subtype: string
name: string
notes?: string
completed: boolean
assignee?: {
gid: string
name: string
}
created_by?: {
gid: string
resource_type: string
name: string
}
due_on?: string
created_at: string
modified_at: string
}
export interface AsanaProject {
gid: string
name: string
resource_type: string
}
export interface AsanaAddCommentParams {
accessToken: string
taskGid: string
text: string
}
export interface AsanaAddCommentResponse extends ToolResponse {
output: {
ts: string
gid: string
text: string
created_at: string
created_by: {
gid: string
name: string
}
}
}
export type AsanaResponse =
| AsanaGetTaskResponse
| AsanaCreateTaskResponse
| AsanaUpdateTaskResponse
| AsanaGetProjectsResponse
| AsanaSearchTasksResponse
| AsanaAddCommentResponse

View File

@@ -0,0 +1,125 @@
import type { AsanaUpdateTaskParams, AsanaUpdateTaskResponse } from '@/tools/asana/types'
import type { ToolConfig } from '@/tools/types'
export const asanaUpdateTaskTool: ToolConfig<AsanaUpdateTaskParams, AsanaUpdateTaskResponse> = {
id: 'asana_update_task',
name: 'Asana Update Task',
description: 'Update an existing task in Asana',
version: '1.0.0',
oauth: {
required: true,
provider: 'asana',
additionalScopes: [],
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Asana',
},
taskGid: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The globally unique identifier (GID) of the task to update',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated name for the task',
},
notes: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated notes or description for the task',
},
assignee: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated assignee user GID',
},
completed: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Mark task as completed or not completed',
},
due_on: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Updated due date in YYYY-MM-DD format',
},
},
request: {
url: '/api/tools/asana/update-task',
method: 'PUT',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
accessToken: params.accessToken,
taskGid: params.taskGid,
name: params.name,
notes: params.notes,
assignee: params.assignee,
completed: params.completed,
due_on: params.due_on,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: true,
output: {
ts: new Date().toISOString(),
gid: 'unknown',
name: 'Task updated successfully',
notes: '',
completed: false,
modified_at: new Date().toISOString(),
},
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) {
return data
}
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
gid: 'unknown',
name: 'Task update failed',
notes: '',
completed: false,
modified_at: new Date().toISOString(),
},
error: data.error,
}
},
outputs: {
success: {
type: 'boolean',
description: 'Operation success status',
},
output: {
type: 'object',
description: 'Updated task details with timestamp, gid, name, notes, and modified timestamp',
},
},
}

View File

@@ -0,0 +1,110 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotCreateCompanyParams,
HubSpotCreateCompanyResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotCreateCompany')
export const hubspotCreateCompanyTool: ToolConfig<
HubSpotCreateCompanyParams,
HubSpotCreateCompanyResponse
> = {
id: 'hubspot_create_company',
name: 'Create Company in HubSpot',
description: 'Create a new company in HubSpot',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
properties: {
type: 'object',
required: true,
visibility: 'user-only',
description: 'Company properties as JSON object (e.g., name, domain, city, industry)',
},
associations: {
type: 'array',
required: false,
visibility: 'user-only',
description: 'Array of associations to create with the company',
},
},
request: {
url: () => 'https://api.hubapi.com/crm/v3/objects/companies',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: any = {
properties: params.properties,
}
if (params.associations && params.associations.length > 0) {
body.associations = params.associations
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create company in HubSpot')
}
return {
success: true,
output: {
company: data,
metadata: {
operation: 'create_company' as const,
companyId: data.id,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created company data',
properties: {
company: {
type: 'object',
description: 'Created company object with properties and ID',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,113 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotCreateContactParams,
HubSpotCreateContactResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotCreateContact')
export const hubspotCreateContactTool: ToolConfig<
HubSpotCreateContactParams,
HubSpotCreateContactResponse
> = {
id: 'hubspot_create_contact',
name: 'Create Contact in HubSpot',
description:
'Create a new contact in HubSpot. Requires at least one of: email, firstname, or lastname',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
properties: {
type: 'object',
required: true,
visibility: 'user-only',
description:
'Contact properties as JSON object. Must include at least one of: email, firstname, or lastname',
},
associations: {
type: 'array',
required: false,
visibility: 'user-only',
description:
'Array of associations to create with the contact (e.g., companies, deals). Each object should have "to" (with "id") and "types" (with "associationCategory" and "associationTypeId")',
},
},
request: {
url: () => 'https://api.hubapi.com/crm/v3/objects/contacts',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: any = {
properties: params.properties,
}
if (params.associations && params.associations.length > 0) {
body.associations = params.associations
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to create contact in HubSpot')
}
return {
success: true,
output: {
contact: data,
metadata: {
operation: 'create_contact' as const,
contactId: data.id,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created contact data',
properties: {
contact: {
type: 'object',
description: 'Created contact object with properties and ID',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,123 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { HubSpotGetCompanyParams, HubSpotGetCompanyResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotGetCompany')
export const hubspotGetCompanyTool: ToolConfig<HubSpotGetCompanyParams, HubSpotGetCompanyResponse> =
{
id: 'hubspot_get_company',
name: 'Get Company from HubSpot',
description: 'Retrieve a single company by ID or domain from HubSpot',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
companyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID or domain of the company to retrieve',
},
idProperty: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Property to use as unique identifier (e.g., "domain"). If not specified, uses record ID',
},
properties: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of properties to return',
},
associations: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of object types to retrieve associated IDs for',
},
},
request: {
url: (params) => {
const baseUrl = `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId}`
const queryParams = new URLSearchParams()
if (params.idProperty) {
queryParams.append('idProperty', params.idProperty)
}
if (params.properties) {
queryParams.append('properties', params.properties)
}
if (params.associations) {
queryParams.append('associations', params.associations)
}
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get company from HubSpot')
}
return {
success: true,
output: {
company: data,
metadata: {
operation: 'get_company' as const,
companyId: data.id,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Company data',
properties: {
company: {
type: 'object',
description: 'Company object with properties',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,123 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { HubSpotGetContactParams, HubSpotGetContactResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotGetContact')
export const hubspotGetContactTool: ToolConfig<HubSpotGetContactParams, HubSpotGetContactResponse> =
{
id: 'hubspot_get_contact',
name: 'Get Contact from HubSpot',
description: 'Retrieve a single contact by ID or email from HubSpot',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
contactId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID or email of the contact to retrieve',
},
idProperty: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Property to use as unique identifier (e.g., "email"). If not specified, uses record ID',
},
properties: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of properties to return',
},
associations: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of object types to retrieve associated IDs for',
},
},
request: {
url: (params) => {
const baseUrl = `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId}`
const queryParams = new URLSearchParams()
if (params.idProperty) {
queryParams.append('idProperty', params.idProperty)
}
if (params.properties) {
queryParams.append('properties', params.properties)
}
if (params.associations) {
queryParams.append('associations', params.associations)
}
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to get contact from HubSpot')
}
return {
success: true,
output: {
contact: data,
metadata: {
operation: 'get_contact' as const,
contactId: data.id,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Contact data',
properties: {
contact: {
type: 'object',
description: 'Contact object with properties',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,99 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { HubSpotGetUsersParams, HubSpotGetUsersResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotGetUsers')
export const hubspotGetUsersTool: ToolConfig<HubSpotGetUsersParams, HubSpotGetUsersResponse> = {
id: 'hubspot_get_users',
name: 'Get Users from HubSpot',
description: 'Retrieve all users from HubSpot account',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100)',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.hubapi.com/crm/v3/objects/users'
const queryParams = new URLSearchParams()
if (params.limit) {
queryParams.append('limit', params.limit)
}
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to fetch users from HubSpot')
}
const users = data.results || []
return {
success: true,
output: {
users,
metadata: {
operation: 'get_users' as const,
totalItems: users.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Users data',
properties: {
users: {
type: 'array',
description: 'Array of user objects',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,12 @@
export { hubspotCreateCompanyTool } from './create_company'
export { hubspotCreateContactTool } from './create_contact'
export { hubspotGetCompanyTool } from './get_company'
export { hubspotGetContactTool } from './get_contact'
export { hubspotGetUsersTool } from './get_users'
export { hubspotListCompaniesTool } from './list_companies'
export { hubspotListContactsTool } from './list_contacts'
export { hubspotListDealsTool } from './list_deals'
export { hubspotSearchCompaniesTool } from './search_companies'
export { hubspotSearchContactsTool } from './search_contacts'
export { hubspotUpdateCompanyTool } from './update_company'
export { hubspotUpdateContactTool } from './update_contact'

View File

@@ -0,0 +1,136 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotListCompaniesParams,
HubSpotListCompaniesResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotListCompanies')
export const hubspotListCompaniesTool: ToolConfig<
HubSpotListCompaniesParams,
HubSpotListCompaniesResponse
> = {
id: 'hubspot_list_companies',
name: 'List Companies from HubSpot',
description: 'Retrieve all companies from HubSpot account with pagination support',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Maximum number of results per page (max 100, default 100)',
},
after: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Pagination cursor for next page of results',
},
properties: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of properties to return',
},
associations: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of object types to retrieve associated IDs for',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.hubapi.com/crm/v3/objects/companies'
const queryParams = new URLSearchParams()
if (params.limit) {
queryParams.append('limit', params.limit)
}
if (params.after) {
queryParams.append('after', params.after)
}
if (params.properties) {
queryParams.append('properties', params.properties)
}
if (params.associations) {
queryParams.append('associations', params.associations)
}
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list companies from HubSpot')
}
return {
success: true,
output: {
companies: data.results || [],
paging: data.paging,
metadata: {
operation: 'list_companies' as const,
totalReturned: data.results?.length || 0,
hasMore: !!data.paging?.next,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Companies data',
properties: {
companies: {
type: 'array',
description: 'Array of company objects',
},
paging: {
type: 'object',
description: 'Pagination information',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,134 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { HubSpotListContactsParams, HubSpotListContactsResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotListContacts')
export const hubspotListContactsTool: ToolConfig<
HubSpotListContactsParams,
HubSpotListContactsResponse
> = {
id: 'hubspot_list_contacts',
name: 'List Contacts from HubSpot',
description: 'Retrieve all contacts from HubSpot account with pagination support',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Maximum number of results per page (max 100, default 100)',
},
after: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Pagination cursor for next page of results',
},
properties: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Comma-separated list of properties to return (e.g., "email,firstname,lastname")',
},
associations: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of object types to retrieve associated IDs for',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.hubapi.com/crm/v3/objects/contacts'
const queryParams = new URLSearchParams()
if (params.limit) {
queryParams.append('limit', params.limit)
}
if (params.after) {
queryParams.append('after', params.after)
}
if (params.properties) {
queryParams.append('properties', params.properties)
}
if (params.associations) {
queryParams.append('associations', params.associations)
}
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list contacts from HubSpot')
}
return {
success: true,
output: {
contacts: data.results || [],
paging: data.paging,
metadata: {
operation: 'list_contacts' as const,
totalReturned: data.results?.length || 0,
hasMore: !!data.paging?.next,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Contacts data',
properties: {
contacts: {
type: 'array',
description: 'Array of contact objects',
},
paging: {
type: 'object',
description: 'Pagination information',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,130 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { HubSpotListDealsParams, HubSpotListDealsResponse } from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotListDeals')
export const hubspotListDealsTool: ToolConfig<HubSpotListDealsParams, HubSpotListDealsResponse> = {
id: 'hubspot_list_deals',
name: 'List Deals from HubSpot',
description: 'Retrieve all deals from HubSpot account with pagination support',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Maximum number of results per page (max 100, default 100)',
},
after: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Pagination cursor for next page of results',
},
properties: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of properties to return',
},
associations: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of object types to retrieve associated IDs for',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.hubapi.com/crm/v3/objects/deals'
const queryParams = new URLSearchParams()
if (params.limit) {
queryParams.append('limit', params.limit)
}
if (params.after) {
queryParams.append('after', params.after)
}
if (params.properties) {
queryParams.append('properties', params.properties)
}
if (params.associations) {
queryParams.append('associations', params.associations)
}
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to list deals from HubSpot')
}
return {
success: true,
output: {
deals: data.results || [],
paging: data.paging,
metadata: {
operation: 'list_deals' as const,
totalReturned: data.results?.length || 0,
hasMore: !!data.paging?.next,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deals data',
properties: {
deals: {
type: 'array',
description: 'Array of deal objects',
},
paging: {
type: 'object',
description: 'Pagination information',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,160 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotSearchCompaniesParams,
HubSpotSearchCompaniesResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotSearchCompanies')
export const hubspotSearchCompaniesTool: ToolConfig<
HubSpotSearchCompaniesParams,
HubSpotSearchCompaniesResponse
> = {
id: 'hubspot_search_companies',
name: 'Search Companies in HubSpot',
description: 'Search for companies in HubSpot using filters, sorting, and queries',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
filterGroups: {
type: 'array',
required: false,
visibility: 'user-only',
description:
'Array of filter groups. Each group contains filters with propertyName, operator, and value',
},
sorts: {
type: 'array',
required: false,
visibility: 'user-only',
description:
'Array of sort objects with propertyName and direction ("ASCENDING" or "DESCENDING")',
},
query: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Search query string',
},
properties: {
type: 'array',
required: false,
visibility: 'user-only',
description: 'Array of property names to return',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of results to return (max 100)',
},
after: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Pagination cursor for next page',
},
},
request: {
url: () => 'https://api.hubapi.com/crm/v3/objects/companies/search',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: any = {}
if (params.filterGroups && params.filterGroups.length > 0) {
body.filterGroups = params.filterGroups
}
if (params.sorts && params.sorts.length > 0) {
body.sorts = params.sorts
}
if (params.query) {
body.query = params.query
}
if (params.properties && params.properties.length > 0) {
body.properties = params.properties
}
if (params.limit) {
body.limit = params.limit
}
if (params.after) {
body.after = params.after
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to search companies in HubSpot')
}
return {
success: true,
output: {
companies: data.results || [],
total: data.total,
paging: data.paging,
metadata: {
operation: 'search_companies' as const,
totalReturned: data.results?.length || 0,
total: data.total,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Search results',
properties: {
companies: {
type: 'array',
description: 'Array of matching company objects',
},
total: {
type: 'number',
description: 'Total number of matching companies',
},
paging: {
type: 'object',
description: 'Pagination information',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,160 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotSearchContactsParams,
HubSpotSearchContactsResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotSearchContacts')
export const hubspotSearchContactsTool: ToolConfig<
HubSpotSearchContactsParams,
HubSpotSearchContactsResponse
> = {
id: 'hubspot_search_contacts',
name: 'Search Contacts in HubSpot',
description: 'Search for contacts in HubSpot using filters, sorting, and queries',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
filterGroups: {
type: 'array',
required: false,
visibility: 'user-only',
description:
'Array of filter groups. Each group contains filters with propertyName, operator, and value',
},
sorts: {
type: 'array',
required: false,
visibility: 'user-only',
description:
'Array of sort objects with propertyName and direction ("ASCENDING" or "DESCENDING")',
},
query: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Search query string',
},
properties: {
type: 'array',
required: false,
visibility: 'user-only',
description: 'Array of property names to return',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of results to return (max 100)',
},
after: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Pagination cursor for next page',
},
},
request: {
url: () => 'https://api.hubapi.com/crm/v3/objects/contacts/search',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: any = {}
if (params.filterGroups && params.filterGroups.length > 0) {
body.filterGroups = params.filterGroups
}
if (params.sorts && params.sorts.length > 0) {
body.sorts = params.sorts
}
if (params.query) {
body.query = params.query
}
if (params.properties && params.properties.length > 0) {
body.properties = params.properties
}
if (params.limit) {
body.limit = params.limit
}
if (params.after) {
body.after = params.after
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to search contacts in HubSpot')
}
return {
success: true,
output: {
contacts: data.results || [],
total: data.total,
paging: data.paging,
metadata: {
operation: 'search_contacts' as const,
totalReturned: data.results?.length || 0,
total: data.total,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Search results',
properties: {
contacts: {
type: 'array',
description: 'Array of matching contact objects',
},
total: {
type: 'number',
description: 'Total number of matching contacts',
},
paging: {
type: 'object',
description: 'Pagination information',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,258 @@
import type { ToolResponse } from '@/tools/types'
// Common HubSpot types
export interface HubSpotUser {
id: string
email: string
firstName?: string
lastName?: string
roleId?: string
primaryTeamId?: string
superAdmin?: boolean
}
export interface HubSpotContact {
id: string
properties: Record<string, any>
createdAt: string
updatedAt: string
archived: boolean
associations?: Record<string, any>
}
export interface HubSpotPaging {
next?: {
after: string
link?: string
}
}
// Users
export interface HubSpotGetUsersResponse extends ToolResponse {
output: {
users: HubSpotUser[]
metadata: {
operation: 'get_users'
totalItems?: number
}
success: boolean
}
}
export interface HubSpotGetUsersParams {
accessToken: string
limit?: string
}
// List Contacts
export interface HubSpotListContactsResponse extends ToolResponse {
output: {
contacts: HubSpotContact[]
paging?: HubSpotPaging
metadata: {
operation: 'list_contacts'
totalReturned: number
hasMore: boolean
}
success: boolean
}
}
export interface HubSpotListContactsParams {
accessToken: string
limit?: string
after?: string
properties?: string
associations?: string
}
// Get Contact
export interface HubSpotGetContactResponse extends ToolResponse {
output: {
contact: HubSpotContact
metadata: {
operation: 'get_contact'
contactId: string
}
success: boolean
}
}
export interface HubSpotGetContactParams {
accessToken: string
contactId: string
idProperty?: string
properties?: string
associations?: string
}
// Create Contact
export interface HubSpotCreateContactResponse extends ToolResponse {
output: {
contact: HubSpotContact
metadata: {
operation: 'create_contact'
contactId: string
}
success: boolean
}
}
export interface HubSpotCreateContactParams {
accessToken: string
properties: Record<string, any>
associations?: Array<{
to: { id: string }
types: Array<{
associationCategory: string
associationTypeId: number
}>
}>
}
// Update Contact
export interface HubSpotUpdateContactResponse extends ToolResponse {
output: {
contact: HubSpotContact
metadata: {
operation: 'update_contact'
contactId: string
}
success: boolean
}
}
export interface HubSpotUpdateContactParams {
accessToken: string
contactId: string
idProperty?: string
properties: Record<string, any>
}
// Search Contacts
export interface HubSpotSearchContactsResponse extends ToolResponse {
output: {
contacts: HubSpotContact[]
total: number
paging?: HubSpotPaging
metadata: {
operation: 'search_contacts'
totalReturned: number
total: number
}
success: boolean
}
}
export interface HubSpotSearchContactsParams {
accessToken: string
filterGroups?: Array<{
filters: Array<{
propertyName: string
operator: string
value: string
}>
}>
sorts?: Array<{
propertyName: string
direction: 'ASCENDING' | 'DESCENDING'
}>
query?: string
properties?: string[]
limit?: number
after?: string
}
// Companies (same structure as contacts)
export type HubSpotCompany = HubSpotContact
export type HubSpotListCompaniesParams = HubSpotListContactsParams
export type HubSpotListCompaniesResponse = Omit<HubSpotListContactsResponse, 'output'> & {
output: {
companies: HubSpotContact[]
paging?: HubSpotPaging
metadata: {
operation: 'list_companies'
totalReturned: number
hasMore: boolean
}
success: boolean
}
}
export type HubSpotGetCompanyParams = HubSpotGetContactParams & { companyId: string }
export type HubSpotGetCompanyResponse = Omit<HubSpotGetContactResponse, 'output'> & {
output: {
company: HubSpotContact
metadata: {
operation: 'get_company'
companyId: string
}
success: boolean
}
}
export type HubSpotCreateCompanyParams = HubSpotCreateContactParams
export type HubSpotCreateCompanyResponse = Omit<HubSpotCreateContactResponse, 'output'> & {
output: {
company: HubSpotContact
metadata: {
operation: 'create_company'
companyId: string
}
success: boolean
}
}
export type HubSpotUpdateCompanyParams = HubSpotUpdateContactParams & { companyId: string }
export type HubSpotUpdateCompanyResponse = Omit<HubSpotUpdateContactResponse, 'output'> & {
output: {
company: HubSpotContact
metadata: {
operation: 'update_company'
companyId: string
}
success: boolean
}
}
export type HubSpotSearchCompaniesParams = HubSpotSearchContactsParams
export type HubSpotSearchCompaniesResponse = Omit<HubSpotSearchContactsResponse, 'output'> & {
output: {
companies: HubSpotContact[]
total: number
paging?: HubSpotPaging
metadata: {
operation: 'search_companies'
totalReturned: number
total: number
}
success: boolean
}
}
// Deals (same structure as contacts)
export type HubSpotDeal = HubSpotContact
export type HubSpotListDealsParams = HubSpotListContactsParams
export type HubSpotListDealsResponse = Omit<HubSpotListContactsResponse, 'output'> & {
output: {
deals: HubSpotContact[]
paging?: HubSpotPaging
metadata: {
operation: 'list_deals'
totalReturned: number
hasMore: boolean
}
success: boolean
}
}
// Generic HubSpot response type for the block
export type HubSpotResponse =
| HubSpotGetUsersResponse
| HubSpotListContactsResponse
| HubSpotGetContactResponse
| HubSpotCreateContactResponse
| HubSpotUpdateContactResponse
| HubSpotSearchContactsResponse
| HubSpotListCompaniesResponse
| HubSpotGetCompanyResponse
| HubSpotCreateCompanyResponse
| HubSpotUpdateCompanyResponse
| HubSpotSearchCompaniesResponse
| HubSpotListDealsResponse

View File

@@ -0,0 +1,117 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotUpdateCompanyParams,
HubSpotUpdateCompanyResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotUpdateCompany')
export const hubspotUpdateCompanyTool: ToolConfig<
HubSpotUpdateCompanyParams,
HubSpotUpdateCompanyResponse
> = {
id: 'hubspot_update_company',
name: 'Update Company in HubSpot',
description: 'Update an existing company in HubSpot by ID or domain',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
companyId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID or domain of the company to update',
},
idProperty: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Property to use as unique identifier (e.g., "domain"). If not specified, uses record ID',
},
properties: {
type: 'object',
required: true,
visibility: 'user-only',
description: 'Company properties to update as JSON object',
},
},
request: {
url: (params) => {
const baseUrl = `https://api.hubapi.com/crm/v3/objects/companies/${params.companyId}`
if (params.idProperty) {
return `${baseUrl}?idProperty=${params.idProperty}`
}
return baseUrl
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
return {
properties: params.properties,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update company in HubSpot')
}
return {
success: true,
output: {
company: data,
metadata: {
operation: 'update_company' as const,
companyId: data.id,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated company data',
properties: {
company: {
type: 'object',
description: 'Updated company object with properties',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,117 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
HubSpotUpdateContactParams,
HubSpotUpdateContactResponse,
} from '@/tools/hubspot/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('HubSpotUpdateContact')
export const hubspotUpdateContactTool: ToolConfig<
HubSpotUpdateContactParams,
HubSpotUpdateContactResponse
> = {
id: 'hubspot_update_contact',
name: 'Update Contact in HubSpot',
description: 'Update an existing contact in HubSpot by ID or email',
version: '1.0.0',
oauth: {
required: true,
provider: 'hubspot',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the HubSpot API',
},
contactId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID or email of the contact to update',
},
idProperty: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Property to use as unique identifier (e.g., "email"). If not specified, uses record ID',
},
properties: {
type: 'object',
required: true,
visibility: 'user-only',
description: 'Contact properties to update as JSON object',
},
},
request: {
url: (params) => {
const baseUrl = `https://api.hubapi.com/crm/v3/objects/contacts/${params.contactId}`
if (params.idProperty) {
return `${baseUrl}?idProperty=${params.idProperty}`
}
return baseUrl
},
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
return {
properties: params.properties,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('HubSpot API request failed', { data, status: response.status })
throw new Error(data.message || 'Failed to update contact in HubSpot')
}
return {
success: true,
output: {
contact: data,
metadata: {
operation: 'update_contact' as const,
contactId: data.id,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated contact data',
properties: {
contact: {
type: 'object',
description: 'Updated contact object with properties',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -193,6 +193,9 @@ export async function executeTool(
const data = await response.json()
contextParams.accessToken = data.accessToken
if (data.idToken) {
contextParams.idToken = data.idToken
}
logger.info(
`[${requestId}] Successfully got access token for ${toolId}, length: ${data.accessToken?.length || 0}`

View File

@@ -0,0 +1,152 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveCreateActivityParams,
PipedriveCreateActivityResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveCreateActivity')
export const pipedriveCreateActivityTool: ToolConfig<
PipedriveCreateActivityParams,
PipedriveCreateActivityResponse
> = {
id: 'pipedrive_create_activity',
name: 'Create Activity in Pipedrive',
description: 'Create a new activity (task) in Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
subject: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The subject/title of the activity',
},
type: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Activity type: call, meeting, task, deadline, email, lunch',
},
due_date: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Due date in YYYY-MM-DD format',
},
due_time: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Due time in HH:MM format',
},
duration: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Duration in HH:MM format',
},
deal_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the deal to associate with',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the person to associate with',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the organization to associate with',
},
note: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Notes for the activity',
},
},
request: {
url: () => 'https://api.pipedrive.com/v1/activities',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {
subject: params.subject,
type: params.type,
due_date: params.due_date,
}
if (params.due_time) body.due_time = params.due_time
if (params.duration) body.duration = params.duration
if (params.deal_id) body.deal_id = Number(params.deal_id)
if (params.person_id) body.person_id = Number(params.person_id)
if (params.org_id) body.org_id = Number(params.org_id)
if (params.note) body.note = params.note
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to create activity in Pipedrive')
}
return {
success: true,
output: {
activity: data.data,
metadata: {
operation: 'create_activity' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created activity details',
properties: {
activity: {
type: 'object',
description: 'The created activity object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,152 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveCreateDealParams,
PipedriveCreateDealResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveCreateDeal')
export const pipedriveCreateDealTool: ToolConfig<
PipedriveCreateDealParams,
PipedriveCreateDealResponse
> = {
id: 'pipedrive_create_deal',
name: 'Create Deal in Pipedrive',
description: 'Create a new deal in Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
title: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The title of the deal',
},
value: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'The monetary value of the deal',
},
currency: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Currency code (e.g., USD, EUR)',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the person this deal is associated with',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the organization this deal is associated with',
},
pipeline_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the pipeline this deal should be placed in',
},
stage_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the stage this deal should be placed in',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Status of the deal: open, won, lost',
},
expected_close_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Expected close date in YYYY-MM-DD format',
},
},
request: {
url: () => 'https://api.pipedrive.com/api/v2/deals',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {
title: params.title,
}
if (params.value) body.value = Number(params.value)
if (params.currency) body.currency = params.currency
if (params.person_id) body.person_id = Number(params.person_id)
if (params.org_id) body.org_id = Number(params.org_id)
if (params.pipeline_id) body.pipeline_id = Number(params.pipeline_id)
if (params.stage_id) body.stage_id = Number(params.stage_id)
if (params.status) body.status = params.status
if (params.expected_close_date) body.expected_close_date = params.expected_close_date
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to create deal in Pipedrive')
}
return {
success: true,
output: {
deal: data.data,
metadata: {
operation: 'create_deal' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created deal details',
properties: {
deal: {
type: 'object',
description: 'The created deal object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,161 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveCreateLeadParams,
PipedriveCreateLeadResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveCreateLead')
export const pipedriveCreateLeadTool: ToolConfig<
PipedriveCreateLeadParams,
PipedriveCreateLeadResponse
> = {
id: 'pipedrive_create_lead',
name: 'Create Lead in Pipedrive',
description: 'Create a new lead in Pipedrive',
version: '1.0.0',
oauth: {
required: true,
provider: 'pipedrive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
title: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The name of the lead',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the person (REQUIRED unless organization_id is provided)',
},
organization_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the organization (REQUIRED unless person_id is provided)',
},
owner_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ID of the user who will own the lead',
},
value_amount: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Potential value amount',
},
value_currency: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Currency code (e.g., USD, EUR)',
},
expected_close_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Expected close date in YYYY-MM-DD format',
},
visible_to: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Visibility: 1 (Owner & followers), 3 (Entire company)',
},
},
request: {
url: () => 'https://api.pipedrive.com/v1/leads',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
if (!params.person_id && !params.organization_id) {
throw new Error('Either person_id or organization_id is required to create a lead')
}
const body: Record<string, any> = {
title: params.title,
}
if (params.person_id) body.person_id = Number(params.person_id)
if (params.organization_id) body.organization_id = Number(params.organization_id)
if (params.owner_id) body.owner_id = Number(params.owner_id)
// Build value object if both amount and currency are provided
if (params.value_amount && params.value_currency) {
body.value = {
amount: Number(params.value_amount),
currency: params.value_currency,
}
}
if (params.expected_close_date) body.expected_close_date = params.expected_close_date
if (params.visible_to) body.visible_to = Number(params.visible_to)
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to create lead in Pipedrive')
}
return {
success: true,
output: {
lead: data.data,
metadata: {
operation: 'create_lead' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created lead details',
properties: {
lead: {
type: 'object',
description: 'The created lead object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,117 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveCreateProjectParams,
PipedriveCreateProjectResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveCreateProject')
export const pipedriveCreateProjectTool: ToolConfig<
PipedriveCreateProjectParams,
PipedriveCreateProjectResponse
> = {
id: 'pipedrive_create_project',
name: 'Create Project in Pipedrive',
description: 'Create a new project in Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
title: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The title of the project',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description of the project',
},
start_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Project start date in YYYY-MM-DD format',
},
end_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Project end date in YYYY-MM-DD format',
},
},
request: {
url: () => 'https://api.pipedrive.com/v1/projects',
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {
title: params.title,
}
if (params.description) body.description = params.description
if (params.start_date) body.start_date = params.start_date
if (params.end_date) body.end_date = params.end_date
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to create project in Pipedrive')
}
return {
success: true,
output: {
project: data.data,
metadata: {
operation: 'create_project' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created project details',
properties: {
project: {
type: 'object',
description: 'The created project object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,92 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveDeleteLeadParams,
PipedriveDeleteLeadResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveDeleteLead')
export const pipedriveDeleteLeadTool: ToolConfig<
PipedriveDeleteLeadParams,
PipedriveDeleteLeadResponse
> = {
id: 'pipedrive_delete_lead',
name: 'Delete Lead from Pipedrive',
description: 'Delete a specific lead from Pipedrive',
version: '1.0.0',
oauth: {
required: true,
provider: 'pipedrive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
lead_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the lead to delete',
},
},
request: {
url: (params) => `https://api.pipedrive.com/v1/leads/${params.lead_id}`,
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to delete lead from Pipedrive')
}
return {
success: true,
output: {
data: data.data,
metadata: {
operation: 'delete_lead' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deletion result',
properties: {
data: {
type: 'object',
description: 'Deletion confirmation data',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,133 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetActivitiesParams,
PipedriveGetActivitiesResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetActivities')
export const pipedriveGetActivitiesTool: ToolConfig<
PipedriveGetActivitiesParams,
PipedriveGetActivitiesResponse
> = {
id: 'pipedrive_get_activities',
name: 'Get Activities from Pipedrive',
description: 'Retrieve activities (tasks) from Pipedrive with optional filters',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
deal_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter activities by deal ID',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter activities by person ID',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter activities by organization ID',
},
type: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by activity type (call, meeting, task, deadline, email, lunch)',
},
done: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by completion status: 0 for not done, 1 for done',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500)',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.pipedrive.com/v1/activities'
const queryParams = new URLSearchParams()
if (params.deal_id) queryParams.append('deal_id', params.deal_id)
if (params.person_id) queryParams.append('person_id', params.person_id)
if (params.org_id) queryParams.append('org_id', params.org_id)
if (params.type) queryParams.append('type', params.type)
if (params.done) queryParams.append('done', params.done)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch activities from Pipedrive')
}
const activities = data.data || []
return {
success: true,
output: {
activities,
metadata: {
operation: 'get_activities' as const,
totalItems: activities.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Activities data',
properties: {
activities: {
type: 'array',
description: 'Array of activity objects from Pipedrive',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,161 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetAllDealsParams,
PipedriveGetAllDealsResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetAllDeals')
export const pipedriveGetAllDealsTool: ToolConfig<
PipedriveGetAllDealsParams,
PipedriveGetAllDealsResponse
> = {
id: 'pipedrive_get_all_deals',
name: 'Get All Deals from Pipedrive',
description: 'Retrieve all deals from Pipedrive with optional filters',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Only fetch deals with a specific status. Values: open, won, lost. If omitted, all not deleted deals are returned',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'If supplied, only deals linked to the specified person are returned',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'If supplied, only deals linked to the specified organization are returned',
},
pipeline_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'If supplied, only deals in the specified pipeline are returned',
},
updated_since: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'If set, only deals updated after this time are returned. Format: 2025-01-01T10:20:00Z',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500)',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.pipedrive.com/api/v2/deals'
const queryParams = new URLSearchParams()
// Add optional parameters to query string if they exist
if (params.status) queryParams.append('status', params.status)
if (params.person_id) queryParams.append('person_id', params.person_id)
if (params.org_id) queryParams.append('org_id', params.org_id)
if (params.pipeline_id) queryParams.append('pipeline_id', params.pipeline_id)
if (params.updated_since) queryParams.append('updated_since', params.updated_since)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response, params?: PipedriveGetAllDealsParams) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch deals from Pipedrive')
}
const deals = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
return {
success: true,
output: {
deals,
metadata: {
operation: 'get_all_deals' as const,
totalItems: deals.length,
hasMore,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deals data and metadata',
properties: {
deals: {
type: 'array',
description: 'Array of deal objects from Pipedrive',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Deal ID' },
title: { type: 'string', description: 'Deal title' },
value: { type: 'number', description: 'Deal value' },
currency: { type: 'string', description: 'Deal currency' },
status: { type: 'string', description: 'Deal status' },
stage_id: { type: 'number', description: 'Stage ID' },
pipeline_id: { type: 'number', description: 'Pipeline ID' },
owner_id: { type: 'number', description: 'Owner user ID' },
add_time: { type: 'string', description: 'Deal creation time' },
update_time: { type: 'string', description: 'Deal last update time' },
},
},
},
metadata: {
type: 'object',
description: 'Operation metadata',
properties: {
operation: { type: 'string', description: 'The operation performed' },
totalItems: { type: 'number', description: 'Total number of deals returned' },
hasMore: {
type: 'boolean',
description: 'Whether there are more items to fetch via pagination',
},
},
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,81 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { PipedriveGetDealParams, PipedriveGetDealResponse } from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetDeal')
export const pipedriveGetDealTool: ToolConfig<PipedriveGetDealParams, PipedriveGetDealResponse> = {
id: 'pipedrive_get_deal',
name: 'Get Deal Details from Pipedrive',
description: 'Retrieve detailed information about a specific deal',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
deal_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the deal to retrieve',
},
},
request: {
url: (params) => `https://api.pipedrive.com/api/v2/deals/${params.deal_id}`,
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch deal from Pipedrive')
}
return {
success: true,
output: {
deal: data.data,
metadata: {
operation: 'get_deal' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deal details',
properties: {
deal: {
type: 'object',
description: 'Deal object with full details',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,114 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { PipedriveGetFilesParams, PipedriveGetFilesResponse } from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetFiles')
export const pipedriveGetFilesTool: ToolConfig<PipedriveGetFilesParams, PipedriveGetFilesResponse> =
{
id: 'pipedrive_get_files',
name: 'Get Files from Pipedrive',
description: 'Retrieve files from Pipedrive with optional filters',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
deal_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter files by deal ID',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter files by person ID',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter files by organization ID',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500)',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.pipedrive.com/v1/files'
const queryParams = new URLSearchParams()
if (params.deal_id) queryParams.append('deal_id', params.deal_id)
if (params.person_id) queryParams.append('person_id', params.person_id)
if (params.org_id) queryParams.append('org_id', params.org_id)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch files from Pipedrive')
}
const files = data.data || []
return {
success: true,
output: {
files,
metadata: {
operation: 'get_files' as const,
totalItems: files.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Files data',
properties: {
files: {
type: 'array',
description: 'Array of file objects from Pipedrive',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,160 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { PipedriveGetLeadsParams, PipedriveGetLeadsResponse } from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetLeads')
export const pipedriveGetLeadsTool: ToolConfig<PipedriveGetLeadsParams, PipedriveGetLeadsResponse> =
{
id: 'pipedrive_get_leads',
name: 'Get Leads from Pipedrive',
description: 'Retrieve all leads or a specific lead from Pipedrive',
version: '1.0.0',
oauth: {
required: true,
provider: 'pipedrive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
lead_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Optional: ID of a specific lead to retrieve',
},
archived: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Get archived leads instead of active ones',
},
owner_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by owner user ID',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by person ID',
},
organization_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by organization ID',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500)',
},
},
request: {
url: (params) => {
// If lead_id is provided, get specific lead
if (params.lead_id) {
return `https://api.pipedrive.com/v1/leads/${params.lead_id}`
}
// Get archived or active leads with optional filters
const baseUrl =
params.archived === 'true'
? 'https://api.pipedrive.com/v1/leads/archived'
: 'https://api.pipedrive.com/v1/leads'
const queryParams = new URLSearchParams()
if (params.owner_id) queryParams.append('owner_id', params.owner_id)
if (params.person_id) queryParams.append('person_id', params.person_id)
if (params.organization_id) queryParams.append('organization_id', params.organization_id)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch lead(s) from Pipedrive')
}
// If lead_id was provided, return single lead
if (params?.lead_id) {
return {
success: true,
output: {
lead: data.data,
metadata: {
operation: 'get_leads' as const,
},
success: true,
},
}
}
// Otherwise, return list of leads
const leads = data.data || []
return {
success: true,
output: {
leads,
metadata: {
operation: 'get_leads' as const,
totalItems: leads.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Leads data or single lead details',
properties: {
leads: {
type: 'array',
description: 'Array of lead objects (when listing all)',
},
lead: {
type: 'object',
description: 'Single lead object (when lead_id is provided)',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,110 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetMailMessagesParams,
PipedriveGetMailMessagesResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetMailMessages')
export const pipedriveGetMailMessagesTool: ToolConfig<
PipedriveGetMailMessagesParams,
PipedriveGetMailMessagesResponse
> = {
id: 'pipedrive_get_mail_messages',
name: 'Get Mail Threads from Pipedrive',
description: 'Retrieve mail threads from Pipedrive mailbox',
version: '1.0.0',
oauth: {
required: true,
provider: 'pipedrive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
folder: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by folder: inbox, drafts, sent, archive (default: inbox)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 50)',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.pipedrive.com/v1/mailbox/mailThreads'
const queryParams = new URLSearchParams()
if (params.folder) queryParams.append('folder', params.folder)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch mail threads from Pipedrive')
}
const threads = data.data || []
return {
success: true,
output: {
messages: threads,
metadata: {
operation: 'get_mail_messages' as const,
totalItems: threads.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Mail threads data',
properties: {
messages: {
type: 'array',
description: 'Array of mail thread objects from Pipedrive mailbox',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,97 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetMailThreadParams,
PipedriveGetMailThreadResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetMailThread')
export const pipedriveGetMailThreadTool: ToolConfig<
PipedriveGetMailThreadParams,
PipedriveGetMailThreadResponse
> = {
id: 'pipedrive_get_mail_thread',
name: 'Get Mail Thread Messages from Pipedrive',
description: 'Retrieve all messages from a specific mail thread',
version: '1.0.0',
oauth: {
required: true,
provider: 'pipedrive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
thread_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the mail thread',
},
},
request: {
url: (params) =>
`https://api.pipedrive.com/v1/mailbox/mailThreads/${params.thread_id}/mailMessages`,
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch mail thread from Pipedrive')
}
const messages = data.data || []
return {
success: true,
output: {
messages,
metadata: {
operation: 'get_mail_thread' as const,
threadId: params?.thread_id || '',
totalItems: messages.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Mail thread messages data',
properties: {
messages: {
type: 'array',
description: 'Array of mail message objects from the thread',
},
metadata: {
type: 'object',
description: 'Operation metadata including thread ID',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,119 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetPipelineDealsParams,
PipedriveGetPipelineDealsResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetPipelineDeals')
export const pipedriveGetPipelineDealsTool: ToolConfig<
PipedriveGetPipelineDealsParams,
PipedriveGetPipelineDealsResponse
> = {
id: 'pipedrive_get_pipeline_deals',
name: 'Get Pipeline Deals from Pipedrive',
description: 'Retrieve all deals in a specific pipeline',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
pipeline_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the pipeline',
},
stage_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by specific stage within the pipeline',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by deal status: open, won, lost',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500)',
},
},
request: {
url: (params) => {
const baseUrl = `https://api.pipedrive.com/v1/pipelines/${params.pipeline_id}/deals`
const queryParams = new URLSearchParams()
if (params.stage_id) queryParams.append('stage_id', params.stage_id)
if (params.status) queryParams.append('status', params.status)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch pipeline deals from Pipedrive')
}
const deals = data.data || []
return {
success: true,
output: {
deals,
metadata: {
operation: 'get_pipeline_deals' as const,
pipelineId: params?.pipeline_id || '',
totalItems: deals.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Pipeline deals data',
properties: {
deals: {
type: 'array',
description: 'Array of deal objects from the pipeline',
},
metadata: {
type: 'object',
description: 'Operation metadata including pipeline ID',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,119 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetPipelinesParams,
PipedriveGetPipelinesResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetPipelines')
export const pipedriveGetPipelinesTool: ToolConfig<
PipedriveGetPipelinesParams,
PipedriveGetPipelinesResponse
> = {
id: 'pipedrive_get_pipelines',
name: 'Get Pipelines from Pipedrive',
description: 'Retrieve all pipelines from Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
sort_by: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Field to sort by: id, update_time, add_time (default: id)',
},
sort_direction: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Sorting direction: asc, desc (default: asc)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'For pagination, the marker representing the first item on the next page',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.pipedrive.com/v1/pipelines'
const queryParams = new URLSearchParams()
if (params.sort_by) queryParams.append('sort_by', params.sort_by)
if (params.sort_direction) queryParams.append('sort_direction', params.sort_direction)
if (params.limit) queryParams.append('limit', params.limit)
if (params.cursor) queryParams.append('cursor', params.cursor)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch pipelines from Pipedrive')
}
const pipelines = data.data || []
return {
success: true,
output: {
pipelines,
metadata: {
operation: 'get_pipelines' as const,
totalItems: pipelines.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Pipelines data',
properties: {
pipelines: {
type: 'array',
description: 'Array of pipeline objects from Pipedrive',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,136 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveGetProjectsParams,
PipedriveGetProjectsResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveGetProjects')
export const pipedriveGetProjectsTool: ToolConfig<
PipedriveGetProjectsParams,
PipedriveGetProjectsResponse
> = {
id: 'pipedrive_get_projects',
name: 'Get Projects from Pipedrive',
description: 'Retrieve all projects or a specific project from Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
project_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Optional: ID of a specific project to retrieve',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by project status: open, completed, deleted (only for listing all)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 500, only for listing all)',
},
},
request: {
url: (params) => {
// If project_id is provided, get specific project
if (params.project_id) {
return `https://api.pipedrive.com/v1/projects/${params.project_id}`
}
// Otherwise, get all projects with optional filters
const baseUrl = 'https://api.pipedrive.com/v1/projects'
const queryParams = new URLSearchParams()
if (params.status) queryParams.append('status', params.status)
if (params.limit) queryParams.append('limit', params.limit)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to fetch project(s) from Pipedrive')
}
// If project_id was provided, return single project
if (params?.project_id) {
return {
success: true,
output: {
project: data.data,
metadata: {
operation: 'get_projects' as const,
},
success: true,
},
}
}
// Otherwise, return list of projects
const projects = data.data || []
return {
success: true,
output: {
projects,
metadata: {
operation: 'get_projects' as const,
totalItems: projects.length,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Projects data or single project details',
properties: {
projects: {
type: 'array',
description: 'Array of project objects (when listing all)',
},
project: {
type: 'object',
description: 'Single project object (when project_id is provided)',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,26 @@
// Deal operations
export { pipedriveCreateActivityTool } from '@/tools/pipedrive/create_activity'
export { pipedriveCreateDealTool } from '@/tools/pipedrive/create_deal'
export { pipedriveCreateLeadTool } from '@/tools/pipedrive/create_lead'
export { pipedriveCreateProjectTool } from '@/tools/pipedrive/create_project'
export { pipedriveDeleteLeadTool } from '@/tools/pipedrive/delete_lead'
// Activity operations
export { pipedriveGetActivitiesTool } from '@/tools/pipedrive/get_activities'
export { pipedriveGetAllDealsTool } from '@/tools/pipedrive/get_all_deals'
export { pipedriveGetDealTool } from '@/tools/pipedrive/get_deal'
// File operations
export { pipedriveGetFilesTool } from '@/tools/pipedrive/get_files'
// Lead operations
export { pipedriveGetLeadsTool } from '@/tools/pipedrive/get_leads'
// Mail operations
export { pipedriveGetMailMessagesTool } from '@/tools/pipedrive/get_mail_messages'
export { pipedriveGetMailThreadTool } from '@/tools/pipedrive/get_mail_thread'
export { pipedriveGetPipelineDealsTool } from '@/tools/pipedrive/get_pipeline_deals'
// Pipeline operations
export { pipedriveGetPipelinesTool } from '@/tools/pipedrive/get_pipelines'
// Project operations
export { pipedriveGetProjectsTool } from '@/tools/pipedrive/get_projects'
export { pipedriveUpdateActivityTool } from '@/tools/pipedrive/update_activity'
export { pipedriveUpdateDealTool } from '@/tools/pipedrive/update_deal'
export { pipedriveUpdateLeadTool } from '@/tools/pipedrive/update_lead'

View File

@@ -0,0 +1,536 @@
import type { ToolResponse } from '@/tools/types'
// Common Pipedrive types
export interface PipedriveLead {
id: string
title: string
person_id?: number
organization_id?: number
owner_id: number
value?: {
amount: number
currency: string
}
expected_close_date?: string
is_archived: boolean
was_seen: boolean
add_time: string
update_time: string
}
export interface PipedriveDeal {
id: number
title: string
value: number
currency: string
status: string
stage_id: number
pipeline_id: number
person_id?: number
org_id?: number
owner_id: number
add_time: string
update_time: string
won_time?: string
lost_time?: string
close_time?: string
expected_close_date?: string
}
export interface PipedriveActivity {
id: number
subject: string
type: string
due_date: string
due_time: string
duration: string
deal_id?: number
person_id?: number
org_id?: number
done: boolean
note: string
add_time: string
update_time: string
}
export interface PipedriveFile {
id: number
name: string
file_type: string
file_size: number
add_time: string
update_time: string
deal_id?: number
person_id?: number
org_id?: number
url: string
}
export interface PipedrivePipeline {
id: number
name: string
url_title: string
order_nr: number
active: boolean
deal_probability: boolean
add_time: string
update_time: string
}
export interface PipedriveProject {
id: number
title: string
description?: string
status: string
owner_id: number
start_date?: string
end_date?: string
add_time: string
update_time: string
}
export interface PipedriveMailMessage {
id: number
subject: string
snippet: string
mail_thread_id: number
from_address: string
to_addresses: string[]
cc_addresses?: string[]
bcc_addresses?: string[]
timestamp: string
item_type: string
deal_id?: number
person_id?: number
org_id?: number
}
// GET All Deals
export interface PipedriveGetAllDealsParams {
accessToken: string
status?: string
person_id?: string
org_id?: string
pipeline_id?: string
updated_since?: string
limit?: string
}
export interface PipedriveGetAllDealsOutput {
deals: PipedriveDeal[]
metadata: {
operation: 'get_all_deals'
totalItems: number
hasMore: boolean
}
success: boolean
}
export interface PipedriveGetAllDealsResponse extends ToolResponse {
output: PipedriveGetAllDealsOutput
}
// GET Deal
export interface PipedriveGetDealParams {
accessToken: string
deal_id: string
}
export interface PipedriveGetDealOutput {
deal: PipedriveDeal
metadata: {
operation: 'get_deal'
}
success: boolean
}
export interface PipedriveGetDealResponse extends ToolResponse {
output: PipedriveGetDealOutput
}
// CREATE Deal
export interface PipedriveCreateDealParams {
accessToken: string
title: string
value?: string
currency?: string
person_id?: string
org_id?: string
pipeline_id?: string
stage_id?: string
status?: string
expected_close_date?: string
}
export interface PipedriveCreateDealOutput {
deal: PipedriveDeal
metadata: {
operation: 'create_deal'
}
success: boolean
}
export interface PipedriveCreateDealResponse extends ToolResponse {
output: PipedriveCreateDealOutput
}
// UPDATE Deal
export interface PipedriveUpdateDealParams {
accessToken: string
deal_id: string
title?: string
value?: string
status?: string
stage_id?: string
expected_close_date?: string
}
export interface PipedriveUpdateDealOutput {
deal: PipedriveDeal
metadata: {
operation: 'update_deal'
}
success: boolean
}
export interface PipedriveUpdateDealResponse extends ToolResponse {
output: PipedriveUpdateDealOutput
}
// GET Files
export interface PipedriveGetFilesParams {
accessToken: string
deal_id?: string
person_id?: string
org_id?: string
limit?: string
}
export interface PipedriveGetFilesOutput {
files: PipedriveFile[]
metadata: {
operation: 'get_files'
totalItems: number
}
success: boolean
}
export interface PipedriveGetFilesResponse extends ToolResponse {
output: PipedriveGetFilesOutput
}
export interface PipedriveGetMailMessagesParams {
accessToken: string
folder?: string
limit?: string
}
export interface PipedriveGetMailMessagesOutput {
messages: PipedriveMailMessage[]
metadata: {
operation: 'get_mail_messages'
totalItems: number
}
success: boolean
}
export interface PipedriveGetMailMessagesResponse extends ToolResponse {
output: PipedriveGetMailMessagesOutput
}
// GET Mail Thread
export interface PipedriveGetMailThreadParams {
accessToken: string
thread_id: string
}
export interface PipedriveGetMailThreadOutput {
messages: PipedriveMailMessage[]
metadata: {
operation: 'get_mail_thread'
threadId: string
totalItems: number
}
success: boolean
}
export interface PipedriveGetMailThreadResponse extends ToolResponse {
output: PipedriveGetMailThreadOutput
}
// GET All Pipelines
export interface PipedriveGetPipelinesParams {
accessToken: string
sort_by?: string
sort_direction?: string
limit?: string
cursor?: string
}
export interface PipedriveGetPipelinesOutput {
pipelines: PipedrivePipeline[]
metadata: {
operation: 'get_pipelines'
totalItems: number
}
success: boolean
}
export interface PipedriveGetPipelinesResponse extends ToolResponse {
output: PipedriveGetPipelinesOutput
}
// GET Pipeline Deals
export interface PipedriveGetPipelineDealsParams {
accessToken: string
pipeline_id: string
stage_id?: string
status?: string
limit?: string
}
export interface PipedriveGetPipelineDealsOutput {
deals: PipedriveDeal[]
metadata: {
operation: 'get_pipeline_deals'
pipelineId: string
totalItems: number
}
success: boolean
}
export interface PipedriveGetPipelineDealsResponse extends ToolResponse {
output: PipedriveGetPipelineDealsOutput
}
// GET All Projects (or single project if project_id provided)
export interface PipedriveGetProjectsParams {
accessToken: string
project_id?: string
status?: string
limit?: string
}
export interface PipedriveGetProjectsOutput {
projects?: PipedriveProject[]
project?: PipedriveProject
metadata: {
operation: 'get_projects'
totalItems?: number
}
success: boolean
}
export interface PipedriveGetProjectsResponse extends ToolResponse {
output: PipedriveGetProjectsOutput
}
// CREATE Project
export interface PipedriveCreateProjectParams {
accessToken: string
title: string
description?: string
start_date?: string
end_date?: string
}
export interface PipedriveCreateProjectOutput {
project: PipedriveProject
metadata: {
operation: 'create_project'
}
success: boolean
}
export interface PipedriveCreateProjectResponse extends ToolResponse {
output: PipedriveCreateProjectOutput
}
// GET All Activities
export interface PipedriveGetActivitiesParams {
accessToken: string
deal_id?: string
person_id?: string
org_id?: string
type?: string
done?: string
limit?: string
}
export interface PipedriveGetActivitiesOutput {
activities: PipedriveActivity[]
metadata: {
operation: 'get_activities'
totalItems: number
}
success: boolean
}
export interface PipedriveGetActivitiesResponse extends ToolResponse {
output: PipedriveGetActivitiesOutput
}
// CREATE Activity
export interface PipedriveCreateActivityParams {
accessToken: string
subject: string
type: string
due_date: string
due_time?: string
duration?: string
deal_id?: string
person_id?: string
org_id?: string
note?: string
}
export interface PipedriveCreateActivityOutput {
activity: PipedriveActivity
metadata: {
operation: 'create_activity'
}
success: boolean
}
export interface PipedriveCreateActivityResponse extends ToolResponse {
output: PipedriveCreateActivityOutput
}
// UPDATE Activity
export interface PipedriveUpdateActivityParams {
accessToken: string
activity_id: string
subject?: string
due_date?: string
due_time?: string
duration?: string
done?: string
note?: string
}
export interface PipedriveUpdateActivityOutput {
activity: PipedriveActivity
metadata: {
operation: 'update_activity'
}
success: boolean
}
export interface PipedriveUpdateActivityResponse extends ToolResponse {
output: PipedriveUpdateActivityOutput
}
// GET Leads
export interface PipedriveGetLeadsParams {
accessToken: string
lead_id?: string
archived?: string
owner_id?: string
person_id?: string
organization_id?: string
limit?: string
}
export interface PipedriveGetLeadsOutput {
leads?: PipedriveLead[]
lead?: PipedriveLead
metadata: {
operation: 'get_leads'
totalItems?: number
}
success: boolean
}
export interface PipedriveGetLeadsResponse extends ToolResponse {
output: PipedriveGetLeadsOutput
}
// CREATE Lead
export interface PipedriveCreateLeadParams {
accessToken: string
title: string
person_id?: string
organization_id?: string
owner_id?: string
value_amount?: string
value_currency?: string
expected_close_date?: string
visible_to?: string
}
export interface PipedriveCreateLeadOutput {
lead: PipedriveLead
metadata: {
operation: 'create_lead'
}
success: boolean
}
export interface PipedriveCreateLeadResponse extends ToolResponse {
output: PipedriveCreateLeadOutput
}
// UPDATE Lead
export interface PipedriveUpdateLeadParams {
accessToken: string
lead_id: string
title?: string
person_id?: string
organization_id?: string
owner_id?: string
value_amount?: string
value_currency?: string
expected_close_date?: string
is_archived?: string
}
export interface PipedriveUpdateLeadOutput {
lead: PipedriveLead
metadata: {
operation: 'update_lead'
}
success: boolean
}
export interface PipedriveUpdateLeadResponse extends ToolResponse {
output: PipedriveUpdateLeadOutput
}
// DELETE Lead
export interface PipedriveDeleteLeadParams {
accessToken: string
lead_id: string
}
export interface PipedriveDeleteLeadOutput {
data: any
metadata: {
operation: 'delete_lead'
}
success: boolean
}
export interface PipedriveDeleteLeadResponse extends ToolResponse {
output: PipedriveDeleteLeadOutput
}
// Union type of all responses
export type PipedriveResponse =
| PipedriveGetAllDealsResponse
| PipedriveGetDealResponse
| PipedriveCreateDealResponse
| PipedriveUpdateDealResponse
| PipedriveGetFilesResponse
| PipedriveGetMailMessagesResponse
| PipedriveGetMailThreadResponse
| PipedriveGetPipelinesResponse
| PipedriveGetPipelineDealsResponse
| PipedriveGetProjectsResponse
| PipedriveCreateProjectResponse
| PipedriveGetActivitiesResponse
| PipedriveCreateActivityResponse
| PipedriveUpdateActivityResponse
| PipedriveGetLeadsResponse
| PipedriveCreateLeadResponse
| PipedriveUpdateLeadResponse
| PipedriveDeleteLeadResponse

View File

@@ -0,0 +1,136 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveUpdateActivityParams,
PipedriveUpdateActivityResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveUpdateActivity')
export const pipedriveUpdateActivityTool: ToolConfig<
PipedriveUpdateActivityParams,
PipedriveUpdateActivityResponse
> = {
id: 'pipedrive_update_activity',
name: 'Update Activity in Pipedrive',
description: 'Update an existing activity (task) in Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
activity_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the activity to update',
},
subject: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New subject/title for the activity',
},
due_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New due date in YYYY-MM-DD format',
},
due_time: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New due time in HH:MM format',
},
duration: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New duration in HH:MM format',
},
done: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mark as done: 0 for not done, 1 for done',
},
note: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New notes for the activity',
},
},
request: {
url: (params) => `https://api.pipedrive.com/v1/activities/${params.activity_id}`,
method: 'PUT',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {}
if (params.subject) body.subject = params.subject
if (params.due_date) body.due_date = params.due_date
if (params.due_time) body.due_time = params.due_time
if (params.duration) body.duration = params.duration
if (params.done !== undefined) body.done = params.done === '1' ? 1 : 0
if (params.note) body.note = params.note
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to update activity in Pipedrive')
}
return {
success: true,
output: {
activity: data.data,
metadata: {
operation: 'update_activity' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated activity details',
properties: {
activity: {
type: 'object',
description: 'The updated activity object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,129 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveUpdateDealParams,
PipedriveUpdateDealResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveUpdateDeal')
export const pipedriveUpdateDealTool: ToolConfig<
PipedriveUpdateDealParams,
PipedriveUpdateDealResponse
> = {
id: 'pipedrive_update_deal',
name: 'Update Deal in Pipedrive',
description: 'Update an existing deal in Pipedrive',
version: '1.0.0',
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
deal_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the deal to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New title for the deal',
},
value: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New monetary value for the deal',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New status: open, won, lost',
},
stage_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New stage ID for the deal',
},
expected_close_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New expected close date in YYYY-MM-DD format',
},
},
request: {
url: (params) => `https://api.pipedrive.com/api/v2/deals/${params.deal_id}`,
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {}
if (params.title) body.title = params.title
if (params.value) body.value = Number(params.value)
if (params.status) body.status = params.status
if (params.stage_id) body.stage_id = Number(params.stage_id)
if (params.expected_close_date) body.expected_close_date = params.expected_close_date
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to update deal in Pipedrive')
}
return {
success: true,
output: {
deal: data.data,
metadata: {
operation: 'update_deal' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated deal details',
properties: {
deal: {
type: 'object',
description: 'The updated deal object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,162 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
PipedriveUpdateLeadParams,
PipedriveUpdateLeadResponse,
} from '@/tools/pipedrive/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('PipedriveUpdateLead')
export const pipedriveUpdateLeadTool: ToolConfig<
PipedriveUpdateLeadParams,
PipedriveUpdateLeadResponse
> = {
id: 'pipedrive_update_lead',
name: 'Update Lead in Pipedrive',
description: 'Update an existing lead in Pipedrive',
version: '1.0.0',
oauth: {
required: true,
provider: 'pipedrive',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
lead_id: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The ID of the lead to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New name for the lead',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New person ID',
},
organization_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New organization ID',
},
owner_id: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New owner user ID',
},
value_amount: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New value amount',
},
value_currency: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New currency code (e.g., USD, EUR)',
},
expected_close_date: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New expected close date in YYYY-MM-DD format',
},
is_archived: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Archive the lead: true or false',
},
},
request: {
url: (params) => `https://api.pipedrive.com/v1/leads/${params.lead_id}`,
method: 'PATCH',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {}
if (params.title) body.title = params.title
if (params.person_id) body.person_id = Number(params.person_id)
if (params.organization_id) body.organization_id = Number(params.organization_id)
if (params.owner_id) body.owner_id = Number(params.owner_id)
// Build value object if both amount and currency are provided
if (params.value_amount && params.value_currency) {
body.value = {
amount: Number(params.value_amount),
currency: params.value_currency,
}
}
if (params.expected_close_date) body.expected_close_date = params.expected_close_date
if (params.is_archived) body.is_archived = params.is_archived === 'true'
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
logger.error('Pipedrive API request failed', { data })
throw new Error(data.error || 'Failed to update lead in Pipedrive')
}
return {
success: true,
output: {
lead: data.data,
metadata: {
operation: 'update_lead' as const,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated lead details',
properties: {
lead: {
type: 'object',
description: 'The updated lead object',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -5,6 +5,14 @@ import {
airtableUpdateRecordTool,
} from '@/tools/airtable'
import { arxivGetAuthorPapersTool, arxivGetPaperTool, arxivSearchTool } from '@/tools/arxiv'
import {
asanaAddCommentTool,
asanaCreateTaskTool,
asanaGetProjectsTool,
asanaGetTaskTool,
asanaSearchTasksTool,
asanaUpdateTaskTool,
} from '@/tools/asana'
import { browserUseRunTaskTool } from '@/tools/browser_use'
import { clayPopulateTool } from '@/tools/clay'
import {
@@ -173,6 +181,20 @@ import {
} from '@/tools/google_vault'
import { guardrailsValidateTool } from '@/tools/guardrails'
import { requestTool as httpRequest } from '@/tools/http'
import {
hubspotCreateCompanyTool,
hubspotCreateContactTool,
hubspotGetCompanyTool,
hubspotGetContactTool,
hubspotGetUsersTool,
hubspotListCompaniesTool,
hubspotListContactsTool,
hubspotListDealsTool,
hubspotSearchCompaniesTool,
hubspotSearchContactsTool,
hubspotUpdateCompanyTool,
hubspotUpdateContactTool,
} from '@/tools/hubspot'
import { huggingfaceChatTool } from '@/tools/huggingface'
import {
hunterCompaniesFindTool,
@@ -384,6 +406,26 @@ import {
pineconeSearchVectorTool,
pineconeUpsertTextTool,
} from '@/tools/pinecone'
import {
pipedriveCreateActivityTool,
pipedriveCreateDealTool,
pipedriveCreateLeadTool,
pipedriveCreateProjectTool,
pipedriveDeleteLeadTool,
pipedriveGetActivitiesTool,
pipedriveGetAllDealsTool,
pipedriveGetDealTool,
pipedriveGetFilesTool,
pipedriveGetLeadsTool,
pipedriveGetMailMessagesTool,
pipedriveGetMailThreadTool,
pipedriveGetPipelineDealsTool,
pipedriveGetPipelinesTool,
pipedriveGetProjectsTool,
pipedriveUpdateActivityTool,
pipedriveUpdateDealTool,
pipedriveUpdateLeadTool,
} from '@/tools/pipedrive'
import {
deleteTool as postgresDeleteTool,
executeTool as postgresExecuteTool,
@@ -415,6 +457,32 @@ import {
s3ListObjectsTool,
s3PutObjectTool,
} from '@/tools/s3'
import {
salesforceCreateAccountTool,
salesforceCreateCaseTool,
salesforceCreateContactTool,
salesforceCreateLeadTool,
salesforceCreateOpportunityTool,
salesforceCreateTaskTool,
salesforceDeleteAccountTool,
salesforceDeleteCaseTool,
salesforceDeleteContactTool,
salesforceDeleteLeadTool,
salesforceDeleteOpportunityTool,
salesforceDeleteTaskTool,
salesforceGetAccountsTool,
salesforceGetCasesTool,
salesforceGetContactsTool,
salesforceGetLeadsTool,
salesforceGetOpportunitiesTool,
salesforceGetTasksTool,
salesforceUpdateAccountTool,
salesforceUpdateCaseTool,
salesforceUpdateContactTool,
salesforceUpdateLeadTool,
salesforceUpdateOpportunityTool,
salesforceUpdateTaskTool,
} from '@/tools/salesforce'
import { searchTool as serperSearch } from '@/tools/serper'
import {
sharepointAddListItemTool,
@@ -523,6 +591,14 @@ import {
telegramSendVideoTool,
} from '@/tools/telegram'
import { thinkingTool } from '@/tools/thinking'
import {
trelloAddCommentTool,
trelloCreateCardTool,
trelloGetActionsTool,
trelloListCardsTool,
trelloListListsTool,
trelloUpdateCardTool,
} from '@/tools/trello'
import { sendSMSTool } from '@/tools/twilio'
import { getRecordingTool, listCallsTool, makeCallTool } from '@/tools/twilio_voice'
import {
@@ -588,6 +664,12 @@ export const tools: Record<string, ToolConfig> = {
arxiv_search: arxivSearchTool,
arxiv_get_paper: arxivGetPaperTool,
arxiv_get_author_papers: arxivGetAuthorPapersTool,
asana_get_task: asanaGetTaskTool,
asana_create_task: asanaCreateTaskTool,
asana_update_task: asanaUpdateTaskTool,
asana_get_projects: asanaGetProjectsTool,
asana_search_tasks: asanaSearchTasksTool,
asana_add_comment: asanaAddCommentTool,
browser_use_run_task: browserUseRunTaskTool,
openai_embeddings: openAIEmbeddings,
http_request: httpRequest,
@@ -709,6 +791,24 @@ export const tools: Record<string, ToolConfig> = {
pinecone_search_text: pineconeSearchTextTool,
pinecone_search_vector: pineconeSearchVectorTool,
pinecone_upsert_text: pineconeUpsertTextTool,
pipedrive_create_activity: pipedriveCreateActivityTool,
pipedrive_create_deal: pipedriveCreateDealTool,
pipedrive_create_lead: pipedriveCreateLeadTool,
pipedrive_create_project: pipedriveCreateProjectTool,
pipedrive_delete_lead: pipedriveDeleteLeadTool,
pipedrive_get_activities: pipedriveGetActivitiesTool,
pipedrive_get_all_deals: pipedriveGetAllDealsTool,
pipedrive_get_deal: pipedriveGetDealTool,
pipedrive_get_files: pipedriveGetFilesTool,
pipedrive_get_leads: pipedriveGetLeadsTool,
pipedrive_get_mail_messages: pipedriveGetMailMessagesTool,
pipedrive_get_mail_thread: pipedriveGetMailThreadTool,
pipedrive_get_pipeline_deals: pipedriveGetPipelineDealsTool,
pipedrive_get_pipelines: pipedriveGetPipelinesTool,
pipedrive_get_projects: pipedriveGetProjectsTool,
pipedrive_update_activity: pipedriveUpdateActivityTool,
pipedrive_update_deal: pipedriveUpdateDealTool,
pipedrive_update_lead: pipedriveUpdateLeadTool,
postgresql_query: postgresQueryTool,
postgresql_insert: postgresInsertTool,
postgresql_update: postgresUpdateTool,
@@ -823,6 +923,12 @@ export const tools: Record<string, ToolConfig> = {
confluence_list_labels: confluenceListLabelsTool,
confluence_get_space: confluenceGetSpaceTool,
confluence_list_spaces: confluenceListSpacesTool,
trello_list_lists: trelloListListsTool,
trello_list_cards: trelloListCardsTool,
trello_create_card: trelloCreateCardTool,
trello_update_card: trelloUpdateCardTool,
trello_get_actions: trelloGetActionsTool,
trello_add_comment: trelloAddCommentTool,
twilio_send_sms: sendSMSTool,
twilio_voice_make_call: makeCallTool,
twilio_voice_list_calls: listCallsTool,
@@ -1065,6 +1171,18 @@ export const tools: Record<string, ToolConfig> = {
hunter_email_verifier: hunterEmailVerifierTool,
hunter_companies_find: hunterCompaniesFindTool,
hunter_email_count: hunterEmailCountTool,
hubspot_create_company: hubspotCreateCompanyTool,
hubspot_create_contact: hubspotCreateContactTool,
hubspot_get_company: hubspotGetCompanyTool,
hubspot_get_contact: hubspotGetContactTool,
hubspot_get_users: hubspotGetUsersTool,
hubspot_list_companies: hubspotListCompaniesTool,
hubspot_list_contacts: hubspotListContactsTool,
hubspot_list_deals: hubspotListDealsTool,
hubspot_search_companies: hubspotSearchCompaniesTool,
hubspot_search_contacts: hubspotSearchContactsTool,
hubspot_update_company: hubspotUpdateCompanyTool,
hubspot_update_contact: hubspotUpdateContactTool,
sharepoint_create_page: sharepointCreatePageTool,
sharepoint_read_page: sharepointReadPageTool,
sharepoint_list_sites: sharepointListSitesTool,
@@ -1123,4 +1241,28 @@ export const tools: Record<string, ToolConfig> = {
stripe_search_prices: stripeSearchPricesTool,
stripe_retrieve_event: stripeRetrieveEventTool,
stripe_list_events: stripeListEventsTool,
salesforce_get_accounts: salesforceGetAccountsTool,
salesforce_create_account: salesforceCreateAccountTool,
salesforce_update_account: salesforceUpdateAccountTool,
salesforce_delete_account: salesforceDeleteAccountTool,
salesforce_get_contacts: salesforceGetContactsTool,
salesforce_create_contact: salesforceCreateContactTool,
salesforce_update_contact: salesforceUpdateContactTool,
salesforce_delete_contact: salesforceDeleteContactTool,
salesforce_get_leads: salesforceGetLeadsTool,
salesforce_create_lead: salesforceCreateLeadTool,
salesforce_update_lead: salesforceUpdateLeadTool,
salesforce_delete_lead: salesforceDeleteLeadTool,
salesforce_get_opportunities: salesforceGetOpportunitiesTool,
salesforce_create_opportunity: salesforceCreateOpportunityTool,
salesforce_update_opportunity: salesforceUpdateOpportunityTool,
salesforce_delete_opportunity: salesforceDeleteOpportunityTool,
salesforce_get_cases: salesforceGetCasesTool,
salesforce_create_case: salesforceCreateCaseTool,
salesforce_update_case: salesforceUpdateCaseTool,
salesforce_delete_case: salesforceDeleteCaseTool,
salesforce_get_tasks: salesforceGetTasksTool,
salesforce_create_task: salesforceCreateTaskTool,
salesforce_update_task: salesforceUpdateTaskTool,
salesforce_delete_task: salesforceDeleteTaskTool,
}

View File

@@ -0,0 +1,315 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceCases')
function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
if (instanceUrl) return instanceUrl
if (idToken) {
try {
const base64Url = idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) return match[1]
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') return match[1]
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
throw new Error('Salesforce instance URL is required but not provided')
}
// Get Cases
export const salesforceGetCasesTool: ToolConfig<any, any> = {
id: 'salesforce_get_cases',
name: 'Get Cases from Salesforce',
description: 'Get case(s) from Salesforce',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
caseId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Case ID (optional)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Max results (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated fields',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Order by field',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.caseId) {
const fields =
params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId'
return `${instanceUrl}/services/data/v59.0/sobjects/Case/${params.caseId}?fields=${fields}`
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields =
params.fields || 'Id,CaseNumber,Subject,Status,Priority,Origin,ContactId,AccountId'
const orderBy = params.orderBy || 'CreatedDate DESC'
const query = `SELECT ${fields} FROM Case ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params) => {
const data = await response.json()
if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to fetch cases')
if (params.caseId) {
return {
success: true,
output: { case: data, metadata: { operation: 'get_cases' }, success: true },
}
}
const cases = data.records || []
return {
success: true,
output: {
cases,
paging: {
nextRecordsUrl: data.nextRecordsUrl,
totalSize: data.totalSize || cases.length,
done: data.done !== false,
},
metadata: { operation: 'get_cases', totalReturned: cases.length, hasMore: !data.done },
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Case data' },
},
}
// Create Case
export const salesforceCreateCaseTool: ToolConfig<any, any> = {
id: 'salesforce_create_case',
name: 'Create Case in Salesforce',
description: 'Create a new case',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
subject: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Case subject (required)',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Status (e.g., New, Working, Escalated)',
},
priority: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Priority (e.g., Low, Medium, High)',
},
origin: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Origin (e.g., Phone, Email, Web)',
},
contactId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Contact ID',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account ID',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { Subject: params.subject }
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.origin) body.Origin = params.origin
if (params.contactId) body.ContactId = params.contactId
if (params.accountId) body.AccountId = params.accountId
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to create case')
return {
success: true,
output: {
id: data.id,
success: data.success,
created: true,
metadata: { operation: 'create_case' },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Created case' },
},
}
// Update Case
export const salesforceUpdateCaseTool: ToolConfig<any, any> = {
id: 'salesforce_update_case',
name: 'Update Case in Salesforce',
description: 'Update an existing case',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
caseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Case ID (required)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Case subject',
},
status: { type: 'string', required: false, visibility: 'user-only', description: 'Status' },
priority: { type: 'string', required: false, visibility: 'user-only', description: 'Priority' },
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${params.caseId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.subject) body.Subject = params.subject
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json()
throw new Error(data[0]?.message || data.message || 'Failed to update case')
}
return {
success: true,
output: { id: params.caseId, updated: true, metadata: { operation: 'update_case' } },
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Updated case' },
},
}
// Delete Case
export const salesforceDeleteCaseTool: ToolConfig<any, any> = {
id: 'salesforce_delete_case',
name: 'Delete Case from Salesforce',
description: 'Delete a case',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
caseId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Case ID (required)',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Case/${params.caseId}`,
method: 'DELETE',
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(data[0]?.message || data.message || 'Failed to delete case')
}
return {
success: true,
output: { id: params.caseId, deleted: true, metadata: { operation: 'delete_case' } },
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Deleted case' },
},
}

View File

@@ -0,0 +1,658 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceContacts')
// Helper to extract instance URL from idToken
function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
if (instanceUrl) return instanceUrl
if (idToken) {
try {
const base64Url = idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) return match[1]
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') {
return match[1]
}
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
throw new Error('Salesforce instance URL is required but not provided')
}
// Get Contacts (with optional contactId)
export interface SalesforceGetContactsParams {
accessToken: string
idToken?: string
instanceUrl?: string
contactId?: string
limit?: string
fields?: string
orderBy?: string
}
export interface SalesforceGetContactsResponse {
success: boolean
output: {
contacts?: any[]
contact?: any
paging?: {
nextRecordsUrl?: string
totalSize: number
done: boolean
}
metadata: {
operation: 'get_contacts'
totalReturned?: number
hasMore?: boolean
singleContact?: boolean
}
success: boolean
}
}
export const salesforceGetContactsTool: ToolConfig<
SalesforceGetContactsParams,
SalesforceGetContactsResponse
> = {
id: 'salesforce_get_contacts',
name: 'Get Contacts from Salesforce',
description: 'Get contact(s) from Salesforce - single contact if ID provided, or list if not',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
contactId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Contact ID (if provided, returns single contact)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results (default: 100, max: 2000). Only for list query.',
},
fields: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated fields (e.g., "Id,FirstName,LastName,Email,Phone")',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Order by field (e.g., "LastName ASC"). Only for list query.',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
// Single contact by ID
if (params.contactId) {
const fields =
params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department'
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${params.contactId}?fields=${fields}`
}
// List contacts with SOQL query
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields = params.fields || 'Id,FirstName,LastName,Email,Phone,AccountId,Title,Department'
const orderBy = params.orderBy || 'LastName ASC'
const query = `SELECT ${fields} FROM Contact ORDER BY ${orderBy} LIMIT ${limit}`
const encodedQuery = encodeURIComponent(query)
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(
data[0]?.message || data.message || 'Failed to fetch contacts from Salesforce'
)
}
// Single contact response
if (params?.contactId) {
return {
success: true,
output: {
contact: data,
metadata: {
operation: 'get_contacts' as const,
singleContact: true,
},
success: true,
},
}
}
// List contacts response
const contacts = data.records || []
return {
success: true,
output: {
contacts,
paging: {
nextRecordsUrl: data.nextRecordsUrl,
totalSize: data.totalSize || contacts.length,
done: data.done !== false,
},
metadata: {
operation: 'get_contacts' as const,
totalReturned: contacts.length,
hasMore: !data.done,
singleContact: false,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Contact(s) data',
properties: {
contacts: { type: 'array', description: 'Array of contacts (list query)' },
contact: { type: 'object', description: 'Single contact (by ID)' },
paging: { type: 'object', description: 'Pagination info (list query)' },
metadata: { type: 'object', description: 'Operation metadata' },
success: { type: 'boolean', description: 'Operation success' },
},
},
},
}
// Create Contact
export interface SalesforceCreateContactParams {
accessToken: string
idToken?: string
instanceUrl?: string
lastName: string
firstName?: string
email?: string
phone?: string
accountId?: string
title?: string
department?: string
mailingStreet?: string
mailingCity?: string
mailingState?: string
mailingPostalCode?: string
mailingCountry?: string
description?: string
}
export interface SalesforceCreateContactResponse {
success: boolean
output: {
id: string
success: boolean
created: boolean
metadata: { operation: 'create_contact' }
}
}
export const salesforceCreateContactTool: ToolConfig<
SalesforceCreateContactParams,
SalesforceCreateContactResponse
> = {
id: 'salesforce_create_contact',
name: 'Create Contact in Salesforce',
description: 'Create a new contact in Salesforce CRM',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
lastName: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Last name (required)',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'First name',
},
email: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Email address',
},
phone: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Phone number',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account ID to associate contact with',
},
title: { type: 'string', required: false, visibility: 'user-only', description: 'Job title' },
department: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Department',
},
mailingStreet: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing street',
},
mailingCity: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing city',
},
mailingState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing state',
},
mailingPostalCode: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing postal code',
},
mailingCountry: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Contact description',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/sobjects/Contact`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { LastName: params.lastName }
if (params.firstName) body.FirstName = params.firstName
if (params.email) body.Email = params.email
if (params.phone) body.Phone = params.phone
if (params.accountId) body.AccountId = params.accountId
if (params.title) body.Title = params.title
if (params.department) body.Department = params.department
if (params.mailingStreet) body.MailingStreet = params.mailingStreet
if (params.mailingCity) body.MailingCity = params.mailingCity
if (params.mailingState) body.MailingState = params.mailingState
if (params.mailingPostalCode) body.MailingPostalCode = params.mailingPostalCode
if (params.mailingCountry) body.MailingCountry = params.mailingCountry
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(data[0]?.message || data.message || 'Failed to create contact in Salesforce')
}
return {
success: true,
output: {
id: data.id,
success: data.success,
created: true,
metadata: { operation: 'create_contact' as const },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created contact data',
properties: {
id: { type: 'string', description: 'Created contact ID' },
success: { type: 'boolean', description: 'Salesforce operation success' },
created: { type: 'boolean', description: 'Whether contact was created' },
metadata: { type: 'object', description: 'Operation metadata' },
},
},
},
}
// Update Contact
export interface SalesforceUpdateContactParams {
accessToken: string
idToken?: string
instanceUrl?: string
contactId: string
lastName?: string
firstName?: string
email?: string
phone?: string
accountId?: string
title?: string
department?: string
mailingStreet?: string
mailingCity?: string
mailingState?: string
mailingPostalCode?: string
mailingCountry?: string
description?: string
}
export interface SalesforceUpdateContactResponse {
success: boolean
output: {
id: string
updated: boolean
metadata: { operation: 'update_contact' }
}
}
export const salesforceUpdateContactTool: ToolConfig<
SalesforceUpdateContactParams,
SalesforceUpdateContactResponse
> = {
id: 'salesforce_update_contact',
name: 'Update Contact in Salesforce',
description: 'Update an existing contact in Salesforce CRM',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
contactId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Contact ID to update (required)',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Last name',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'First name',
},
email: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Email address',
},
phone: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Phone number',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account ID to associate with',
},
title: { type: 'string', required: false, visibility: 'user-only', description: 'Job title' },
department: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Department',
},
mailingStreet: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing street',
},
mailingCity: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing city',
},
mailingState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing state',
},
mailingPostalCode: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing postal code',
},
mailingCountry: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Mailing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${params.contactId}`
},
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.lastName) body.LastName = params.lastName
if (params.firstName) body.FirstName = params.firstName
if (params.email) body.Email = params.email
if (params.phone) body.Phone = params.phone
if (params.accountId) body.AccountId = params.accountId
if (params.title) body.Title = params.title
if (params.department) body.Department = params.department
if (params.mailingStreet) body.MailingStreet = params.mailingStreet
if (params.mailingCity) body.MailingCity = params.mailingCity
if (params.mailingState) body.MailingState = params.mailingState
if (params.mailingPostalCode) body.MailingPostalCode = params.mailingPostalCode
if (params.mailingCountry) body.MailingCountry = params.mailingCountry
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json()
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(data[0]?.message || data.message || 'Failed to update contact in Salesforce')
}
return {
success: true,
output: {
id: params?.contactId || '',
updated: true,
metadata: { operation: 'update_contact' as const },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Updated contact data',
properties: {
id: { type: 'string', description: 'Updated contact ID' },
updated: { type: 'boolean', description: 'Whether contact was updated' },
metadata: { type: 'object', description: 'Operation metadata' },
},
},
},
}
// Delete Contact
export interface SalesforceDeleteContactParams {
accessToken: string
idToken?: string
instanceUrl?: string
contactId: string
}
export interface SalesforceDeleteContactResponse {
success: boolean
output: {
id: string
deleted: boolean
metadata: { operation: 'delete_contact' }
}
}
export const salesforceDeleteContactTool: ToolConfig<
SalesforceDeleteContactParams,
SalesforceDeleteContactResponse
> = {
id: 'salesforce_delete_contact',
name: 'Delete Contact from Salesforce',
description: 'Delete a contact from Salesforce CRM',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
contactId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Contact ID to delete (required)',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
return `${instanceUrl}/services/data/v59.0/sobjects/Contact/${params.contactId}`
},
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(
data[0]?.message || data.message || 'Failed to delete contact from Salesforce'
)
}
return {
success: true,
output: {
id: params?.contactId || '',
deleted: true,
metadata: { operation: 'delete_contact' as const },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted contact data',
properties: {
id: { type: 'string', description: 'Deleted contact ID' },
deleted: { type: 'boolean', description: 'Whether contact was deleted' },
metadata: { type: 'object', description: 'Operation metadata' },
},
},
},
}

View File

@@ -0,0 +1,253 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceCreateAccount')
export interface SalesforceCreateAccountParams {
accessToken: string
idToken?: string
instanceUrl?: string
name: string
type?: string
industry?: string
phone?: string
website?: string
billingStreet?: string
billingCity?: string
billingState?: string
billingPostalCode?: string
billingCountry?: string
description?: string
annualRevenue?: string
numberOfEmployees?: string
}
export interface SalesforceCreateAccountResponse {
success: boolean
output: {
id: string
success: boolean
created: boolean
metadata: {
operation: 'create_account'
}
}
}
export const salesforceCreateAccountTool: ToolConfig<
SalesforceCreateAccountParams,
SalesforceCreateAccountResponse
> = {
id: 'salesforce_create_account',
name: 'Create Account in Salesforce',
description: 'Create a new account in Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
name: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Account name (required)',
},
type: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account type (e.g., Customer, Partner, Prospect)',
},
industry: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Industry (e.g., Technology, Healthcare, Finance)',
},
phone: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Phone number',
},
website: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Website URL',
},
billingStreet: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Billing street address',
},
billingCity: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Billing city',
},
billingState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Billing state/province',
},
billingPostalCode: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Billing postal code',
},
billingCountry: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Billing country',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account description',
},
annualRevenue: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Annual revenue (number)',
},
numberOfEmployees: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of employees (number)',
},
},
request: {
url: (params) => {
let instanceUrl = params.instanceUrl
if (!instanceUrl && params.idToken) {
try {
const base64Url = params.idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) {
instanceUrl = match[1]
}
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') {
instanceUrl = match[1]
}
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
if (!instanceUrl) {
throw new Error('Salesforce instance URL is required but not provided')
}
return `${instanceUrl}/services/data/v59.0/sobjects/Account`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, any> = {
Name: params.name,
}
if (params.type) body.Type = params.type
if (params.industry) body.Industry = params.industry
if (params.phone) body.Phone = params.phone
if (params.website) body.Website = params.website
if (params.billingStreet) body.BillingStreet = params.billingStreet
if (params.billingCity) body.BillingCity = params.billingCity
if (params.billingState) body.BillingState = params.billingState
if (params.billingPostalCode) body.BillingPostalCode = params.billingPostalCode
if (params.billingCountry) body.BillingCountry = params.billingCountry
if (params.description) body.Description = params.description
if (params.annualRevenue) body.AnnualRevenue = Number.parseFloat(params.annualRevenue)
if (params.numberOfEmployees)
body.NumberOfEmployees = Number.parseInt(params.numberOfEmployees)
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(data[0]?.message || data.message || 'Failed to create account in Salesforce')
}
return {
success: true,
output: {
id: data.id,
success: data.success,
created: true,
metadata: {
operation: 'create_account' as const,
},
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Created account data',
properties: {
id: { type: 'string', description: 'Created account ID' },
success: { type: 'boolean', description: 'Salesforce operation success' },
created: { type: 'boolean', description: 'Whether account was created' },
metadata: { type: 'object', description: 'Operation metadata' },
},
},
},
}

View File

@@ -0,0 +1,145 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceDeleteAccount')
export interface SalesforceDeleteAccountParams {
accessToken: string
idToken?: string
instanceUrl?: string
accountId: string
}
export interface SalesforceDeleteAccountResponse {
success: boolean
output: {
id: string
deleted: boolean
metadata: {
operation: 'delete_account'
}
}
}
export const salesforceDeleteAccountTool: ToolConfig<
SalesforceDeleteAccountParams,
SalesforceDeleteAccountResponse
> = {
id: 'salesforce_delete_account',
name: 'Delete Account from Salesforce',
description: 'Delete an account from Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
},
accountId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Account ID to delete (required)',
},
},
request: {
url: (params) => {
let instanceUrl = params.instanceUrl
if (!instanceUrl && params.idToken) {
try {
const base64Url = params.idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) {
instanceUrl = match[1]
}
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') {
instanceUrl = match[1]
}
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
if (!instanceUrl) {
throw new Error('Salesforce instance URL is required but not provided')
}
return `${instanceUrl}/services/data/v59.0/sobjects/Account/${params.accountId}`
},
method: 'DELETE',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
}
},
},
transformResponse: async (response: Response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(
data[0]?.message || data.message || 'Failed to delete account from Salesforce'
)
}
return {
success: true,
output: {
id: params?.accountId || '',
deleted: true,
metadata: {
operation: 'delete_account' as const,
},
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Deleted account data',
properties: {
id: { type: 'string', description: 'Deleted account ID' },
deleted: { type: 'boolean', description: 'Whether account was deleted' },
metadata: { type: 'object', description: 'Operation metadata' },
},
},
},
}

View File

@@ -0,0 +1,177 @@
import { createLogger } from '@/lib/logs/console/logger'
import type {
SalesforceGetAccountsParams,
SalesforceGetAccountsResponse,
} from '@/tools/salesforce/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceGetAccounts')
export const salesforceGetAccountsTool: ToolConfig<
SalesforceGetAccountsParams,
SalesforceGetAccountsResponse
> = {
id: 'salesforce_get_accounts',
name: 'Get Accounts from Salesforce',
description: 'Retrieve accounts from Salesforce CRM',
version: '1.0.0',
oauth: {
required: true,
provider: 'salesforce',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'The access token for the Salesforce API',
},
idToken: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The ID token from Salesforce OAuth (contains instance URL)',
},
instanceUrl: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'The Salesforce instance URL',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Number of results to return (default: 100, max: 2000)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated list of fields to return (e.g., "Id,Name,Industry,Phone")',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Field to order by (e.g., "Name ASC" or "CreatedDate DESC")',
},
},
request: {
url: (params) => {
let instanceUrl = params.instanceUrl
if (!instanceUrl && params.idToken) {
try {
const base64Url = params.idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) {
instanceUrl = match[1]
}
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') {
instanceUrl = match[1]
}
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
if (!instanceUrl) {
throw new Error('Salesforce instance URL is required but not provided')
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields =
params.fields ||
'Id,Name,Type,Industry,BillingCity,BillingState,BillingCountry,Phone,Website'
const orderBy = params.orderBy || 'Name ASC'
// Build SOQL query
const query = `SELECT ${fields} FROM Account ORDER BY ${orderBy} LIMIT ${limit}`
const encodedQuery = encodeURIComponent(query)
return `${instanceUrl}/services/data/v59.0/query?q=${encodedQuery}`
},
method: 'GET',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
logger.error('Salesforce API request failed', { data, status: response.status })
throw new Error(
data[0]?.message || data.message || 'Failed to fetch accounts from Salesforce'
)
}
const accounts = data.records || []
return {
success: true,
output: {
accounts,
paging: {
nextRecordsUrl: data.nextRecordsUrl,
totalSize: data.totalSize || accounts.length,
done: data.done !== false,
},
metadata: {
operation: 'get_accounts' as const,
totalReturned: accounts.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Operation success status' },
output: {
type: 'object',
description: 'Accounts data',
properties: {
accounts: {
type: 'array',
description: 'Array of account objects',
},
paging: {
type: 'object',
description: 'Pagination information',
},
metadata: {
type: 'object',
description: 'Operation metadata',
},
success: { type: 'boolean', description: 'Operation success status' },
},
},
},
}

View File

@@ -0,0 +1,34 @@
export {
salesforceCreateCaseTool,
salesforceDeleteCaseTool,
salesforceGetCasesTool,
salesforceUpdateCaseTool,
} from './cases'
export {
salesforceCreateContactTool,
salesforceDeleteContactTool,
salesforceGetContactsTool,
salesforceUpdateContactTool,
} from './contacts'
export { salesforceCreateAccountTool } from './create_account'
export { salesforceDeleteAccountTool } from './delete_account'
export { salesforceGetAccountsTool } from './get_accounts'
export {
salesforceCreateLeadTool,
salesforceDeleteLeadTool,
salesforceGetLeadsTool,
salesforceUpdateLeadTool,
} from './leads'
export {
salesforceCreateOpportunityTool,
salesforceDeleteOpportunityTool,
salesforceGetOpportunitiesTool,
salesforceUpdateOpportunityTool,
} from './opportunities'
export {
salesforceCreateTaskTool,
salesforceDeleteTaskTool,
salesforceGetTasksTool,
salesforceUpdateTaskTool,
} from './tasks'
export { salesforceUpdateAccountTool } from './update_account'

View File

@@ -0,0 +1,351 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceLeads')
function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
if (instanceUrl) return instanceUrl
if (idToken) {
try {
const base64Url = idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) return match[1]
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') return match[1]
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
throw new Error('Salesforce instance URL is required but not provided')
}
// Get Leads
export interface SalesforceGetLeadsParams {
accessToken: string
idToken?: string
instanceUrl?: string
leadId?: string
limit?: string
fields?: string
orderBy?: string
}
export const salesforceGetLeadsTool: ToolConfig<any, any> = {
id: 'salesforce_get_leads',
name: 'Get Leads from Salesforce',
description: 'Get lead(s) from Salesforce',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
leadId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Lead ID (optional)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Max results (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated fields',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Order by field',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.leadId) {
const fields =
params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource'
return `${instanceUrl}/services/data/v59.0/sobjects/Lead/${params.leadId}?fields=${fields}`
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields = params.fields || 'Id,FirstName,LastName,Company,Email,Phone,Status,LeadSource'
const orderBy = params.orderBy || 'LastName ASC'
const query = `SELECT ${fields} FROM Lead ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params) => {
const data = await response.json()
if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to fetch leads')
if (params.leadId) {
return {
success: true,
output: {
lead: data,
metadata: { operation: 'get_leads', singleLead: true },
success: true,
},
}
}
const leads = data.records || []
return {
success: true,
output: {
leads,
paging: {
nextRecordsUrl: data.nextRecordsUrl,
totalSize: data.totalSize || leads.length,
done: data.done !== false,
},
metadata: { operation: 'get_leads', totalReturned: leads.length, hasMore: !data.done },
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success status' },
output: { type: 'object', description: 'Lead data' },
},
}
// Create Lead
export const salesforceCreateLeadTool: ToolConfig<any, any> = {
id: 'salesforce_create_lead',
name: 'Create Lead in Salesforce',
description: 'Create a new lead',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
lastName: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Last name (required)',
},
company: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Company (required)',
},
firstName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'First name',
},
email: { type: 'string', required: false, visibility: 'user-only', description: 'Email' },
phone: { type: 'string', required: false, visibility: 'user-only', description: 'Phone' },
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Lead status',
},
leadSource: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Lead source',
},
title: { type: 'string', required: false, visibility: 'user-only', description: 'Title' },
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { LastName: params.lastName, Company: params.company }
if (params.firstName) body.FirstName = params.firstName
if (params.email) body.Email = params.email
if (params.phone) body.Phone = params.phone
if (params.status) body.Status = params.status
if (params.leadSource) body.LeadSource = params.leadSource
if (params.title) body.Title = params.title
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to create lead')
return {
success: true,
output: {
id: data.id,
success: data.success,
created: true,
metadata: { operation: 'create_lead' },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Created lead' },
},
}
// Update Lead
export const salesforceUpdateLeadTool: ToolConfig<any, any> = {
id: 'salesforce_update_lead',
name: 'Update Lead in Salesforce',
description: 'Update an existing lead',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
leadId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Lead ID (required)',
},
lastName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Last name',
},
company: { type: 'string', required: false, visibility: 'user-only', description: 'Company' },
firstName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'First name',
},
email: { type: 'string', required: false, visibility: 'user-only', description: 'Email' },
phone: { type: 'string', required: false, visibility: 'user-only', description: 'Phone' },
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Lead status',
},
leadSource: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Lead source',
},
title: { type: 'string', required: false, visibility: 'user-only', description: 'Title' },
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${params.leadId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.lastName) body.LastName = params.lastName
if (params.company) body.Company = params.company
if (params.firstName) body.FirstName = params.firstName
if (params.email) body.Email = params.email
if (params.phone) body.Phone = params.phone
if (params.status) body.Status = params.status
if (params.leadSource) body.LeadSource = params.leadSource
if (params.title) body.Title = params.title
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json()
throw new Error(data[0]?.message || data.message || 'Failed to update lead')
}
return {
success: true,
output: { id: params.leadId, updated: true, metadata: { operation: 'update_lead' } },
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Updated lead' },
},
}
// Delete Lead
export const salesforceDeleteLeadTool: ToolConfig<any, any> = {
id: 'salesforce_delete_lead',
name: 'Delete Lead from Salesforce',
description: 'Delete a lead',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
leadId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Lead ID (required)',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Lead/${params.leadId}`,
method: 'DELETE',
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(data[0]?.message || data.message || 'Failed to delete lead')
}
return {
success: true,
output: { id: params.leadId, deleted: true, metadata: { operation: 'delete_lead' } },
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Deleted lead' },
},
}

View File

@@ -0,0 +1,355 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceOpportunities')
function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
if (instanceUrl) return instanceUrl
if (idToken) {
try {
const base64Url = idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) return match[1]
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') return match[1]
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
throw new Error('Salesforce instance URL is required but not provided')
}
// Get Opportunities
export const salesforceGetOpportunitiesTool: ToolConfig<any, any> = {
id: 'salesforce_get_opportunities',
name: 'Get Opportunities from Salesforce',
description: 'Get opportunity(ies) from Salesforce',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
opportunityId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Opportunity ID (optional)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Max results (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated fields',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Order by field',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.opportunityId) {
const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability'
return `${instanceUrl}/services/data/v59.0/sobjects/Opportunity/${params.opportunityId}?fields=${fields}`
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields = params.fields || 'Id,Name,AccountId,Amount,StageName,CloseDate,Probability'
const orderBy = params.orderBy || 'CloseDate DESC'
const query = `SELECT ${fields} FROM Opportunity ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params) => {
const data = await response.json()
if (!response.ok)
throw new Error(data[0]?.message || data.message || 'Failed to fetch opportunities')
if (params.opportunityId) {
return {
success: true,
output: { opportunity: data, metadata: { operation: 'get_opportunities' }, success: true },
}
}
const opportunities = data.records || []
return {
success: true,
output: {
opportunities,
paging: {
nextRecordsUrl: data.nextRecordsUrl,
totalSize: data.totalSize || opportunities.length,
done: data.done !== false,
},
metadata: {
operation: 'get_opportunities',
totalReturned: opportunities.length,
hasMore: !data.done,
},
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Opportunity data' },
},
}
// Create Opportunity
export const salesforceCreateOpportunityTool: ToolConfig<any, any> = {
id: 'salesforce_create_opportunity',
name: 'Create Opportunity in Salesforce',
description: 'Create a new opportunity',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
name: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Opportunity name (required)',
},
stageName: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Stage name (required)',
},
closeDate: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Close date YYYY-MM-DD (required)',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account ID',
},
amount: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Amount (number)',
},
probability: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Probability (0-100)',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {
Name: params.name,
StageName: params.stageName,
CloseDate: params.closeDate,
}
if (params.accountId) body.AccountId = params.accountId
if (params.amount) body.Amount = Number.parseFloat(params.amount)
if (params.probability) body.Probability = Number.parseInt(params.probability)
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok)
throw new Error(data[0]?.message || data.message || 'Failed to create opportunity')
return {
success: true,
output: {
id: data.id,
success: data.success,
created: true,
metadata: { operation: 'create_opportunity' },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Created opportunity' },
},
}
// Update Opportunity
export const salesforceUpdateOpportunityTool: ToolConfig<any, any> = {
id: 'salesforce_update_opportunity',
name: 'Update Opportunity in Salesforce',
description: 'Update an existing opportunity',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
opportunityId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Opportunity ID (required)',
},
name: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Opportunity name',
},
stageName: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Stage name',
},
closeDate: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Close date YYYY-MM-DD',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Account ID',
},
amount: { type: 'string', required: false, visibility: 'user-only', description: 'Amount' },
probability: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Probability (0-100)',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${params.opportunityId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.name) body.Name = params.name
if (params.stageName) body.StageName = params.stageName
if (params.closeDate) body.CloseDate = params.closeDate
if (params.accountId) body.AccountId = params.accountId
if (params.amount) body.Amount = Number.parseFloat(params.amount)
if (params.probability) body.Probability = Number.parseInt(params.probability)
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json()
throw new Error(data[0]?.message || data.message || 'Failed to update opportunity')
}
return {
success: true,
output: {
id: params.opportunityId,
updated: true,
metadata: { operation: 'update_opportunity' },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Updated opportunity' },
},
}
// Delete Opportunity
export const salesforceDeleteOpportunityTool: ToolConfig<any, any> = {
id: 'salesforce_delete_opportunity',
name: 'Delete Opportunity from Salesforce',
description: 'Delete an opportunity',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
opportunityId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Opportunity ID (required)',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Opportunity/${params.opportunityId}`,
method: 'DELETE',
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(data[0]?.message || data.message || 'Failed to delete opportunity')
}
return {
success: true,
output: {
id: params.opportunityId,
deleted: true,
metadata: { operation: 'delete_opportunity' },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Deleted opportunity' },
},
}

View File

@@ -0,0 +1,321 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('SalesforceTasks')
function getInstanceUrl(idToken?: string, instanceUrl?: string): string {
if (instanceUrl) return instanceUrl
if (idToken) {
try {
const base64Url = idToken.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
)
const decoded = JSON.parse(jsonPayload)
if (decoded.profile) {
const match = decoded.profile.match(/^(https:\/\/[^/]+)/)
if (match) return match[1]
} else if (decoded.sub) {
const match = decoded.sub.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') return match[1]
}
} catch (error) {
logger.error('Failed to decode Salesforce idToken', { error })
}
}
throw new Error('Salesforce instance URL is required but not provided')
}
// Get Tasks
export const salesforceGetTasksTool: ToolConfig<any, any> = {
id: 'salesforce_get_tasks',
name: 'Get Tasks from Salesforce',
description: 'Get task(s) from Salesforce',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
taskId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Task ID (optional)',
},
limit: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Max results (default: 100)',
},
fields: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Comma-separated fields',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Order by field',
},
},
request: {
url: (params) => {
const instanceUrl = getInstanceUrl(params.idToken, params.instanceUrl)
if (params.taskId) {
const fields =
params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId'
return `${instanceUrl}/services/data/v59.0/sobjects/Task/${params.taskId}?fields=${fields}`
}
const limit = params.limit ? Number.parseInt(params.limit) : 100
const fields = params.fields || 'Id,Subject,Status,Priority,ActivityDate,WhoId,WhatId,OwnerId'
const orderBy = params.orderBy || 'ActivityDate DESC'
const query = `SELECT ${fields} FROM Task ORDER BY ${orderBy} LIMIT ${limit}`
return `${instanceUrl}/services/data/v59.0/query?q=${encodeURIComponent(query)}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params) => {
const data = await response.json()
if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to fetch tasks')
if (params.taskId) {
return {
success: true,
output: { task: data, metadata: { operation: 'get_tasks' }, success: true },
}
}
const tasks = data.records || []
return {
success: true,
output: {
tasks,
paging: {
nextRecordsUrl: data.nextRecordsUrl,
totalSize: data.totalSize || tasks.length,
done: data.done !== false,
},
metadata: { operation: 'get_tasks', totalReturned: tasks.length, hasMore: !data.done },
success: true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Task data' },
},
}
// Create Task
export const salesforceCreateTaskTool: ToolConfig<any, any> = {
id: 'salesforce_create_task',
name: 'Create Task in Salesforce',
description: 'Create a new task',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
subject: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Task subject (required)',
},
status: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Status (e.g., Not Started, In Progress, Completed)',
},
priority: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Priority (e.g., Low, Normal, High)',
},
activityDate: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Due date YYYY-MM-DD',
},
whoId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Related Contact/Lead ID',
},
whatId: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Related Account/Opportunity ID',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = { Subject: params.subject }
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.activityDate) body.ActivityDate = params.activityDate
if (params.whoId) body.WhoId = params.whoId
if (params.whatId) body.WhatId = params.whatId
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!response.ok) throw new Error(data[0]?.message || data.message || 'Failed to create task')
return {
success: true,
output: {
id: data.id,
success: data.success,
created: true,
metadata: { operation: 'create_task' },
},
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Created task' },
},
}
// Update Task
export const salesforceUpdateTaskTool: ToolConfig<any, any> = {
id: 'salesforce_update_task',
name: 'Update Task in Salesforce',
description: 'Update an existing task',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
taskId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Task ID (required)',
},
subject: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Task subject',
},
status: { type: 'string', required: false, visibility: 'user-only', description: 'Status' },
priority: { type: 'string', required: false, visibility: 'user-only', description: 'Priority' },
activityDate: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Due date YYYY-MM-DD',
},
description: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Description',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${params.taskId}`,
method: 'PATCH',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
if (params.subject) body.Subject = params.subject
if (params.status) body.Status = params.status
if (params.priority) body.Priority = params.priority
if (params.activityDate) body.ActivityDate = params.activityDate
if (params.description) body.Description = params.description
return body
},
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json()
throw new Error(data[0]?.message || data.message || 'Failed to update task')
}
return {
success: true,
output: { id: params.taskId, updated: true, metadata: { operation: 'update_task' } },
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Updated task' },
},
}
// Delete Task
export const salesforceDeleteTaskTool: ToolConfig<any, any> = {
id: 'salesforce_delete_task',
name: 'Delete Task from Salesforce',
description: 'Delete a task',
version: '1.0.0',
oauth: { required: true, provider: 'salesforce' },
params: {
accessToken: { type: 'string', required: true, visibility: 'hidden' },
idToken: { type: 'string', required: false, visibility: 'hidden' },
instanceUrl: { type: 'string', required: false, visibility: 'hidden' },
taskId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Task ID (required)',
},
},
request: {
url: (params) =>
`${getInstanceUrl(params.idToken, params.instanceUrl)}/services/data/v59.0/sobjects/Task/${params.taskId}`,
method: 'DELETE',
headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }),
},
transformResponse: async (response, params) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
throw new Error(data[0]?.message || data.message || 'Failed to delete task')
}
return {
success: true,
output: { id: params.taskId, deleted: true, metadata: { operation: 'delete_task' } },
}
},
outputs: {
success: { type: 'boolean', description: 'Success' },
output: { type: 'object', description: 'Deleted task' },
},
}

View File

@@ -0,0 +1,150 @@
import type { ToolResponse } from '@/tools/types'
// Common Salesforce types
export interface SalesforceAccount {
Id: string
Name: string
Type?: string
Industry?: string
BillingStreet?: string
BillingCity?: string
BillingState?: string
BillingPostalCode?: string
BillingCountry?: string
Phone?: string
Website?: string
AnnualRevenue?: number
NumberOfEmployees?: number
Description?: string
OwnerId?: string
CreatedDate?: string
LastModifiedDate?: string
[key: string]: any
}
export interface SalesforcePaging {
nextRecordsUrl?: string
totalSize: number
done: boolean
}
// Get Accounts
export interface SalesforceGetAccountsResponse extends ToolResponse {
output: {
accounts: SalesforceAccount[]
paging?: SalesforcePaging
metadata: {
operation: 'get_accounts'
totalReturned: number
hasMore: boolean
}
success: boolean
}
}
export interface SalesforceGetAccountsParams {
accessToken: string
idToken?: string
instanceUrl?: string
limit?: string
fields?: string
orderBy?: string
}
// Create Account
export interface SalesforceCreateAccountResponse {
success: boolean
output: {
id: string
success: boolean
created: boolean
metadata: {
operation: 'create_account'
}
}
}
// Update Account
export interface SalesforceUpdateAccountResponse {
success: boolean
output: {
id: string
updated: boolean
metadata: {
operation: 'update_account'
}
}
}
// Delete Account
export interface SalesforceDeleteAccountResponse {
success: boolean
output: {
id: string
deleted: boolean
metadata: {
operation: 'delete_account'
}
}
}
// Contact types
export interface SalesforceGetContactsResponse {
success: boolean
output: {
contacts?: any[]
contact?: any
paging?: {
nextRecordsUrl?: string
totalSize: number
done: boolean
}
metadata: {
operation: 'get_contacts'
totalReturned?: number
hasMore?: boolean
singleContact?: boolean
}
success: boolean
}
}
export interface SalesforceCreateContactResponse {
success: boolean
output: {
id: string
success: boolean
created: boolean
metadata: { operation: 'create_contact' }
}
}
export interface SalesforceUpdateContactResponse {
success: boolean
output: {
id: string
updated: boolean
metadata: { operation: 'update_contact' }
}
}
export interface SalesforceDeleteContactResponse {
success: boolean
output: {
id: string
deleted: boolean
metadata: { operation: 'delete_contact' }
}
}
// Generic Salesforce response type for the block
export type SalesforceResponse =
| SalesforceGetAccountsResponse
| SalesforceCreateAccountResponse
| SalesforceUpdateAccountResponse
| SalesforceDeleteAccountResponse
| SalesforceGetContactsResponse
| SalesforceCreateContactResponse
| SalesforceUpdateContactResponse
| SalesforceDeleteContactResponse
| { success: boolean; output: any } // Generic for leads, opportunities, cases, tasks

Some files were not shown because too many files have changed in this diff Show More