diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index e7286f9193..319ef99d8e 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -43,7 +43,7 @@ In Sim, the Confluence integration enables your agents to access and leverage yo ## Usage Instructions -Integrate Confluence into the workflow. Can read and update a page. +Integrate Confluence into the workflow. Can read, create, update, delete pages, manage comments, attachments, labels, and search content. @@ -94,6 +94,298 @@ Update a Confluence page using the Confluence API. | `title` | string | Updated page title | | `success` | boolean | Update operation success status | +### `confluence_create_page` + +Create a new page in a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Confluence space ID where the page will be created | +| `title` | string | Yes | Title of the new page | +| `content` | string | Yes | Page content in Confluence storage format \(HTML\) | +| `parentId` | string | No | Parent page ID if creating a child page | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of creation | +| `pageId` | string | Created page ID | +| `title` | string | Page title | +| `url` | string | Page URL | + +### `confluence_delete_page` + +Delete a Confluence page (moves it to trash where it can be restored). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of deletion | +| `pageId` | string | Deleted page ID | +| `deleted` | boolean | Deletion status | + +### `confluence_search` + +Search for content across Confluence pages, blog posts, and other content. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `query` | string | Yes | Search query string | +| `limit` | number | No | Maximum number of results to return \(default: 25\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of search | +| `results` | array | Search results | + +### `confluence_create_comment` + +Add a comment to a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to comment on | +| `comment` | string | Yes | Comment text in Confluence storage format | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of creation | +| `commentId` | string | Created comment ID | +| `pageId` | string | Page ID | + +### `confluence_list_comments` + +List all comments on a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to list comments from | +| `limit` | number | No | Maximum number of comments to return \(default: 25\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of retrieval | +| `comments` | array | List of comments | + +### `confluence_update_comment` + +Update an existing comment on a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `commentId` | string | Yes | Confluence comment ID to update | +| `comment` | string | Yes | Updated comment text in Confluence storage format | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of update | +| `commentId` | string | Updated comment ID | +| `updated` | boolean | Update status | + +### `confluence_delete_comment` + +Delete a comment from a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `commentId` | string | Yes | Confluence comment ID to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of deletion | +| `commentId` | string | Deleted comment ID | +| `deleted` | boolean | Deletion status | + +### `confluence_list_attachments` + +List all attachments on a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to list attachments from | +| `limit` | number | No | Maximum number of attachments to return \(default: 25\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of retrieval | +| `attachments` | array | List of attachments | + +### `confluence_delete_attachment` + +Delete an attachment from a Confluence page (moves to trash). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `attachmentId` | string | Yes | Confluence attachment ID to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of deletion | +| `attachmentId` | string | Deleted attachment ID | +| `deleted` | boolean | Deletion status | + +### `confluence_add_label` + +Add a label to a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to add label to | +| `labelName` | string | Yes | Label name to add | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of operation | +| `pageId` | string | Page ID | +| `labelName` | string | Label name | +| `added` | boolean | Addition status | + +### `confluence_list_labels` + +List all labels on a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to list labels from | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of retrieval | +| `labels` | array | List of labels | + +### `confluence_remove_label` + +Remove a label from a Confluence page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Confluence page ID to remove label from | +| `labelName` | string | Yes | Label name to remove | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of operation | +| `pageId` | string | Page ID | +| `labelName` | string | Label name | +| `removed` | boolean | Removal status | + +### `confluence_get_space` + +Get details about a specific Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Confluence space ID to retrieve | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of retrieval | +| `spaceId` | string | Space ID | +| `name` | string | Space name | +| `key` | string | Space key | +| `type` | string | Space type | +| `status` | string | Space status | +| `url` | string | Space URL | + +### `confluence_list_spaces` + +List all Confluence spaces accessible to the user. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `limit` | number | No | Maximum number of spaces to return \(default: 25\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | Timestamp of retrieval | +| `spaces` | array | List of spaces | + ## Notes diff --git a/apps/docs/content/docs/en/tools/discord.mdx b/apps/docs/content/docs/en/tools/discord.mdx index ecf653ef46..b8151d1186 100644 --- a/apps/docs/content/docs/en/tools/discord.mdx +++ b/apps/docs/content/docs/en/tools/discord.mdx @@ -57,7 +57,7 @@ Discord components in Sim use efficient lazy loading, only fetching data when ne ## Usage Instructions -Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information. +Comprehensive Discord integration: messages, threads, channels, roles, members, invites, and webhooks. @@ -101,7 +101,7 @@ Retrieve messages from a Discord channel | Parameter | Type | Description | | --------- | ---- | ----------- | | `message` | string | Success or error message | -| `messages` | array | Array of Discord messages with full metadata | +| `data` | object | Container for messages data | ### `discord_get_server` @@ -139,6 +139,620 @@ Retrieve information about a Discord user | `message` | string | Success or error message | | `data` | object | Discord user information | +### `discord_edit_message` + +Edit an existing message in a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID containing the message | +| `messageId` | string | Yes | The ID of the message to edit | +| `content` | string | No | The new text content for the message | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Updated Discord message data | + +### `discord_delete_message` + +Delete a message from a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID containing the message | +| `messageId` | string | Yes | The ID of the message to delete | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_add_reaction` + +Add a reaction emoji to a Discord message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID containing the message | +| `messageId` | string | Yes | The ID of the message to react to | +| `emoji` | string | Yes | The emoji to react with \(unicode emoji or custom emoji in name:id format\) | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_remove_reaction` + +Remove a reaction from a Discord message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID containing the message | +| `messageId` | string | Yes | The ID of the message with the reaction | +| `emoji` | string | Yes | The emoji to remove \(unicode emoji or custom emoji in name:id format\) | +| `userId` | string | No | The user ID whose reaction to remove \(omit to remove bot's own reaction\) | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_pin_message` + +Pin a message in a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID containing the message | +| `messageId` | string | Yes | The ID of the message to pin | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_unpin_message` + +Unpin a message in a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID containing the message | +| `messageId` | string | Yes | The ID of the message to unpin | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_create_thread` + +Create a thread in a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to create the thread in | +| `name` | string | Yes | The name of the thread \(1-100 characters\) | +| `messageId` | string | No | The message ID to create a thread from \(if creating from existing message\) | +| `autoArchiveDuration` | number | No | Duration in minutes to auto-archive the thread \(60, 1440, 4320, 10080\) | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Created thread data | + +### `discord_join_thread` + +Join a thread in Discord + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `threadId` | string | Yes | The thread ID to join | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_leave_thread` + +Leave a thread in Discord + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `threadId` | string | Yes | The thread ID to leave | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_archive_thread` + +Archive or unarchive a thread in Discord + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `threadId` | string | Yes | The thread ID to archive/unarchive | +| `archived` | boolean | Yes | Whether to archive \(true\) or unarchive \(false\) the thread | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Updated thread data | + +### `discord_create_channel` + +Create a new channel in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `name` | string | Yes | The name of the channel \(1-100 characters\) | +| `type` | number | No | Channel type \(0=text, 2=voice, 4=category, 5=announcement, 13=stage\) | +| `topic` | string | No | Channel topic \(0-1024 characters\) | +| `parentId` | string | No | Parent category ID for the channel | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Created channel data | + +### `discord_update_channel` + +Update a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to update | +| `name` | string | No | The new name for the channel | +| `topic` | string | No | The new topic for the channel | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Updated channel data | + +### `discord_delete_channel` + +Delete a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to delete | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_get_channel` + +Get information about a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to retrieve | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Channel data | + +### `discord_create_role` + +Create a new role in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `name` | string | Yes | The name of the role | +| `color` | number | No | RGB color value as integer \(e.g., 0xFF0000 for red\) | +| `hoist` | boolean | No | Whether to display role members separately from online members | +| `mentionable` | boolean | No | Whether the role can be mentioned | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Created role data | + +### `discord_update_role` + +Update a role in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `roleId` | string | Yes | The role ID to update | +| `name` | string | No | The new name for the role | +| `color` | number | No | RGB color value as integer | +| `hoist` | boolean | No | Whether to display role members separately | +| `mentionable` | boolean | No | Whether the role can be mentioned | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Updated role data | + +### `discord_delete_role` + +Delete a role from a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `roleId` | string | Yes | The role ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_assign_role` + +Assign a role to a member in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to assign the role to | +| `roleId` | string | Yes | The role ID to assign | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_remove_role` + +Remove a role from a member in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to remove the role from | +| `roleId` | string | Yes | The role ID to remove | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_kick_member` + +Kick a member from a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to kick | +| `reason` | string | No | Reason for kicking the member | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_ban_member` + +Ban a member from a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to ban | +| `reason` | string | No | Reason for banning the member | +| `deleteMessageDays` | number | No | Number of days to delete messages for \(0-7\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_unban_member` + +Unban a member from a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to unban | +| `reason` | string | No | Reason for unbanning the member | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_get_member` + +Get information about a member in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Member data | + +### `discord_update_member` + +Update a member in a Discord server (e.g., change nickname) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | +| `userId` | string | Yes | The user ID to update | +| `nick` | string | No | New nickname for the member \(null to remove\) | +| `mute` | boolean | No | Whether to mute the member in voice channels | +| `deaf` | boolean | No | Whether to deafen the member in voice channels | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Updated member data | + +### `discord_create_invite` + +Create an invite link for a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to create an invite for | +| `maxAge` | number | No | Duration of invite in seconds \(0 = never expires, default 86400\) | +| `maxUses` | number | No | Max number of uses \(0 = unlimited, default 0\) | +| `temporary` | boolean | No | Whether invite grants temporary membership | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Created invite data | + +### `discord_get_invite` + +Get information about a Discord invite + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `inviteCode` | string | Yes | The invite code to retrieve | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Invite data | + +### `discord_delete_invite` + +Delete a Discord invite + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `inviteCode` | string | Yes | The invite code to delete | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + +### `discord_create_webhook` + +Create a webhook in a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to create the webhook in | +| `name` | string | Yes | Name of the webhook \(1-80 characters\) | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Created webhook data | + +### `discord_execute_webhook` + +Execute a Discord webhook to send a message + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `webhookId` | string | Yes | The webhook ID | +| `webhookToken` | string | Yes | The webhook token | +| `content` | string | Yes | The message content to send | +| `username` | string | No | Override the default username of the webhook | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Message sent via webhook | + +### `discord_get_webhook` + +Get information about a Discord webhook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `webhookId` | string | Yes | The webhook ID to retrieve | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | object | Webhook data | + +### `discord_delete_webhook` + +Delete a Discord webhook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `webhookId` | string | Yes | The webhook ID to delete | +| `serverId` | string | Yes | The Discord server ID \(guild ID\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + ## Notes diff --git a/apps/docs/content/docs/en/tools/exa.mdx b/apps/docs/content/docs/en/tools/exa.mdx index 7727f5645e..714a052ffb 100644 --- a/apps/docs/content/docs/en/tools/exa.mdx +++ b/apps/docs/content/docs/en/tools/exa.mdx @@ -62,6 +62,17 @@ Search the web using Exa AI. Returns relevant search results with titles, URLs, | `numResults` | number | No | Number of results to return \(default: 10, max: 25\) | | `useAutoprompt` | boolean | No | Whether to use autoprompt to improve the query \(default: false\) | | `type` | string | No | Search type: neural, keyword, auto or fast \(default: auto\) | +| `includeDomains` | string | No | Comma-separated list of domains to include in results | +| `excludeDomains` | string | No | Comma-separated list of domains to exclude from results | +| `startPublishedDate` | string | No | Filter results published after this date \(ISO 8601 format, e.g., 2024-01-01\) | +| `endPublishedDate` | string | No | Filter results published before this date \(ISO 8601 format\) | +| `startCrawlDate` | string | No | Filter results crawled after this date \(ISO 8601 format\) | +| `endCrawlDate` | string | No | Filter results crawled before this date \(ISO 8601 format\) | +| `category` | string | No | Filter by category: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site | +| `text` | boolean | No | Include full text content in results \(default: false\) | +| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) | +| `summary` | boolean | No | Include AI-generated summaries in results \(default: false\) | +| `livecrawl` | string | No | Live crawling mode: always, fallback, or never \(default: never\) | | `apiKey` | string | Yes | Exa AI API Key | #### Output @@ -81,6 +92,10 @@ Retrieve the contents of webpages using Exa AI. Returns the title, text content, | `urls` | string | Yes | Comma-separated list of URLs to retrieve content from | | `text` | boolean | No | If true, returns full page text with default settings. If false, disables text return. | | `summaryQuery` | string | No | Query to guide the summary generation | +| `subpages` | number | No | Number of subpages to crawl from the provided URLs | +| `subpageTarget` | string | No | Comma-separated keywords to target specific subpages \(e.g., "docs,tutorial,about"\) | +| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) | +| `livecrawl` | string | No | Live crawling mode: always, fallback, or never \(default: never\) | | `apiKey` | string | Yes | Exa AI API Key | #### Output @@ -100,6 +115,17 @@ Find webpages similar to a given URL using Exa AI. Returns a list of similar lin | `url` | string | Yes | The URL to find similar links for | | `numResults` | number | No | Number of similar links to return \(default: 10, max: 25\) | | `text` | boolean | No | Whether to include the full text of the similar pages | +| `includeDomains` | string | No | Comma-separated list of domains to include in results | +| `excludeDomains` | string | No | Comma-separated list of domains to exclude from results | +| `excludeSourceDomain` | boolean | No | Exclude the source domain from results \(default: false\) | +| `startPublishedDate` | string | No | Filter results published after this date \(ISO 8601 format, e.g., 2024-01-01\) | +| `endPublishedDate` | string | No | Filter results published before this date \(ISO 8601 format\) | +| `startCrawlDate` | string | No | Filter results crawled after this date \(ISO 8601 format\) | +| `endCrawlDate` | string | No | Filter results crawled before this date \(ISO 8601 format\) | +| `category` | string | No | Filter by category: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site | +| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) | +| `summary` | boolean | No | Include AI-generated summaries in results \(default: false\) | +| `livecrawl` | string | No | Live crawling mode: always, fallback, or never \(default: never\) | | `apiKey` | string | Yes | Exa AI API Key | #### Output @@ -136,7 +162,7 @@ Perform comprehensive research using AI to generate detailed reports with citati | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | Yes | Research query or topic | -| `includeText` | boolean | No | Include full text content in results | +| `model` | string | No | Research model: exa-research-fast, exa-research \(default\), or exa-research-pro | | `apiKey` | string | Yes | Exa AI API Key | #### Output diff --git a/apps/docs/content/docs/en/tools/firecrawl.mdx b/apps/docs/content/docs/en/tools/firecrawl.mdx index 4aa221f9fb..ebcedd0039 100644 --- a/apps/docs/content/docs/en/tools/firecrawl.mdx +++ b/apps/docs/content/docs/en/tools/firecrawl.mdx @@ -1,6 +1,6 @@ --- title: Firecrawl -description: Scrape or search the web +description: Scrape, search, crawl, map, and extract web data --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -59,7 +59,7 @@ This allows your agents to gather information from websites, extract structured ## Usage Instructions -Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites. +Integrate Firecrawl into the workflow. Can scrape pages, search the web, crawl entire websites, map URL structures, and extract structured data using AI. @@ -74,7 +74,25 @@ Extract structured content from web pages with comprehensive metadata support. C | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `url` | string | Yes | The URL to scrape content from | -| `scrapeOptions` | json | No | Options for content scraping | +| `formats` | json | No | Output formats \(markdown, html, rawHtml, links, images, screenshot\). Default: \["markdown"\] | +| `onlyMainContent` | boolean | No | Extract only main content, excluding headers, navs, footers \(default: true\) | +| `includeTags` | json | No | HTML tags to retain in the output | +| `excludeTags` | json | No | HTML tags to remove from the output | +| `maxAge` | number | No | Return cached version if younger than this age in ms \(default: 172800000\) | +| `headers` | json | No | Custom request headers \(cookies, user-agent, etc.\) | +| `waitFor` | number | No | Delay in milliseconds before fetching \(default: 0\) | +| `mobile` | boolean | No | Emulate mobile device \(default: false\) | +| `skipTlsVerification` | boolean | No | Skip TLS certificate verification \(default: true\) | +| `timeout` | number | No | Request timeout in milliseconds | +| `parsers` | json | No | File processing controls \(e.g., \["pdf"\]\) | +| `actions` | json | No | Pre-scrape operations \(wait, click, scroll, screenshot, etc.\) | +| `location` | json | No | Geographic settings \(country, languages\) | +| `removeBase64Images` | boolean | No | Strip base64 images from output \(default: true\) | +| `blockAds` | boolean | No | Enable ad and popup blocking \(default: true\) | +| `proxy` | string | No | Proxy type: basic, stealth, or auto \(default: auto\) | +| `storeInCache` | boolean | No | Cache the page \(default: true\) | +| `zeroDataRetention` | boolean | No | Enable zero data retention mode \(default: false\) | +| `scrapeOptions` | json | No | Options for content scraping \(legacy, prefer top-level params\) | | `apiKey` | string | Yes | Firecrawl API key | #### Output @@ -94,6 +112,15 @@ Search for information on the web using Firecrawl | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `query` | string | Yes | The search query to use | +| `limit` | number | No | Maximum number of results to return \(1-100, default: 5\) | +| `sources` | json | No | Search sources: \["web"\], \["images"\], or \["news"\] \(default: \["web"\]\) | +| `categories` | json | No | Filter by categories: \["github"\], \["research"\], or \["pdf"\] | +| `tbs` | string | No | Time-based search: qdr:h \(hour\), qdr:d \(day\), qdr:w \(week\), qdr:m \(month\), qdr:y \(year\) | +| `location` | string | No | Geographic location for results \(e.g., "San Francisco, California, United States"\) | +| `country` | string | No | ISO country code for geo-targeting \(default: US\) | +| `timeout` | number | No | Timeout in milliseconds \(default: 60000\) | +| `ignoreInvalidURLs` | boolean | No | Exclude invalid URLs from results \(default: false\) | +| `scrapeOptions` | json | No | Advanced scraping configuration for search results | | `apiKey` | string | Yes | Firecrawl API key | #### Output @@ -113,6 +140,20 @@ Crawl entire websites and extract structured content from all accessible pages | `url` | string | Yes | The website URL to crawl | | `limit` | number | No | Maximum number of pages to crawl \(default: 100\) | | `onlyMainContent` | boolean | No | Extract only main content from pages | +| `prompt` | string | No | Natural language instruction to auto-generate crawler options | +| `maxDiscoveryDepth` | number | No | Depth limit for URL discovery \(root pages have depth 0\) | +| `sitemap` | string | No | Whether to use sitemap data: "skip" or "include" \(default: "include"\) | +| `crawlEntireDomain` | boolean | No | Follow sibling/parent URLs or only child paths \(default: false\) | +| `allowExternalLinks` | boolean | No | Follow external website links \(default: false\) | +| `allowSubdomains` | boolean | No | Follow subdomain links \(default: false\) | +| `ignoreQueryParameters` | boolean | No | Prevent re-scraping same path with different query params \(default: false\) | +| `delay` | number | No | Seconds between scrapes for rate limit compliance | +| `maxConcurrency` | number | No | Concurrent scrape limit | +| `excludePaths` | json | No | Array of regex patterns for URLs to exclude | +| `includePaths` | json | No | Array of regex patterns for URLs to include exclusively | +| `webhook` | json | No | Webhook configuration for crawl notifications | +| `scrapeOptions` | json | No | Advanced scraping configuration | +| `zeroDataRetention` | boolean | No | Enable zero data retention \(default: false\) | | `apiKey` | string | Yes | Firecrawl API Key | #### Output @@ -121,6 +162,58 @@ Crawl entire websites and extract structured content from all accessible pages | --------- | ---- | ----------- | | `pages` | array | Array of crawled pages with their content and metadata | +### `firecrawl_map` + +Get a complete list of URLs from any website quickly and reliably. Useful for discovering all pages on a site without crawling them. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `url` | string | Yes | The base URL to map and discover links from | +| `search` | string | No | Filter results by relevance to a search term \(e.g., "blog"\) | +| `sitemap` | string | No | Controls sitemap usage: "skip", "include" \(default\), or "only" | +| `includeSubdomains` | boolean | No | Whether to include URLs from subdomains \(default: true\) | +| `ignoreQueryParameters` | boolean | No | Exclude URLs containing query strings \(default: true\) | +| `limit` | number | No | Maximum number of links to return \(max: 100,000, default: 5,000\) | +| `timeout` | number | No | Request timeout in milliseconds | +| `location` | json | No | Geographic context for proxying \(country, languages\) | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the mapping operation was successful | +| `links` | array | Array of discovered URLs from the website | + +### `firecrawl_extract` + +Extract structured data from entire webpages using natural language prompts and JSON schema. Powerful agentic feature for intelligent data extraction. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `urls` | json | Yes | Array of URLs to extract data from \(supports glob format\) | +| `prompt` | string | No | Natural language guidance for the extraction process | +| `schema` | json | No | JSON Schema defining the structure of data to extract | +| `enableWebSearch` | boolean | No | Enable web search to find supplementary information \(default: false\) | +| `ignoreSitemap` | boolean | No | Ignore sitemap.xml files during scanning \(default: false\) | +| `includeSubdomains` | boolean | No | Extend scanning to subdomains \(default: true\) | +| `showSources` | boolean | No | Return data sources in the response \(default: false\) | +| `ignoreInvalidURLs` | boolean | No | Skip invalid URLs in the array \(default: true\) | +| `scrapeOptions` | json | No | Advanced scraping configuration options | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the extraction operation was successful | +| `data` | object | Extracted structured data according to the schema or prompt | +| `sources` | array | Data sources \(only if showSources is enabled\) | + ## Notes diff --git a/apps/docs/content/docs/en/tools/jina.mdx b/apps/docs/content/docs/en/tools/jina.mdx index 665f2ebe2a..8a52dbfd70 100644 --- a/apps/docs/content/docs/en/tools/jina.mdx +++ b/apps/docs/content/docs/en/tools/jina.mdx @@ -1,6 +1,6 @@ --- title: Jina -description: Convert website content into text +description: Search the web or extract content from URLs --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -63,7 +63,7 @@ This integration is particularly valuable for building agents that need to gathe ## Usage Instructions -Integrate Jina into the workflow. Extracts content from websites. +Integrate Jina AI into the workflow. Search the web and get LLM-friendly results, or extract clean content from specific URLs with advanced parsing options. @@ -78,16 +78,76 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `url` | string | Yes | The URL to read and convert to markdown | -| `useReaderLMv2` | boolean | No | Whether to use ReaderLM-v2 for better quality | +| `useReaderLMv2` | boolean | No | Whether to use ReaderLM-v2 for better quality \(3x token cost\) | | `gatherLinks` | boolean | No | Whether to gather all links at the end | | `jsonResponse` | boolean | No | Whether to return response in JSON format | | `apiKey` | string | Yes | Your Jina AI API key | +| `targetSelector` | string | No | CSS selector to target specific page elements \(e.g., "#main-content"\) | +| `waitForSelector` | string | No | CSS selector to wait for before extracting content \(useful for dynamic pages\) | +| `removeSelector` | string | No | CSS selector for elements to exclude \(e.g., "header, footer, .ad"\) | +| `timeout` | number | No | Maximum seconds to wait for page load | +| `withImagesummary` | boolean | No | Gather all images from the page with metadata | +| `retainImages` | string | No | Control image inclusion: "none" removes all, "all" keeps all | +| `returnFormat` | string | No | Output format: markdown, html, text, screenshot, or pageshot | +| `withIframe` | boolean | No | Include iframe content in extraction | +| `withShadowDom` | boolean | No | Extract Shadow DOM content | +| `setCookie` | string | No | Forward authentication cookies \(disables caching\) | +| `proxyUrl` | string | No | HTTP proxy URL for request routing | +| `proxy` | string | No | Country code for proxy \(e.g., "US", "UK"\) or "auto"/"none" | +| `engine` | string | No | Rendering engine: browser, direct, or cf-browser-rendering | +| `tokenBudget` | number | No | Maximum tokens for the request \(cost control\) | +| `noCache` | boolean | No | Bypass cached content for real-time retrieval | +| `cacheTolerance` | number | No | Custom cache lifetime in seconds | +| `withGeneratedAlt` | boolean | No | Generate alt text for images using VLM | +| `baseUrl` | string | No | Set to "final" to follow redirect chain | +| `locale` | string | No | Browser locale for rendering \(e.g., "en-US"\) | +| `robotsTxt` | string | No | Bot User-Agent for robots.txt checking | +| `dnt` | boolean | No | Do Not Track - prevents caching/tracking | +| `noGfm` | boolean | No | Disable GitHub Flavored Markdown | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | | `content` | string | The extracted content from the URL, processed into clean, LLM-friendly text | +| `links` | array | List of links found on the page \(when gatherLinks or withLinksummary is enabled\) | +| `images` | array | List of images found on the page \(when withImagesummary is enabled\) | + +### `jina_search` + +Search the web and return top 5 results with LLM-friendly content. Each result is automatically processed through Jina Reader API. Supports geographic filtering, site restrictions, and pagination. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `q` | string | Yes | Search query string | +| `apiKey` | string | Yes | Your Jina AI API key | +| `gl` | string | No | Two-letter country code for geo-specific results \(e.g., "US", "UK", "JP"\) | +| `location` | string | No | City-level location for localized search results | +| `hl` | string | No | Two-letter language code for results \(e.g., "en", "es", "fr"\) | +| `num` | number | No | Maximum number of results per page \(default: 5\) | +| `page` | number | No | Page number for pagination \(offset\) | +| `site` | string | No | Restrict results to specific domain\(s\). Can be comma-separated for multiple sites \(e.g., "jina.ai,github.com"\) | +| `withFavicon` | boolean | No | Include website favicons in results | +| `withImagesummary` | boolean | No | Gather all images from result pages with metadata | +| `withLinksummary` | boolean | No | Gather all links from result pages | +| `retainImages` | string | No | Control image inclusion: "none" removes all, "all" keeps all | +| `noCache` | boolean | No | Bypass cached content for real-time retrieval | +| `withGeneratedAlt` | boolean | No | Generate alt text for images using VLM | +| `respondWith` | string | No | Set to "no-content" to get only metadata without page content | +| `returnFormat` | string | No | Output format: markdown, html, text, screenshot, or pageshot | +| `engine` | string | No | Rendering engine: browser or direct | +| `timeout` | number | No | Maximum seconds to wait for page load | +| `setCookie` | string | No | Forward authentication cookies | +| `proxyUrl` | string | No | HTTP proxy URL for request routing | +| `locale` | string | No | Browser locale for rendering \(e.g., "en-US"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Array of search results, each containing title, description, url, and LLM-friendly content | diff --git a/apps/docs/content/docs/en/tools/jira.mdx b/apps/docs/content/docs/en/tools/jira.mdx index 5a01a1cf84..bee499f641 100644 --- a/apps/docs/content/docs/en/tools/jira.mdx +++ b/apps/docs/content/docs/en/tools/jira.mdx @@ -137,6 +137,376 @@ Retrieve multiple Jira issues in bulk | `success` | boolean | Operation success status | | `output` | array | Array of Jira issues with summary, description, created and updated timestamps | +### `jira_delete_issue` + +Delete a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to delete \(e.g., PROJ-123\) | +| `deleteSubtasks` | boolean | No | Whether to delete subtasks. If false, parent issues with subtasks cannot be deleted. | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Deleted issue details with timestamp, issue key, and success status | + +### `jira_assign_issue` + +Assign a Jira issue to a user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to assign \(e.g., PROJ-123\) | +| `accountId` | string | Yes | Account ID of the user to assign the issue to. Use "-1" for automatic assignment or null to unassign. | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Assignment details with timestamp, issue key, assignee ID, and success status | + +### `jira_transition_issue` + +Move a Jira issue between workflow statuses (e.g., To Do -> In Progress) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to transition \(e.g., PROJ-123\) | +| `transitionId` | string | Yes | ID of the transition to execute \(e.g., "11" for "To Do", "21" for "In Progress"\) | +| `comment` | string | No | Optional comment to add when transitioning the issue | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Transition details with timestamp, issue key, transition ID, and success status | + +### `jira_search_issues` + +Search for Jira issues using JQL (Jira Query Language) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `jql` | string | Yes | JQL query string to search for issues \(e.g., "project = PROJ AND status = Open"\) | +| `startAt` | number | No | The index of the first result to return \(for pagination\) | +| `maxResults` | number | No | Maximum number of results to return \(default: 50\) | +| `fields` | array | No | Array of field names to return \(default: \['summary', 'status', 'assignee', 'created', 'updated'\]\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Search results with timestamp, total count, pagination details, and array of matching issues | + +### `jira_add_comment` + +Add a comment to a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to add comment to \(e.g., PROJ-123\) | +| `body` | string | Yes | Comment body text | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Comment details with timestamp, issue key, comment ID, body, and success status | + +### `jira_get_comments` + +Get all comments from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to get comments from \(e.g., PROJ-123\) | +| `startAt` | number | No | Index of the first comment to return \(default: 0\) | +| `maxResults` | number | No | Maximum number of comments to return \(default: 50\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Comments data with timestamp, issue key, total count, and array of comments | + +### `jira_update_comment` + +Update an existing comment on a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key containing the comment \(e.g., PROJ-123\) | +| `commentId` | string | Yes | ID of the comment to update | +| `body` | string | Yes | Updated comment text | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Updated comment details with timestamp, issue key, comment ID, body text, and success status | + +### `jira_delete_comment` + +Delete a comment from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key containing the comment \(e.g., PROJ-123\) | +| `commentId` | string | Yes | ID of the comment to delete | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Deletion details with timestamp, issue key, comment ID, and success status | + +### `jira_get_attachments` + +Get all attachments from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to get attachments from \(e.g., PROJ-123\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Attachments data with timestamp, issue key, and array of attachments | + +### `jira_delete_attachment` + +Delete an attachment from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `attachmentId` | string | Yes | ID of the attachment to delete | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Deletion details with timestamp, attachment ID, and success status | + +### `jira_add_worklog` + +Add a time tracking worklog entry to a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to add worklog to \(e.g., PROJ-123\) | +| `timeSpentSeconds` | number | Yes | Time spent in seconds | +| `comment` | string | No | Optional comment for the worklog entry | +| `started` | string | No | Optional start time in ISO format \(defaults to current time\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Worklog details with timestamp, issue key, worklog ID, time spent in seconds, and success status | + +### `jira_get_worklogs` + +Get all worklog entries from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to get worklogs from \(e.g., PROJ-123\) | +| `startAt` | number | No | Index of the first worklog to return \(default: 0\) | +| `maxResults` | number | No | Maximum number of worklogs to return \(default: 50\) | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Worklogs data with timestamp, issue key, total count, and array of worklogs | + +### `jira_update_worklog` + +Update an existing worklog entry on a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key containing the worklog \(e.g., PROJ-123\) | +| `worklogId` | string | Yes | ID of the worklog entry to update | +| `timeSpentSeconds` | number | No | Time spent in seconds | +| `comment` | string | No | Optional comment for the worklog entry | +| `started` | string | No | Optional start time in ISO format | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Worklog update details with timestamp, issue key, worklog ID, and success status | + +### `jira_delete_worklog` + +Delete a worklog entry from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key containing the worklog \(e.g., PROJ-123\) | +| `worklogId` | string | Yes | ID of the worklog entry to delete | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Deletion details with timestamp, issue key, worklog ID, and success status | + +### `jira_create_issue_link` + +Create a link relationship between two Jira issues + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `inwardIssueKey` | string | Yes | Jira issue key for the inward issue \(e.g., PROJ-123\) | +| `outwardIssueKey` | string | Yes | Jira issue key for the outward issue \(e.g., PROJ-456\) | +| `linkType` | string | Yes | The type of link relationship \(e.g., "Blocks", "Relates to", "Duplicates"\) | +| `comment` | string | No | Optional comment to add to the issue link | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Issue link details with timestamp, inward issue key, outward issue key, link type, and success status | + +### `jira_delete_issue_link` + +Delete a link between two Jira issues + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `linkId` | string | Yes | ID of the issue link to delete | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Deletion details with timestamp, link ID, and success status | + +### `jira_add_watcher` + +Add a watcher to a Jira issue to receive notifications about updates + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to add watcher to \(e.g., PROJ-123\) | +| `accountId` | string | Yes | Account ID of the user to add as watcher | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Watcher details with timestamp, issue key, watcher account ID, and success status | + +### `jira_remove_watcher` + +Remove a watcher from a Jira issue + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) | +| `issueKey` | string | Yes | Jira issue key to remove watcher from \(e.g., PROJ-123\) | +| `accountId` | string | Yes | Account ID of the user to remove as watcher | +| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Operation success status | +| `output` | object | Removal details with timestamp, issue key, watcher account ID, and success status | + ## Notes diff --git a/apps/docs/content/docs/en/tools/linear.mdx b/apps/docs/content/docs/en/tools/linear.mdx index be2fd68940..6f1fd3f80a 100644 --- a/apps/docs/content/docs/en/tools/linear.mdx +++ b/apps/docs/content/docs/en/tools/linear.mdx @@ -1,6 +1,6 @@ --- title: Linear -description: Read and create issues in Linear +description: Interact with Linear issues, projects, and more --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -42,7 +42,7 @@ In Sim, the Linear integration allows your agents to seamlessly interact with yo ## Usage Instructions -Integrate Linear into the workflow. Can read and create issues. +Integrate Linear into the workflow. Can manage issues, comments, projects, labels, workflow states, cycles, attachments, and more. @@ -65,6 +65,22 @@ Fetch and filter issues from Linear | --------- | ---- | ----------- | | `issues` | array | Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId | +### `linear_get_issue` + +Get a single issue by ID from Linear with full details + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issue` | object | The issue with full details | + ### `linear_create_issue` Create a new issue in Linear @@ -84,6 +100,797 @@ Create a new issue in Linear | --------- | ---- | ----------- | | `issue` | object | The created issue containing id, title, description, state, teamId, and projectId | +### `linear_update_issue` + +Update an existing issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID to update | +| `title` | string | No | New issue title | +| `description` | string | No | New issue description | +| `stateId` | string | No | Workflow state ID \(status\) | +| `assigneeId` | string | No | User ID to assign the issue to | +| `priority` | number | No | Priority \(0=No priority, 1=Urgent, 2=High, 3=Normal, 4=Low\) | +| `estimate` | number | No | Estimate in points | +| `labelIds` | array | No | Array of label IDs to set on the issue | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issue` | object | The updated issue | + +### `linear_archive_issue` + +Archive an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID to archive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the archive operation was successful | +| `issueId` | string | The ID of the archived issue | + +### `linear_unarchive_issue` + +Unarchive (restore) an archived issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID to unarchive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the unarchive operation was successful | +| `issueId` | string | The ID of the unarchived issue | + +### `linear_delete_issue` + +Delete (trash) an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the delete operation was successful | + +### `linear_search_issues` + +Search for issues in Linear using full-text search + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | Yes | Search query string | +| `teamId` | string | No | Filter by team ID | +| `includeArchived` | boolean | No | Include archived issues in search results | +| `first` | number | No | Number of results to return \(default: 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `issues` | array | Array of matching issues | + +### `linear_add_label_to_issue` + +Add a label to an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID | +| `labelId` | string | Yes | Label ID to add to the issue | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the label was successfully added | +| `issueId` | string | The ID of the issue | + +### `linear_remove_label_from_issue` + +Remove a label from an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID | +| `labelId` | string | Yes | Label ID to remove from the issue | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the label was successfully removed | +| `issueId` | string | The ID of the issue | + +### `linear_create_comment` + +Add a comment to an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID to comment on | +| `body` | string | Yes | Comment text \(supports Markdown\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | object | The created comment | + +### `linear_update_comment` + +Edit a comment in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `commentId` | string | Yes | Comment ID to update | +| `body` | string | Yes | New comment text \(supports Markdown\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | object | The updated comment | + +### `linear_delete_comment` + +Delete a comment from Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `commentId` | string | Yes | Comment ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the delete operation was successful | + +### `linear_list_comments` + +List all comments on an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Linear issue ID | +| `first` | number | No | Number of comments to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comments` | array | Array of comments on the issue | + +### `linear_list_projects` + +List projects in Linear with optional filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | Filter by team ID | +| `includeArchived` | boolean | No | Include archived projects | +| `first` | number | No | Number of projects to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `projects` | array | Array of projects | + +### `linear_get_project` + +Get a single project by ID from Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Linear project ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `project` | object | The project with full details | + +### `linear_create_project` + +Create a new project in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | Team ID to create the project in | +| `name` | string | Yes | Project name | +| `description` | string | No | Project description | +| `leadId` | string | No | User ID of the project lead | +| `startDate` | string | No | Project start date \(ISO format\) | +| `targetDate` | string | No | Project target date \(ISO format\) | +| `priority` | number | No | Project priority \(0-4\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `project` | object | The created project | + +### `linear_update_project` + +Update an existing project in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID to update | +| `name` | string | No | New project name | +| `description` | string | No | New project description | +| `state` | string | No | Project state \(planned, started, completed, canceled\) | +| `leadId` | string | No | User ID of the project lead | +| `startDate` | string | No | Project start date \(ISO format\) | +| `targetDate` | string | No | Project target date \(ISO format\) | +| `priority` | number | No | Project priority \(0-4\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `project` | object | The updated project | + +### `linear_archive_project` + +Archive a project in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID to archive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the archive operation was successful | +| `projectId` | string | The ID of the archived project | + +### `linear_list_users` + +List all users in the Linear workspace + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `includeDisabled` | boolean | No | Include disabled/inactive users | +| `first` | number | No | Number of users to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of workspace users | + +### `linear_list_teams` + +List all teams in the Linear workspace + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of teams to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `teams` | array | Array of teams | + +### `linear_get_viewer` + +Get the currently authenticated user (viewer) information + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `user` | object | The currently authenticated user | + +### `linear_list_labels` + +List all labels in Linear workspace or team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | Filter by team ID | +| `first` | number | No | Number of labels to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `labels` | array | Array of labels | + +### `linear_create_label` + +Create a new label in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | Yes | Label name | +| `color` | string | No | Label color \(hex format, e.g., "#ff0000"\) | +| `description` | string | No | Label description | +| `teamId` | string | No | Team ID \(if omitted, creates workspace label\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `label` | object | The created label | + +### `linear_update_label` + +Update an existing label in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `labelId` | string | Yes | Label ID to update | +| `name` | string | No | New label name | +| `color` | string | No | New label color \(hex format\) | +| `description` | string | No | New label description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `label` | object | The updated label | + +### `linear_archive_label` + +Archive a label in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `labelId` | string | Yes | Label ID to archive | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the archive operation was successful | +| `labelId` | string | The ID of the archived label | + +### `linear_list_workflow_states` + +List all workflow states (statuses) in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | Filter by team ID | +| `first` | number | No | Number of states to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `states` | array | Array of workflow states | + +### `linear_create_workflow_state` + +Create a new workflow state (status) in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | Team ID to create the state in | +| `name` | string | Yes | State name \(e.g., "In Review"\) | +| `color` | string | Yes | State color \(hex format\) | +| `type` | string | Yes | State type: "backlog", "unstarted", "started", "completed", or "canceled" | +| `description` | string | No | State description | +| `position` | number | No | Position in the workflow | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `state` | object | The created workflow state | + +### `linear_update_workflow_state` + +Update an existing workflow state in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `stateId` | string | Yes | Workflow state ID to update | +| `name` | string | No | New state name | +| `color` | string | No | New state color \(hex format\) | +| `description` | string | No | New state description | +| `position` | number | No | New position in workflow | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `state` | object | The updated workflow state | + +### `linear_list_cycles` + +List cycles (sprints/iterations) in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | Filter by team ID | +| `first` | number | No | Number of cycles to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cycles` | array | Array of cycles | + +### `linear_get_cycle` + +Get a single cycle by ID from Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cycleId` | string | Yes | Cycle ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cycle` | object | The cycle with full details | + +### `linear_create_cycle` + +Create a new cycle (sprint/iteration) in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | Team ID to create the cycle in | +| `startsAt` | string | Yes | Cycle start date \(ISO format\) | +| `endsAt` | string | Yes | Cycle end date \(ISO format\) | +| `name` | string | No | Cycle name \(optional, will be auto-generated if not provided\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cycle` | object | The created cycle | + +### `linear_get_active_cycle` + +Get the currently active cycle for a team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | Team ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `cycle` | object | The active cycle \(null if no active cycle\) | + +### `linear_create_attachment` + +Add an attachment to an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Issue ID to attach to | +| `url` | string | Yes | URL of the attachment | +| `title` | string | No | Attachment title | +| `subtitle` | string | No | Attachment subtitle/description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachment` | object | The created attachment | + +### `linear_list_attachments` + +List all attachments on an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Issue ID | +| `first` | number | No | Number of attachments to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachments` | array | Array of attachments | + +### `linear_update_attachment` + +Update an attachment metadata in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `attachmentId` | string | Yes | Attachment ID to update | +| `title` | string | No | New attachment title | +| `subtitle` | string | No | New attachment subtitle | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attachment` | object | The updated attachment | + +### `linear_delete_attachment` + +Delete an attachment from Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `attachmentId` | string | Yes | Attachment ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the delete operation was successful | + +### `linear_create_issue_relation` + +Link two issues together in Linear (blocks, relates to, duplicates) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Source issue ID | +| `relatedIssueId` | string | Yes | Target issue ID to link to | +| `type` | string | Yes | Relation type: "blocks", "blocked", "duplicate", "related" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `relation` | object | The created issue relation | + +### `linear_list_issue_relations` + +List all relations (dependencies) for an issue in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | Yes | Issue ID | +| `first` | number | No | Number of relations to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `relations` | array | Array of issue relations | + +### `linear_delete_issue_relation` + +Remove a relation between two issues in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `relationId` | string | Yes | Relation ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the delete operation was successful | + +### `linear_create_favorite` + +Bookmark an issue, project, cycle, or label in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `issueId` | string | No | Issue ID to favorite | +| `projectId` | string | No | Project ID to favorite | +| `cycleId` | string | No | Cycle ID to favorite | +| `labelId` | string | No | Label ID to favorite | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `favorite` | object | The created favorite | + +### `linear_list_favorites` + +List all bookmarked items for the current user in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of favorites to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `favorites` | array | Array of favorited items | + +### `linear_create_project_update` + +Post a status update for a project in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID to post update for | +| `body` | string | Yes | Update message \(supports Markdown\) | +| `health` | string | No | Project health: "onTrack", "atRisk", or "offTrack" | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `update` | object | The created project update | + +### `linear_list_project_updates` + +List all status updates for a project in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID | +| `first` | number | No | Number of updates to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `updates` | array | Array of project updates | + +### `linear_create_project_link` + +Add an external link to a project in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Project ID to add link to | +| `url` | string | Yes | URL of the external link | +| `label` | string | No | Link label/title | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `link` | object | The created project link | + +### `linear_list_notifications` + +List notifications for the current user in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of notifications to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `notifications` | array | Array of notifications | + +### `linear_update_notification` + +Mark a notification as read or unread in Linear + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `notificationId` | string | Yes | Notification ID to update | +| `readAt` | string | No | Timestamp to mark as read \(ISO format\). Pass null or omit to mark as unread | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `notification` | object | The updated notification | + ## Notes diff --git a/apps/docs/content/docs/en/tools/linkup.mdx b/apps/docs/content/docs/en/tools/linkup.mdx index 817e1ca9be..87b305aa87 100644 --- a/apps/docs/content/docs/en/tools/linkup.mdx +++ b/apps/docs/content/docs/en/tools/linkup.mdx @@ -59,8 +59,15 @@ Search the web for information using Linkup | --------- | ---- | -------- | ----------- | | `q` | string | Yes | The search query | | `depth` | string | Yes | Search depth \(has to either be "standard" or "deep"\) | -| `outputType` | string | Yes | Type of output to return \(has to either be "sourcedAnswer" or "searchResults"\) | +| `outputType` | string | Yes | Type of output to return \(has to be "sourcedAnswer" or "searchResults"\) | | `apiKey` | string | Yes | Enter your Linkup API key | +| `includeImages` | boolean | No | Whether to include images in search results | +| `fromDate` | string | No | Start date for filtering results \(YYYY-MM-DD format\) | +| `toDate` | string | No | End date for filtering results \(YYYY-MM-DD format\) | +| `excludeDomains` | string | No | Comma-separated list of domain names to exclude from search results | +| `includeDomains` | string | No | Comma-separated list of domain names to restrict search results to | +| `includeInlineCitations` | boolean | No | Add inline citations to answers \(only applies when outputType is "sourcedAnswer"\) | +| `includeSources` | boolean | No | Include sources in response | #### Output diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 7884c39901..fb53e3758b 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -57,6 +57,7 @@ "sms", "stagehand", "stagehand_agent", + "stripe", "supabase", "tavily", "telegram", diff --git a/apps/docs/content/docs/en/tools/microsoft_planner.mdx b/apps/docs/content/docs/en/tools/microsoft_planner.mdx index bba5203eb2..90c2e5cabc 100644 --- a/apps/docs/content/docs/en/tools/microsoft_planner.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_planner.mdx @@ -1,6 +1,6 @@ --- title: Microsoft Planner -description: Read and create tasks in Microsoft Planner +description: Manage tasks, plans, and buckets in Microsoft Planner --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -122,7 +122,7 @@ In Sim, the Microsoft Planner integration allows your agents to programmatically ## Usage Instructions -Integrate Microsoft Planner into the workflow. Can read and create tasks. +Integrate Microsoft Planner into the workflow. Manage tasks, plans, buckets, and task details including checklists and references. @@ -170,6 +170,222 @@ Create a new task in Microsoft Planner | `task` | object | The created task object with all properties | | `metadata` | object | Metadata including planId, taskId, and taskUrl | +### `microsoft_planner_update_task` + +Update a task in Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | The ID of the task to update | +| `etag` | string | Yes | The ETag value from the task to update \(If-Match header\) | +| `title` | string | No | The new title of the task | +| `bucketId` | string | No | The bucket ID to move the task to | +| `dueDateTime` | string | No | The due date and time for the task \(ISO 8601 format\) | +| `startDateTime` | string | No | The start date and time for the task \(ISO 8601 format\) | +| `percentComplete` | number | No | The percentage of task completion \(0-100\) | +| `priority` | number | No | The priority of the task \(0-10\) | +| `assigneeUserId` | string | No | The user ID to assign the task to | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the task was updated successfully | +| `task` | object | The updated task object with all properties | +| `metadata` | object | Metadata including taskId, planId, and taskUrl | + +### `microsoft_planner_delete_task` + +Delete a task from Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | The ID of the task to delete | +| `etag` | string | Yes | The ETag value from the task to delete \(If-Match header\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the task was deleted successfully | +| `deleted` | boolean | Confirmation of deletion | +| `metadata` | object | Additional metadata | + +### `microsoft_planner_list_plans` + +List all plans in a Microsoft 365 group + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupId` | string | Yes | The ID of the Microsoft 365 group | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether plans were retrieved successfully | +| `plans` | array | Array of plan objects | +| `metadata` | object | Metadata including groupId and count | + +### `microsoft_planner_read_plan` + +Get details of a specific Microsoft Planner plan + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the plan was retrieved successfully | +| `plan` | object | The plan object with all properties | +| `metadata` | object | Metadata including planId and planUrl | + +### `microsoft_planner_list_buckets` + +List all buckets in a Microsoft Planner plan + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether buckets were retrieved successfully | +| `buckets` | array | Array of bucket objects | +| `metadata` | object | Metadata including planId and count | + +### `microsoft_planner_read_bucket` + +Get details of a specific bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `bucketId` | string | Yes | The ID of the bucket to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the bucket was retrieved successfully | +| `bucket` | object | The bucket object with all properties | +| `metadata` | object | Metadata including bucketId and planId | + +### `microsoft_planner_create_bucket` + +Create a new bucket in a Microsoft Planner plan + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan where the bucket will be created | +| `name` | string | Yes | The name of the bucket | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the bucket was created successfully | +| `bucket` | object | The created bucket object with all properties | +| `metadata` | object | Metadata including bucketId and planId | + +### `microsoft_planner_update_bucket` + +Update a bucket in Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `bucketId` | string | Yes | The ID of the bucket to update | +| `name` | string | No | The new name of the bucket | +| `etag` | string | Yes | The ETag value from the bucket to update \(If-Match header\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the bucket was updated successfully | +| `bucket` | object | The updated bucket object with all properties | +| `metadata` | object | Metadata including bucketId and planId | + +### `microsoft_planner_delete_bucket` + +Delete a bucket from Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `bucketId` | string | Yes | The ID of the bucket to delete | +| `etag` | string | Yes | The ETag value from the bucket to delete \(If-Match header\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the bucket was deleted successfully | +| `deleted` | boolean | Confirmation of deletion | +| `metadata` | object | Additional metadata | + +### `microsoft_planner_get_task_details` + +Get detailed information about a task including checklist and references + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | The ID of the task | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the task details were retrieved successfully | +| `taskDetails` | object | The task details including description, checklist, and references | +| `metadata` | object | Metadata including taskId | + +### `microsoft_planner_update_task_details` + +Update task details including description, checklist items, and references in Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `taskId` | string | Yes | The ID of the task | +| `etag` | string | Yes | The ETag value from the task details to update \(If-Match header\) | +| `description` | string | No | The description of the task | +| `checklist` | object | No | Checklist items as a JSON object | +| `references` | object | No | References as a JSON object | +| `previewType` | string | No | Preview type: automatic, noPreview, checklist, description, or reference | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the task details were updated successfully | +| `taskDetails` | object | The updated task details object with all properties | +| `metadata` | object | Metadata including taskId | + ## Notes diff --git a/apps/docs/content/docs/en/tools/microsoft_teams.mdx b/apps/docs/content/docs/en/tools/microsoft_teams.mdx index 47eaa424c5..040e2357b4 100644 --- a/apps/docs/content/docs/en/tools/microsoft_teams.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_teams.mdx @@ -1,6 +1,6 @@ --- title: Microsoft Teams -description: Read, write, and create messages +description: Manage messages, reactions, and members in Teams --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -98,7 +98,7 @@ In Sim, the Microsoft Teams integration enables your agents to interact directly ## Usage Instructions -Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName` +Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list team/channel members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName` @@ -202,6 +202,209 @@ Write or send a message to a Microsoft Teams channel | `url` | string | Web URL to the message | | `updatedContent` | boolean | Whether content was successfully updated | +### `microsoft_teams_update_chat_message` + +Update an existing message in a Microsoft Teams chat + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `chatId` | string | Yes | The ID of the chat containing the message | +| `messageId` | string | Yes | The ID of the message to update | +| `content` | string | Yes | The new content for the message | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the update was successful | +| `messageId` | string | ID of the updated message | +| `updatedContent` | boolean | Whether content was successfully updated | + +### `microsoft_teams_update_channel_message` + +Update an existing message in a Microsoft Teams channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | The ID of the team | +| `channelId` | string | Yes | The ID of the channel containing the message | +| `messageId` | string | Yes | The ID of the message to update | +| `content` | string | Yes | The new content for the message | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the update was successful | +| `messageId` | string | ID of the updated message | +| `updatedContent` | boolean | Whether content was successfully updated | + +### `microsoft_teams_delete_chat_message` + +Soft delete a message in a Microsoft Teams chat + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `chatId` | string | Yes | The ID of the chat containing the message | +| `messageId` | string | Yes | The ID of the message to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the deletion was successful | +| `deleted` | boolean | Confirmation of deletion | +| `messageId` | string | ID of the deleted message | + +### `microsoft_teams_delete_channel_message` + +Soft delete a message in a Microsoft Teams channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | The ID of the team | +| `channelId` | string | Yes | The ID of the channel containing the message | +| `messageId` | string | Yes | The ID of the message to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the deletion was successful | +| `deleted` | boolean | Confirmation of deletion | +| `messageId` | string | ID of the deleted message | + +### `microsoft_teams_reply_to_message` + +Reply to an existing message in a Microsoft Teams channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | The ID of the team | +| `channelId` | string | Yes | The ID of the channel | +| `messageId` | string | Yes | The ID of the message to reply to | +| `content` | string | Yes | The reply content | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the reply was successful | +| `messageId` | string | ID of the reply message | +| `updatedContent` | boolean | Whether content was successfully sent | + +### `microsoft_teams_get_message` + +Get a specific message from a Microsoft Teams chat or channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | The ID of the team \(for channel messages\) | +| `channelId` | string | No | The ID of the channel \(for channel messages\) | +| `chatId` | string | No | The ID of the chat \(for chat messages\) | +| `messageId` | string | Yes | The ID of the message to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the retrieval was successful | +| `content` | string | The message content | +| `metadata` | object | Message metadata including sender, timestamp, etc. | + +### `microsoft_teams_set_reaction` + +Add an emoji reaction to a message in Microsoft Teams + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | The ID of the team \(for channel messages\) | +| `channelId` | string | No | The ID of the channel \(for channel messages\) | +| `chatId` | string | No | The ID of the chat \(for chat messages\) | +| `messageId` | string | Yes | The ID of the message to react to | +| `reactionType` | string | Yes | The emoji reaction \(e.g., ❤️, 👍, 😊\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the reaction was added successfully | +| `reactionType` | string | The emoji that was added | +| `messageId` | string | ID of the message | + +### `microsoft_teams_unset_reaction` + +Remove an emoji reaction from a message in Microsoft Teams + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | No | The ID of the team \(for channel messages\) | +| `channelId` | string | No | The ID of the channel \(for channel messages\) | +| `chatId` | string | No | The ID of the chat \(for chat messages\) | +| `messageId` | string | Yes | The ID of the message | +| `reactionType` | string | Yes | The emoji reaction to remove \(e.g., ❤️, 👍, 😊\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the reaction was removed successfully | +| `reactionType` | string | The emoji that was removed | +| `messageId` | string | ID of the message | + +### `microsoft_teams_list_team_members` + +List all members of a Microsoft Teams team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | The ID of the team | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the listing was successful | +| `members` | array | Array of team members | +| `memberCount` | number | Total number of members | + +### `microsoft_teams_list_channel_members` + +List all members of a Microsoft Teams channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | The ID of the team | +| `channelId` | string | Yes | The ID of the channel | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the listing was successful | +| `members` | array | Array of channel members | +| `memberCount` | number | Total number of members | + ## Notes diff --git a/apps/docs/content/docs/en/tools/parallel_ai.mdx b/apps/docs/content/docs/en/tools/parallel_ai.mdx index b1e3d48e44..13fdc92837 100644 --- a/apps/docs/content/docs/en/tools/parallel_ai.mdx +++ b/apps/docs/content/docs/en/tools/parallel_ai.mdx @@ -1,6 +1,6 @@ --- title: Parallel AI -description: Search with Parallel AI +description: Web research with Parallel AI --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -71,7 +71,7 @@ In Sim, the Parallel AI integration empowers your agents to perform web searches ## Usage Instructions -Integrate Parallel AI into the workflow. Can search the web. +Integrate Parallel AI into the workflow. Can search the web, extract information from URLs, and conduct deep research. @@ -98,6 +98,51 @@ Search the web using Parallel AI. Provides comprehensive search results with int | --------- | ---- | ----------- | | `results` | array | Search results with excerpts from relevant pages | +### `parallel_extract` + +Extract targeted information from specific URLs using Parallel AI. Processes provided URLs to pull relevant content based on your objective. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `urls` | string | Yes | Comma-separated list of URLs to extract information from | +| `objective` | string | Yes | What information to extract from the provided URLs | +| `excerpts` | boolean | Yes | Include relevant excerpts from the content | +| `full_content` | boolean | Yes | Include full page content | +| `apiKey` | string | Yes | Parallel AI API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Extracted information from the provided URLs | + +### `parallel_deep_research` + +Conduct comprehensive deep research across the web using Parallel AI. Synthesizes information from multiple sources with citations. Can take up to 15 minutes to complete. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `input` | string | Yes | Research query or question \(up to 15,000 characters\) | +| `processor` | string | No | Compute level: base, lite, pro, ultra, ultra2x, ultra4x, ultra8x \(default: base\) | +| `output_schema` | string | No | Desired output format description. Use "text" for markdown reports with citations, or describe structured JSON format | +| `include_domains` | string | No | Comma-separated list of domains to restrict research to \(source policy\) | +| `exclude_domains` | string | No | Comma-separated list of domains to exclude from research \(source policy\) | +| `apiKey` | string | Yes | Parallel AI API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Task status \(running, completed, failed\) | +| `run_id` | string | Unique ID for this research task | +| `message` | string | Status message \(for running tasks\) | +| `content` | object | Research results \(structured based on output_schema\) | +| `basis` | array | Citations and sources with excerpts and confidence levels | + ## Notes diff --git a/apps/docs/content/docs/en/tools/reddit.mdx b/apps/docs/content/docs/en/tools/reddit.mdx index 085904c414..40a086c558 100644 --- a/apps/docs/content/docs/en/tools/reddit.mdx +++ b/apps/docs/content/docs/en/tools/reddit.mdx @@ -39,7 +39,7 @@ These operations let your agents access and analyze Reddit content as part of yo ## Usage Instructions -Integrate Reddit into the workflow. Can get posts and comments from a subreddit. +Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, and manage your Reddit account. @@ -57,6 +57,11 @@ Fetch posts from a subreddit with different sorting options | `sort` | string | No | Sort method for posts: "hot", "new", "top", or "rising" \(default: "hot"\) | | `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) | | `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "day"\) | +| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | +| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | +| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | +| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | +| `sr_detail` | boolean | No | Expand subreddit details in the response | #### Output @@ -77,6 +82,16 @@ Fetch comments from a specific Reddit post | `subreddit` | string | Yes | The subreddit where the post is located \(without the r/ prefix\) | | `sort` | string | No | Sort method for comments: "confidence", "top", "new", "controversial", "old", "random", "qa" \(default: "confidence"\) | | `limit` | number | No | Maximum number of comments to return \(default: 50, max: 100\) | +| `depth` | number | No | Maximum depth of subtrees in the thread \(controls nested comment levels\) | +| `context` | number | No | Number of parent comments to include | +| `showedits` | boolean | No | Show edit information for comments | +| `showmore` | boolean | No | Include "load more comments" elements in the response | +| `showtitle` | boolean | No | Include submission title in the response | +| `threaded` | boolean | No | Return comments in threaded/nested format | +| `truncate` | number | No | Integer to truncate comment depth | +| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | +| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | +| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | #### Output @@ -84,6 +99,228 @@ Fetch comments from a specific Reddit post | --------- | ---- | ----------- | | `post` | object | Post information including ID, title, author, content, and metadata | +### `reddit_get_controversial` + +Fetch controversial posts from a subreddit + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) | +| `time` | string | No | Time filter for controversial posts: "hour", "day", "week", "month", "year", or "all" \(default: "all"\) | +| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) | +| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | +| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | +| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | +| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | +| `sr_detail` | boolean | No | Expand subreddit details in the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subreddit` | string | Name of the subreddit where posts were fetched from | +| `posts` | array | Array of controversial posts with title, author, URL, score, comments count, and metadata | + +### `reddit_get_gilded` + +Fetch gilded/awarded posts from a subreddit + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) | +| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) | +| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | +| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | +| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | +| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | +| `sr_detail` | boolean | No | Expand subreddit details in the response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subreddit` | string | Name of the subreddit where posts were fetched from | +| `posts` | array | Array of gilded/awarded posts with title, author, URL, score, comments count, and metadata | + +### `reddit_search` + +Search for posts within a subreddit + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `subreddit` | string | Yes | The name of the subreddit to search in \(without the r/ prefix\) | +| `query` | string | Yes | Search query text | +| `sort` | string | No | Sort method for search results: "relevance", "hot", "top", "new", or "comments" \(default: "relevance"\) | +| `time` | string | No | Time filter for search results: "hour", "day", "week", "month", "year", or "all" \(default: "all"\) | +| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) | +| `restrict_sr` | boolean | No | Restrict search to the specified subreddit only \(default: true\) | +| `after` | string | No | Fullname of a thing to fetch items after \(for pagination\) | +| `before` | string | No | Fullname of a thing to fetch items before \(for pagination\) | +| `count` | number | No | A count of items already seen in the listing \(used for numbering\) | +| `show` | string | No | Show items that would normally be filtered \(e.g., "all"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subreddit` | string | Name of the subreddit where search was performed | +| `posts` | array | Array of search result posts with title, author, URL, score, comments count, and metadata | + +### `reddit_submit_post` + +Submit a new post to a subreddit (text or link) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `subreddit` | string | Yes | The name of the subreddit to post to \(without the r/ prefix\) | +| `title` | string | Yes | Title of the submission \(max 300 characters\) | +| `text` | string | No | Text content for a self post \(markdown supported\) | +| `url` | string | No | URL for a link post \(cannot be used with text\) | +| `nsfw` | boolean | No | Mark post as NSFW | +| `spoiler` | boolean | No | Mark post as spoiler | +| `send_replies` | boolean | No | Send reply notifications to inbox \(default: true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the post was submitted successfully | +| `message` | string | Success or error message | +| `data` | object | Post data including ID, name, URL, and permalink | + +### `reddit_vote` + +Upvote, downvote, or unvote a Reddit post or comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Thing fullname to vote on \(e.g., t3_xxxxx for post, t1_xxxxx for comment\) | +| `dir` | number | Yes | Vote direction: 1 \(upvote\), 0 \(unvote\), or -1 \(downvote\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the vote was successful | +| `message` | string | Success or error message | + +### `reddit_save` + +Save a Reddit post or comment to your saved items + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Thing fullname to save \(e.g., t3_xxxxx for post, t1_xxxxx for comment\) | +| `category` | string | No | Category to save under \(Reddit Gold feature\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the save was successful | +| `message` | string | Success or error message | + +### `reddit_unsave` + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subreddit` | string | Subreddit name | +| `posts` | json | Posts data | +| `post` | json | Single post data | +| `comments` | json | Comments data | + +### `reddit_reply` + +Add a comment reply to a Reddit post or comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `parent_id` | string | Yes | Thing fullname to reply to \(e.g., t3_xxxxx for post, t1_xxxxx for comment\) | +| `text` | string | Yes | Comment text in markdown format | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the reply was posted successfully | +| `message` | string | Success or error message | +| `data` | object | Comment data including ID, name, permalink, and body | + +### `reddit_edit` + +Edit the text of your own Reddit post or comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `thing_id` | string | Yes | Thing fullname to edit \(e.g., t3_xxxxx for post, t1_xxxxx for comment\) | +| `text` | string | Yes | New text content in markdown format | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the edit was successful | +| `message` | string | Success or error message | +| `data` | object | Updated content data | + +### `reddit_delete` + +Delete your own Reddit post or comment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | string | Yes | Thing fullname to delete \(e.g., t3_xxxxx for post, t1_xxxxx for comment\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the deletion was successful | +| `message` | string | Success or error message | + +### `reddit_subscribe` + +Subscribe or unsubscribe from a subreddit + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `subreddit` | string | Yes | The name of the subreddit \(without the r/ prefix\) | +| `action` | string | Yes | Action to perform: "sub" to subscribe or "unsub" to unsubscribe | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the subscription action was successful | +| `message` | string | Success or error message | + ## Notes diff --git a/apps/docs/content/docs/en/tools/stripe.mdx b/apps/docs/content/docs/en/tools/stripe.mdx new file mode 100644 index 0000000000..3ca8479304 --- /dev/null +++ b/apps/docs/content/docs/en/tools/stripe.mdx @@ -0,0 +1,1070 @@ +--- +title: Stripe +description: Process payments and manage Stripe data +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + `} +/> + +## Usage Instructions + +Integrates Stripe into the workflow. Manage payment intents, customers, subscriptions, invoices, charges, products, prices, and events. Can be used in trigger mode to trigger a workflow when a Stripe event occurs. + + + +## Tools + +### `stripe_create_payment_intent` + +Create a new Payment Intent to process a payment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `amount` | number | Yes | Amount in cents \(e.g., 2000 for $20.00\) | +| `currency` | string | Yes | Three-letter ISO currency code \(e.g., usd, eur\) | +| `customer` | string | No | Customer ID to associate with this payment | +| `payment_method` | string | No | Payment method ID | +| `description` | string | No | Description of the payment | +| `receipt_email` | string | No | Email address to send receipt to | +| `metadata` | json | No | Set of key-value pairs for storing additional information | +| `automatic_payment_methods` | json | No | Enable automatic payment methods \(e.g., \{"enabled": true\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intent` | json | The created Payment Intent object | +| `metadata` | json | Payment Intent metadata including ID, status, amount, and currency | + +### `stripe_retrieve_payment_intent` + +Retrieve an existing Payment Intent by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intent` | json | The retrieved Payment Intent object | +| `metadata` | json | Payment Intent metadata including ID, status, amount, and currency | + +### `stripe_update_payment_intent` + +Update an existing Payment Intent + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | +| `amount` | number | No | Updated amount in cents | +| `currency` | string | No | Three-letter ISO currency code | +| `customer` | string | No | Customer ID | +| `description` | string | No | Updated description | +| `metadata` | json | No | Updated metadata | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intent` | json | The updated Payment Intent object | +| `metadata` | json | Payment Intent metadata including ID, status, amount, and currency | + +### `stripe_confirm_payment_intent` + +Confirm a Payment Intent to complete the payment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | +| `payment_method` | string | No | Payment method ID to confirm with | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intent` | json | The confirmed Payment Intent object | +| `metadata` | json | Payment Intent metadata including ID, status, amount, and currency | + +### `stripe_capture_payment_intent` + +Capture an authorized Payment Intent + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | +| `amount_to_capture` | number | No | Amount to capture in cents \(defaults to full amount\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intent` | json | The captured Payment Intent object | +| `metadata` | json | Payment Intent metadata including ID, status, amount, and currency | + +### `stripe_cancel_payment_intent` + +Cancel a Payment Intent + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Payment Intent ID \(e.g., pi_1234567890\) | +| `cancellation_reason` | string | No | Reason for cancellation \(duplicate, fraudulent, requested_by_customer, abandoned\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intent` | json | The canceled Payment Intent object | +| `metadata` | json | Payment Intent metadata including ID, status, amount, and currency | + +### `stripe_list_payment_intents` + +List all Payment Intents + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `customer` | string | No | Filter by customer ID | +| `created` | json | No | Filter by creation date \(e.g., \{"gt": 1633024800\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intents` | json | Array of Payment Intent objects | +| `metadata` | json | List metadata including count and has_more | + +### `stripe_search_payment_intents` + +Search for Payment Intents using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., \"status:'succeeded' AND currency:'usd'\"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `payment_intents` | json | Array of matching Payment Intent objects | +| `metadata` | json | Search metadata including count and has_more | + +### `stripe_create_customer` + +Create a new customer object + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `email` | string | No | Customer email address | +| `name` | string | No | Customer full name | +| `phone` | string | No | Customer phone number | +| `description` | string | No | Description of the customer | +| `address` | json | No | Customer address object | +| `metadata` | json | No | Set of key-value pairs | +| `payment_method` | string | No | Payment method ID to attach | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | json | The created customer object | +| `metadata` | json | Customer metadata | + +### `stripe_retrieve_customer` + +Retrieve an existing customer by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Customer ID \(e.g., cus_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | json | The retrieved customer object | +| `metadata` | json | Customer metadata | + +### `stripe_update_customer` + +Update an existing customer + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Customer ID \(e.g., cus_1234567890\) | +| `email` | string | No | Updated email address | +| `name` | string | No | Updated name | +| `phone` | string | No | Updated phone number | +| `description` | string | No | Updated description | +| `address` | json | No | Updated address object | +| `metadata` | json | No | Updated metadata | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customer` | json | The updated customer object | +| `metadata` | json | Customer metadata | + +### `stripe_delete_customer` + +Permanently delete a customer + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Customer ID \(e.g., cus_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the customer was deleted | +| `id` | string | The ID of the deleted customer | +| `metadata` | json | Deletion metadata | + +### `stripe_list_customers` + +List all customers + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `email` | string | No | Filter by email address | +| `created` | json | No | Filter by creation date | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customers` | json | Array of customer objects | +| `metadata` | json | List metadata | + +### `stripe_search_customers` + +Search for customers using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., "email:\'customer@example.com\'"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customers` | json | Array of matching customer objects | +| `metadata` | json | Search metadata | + +### `stripe_create_subscription` + +Create a new subscription for a customer + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `customer` | string | Yes | Customer ID to subscribe | +| `items` | json | Yes | Array of items with price IDs \(e.g., \[\{"price": "price_xxx", "quantity": 1\}\]\) | +| `trial_period_days` | number | No | Number of trial days | +| `default_payment_method` | string | No | Payment method ID | +| `cancel_at_period_end` | boolean | No | Cancel subscription at period end | +| `metadata` | json | No | Set of key-value pairs for storing additional information | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscription` | json | The created subscription object | +| `metadata` | json | Subscription metadata including ID, status, and customer | + +### `stripe_retrieve_subscription` + +Retrieve an existing subscription by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscription` | json | The retrieved subscription object | +| `metadata` | json | Subscription metadata including ID, status, and customer | + +### `stripe_update_subscription` + +Update an existing subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | +| `items` | json | No | Updated array of items with price IDs | +| `cancel_at_period_end` | boolean | No | Cancel subscription at period end | +| `metadata` | json | No | Updated metadata | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscription` | json | The updated subscription object | +| `metadata` | json | Subscription metadata including ID, status, and customer | + +### `stripe_cancel_subscription` + +Cancel a subscription + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | +| `prorate` | boolean | No | Whether to prorate the cancellation | +| `invoice_now` | boolean | No | Whether to invoice immediately | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscription` | json | The canceled subscription object | +| `metadata` | json | Subscription metadata including ID, status, and customer | + +### `stripe_resume_subscription` + +Resume a subscription that was scheduled for cancellation + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Subscription ID \(e.g., sub_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscription` | json | The resumed subscription object | +| `metadata` | json | Subscription metadata including ID, status, and customer | + +### `stripe_list_subscriptions` + +List all subscriptions + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `customer` | string | No | Filter by customer ID | +| `status` | string | No | Filter by status \(active, past_due, unpaid, canceled, incomplete, incomplete_expired, trialing, all\) | +| `price` | string | No | Filter by price ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscriptions` | json | Array of subscription objects | +| `metadata` | json | List metadata | + +### `stripe_search_subscriptions` + +Search for subscriptions using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., \"status:'active' AND customer:'cus_xxx'\"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `subscriptions` | json | Array of matching subscription objects | +| `metadata` | json | Search metadata | + +### `stripe_create_invoice` + +Create a new invoice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `customer` | string | Yes | Customer ID \(e.g., cus_1234567890\) | +| `description` | string | No | Description of the invoice | +| `metadata` | json | No | Set of key-value pairs | +| `auto_advance` | boolean | No | Auto-finalize the invoice | +| `collection_method` | string | No | Collection method: charge_automatically or send_invoice | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The created invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_retrieve_invoice` + +Retrieve an existing invoice by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The retrieved invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_update_invoice` + +Update an existing invoice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | +| `description` | string | No | Description of the invoice | +| `metadata` | json | No | Set of key-value pairs | +| `auto_advance` | boolean | No | Auto-finalize the invoice | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The updated invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_delete_invoice` + +Permanently delete a draft invoice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the invoice was deleted | +| `id` | string | The ID of the deleted invoice | +| `metadata` | json | Deletion metadata | + +### `stripe_finalize_invoice` + +Finalize a draft invoice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | +| `auto_advance` | boolean | No | Auto-advance the invoice | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The finalized invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_pay_invoice` + +Pay an invoice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | +| `paid_out_of_band` | boolean | No | Mark invoice as paid out of band | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The paid invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_void_invoice` + +Void an invoice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The voided invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_send_invoice` + +Send an invoice to the customer + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Invoice ID \(e.g., in_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoice` | json | The sent invoice object | +| `metadata` | json | Invoice metadata | + +### `stripe_list_invoices` + +List all invoices + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `customer` | string | No | Filter by customer ID | +| `status` | string | No | Filter by invoice status | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoices` | json | Array of invoice objects | +| `metadata` | json | List metadata | + +### `stripe_search_invoices` + +Search for invoices using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., "customer:\'cus_1234567890\'"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `invoices` | json | Array of matching invoice objects | +| `metadata` | json | Search metadata | + +### `stripe_create_charge` + +Create a new charge to process a payment + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `amount` | number | Yes | Amount in cents \(e.g., 2000 for $20.00\) | +| `currency` | string | Yes | Three-letter ISO currency code \(e.g., usd, eur\) | +| `customer` | string | No | Customer ID to associate with this charge | +| `source` | string | No | Payment source ID \(e.g., card token or saved card ID\) | +| `description` | string | No | Description of the charge | +| `metadata` | json | No | Set of key-value pairs for storing additional information | +| `capture` | boolean | No | Whether to immediately capture the charge \(defaults to true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `charge` | json | The created Charge object | +| `metadata` | json | Charge metadata including ID, status, amount, currency, and paid status | + +### `stripe_retrieve_charge` + +Retrieve an existing charge by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Charge ID \(e.g., ch_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `charge` | json | The retrieved Charge object | +| `metadata` | json | Charge metadata including ID, status, amount, currency, and paid status | + +### `stripe_update_charge` + +Update an existing charge + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Charge ID \(e.g., ch_1234567890\) | +| `description` | string | No | Updated description | +| `metadata` | json | No | Updated metadata | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `charge` | json | The updated Charge object | +| `metadata` | json | Charge metadata including ID, status, amount, currency, and paid status | + +### `stripe_capture_charge` + +Capture an uncaptured charge + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Charge ID \(e.g., ch_1234567890\) | +| `amount` | number | No | Amount to capture in cents \(defaults to full amount\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `charge` | json | The captured Charge object | +| `metadata` | json | Charge metadata including ID, status, amount, currency, and paid status | + +### `stripe_list_charges` + +List all charges + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `customer` | string | No | Filter by customer ID | +| `created` | json | No | Filter by creation date \(e.g., \{"gt": 1633024800\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `charges` | json | Array of Charge objects | +| `metadata` | json | List metadata including count and has_more | + +### `stripe_search_charges` + +Search for charges using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., \"status:'succeeded' AND currency:'usd'\"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `charges` | json | Array of matching Charge objects | +| `metadata` | json | Search metadata including count and has_more | + +### `stripe_create_product` + +Create a new product object + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `name` | string | Yes | Product name | +| `description` | string | No | Product description | +| `active` | boolean | No | Whether the product is active | +| `images` | json | No | Array of image URLs for the product | +| `metadata` | json | No | Set of key-value pairs | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `product` | json | The created product object | +| `metadata` | json | Product metadata | + +### `stripe_retrieve_product` + +Retrieve an existing product by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Product ID \(e.g., prod_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `product` | json | The retrieved product object | +| `metadata` | json | Product metadata | + +### `stripe_update_product` + +Update an existing product + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Product ID \(e.g., prod_1234567890\) | +| `name` | string | No | Updated product name | +| `description` | string | No | Updated product description | +| `active` | boolean | No | Updated active status | +| `images` | json | No | Updated array of image URLs | +| `metadata` | json | No | Updated metadata | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `product` | json | The updated product object | +| `metadata` | json | Product metadata | + +### `stripe_delete_product` + +Permanently delete a product + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Product ID \(e.g., prod_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the product was deleted | +| `id` | string | The ID of the deleted product | +| `metadata` | json | Deletion metadata | + +### `stripe_list_products` + +List all products + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `active` | boolean | No | Filter by active status | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `products` | json | Array of product objects | +| `metadata` | json | List metadata | + +### `stripe_search_products` + +Search for products using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., "name:\'shirt\'"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `products` | json | Array of matching product objects | +| `metadata` | json | Search metadata | + +### `stripe_create_price` + +Create a new price for a product + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `product` | string | Yes | Product ID \(e.g., prod_1234567890\) | +| `currency` | string | Yes | Three-letter ISO currency code \(e.g., usd, eur\) | +| `unit_amount` | number | No | Amount in cents \(e.g., 1000 for $10.00\) | +| `recurring` | json | No | Recurring billing configuration \(interval: day/week/month/year\) | +| `metadata` | json | No | Set of key-value pairs | +| `billing_scheme` | string | No | Billing scheme \(per_unit or tiered\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `price` | json | The created price object | +| `metadata` | json | Price metadata | + +### `stripe_retrieve_price` + +Retrieve an existing price by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Price ID \(e.g., price_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `price` | json | The retrieved price object | +| `metadata` | json | Price metadata | + +### `stripe_update_price` + +Update an existing price + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Price ID \(e.g., price_1234567890\) | +| `active` | boolean | No | Whether the price is active | +| `metadata` | json | No | Updated metadata | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `price` | json | The updated price object | +| `metadata` | json | Price metadata | + +### `stripe_list_prices` + +List all prices + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `product` | string | No | Filter by product ID | +| `active` | boolean | No | Filter by active status | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `prices` | json | Array of price objects | +| `metadata` | json | List metadata | + +### `stripe_search_prices` + +Search for prices using query syntax + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `query` | string | Yes | Search query \(e.g., \"active:'true' AND currency:'usd'\"\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `prices` | json | Array of matching price objects | +| `metadata` | json | Search metadata | + +### `stripe_retrieve_event` + +Retrieve an existing Event by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `id` | string | Yes | Event ID \(e.g., evt_1234567890\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `event` | json | The retrieved Event object | +| `metadata` | json | Event metadata including ID, type, and created timestamp | + +### `stripe_list_events` + +List all Events + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Stripe API key \(secret key\) | +| `limit` | number | No | Number of results to return \(default 10, max 100\) | +| `type` | string | No | Filter by event type \(e.g., payment_intent.created\) | +| `created` | json | No | Filter by creation date \(e.g., \{"gt": 1633024800\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | json | Array of Event objects | +| `metadata` | json | List metadata including count and has_more | + + + +## Notes + +- Category: `tools` +- Type: `stripe` diff --git a/apps/docs/content/docs/en/tools/supabase.mdx b/apps/docs/content/docs/en/tools/supabase.mdx index a839b00750..b7c1ae3c37 100644 --- a/apps/docs/content/docs/en/tools/supabase.mdx +++ b/apps/docs/content/docs/en/tools/supabase.mdx @@ -76,7 +76,7 @@ Whether you’re building internal tools, automating business processes, or powe ## Usage Instructions -Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row. +Integrate Supabase into the workflow. Supports database operations (query, insert, update, delete, upsert), full-text search, RPC functions, row counting, vector search, and complete storage management (upload, download, list, move, copy, delete files and buckets). @@ -205,6 +205,51 @@ Insert or update data in a Supabase table (upsert operation) | `message` | string | Operation status message | | `results` | array | Array of upserted records | +### `supabase_count` + +Count rows in a Supabase table + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `table` | string | Yes | The name of the Supabase table to count rows from | +| `filter` | string | No | PostgREST filter \(e.g., "status=eq.active"\) | +| `countType` | string | No | Count type: exact, planned, or estimated \(default: exact\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `count` | number | Number of rows matching the filter | + +### `supabase_text_search` + +Perform full-text search on a Supabase table + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `table` | string | Yes | The name of the Supabase table to search | +| `column` | string | Yes | The column to search in | +| `query` | string | Yes | The search query | +| `searchType` | string | No | Search type: plain, phrase, or websearch \(default: websearch\) | +| `language` | string | No | Language for text search configuration \(default: english\) | +| `limit` | number | No | Maximum number of rows to return | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | array | Array of records matching the search query | + ### `supabase_vector_search` Perform similarity search using pgvector in a Supabase table @@ -227,6 +272,259 @@ Perform similarity search using pgvector in a Supabase table | `message` | string | Operation status message | | `results` | array | Array of records with similarity scores from the vector search. Each record includes a similarity field \(0-1\) indicating how similar it is to the query vector. | +### `supabase_rpc` + +Call a PostgreSQL function in Supabase + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `functionName` | string | Yes | The name of the PostgreSQL function to call | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | json | Result returned from the function | + +### `supabase_storage_upload` + +Upload a file to a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `path` | string | Yes | The path where the file will be stored \(e.g., "folder/file.jpg"\) | +| `fileContent` | string | Yes | The file content \(base64 encoded for binary files, or plain text\) | +| `contentType` | string | No | MIME type of the file \(e.g., "image/jpeg", "text/plain"\) | +| `upsert` | boolean | No | If true, overwrites existing file \(default: false\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | object | Upload result including file path and metadata | + +### `supabase_storage_download` + +Download a file from a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `path` | string | Yes | The path to the file to download \(e.g., "folder/file.jpg"\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `fileContent` | string | File content \(base64 encoded if binary, plain text otherwise\) | +| `contentType` | string | MIME type of the file | +| `isBase64` | boolean | Whether the file content is base64 encoded | + +### `supabase_storage_list` + +List files in a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `path` | string | No | The folder path to list files from \(default: root\) | +| `limit` | number | No | Maximum number of files to return \(default: 100\) | +| `offset` | number | No | Number of files to skip \(for pagination\) | +| `sortBy` | string | No | Column to sort by: name, created_at, updated_at \(default: name\) | +| `sortOrder` | string | No | Sort order: asc or desc \(default: asc\) | +| `search` | string | No | Search term to filter files by name | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | array | Array of file objects with metadata | + +### `supabase_storage_delete` + +Delete files from a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `paths` | array | Yes | Array of file paths to delete \(e.g., \["folder/file1.jpg", "folder/file2.jpg"\]\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | array | Array of deleted file objects | + +### `supabase_storage_move` + +Move a file within a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `fromPath` | string | Yes | The current path of the file \(e.g., "folder/old.jpg"\) | +| `toPath` | string | Yes | The new path for the file \(e.g., "newfolder/new.jpg"\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | object | Move operation result | + +### `supabase_storage_copy` + +Copy a file within a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `fromPath` | string | Yes | The path of the source file \(e.g., "folder/source.jpg"\) | +| `toPath` | string | Yes | The path for the copied file \(e.g., "folder/copy.jpg"\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | object | Copy operation result | + +### `supabase_storage_create_bucket` + +Create a new storage bucket in Supabase + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the bucket to create | +| `isPublic` | boolean | No | Whether the bucket should be publicly accessible \(default: false\) | +| `fileSizeLimit` | number | No | Maximum file size in bytes \(optional\) | +| `allowedMimeTypes` | array | No | Array of allowed MIME types \(e.g., \["image/png", "image/jpeg"\]\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | object | Created bucket information | + +### `supabase_storage_list_buckets` + +List all storage buckets in Supabase + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | array | Array of bucket objects | + +### `supabase_storage_delete_bucket` + +Delete a storage bucket in Supabase + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the bucket to delete | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `results` | object | Delete operation result | + +### `supabase_storage_get_public_url` + +Get the public URL for a file in a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `path` | string | Yes | The path to the file \(e.g., "folder/file.jpg"\) | +| `download` | boolean | No | If true, forces download instead of inline display \(default: false\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `publicUrl` | string | The public URL to access the file | + +### `supabase_storage_create_signed_url` + +Create a temporary signed URL for a file in a Supabase storage bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `bucket` | string | Yes | The name of the storage bucket | +| `path` | string | Yes | The path to the file \(e.g., "folder/file.jpg"\) | +| `expiresIn` | number | Yes | Number of seconds until the URL expires \(e.g., 3600 for 1 hour\) | +| `download` | boolean | No | If true, forces download instead of inline display \(default: false\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `signedUrl` | string | The temporary signed URL to access the file | + ## Notes diff --git a/apps/docs/content/docs/en/tools/tavily.mdx b/apps/docs/content/docs/en/tools/tavily.mdx index 6b6d3c61ae..ef7b7a40ed 100644 --- a/apps/docs/content/docs/en/tools/tavily.mdx +++ b/apps/docs/content/docs/en/tools/tavily.mdx @@ -74,6 +74,21 @@ Perform AI-powered web searches using Tavily | --------- | ---- | -------- | ----------- | | `query` | string | Yes | The search query to execute | | `max_results` | number | No | Maximum number of results \(1-20\) | +| `topic` | string | No | Category type: general, news, or finance \(default: general\) | +| `search_depth` | string | No | Search scope: basic \(1 credit\) or advanced \(2 credits\) \(default: basic\) | +| `include_answer` | string | No | LLM-generated response: true/basic for quick answer or advanced for detailed | +| `include_raw_content` | string | No | Parsed HTML content: true/markdown or text format | +| `include_images` | boolean | No | Include image search results | +| `include_image_descriptions` | boolean | No | Add descriptive text for images | +| `include_favicon` | boolean | No | Include favicon URLs | +| `chunks_per_source` | number | No | Maximum number of relevant chunks per source \(1-3, default: 3\) | +| `time_range` | string | No | Filter by recency: day/d, week/w, month/m, year/y | +| `start_date` | string | No | Earliest publication date \(YYYY-MM-DD format\) | +| `end_date` | string | No | Latest publication date \(YYYY-MM-DD format\) | +| `include_domains` | string | No | Comma-separated list of domains to whitelist \(max 300\) | +| `exclude_domains` | string | No | Comma-separated list of domains to blacklist \(max 150\) | +| `country` | string | No | Boost results from specified country \(general topic only\) | +| `auto_parameters` | boolean | No | Automatic parameter configuration based on query intent | | `apiKey` | string | Yes | Tavily API Key | #### Output @@ -93,6 +108,9 @@ Extract raw content from multiple web pages simultaneously using Tavily | --------- | ---- | -------- | ----------- | | `urls` | string | Yes | URL or array of URLs to extract content from | | `extract_depth` | string | No | The depth of extraction \(basic=1 credit/5 URLs, advanced=2 credits/5 URLs\) | +| `format` | string | No | Output format: markdown or text \(default: markdown\) | +| `include_images` | boolean | No | Incorporate images in extraction output | +| `include_favicon` | boolean | No | Add favicon URL for each result | | `apiKey` | string | Yes | Tavily API Key | #### Output @@ -101,6 +119,64 @@ Extract raw content from multiple web pages simultaneously using Tavily | --------- | ---- | ----------- | | `results` | array | The URL that was extracted | +### `tavily_crawl` + +Systematically crawl and extract content from websites using Tavily + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `url` | string | Yes | The root URL to begin the crawl | +| `instructions` | string | No | Natural language directions for the crawler \(costs 2 credits per 10 pages\) | +| `max_depth` | number | No | How far from base URL to explore \(1-5, default: 1\) | +| `max_breadth` | number | No | Links followed per page level \(≥1, default: 20\) | +| `limit` | number | No | Total links processed before stopping \(≥1, default: 50\) | +| `select_paths` | string | No | Comma-separated regex patterns to include specific URL paths \(e.g., /docs/.*\) | +| `select_domains` | string | No | Comma-separated regex patterns to restrict crawling to certain domains | +| `exclude_paths` | string | No | Comma-separated regex patterns to skip specific URL paths | +| `exclude_domains` | string | No | Comma-separated regex patterns to block certain domains | +| `allow_external` | boolean | No | Include external domain links in results \(default: true\) | +| `include_images` | boolean | No | Incorporate images in crawl output | +| `extract_depth` | string | No | Extraction depth: basic \(1 credit/5 pages\) or advanced \(2 credits/5 pages\) | +| `format` | string | No | Output format: markdown or text \(default: markdown\) | +| `include_favicon` | boolean | No | Add favicon URL for each result | +| `apiKey` | string | Yes | Tavily API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `base_url` | string | The base URL that was crawled | +| `results` | array | The crawled page URL | + +### `tavily_map` + +Discover and visualize website structure using Tavily + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `url` | string | Yes | The root URL to begin mapping | +| `instructions` | string | No | Natural language guidance for mapping behavior \(costs 2 credits per 10 pages\) | +| `max_depth` | number | No | How far from base URL to explore \(1-5, default: 1\) | +| `max_breadth` | number | No | Links to follow per level \(default: 20\) | +| `limit` | number | No | Total links to process \(default: 50\) | +| `select_paths` | string | No | Comma-separated regex patterns for URL path filtering \(e.g., /docs/.*\) | +| `select_domains` | string | No | Comma-separated regex patterns to restrict mapping to specific domains | +| `exclude_paths` | string | No | Comma-separated regex patterns to exclude specific URL paths | +| `exclude_domains` | string | No | Comma-separated regex patterns to exclude domains | +| `allow_external` | boolean | No | Include external domain links in results \(default: true\) | +| `apiKey` | string | Yes | Tavily API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `base_url` | string | The base URL that was mapped | +| `results` | array | Discovered URL | + ## Notes diff --git a/apps/sim/app/api/tools/confluence/attachment/route.ts b/apps/sim/app/api/tools/confluence/attachment/route.ts new file mode 100644 index 0000000000..e12e3444d1 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/attachment/route.ts @@ -0,0 +1,66 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// Delete an attachment +export async function DELETE(request: Request) { + try { + const { domain, accessToken, cloudId: providedCloudId, attachmentId } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!attachmentId) { + return NextResponse.json({ error: 'Attachment ID is required' }, { status: 400 }) + } + + const attachmentIdValidation = validateAlphanumericId(attachmentId, 'attachmentId', 255) + if (!attachmentIdValidation.isValid) { + return NextResponse.json({ error: attachmentIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/attachments/${attachmentId}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete Confluence attachment (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ attachmentId, deleted: true }) + } catch (error) { + console.error('Error deleting Confluence attachment:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/attachments/route.ts b/apps/sim/app/api/tools/confluence/attachments/route.ts new file mode 100644 index 0000000000..40b7459b5b --- /dev/null +++ b/apps/sim/app/api/tools/confluence/attachments/route.ts @@ -0,0 +1,81 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// List attachments on a page +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const pageId = searchParams.get('pageId') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '25' + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?limit=${limit}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list Confluence attachments (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const attachments = (data.results || []).map((attachment: any) => ({ + id: attachment.id, + title: attachment.title, + fileSize: attachment.fileSize || 0, + mediaType: attachment.mediaType || '', + downloadUrl: attachment.downloadLink || attachment._links?.download || '', + })) + + return NextResponse.json({ attachments }) + } catch (error) { + console.error('Error listing Confluence attachments:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/comment/route.ts b/apps/sim/app/api/tools/confluence/comment/route.ts new file mode 100644 index 0000000000..bde8594be8 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/comment/route.ts @@ -0,0 +1,167 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// Update a comment +export async function PUT(request: Request) { + try { + const { + domain, + accessToken, + cloudId: providedCloudId, + commentId, + comment, + } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!commentId) { + return NextResponse.json({ error: 'Comment ID is required' }, { status: 400 }) + } + + if (!comment) { + return NextResponse.json({ error: 'Comment is required' }, { status: 400 }) + } + + const commentIdValidation = validateAlphanumericId(commentId, 'commentId', 255) + if (!commentIdValidation.isValid) { + return NextResponse.json({ error: commentIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // Get current comment version + const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}` + const getResponse = await fetch(getUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!getResponse.ok) { + throw new Error(`Failed to fetch current comment: ${getResponse.status}`) + } + + const currentComment = await getResponse.json() + const currentVersion = currentComment.version?.number || 1 + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}` + + const body = { + body: { + representation: 'storage', + value: comment, + }, + version: { + number: currentVersion + 1, + message: 'Updated via Sim', + }, + } + + const response = await fetch(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to update Confluence comment (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Error updating Confluence comment:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +// Delete a comment +export async function DELETE(request: Request) { + try { + const { domain, accessToken, cloudId: providedCloudId, commentId } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!commentId) { + return NextResponse.json({ error: 'Comment ID is required' }, { status: 400 }) + } + + const commentIdValidation = validateAlphanumericId(commentId, 'commentId', 255) + if (!commentIdValidation.isValid) { + return NextResponse.json({ error: commentIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments/${commentId}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete Confluence comment (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ commentId, deleted: true }) + } catch (error) { + console.error('Error deleting Confluence comment:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/comments/route.ts b/apps/sim/app/api/tools/confluence/comments/route.ts new file mode 100644 index 0000000000..8b09cca63a --- /dev/null +++ b/apps/sim/app/api/tools/confluence/comments/route.ts @@ -0,0 +1,156 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// Create a comment +export async function POST(request: Request) { + try { + const { domain, accessToken, cloudId: providedCloudId, pageId, comment } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + if (!comment) { + return NextResponse.json({ error: 'Comment is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/footer-comments` + + const body = { + pageId, + body: { + representation: 'storage', + value: comment, + }, + } + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to create Confluence comment (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ ...data, pageId }) + } catch (error) { + console.error('Error creating Confluence comment:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +// List comments on a page +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const pageId = searchParams.get('pageId') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '25' + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?limit=${limit}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list Confluence comments (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const comments = (data.results || []).map((comment: any) => ({ + id: comment.id, + body: comment.body?.storage?.value || comment.body?.view?.value || '', + createdAt: comment.createdAt || '', + authorId: comment.authorId || '', + })) + + return NextResponse.json({ comments }) + } catch (error) { + console.error('Error listing Confluence comments:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/create-page/route.ts b/apps/sim/app/api/tools/confluence/create-page/route.ts new file mode 100644 index 0000000000..0ff03d40dc --- /dev/null +++ b/apps/sim/app/api/tools/confluence/create-page/route.ts @@ -0,0 +1,107 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + const { + domain, + accessToken, + cloudId: providedCloudId, + spaceId, + title, + content, + parentId, + } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + if (!title) { + return NextResponse.json({ error: 'Title is required' }, { status: 400 }) + } + + if (!content) { + return NextResponse.json({ error: 'Content is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + if (parentId) { + const parentIdValidation = validateAlphanumericId(parentId, 'parentId', 255) + if (!parentIdValidation.isValid) { + return NextResponse.json({ error: parentIdValidation.error }, { status: 400 }) + } + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const createBody: any = { + spaceId, + status: 'current', + title, + body: { + representation: 'storage', + value: content, + }, + } + + if (parentId) { + createBody.parentId = parentId + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages` + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(createBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || + (errorData?.errors && JSON.stringify(errorData.errors)) || + `Failed to create Confluence page (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Error creating Confluence page:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/label/route.ts b/apps/sim/app/api/tools/confluence/label/route.ts new file mode 100644 index 0000000000..24322aacf5 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/label/route.ts @@ -0,0 +1,96 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// Remove a label from a page +export async function DELETE(request: Request) { + try { + const { + domain, + accessToken, + cloudId: providedCloudId, + pageId, + labelName, + } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + if (!labelName) { + return NextResponse.json({ error: 'Label name is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // First, get all labels to find the label ID + const listUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels` + const listResponse = await fetch(listUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!listResponse.ok) { + throw new Error(`Failed to list labels: ${listResponse.status}`) + } + + const listData = await listResponse.json() + const label = (listData.results || []).find((l: any) => l.name === labelName) + + if (!label) { + return NextResponse.json({ error: `Label "${labelName}" not found on page` }, { status: 404 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels/${label.id}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to remove Confluence label (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ pageId, labelName, removed: true }) + } catch (error) { + console.error('Error removing Confluence label:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/labels/route.ts b/apps/sim/app/api/tools/confluence/labels/route.ts new file mode 100644 index 0000000000..a97e19c762 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/labels/route.ts @@ -0,0 +1,157 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// Add a label to a page +export async function POST(request: Request) { + try { + const { + domain, + accessToken, + cloudId: providedCloudId, + pageId, + labelName, + } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + if (!labelName) { + return NextResponse.json({ error: 'Label name is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels` + + const body = { + prefix: 'global', + name: labelName, + } + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(body), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to add Confluence label (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ ...data, pageId, labelName }) + } catch (error) { + console.error('Error adding Confluence label:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +// List labels on a page +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const pageId = searchParams.get('pageId') + const providedCloudId = searchParams.get('cloudId') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list Confluence labels (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const labels = (data.results || []).map((label: any) => ({ + id: label.id, + name: label.name, + prefix: label.prefix || 'global', + })) + + return NextResponse.json({ labels }) + } catch (error) { + console.error('Error listing Confluence labels:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page/route.ts b/apps/sim/app/api/tools/confluence/page/route.ts index 1fa3c5b1ff..81a5e7dd66 100644 --- a/apps/sim/app/api/tools/confluence/page/route.ts +++ b/apps/sim/app/api/tools/confluence/page/route.ts @@ -185,3 +185,63 @@ export async function PUT(request: Request) { ) } } + +export async function DELETE(request: Request) { + try { + const { domain, accessToken, pageId, cloudId: providedCloudId } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete Confluence page (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ pageId, deleted: true }) + } catch (error) { + console.error('Error deleting Confluence page:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/pages/route.ts b/apps/sim/app/api/tools/confluence/pages/route.ts index 934c5f61a9..30bd72ffbb 100644 --- a/apps/sim/app/api/tools/confluence/pages/route.ts +++ b/apps/sim/app/api/tools/confluence/pages/route.ts @@ -1,11 +1,13 @@ import { NextResponse } from 'next/server' import { createLogger } from '@/lib/logs/console/logger' +import { validateJiraCloudId } from '@/lib/security/input-validation' import { getConfluenceCloudId } from '@/tools/confluence/utils' const logger = createLogger('ConfluencePagesAPI') export const dynamic = 'force-dynamic' +// List pages or search pages export async function POST(request: Request) { try { const { @@ -27,6 +29,11 @@ export async function POST(request: Request) { // Use provided cloudId or fetch it if not provided const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + // Build the URL with query parameters const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages` const queryParams = new URLSearchParams() diff --git a/apps/sim/app/api/tools/confluence/search/route.ts b/apps/sim/app/api/tools/confluence/search/route.ts new file mode 100644 index 0000000000..9fb7429e0f --- /dev/null +++ b/apps/sim/app/api/tools/confluence/search/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from 'next/server' +import { validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +export async function POST(request: Request) { + try { + const { + domain, + accessToken, + cloudId: providedCloudId, + query, + limit = 25, + } = await request.json() + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!query) { + return NextResponse.json({ error: 'Search query is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const searchParams = new URLSearchParams({ + cql: `text ~ "${query}"`, + limit: limit.toString(), + }) + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/search?${searchParams.toString()}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to search Confluence (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const results = (data.results || []).map((result: any) => ({ + id: result.content?.id || result.id, + title: result.content?.title || result.title, + type: result.content?.type || result.type, + url: result.url || result._links?.webui || '', + excerpt: result.excerpt || '', + })) + + return NextResponse.json({ results }) + } catch (error) { + console.error('Error searching Confluence:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts new file mode 100644 index 0000000000..d13141775b --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// Get a specific space +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const spaceId = searchParams.get('spaceId') + const providedCloudId = searchParams.get('cloudId') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to get Confluence space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('Error getting Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/spaces/route.ts b/apps/sim/app/api/tools/confluence/spaces/route.ts new file mode 100644 index 0000000000..9a0f262f4a --- /dev/null +++ b/apps/sim/app/api/tools/confluence/spaces/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' +import { validateJiraCloudId } from '@/lib/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +export const dynamic = 'force-dynamic' + +// List all spaces +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const providedCloudId = searchParams.get('cloudId') + const limit = searchParams.get('limit') || '25' + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=${limit}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + console.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list Confluence spaces (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const spaces = (data.results || []).map((space: any) => ({ + id: space.id, + name: space.name, + key: space.key, + type: space.type, + status: space.status, + })) + + return NextResponse.json({ spaces }) + } catch (error) { + console.error('Error listing Confluence spaces:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 5d519eceef..9b99fa9f46 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -146,6 +146,22 @@ export async function POST( } } + if (foundWebhook.provider === 'stripe') { + const providerConfig = (foundWebhook.providerConfig as Record) || {} + const eventTypes = providerConfig.eventTypes + + if (eventTypes && Array.isArray(eventTypes) && eventTypes.length > 0) { + const eventType = body?.type + + if (eventType && !eventTypes.includes(eventType)) { + logger.info( + `[${requestId}] Stripe event type '${eventType}' not in allowed list, skipping execution` + ) + return new NextResponse('Event type filtered', { status: 200 }) + } + } + } + return queueWebhookExecution(foundWebhook, foundWorkflow, body, request, { requestId, path, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 267b9817e3..f5bde76be0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -44,8 +44,16 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to your Google Forms', 'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery', 'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage', - 'read:page:confluence': 'Read Confluence pages', - 'write:page:confluence': 'Write Confluence pages', + 'read:confluence-content.all': 'Read all Confluence content', + 'read:confluence-space.summary': 'Read Confluence space information', + 'write:confluence-content': 'Create and edit Confluence pages', + 'write:confluence-space': 'Manage Confluence spaces', + 'write:confluence-file': 'Upload files to Confluence', + 'write:comment:confluence': 'Create and manage comments', + 'write:attachment:confluence': 'Manage attachments', + 'write:label:confluence': 'Add and remove labels', + 'search:confluence': 'Search Confluence content', + 'readonly:content.attachment:confluence': 'View attachments', 'read:me': 'Read your profile information', 'database.read': 'Read your database', 'database.write': 'Write to your database', @@ -86,23 +94,38 @@ const SCOPE_DESCRIPTIONS: Record = { 'read:user:jira': 'Read your Jira user', 'read:field-configuration:jira': 'Read your Jira field configuration', 'read:issue-details:jira': 'Read your Jira issue details', + // New scopes for expanded Jira operations + 'delete:issue:jira': 'Delete Jira issues', + 'write:comment:jira': 'Add and update comments on Jira issues', + 'read:comment:jira': 'Read comments on Jira issues', + 'delete:comment:jira': 'Delete comments from Jira issues', + 'read:attachment:jira': 'Read attachments from Jira issues', + 'delete:attachment:jira': 'Delete attachments from Jira issues', + 'write:issue-worklog:jira': 'Add and update worklog entries on Jira issues', + 'read:issue-worklog:jira': 'Read worklog entries from Jira issues', + 'delete:issue-worklog:jira': 'Delete worklog entries from Jira issues', + 'write:issue-link:jira': 'Create links between Jira issues', + 'delete:issue-link:jira': 'Delete links between Jira issues', 'User.Read': 'Read your Microsoft user', 'Chat.Read': 'Read your Microsoft chats', 'Chat.ReadWrite': 'Write to your Microsoft chats', 'Chat.ReadBasic': 'Read your Microsoft chats', + 'ChatMessage.Send': 'Send chat messages on your behalf', 'Channel.ReadBasic.All': 'Read your Microsoft channels', 'ChannelMessage.Send': 'Write to your Microsoft channels', 'ChannelMessage.Read.All': 'Read your Microsoft channels', + 'ChannelMessage.ReadWrite': 'Read and write to your Microsoft channels', + 'ChannelMember.Read.All': 'Read team channel members', 'Group.Read.All': 'Read your Microsoft groups', 'Group.ReadWrite.All': 'Write to your Microsoft groups', 'Team.ReadBasic.All': 'Read your Microsoft teams', + 'TeamMember.Read.All': 'Read team members', 'Mail.ReadWrite': 'Write to your Microsoft emails', 'Mail.ReadBasic': 'Read your Microsoft emails', 'Mail.Read': 'Read your Microsoft emails', 'Mail.Send': 'Send emails on your behalf', 'Files.Read': 'Read your OneDrive files', 'Files.ReadWrite': 'Read and write your OneDrive files', - 'ChannelMember.Read.All': 'Read team channel members', 'Tasks.ReadWrite': 'Read and manage your Planner tasks', 'Sites.Read.All': 'Read Sharepoint sites', 'Sites.ReadWrite.All': 'Read and write Sharepoint sites', @@ -116,6 +139,20 @@ const SCOPE_DESCRIPTIONS: Record = { guilds: 'Read your Discord guilds', 'guilds.members.read': 'Read your Discord guild members', identity: 'Access your Reddit identity', + submit: 'Submit posts and comments on your behalf', + vote: 'Vote on posts and comments', + save: 'Save and unsave posts and comments', + edit: 'Edit your posts and comments', + subscribe: 'Subscribe and unsubscribe from subreddits', + history: 'Access your Reddit history', + privatemessages: 'Access your inbox and send private messages', + account: 'Update your account preferences and settings', + mysubreddits: 'Access your subscribed and moderated subreddits', + flair: 'Manage user and post flair', + report: 'Report posts and comments for rule violations', + modposts: 'Approve, remove, and moderate posts in subreddits you moderate', + modflair: 'Manage flair in subreddits you moderate', + modmail: 'Access and respond to moderator mail', login: 'Access your Wealthbox account', data: 'Access your Wealthbox data', read: 'Read access to your workspace', diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index dcb040f4e3..ad62ae81a2 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -8,7 +8,8 @@ export const ConfluenceBlock: BlockConfig = { name: 'Confluence', description: 'Interact with Confluence', authMode: AuthMode.OAuth, - longDescription: 'Integrate Confluence into the workflow. Can read and update a page.', + longDescription: + 'Integrate Confluence into the workflow. Can read, create, update, delete pages, manage comments, attachments, labels, and search content.', docsLink: 'https://docs.sim.ai/tools/confluence', category: 'tools', bgColor: '#E0E0E0', @@ -21,7 +22,21 @@ export const ConfluenceBlock: BlockConfig = { layout: 'full', options: [ { label: 'Read Page', id: 'read' }, + { label: 'Create Page', id: 'create' }, { label: 'Update Page', id: 'update' }, + { label: 'Delete Page', id: 'delete' }, + { label: 'Search Content', id: 'search' }, + { label: 'Create Comment', id: 'create_comment' }, + { label: 'List Comments', id: 'list_comments' }, + { label: 'Update Comment', id: 'update_comment' }, + { label: 'Delete Comment', id: 'delete_comment' }, + { label: 'List Attachments', id: 'list_attachments' }, + { label: 'Delete Attachment', id: 'delete_attachment' }, + { label: 'Add Label', id: 'add_label' }, + { label: 'List Labels', id: 'list_labels' }, + { label: 'Remove Label', id: 'remove_label' }, + { label: 'Get Space', id: 'get_space' }, + { label: 'List Spaces', id: 'list_spaces' }, ], value: () => 'read', }, @@ -41,8 +56,16 @@ export const ConfluenceBlock: BlockConfig = { provider: 'confluence', serviceId: 'confluence', requiredScopes: [ - 'read:page:confluence', - 'write:page:confluence', + 'read:confluence-content.all', + 'read:confluence-space.summary', + 'write:confluence-content', + 'write:confluence-space', + 'write:confluence-file', + 'write:comment:confluence', + 'write:attachment:confluence', + 'write:label:confluence', + 'search:confluence', + 'readonly:content.attachment:confluence', 'read:me', 'offline_access', ], @@ -70,48 +93,186 @@ export const ConfluenceBlock: BlockConfig = { placeholder: 'Enter Confluence page ID', mode: 'advanced', }, + { + id: 'spaceId', + title: 'Space ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Confluence space ID', + condition: { field: 'operation', value: ['create', 'get_space'] }, + }, { id: 'title', - title: 'New Title', + title: 'Title', type: 'short-input', layout: 'full', - placeholder: 'Enter new title for the page', - condition: { field: 'operation', value: 'update' }, + placeholder: 'Enter title for the page', + condition: { field: 'operation', value: ['create', 'update'] }, }, { id: 'content', - title: 'New Content', + title: 'Content', + type: 'long-input', + layout: 'full', + placeholder: 'Enter content for the page', + condition: { field: 'operation', value: ['create', 'update'] }, + }, + { + id: 'parentId', + title: 'Parent Page ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter parent page ID (optional)', + condition: { field: 'operation', value: 'create' }, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Enter search query', + required: true, + condition: { field: 'operation', value: 'search' }, + }, + { + id: 'comment', + title: 'Comment Text', type: 'long-input', layout: 'full', - placeholder: 'Enter new content for the page', - condition: { field: 'operation', value: 'update' }, + placeholder: 'Enter comment text', + required: true, + condition: { field: 'operation', value: ['create_comment', 'update_comment'] }, + }, + { + id: 'commentId', + title: 'Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter comment ID', + required: true, + condition: { field: 'operation', value: ['update_comment', 'delete_comment'] }, + }, + { + id: 'attachmentId', + title: 'Attachment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter attachment ID', + required: true, + condition: { field: 'operation', value: 'delete_attachment' }, + }, + { + id: 'labelName', + title: 'Label Name', + type: 'short-input', + layout: 'full', + placeholder: 'Enter label name', + required: true, + condition: { field: 'operation', value: ['add_label', 'remove_label'] }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + layout: 'full', + placeholder: 'Enter maximum number of results (default: 25)', + condition: { + field: 'operation', + value: ['search', 'list_comments', 'list_attachments', 'list_spaces'], + }, }, ], tools: { - access: ['confluence_retrieve', 'confluence_update'], + access: [ + 'confluence_retrieve', + 'confluence_update', + 'confluence_create_page', + 'confluence_delete_page', + 'confluence_search', + 'confluence_create_comment', + 'confluence_list_comments', + 'confluence_update_comment', + 'confluence_delete_comment', + 'confluence_list_attachments', + 'confluence_delete_attachment', + 'confluence_add_label', + 'confluence_list_labels', + 'confluence_remove_label', + 'confluence_get_space', + 'confluence_list_spaces', + ], config: { tool: (params) => { switch (params.operation) { case 'read': return 'confluence_retrieve' + case 'create': + return 'confluence_create_page' case 'update': return 'confluence_update' + case 'delete': + return 'confluence_delete_page' + case 'search': + return 'confluence_search' + case 'create_comment': + return 'confluence_create_comment' + case 'list_comments': + return 'confluence_list_comments' + case 'update_comment': + return 'confluence_update_comment' + case 'delete_comment': + return 'confluence_delete_comment' + case 'list_attachments': + return 'confluence_list_attachments' + case 'delete_attachment': + return 'confluence_delete_attachment' + case 'add_label': + return 'confluence_add_label' + case 'list_labels': + return 'confluence_list_labels' + case 'remove_label': + return 'confluence_remove_label' + case 'get_space': + return 'confluence_get_space' + case 'list_spaces': + return 'confluence_list_spaces' default: return 'confluence_retrieve' } }, params: (params) => { - const { credential, pageId, manualPageId, ...rest } = params + const { credential, pageId, manualPageId, operation, ...rest } = params const effectivePageId = (pageId || manualPageId || '').trim() - if (!effectivePageId) { + // Operations that require pageId + const requiresPageId = [ + 'read', + 'update', + 'delete', + 'create_comment', + 'list_comments', + 'list_attachments', + 'add_label', + 'list_labels', + 'remove_label', + ] + + // Operations that require spaceId + const requiresSpaceId = ['create', 'get_space'] + + if (requiresPageId.includes(operation) && !effectivePageId) { throw new Error('Page ID is required. Please select a page or enter a page ID manually.') } + if (requiresSpaceId.includes(operation) && !rest.spaceId) { + throw new Error('Space ID is required for this operation.') + } + return { credential, - pageId: effectivePageId, + pageId: effectivePageId || undefined, + operation, ...rest, } }, @@ -123,14 +284,40 @@ export const ConfluenceBlock: BlockConfig = { credential: { type: 'string', description: 'Confluence access token' }, pageId: { type: 'string', description: 'Page identifier' }, manualPageId: { type: 'string', description: 'Manual page identifier' }, - title: { type: 'string', description: 'New page title' }, - content: { type: 'string', description: 'New page content' }, + spaceId: { type: 'string', description: 'Space identifier' }, + title: { type: 'string', description: 'Page title' }, + content: { type: 'string', description: 'Page content' }, + parentId: { type: 'string', description: 'Parent page identifier' }, + query: { type: 'string', description: 'Search query' }, + comment: { type: 'string', description: 'Comment text' }, + commentId: { type: 'string', description: 'Comment identifier' }, + attachmentId: { type: 'string', description: 'Attachment identifier' }, + labelName: { type: 'string', description: 'Label name' }, + limit: { type: 'number', description: 'Maximum number of results' }, }, outputs: { ts: { type: 'string', description: 'Timestamp' }, pageId: { type: 'string', description: 'Page identifier' }, content: { type: 'string', description: 'Page content' }, title: { type: 'string', description: 'Page title' }, + url: { type: 'string', description: 'Page or resource URL' }, success: { type: 'boolean', description: 'Operation success status' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + added: { type: 'boolean', description: 'Addition status' }, + removed: { type: 'boolean', description: 'Removal status' }, + updated: { type: 'boolean', description: 'Update status' }, + results: { type: 'array', description: 'Search results' }, + comments: { type: 'array', description: 'List of comments' }, + attachments: { type: 'array', description: 'List of attachments' }, + labels: { type: 'array', description: 'List of labels' }, + spaces: { type: 'array', description: 'List of spaces' }, + commentId: { type: 'string', description: 'Comment identifier' }, + attachmentId: { type: 'string', description: 'Attachment identifier' }, + labelName: { type: 'string', description: 'Label name' }, + spaceId: { type: 'string', description: 'Space identifier' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space or content type' }, + status: { type: 'string', description: 'Space status' }, }, } diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index 78a48619d9..1ee01e75f7 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -9,7 +9,7 @@ export const DiscordBlock: BlockConfig = { description: 'Interact with Discord', authMode: AuthMode.BotToken, longDescription: - 'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information.', + 'Comprehensive Discord integration: messages, threads, channels, roles, members, invites, and webhooks.', category: 'tools', bgColor: '#5865F2', icon: DiscordIcon, @@ -24,6 +24,37 @@ export const DiscordBlock: BlockConfig = { { label: 'Get Channel Messages', id: 'discord_get_messages' }, { label: 'Get Server Information', id: 'discord_get_server' }, { label: 'Get User Information', id: 'discord_get_user' }, + { label: 'Edit Message', id: 'discord_edit_message' }, + { label: 'Delete Message', id: 'discord_delete_message' }, + { label: 'Add Reaction', id: 'discord_add_reaction' }, + { label: 'Remove Reaction', id: 'discord_remove_reaction' }, + { label: 'Pin Message', id: 'discord_pin_message' }, + { label: 'Unpin Message', id: 'discord_unpin_message' }, + { label: 'Create Thread', id: 'discord_create_thread' }, + { label: 'Join Thread', id: 'discord_join_thread' }, + { label: 'Leave Thread', id: 'discord_leave_thread' }, + { label: 'Archive Thread', id: 'discord_archive_thread' }, + { label: 'Create Channel', id: 'discord_create_channel' }, + { label: 'Update Channel', id: 'discord_update_channel' }, + { label: 'Delete Channel', id: 'discord_delete_channel' }, + { label: 'Get Channel', id: 'discord_get_channel' }, + { label: 'Create Role', id: 'discord_create_role' }, + { label: 'Update Role', id: 'discord_update_role' }, + { label: 'Delete Role', id: 'discord_delete_role' }, + { label: 'Assign Role', id: 'discord_assign_role' }, + { label: 'Remove Role', id: 'discord_remove_role' }, + { label: 'Kick Member', id: 'discord_kick_member' }, + { label: 'Ban Member', id: 'discord_ban_member' }, + { label: 'Unban Member', id: 'discord_unban_member' }, + { label: 'Get Member', id: 'discord_get_member' }, + { label: 'Update Member', id: 'discord_update_member' }, + { label: 'Create Invite', id: 'discord_create_invite' }, + { label: 'Get Invite', id: 'discord_get_invite' }, + { label: 'Delete Invite', id: 'discord_delete_invite' }, + { label: 'Create Webhook', id: 'discord_create_webhook' }, + { label: 'Execute Webhook', id: 'discord_execute_webhook' }, + { label: 'Get Webhook', id: 'discord_get_webhook' }, + { label: 'Delete Webhook', id: 'discord_delete_webhook' }, ], value: () => 'discord_send_message', }, @@ -45,12 +76,8 @@ export const DiscordBlock: BlockConfig = { required: true, provider: 'discord', serviceId: 'discord', - condition: { - field: 'operation', - value: ['discord_send_message', 'discord_get_messages', 'discord_get_server'], - }, }, - // Channel ID (single input used in all modes) + // Channel ID - for operations that need it { id: 'channelId', title: 'Channel ID', @@ -60,16 +87,249 @@ export const DiscordBlock: BlockConfig = { required: true, provider: 'discord', serviceId: 'discord', - condition: { field: 'operation', value: ['discord_send_message', 'discord_get_messages'] }, + condition: { + field: 'operation', + value: [ + 'discord_send_message', + 'discord_get_messages', + 'discord_edit_message', + 'discord_delete_message', + 'discord_add_reaction', + 'discord_remove_reaction', + 'discord_pin_message', + 'discord_unpin_message', + 'discord_create_thread', + 'discord_update_channel', + 'discord_delete_channel', + 'discord_get_channel', + 'discord_create_invite', + 'discord_create_webhook', + ], + }, }, + // Message ID - for message operations + { + id: 'messageId', + title: 'Message ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter message ID', + required: true, + condition: { + field: 'operation', + value: [ + 'discord_edit_message', + 'discord_delete_message', + 'discord_add_reaction', + 'discord_remove_reaction', + 'discord_pin_message', + 'discord_unpin_message', + 'discord_create_thread', + ], + }, + }, + // Content - for send/edit message + { + id: 'content', + title: 'Message Content', + type: 'long-input', + layout: 'full', + placeholder: 'Enter message content...', + condition: { + field: 'operation', + value: ['discord_send_message', 'discord_edit_message', 'discord_execute_webhook'], + }, + }, + // Emoji - for reaction operations + { + id: 'emoji', + title: 'Emoji', + type: 'short-input', + layout: 'full', + placeholder: 'Enter emoji (e.g., 👍 or custom:123456789)', + required: true, + condition: { + field: 'operation', + value: ['discord_add_reaction', 'discord_remove_reaction'], + }, + }, + // User ID - for user/member operations { id: 'userId', title: 'User ID', type: 'short-input', layout: 'full', placeholder: 'Enter Discord user ID', - condition: { field: 'operation', value: 'discord_get_user' }, + condition: { + field: 'operation', + value: [ + 'discord_get_user', + 'discord_remove_reaction', + 'discord_assign_role', + 'discord_remove_role', + 'discord_kick_member', + 'discord_ban_member', + 'discord_unban_member', + 'discord_get_member', + 'discord_update_member', + ], + }, }, + // Thread ID - for thread operations + { + id: 'threadId', + title: 'Thread ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter thread ID', + required: true, + condition: { + field: 'operation', + value: ['discord_join_thread', 'discord_leave_thread', 'discord_archive_thread'], + }, + }, + // Thread/Channel Name + { + id: 'name', + title: 'Name', + type: 'short-input', + layout: 'full', + placeholder: 'Enter name', + required: true, + condition: { + field: 'operation', + value: [ + 'discord_create_thread', + 'discord_create_channel', + 'discord_update_channel', + 'discord_create_role', + 'discord_update_role', + 'discord_create_webhook', + ], + }, + }, + // Role ID + { + id: 'roleId', + title: 'Role ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter role ID', + required: true, + condition: { + field: 'operation', + value: [ + 'discord_update_role', + 'discord_delete_role', + 'discord_assign_role', + 'discord_remove_role', + ], + }, + }, + // Webhook ID + { + id: 'webhookId', + title: 'Webhook ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter webhook ID', + required: true, + condition: { + field: 'operation', + value: ['discord_execute_webhook', 'discord_get_webhook', 'discord_delete_webhook'], + }, + }, + // Webhook Token + { + id: 'webhookToken', + title: 'Webhook Token', + type: 'short-input', + layout: 'full', + placeholder: 'Enter webhook token', + required: true, + condition: { + field: 'operation', + value: ['discord_execute_webhook'], + }, + }, + // Invite Code + { + id: 'inviteCode', + title: 'Invite Code', + type: 'short-input', + layout: 'full', + placeholder: 'Enter invite code', + required: true, + condition: { + field: 'operation', + value: ['discord_get_invite', 'discord_delete_invite'], + }, + }, + // Archived (for thread operations) + { + id: 'archived', + title: 'Archived', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Archive', id: 'true' }, + { label: 'Unarchive', id: 'false' }, + ], + value: () => 'true', + condition: { + field: 'operation', + value: ['discord_archive_thread'], + }, + }, + // Topic (for channels) + { + id: 'topic', + title: 'Topic', + type: 'long-input', + layout: 'full', + placeholder: 'Enter channel topic (optional)', + condition: { + field: 'operation', + value: ['discord_create_channel', 'discord_update_channel'], + }, + }, + // Color (for roles) + { + id: 'color', + title: 'Color', + type: 'short-input', + layout: 'full', + placeholder: 'Enter color as integer (e.g., 16711680 for red)', + condition: { + field: 'operation', + value: ['discord_create_role', 'discord_update_role'], + }, + }, + // Nickname (for member update) + { + id: 'nick', + title: 'Nickname', + type: 'short-input', + layout: 'full', + placeholder: 'Enter new nickname', + condition: { + field: 'operation', + value: ['discord_update_member'], + }, + }, + // Reason (for moderation actions) + { + id: 'reason', + title: 'Reason', + type: 'short-input', + layout: 'full', + placeholder: 'Enter reason for this action', + condition: { + field: 'operation', + value: ['discord_kick_member', 'discord_ban_member', 'discord_unban_member'], + }, + }, + // Limit (for get messages) { id: 'limit', title: 'Message Limit', @@ -78,15 +338,196 @@ export const DiscordBlock: BlockConfig = { placeholder: 'Number of messages (default: 10, max: 100)', condition: { field: 'operation', value: 'discord_get_messages' }, }, + // Auto Archive Duration (for threads) { - id: 'content', - title: 'Message Content', - type: 'long-input', + id: 'autoArchiveDuration', + title: 'Auto Archive Duration (minutes)', + type: 'dropdown', layout: 'full', - placeholder: 'Enter message content...', - condition: { field: 'operation', value: 'discord_send_message' }, + options: [ + { label: '1 hour (60 minutes)', id: '60' }, + { label: '24 hours (1440 minutes)', id: '1440' }, + { label: '3 days (4320 minutes)', id: '4320' }, + { label: '1 week (10080 minutes)', id: '10080' }, + ], + value: () => '1440', + condition: { + field: 'operation', + value: ['discord_create_thread'], + }, + }, + // Channel Type (for create_channel) + { + id: 'channelType', + title: 'Channel Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Text Channel', id: '0' }, + { label: 'Voice Channel', id: '2' }, + { label: 'Announcement Channel', id: '5' }, + { label: 'Stage Channel', id: '13' }, + { label: 'Forum Channel', id: '15' }, + ], + value: () => '0', + condition: { + field: 'operation', + value: ['discord_create_channel'], + }, + }, + // Parent ID (for create_channel) + { + id: 'parentId', + title: 'Parent Category ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter parent category ID (optional)', + condition: { + field: 'operation', + value: ['discord_create_channel'], + }, }, - // File upload (basic mode) + // Hoist (for roles) + { + id: 'hoist', + title: 'Display Separately', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Yes - Display role members separately', id: 'true' }, + { label: "No - Don't display separately", id: 'false' }, + ], + value: () => 'false', + condition: { + field: 'operation', + value: ['discord_create_role', 'discord_update_role'], + }, + }, + // Mentionable (for roles) + { + id: 'mentionable', + title: 'Mentionable', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Yes - Role can be mentioned', id: 'true' }, + { label: 'No - Role cannot be mentioned', id: 'false' }, + ], + value: () => 'true', + condition: { + field: 'operation', + value: ['discord_create_role', 'discord_update_role'], + }, + }, + // Delete Message Days (for ban_member) + { + id: 'deleteMessageDays', + title: 'Delete Message History', + type: 'dropdown', + layout: 'full', + options: [ + { label: "Don't delete any messages", id: '0' }, + { label: 'Delete messages from last 1 day', id: '1' }, + { label: 'Delete messages from last 7 days', id: '7' }, + ], + value: () => '0', + condition: { + field: 'operation', + value: ['discord_ban_member'], + }, + }, + // Mute (for update_member) + { + id: 'mute', + title: 'Server Mute', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Mute member', id: 'true' }, + { label: 'Unmute member', id: 'false' }, + ], + condition: { + field: 'operation', + value: ['discord_update_member'], + }, + }, + // Deaf (for update_member) + { + id: 'deaf', + title: 'Server Deafen', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Deafen member', id: 'true' }, + { label: 'Undeafen member', id: 'false' }, + ], + condition: { + field: 'operation', + value: ['discord_update_member'], + }, + }, + // Max Age (for create_invite) + { + id: 'maxAge', + title: 'Invite Expiration', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Never expire', id: '0' }, + { label: '30 minutes', id: '1800' }, + { label: '1 hour', id: '3600' }, + { label: '6 hours', id: '21600' }, + { label: '12 hours', id: '43200' }, + { label: '1 day', id: '86400' }, + { label: '7 days', id: '604800' }, + ], + value: () => '86400', + condition: { + field: 'operation', + value: ['discord_create_invite'], + }, + }, + // Max Uses (for create_invite) + { + id: 'maxUses', + title: 'Max Uses', + type: 'short-input', + layout: 'full', + placeholder: 'Maximum number of uses (0 = unlimited)', + condition: { + field: 'operation', + value: ['discord_create_invite'], + }, + }, + // Temporary (for create_invite) + { + id: 'temporary', + title: 'Temporary Membership', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'No - Grant permanent membership', id: 'false' }, + { label: 'Yes - Kick on disconnect if no role', id: 'true' }, + ], + value: () => 'false', + condition: { + field: 'operation', + value: ['discord_create_invite'], + }, + }, + // Username (for execute_webhook) + { + id: 'username', + title: 'Override Username', + type: 'short-input', + layout: 'full', + placeholder: 'Custom username to display (optional)', + condition: { + field: 'operation', + value: ['discord_execute_webhook'], + }, + }, + // File attachments { id: 'attachmentFiles', title: 'Attachments', @@ -99,7 +540,6 @@ export const DiscordBlock: BlockConfig = { multiple: true, required: false, }, - // Variable reference (advanced mode) { id: 'files', title: 'File Attachments', @@ -118,75 +558,219 @@ export const DiscordBlock: BlockConfig = { 'discord_get_messages', 'discord_get_server', 'discord_get_user', + 'discord_edit_message', + 'discord_delete_message', + 'discord_add_reaction', + 'discord_remove_reaction', + 'discord_pin_message', + 'discord_unpin_message', + 'discord_create_thread', + 'discord_join_thread', + 'discord_leave_thread', + 'discord_archive_thread', + 'discord_create_channel', + 'discord_update_channel', + 'discord_delete_channel', + 'discord_get_channel', + 'discord_create_role', + 'discord_update_role', + 'discord_delete_role', + 'discord_assign_role', + 'discord_remove_role', + 'discord_kick_member', + 'discord_ban_member', + 'discord_unban_member', + 'discord_get_member', + 'discord_update_member', + 'discord_create_invite', + 'discord_get_invite', + 'discord_delete_invite', + 'discord_create_webhook', + 'discord_execute_webhook', + 'discord_get_webhook', + 'discord_delete_webhook', ], config: { tool: (params) => { - switch (params.operation) { - case 'discord_send_message': - return 'discord_send_message' - case 'discord_get_messages': - return 'discord_get_messages' - case 'discord_get_server': - return 'discord_get_server' - case 'discord_get_user': - return 'discord_get_user' - default: - return 'discord_send_message' - } + return params.operation || 'discord_send_message' }, params: (params) => { - const commonParams: Record = {} - - if (!params.botToken) throw new Error('Bot token required for this operation') - commonParams.botToken = params.botToken + const commonParams: Record = { + botToken: params.botToken, + serverId: params.serverId, + } - // Single inputs - const serverId = (params.serverId || '').trim() - const channelId = (params.channelId || '').trim() + if (!params.botToken) throw new Error('Bot token is required') + if (!params.serverId) throw new Error('Server ID is required') switch (params.operation) { - case 'discord_send_message': { - if (!serverId) { - throw new Error('Server ID is required.') + case 'discord_send_message': + return { + ...commonParams, + channelId: params.channelId, + content: params.content, + files: params.attachmentFiles || params.files, } - if (!channelId) { - throw new Error('Channel ID is required.') + case 'discord_get_messages': + return { + ...commonParams, + channelId: params.channelId, + limit: params.limit ? Math.min(Math.max(1, Number(params.limit)), 100) : 10, } - const fileParam = params.attachmentFiles || params.files + case 'discord_get_server': + return commonParams + case 'discord_get_user': + return { ...commonParams, userId: params.userId } + case 'discord_edit_message': return { ...commonParams, - serverId, - channelId, + channelId: params.channelId, + messageId: params.messageId, content: params.content, - ...(fileParam && { files: fileParam }), } - } - case 'discord_get_messages': - if (!serverId) { - throw new Error('Server ID is required.') + case 'discord_delete_message': + return { + ...commonParams, + channelId: params.channelId, + messageId: params.messageId, } - if (!channelId) { - throw new Error('Channel ID is required.') + case 'discord_add_reaction': + case 'discord_remove_reaction': + return { + ...commonParams, + channelId: params.channelId, + messageId: params.messageId, + emoji: params.emoji, + ...(params.userId && { userId: params.userId }), } + case 'discord_pin_message': + case 'discord_unpin_message': return { ...commonParams, - serverId, - channelId, - limit: params.limit ? Math.min(Math.max(1, Number(params.limit)), 100) : 10, + channelId: params.channelId, + messageId: params.messageId, } - case 'discord_get_server': - if (!serverId) { - throw new Error('Server ID is required.') + case 'discord_create_thread': + return { + ...commonParams, + channelId: params.channelId, + name: params.name, + ...(params.messageId && { messageId: params.messageId }), + ...(params.autoArchiveDuration && { + autoArchiveDuration: Number(params.autoArchiveDuration), + }), } + case 'discord_join_thread': + case 'discord_leave_thread': + return { ...commonParams, threadId: params.threadId } + case 'discord_archive_thread': return { ...commonParams, - serverId, + threadId: params.threadId, + archived: params.archived === 'true', } - case 'discord_get_user': + case 'discord_create_channel': + return { + ...commonParams, + name: params.name, + ...(params.topic && { topic: params.topic }), + ...(params.channelType && { type: Number(params.channelType) }), + ...(params.parentId && { parentId: params.parentId }), + } + case 'discord_update_channel': + return { + ...commonParams, + channelId: params.channelId, + ...(params.name && { name: params.name }), + ...(params.topic !== undefined && { topic: params.topic }), + } + case 'discord_delete_channel': + case 'discord_get_channel': + return { ...commonParams, channelId: params.channelId } + case 'discord_create_role': + return { + ...commonParams, + name: params.name, + ...(params.color && { color: Number(params.color) }), + ...(params.hoist !== undefined && { hoist: params.hoist === 'true' }), + ...(params.mentionable !== undefined && { + mentionable: params.mentionable === 'true', + }), + } + case 'discord_update_role': + return { + ...commonParams, + roleId: params.roleId, + ...(params.name && { name: params.name }), + ...(params.color && { color: Number(params.color) }), + ...(params.hoist !== undefined && { hoist: params.hoist === 'true' }), + ...(params.mentionable !== undefined && { + mentionable: params.mentionable === 'true', + }), + } + case 'discord_delete_role': + return { ...commonParams, roleId: params.roleId } + case 'discord_assign_role': + case 'discord_remove_role': + return { + ...commonParams, + userId: params.userId, + roleId: params.roleId, + } + case 'discord_kick_member': + case 'discord_unban_member': + return { + ...commonParams, + userId: params.userId, + ...(params.reason && { reason: params.reason }), + } + case 'discord_ban_member': return { ...commonParams, userId: params.userId, + ...(params.reason && { reason: params.reason }), + ...(params.deleteMessageDays && { + deleteMessageDays: Number(params.deleteMessageDays), + }), + } + case 'discord_get_member': + return { ...commonParams, userId: params.userId } + case 'discord_update_member': + return { + ...commonParams, + userId: params.userId, + ...(params.nick !== undefined && { nick: params.nick }), + ...(params.mute !== undefined && { mute: params.mute === 'true' }), + ...(params.deaf !== undefined && { deaf: params.deaf === 'true' }), + } + case 'discord_create_invite': + return { + ...commonParams, + channelId: params.channelId, + ...(params.maxAge !== undefined && { maxAge: Number(params.maxAge) }), + ...(params.maxUses !== undefined && { maxUses: Number(params.maxUses) }), + ...(params.temporary !== undefined && { temporary: params.temporary === 'true' }), } + case 'discord_get_invite': + case 'discord_delete_invite': + return { ...commonParams, inviteCode: params.inviteCode } + case 'discord_create_webhook': + return { + ...commonParams, + channelId: params.channelId, + name: params.name, + } + case 'discord_execute_webhook': + return { + ...commonParams, + webhookId: params.webhookId, + webhookToken: params.webhookToken, + content: params.content, + ...(params.username && { username: params.username }), + } + case 'discord_get_webhook': + case 'discord_delete_webhook': + return { ...commonParams, webhookId: params.webhookId } default: return commonParams } @@ -198,14 +782,39 @@ export const DiscordBlock: BlockConfig = { botToken: { type: 'string', description: 'Discord bot token' }, serverId: { type: 'string', description: 'Discord server identifier' }, channelId: { type: 'string', description: 'Discord channel identifier' }, + messageId: { type: 'string', description: 'Discord message identifier' }, + threadId: { type: 'string', description: 'Discord thread identifier' }, + userId: { type: 'string', description: 'Discord user identifier' }, + roleId: { type: 'string', description: 'Discord role identifier' }, + webhookId: { type: 'string', description: 'Discord webhook identifier' }, + webhookToken: { type: 'string', description: 'Discord webhook token' }, + inviteCode: { type: 'string', description: 'Discord invite code' }, content: { type: 'string', description: 'Message content' }, + emoji: { type: 'string', description: 'Emoji for reaction' }, + name: { type: 'string', description: 'Name for channel/role/thread/webhook' }, + topic: { type: 'string', description: 'Channel topic' }, + color: { type: 'string', description: 'Role color as integer' }, + nick: { type: 'string', description: 'Member nickname' }, + reason: { type: 'string', description: 'Reason for moderation action' }, + archived: { type: 'string', description: 'Archive status (true/false)' }, attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, files: { type: 'json', description: 'Files to attach (UserFile array)' }, limit: { type: 'number', description: 'Message limit' }, - userId: { type: 'string', description: 'Discord user identifier' }, + autoArchiveDuration: { type: 'number', description: 'Thread auto-archive duration in minutes' }, + channelType: { type: 'number', description: 'Discord channel type (0=text, 2=voice, etc.)' }, + parentId: { type: 'string', description: 'Parent category ID for channel' }, + hoist: { type: 'boolean', description: 'Whether to display role members separately' }, + mentionable: { type: 'boolean', description: 'Whether role can be mentioned' }, + deleteMessageDays: { type: 'number', description: 'Days of message history to delete on ban' }, + mute: { type: 'boolean', description: 'Server mute status' }, + deaf: { type: 'boolean', description: 'Server deafen status' }, + maxAge: { type: 'number', description: 'Invite expiration time in seconds' }, + maxUses: { type: 'number', description: 'Maximum number of invite uses' }, + temporary: { type: 'boolean', description: 'Whether invite grants temporary membership' }, + username: { type: 'string', description: 'Custom username for webhook execution' }, }, outputs: { - message: { type: 'string', description: 'Message content' }, + message: { type: 'string', description: 'Status message' }, data: { type: 'json', description: 'Response data' }, }, } diff --git a/apps/sim/blocks/blocks/exa.ts b/apps/sim/blocks/blocks/exa.ts index eb39302ce6..3d51add37c 100644 --- a/apps/sim/blocks/blocks/exa.ts +++ b/apps/sim/blocks/blocks/exa.ts @@ -68,6 +68,108 @@ export const ExaBlock: BlockConfig = { value: () => 'auto', condition: { field: 'operation', value: 'exa_search' }, }, + { + id: 'includeDomains', + title: 'Include Domains', + type: 'long-input', + layout: 'full', + placeholder: 'example.com, another.com (comma-separated)', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'excludeDomains', + title: 'Exclude Domains', + type: 'long-input', + layout: 'full', + placeholder: 'exclude.com, another.com (comma-separated)', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'startPublishedDate', + title: 'Start Published Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-01-01', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'endPublishedDate', + title: 'End Published Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-12-31', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'startCrawlDate', + title: 'Start Crawl Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-01-01', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'endCrawlDate', + title: 'End Crawl Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-12-31', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'category', + title: 'Category Filter', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'None', id: '' }, + { label: 'Company', id: 'company' }, + { label: 'Research Paper', id: 'research_paper' }, + { label: 'News Article', id: 'news_article' }, + { label: 'PDF', id: 'pdf' }, + { label: 'GitHub', id: 'github' }, + { label: 'Tweet', id: 'tweet' }, + { label: 'Movie', id: 'movie' }, + { label: 'Song', id: 'song' }, + { label: 'Personal Site', id: 'personal_site' }, + ], + value: () => '', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'text', + title: 'Include Text', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'highlights', + title: 'Include Highlights', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'summary', + title: 'Include Summary', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_search' }, + }, + { + id: 'livecrawl', + title: 'Live Crawl Mode', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Never (default)', id: 'never' }, + { label: 'Fallback', id: 'fallback' }, + { label: 'Always', id: 'always' }, + ], + value: () => 'never', + condition: { field: 'operation', value: 'exa_search' }, + }, // Get Contents operation inputs { id: 'urls', @@ -93,6 +195,29 @@ export const ExaBlock: BlockConfig = { placeholder: 'Enter a query to guide the summary generation...', condition: { field: 'operation', value: 'exa_get_contents' }, }, + { + id: 'subpages', + title: 'Number of Subpages', + type: 'short-input', + layout: 'full', + placeholder: '5', + condition: { field: 'operation', value: 'exa_get_contents' }, + }, + { + id: 'subpageTarget', + title: 'Subpage Target Keywords', + type: 'long-input', + layout: 'full', + placeholder: 'docs, tutorial, about (comma-separated)', + condition: { field: 'operation', value: 'exa_get_contents' }, + }, + { + id: 'highlights', + title: 'Include Highlights', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_get_contents' }, + }, // Find Similar Links operation inputs { id: 'url', @@ -118,6 +243,108 @@ export const ExaBlock: BlockConfig = { layout: 'full', condition: { field: 'operation', value: 'exa_find_similar_links' }, }, + { + id: 'includeDomains', + title: 'Include Domains', + type: 'long-input', + layout: 'full', + placeholder: 'example.com, another.com (comma-separated)', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'excludeDomains', + title: 'Exclude Domains', + type: 'long-input', + layout: 'full', + placeholder: 'exclude.com, another.com (comma-separated)', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'excludeSourceDomain', + title: 'Exclude Source Domain', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'startPublishedDate', + title: 'Start Published Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-01-01', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'endPublishedDate', + title: 'End Published Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-12-31', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'startCrawlDate', + title: 'Start Crawl Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-01-01', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'endCrawlDate', + title: 'End Crawl Date', + type: 'short-input', + layout: 'full', + placeholder: '2024-12-31', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'category', + title: 'Category Filter', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'None', id: '' }, + { label: 'Company', id: 'company' }, + { label: 'Research Paper', id: 'research_paper' }, + { label: 'News Article', id: 'news_article' }, + { label: 'PDF', id: 'pdf' }, + { label: 'GitHub', id: 'github' }, + { label: 'Tweet', id: 'tweet' }, + { label: 'Movie', id: 'movie' }, + { label: 'Song', id: 'song' }, + { label: 'Personal Site', id: 'personal_site' }, + ], + value: () => '', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'highlights', + title: 'Include Highlights', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'summary', + title: 'Include Summary', + type: 'switch', + layout: 'full', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, + { + id: 'livecrawl', + title: 'Live Crawl Mode', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Never (default)', id: 'never' }, + { label: 'Fallback', id: 'fallback' }, + { label: 'Always', id: 'always' }, + ], + value: () => 'never', + condition: { field: 'operation', value: 'exa_find_similar_links' }, + }, // Answer operation inputs { id: 'query', @@ -146,10 +373,16 @@ export const ExaBlock: BlockConfig = { required: true, }, { - id: 'includeText', - title: 'Include Full Text', - type: 'switch', + id: 'model', + title: 'Research Model', + type: 'dropdown', layout: 'full', + options: [ + { label: 'Standard (default)', id: 'exa-research' }, + { label: 'Fast', id: 'exa-research-fast' }, + { label: 'Pro', id: 'exa-research-pro' }, + ], + value: () => 'exa-research', condition: { field: 'operation', value: 'exa_research' }, }, // API Key (common) @@ -178,6 +411,11 @@ export const ExaBlock: BlockConfig = { params.numResults = Number(params.numResults) } + // Convert subpages to a number if provided + if (params.subpages) { + params.subpages = Number(params.subpages) + } + switch (params.operation) { case 'exa_search': return 'exa_search' @@ -203,12 +441,27 @@ export const ExaBlock: BlockConfig = { numResults: { type: 'number', description: 'Number of results' }, useAutoprompt: { type: 'boolean', description: 'Use autoprompt feature' }, type: { type: 'string', description: 'Search type' }, + includeDomains: { type: 'string', description: 'Include domains filter' }, + excludeDomains: { type: 'string', description: 'Exclude domains filter' }, + startPublishedDate: { type: 'string', description: 'Start published date filter' }, + endPublishedDate: { type: 'string', description: 'End published date filter' }, + startCrawlDate: { type: 'string', description: 'Start crawl date filter' }, + endCrawlDate: { type: 'string', description: 'End crawl date filter' }, + category: { type: 'string', description: 'Category filter' }, + text: { type: 'boolean', description: 'Include text content' }, + highlights: { type: 'boolean', description: 'Include highlights' }, + summary: { type: 'boolean', description: 'Include summary' }, + livecrawl: { type: 'string', description: 'Live crawl mode' }, // Get Contents operation urls: { type: 'string', description: 'URLs to retrieve' }, - text: { type: 'boolean', description: 'Include text content' }, summaryQuery: { type: 'string', description: 'Summary query guidance' }, + subpages: { type: 'number', description: 'Number of subpages to crawl' }, + subpageTarget: { type: 'string', description: 'Subpage target keywords' }, // Find Similar Links operation url: { type: 'string', description: 'Source URL' }, + excludeSourceDomain: { type: 'boolean', description: 'Exclude source domain' }, + // Research operation + model: { type: 'string', description: 'Research model selection' }, }, outputs: { // Search output diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index 6487f5d213..e713c03cad 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -6,9 +6,10 @@ import type { FirecrawlResponse } from '@/tools/firecrawl/types' export const FirecrawlBlock: BlockConfig = { type: 'firecrawl', name: 'Firecrawl', - description: 'Scrape or search the web', + description: 'Scrape, search, crawl, map, and extract web data', authMode: AuthMode.ApiKey, - longDescription: 'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites.', + longDescription: + 'Integrate Firecrawl into the workflow. Can scrape pages, search the web, crawl entire websites, map URL structures, and extract structured data using AI.', docsLink: 'https://docs.sim.ai/tools/firecrawl', category: 'tools', bgColor: '#181C1E', @@ -23,6 +24,8 @@ export const FirecrawlBlock: BlockConfig = { { label: 'Scrape', id: 'scrape' }, { label: 'Search', id: 'search' }, { label: 'Crawl', id: 'crawl' }, + { label: 'Map', id: 'map' }, + { label: 'Extract', id: 'extract' }, ], value: () => 'scrape', }, @@ -34,10 +37,57 @@ export const FirecrawlBlock: BlockConfig = { placeholder: 'Enter the website URL', condition: { field: 'operation', - value: ['scrape', 'crawl'], + value: ['scrape', 'crawl', 'map'], }, required: true, }, + { + id: 'urls', + title: 'URLs', + type: 'long-input', + layout: 'full', + placeholder: + 'Enter URLs as JSON array (e.g., ["https://example.com", "https://example.com/about"])', + condition: { + field: 'operation', + value: 'extract', + }, + required: true, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the search query', + condition: { + field: 'operation', + value: 'search', + }, + required: true, + }, + { + id: 'prompt', + title: 'Prompt', + type: 'long-input', + layout: 'full', + placeholder: 'Natural language instruction for extraction or crawling', + condition: { + field: 'operation', + value: ['extract', 'crawl'], + }, + }, + { + id: 'schema', + title: 'Schema', + type: 'long-input', + layout: 'full', + placeholder: 'JSON Schema for data extraction', + condition: { + field: 'operation', + value: 'extract', + }, + }, { id: 'onlyMainContent', title: 'Only Main Content', @@ -48,28 +98,142 @@ export const FirecrawlBlock: BlockConfig = { value: 'scrape', }, }, + { + id: 'formats', + title: 'Output Formats', + type: 'long-input', + layout: 'half', + placeholder: '["markdown", "html"]', + condition: { + field: 'operation', + value: 'scrape', + }, + }, { id: 'limit', - title: 'Page Limit', + title: 'Limit', type: 'short-input', layout: 'half', placeholder: '100', + condition: { + field: 'operation', + value: ['crawl', 'search', 'map'], + }, + }, + { + id: 'timeout', + title: 'Timeout (ms)', + type: 'short-input', + layout: 'half', + placeholder: '60000', + condition: { + field: 'operation', + value: ['scrape', 'search', 'map'], + }, + }, + { + id: 'mobile', + title: 'Mobile Mode', + type: 'switch', + layout: 'half', + condition: { + field: 'operation', + value: 'scrape', + }, + }, + { + id: 'blockAds', + title: 'Block Ads', + type: 'switch', + layout: 'half', + condition: { + field: 'operation', + value: 'scrape', + }, + }, + { + id: 'waitFor', + title: 'Wait For (ms)', + type: 'short-input', + layout: 'half', + placeholder: '0', + condition: { + field: 'operation', + value: 'scrape', + }, + }, + { + id: 'excludePaths', + title: 'Exclude Paths', + type: 'long-input', + layout: 'full', + placeholder: '["^/admin", "^/private"]', condition: { field: 'operation', value: 'crawl', }, }, { - id: 'query', - title: 'Search Query', + id: 'includePaths', + title: 'Include Paths', + type: 'long-input', + layout: 'full', + placeholder: '["^/blog", "^/docs"]', + condition: { + field: 'operation', + value: 'crawl', + }, + }, + { + id: 'allowSubdomains', + title: 'Allow Subdomains', + type: 'switch', + layout: 'half', + condition: { + field: 'operation', + value: 'crawl', + }, + }, + { + id: 'allowExternalLinks', + title: 'Allow External Links', + type: 'switch', + layout: 'half', + condition: { + field: 'operation', + value: 'crawl', + }, + }, + { + id: 'search', + title: 'Search Filter', type: 'short-input', layout: 'full', - placeholder: 'Enter the search query', + placeholder: 'Filter results by relevance (e.g., "blog")', condition: { field: 'operation', - value: 'search', + value: 'map', + }, + }, + { + id: 'includeSubdomains', + title: 'Include Subdomains', + type: 'switch', + layout: 'half', + condition: { + field: 'operation', + value: ['map', 'extract'], + }, + }, + { + id: 'showSources', + title: 'Show Sources', + type: 'switch', + layout: 'half', + condition: { + field: 'operation', + value: 'extract', }, - required: true, }, { id: 'apiKey', @@ -82,7 +246,13 @@ export const FirecrawlBlock: BlockConfig = { }, ], tools: { - access: ['firecrawl_scrape', 'firecrawl_search', 'firecrawl_crawl'], + access: [ + 'firecrawl_scrape', + 'firecrawl_search', + 'firecrawl_crawl', + 'firecrawl_map', + 'firecrawl_extract', + ], config: { tool: (params) => { switch (params.operation) { @@ -92,22 +262,58 @@ export const FirecrawlBlock: BlockConfig = { return 'firecrawl_search' case 'crawl': return 'firecrawl_crawl' + case 'map': + return 'firecrawl_map' + case 'extract': + return 'firecrawl_extract' default: return 'firecrawl_scrape' } }, params: (params) => { - const { operation, limit, ...rest } = params + const { operation, limit, urls, formats, schema, ...rest } = params - switch (operation) { - case 'crawl': - return { - ...rest, - limit: limit ? Number.parseInt(limit) : undefined, - } - default: - return rest + // Parse JSON fields if provided as strings + const parsedParams: Record = { ...rest } + + // Handle limit as number + if (limit) { + parsedParams.limit = Number.parseInt(limit) } + + // Handle JSON array fields + if (urls && typeof urls === 'string') { + try { + parsedParams.urls = JSON.parse(urls) + } catch { + parsedParams.urls = [urls] + } + } else if (urls) { + parsedParams.urls = urls + } + + if (formats && typeof formats === 'string') { + try { + parsedParams.formats = JSON.parse(formats) + } catch { + parsedParams.formats = ['markdown'] + } + } else if (formats) { + parsedParams.formats = formats + } + + if (schema && typeof schema === 'string') { + try { + parsedParams.schema = JSON.parse(schema) + } catch { + // Keep as string if not valid JSON + parsedParams.schema = schema + } + } else if (schema) { + parsedParams.schema = schema + } + + return parsedParams }, }, }, @@ -115,21 +321,42 @@ export const FirecrawlBlock: BlockConfig = { apiKey: { type: 'string', description: 'Firecrawl API key' }, operation: { type: 'string', description: 'Operation to perform' }, url: { type: 'string', description: 'Target website URL' }, - limit: { type: 'string', description: 'Page crawl limit' }, + urls: { type: 'json', description: 'Array of URLs for extraction' }, query: { type: 'string', description: 'Search query terms' }, - scrapeOptions: { type: 'json', description: 'Scraping options' }, + prompt: { type: 'string', description: 'Natural language instruction' }, + schema: { type: 'json', description: 'JSON Schema for extraction' }, + limit: { type: 'string', description: 'Result/page limit' }, + formats: { type: 'json', description: 'Output formats array' }, + onlyMainContent: { type: 'boolean', description: 'Extract only main content' }, + timeout: { type: 'number', description: 'Request timeout in ms' }, + mobile: { type: 'boolean', description: 'Use mobile emulation' }, + blockAds: { type: 'boolean', description: 'Block ads and popups' }, + waitFor: { type: 'number', description: 'Wait time before scraping' }, + excludePaths: { type: 'json', description: 'Paths to exclude from crawl' }, + includePaths: { type: 'json', description: 'Paths to include in crawl' }, + allowSubdomains: { type: 'boolean', description: 'Allow subdomain crawling' }, + allowExternalLinks: { type: 'boolean', description: 'Allow external links' }, + search: { type: 'string', description: 'Search filter for map' }, + includeSubdomains: { type: 'boolean', description: 'Include subdomains' }, + showSources: { type: 'boolean', description: 'Show data sources' }, + scrapeOptions: { type: 'json', description: 'Advanced scraping options' }, }, outputs: { - // Scrape output + // Scrape outputs markdown: { type: 'string', description: 'Page content markdown' }, html: { type: 'string', description: 'Raw HTML content' }, metadata: { type: 'json', description: 'Page metadata' }, - // Search output - data: { type: 'json', description: 'Search results data' }, + // Search outputs + data: { type: 'json', description: 'Search results or extracted data' }, warning: { type: 'string', description: 'Warning messages' }, - // Crawl output + // Crawl outputs pages: { type: 'json', description: 'Crawled pages data' }, total: { type: 'number', description: 'Total pages found' }, creditsUsed: { type: 'number', description: 'Credits consumed' }, + // Map outputs + success: { type: 'boolean', description: 'Operation success status' }, + links: { type: 'json', description: 'Array of discovered URLs' }, + // Extract outputs + sources: { type: 'json', description: 'Data sources array' }, }, } diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 7f1a22e30d..e7bc4f0430 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -199,7 +199,7 @@ export const GoogleCalendarBlock: BlockConfig = { value: ['create', 'quick_add', 'invite'], }, options: [ - { label: 'All attendees (recommended)', id: 'all' }, + { label: 'All attendees', id: 'all' }, { label: 'External attendees only', id: 'externalOnly' }, { label: 'None (no emails sent)', id: 'none' }, ], diff --git a/apps/sim/blocks/blocks/jina.ts b/apps/sim/blocks/blocks/jina.ts index 7831555eee..5e6acd1847 100644 --- a/apps/sim/blocks/blocks/jina.ts +++ b/apps/sim/blocks/blocks/jina.ts @@ -1,37 +1,330 @@ import { JinaAIIcon } from '@/components/icons' import { AuthMode, type BlockConfig } from '@/blocks/types' -import type { ReadUrlResponse } from '@/tools/jina/types' +import type { ReadUrlResponse, SearchResponse } from '@/tools/jina/types' -export const JinaBlock: BlockConfig = { +export const JinaBlock: BlockConfig = { type: 'jina', name: 'Jina', - description: 'Convert website content into text', + description: 'Search the web or extract content from URLs', authMode: AuthMode.ApiKey, - longDescription: 'Integrate Jina into the workflow. Extracts content from websites.', + longDescription: + 'Integrate Jina AI into the workflow. Search the web and get LLM-friendly results, or extract clean content from specific URLs with advanced parsing options.', docsLink: 'https://docs.sim.ai/tools/jina', category: 'tools', bgColor: '#333333', icon: JinaAIIcon, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Read URL', id: 'jina_read_url' }, + { label: 'Search', id: 'jina_search' }, + ], + value: () => 'jina_read_url', + }, + // Read URL params { id: 'url', title: 'URL', type: 'short-input', layout: 'full', required: true, - placeholder: 'Enter URL to extract content from', + placeholder: 'https://example.com', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'targetSelector', + title: 'Target Selector', + type: 'short-input', + layout: 'full', + placeholder: '#main-content (CSS selector)', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'waitForSelector', + title: 'Wait For Selector', + type: 'short-input', + layout: 'full', + placeholder: '.dynamic-content (CSS selector)', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'removeSelector', + title: 'Remove Selector', + type: 'short-input', + layout: 'full', + placeholder: 'header, footer, .ad (CSS selector)', + condition: { field: 'operation', value: 'jina_read_url' }, }, { - id: 'options', + id: 'timeout', + title: 'Timeout (seconds)', + type: 'short-input', + layout: 'full', + placeholder: '30', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'returnFormat', + title: 'Return Format', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Markdown', id: 'markdown' }, + { label: 'HTML', id: 'html' }, + { label: 'Text', id: 'text' }, + { label: 'Screenshot', id: 'screenshot' }, + { label: 'Pageshot', id: 'pageshot' }, + ], + value: () => 'markdown', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'retainImages', + title: 'Retain Images', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'All', id: 'all' }, + { label: 'None', id: 'none' }, + ], + value: () => 'all', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'engine', + title: 'Engine', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Browser', id: 'browser' }, + { label: 'Direct', id: 'direct' }, + { label: 'CF Browser Rendering', id: 'cf-browser-rendering' }, + ], + value: () => 'browser', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'proxy', + title: 'Proxy (Country Code)', + type: 'short-input', + layout: 'full', + placeholder: 'US, UK, auto, none', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'proxyUrl', + title: 'Proxy URL', + type: 'short-input', + layout: 'full', + placeholder: 'http://proxy.example.com:8080', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'setCookie', + title: 'Cookies', + type: 'short-input', + layout: 'full', + placeholder: 'session=abc123; token=xyz', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'tokenBudget', + title: 'Token Budget', + type: 'short-input', + layout: 'full', + placeholder: '10000', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'cacheTolerance', + title: 'Cache Tolerance (seconds)', + type: 'short-input', + layout: 'full', + placeholder: '3600', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'locale', + title: 'Locale', + type: 'short-input', + layout: 'full', + placeholder: 'en-US', + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'baseUrl', + title: 'Base URL', + type: 'dropdown', + layout: 'full', + options: [{ label: 'Final (Follow Redirects)', id: 'final' }], + condition: { field: 'operation', value: 'jina_read_url' }, + }, + { + id: 'readUrlOptions', title: 'Options', type: 'checkbox-list', layout: 'full', options: [ - { label: 'Use Reader LM v2', id: 'useReaderLMv2' }, + { label: 'Use Reader LM v2 (3x cost)', id: 'useReaderLMv2' }, { label: 'Gather Links', id: 'gatherLinks' }, + { label: 'Gather Images', id: 'withImagesummary' }, + { label: 'Generate Image Alt Text', id: 'withGeneratedAlt' }, + { label: 'Include Iframes', id: 'withIframe' }, + { label: 'Include Shadow DOM', id: 'withShadowDom' }, { label: 'JSON Response', id: 'jsonResponse' }, + { label: 'No Cache', id: 'noCache' }, + { label: 'Do Not Track', id: 'dnt' }, + { label: 'Disable GitHub Flavored Markdown', id: 'noGfm' }, ], + condition: { field: 'operation', value: 'jina_read_url' }, + }, + // Search params + { + id: 'q', + title: 'Search Query', + type: 'long-input', + layout: 'full', + required: true, + placeholder: 'Enter your search query...', + condition: { field: 'operation', value: 'jina_search' }, }, + { + id: 'gl', + title: 'Country Code', + type: 'short-input', + layout: 'full', + placeholder: 'US, UK, JP, etc.', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'location', + title: 'Location', + type: 'short-input', + layout: 'full', + placeholder: 'New York, London, Tokyo', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'hl', + title: 'Language Code', + type: 'short-input', + layout: 'full', + placeholder: 'en, es, fr, etc.', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'num', + title: 'Number of Results', + type: 'short-input', + layout: 'full', + placeholder: '5', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'page', + title: 'Page Number', + type: 'short-input', + layout: 'full', + placeholder: '1', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'site', + title: 'Site Restriction', + type: 'short-input', + layout: 'full', + placeholder: 'jina.ai,github.com (comma-separated)', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchReturnFormat', + title: 'Return Format', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Markdown', id: 'markdown' }, + { label: 'HTML', id: 'html' }, + { label: 'Text', id: 'text' }, + ], + value: () => 'markdown', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchRetainImages', + title: 'Retain Images', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'All', id: 'all' }, + { label: 'None', id: 'none' }, + ], + value: () => 'all', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchEngine', + title: 'Engine', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Browser', id: 'browser' }, + { label: 'Direct', id: 'direct' }, + ], + value: () => 'browser', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchTimeout', + title: 'Timeout (seconds)', + type: 'short-input', + layout: 'full', + placeholder: '30', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchSetCookie', + title: 'Cookies', + type: 'short-input', + layout: 'full', + placeholder: 'session=abc123; token=xyz', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchProxyUrl', + title: 'Proxy URL', + type: 'short-input', + layout: 'full', + placeholder: 'http://proxy.example.com:8080', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchLocale', + title: 'Locale', + type: 'short-input', + layout: 'full', + placeholder: 'en-US', + condition: { field: 'operation', value: 'jina_search' }, + }, + { + id: 'searchOptions', + title: 'Options', + type: 'checkbox-list', + layout: 'full', + options: [ + { label: 'Include Favicons', id: 'withFavicon' }, + { label: 'Gather Images', id: 'withImagesummary' }, + { label: 'Gather Links', id: 'withLinksummary' }, + { label: 'Generate Image Alt Text', id: 'withGeneratedAlt' }, + { label: 'No Cache', id: 'noCache' }, + { label: 'No Content (metadata only)', id: 'respondWith' }, + ], + condition: { field: 'operation', value: 'jina_search' }, + }, + // API Key (shared) { id: 'apiKey', title: 'API Key', @@ -43,16 +336,71 @@ export const JinaBlock: BlockConfig = { }, ], tools: { - access: ['jina_read_url'], + access: ['jina_read_url', 'jina_search'], + config: { + tool: (params) => { + return params.operation || 'jina_read_url' + }, + }, }, inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Jina API key' }, + // Read URL inputs url: { type: 'string', description: 'URL to extract' }, - useReaderLMv2: { type: 'boolean', description: 'Use Reader LM v2' }, + useReaderLMv2: { type: 'boolean', description: 'Use Reader LM v2 (3x cost)' }, gatherLinks: { type: 'boolean', description: 'Gather page links' }, jsonResponse: { type: 'boolean', description: 'JSON response format' }, - apiKey: { type: 'string', description: 'Jina API key' }, + targetSelector: { type: 'string', description: 'CSS selector to target' }, + waitForSelector: { type: 'string', description: 'CSS selector to wait for' }, + removeSelector: { type: 'string', description: 'CSS selector to remove' }, + timeout: { type: 'number', description: 'Timeout in seconds' }, + withImagesummary: { type: 'boolean', description: 'Gather images' }, + retainImages: { type: 'string', description: 'Retain images setting' }, + returnFormat: { type: 'string', description: 'Output format' }, + withIframe: { type: 'boolean', description: 'Include iframes' }, + withShadowDom: { type: 'boolean', description: 'Include Shadow DOM' }, + setCookie: { type: 'string', description: 'Authentication cookies' }, + proxyUrl: { type: 'string', description: 'Proxy URL' }, + proxy: { type: 'string', description: 'Proxy country code' }, + engine: { type: 'string', description: 'Rendering engine' }, + tokenBudget: { type: 'number', description: 'Token budget' }, + noCache: { type: 'boolean', description: 'Bypass cache' }, + cacheTolerance: { type: 'number', description: 'Cache tolerance seconds' }, + withGeneratedAlt: { type: 'boolean', description: 'Generate image alt text' }, + baseUrl: { type: 'string', description: 'Follow redirects' }, + locale: { type: 'string', description: 'Browser locale' }, + robotsTxt: { type: 'string', description: 'Bot User-Agent' }, + dnt: { type: 'boolean', description: 'Do Not Track' }, + noGfm: { type: 'boolean', description: 'Disable GitHub Flavored Markdown' }, + // Search inputs + q: { type: 'string', description: 'Search query' }, + gl: { type: 'string', description: 'Country code' }, + location: { type: 'string', description: 'City location' }, + hl: { type: 'string', description: 'Language code' }, + num: { type: 'number', description: 'Number of results' }, + page: { type: 'number', description: 'Page number' }, + site: { type: 'string', description: 'Site restriction' }, + withFavicon: { type: 'boolean', description: 'Include favicons' }, + withLinksummary: { type: 'boolean', description: 'Gather links' }, + respondWith: { type: 'string', description: 'Response mode' }, + searchReturnFormat: { type: 'string', description: 'Search output format' }, + searchRetainImages: { type: 'string', description: 'Search retain images' }, + searchEngine: { type: 'string', description: 'Search engine' }, + searchTimeout: { type: 'number', description: 'Search timeout' }, + searchSetCookie: { type: 'string', description: 'Search cookies' }, + searchProxyUrl: { type: 'string', description: 'Search proxy URL' }, + searchLocale: { type: 'string', description: 'Search locale' }, }, outputs: { + // Read URL outputs content: { type: 'string', description: 'Extracted content' }, + links: { type: 'array', description: 'List of links from page' }, + images: { type: 'array', description: 'List of images from page' }, + // Search outputs + results: { + type: 'array', + description: 'Array of search results with title, description, url, and content', + }, }, } diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index d59f0683cd..c5b0fed5f1 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -23,6 +23,24 @@ export const JiraBlock: BlockConfig = { { label: 'Read Issue', id: 'read' }, { label: 'Update Issue', id: 'update' }, { label: 'Write Issue', id: 'write' }, + { label: 'Delete Issue', id: 'delete' }, + { label: 'Assign Issue', id: 'assign' }, + { label: 'Transition Issue', id: 'transition' }, + { label: 'Search Issues', id: 'search' }, + { label: 'Add Comment', id: 'add_comment' }, + { label: 'Get Comments', id: 'get_comments' }, + { label: 'Update Comment', id: 'update_comment' }, + { label: 'Delete Comment', id: 'delete_comment' }, + { label: 'Get Attachments', id: 'get_attachments' }, + { label: 'Delete Attachment', id: 'delete_attachment' }, + { label: 'Add Worklog', id: 'add_worklog' }, + { label: 'Get Worklogs', id: 'get_worklogs' }, + { label: 'Update Worklog', id: 'update_worklog' }, + { label: 'Delete Worklog', id: 'delete_worklog' }, + { label: 'Create Issue Link', id: 'create_link' }, + { label: 'Delete Issue Link', id: 'delete_link' }, + { label: 'Add Watcher', id: 'add_watcher' }, + { label: 'Remove Watcher', id: 'remove_watcher' }, ], value: () => 'read', }, @@ -48,8 +66,32 @@ export const JiraBlock: BlockConfig = { 'write:jira-work', 'read:issue-event:jira', 'write:issue:jira', + 'read:project:jira', + 'read:issue-type:jira', 'read:me', 'offline_access', + 'read:issue-meta:jira', + 'read:issue-security-level:jira', + 'read:issue.vote:jira', + 'read:issue.changelog:jira', + 'read:avatar:jira', + 'read:issue:jira', + 'read:status:jira', + 'read:user:jira', + 'read:field-configuration:jira', + 'read:issue-details:jira', + // New scopes for expanded Jira operations + 'delete:issue:jira', + 'write:comment:jira', + 'read:comment:jira', + 'delete:comment:jira', + 'read:attachment:jira', + 'delete:attachment:jira', + 'write:issue-worklog:jira', + 'read:issue-worklog:jira', + 'delete:issue-worklog:jira', + 'write:issue-link:jira', + 'delete:issue-link:jira', ], placeholder: 'Select Jira account', }, @@ -88,7 +130,27 @@ export const JiraBlock: BlockConfig = { serviceId: 'jira', placeholder: 'Select Jira issue', dependsOn: ['credential', 'domain', 'projectId'], - condition: { field: 'operation', value: ['read', 'update'] }, + condition: { + field: 'operation', + value: [ + 'read', + 'update', + 'delete', + 'assign', + 'transition', + 'add_comment', + 'get_comments', + 'update_comment', + 'delete_comment', + 'get_attachments', + 'add_worklog', + 'get_worklogs', + 'update_worklog', + 'delete_worklog', + 'add_watcher', + 'remove_watcher', + ], + }, mode: 'basic', }, // Manual issue key input (advanced mode) @@ -100,7 +162,27 @@ export const JiraBlock: BlockConfig = { canonicalParamId: 'issueKey', placeholder: 'Enter Jira issue key', dependsOn: ['credential', 'domain', 'projectId', 'manualProjectId'], - condition: { field: 'operation', value: ['read', 'update'] }, + condition: { + field: 'operation', + value: [ + 'read', + 'update', + 'delete', + 'assign', + 'transition', + 'add_comment', + 'get_comments', + 'update_comment', + 'delete_comment', + 'get_attachments', + 'add_worklog', + 'get_worklogs', + 'update_worklog', + 'delete_worklog', + 'add_watcher', + 'remove_watcher', + ], + }, mode: 'advanced', }, { @@ -122,9 +204,208 @@ export const JiraBlock: BlockConfig = { dependsOn: ['issueKey'], condition: { field: 'operation', value: ['update', 'write'] }, }, + // Delete Issue fields + { + id: 'deleteSubtasks', + title: 'Delete Subtasks', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'delete' }, + }, + // Assign Issue fields + { + id: 'accountId', + title: 'Account ID', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter user account ID to assign', + condition: { field: 'operation', value: ['assign', 'add_watcher', 'remove_watcher'] }, + }, + // Transition Issue fields + { + id: 'transitionId', + title: 'Transition ID', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter transition ID (e.g., 21)', + condition: { field: 'operation', value: 'transition' }, + }, + { + id: 'transitionComment', + title: 'Comment (Optional)', + type: 'long-input', + layout: 'full', + placeholder: 'Add optional comment for transition', + condition: { field: 'operation', value: 'transition' }, + }, + // Search Issues fields + { + id: 'jql', + title: 'JQL Query', + type: 'long-input', + layout: 'full', + required: true, + placeholder: 'Enter JQL query (e.g., project = PROJ AND status = "In Progress")', + condition: { field: 'operation', value: 'search' }, + }, + { + id: 'maxResults', + title: 'Max Results', + type: 'short-input', + layout: 'full', + placeholder: 'Maximum results to return (default: 50)', + condition: { field: 'operation', value: ['search', 'get_comments', 'get_worklogs'] }, + }, + // Comment fields + { + id: 'commentBody', + title: 'Comment Text', + type: 'long-input', + layout: 'full', + required: true, + placeholder: 'Enter comment text', + condition: { field: 'operation', value: ['add_comment', 'update_comment'] }, + }, + { + id: 'commentId', + title: 'Comment ID', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter comment ID', + condition: { field: 'operation', value: ['update_comment', 'delete_comment'] }, + }, + // Attachment fields + { + id: 'attachmentId', + title: 'Attachment ID', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter attachment ID', + condition: { field: 'operation', value: 'delete_attachment' }, + }, + // Worklog fields + { + id: 'timeSpentSeconds', + title: 'Time Spent (seconds)', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter time in seconds (e.g., 3600 for 1 hour)', + condition: { field: 'operation', value: 'add_worklog' }, + }, + { + id: 'timeSpentSecondsUpdate', + title: 'Time Spent (seconds) - Optional', + type: 'short-input', + layout: 'full', + placeholder: 'Enter time in seconds (leave empty to keep unchanged)', + condition: { field: 'operation', value: 'update_worklog' }, + }, + { + id: 'worklogComment', + title: 'Worklog Comment (Optional)', + type: 'long-input', + layout: 'full', + placeholder: 'Enter optional worklog comment', + condition: { field: 'operation', value: ['add_worklog', 'update_worklog'] }, + }, + { + id: 'started', + title: 'Started At (Optional)', + type: 'short-input', + layout: 'full', + placeholder: 'ISO timestamp (defaults to now)', + condition: { field: 'operation', value: ['add_worklog', 'update_worklog'] }, + }, + { + id: 'worklogId', + title: 'Worklog ID', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter worklog ID', + condition: { field: 'operation', value: ['update_worklog', 'delete_worklog'] }, + }, + // Issue Link fields + { + id: 'inwardIssueKey', + title: 'Inward Issue Key', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter inward issue key (e.g., PROJ-123)', + condition: { field: 'operation', value: 'create_link' }, + }, + { + id: 'outwardIssueKey', + title: 'Outward Issue Key', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter outward issue key (e.g., PROJ-456)', + condition: { field: 'operation', value: 'create_link' }, + }, + { + id: 'linkType', + title: 'Link Type', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter link type (e.g., "Blocks", "Relates")', + condition: { field: 'operation', value: 'create_link' }, + }, + { + id: 'linkComment', + title: 'Link Comment (Optional)', + type: 'long-input', + layout: 'full', + placeholder: 'Add optional comment for the link', + condition: { field: 'operation', value: 'create_link' }, + }, + { + id: 'linkId', + title: 'Link ID', + type: 'short-input', + layout: 'full', + required: true, + placeholder: 'Enter link ID to delete', + condition: { field: 'operation', value: 'delete_link' }, + }, ], tools: { - access: ['jira_retrieve', 'jira_update', 'jira_write', 'jira_bulk_read'], + access: [ + 'jira_retrieve', + 'jira_update', + 'jira_write', + 'jira_bulk_read', + 'jira_delete_issue', + 'jira_assign_issue', + 'jira_transition_issue', + 'jira_search_issues', + 'jira_add_comment', + 'jira_get_comments', + 'jira_update_comment', + 'jira_delete_comment', + 'jira_get_attachments', + 'jira_delete_attachment', + 'jira_add_worklog', + 'jira_get_worklogs', + 'jira_update_worklog', + 'jira_delete_worklog', + 'jira_create_issue_link', + 'jira_delete_issue_link', + 'jira_add_watcher', + 'jira_remove_watcher', + ], config: { tool: (params) => { const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim() @@ -143,6 +424,42 @@ export const JiraBlock: BlockConfig = { return 'jira_write' case 'read-bulk': return 'jira_bulk_read' + case 'delete': + return 'jira_delete_issue' + case 'assign': + return 'jira_assign_issue' + case 'transition': + return 'jira_transition_issue' + case 'search': + return 'jira_search_issues' + case 'add_comment': + return 'jira_add_comment' + case 'get_comments': + return 'jira_get_comments' + case 'update_comment': + return 'jira_update_comment' + case 'delete_comment': + return 'jira_delete_comment' + case 'get_attachments': + return 'jira_get_attachments' + case 'delete_attachment': + return 'jira_delete_attachment' + case 'add_worklog': + return 'jira_add_worklog' + case 'get_worklogs': + return 'jira_get_worklogs' + case 'update_worklog': + return 'jira_update_worklog' + case 'delete_worklog': + return 'jira_delete_worklog' + case 'create_link': + return 'jira_create_issue_link' + case 'delete_link': + return 'jira_delete_issue_link' + case 'add_watcher': + return 'jira_add_watcher' + case 'remove_watcher': + return 'jira_remove_watcher' default: return 'jira_retrieve' } @@ -231,6 +548,184 @@ export const JiraBlock: BlockConfig = { projectId: finalProjectId.trim(), } } + case 'delete': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to delete an issue.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + deleteSubtasks: params.deleteSubtasks === 'true', + } + } + case 'assign': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to assign an issue.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + accountId: params.accountId, + } + } + case 'transition': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to transition an issue.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + transitionId: params.transitionId, + comment: params.transitionComment, + } + } + case 'search': { + return { + ...baseParams, + jql: params.jql, + maxResults: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + } + } + case 'add_comment': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to add a comment.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + body: params.commentBody, + } + } + case 'get_comments': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to get comments.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + maxResults: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + } + } + case 'update_comment': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to update a comment.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + commentId: params.commentId, + body: params.commentBody, + } + } + case 'delete_comment': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to delete a comment.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + commentId: params.commentId, + } + } + case 'get_attachments': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to get attachments.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + } + } + case 'delete_attachment': { + return { + ...baseParams, + attachmentId: params.attachmentId, + } + } + case 'add_worklog': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to add a worklog.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + timeSpentSeconds: params.timeSpentSeconds + ? Number.parseInt(params.timeSpentSeconds) + : undefined, + comment: params.worklogComment, + started: params.started, + } + } + case 'get_worklogs': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to get worklogs.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + maxResults: params.maxResults ? Number.parseInt(params.maxResults) : undefined, + } + } + case 'update_worklog': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to update a worklog.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + worklogId: params.worklogId, + timeSpentSeconds: params.timeSpentSecondsUpdate + ? Number.parseInt(params.timeSpentSecondsUpdate) + : undefined, + comment: params.worklogComment, + started: params.started, + } + } + case 'delete_worklog': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to delete a worklog.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + worklogId: params.worklogId, + } + } + case 'create_link': { + return { + ...baseParams, + inwardIssueKey: params.inwardIssueKey, + outwardIssueKey: params.outwardIssueKey, + linkType: params.linkType, + comment: params.linkComment, + } + } + case 'delete_link': { + return { + ...baseParams, + linkId: params.linkId, + } + } + case 'add_watcher': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to add a watcher.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + accountId: params.accountId, + } + } + case 'remove_watcher': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to remove a watcher.') + } + return { + ...baseParams, + issueKey: effectiveIssueKey, + accountId: params.accountId, + } + } default: return baseParams } @@ -245,15 +740,51 @@ export const JiraBlock: BlockConfig = { projectId: { type: 'string', description: 'Project identifier' }, manualProjectId: { type: 'string', description: 'Manual project identifier' }, manualIssueKey: { type: 'string', description: 'Manual issue key' }, - // Update operation inputs + // Update/Write operation inputs summary: { type: 'string', description: 'Issue summary' }, description: { type: 'string', description: 'Issue description' }, - // Write operation inputs issueType: { type: 'string', description: 'Issue type' }, + // Delete operation inputs + deleteSubtasks: { type: 'string', description: 'Whether to delete subtasks (true/false)' }, + // Assign/Watcher operation inputs + accountId: { + type: 'string', + description: 'User account ID for assignment or watcher operations', + }, + // Transition operation inputs + transitionId: { type: 'string', description: 'Transition ID for workflow status changes' }, + transitionComment: { type: 'string', description: 'Optional comment for transition' }, + // Search operation inputs + jql: { type: 'string', description: 'JQL (Jira Query Language) search query' }, + maxResults: { type: 'string', description: 'Maximum number of results to return' }, + // Comment operation inputs + commentBody: { type: 'string', description: 'Text content for comment operations' }, + commentId: { type: 'string', description: 'Comment ID for update/delete operations' }, + // Attachment operation inputs + attachmentId: { type: 'string', description: 'Attachment ID for delete operation' }, + // Worklog operation inputs + timeSpentSeconds: { + type: 'string', + description: 'Time spent in seconds for add worklog (required)', + }, + timeSpentSecondsUpdate: { + type: 'string', + description: 'Time spent in seconds for update worklog (optional)', + }, + worklogComment: { type: 'string', description: 'Optional comment for worklog' }, + started: { type: 'string', description: 'ISO timestamp when work started (optional)' }, + worklogId: { type: 'string', description: 'Worklog ID for update/delete operations' }, + // Issue Link operation inputs + inwardIssueKey: { type: 'string', description: 'Inward issue key for creating link' }, + outwardIssueKey: { type: 'string', description: 'Outward issue key for creating link' }, + linkType: { type: 'string', description: 'Type of link (e.g., "Blocks", "Relates")' }, + linkComment: { type: 'string', description: 'Optional comment for issue link' }, + linkId: { type: 'string', description: 'Link ID for delete operation' }, }, outputs: { // Common outputs across all Jira operations ts: { type: 'string', description: 'Timestamp of the operation' }, + success: { type: 'boolean', description: 'Whether the operation was successful' }, // jira_retrieve (read) outputs issueKey: { type: 'string', description: 'Issue key (e.g., PROJ-123)' }, @@ -261,15 +792,73 @@ export const JiraBlock: BlockConfig = { description: { type: 'string', description: 'Issue description content' }, created: { type: 'string', description: 'Issue creation date' }, updated: { type: 'string', description: 'Issue last update date' }, - - // jira_update outputs - success: { type: 'boolean', description: 'Whether the update operation was successful' }, + status: { type: 'string', description: 'Issue status name' }, + assignee: { type: 'string', description: 'Issue assignee display name or account ID' }, // jira_write (create) outputs url: { type: 'string', description: 'URL to the created/accessed issue' }, + id: { type: 'string', description: 'Jira issue ID' }, + key: { type: 'string', description: 'Jira issue key' }, + + // jira_search_issues outputs + total: { type: 'number', description: 'Total number of matching issues' }, + startAt: { type: 'number', description: 'Pagination start index' }, + maxResults: { type: 'number', description: 'Maximum results per page' }, + issues: { + type: 'json', + description: 'Array of matching issues with key, summary, status, assignee, dates', + }, + + // jira_get_comments outputs + comments: { + type: 'json', + description: 'Array of comments with id, author, body, created, updated', + }, + + // jira_add_comment, jira_update_comment outputs + commentId: { type: 'string', description: 'Comment ID' }, + commentBody: { type: 'string', description: 'Comment text content' }, + author: { type: 'string', description: 'Comment author display name' }, + + // jira_get_attachments outputs + attachments: { + type: 'json', + description: 'Array of attachments with id, filename, size, mimeType, created, author', + }, + + // jira_delete_attachment, jira_delete_comment, jira_delete_issue, jira_delete_worklog, jira_delete_issue_link outputs + attachmentId: { type: 'string', description: 'Deleted attachment ID' }, + + // jira_get_worklogs outputs + worklogs: { + type: 'json', + description: + 'Array of worklogs with id, author, timeSpentSeconds, timeSpent, comment, created, updated, started', + }, + + // jira_add_worklog, jira_update_worklog outputs + worklogId: { type: 'string', description: 'Worklog ID' }, + timeSpentSeconds: { type: 'number', description: 'Time spent in seconds' }, + timeSpent: { type: 'string', description: 'Formatted time spent string' }, + + // jira_assign_issue outputs + assigneeId: { type: 'string', description: 'Assigned user account ID' }, + + // jira_transition_issue outputs + transitionId: { type: 'string', description: 'Applied transition ID' }, + newStatus: { type: 'string', description: 'New status after transition' }, + + // jira_create_issue_link outputs + linkId: { type: 'string', description: 'Created link ID' }, + inwardIssue: { type: 'string', description: 'Inward issue key' }, + outwardIssue: { type: 'string', description: 'Outward issue key' }, + linkType: { type: 'string', description: 'Type of issue link' }, + + // jira_add_watcher, jira_remove_watcher outputs + watcherAccountId: { type: 'string', description: 'Watcher account ID' }, - // jira_bulk_read outputs (array of issues) + // jira_bulk_read outputs // Note: bulk_read returns an array in the output field, each item contains: - // ts, summary, description, created, updated + // ts, issueKey, summary, description, status, assignee, created, updated }, } diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 86d575834c..f5efd7a252 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -6,9 +6,11 @@ import type { LinearResponse } from '@/tools/linear/types' export const LinearBlock: BlockConfig = { type: 'linear', name: 'Linear', - description: 'Read and create issues in Linear', + description: 'Interact with Linear issues, projects, and more', authMode: AuthMode.OAuth, - longDescription: 'Integrate Linear into the workflow. Can read and create issues.', + longDescription: + 'Integrate Linear into the workflow. Can manage issues, comments, projects, labels, workflow states, cycles, attachments, and more.', + docsLink: 'https://docs.sim.ai/tools/linear', category: 'tools', icon: LinearIcon, bgColor: '#5E6AD2', @@ -19,10 +21,67 @@ export const LinearBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ - { label: 'Read Issues', id: 'read' }, - { label: 'Create Issue', id: 'write' }, + // Issue Operations + { label: 'Read Issues', id: 'linear_read_issues' }, + { label: 'Get Issue', id: 'linear_get_issue' }, + { label: 'Create Issue', id: 'linear_create_issue' }, + { label: 'Update Issue', id: 'linear_update_issue' }, + { label: 'Archive Issue', id: 'linear_archive_issue' }, + { label: 'Unarchive Issue', id: 'linear_unarchive_issue' }, + { label: 'Delete Issue', id: 'linear_delete_issue' }, + { label: 'Search Issues', id: 'linear_search_issues' }, + { label: 'Add Label to Issue', id: 'linear_add_label_to_issue' }, + { label: 'Remove Label from Issue', id: 'linear_remove_label_from_issue' }, + // Comment Operations + { label: 'Create Comment', id: 'linear_create_comment' }, + { label: 'Update Comment', id: 'linear_update_comment' }, + { label: 'Delete Comment', id: 'linear_delete_comment' }, + { label: 'List Comments', id: 'linear_list_comments' }, + // Project Operations + { label: 'List Projects', id: 'linear_list_projects' }, + { label: 'Get Project', id: 'linear_get_project' }, + { label: 'Create Project', id: 'linear_create_project' }, + { label: 'Update Project', id: 'linear_update_project' }, + { label: 'Archive Project', id: 'linear_archive_project' }, + // User & Team Operations + { label: 'List Users', id: 'linear_list_users' }, + { label: 'List Teams', id: 'linear_list_teams' }, + { label: 'Get Viewer', id: 'linear_get_viewer' }, + // Label Operations + { label: 'List Labels', id: 'linear_list_labels' }, + { label: 'Create Label', id: 'linear_create_label' }, + { label: 'Update Label', id: 'linear_update_label' }, + { label: 'Archive Label', id: 'linear_archive_label' }, + // Workflow State Operations + { label: 'List Workflow States', id: 'linear_list_workflow_states' }, + { label: 'Create Workflow State', id: 'linear_create_workflow_state' }, + { label: 'Update Workflow State', id: 'linear_update_workflow_state' }, + // Cycle Operations + { label: 'List Cycles', id: 'linear_list_cycles' }, + { label: 'Get Cycle', id: 'linear_get_cycle' }, + { label: 'Create Cycle', id: 'linear_create_cycle' }, + { label: 'Get Active Cycle', id: 'linear_get_active_cycle' }, + // Attachment Operations + { label: 'Create Attachment', id: 'linear_create_attachment' }, + { label: 'List Attachments', id: 'linear_list_attachments' }, + { label: 'Update Attachment', id: 'linear_update_attachment' }, + { label: 'Delete Attachment', id: 'linear_delete_attachment' }, + // Issue Relation Operations + { label: 'Create Issue Relation', id: 'linear_create_issue_relation' }, + { label: 'List Issue Relations', id: 'linear_list_issue_relations' }, + { label: 'Delete Issue Relation', id: 'linear_delete_issue_relation' }, + // Favorite Operations + { label: 'Create Favorite', id: 'linear_create_favorite' }, + { label: 'List Favorites', id: 'linear_list_favorites' }, + // Project Update Operations + { label: 'Create Project Update', id: 'linear_create_project_update' }, + { label: 'List Project Updates', id: 'linear_list_project_updates' }, + { label: 'Create Project Link', id: 'linear_create_project_link' }, + // Notification Operations + { label: 'List Notifications', id: 'linear_list_notifications' }, + { label: 'Update Notification', id: 'linear_update_notification' }, ], - value: () => 'read', + value: () => 'linear_read_issues', }, { id: 'credential', @@ -35,6 +94,7 @@ export const LinearBlock: BlockConfig = { placeholder: 'Select Linear account', required: true, }, + // Team selector (for most operations) { id: 'teamId', title: 'Team', @@ -46,7 +106,50 @@ export const LinearBlock: BlockConfig = { placeholder: 'Select a team', dependsOn: ['credential'], mode: 'basic', + condition: { + field: 'operation', + value: [ + 'linear_read_issues', + 'linear_create_issue', + 'linear_search_issues', + 'linear_list_projects', + 'linear_create_project', + 'linear_list_labels', + 'linear_list_workflow_states', + 'linear_create_workflow_state', + 'linear_list_cycles', + 'linear_create_cycle', + 'linear_get_active_cycle', + ], + }, + }, + // Manual team ID input (advanced mode) + { + id: 'manualTeamId', + title: 'Team ID', + type: 'short-input', + layout: 'full', + canonicalParamId: 'teamId', + placeholder: 'Enter Linear team ID', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'linear_read_issues', + 'linear_create_issue', + 'linear_search_issues', + 'linear_list_projects', + 'linear_create_project', + 'linear_list_labels', + 'linear_list_workflow_states', + 'linear_create_workflow_state', + 'linear_list_cycles', + 'linear_create_cycle', + 'linear_get_active_cycle', + ], + }, }, + // Project selector (for issue creation) { id: 'projectId', title: 'Project', @@ -58,16 +161,10 @@ export const LinearBlock: BlockConfig = { placeholder: 'Select a project', dependsOn: ['credential', 'teamId'], mode: 'basic', - }, - // Manual team ID input (advanced mode) - { - id: 'manualTeamId', - title: 'Team ID', - type: 'short-input', - layout: 'full', - canonicalParamId: 'teamId', - placeholder: 'Enter Linear team ID', - mode: 'advanced', + condition: { + field: 'operation', + value: ['linear_read_issues', 'linear_create_issue'], + }, }, // Manual project ID input (advanced mode) { @@ -78,59 +175,966 @@ export const LinearBlock: BlockConfig = { canonicalParamId: 'projectId', placeholder: 'Enter Linear project ID', mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'linear_read_issues', + 'linear_create_issue', + 'linear_get_project', + 'linear_update_project', + 'linear_archive_project', + 'linear_create_project_update', + 'linear_list_project_updates', + 'linear_create_project_link', + ], + }, }, + // Issue ID input (for operations requiring issue ID) + { + id: 'issueId', + title: 'Issue ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter Linear issue ID', + required: true, + condition: { + field: 'operation', + value: [ + 'linear_get_issue', + 'linear_update_issue', + 'linear_archive_issue', + 'linear_unarchive_issue', + 'linear_delete_issue', + 'linear_add_label_to_issue', + 'linear_remove_label_from_issue', + 'linear_create_comment', + 'linear_list_comments', + 'linear_create_attachment', + 'linear_list_attachments', + 'linear_create_issue_relation', + 'linear_list_issue_relations', + ], + }, + }, + // Title (for issue creation/update) { id: 'title', title: 'Title', type: 'short-input', layout: 'full', - condition: { field: 'operation', value: ['write'] }, + placeholder: 'Enter issue title', required: true, + condition: { + field: 'operation', + value: ['linear_create_issue', 'linear_update_issue'], + }, }, + // Description (for issue creation/update, comments, projects) { id: 'description', title: 'Description', type: 'long-input', layout: 'full', - condition: { field: 'operation', value: ['write'] }, + placeholder: 'Enter description', + condition: { + field: 'operation', + value: [ + 'linear_create_issue', + 'linear_update_issue', + 'linear_create_project', + 'linear_update_project', + ], + }, + }, + // Comment body + { + id: 'body', + title: 'Comment', + type: 'long-input', + layout: 'full', + placeholder: 'Enter comment text', + required: true, + condition: { + field: 'operation', + value: ['linear_create_comment', 'linear_update_comment', 'linear_create_project_update'], + }, + }, + // Comment ID + { + id: 'commentId', + title: 'Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter comment ID', + required: true, + condition: { + field: 'operation', + value: ['linear_update_comment', 'linear_delete_comment'], + }, + }, + // Label ID + { + id: 'labelId', + title: 'Label ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter label ID', + required: true, + condition: { + field: 'operation', + value: [ + 'linear_add_label_to_issue', + 'linear_remove_label_from_issue', + 'linear_update_label', + 'linear_archive_label', + ], + }, + }, + // Label name (for creating labels) + { + id: 'name', + title: 'Name', + type: 'short-input', + layout: 'full', + placeholder: 'Enter name', + required: true, + condition: { + field: 'operation', + value: [ + 'linear_create_label', + 'linear_update_label', + 'linear_create_project', + 'linear_update_project', + 'linear_create_workflow_state', + 'linear_update_workflow_state', + 'linear_create_cycle', + ], + }, + }, + // Label color + { + id: 'color', + title: 'Color (hex)', + type: 'short-input', + layout: 'full', + placeholder: '#5E6AD2', + condition: { + field: 'operation', + value: [ + 'linear_create_label', + 'linear_update_label', + 'linear_create_workflow_state', + 'linear_update_workflow_state', + ], + }, + }, + // State ID (for issue updates) + { + id: 'stateId', + title: 'State ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter workflow state ID', + condition: { + field: 'operation', + value: ['linear_update_issue', 'linear_update_workflow_state'], + }, + }, + // Assignee ID (for issue operations) + { + id: 'assigneeId', + title: 'Assignee ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter user ID to assign', + condition: { + field: 'operation', + value: ['linear_create_issue', 'linear_update_issue'], + }, + }, + // Priority (for issues and projects) + { + id: 'priority', + title: 'Priority', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'No Priority', id: '0' }, + { label: 'Urgent', id: '1' }, + { label: 'High', id: '2' }, + { label: 'Normal', id: '3' }, + { label: 'Low', id: '4' }, + ], + value: () => '0', + condition: { + field: 'operation', + value: ['linear_create_issue', 'linear_update_issue', 'linear_create_project'], + }, + }, + // Estimate (for issues) + { + id: 'estimate', + title: 'Estimate', + type: 'short-input', + layout: 'full', + placeholder: 'Enter estimate points', + condition: { + field: 'operation', + value: ['linear_create_issue', 'linear_update_issue'], + }, + }, + // Search query + { + id: 'query', + title: 'Search Query', + type: 'long-input', + layout: 'full', + placeholder: 'Enter search query', + required: true, + condition: { + field: 'operation', + value: ['linear_search_issues'], + }, + }, + // Include archived (for list operations) + { + id: 'includeArchived', + title: 'Include Archived', + type: 'switch', + layout: 'full', + condition: { + field: 'operation', + value: ['linear_read_issues', 'linear_search_issues', 'linear_list_projects'], + }, + }, + // Cycle ID + { + id: 'cycleId', + title: 'Cycle ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter cycle ID', + required: true, + condition: { + field: 'operation', + value: ['linear_get_cycle'], + }, + }, + // Cycle start/end dates + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + layout: 'full', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['linear_create_cycle', 'linear_create_project'], + }, + }, + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + layout: 'full', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['linear_create_cycle'], + }, + }, + // Target date (for projects) + { + id: 'targetDate', + title: 'Target Date', + type: 'short-input', + layout: 'full', + placeholder: 'YYYY-MM-DD', + condition: { + field: 'operation', + value: ['linear_create_project', 'linear_update_project'], + }, + }, + // Attachment URL + { + id: 'url', + title: 'URL', + type: 'short-input', + layout: 'full', + placeholder: 'Enter URL', + required: true, + condition: { + field: 'operation', + value: ['linear_create_attachment', 'linear_create_project_link'], + }, + }, + // Attachment title + { + id: 'attachmentTitle', + title: 'Title', + type: 'short-input', + layout: 'full', + placeholder: 'Enter attachment title', + condition: { + field: 'operation', + value: ['linear_create_attachment', 'linear_update_attachment'], + }, + }, + // Attachment ID + { + id: 'attachmentId', + title: 'Attachment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter attachment ID', + required: true, + condition: { + field: 'operation', + value: ['linear_update_attachment', 'linear_delete_attachment'], + }, + }, + // Issue relation type + { + id: 'relationType', + title: 'Relation Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Blocks', id: 'blocks' }, + { label: 'Blocked by', id: 'blocked' }, + { label: 'Duplicate', id: 'duplicate' }, + { label: 'Related', id: 'related' }, + ], + value: () => 'related', + condition: { + field: 'operation', + value: ['linear_create_issue_relation'], + }, + }, + // Related issue ID + { + id: 'relatedIssueId', + title: 'Related Issue ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter related issue ID', + required: true, + condition: { + field: 'operation', + value: ['linear_create_issue_relation'], + }, + }, + // Relation ID + { + id: 'relationId', + title: 'Relation ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter relation ID', + required: true, + condition: { + field: 'operation', + value: ['linear_delete_issue_relation'], + }, + }, + // Favorite type + { + id: 'favoriteType', + title: 'Favorite Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Issue', id: 'issue' }, + { label: 'Project', id: 'project' }, + { label: 'Cycle', id: 'cycle' }, + { label: 'Label', id: 'label' }, + ], + value: () => 'issue', + condition: { + field: 'operation', + value: ['linear_create_favorite'], + }, + }, + // Favorite target ID + { + id: 'favoriteTargetId', + title: 'Target ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter ID to favorite', + required: true, + condition: { + field: 'operation', + value: ['linear_create_favorite'], + }, + }, + // Project health (for project updates) + { + id: 'health', + title: 'Project Health', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'On Track', id: 'onTrack' }, + { label: 'At Risk', id: 'atRisk' }, + { label: 'Off Track', id: 'offTrack' }, + ], + value: () => 'onTrack', + condition: { + field: 'operation', + value: ['linear_create_project_update'], + }, + }, + // Notification ID + { + id: 'notificationId', + title: 'Notification ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter notification ID', + required: true, + condition: { + field: 'operation', + value: ['linear_update_notification'], + }, + }, + // Mark as read + { + id: 'markAsRead', + title: 'Mark as Read', + type: 'switch', + layout: 'full', + condition: { + field: 'operation', + value: ['linear_update_notification'], + }, + }, + // Workflow state type + { + id: 'workflowType', + title: 'Workflow Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Backlog', id: 'backlog' }, + { label: 'Unstarted', id: 'unstarted' }, + { label: 'Started', id: 'started' }, + { label: 'Completed', id: 'completed' }, + { label: 'Canceled', id: 'canceled' }, + ], + value: () => 'started', + condition: { + field: 'operation', + value: ['linear_create_workflow_state'], + }, + }, + // Lead ID (for projects) + { + id: 'leadId', + title: 'Lead ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter user ID for project lead', + condition: { + field: 'operation', + value: ['linear_create_project', 'linear_update_project'], + }, + }, + // Project state + { + id: 'projectState', + title: 'Project State', + type: 'short-input', + layout: 'full', + placeholder: 'Enter project state', + condition: { + field: 'operation', + value: ['linear_update_project'], + }, }, ], tools: { - access: ['linear_read_issues', 'linear_create_issue'], + access: [ + 'linear_read_issues', + 'linear_get_issue', + 'linear_create_issue', + 'linear_update_issue', + 'linear_archive_issue', + 'linear_unarchive_issue', + 'linear_delete_issue', + 'linear_search_issues', + 'linear_add_label_to_issue', + 'linear_remove_label_from_issue', + 'linear_create_comment', + 'linear_update_comment', + 'linear_delete_comment', + 'linear_list_comments', + 'linear_list_projects', + 'linear_get_project', + 'linear_create_project', + 'linear_update_project', + 'linear_archive_project', + 'linear_list_users', + 'linear_list_teams', + 'linear_get_viewer', + 'linear_list_labels', + 'linear_create_label', + 'linear_update_label', + 'linear_archive_label', + 'linear_list_workflow_states', + 'linear_create_workflow_state', + 'linear_update_workflow_state', + 'linear_list_cycles', + 'linear_get_cycle', + 'linear_create_cycle', + 'linear_get_active_cycle', + 'linear_create_attachment', + 'linear_list_attachments', + 'linear_update_attachment', + 'linear_delete_attachment', + 'linear_create_issue_relation', + 'linear_list_issue_relations', + 'linear_delete_issue_relation', + 'linear_create_favorite', + 'linear_list_favorites', + 'linear_create_project_update', + 'linear_list_project_updates', + 'linear_create_project_link', + 'linear_list_notifications', + 'linear_update_notification', + ], config: { - tool: (params) => - params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues', + tool: (params) => { + return params.operation || 'linear_read_issues' + }, params: (params) => { // Handle both selector and manual inputs const effectiveTeamId = (params.teamId || params.manualTeamId || '').trim() const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim() - if (!effectiveTeamId) { - throw new Error('Team ID is required.') - } - if (!effectiveProjectId) { - throw new Error('Project ID is required.') + // Base params that most operations need + const baseParams: Record = { + credential: params.credential, } - if (params.operation === 'write') { - if (!params.title?.trim()) { - throw new Error('Title is required for creating issues.') - } - if (!params.description?.trim()) { - throw new Error('Description is required for creating issues.') - } - return { - credential: params.credential, - teamId: effectiveTeamId, - projectId: effectiveProjectId, - title: params.title, - description: params.description, - } - } - return { - credential: params.credential, - teamId: effectiveTeamId, - projectId: effectiveProjectId, + // Operation-specific param mapping + switch (params.operation) { + case 'linear_read_issues': + if (!effectiveTeamId || !effectiveProjectId) { + throw new Error('Team ID and Project ID are required.') + } + return { + ...baseParams, + teamId: effectiveTeamId, + projectId: effectiveProjectId, + includeArchived: params.includeArchived, + } + + case 'linear_get_issue': + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + } + + case 'linear_create_issue': + if (!effectiveTeamId || !effectiveProjectId) { + throw new Error('Team ID and Project ID are required.') + } + if (!params.title?.trim()) { + throw new Error('Title is required.') + } + return { + ...baseParams, + teamId: effectiveTeamId, + projectId: effectiveProjectId, + title: params.title.trim(), + description: params.description, + stateId: params.stateId, + assigneeId: params.assigneeId, + priority: params.priority ? Number(params.priority) : undefined, + estimate: params.estimate ? Number(params.estimate) : undefined, + } + + case 'linear_update_issue': + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + title: params.title, + description: params.description, + stateId: params.stateId, + assigneeId: params.assigneeId, + priority: params.priority ? Number(params.priority) : undefined, + estimate: params.estimate ? Number(params.estimate) : undefined, + } + + case 'linear_archive_issue': + case 'linear_unarchive_issue': + case 'linear_delete_issue': + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + } + + case 'linear_search_issues': + if (!params.query?.trim()) { + throw new Error('Search query is required.') + } + return { + ...baseParams, + query: params.query.trim(), + teamId: effectiveTeamId, + includeArchived: params.includeArchived, + } + + case 'linear_add_label_to_issue': + case 'linear_remove_label_from_issue': + if (!params.issueId?.trim() || !params.labelId?.trim()) { + throw new Error('Issue ID and Label ID are required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + labelId: params.labelId.trim(), + } + + case 'linear_create_comment': + if (!params.issueId?.trim() || !params.body?.trim()) { + throw new Error('Issue ID and comment body are required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + body: params.body.trim(), + } + + case 'linear_update_comment': + if (!params.commentId?.trim() || !params.body?.trim()) { + throw new Error('Comment ID and body are required.') + } + return { + ...baseParams, + commentId: params.commentId.trim(), + body: params.body.trim(), + } + + case 'linear_delete_comment': + if (!params.commentId?.trim()) { + throw new Error('Comment ID is required.') + } + return { + ...baseParams, + commentId: params.commentId.trim(), + } + + case 'linear_list_comments': + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + } + + case 'linear_list_projects': + return { + ...baseParams, + teamId: effectiveTeamId, + includeArchived: params.includeArchived, + } + + case 'linear_get_project': + if (!effectiveProjectId) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: effectiveProjectId, + } + + case 'linear_create_project': + if (!effectiveTeamId || !params.name?.trim()) { + throw new Error('Team ID and project name are required.') + } + return { + ...baseParams, + teamId: effectiveTeamId, + name: params.name.trim(), + description: params.description, + leadId: params.leadId, + startDate: params.startDate, + targetDate: params.targetDate, + priority: params.priority ? Number(params.priority) : undefined, + } + + case 'linear_update_project': + if (!effectiveProjectId) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: effectiveProjectId, + name: params.name, + description: params.description, + state: params.projectState, + leadId: params.leadId, + targetDate: params.targetDate, + } + + case 'linear_archive_project': + if (!effectiveProjectId) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: effectiveProjectId, + } + + case 'linear_list_users': + case 'linear_list_teams': + case 'linear_get_viewer': + return baseParams + + case 'linear_list_labels': + return { + ...baseParams, + teamId: effectiveTeamId, + } + + case 'linear_create_label': + if (!params.name?.trim()) { + throw new Error('Label name is required.') + } + return { + ...baseParams, + name: params.name.trim(), + color: params.color, + teamId: effectiveTeamId, + } + + case 'linear_update_label': + if (!params.labelId?.trim()) { + throw new Error('Label ID is required.') + } + return { + ...baseParams, + labelId: params.labelId.trim(), + name: params.name, + color: params.color, + } + + case 'linear_archive_label': + if (!params.labelId?.trim()) { + throw new Error('Label ID is required.') + } + return { + ...baseParams, + labelId: params.labelId.trim(), + } + + case 'linear_list_workflow_states': + return { + ...baseParams, + teamId: effectiveTeamId, + } + + case 'linear_create_workflow_state': + if (!effectiveTeamId || !params.name?.trim() || !params.workflowType) { + throw new Error('Team ID, name, and workflow type are required.') + } + if (!params.color?.trim()) { + throw new Error('Color is required for workflow state creation.') + } + return { + ...baseParams, + teamId: effectiveTeamId, + name: params.name.trim(), + type: params.workflowType, + color: params.color.trim(), + } + + case 'linear_update_workflow_state': + if (!params.stateId?.trim()) { + throw new Error('State ID is required.') + } + return { + ...baseParams, + stateId: params.stateId.trim(), + name: params.name, + color: params.color, + } + + case 'linear_list_cycles': + return { + ...baseParams, + teamId: effectiveTeamId, + } + + case 'linear_get_cycle': + if (!params.cycleId?.trim()) { + throw new Error('Cycle ID is required.') + } + return { + ...baseParams, + cycleId: params.cycleId.trim(), + } + + case 'linear_create_cycle': + if (!effectiveTeamId || !params.name?.trim()) { + throw new Error('Team ID and cycle name are required.') + } + return { + ...baseParams, + teamId: effectiveTeamId, + name: params.name.trim(), + startsAt: params.startDate, + endsAt: params.endDate, + } + + case 'linear_get_active_cycle': + if (!effectiveTeamId) { + throw new Error('Team ID is required.') + } + return { + ...baseParams, + teamId: effectiveTeamId, + } + + case 'linear_create_attachment': + if (!params.issueId?.trim() || !params.url?.trim()) { + throw new Error('Issue ID and URL are required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + url: params.url.trim(), + title: params.attachmentTitle, + } + + case 'linear_list_attachments': + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + } + + case 'linear_update_attachment': + if (!params.attachmentId?.trim()) { + throw new Error('Attachment ID is required.') + } + return { + ...baseParams, + attachmentId: params.attachmentId.trim(), + title: params.attachmentTitle, + } + + case 'linear_delete_attachment': + if (!params.attachmentId?.trim()) { + throw new Error('Attachment ID is required.') + } + return { + ...baseParams, + attachmentId: params.attachmentId.trim(), + } + + case 'linear_create_issue_relation': + if (!params.issueId?.trim() || !params.relatedIssueId?.trim() || !params.relationType) { + throw new Error('Issue ID, related issue ID, and relation type are required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + relatedIssueId: params.relatedIssueId.trim(), + type: params.relationType, + } + + case 'linear_list_issue_relations': + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + return { + ...baseParams, + issueId: params.issueId.trim(), + } + + case 'linear_delete_issue_relation': + if (!params.relationId?.trim()) { + throw new Error('Relation ID is required.') + } + return { + ...baseParams, + relationId: params.relationId.trim(), + } + + case 'linear_create_favorite': + if (!params.favoriteTargetId?.trim() || !params.favoriteType) { + throw new Error('Target ID and favorite type are required.') + } + return { + ...baseParams, + type: params.favoriteType, + [`${params.favoriteType}Id`]: params.favoriteTargetId.trim(), + } + + case 'linear_list_favorites': + return baseParams + + case 'linear_create_project_update': + if (!effectiveProjectId || !params.body?.trim()) { + throw new Error('Project ID and update body are required.') + } + return { + ...baseParams, + projectId: effectiveProjectId, + body: params.body.trim(), + health: params.health, + } + + case 'linear_list_project_updates': + if (!effectiveProjectId) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: effectiveProjectId, + } + + case 'linear_create_project_link': + if (!effectiveProjectId || !params.url?.trim()) { + throw new Error('Project ID and URL are required.') + } + return { + ...baseParams, + projectId: effectiveProjectId, + url: params.url.trim(), + label: params.name, + } + + case 'linear_list_notifications': + return baseParams + + case 'linear_update_notification': + if (!params.notificationId?.trim()) { + throw new Error('Notification ID is required.') + } + return { + ...baseParams, + notificationId: params.notificationId.trim(), + readAt: params.markAsRead ? new Date().toISOString() : null, + } + + default: + return baseParams } }, }, @@ -142,11 +1146,88 @@ export const LinearBlock: BlockConfig = { projectId: { type: 'string', description: 'Linear project identifier' }, manualTeamId: { type: 'string', description: 'Manual team identifier' }, manualProjectId: { type: 'string', description: 'Manual project identifier' }, - title: { type: 'string', description: 'Issue title' }, - description: { type: 'string', description: 'Issue description' }, + issueId: { type: 'string', description: 'Issue identifier' }, + title: { type: 'string', description: 'Title' }, + description: { type: 'string', description: 'Description' }, + body: { type: 'string', description: 'Comment or update body' }, + commentId: { type: 'string', description: 'Comment identifier' }, + labelId: { type: 'string', description: 'Label identifier' }, + name: { type: 'string', description: 'Name field' }, + color: { type: 'string', description: 'Color in hex format' }, + stateId: { type: 'string', description: 'Workflow state identifier' }, + assigneeId: { type: 'string', description: 'Assignee user identifier' }, + priority: { type: 'string', description: 'Priority level' }, + estimate: { type: 'string', description: 'Estimate points' }, + query: { type: 'string', description: 'Search query' }, + includeArchived: { type: 'boolean', description: 'Include archived items' }, + cycleId: { type: 'string', description: 'Cycle identifier' }, + startDate: { type: 'string', description: 'Start date' }, + endDate: { type: 'string', description: 'End date' }, + targetDate: { type: 'string', description: 'Target date' }, + url: { type: 'string', description: 'URL' }, + attachmentTitle: { type: 'string', description: 'Attachment title' }, + attachmentId: { type: 'string', description: 'Attachment identifier' }, + relationType: { type: 'string', description: 'Relation type' }, + relatedIssueId: { type: 'string', description: 'Related issue identifier' }, + relationId: { type: 'string', description: 'Relation identifier' }, + favoriteType: { type: 'string', description: 'Favorite type' }, + favoriteTargetId: { type: 'string', description: 'Favorite target identifier' }, + health: { type: 'string', description: 'Project health status' }, + notificationId: { type: 'string', description: 'Notification identifier' }, + markAsRead: { type: 'boolean', description: 'Mark as read flag' }, + workflowType: { type: 'string', description: 'Workflow state type' }, + leadId: { type: 'string', description: 'Project lead identifier' }, + projectState: { type: 'string', description: 'Project state' }, }, outputs: { + // Issue outputs issues: { type: 'json', description: 'Issues list' }, issue: { type: 'json', description: 'Single issue data' }, + issueId: { type: 'string', description: 'Issue ID for operations' }, + // Comment outputs + comment: { type: 'json', description: 'Comment data' }, + comments: { type: 'json', description: 'Comments list' }, + // Project outputs + project: { type: 'json', description: 'Project data' }, + projects: { type: 'json', description: 'Projects list' }, + projectId: { type: 'string', description: 'Project ID for operations' }, + // User/Team outputs + users: { type: 'json', description: 'Users list' }, + teams: { type: 'json', description: 'Teams list' }, + user: { type: 'json', description: 'User data' }, + viewer: { type: 'json', description: 'Current user data' }, + // Label outputs + label: { type: 'json', description: 'Label data' }, + labels: { type: 'json', description: 'Labels list' }, + labelId: { type: 'string', description: 'Label ID for operations' }, + // Workflow state outputs + state: { type: 'json', description: 'Workflow state data' }, + states: { type: 'json', description: 'Workflow states list' }, + // Cycle outputs + cycle: { type: 'json', description: 'Cycle data' }, + cycles: { type: 'json', description: 'Cycles list' }, + // Attachment outputs + attachment: { type: 'json', description: 'Attachment data' }, + attachments: { type: 'json', description: 'Attachments list' }, + // Relation outputs + relation: { type: 'json', description: 'Issue relation data' }, + relations: { type: 'json', description: 'Issue relations list' }, + // Favorite outputs + favorite: { type: 'json', description: 'Favorite data' }, + favorites: { type: 'json', description: 'Favorites list' }, + // Project update outputs + update: { type: 'json', description: 'Project update data' }, + updates: { type: 'json', description: 'Project updates list' }, + link: { type: 'json', description: 'Project link data' }, + // Notification outputs + notification: { type: 'json', description: 'Notification data' }, + notifications: { type: 'json', description: 'Notifications list' }, + // Pagination + pageInfo: { + type: 'json', + description: 'Pagination information (hasNextPage, endCursor) for list operations', + }, + // Success indicators + success: { type: 'boolean', description: 'Operation success status' }, }, } diff --git a/apps/sim/blocks/blocks/linkup.ts b/apps/sim/blocks/blocks/linkup.ts index cad5cf3ca8..a709d13a89 100644 --- a/apps/sim/blocks/blocks/linkup.ts +++ b/apps/sim/blocks/blocks/linkup.ts @@ -42,6 +42,53 @@ export const LinkupBlock: BlockConfig = { { label: 'Standard', id: 'standard' }, { label: 'Deep', id: 'deep' }, ], + value: () => 'standard', + }, + { + id: 'includeImages', + title: 'Include Images', + type: 'switch', + layout: 'half', + }, + { + id: 'includeInlineCitations', + title: 'Include Inline Citations', + type: 'switch', + layout: 'half', + }, + { + id: 'includeSources', + title: 'Include Sources', + type: 'switch', + layout: 'half', + }, + { + id: 'fromDate', + title: 'From Date', + type: 'short-input', + layout: 'half', + placeholder: 'YYYY-MM-DD', + }, + { + id: 'toDate', + title: 'To Date', + type: 'short-input', + layout: 'half', + placeholder: 'YYYY-MM-DD', + }, + { + id: 'includeDomains', + title: 'Include Domains', + type: 'long-input', + layout: 'full', + placeholder: 'example.com, another.com (comma-separated)', + }, + { + id: 'excludeDomains', + title: 'Exclude Domains', + type: 'long-input', + layout: 'full', + placeholder: 'example.com, another.com (comma-separated)', }, { id: 'apiKey', @@ -63,6 +110,19 @@ export const LinkupBlock: BlockConfig = { apiKey: { type: 'string', description: 'Linkup API key' }, depth: { type: 'string', description: 'Search depth level' }, outputType: { type: 'string', description: 'Output format type' }, + includeImages: { type: 'boolean', description: 'Include images in results' }, + includeInlineCitations: { type: 'boolean', description: 'Add inline citations to answers' }, + includeSources: { type: 'boolean', description: 'Include sources in response' }, + fromDate: { type: 'string', description: 'Start date for filtering (YYYY-MM-DD)' }, + toDate: { type: 'string', description: 'End date for filtering (YYYY-MM-DD)' }, + includeDomains: { + type: 'string', + description: 'Domains to restrict search to (comma-separated)', + }, + excludeDomains: { + type: 'string', + description: 'Domains to exclude from search (comma-separated)', + }, }, outputs: { diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index 553baf6577..a2fa2e35f6 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -8,20 +8,30 @@ interface MicrosoftPlannerBlockParams { accessToken?: string planId?: string taskId?: string + bucketId?: string + groupId?: string title?: string + name?: string description?: string dueDateTime?: string + startDateTime?: string assigneeUserId?: string - bucketId?: string + priority?: number + percentComplete?: number + etag?: string + checklist?: string + references?: string + previewType?: string [key: string]: string | number | boolean | undefined } export const MicrosoftPlannerBlock: BlockConfig = { type: 'microsoft_planner', name: 'Microsoft Planner', - description: 'Read and create tasks in Microsoft Planner', + description: 'Manage tasks, plans, and buckets in Microsoft Planner', authMode: AuthMode.OAuth, - longDescription: 'Integrate Microsoft Planner into the workflow. Can read and create tasks.', + longDescription: + 'Integrate Microsoft Planner into the workflow. Manage tasks, plans, buckets, and task details including checklists and references.', docsLink: 'https://docs.sim.ai/tools/microsoft_planner', category: 'tools', bgColor: '#E0E0E0', @@ -35,6 +45,17 @@ export const MicrosoftPlannerBlock: BlockConfig = { options: [ { label: 'Read Task', id: 'read_task' }, { label: 'Create Task', id: 'create_task' }, + { label: 'Update Task', id: 'update_task' }, + { label: 'Delete Task', id: 'delete_task' }, + { label: 'List Plans', id: 'list_plans' }, + { label: 'Read Plan', id: 'read_plan' }, + { label: 'List Buckets', id: 'list_buckets' }, + { label: 'Read Bucket', id: 'read_bucket' }, + { label: 'Create Bucket', id: 'create_bucket' }, + { label: 'Update Bucket', id: 'update_bucket' }, + { label: 'Delete Bucket', id: 'delete_bucket' }, + { label: 'Get Task Details', id: 'get_task_details' }, + { label: 'Update Task Details', id: 'update_task_details' }, ], }, { @@ -55,15 +76,33 @@ export const MicrosoftPlannerBlock: BlockConfig = { ], placeholder: 'Select Microsoft account', }, + + // Group ID - for list_plans + { + id: 'groupId', + title: 'Group ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the Microsoft 365 group ID', + condition: { field: 'operation', value: ['list_plans'] }, + dependsOn: ['credential'], + }, + + // Plan ID - for various operations { id: 'planId', title: 'Plan ID', type: 'short-input', layout: 'full', placeholder: 'Enter the plan ID', - condition: { field: 'operation', value: ['create_task', 'read_task'] }, + condition: { + field: 'operation', + value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'], + }, dependsOn: ['credential'], }, + + // Task ID selector - for read_task { id: 'taskId', title: 'Task ID', @@ -77,7 +116,7 @@ export const MicrosoftPlannerBlock: BlockConfig = { canonicalParamId: 'taskId', }, - // Advanced mode + // Manual Task ID - for read_task advanced mode { id: 'manualTaskId', title: 'Manual Task ID', @@ -90,49 +129,189 @@ export const MicrosoftPlannerBlock: BlockConfig = { canonicalParamId: 'taskId', }, + // Task ID for update/delete operations + { + id: 'taskIdForUpdate', + title: 'Task ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the task ID', + condition: { + field: 'operation', + value: ['update_task', 'delete_task', 'get_task_details', 'update_task_details'], + }, + dependsOn: ['credential'], + canonicalParamId: 'taskId', + }, + + // Bucket ID for bucket operations + { + id: 'bucketIdForRead', + title: 'Bucket ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the bucket ID', + condition: { field: 'operation', value: ['read_bucket', 'update_bucket', 'delete_bucket'] }, + dependsOn: ['credential'], + canonicalParamId: 'bucketId', + }, + + // ETag for update/delete operations + { + id: 'etag', + title: 'ETag', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the ETag from the resource (required for updates/deletes)', + condition: { + field: 'operation', + value: [ + 'update_task', + 'delete_task', + 'update_bucket', + 'delete_bucket', + 'update_task_details', + ], + }, + dependsOn: ['credential'], + }, + + // Task fields for create/update { id: 'title', title: 'Task Title', type: 'short-input', layout: 'full', placeholder: 'Enter the task title', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task'] }, }, + + // Name for bucket operations + { + id: 'name', + title: 'Bucket Name', + type: 'short-input', + layout: 'full', + placeholder: 'Enter the bucket name', + condition: { field: 'operation', value: ['create_bucket', 'update_bucket'] }, + }, + + // Description for task details { id: 'description', title: 'Description', type: 'long-input', layout: 'full', placeholder: 'Enter task description (optional)', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task_details'] }, }, + + // Due Date { id: 'dueDateTime', title: 'Due Date', type: 'short-input', layout: 'full', placeholder: 'Enter due date in ISO 8601 format (e.g., 2024-12-31T23:59:59Z)', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task'] }, + }, + + // Start Date + { + id: 'startDateTime', + title: 'Start Date', + type: 'short-input', + layout: 'full', + placeholder: 'Enter start date in ISO 8601 format (optional)', + condition: { field: 'operation', value: ['update_task'] }, }, + + // Assignee { id: 'assigneeUserId', title: 'Assignee User ID', type: 'short-input', layout: 'full', placeholder: 'Enter the user ID to assign this task to (optional)', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task'] }, }, + + // Bucket ID for task { id: 'bucketId', title: 'Bucket ID', type: 'short-input', layout: 'full', placeholder: 'Enter the bucket ID to organize the task (optional)', - condition: { field: 'operation', value: ['create_task'] }, + condition: { field: 'operation', value: ['create_task', 'update_task'] }, + }, + + // Priority + { + id: 'priority', + title: 'Priority', + type: 'short-input', + layout: 'full', + placeholder: 'Enter priority (0-10, optional)', + condition: { field: 'operation', value: ['update_task'] }, + }, + + // Percent Complete + { + id: 'percentComplete', + title: 'Percent Complete', + type: 'short-input', + layout: 'full', + placeholder: 'Enter completion percentage (0-100, optional)', + condition: { field: 'operation', value: ['update_task'] }, + }, + + // Checklist for task details + { + id: 'checklist', + title: 'Checklist (JSON)', + type: 'long-input', + layout: 'full', + placeholder: 'Enter checklist as JSON object (optional)', + condition: { field: 'operation', value: ['update_task_details'] }, + }, + + // References for task details + { + id: 'references', + title: 'References (JSON)', + type: 'long-input', + layout: 'full', + placeholder: 'Enter references as JSON object (optional)', + condition: { field: 'operation', value: ['update_task_details'] }, + }, + + // Preview Type + { + id: 'previewType', + title: 'Preview Type', + type: 'short-input', + layout: 'full', + placeholder: 'Enter preview type (automatic, noPreview, checklist, description, reference)', + condition: { field: 'operation', value: ['update_task_details'] }, }, ], tools: { - access: ['microsoft_planner_read_task', 'microsoft_planner_create_task'], + access: [ + 'microsoft_planner_read_task', + 'microsoft_planner_create_task', + 'microsoft_planner_update_task', + 'microsoft_planner_delete_task', + 'microsoft_planner_list_plans', + 'microsoft_planner_read_plan', + 'microsoft_planner_list_buckets', + 'microsoft_planner_read_bucket', + 'microsoft_planner_create_bucket', + 'microsoft_planner_update_bucket', + 'microsoft_planner_delete_bucket', + 'microsoft_planner_get_task_details', + 'microsoft_planner_update_task_details', + ], config: { tool: (params) => { switch (params.operation) { @@ -140,6 +319,28 @@ export const MicrosoftPlannerBlock: BlockConfig = { return 'microsoft_planner_read_task' case 'create_task': return 'microsoft_planner_create_task' + case 'update_task': + return 'microsoft_planner_update_task' + case 'delete_task': + return 'microsoft_planner_delete_task' + case 'list_plans': + return 'microsoft_planner_list_plans' + case 'read_plan': + return 'microsoft_planner_read_plan' + case 'list_buckets': + return 'microsoft_planner_list_buckets' + case 'read_bucket': + return 'microsoft_planner_read_bucket' + case 'create_bucket': + return 'microsoft_planner_create_bucket' + case 'update_bucket': + return 'microsoft_planner_update_bucket' + case 'delete_bucket': + return 'microsoft_planner_delete_bucket' + case 'get_task_details': + return 'microsoft_planner_get_task_details' + case 'update_task_details': + return 'microsoft_planner_update_task_details' default: throw new Error(`Invalid Microsoft Planner operation: ${params.operation}`) } @@ -148,43 +349,144 @@ export const MicrosoftPlannerBlock: BlockConfig = { const { credential, operation, + groupId, planId, taskId, manualTaskId, + taskIdForUpdate, + bucketId, + bucketIdForRead, title, + name, description, dueDateTime, + startDateTime, assigneeUserId, - bucketId, + priority, + percentComplete, + etag, + checklist, + references, + previewType, ...rest } = params - const baseParams = { + const baseParams: MicrosoftPlannerBlockParams = { ...rest, credential, } - // Handle both selector and manual task ID - const effectiveTaskId = (taskId || manualTaskId || '').trim() + // Handle different task ID fields + const effectiveTaskId = (taskIdForUpdate || taskId || manualTaskId || '').trim() + const effectiveBucketId = (bucketIdForRead || bucketId || '').trim() + + // List Plans + if (operation === 'list_plans') { + if (!groupId?.trim()) { + throw new Error('Group ID is required to list plans.') + } + return { + ...baseParams, + groupId: groupId.trim(), + } + } - // For read operations + // Read Plan + if (operation === 'read_plan') { + if (!planId?.trim()) { + throw new Error('Plan ID is required to read a plan.') + } + return { + ...baseParams, + planId: planId.trim(), + } + } + + // List Buckets + if (operation === 'list_buckets') { + if (!planId?.trim()) { + throw new Error('Plan ID is required to list buckets.') + } + return { + ...baseParams, + planId: planId.trim(), + } + } + + // Read Bucket + if (operation === 'read_bucket') { + if (!effectiveBucketId) { + throw new Error('Bucket ID is required to read a bucket.') + } + return { + ...baseParams, + bucketId: effectiveBucketId, + } + } + + // Create Bucket + if (operation === 'create_bucket') { + if (!planId?.trim()) { + throw new Error('Plan ID is required to create a bucket.') + } + if (!name?.trim()) { + throw new Error('Bucket name is required to create a bucket.') + } + return { + ...baseParams, + planId: planId.trim(), + name: name.trim(), + } + } + + // Update Bucket + if (operation === 'update_bucket') { + if (!effectiveBucketId) { + throw new Error('Bucket ID is required to update a bucket.') + } + if (!etag?.trim()) { + throw new Error('ETag is required to update a bucket.') + } + const updateBucketParams: MicrosoftPlannerBlockParams = { + ...baseParams, + bucketId: effectiveBucketId, + etag: etag.trim(), + } + if (name?.trim()) { + updateBucketParams.name = name.trim() + } + return updateBucketParams + } + + // Delete Bucket + if (operation === 'delete_bucket') { + if (!effectiveBucketId) { + throw new Error('Bucket ID is required to delete a bucket.') + } + if (!etag?.trim()) { + throw new Error('ETag is required to delete a bucket.') + } + return { + ...baseParams, + bucketId: effectiveBucketId, + etag: etag.trim(), + } + } + + // Read Task if (operation === 'read_task') { const readParams: MicrosoftPlannerBlockParams = { ...baseParams } - // If taskId is provided, add it (highest priority - get specific task) if (effectiveTaskId) { readParams.taskId = effectiveTaskId - } - // If no taskId but planId is provided, add planId (get tasks from plan) - else if (planId?.trim()) { + } else if (planId?.trim()) { readParams.planId = planId.trim() } - // If neither, get all user tasks (baseParams only) return readParams } - // For create operation + // Create Task if (operation === 'create_task') { if (!planId?.trim()) { throw new Error('Plan ID is required to create a task.') @@ -202,22 +504,116 @@ export const MicrosoftPlannerBlock: BlockConfig = { if (description?.trim()) { createParams.description = description.trim() } - if (dueDateTime?.trim()) { createParams.dueDateTime = dueDateTime.trim() } - if (assigneeUserId?.trim()) { createParams.assigneeUserId = assigneeUserId.trim() } - - if (bucketId?.trim()) { - createParams.bucketId = bucketId.trim() + if (effectiveBucketId) { + createParams.bucketId = effectiveBucketId } return createParams } + // Update Task + if (operation === 'update_task') { + if (!effectiveTaskId) { + throw new Error('Task ID is required to update a task.') + } + if (!etag?.trim()) { + throw new Error('ETag is required to update a task.') + } + + const updateParams: MicrosoftPlannerBlockParams = { + ...baseParams, + taskId: effectiveTaskId, + etag: etag.trim(), + } + + if (title?.trim()) { + updateParams.title = title.trim() + } + if (effectiveBucketId) { + updateParams.bucketId = effectiveBucketId + } + if (dueDateTime?.trim()) { + updateParams.dueDateTime = dueDateTime.trim() + } + if (startDateTime?.trim()) { + updateParams.startDateTime = startDateTime.trim() + } + if (assigneeUserId?.trim()) { + updateParams.assigneeUserId = assigneeUserId.trim() + } + if (priority !== undefined) { + updateParams.priority = Number(priority) + } + if (percentComplete !== undefined) { + updateParams.percentComplete = Number(percentComplete) + } + + return updateParams + } + + // Delete Task + if (operation === 'delete_task') { + if (!effectiveTaskId) { + throw new Error('Task ID is required to delete a task.') + } + if (!etag?.trim()) { + throw new Error('ETag is required to delete a task.') + } + return { + ...baseParams, + taskId: effectiveTaskId, + etag: etag.trim(), + } + } + + // Get Task Details + if (operation === 'get_task_details') { + if (!effectiveTaskId) { + throw new Error('Task ID is required to get task details.') + } + return { + ...baseParams, + taskId: effectiveTaskId, + } + } + + // Update Task Details + if (operation === 'update_task_details') { + if (!effectiveTaskId) { + throw new Error('Task ID is required to update task details.') + } + if (!etag?.trim()) { + throw new Error('ETag is required to update task details.') + } + + const updateDetailsParams: MicrosoftPlannerBlockParams = { + ...baseParams, + taskId: effectiveTaskId, + etag: etag.trim(), + } + + if (description?.trim()) { + updateDetailsParams.description = description.trim() + } + if (checklist?.trim()) { + updateDetailsParams.checklist = checklist.trim() + } + if (references?.trim()) { + updateDetailsParams.references = references.trim() + } + if (previewType?.trim()) { + updateDetailsParams.previewType = previewType.trim() + } + + return updateDetailsParams + } + return baseParams }, }, @@ -225,14 +621,25 @@ export const MicrosoftPlannerBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, credential: { type: 'string', description: 'Microsoft account credential' }, + groupId: { type: 'string', description: 'Microsoft 365 group ID' }, planId: { type: 'string', description: 'Plan ID' }, taskId: { type: 'string', description: 'Task ID' }, manualTaskId: { type: 'string', description: 'Manual Task ID' }, + taskIdForUpdate: { type: 'string', description: 'Task ID for update operations' }, + bucketId: { type: 'string', description: 'Bucket ID' }, + bucketIdForRead: { type: 'string', description: 'Bucket ID for read operations' }, title: { type: 'string', description: 'Task title' }, - description: { type: 'string', description: 'Task description' }, + name: { type: 'string', description: 'Bucket name' }, + description: { type: 'string', description: 'Task or task details description' }, dueDateTime: { type: 'string', description: 'Due date' }, + startDateTime: { type: 'string', description: 'Start date' }, assigneeUserId: { type: 'string', description: 'Assignee user ID' }, - bucketId: { type: 'string', description: 'Bucket ID' }, + priority: { type: 'number', description: 'Task priority (0-10)' }, + percentComplete: { type: 'number', description: 'Task completion percentage (0-100)' }, + etag: { type: 'string', description: 'ETag for update/delete operations' }, + checklist: { type: 'string', description: 'Checklist items as JSON' }, + references: { type: 'string', description: 'References as JSON' }, + previewType: { type: 'string', description: 'Preview type for task details' }, }, outputs: { task: { @@ -240,6 +647,34 @@ export const MicrosoftPlannerBlock: BlockConfig = { description: 'The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees.', }, + tasks: { + type: 'json', + description: 'Array of Microsoft Planner tasks', + }, + plan: { + type: 'json', + description: 'The Microsoft Planner plan object', + }, + plans: { + type: 'json', + description: 'Array of Microsoft Planner plans', + }, + bucket: { + type: 'json', + description: 'The Microsoft Planner bucket object', + }, + buckets: { + type: 'json', + description: 'Array of Microsoft Planner buckets', + }, + taskDetails: { + type: 'json', + description: 'The Microsoft Planner task details including checklist and references', + }, + deleted: { + type: 'boolean', + description: 'Confirmation of deletion', + }, metadata: { type: 'json', description: diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index 8317237d61..de6596310e 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -7,10 +7,10 @@ import { getTrigger } from '@/triggers' export const MicrosoftTeamsBlock: BlockConfig = { type: 'microsoft_teams', name: 'Microsoft Teams', - description: 'Read, write, and create messages', + description: 'Manage messages, reactions, and members in Teams', authMode: AuthMode.OAuth, longDescription: - 'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName`', + 'Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list team/channel members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName`', docsLink: 'https://docs.sim.ai/tools/microsoft_teams', category: 'tools', triggerAllowed: true, @@ -25,8 +25,18 @@ export const MicrosoftTeamsBlock: BlockConfig = { options: [ { label: 'Read Chat Messages', id: 'read_chat' }, { label: 'Write Chat Message', id: 'write_chat' }, + { label: 'Update Chat Message', id: 'update_chat_message' }, + { label: 'Delete Chat Message', id: 'delete_chat_message' }, { label: 'Read Channel Messages', id: 'read_channel' }, { label: 'Write Channel Message', id: 'write_channel' }, + { label: 'Update Channel Message', id: 'update_channel_message' }, + { label: 'Delete Channel Message', id: 'delete_channel_message' }, + { label: 'Reply to Channel Message', id: 'reply_to_message' }, + { label: 'Get Message', id: 'get_message' }, + { label: 'Add Reaction', id: 'set_reaction' }, + { label: 'Remove Reaction', id: 'unset_reaction' }, + { label: 'List Team Members', id: 'list_team_members' }, + { label: 'List Channel Members', id: 'list_channel_members' }, ], value: () => 'read_chat', }, @@ -45,13 +55,16 @@ export const MicrosoftTeamsBlock: BlockConfig = { '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', @@ -71,7 +84,18 @@ export const MicrosoftTeamsBlock: BlockConfig = { placeholder: 'Select a team', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: ['read_channel', 'write_channel'] }, + condition: { + field: 'operation', + value: [ + 'read_channel', + 'write_channel', + 'update_channel_message', + 'delete_channel_message', + 'reply_to_message', + 'list_team_members', + 'list_channel_members', + ], + }, }, { id: 'manualTeamId', @@ -81,7 +105,18 @@ export const MicrosoftTeamsBlock: BlockConfig = { canonicalParamId: 'teamId', placeholder: 'Enter team ID', mode: 'advanced', - condition: { field: 'operation', value: ['read_channel', 'write_channel'] }, + condition: { + field: 'operation', + value: [ + 'read_channel', + 'write_channel', + 'update_channel_message', + 'delete_channel_message', + 'reply_to_message', + 'list_team_members', + 'list_channel_members', + ], + }, }, { id: 'chatId', @@ -95,7 +130,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { placeholder: 'Select a chat', dependsOn: ['credential'], mode: 'basic', - condition: { field: 'operation', value: ['read_chat', 'write_chat'] }, + condition: { + field: 'operation', + value: ['read_chat', 'write_chat', 'update_chat_message', 'delete_chat_message'], + }, }, { id: 'manualChatId', @@ -105,7 +143,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { canonicalParamId: 'chatId', placeholder: 'Enter chat ID', mode: 'advanced', - condition: { field: 'operation', value: ['read_chat', 'write_chat'] }, + condition: { + field: 'operation', + value: ['read_chat', 'write_chat', 'update_chat_message', 'delete_chat_message'], + }, }, { id: 'channelId', @@ -119,7 +160,17 @@ export const MicrosoftTeamsBlock: BlockConfig = { placeholder: 'Select a channel', dependsOn: ['credential', 'teamId'], mode: 'basic', - condition: { field: 'operation', value: ['read_channel', 'write_channel'] }, + condition: { + field: 'operation', + value: [ + 'read_channel', + 'write_channel', + 'update_channel_message', + 'delete_channel_message', + 'reply_to_message', + 'list_channel_members', + ], + }, }, { id: 'manualChannelId', @@ -129,7 +180,38 @@ export const MicrosoftTeamsBlock: BlockConfig = { canonicalParamId: 'channelId', placeholder: 'Enter channel ID', mode: 'advanced', - condition: { field: 'operation', value: ['read_channel', 'write_channel'] }, + condition: { + field: 'operation', + value: [ + 'read_channel', + 'write_channel', + 'update_channel_message', + 'delete_channel_message', + 'reply_to_message', + 'list_channel_members', + ], + }, + }, + { + id: 'messageId', + title: 'Message ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter message ID', + condition: { + field: 'operation', + value: [ + 'update_chat_message', + 'delete_chat_message', + 'update_channel_message', + 'delete_channel_message', + 'reply_to_message', + 'get_message', + 'set_reaction', + 'unset_reaction', + ], + }, + required: true, }, { id: 'content', @@ -137,7 +219,28 @@ export const MicrosoftTeamsBlock: BlockConfig = { type: 'long-input', layout: 'full', placeholder: 'Enter message content', - condition: { field: 'operation', value: ['write_chat', 'write_channel'] }, + condition: { + field: 'operation', + value: [ + 'write_chat', + 'write_channel', + 'update_chat_message', + 'update_channel_message', + 'reply_to_message', + ], + }, + required: true, + }, + { + id: 'reactionType', + title: 'Reaction', + type: 'short-input', + layout: 'full', + placeholder: 'Enter emoji (e.g., ❤️, 👍, 😊)', + condition: { + field: 'operation', + value: ['set_reaction', 'unset_reaction'], + }, required: true, }, // File upload (basic mode) @@ -174,6 +277,16 @@ export const MicrosoftTeamsBlock: BlockConfig = { 'microsoft_teams_write_chat', 'microsoft_teams_read_channel', 'microsoft_teams_write_channel', + 'microsoft_teams_update_chat_message', + 'microsoft_teams_update_channel_message', + 'microsoft_teams_delete_chat_message', + 'microsoft_teams_delete_channel_message', + 'microsoft_teams_reply_to_message', + 'microsoft_teams_get_message', + 'microsoft_teams_set_reaction', + 'microsoft_teams_unset_reaction', + 'microsoft_teams_list_team_members', + 'microsoft_teams_list_channel_members', ], config: { tool: (params) => { @@ -186,6 +299,26 @@ export const MicrosoftTeamsBlock: BlockConfig = { return 'microsoft_teams_read_channel' case 'write_channel': return 'microsoft_teams_write_channel' + case 'update_chat_message': + return 'microsoft_teams_update_chat_message' + case 'update_channel_message': + return 'microsoft_teams_update_channel_message' + case 'delete_chat_message': + return 'microsoft_teams_delete_chat_message' + case 'delete_channel_message': + return 'microsoft_teams_delete_channel_message' + case 'reply_to_message': + return 'microsoft_teams_reply_to_message' + case 'get_message': + return 'microsoft_teams_get_message' + case 'set_reaction': + return 'microsoft_teams_set_reaction' + case 'unset_reaction': + return 'microsoft_teams_unset_reaction' + case 'list_team_members': + return 'microsoft_teams_list_team_members' + case 'list_channel_members': + return 'microsoft_teams_list_channel_members' default: return 'microsoft_teams_read_chat' } @@ -202,6 +335,8 @@ export const MicrosoftTeamsBlock: BlockConfig = { manualChannelId, attachmentFiles, files, + messageId, + reactionType, ...rest } = params @@ -220,14 +355,37 @@ export const MicrosoftTeamsBlock: BlockConfig = { baseParams.files = fileParam } - if (operation === 'read_chat' || operation === 'write_chat') { + // Add messageId if provided + if (messageId) { + baseParams.messageId = messageId + } + + // Add reactionType if provided + if (reactionType) { + baseParams.reactionType = reactionType + } + + // Chat operations + if ( + operation === 'read_chat' || + operation === 'write_chat' || + operation === 'update_chat_message' || + operation === 'delete_chat_message' + ) { if (!effectiveChatId) { throw new Error('Chat ID is required. Please select a chat or enter a chat ID.') } return { ...baseParams, chatId: effectiveChatId } } - if (operation === 'read_channel' || operation === 'write_channel') { + // Channel operations + if ( + operation === 'read_channel' || + operation === 'write_channel' || + operation === 'update_channel_message' || + operation === 'delete_channel_message' || + operation === 'reply_to_message' + ) { if (!effectiveTeamId) { throw new Error('Team ID is required for channel operations.') } @@ -237,6 +395,43 @@ export const MicrosoftTeamsBlock: BlockConfig = { return { ...baseParams, teamId: effectiveTeamId, channelId: effectiveChannelId } } + // Team member operations + if (operation === 'list_team_members') { + if (!effectiveTeamId) { + throw new Error('Team ID is required for team member operations.') + } + return { ...baseParams, teamId: effectiveTeamId } + } + + // Channel member operations + if (operation === 'list_channel_members') { + if (!effectiveTeamId) { + throw new Error('Team ID is required for channel member operations.') + } + if (!effectiveChannelId) { + throw new Error('Channel ID is required for channel member operations.') + } + return { ...baseParams, teamId: effectiveTeamId, channelId: effectiveChannelId } + } + + // Operations that work with either chat or channel (get_message, reactions) + // These tools handle the routing internally based on what IDs are provided + if ( + operation === 'get_message' || + operation === 'set_reaction' || + operation === 'unset_reaction' + ) { + if (effectiveChatId) { + return { ...baseParams, chatId: effectiveChatId } + } + if (effectiveTeamId && effectiveChannelId) { + return { ...baseParams, teamId: effectiveTeamId, channelId: effectiveChannelId } + } + throw new Error( + 'Either Chat ID or both Team ID and Channel ID are required for this operation.' + ) + } + return baseParams }, }, @@ -244,7 +439,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, credential: { type: 'string', description: 'Microsoft Teams access token' }, - messageId: { type: 'string', description: 'Message identifier' }, + messageId: { + type: 'string', + description: 'Message identifier for update/delete/reply/reaction operations', + }, chatId: { type: 'string', description: 'Chat identifier' }, manualChatId: { type: 'string', description: 'Manual chat identifier' }, channelId: { type: 'string', description: 'Channel identifier' }, @@ -255,6 +453,7 @@ export const MicrosoftTeamsBlock: BlockConfig = { type: 'string', description: 'Message content. Mention users with userName', }, + reactionType: { type: 'string', description: 'Emoji reaction (e.g., ❤️, 👍, 😊)' }, attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, files: { type: 'json', description: 'Files to attach (UserFile array)' }, }, @@ -269,7 +468,8 @@ export const MicrosoftTeamsBlock: BlockConfig = { type: 'boolean', description: 'Whether content was successfully updated/sent', }, - messageId: { type: 'string', description: 'ID of the created/sent message' }, + deleted: { type: 'boolean', description: 'Whether message was successfully deleted' }, + messageId: { type: 'string', description: 'ID of the created/sent/deleted message' }, createdTime: { type: 'string', description: 'Timestamp when message was created' }, url: { type: 'string', description: 'Web URL to the message' }, sender: { type: 'string', description: 'Message sender display name' }, @@ -278,6 +478,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { type: 'string', description: 'Type of message (message, systemEventMessage, etc.)', }, + reactionType: { type: 'string', description: 'Emoji reaction that was added/removed' }, + success: { type: 'boolean', description: 'Whether the operation was successful' }, + members: { type: 'json', description: 'Array of team/channel member objects' }, + memberCount: { type: 'number', description: 'Total number of members' }, type: { type: 'string', description: 'Type of Teams message' }, id: { type: 'string', description: 'Unique message identifier' }, timestamp: { type: 'string', description: 'Message timestamp' }, diff --git a/apps/sim/blocks/blocks/parallel.ts b/apps/sim/blocks/blocks/parallel.ts index 69de17d1d4..e3807cc20e 100644 --- a/apps/sim/blocks/blocks/parallel.ts +++ b/apps/sim/blocks/blocks/parallel.ts @@ -5,14 +5,27 @@ import type { ToolResponse } from '@/tools/types' export const ParallelBlock: BlockConfig = { type: 'parallel_ai', name: 'Parallel AI', - description: 'Search with Parallel AI', + description: 'Web research with Parallel AI', authMode: AuthMode.ApiKey, - longDescription: 'Integrate Parallel AI into the workflow. Can search the web.', - docsLink: 'https://docs.parallel.ai/search-api/search-quickstart', + longDescription: + 'Integrate Parallel AI into the workflow. Can search the web, extract information from URLs, and conduct deep research.', + docsLink: 'https://docs.parallel.ai/', category: 'tools', bgColor: '#E0E0E0', icon: ParallelIcon, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Search', id: 'search' }, + { label: 'Extract from URLs', id: 'extract' }, + { label: 'Deep Research', id: 'deep_research' }, + ], + value: () => 'search', + }, { id: 'objective', title: 'Search Objective', @@ -20,6 +33,7 @@ export const ParallelBlock: BlockConfig = { layout: 'full', placeholder: "When was the United Nations established? Prefer UN's websites.", required: true, + condition: { field: 'operation', value: 'search' }, }, { id: 'search_queries', @@ -29,6 +43,87 @@ export const ParallelBlock: BlockConfig = { placeholder: 'Enter search queries separated by commas (e.g., "Founding year UN", "Year of founding United Nations")', required: false, + condition: { field: 'operation', value: 'search' }, + }, + { + id: 'urls', + title: 'URLs', + type: 'long-input', + layout: 'full', + placeholder: + 'Enter URLs separated by commas (e.g., https://example.com, https://another.com)', + required: true, + condition: { field: 'operation', value: 'extract' }, + }, + { + id: 'extract_objective', + title: 'Extract Objective', + type: 'long-input', + layout: 'full', + placeholder: 'What information to extract from the URLs?', + required: true, + condition: { field: 'operation', value: 'extract' }, + }, + { + id: 'excerpts', + title: 'Include Excerpts', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'extract' }, + }, + { + id: 'full_content', + title: 'Include Full Content', + type: 'dropdown', + layout: 'half', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'extract' }, + }, + { + id: 'research_input', + title: 'Research Query', + type: 'long-input', + layout: 'full', + placeholder: 'Enter your research question (up to 15,000 characters)', + required: true, + condition: { field: 'operation', value: 'deep_research' }, + }, + { + id: 'output_schema', + title: 'Output Format', + type: 'long-input', + layout: 'full', + placeholder: + 'Enter "text" for markdown report, or describe desired output structure (leave empty for auto)', + required: false, + condition: { field: 'operation', value: 'deep_research' }, + }, + { + id: 'include_domains', + title: 'Include Domains', + type: 'short-input', + layout: 'half', + placeholder: 'Comma-separated domains to include', + required: false, + condition: { field: 'operation', value: 'deep_research' }, + }, + { + id: 'exclude_domains', + title: 'Exclude Domains', + type: 'short-input', + layout: 'half', + placeholder: 'Comma-separated domains to exclude', + required: false, + condition: { field: 'operation', value: 'deep_research' }, }, { id: 'processor', @@ -36,10 +131,17 @@ export const ParallelBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ - { label: 'Base', id: 'base' }, - { label: 'Pro', id: 'pro' }, + { label: 'Lite ($5/1K)', id: 'lite' }, + { label: 'Base ($10/1K)', id: 'base' }, + { label: 'Core ($25/1K)', id: 'core' }, + { label: 'Core 2x ($50/1K)', id: 'core2x' }, + { label: 'Pro ($100/1K)', id: 'pro' }, + { label: 'Ultra ($300/1K)', id: 'ultra' }, + { label: 'Ultra 2x ($600/1K)', id: 'ultra2x' }, + { label: 'Ultra 4x ($1,200/1K)', id: 'ultra4x' }, ], value: () => 'base', + condition: { field: 'operation', value: ['search', 'deep_research'] }, }, { id: 'max_results', @@ -47,6 +149,7 @@ export const ParallelBlock: BlockConfig = { type: 'short-input', layout: 'half', placeholder: '5', + condition: { field: 'operation', value: 'search' }, }, { id: 'max_chars_per_result', @@ -54,6 +157,7 @@ export const ParallelBlock: BlockConfig = { type: 'short-input', layout: 'half', placeholder: '1500', + condition: { field: 'operation', value: 'search' }, }, { id: 'apiKey', @@ -66,44 +170,104 @@ export const ParallelBlock: BlockConfig = { }, ], tools: { - access: ['parallel_search'], + access: ['parallel_search', 'parallel_extract', 'parallel_deep_research'], config: { tool: (params) => { - // Convert search_queries from comma-separated string to array (if provided) - if (params.search_queries && typeof params.search_queries === 'string') { - const queries = params.search_queries - .split(',') - .map((query: string) => query.trim()) - .filter((query: string) => query.length > 0) - // Only set if we have actual queries - if (queries.length > 0) { - params.search_queries = queries - } else { - params.search_queries = undefined - } - } + switch (params.operation) { + case 'search': + // Convert search_queries from comma-separated string to array (if provided) + if (params.search_queries && typeof params.search_queries === 'string') { + const queries = params.search_queries + .split(',') + .map((query: string) => query.trim()) + .filter((query: string) => query.length > 0) + // Only set if we have actual queries + if (queries.length > 0) { + params.search_queries = queries + } else { + params.search_queries = undefined + } + } - // Convert numeric parameters - if (params.max_results) { - params.max_results = Number(params.max_results) - } - if (params.max_chars_per_result) { - params.max_chars_per_result = Number(params.max_chars_per_result) - } + // Convert numeric parameters + if (params.max_results) { + params.max_results = Number(params.max_results) + } + if (params.max_chars_per_result) { + params.max_chars_per_result = Number(params.max_chars_per_result) + } + + return 'parallel_search' + + case 'extract': + // Map extract_objective to objective for the tool + params.objective = params.extract_objective + + // Convert boolean strings to actual booleans with defaults + if (params.excerpts === 'true' || params.excerpts === true) { + params.excerpts = true + } else if (params.excerpts === 'false' || params.excerpts === false) { + params.excerpts = false + } else { + // Default to true if not provided + params.excerpts = true + } - return 'parallel_search' + if (params.full_content === 'true' || params.full_content === true) { + params.full_content = true + } else if (params.full_content === 'false' || params.full_content === false) { + params.full_content = false + } else { + // Default to false if not provided + params.full_content = false + } + + return 'parallel_extract' + + case 'deep_research': + // Map research_input to input for the tool + params.input = params.research_input + return 'parallel_deep_research' + + default: + return 'parallel_search' + } }, }, }, inputs: { + operation: { type: 'string', description: 'Operation type' }, objective: { type: 'string', description: 'Search objective or question' }, search_queries: { type: 'string', description: 'Comma-separated search queries' }, + urls: { type: 'string', description: 'Comma-separated URLs' }, + extract_objective: { type: 'string', description: 'What to extract from URLs' }, + excerpts: { type: 'boolean', description: 'Include excerpts' }, + full_content: { type: 'boolean', description: 'Include full content' }, + research_input: { type: 'string', description: 'Deep research query' }, + output_schema: { type: 'string', description: 'Output format specification' }, + include_domains: { type: 'string', description: 'Domains to include' }, + exclude_domains: { type: 'string', description: 'Domains to exclude' }, processor: { type: 'string', description: 'Processing method' }, max_results: { type: 'number', description: 'Maximum number of results' }, max_chars_per_result: { type: 'number', description: 'Maximum characters per result' }, apiKey: { type: 'string', description: 'Parallel AI API key' }, }, outputs: { - results: { type: 'array', description: 'Search results with excerpts from relevant pages' }, + results: { type: 'string', description: 'Search or extract results (JSON stringified)' }, + status: { type: 'string', description: 'Task status (for deep research)' }, + run_id: { type: 'string', description: 'Task run ID (for deep research)' }, + message: { type: 'string', description: 'Status message (for deep research)' }, + content: { + type: 'string', + description: 'Research content (for deep research, JSON stringified)', + }, + basis: { + type: 'string', + description: 'Citations and sources (for deep research, JSON stringified)', + }, + metadata: { + type: 'string', + description: 'Task metadata (for deep research, JSON stringified)', + }, }, } diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index ffbe0509e2..3435ad839d 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -9,7 +9,7 @@ export const RedditBlock: BlockConfig = { description: 'Access Reddit data and content', authMode: AuthMode.OAuth, longDescription: - 'Integrate Reddit into the workflow. Can get posts and comments from a subreddit.', + 'Integrate Reddit into workflows. Read posts, comments, and search content. Submit posts, vote, reply, edit, and manage your Reddit account.', docsLink: 'https://docs.sim.ai/tools/reddit', category: 'tools', bgColor: '#FF5700', @@ -24,6 +24,17 @@ export const RedditBlock: BlockConfig = { options: [ { label: 'Get Posts', id: 'get_posts' }, { label: 'Get Comments', id: 'get_comments' }, + { label: 'Get Controversial Posts', id: 'get_controversial' }, + { label: 'Get Gilded Posts', id: 'get_gilded' }, + { label: 'Search Subreddit', id: 'search' }, + { label: 'Submit Post', id: 'submit_post' }, + { label: 'Vote', id: 'vote' }, + { label: 'Save', id: 'save' }, + { label: 'Unsave', id: 'unsave' }, + { label: 'Reply', id: 'reply' }, + { label: 'Edit', id: 'edit' }, + { label: 'Delete', id: 'delete' }, + { label: 'Subscribe', id: 'subscribe' }, ], value: () => 'get_posts', }, @@ -36,7 +47,24 @@ export const RedditBlock: BlockConfig = { layout: 'full', provider: 'reddit', serviceId: 'reddit', - requiredScopes: ['identity', 'read'], + requiredScopes: [ + 'identity', + 'read', + 'submit', + 'vote', + 'save', + 'edit', + 'subscribe', + 'history', + 'privatemessages', + 'account', + 'mysubreddits', + 'flair', + 'report', + 'modposts', + 'modflair', + 'modmail', + ], placeholder: 'Select Reddit account', required: true, }, @@ -50,7 +78,7 @@ export const RedditBlock: BlockConfig = { placeholder: 'Enter subreddit name (without r/)', condition: { field: 'operation', - value: ['get_posts', 'get_comments'], + value: ['get_posts', 'get_comments', 'get_controversial', 'get_gilded', 'search'], }, required: true, }, @@ -149,9 +177,381 @@ export const RedditBlock: BlockConfig = { value: 'get_comments', }, }, + + // Get Controversial specific fields + { + id: 'controversialTime', + title: 'Time Filter', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Hour', id: 'hour' }, + { label: 'Day', id: 'day' }, + { label: 'Week', id: 'week' }, + { label: 'Month', id: 'month' }, + { label: 'Year', id: 'year' }, + { label: 'All Time', id: 'all' }, + ], + condition: { + field: 'operation', + value: 'get_controversial', + }, + }, + { + id: 'controversialLimit', + title: 'Max Posts', + type: 'short-input', + layout: 'full', + placeholder: '10', + condition: { + field: 'operation', + value: 'get_controversial', + }, + }, + + // Get Gilded specific fields + { + id: 'gildedLimit', + title: 'Max Posts', + type: 'short-input', + layout: 'full', + placeholder: '10', + condition: { + field: 'operation', + value: 'get_gilded', + }, + }, + + // Search specific fields + { + id: 'searchQuery', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'Enter search query', + condition: { + field: 'operation', + value: 'search', + }, + required: true, + }, + { + id: 'searchSort', + title: 'Sort By', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Relevance', id: 'relevance' }, + { label: 'Hot', id: 'hot' }, + { label: 'Top', id: 'top' }, + { label: 'New', id: 'new' }, + { label: 'Comments', id: 'comments' }, + ], + condition: { + field: 'operation', + value: 'search', + }, + }, + { + id: 'searchTime', + title: 'Time Filter', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Hour', id: 'hour' }, + { label: 'Day', id: 'day' }, + { label: 'Week', id: 'week' }, + { label: 'Month', id: 'month' }, + { label: 'Year', id: 'year' }, + { label: 'All Time', id: 'all' }, + ], + condition: { + field: 'operation', + value: 'search', + }, + }, + { + id: 'searchLimit', + title: 'Max Results', + type: 'short-input', + layout: 'full', + placeholder: '10', + condition: { + field: 'operation', + value: 'search', + }, + }, + + // Submit Post specific fields + { + id: 'submitSubreddit', + title: 'Subreddit', + type: 'short-input', + layout: 'full', + placeholder: 'Enter subreddit name (without r/)', + condition: { + field: 'operation', + value: 'submit_post', + }, + required: true, + }, + { + id: 'title', + title: 'Post Title', + type: 'short-input', + layout: 'full', + placeholder: 'Enter post title', + condition: { + field: 'operation', + value: 'submit_post', + }, + required: true, + }, + { + id: 'postType', + title: 'Post Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Text Post', id: 'text' }, + { label: 'Link Post', id: 'link' }, + ], + condition: { + field: 'operation', + value: 'submit_post', + }, + value: () => 'text', + required: true, + }, + { + id: 'text', + title: 'Post Text (Markdown)', + type: 'long-input', + layout: 'full', + placeholder: 'Enter post text in markdown format', + condition: { + field: 'operation', + value: 'submit_post', + and: { + field: 'postType', + value: 'text', + }, + }, + }, + { + id: 'url', + title: 'URL', + type: 'short-input', + layout: 'full', + placeholder: 'Enter URL to share', + condition: { + field: 'operation', + value: 'submit_post', + and: { + field: 'postType', + value: 'link', + }, + }, + }, + { + id: 'nsfw', + title: 'Mark as NSFW', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { + field: 'operation', + value: 'submit_post', + }, + value: () => 'false', + }, + { + id: 'spoiler', + title: 'Mark as Spoiler', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { + field: 'operation', + value: 'submit_post', + }, + value: () => 'false', + }, + + // Vote specific fields + { + id: 'voteId', + title: 'Post/Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter thing ID (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { + field: 'operation', + value: 'vote', + }, + required: true, + }, + { + id: 'voteDirection', + title: 'Vote Direction', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Upvote', id: '1' }, + { label: 'Unvote', id: '0' }, + { label: 'Downvote', id: '-1' }, + ], + condition: { + field: 'operation', + value: 'vote', + }, + value: () => '1', + required: true, + }, + + // Save/Unsave specific fields + { + id: 'saveId', + title: 'Post/Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter thing ID (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { + field: 'operation', + value: ['save', 'unsave'], + }, + required: true, + }, + { + id: 'saveCategory', + title: 'Category (optional)', + type: 'short-input', + layout: 'full', + placeholder: 'Enter category name', + condition: { + field: 'operation', + value: 'save', + }, + }, + + // Reply specific fields + { + id: 'replyParentId', + title: 'Parent Post/Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter thing ID to reply to (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { + field: 'operation', + value: 'reply', + }, + required: true, + }, + { + id: 'replyText', + title: 'Reply Text (Markdown)', + type: 'long-input', + layout: 'full', + placeholder: 'Enter reply text in markdown format', + condition: { + field: 'operation', + value: 'reply', + }, + required: true, + }, + + // Edit specific fields + { + id: 'editThingId', + title: 'Post/Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter thing ID to edit (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { + field: 'operation', + value: 'edit', + }, + required: true, + }, + { + id: 'editText', + title: 'New Text (Markdown)', + type: 'long-input', + layout: 'full', + placeholder: 'Enter new text in markdown format', + condition: { + field: 'operation', + value: 'edit', + }, + required: true, + }, + + // Delete specific fields + { + id: 'deleteId', + title: 'Post/Comment ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter thing ID to delete (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + condition: { + field: 'operation', + value: 'delete', + }, + required: true, + }, + + // Subscribe specific fields + { + id: 'subscribeSubreddit', + title: 'Subreddit', + type: 'short-input', + layout: 'full', + placeholder: 'Enter subreddit name (without r/)', + condition: { + field: 'operation', + value: 'subscribe', + }, + required: true, + }, + { + id: 'subscribeAction', + title: 'Action', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Subscribe', id: 'sub' }, + { label: 'Unsubscribe', id: 'unsub' }, + ], + condition: { + field: 'operation', + value: 'subscribe', + }, + value: () => 'sub', + required: true, + }, ], tools: { - access: ['reddit_get_posts', 'reddit_get_comments'], + access: [ + 'reddit_get_posts', + 'reddit_get_comments', + 'reddit_get_controversial', + 'reddit_get_gilded', + 'reddit_search', + 'reddit_submit_post', + 'reddit_vote', + 'reddit_save', + 'reddit_unsave', + 'reddit_reply', + 'reddit_edit', + 'reddit_delete', + 'reddit_subscribe', + ], config: { tool: (inputs) => { const operation = inputs.operation || 'get_posts' @@ -160,6 +560,50 @@ export const RedditBlock: BlockConfig = { return 'reddit_get_comments' } + if (operation === 'get_controversial') { + return 'reddit_get_controversial' + } + + if (operation === 'get_gilded') { + return 'reddit_get_gilded' + } + + if (operation === 'search') { + return 'reddit_search' + } + + if (operation === 'submit_post') { + return 'reddit_submit_post' + } + + if (operation === 'vote') { + return 'reddit_vote' + } + + if (operation === 'save') { + return 'reddit_save' + } + + if (operation === 'unsave') { + return 'reddit_unsave' + } + + if (operation === 'reply') { + return 'reddit_reply' + } + + if (operation === 'edit') { + return 'reddit_edit' + } + + if (operation === 'delete') { + return 'reddit_delete' + } + + if (operation === 'subscribe') { + return 'reddit_subscribe' + } + return 'reddit_get_posts' }, params: (inputs) => { @@ -176,6 +620,100 @@ export const RedditBlock: BlockConfig = { } } + if (operation === 'get_controversial') { + return { + subreddit: rest.subreddit, + time: rest.controversialTime, + limit: rest.controversialLimit ? Number.parseInt(rest.controversialLimit) : undefined, + credential: credential, + } + } + + if (operation === 'get_gilded') { + return { + subreddit: rest.subreddit, + limit: rest.gildedLimit ? Number.parseInt(rest.gildedLimit) : undefined, + credential: credential, + } + } + + if (operation === 'search') { + return { + subreddit: rest.subreddit, + query: rest.searchQuery, + sort: rest.searchSort, + time: rest.searchTime, + limit: rest.searchLimit ? Number.parseInt(rest.searchLimit) : undefined, + credential: credential, + } + } + + if (operation === 'submit_post') { + return { + subreddit: rest.submitSubreddit, + title: rest.title, + text: rest.postType === 'text' ? rest.text : undefined, + url: rest.postType === 'link' ? rest.url : undefined, + nsfw: rest.nsfw === 'true', + spoiler: rest.spoiler === 'true', + credential: credential, + } + } + + if (operation === 'vote') { + return { + id: rest.voteId, + dir: Number.parseInt(rest.voteDirection), + credential: credential, + } + } + + if (operation === 'save') { + return { + id: rest.saveId, + category: rest.saveCategory, + credential: credential, + } + } + + if (operation === 'unsave') { + return { + id: rest.saveId, + credential: credential, + } + } + + if (operation === 'reply') { + return { + parent_id: rest.replyParentId, + text: rest.replyText, + credential: credential, + } + } + + if (operation === 'edit') { + return { + thing_id: rest.editThingId, + text: rest.editText, + credential: credential, + } + } + + if (operation === 'delete') { + return { + id: rest.deleteId, + credential: credential, + } + } + + if (operation === 'subscribe') { + return { + subreddit: rest.subscribeSubreddit, + action: rest.subscribeAction, + credential: credential, + } + } + return { subreddit: rest.subreddit, sort: rest.sort, @@ -196,6 +734,34 @@ export const RedditBlock: BlockConfig = { postId: { type: 'string', description: 'Post identifier' }, commentSort: { type: 'string', description: 'Comment sort order' }, commentLimit: { type: 'number', description: 'Maximum comments' }, + controversialTime: { type: 'string', description: 'Time filter for controversial posts' }, + controversialLimit: { type: 'number', description: 'Maximum controversial posts' }, + gildedLimit: { type: 'number', description: 'Maximum gilded posts' }, + searchQuery: { type: 'string', description: 'Search query text' }, + searchSort: { type: 'string', description: 'Search result sort order' }, + searchTime: { type: 'string', description: 'Time filter for search results' }, + searchLimit: { type: 'number', description: 'Maximum search results' }, + submitSubreddit: { type: 'string', description: 'Subreddit to submit post to' }, + title: { type: 'string', description: 'Post title' }, + postType: { type: 'string', description: 'Type of post (text or link)' }, + text: { type: 'string', description: 'Post text content in markdown' }, + url: { type: 'string', description: 'URL for link posts' }, + nsfw: { type: 'boolean', description: 'Mark post as NSFW' }, + spoiler: { type: 'boolean', description: 'Mark post as spoiler' }, + voteId: { type: 'string', description: 'Post or comment ID to vote on' }, + voteDirection: { + type: 'number', + description: 'Vote direction (1=upvote, 0=unvote, -1=downvote)', + }, + saveId: { type: 'string', description: 'Post or comment ID to save/unsave' }, + saveCategory: { type: 'string', description: 'Category for saved items' }, + replyParentId: { type: 'string', description: 'Parent post or comment ID to reply to' }, + replyText: { type: 'string', description: 'Reply text in markdown' }, + editThingId: { type: 'string', description: 'Post or comment ID to edit' }, + editText: { type: 'string', description: 'New text content in markdown' }, + deleteId: { type: 'string', description: 'Post or comment ID to delete' }, + subscribeSubreddit: { type: 'string', description: 'Subreddit to subscribe/unsubscribe' }, + subscribeAction: { type: 'string', description: 'Subscribe action (sub or unsub)' }, }, outputs: { subreddit: { type: 'string', description: 'Subreddit name' }, diff --git a/apps/sim/blocks/blocks/stripe.ts b/apps/sim/blocks/blocks/stripe.ts new file mode 100644 index 0000000000..6d79e42b23 --- /dev/null +++ b/apps/sim/blocks/blocks/stripe.ts @@ -0,0 +1,820 @@ +import { StripeIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { StripeResponse } from '@/tools/stripe/types' +import { getTrigger } from '@/triggers' + +export const StripeBlock: BlockConfig = { + type: 'stripe', + name: 'Stripe', + description: 'Process payments and manage Stripe data', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrates Stripe into the workflow. Manage payment intents, customers, subscriptions, invoices, charges, products, prices, and events. Can be used in trigger mode to trigger a workflow when a Stripe event occurs.', + docsLink: 'https://docs.sim.ai/tools/stripe', + category: 'tools', + bgColor: '#635BFF', + icon: StripeIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + // Payment Intents + { label: 'Create Payment Intent', id: 'create_payment_intent' }, + { label: 'Retrieve Payment Intent', id: 'retrieve_payment_intent' }, + { label: 'Update Payment Intent', id: 'update_payment_intent' }, + { label: 'Confirm Payment Intent', id: 'confirm_payment_intent' }, + { label: 'Capture Payment Intent', id: 'capture_payment_intent' }, + { label: 'Cancel Payment Intent', id: 'cancel_payment_intent' }, + { label: 'List Payment Intents', id: 'list_payment_intents' }, + { label: 'Search Payment Intents', id: 'search_payment_intents' }, + // Customers + { label: 'Create Customer', id: 'create_customer' }, + { label: 'Retrieve Customer', id: 'retrieve_customer' }, + { label: 'Update Customer', id: 'update_customer' }, + { label: 'Delete Customer', id: 'delete_customer' }, + { label: 'List Customers', id: 'list_customers' }, + { label: 'Search Customers', id: 'search_customers' }, + // Subscriptions + { label: 'Create Subscription', id: 'create_subscription' }, + { label: 'Retrieve Subscription', id: 'retrieve_subscription' }, + { label: 'Update Subscription', id: 'update_subscription' }, + { label: 'Cancel Subscription', id: 'cancel_subscription' }, + { label: 'Resume Subscription', id: 'resume_subscription' }, + { label: 'List Subscriptions', id: 'list_subscriptions' }, + { label: 'Search Subscriptions', id: 'search_subscriptions' }, + // Invoices + { label: 'Create Invoice', id: 'create_invoice' }, + { label: 'Retrieve Invoice', id: 'retrieve_invoice' }, + { label: 'Update Invoice', id: 'update_invoice' }, + { label: 'Delete Invoice', id: 'delete_invoice' }, + { label: 'Finalize Invoice', id: 'finalize_invoice' }, + { label: 'Pay Invoice', id: 'pay_invoice' }, + { label: 'Void Invoice', id: 'void_invoice' }, + { label: 'Send Invoice', id: 'send_invoice' }, + { label: 'List Invoices', id: 'list_invoices' }, + { label: 'Search Invoices', id: 'search_invoices' }, + // Charges + { label: 'Create Charge', id: 'create_charge' }, + { label: 'Retrieve Charge', id: 'retrieve_charge' }, + { label: 'Update Charge', id: 'update_charge' }, + { label: 'Capture Charge', id: 'capture_charge' }, + { label: 'List Charges', id: 'list_charges' }, + { label: 'Search Charges', id: 'search_charges' }, + // Products + { label: 'Create Product', id: 'create_product' }, + { label: 'Retrieve Product', id: 'retrieve_product' }, + { label: 'Update Product', id: 'update_product' }, + { label: 'Delete Product', id: 'delete_product' }, + { label: 'List Products', id: 'list_products' }, + { label: 'Search Products', id: 'search_products' }, + // Prices + { label: 'Create Price', id: 'create_price' }, + { label: 'Retrieve Price', id: 'retrieve_price' }, + { label: 'Update Price', id: 'update_price' }, + { label: 'List Prices', id: 'list_prices' }, + { label: 'Search Prices', id: 'search_prices' }, + // Events + { label: 'Retrieve Event', id: 'retrieve_event' }, + { label: 'List Events', id: 'list_events' }, + ], + value: () => 'create_payment_intent', + }, + { + id: 'apiKey', + title: 'Stripe API Key', + type: 'short-input', + password: true, + placeholder: 'Enter your Stripe secret key (sk_test_... or sk_live_...)', + required: true, + }, + // Common ID field for retrieve/update/delete/confirm/capture/cancel operations + { + id: 'id', + title: 'ID', + type: 'short-input', + placeholder: 'Enter the ID', + condition: { + field: 'operation', + value: [ + 'retrieve_payment_intent', + 'update_payment_intent', + 'confirm_payment_intent', + 'capture_payment_intent', + 'cancel_payment_intent', + 'retrieve_customer', + 'update_customer', + 'delete_customer', + 'retrieve_subscription', + 'update_subscription', + 'cancel_subscription', + 'resume_subscription', + 'retrieve_invoice', + 'update_invoice', + 'delete_invoice', + 'finalize_invoice', + 'pay_invoice', + 'void_invoice', + 'send_invoice', + 'retrieve_charge', + 'update_charge', + 'capture_charge', + 'retrieve_product', + 'update_product', + 'delete_product', + 'retrieve_price', + 'update_price', + 'retrieve_event', + ], + }, + required: true, + }, + // Payment Intent specific fields - CREATE (amount required) + { + id: 'amount', + title: 'Amount (in cents)', + type: 'short-input', + placeholder: 'e.g., 1000 for $10.00', + condition: { + field: 'operation', + value: ['create_payment_intent', 'create_charge'], + }, + required: true, + }, + // Payment Intent specific fields - UPDATE/CAPTURE (amount optional) + { + id: 'amount', + title: 'Amount (in cents)', + type: 'short-input', + placeholder: 'e.g., 1000 for $10.00', + condition: { + field: 'operation', + value: ['update_payment_intent', 'capture_payment_intent', 'capture_charge'], + }, + }, + // Currency - REQUIRED for create operations + { + id: 'currency', + title: 'Currency', + type: 'short-input', + placeholder: 'e.g., usd, eur, gbp', + condition: { + field: 'operation', + value: ['create_payment_intent', 'create_charge', 'create_price'], + }, + required: true, + }, + // Currency - OPTIONAL for update operations + { + id: 'currency', + title: 'Currency', + type: 'short-input', + placeholder: 'e.g., usd, eur, gbp', + condition: { + field: 'operation', + value: ['update_payment_intent'], + }, + }, + { + id: 'payment_method', + title: 'Payment Method ID', + type: 'short-input', + placeholder: 'e.g., pm_1234567890', + condition: { + field: 'operation', + value: ['create_payment_intent', 'confirm_payment_intent', 'create_customer'], + }, + }, + // Customer specific fields - REQUIRED for create_subscription and create_invoice + { + id: 'customer', + title: 'Customer ID', + type: 'short-input', + placeholder: 'e.g., cus_1234567890', + condition: { + field: 'operation', + value: ['create_subscription', 'create_invoice'], + }, + required: true, + }, + // Customer specific fields - OPTIONAL for other operations + { + id: 'customer', + title: 'Customer ID', + type: 'short-input', + placeholder: 'e.g., cus_1234567890', + condition: { + field: 'operation', + value: ['create_payment_intent', 'update_payment_intent', 'create_charge', 'list_charges'], + }, + }, + { + id: 'email', + title: 'Email', + type: 'short-input', + placeholder: 'customer@example.com', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer'], + }, + }, + // Name - REQUIRED for create_product + { + id: 'name', + title: 'Name', + type: 'short-input', + placeholder: 'Product Name', + condition: { + field: 'operation', + value: ['create_product'], + }, + required: true, + }, + // Name - OPTIONAL for customers and update_product + { + id: 'name', + title: 'Name', + type: 'short-input', + placeholder: 'Customer or Product Name', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer', 'update_product'], + }, + }, + { + id: 'phone', + title: 'Phone', + type: 'short-input', + placeholder: '+1234567890', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer'], + }, + }, + { + id: 'address', + title: 'Address (JSON)', + type: 'code', + placeholder: '{"line1": "123 Main St", "city": "New York", "country": "US"}', + condition: { + field: 'operation', + value: ['create_customer', 'update_customer'], + }, + }, + // Subscription specific fields - REQUIRED for create_subscription + { + id: 'items', + title: 'Items (JSON Array)', + type: 'code', + placeholder: '[{"price": "price_1234567890", "quantity": 1}]', + condition: { + field: 'operation', + value: ['create_subscription'], + }, + required: true, + }, + // Items - OPTIONAL for update_subscription + { + id: 'items', + title: 'Items (JSON Array)', + type: 'code', + placeholder: '[{"price": "price_1234567890", "quantity": 1}]', + condition: { + field: 'operation', + value: ['update_subscription'], + }, + }, + { + id: 'trial_period_days', + title: 'Trial Period (days)', + type: 'short-input', + placeholder: 'e.g., 14', + condition: { + field: 'operation', + value: 'create_subscription', + }, + }, + { + id: 'default_payment_method', + title: 'Default Payment Method', + type: 'short-input', + placeholder: 'e.g., pm_1234567890', + condition: { + field: 'operation', + value: 'create_subscription', + }, + }, + { + id: 'cancel_at_period_end', + title: 'Cancel at Period End', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: ['create_subscription', 'update_subscription'], + }, + }, + // Invoice specific fields + { + id: 'collection_method', + title: 'Collection Method', + type: 'dropdown', + options: [ + { label: 'Charge Automatically', id: 'charge_automatically' }, + { label: 'Send Invoice', id: 'send_invoice' }, + ], + condition: { + field: 'operation', + value: 'create_invoice', + }, + }, + { + id: 'auto_advance', + title: 'Auto Advance', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: ['create_invoice', 'update_invoice', 'finalize_invoice'], + }, + }, + // Charge specific fields + { + id: 'source', + title: 'Payment Source', + type: 'short-input', + placeholder: 'e.g., tok_visa, card ID', + condition: { + field: 'operation', + value: 'create_charge', + }, + }, + { + id: 'capture', + title: 'Capture Immediately', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: 'create_charge', + }, + }, + // Product specific fields + { + id: 'active', + title: 'Active', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: ['create_product', 'update_product', 'update_price'], + }, + }, + { + id: 'images', + title: 'Images (JSON Array)', + type: 'code', + placeholder: '["https://example.com/image1.jpg", "https://example.com/image2.jpg"]', + condition: { + field: 'operation', + value: ['create_product', 'update_product'], + }, + }, + // Price specific fields + { + id: 'product', + title: 'Product ID', + type: 'short-input', + placeholder: 'e.g., prod_1234567890', + condition: { + field: 'operation', + value: 'create_price', + }, + required: true, + }, + { + id: 'unit_amount', + title: 'Unit Amount (in cents)', + type: 'short-input', + placeholder: 'e.g., 1000 for $10.00', + condition: { + field: 'operation', + value: 'create_price', + }, + }, + { + id: 'recurring', + title: 'Recurring (JSON)', + type: 'code', + placeholder: '{"interval": "month", "interval_count": 1}', + condition: { + field: 'operation', + value: 'create_price', + }, + }, + // Common description field + { + id: 'description', + title: 'Description', + type: 'long-input', + placeholder: 'Enter description', + condition: { + field: 'operation', + value: [ + 'create_payment_intent', + 'update_payment_intent', + 'create_customer', + 'update_customer', + 'create_invoice', + 'update_invoice', + 'create_charge', + 'update_charge', + 'create_product', + 'update_product', + ], + }, + }, + // Common metadata field + { + id: 'metadata', + title: 'Metadata (JSON)', + type: 'code', + placeholder: '{"key1": "value1", "key2": "value2"}', + condition: { + field: 'operation', + value: [ + 'create_payment_intent', + 'update_payment_intent', + 'create_customer', + 'update_customer', + 'create_subscription', + 'update_subscription', + 'create_invoice', + 'update_invoice', + 'create_charge', + 'update_charge', + 'create_product', + 'update_product', + 'create_price', + 'update_price', + ], + }, + }, + // List/Search common fields + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max results (default: 10)', + condition: { + field: 'operation', + value: [ + 'list_payment_intents', + 'list_customers', + 'list_subscriptions', + 'list_invoices', + 'list_charges', + 'list_products', + 'list_prices', + 'list_events', + 'search_payment_intents', + 'search_customers', + 'search_subscriptions', + 'search_invoices', + 'search_charges', + 'search_products', + 'search_prices', + ], + }, + }, + { + id: 'query', + title: 'Search Query', + type: 'long-input', + placeholder: 'Enter Stripe search query', + condition: { + field: 'operation', + value: [ + 'search_payment_intents', + 'search_customers', + 'search_subscriptions', + 'search_invoices', + 'search_charges', + 'search_products', + 'search_prices', + ], + }, + required: true, + }, + // Additional filters for specific list operations + { + id: 'status', + title: 'Status', + type: 'short-input', + placeholder: 'e.g., succeeded, pending', + condition: { + field: 'operation', + value: ['list_subscriptions', 'list_invoices'], + }, + }, + { + id: 'receipt_email', + title: 'Receipt Email', + type: 'short-input', + placeholder: 'customer@example.com', + condition: { + field: 'operation', + value: 'create_payment_intent', + }, + }, + { + id: 'cancellation_reason', + title: 'Cancellation Reason', + type: 'short-input', + placeholder: 'e.g., requested_by_customer', + condition: { + field: 'operation', + value: 'cancel_payment_intent', + }, + }, + { + id: 'amount_to_capture', + title: 'Amount to Capture (in cents)', + type: 'short-input', + placeholder: 'Leave empty to capture full amount', + condition: { + field: 'operation', + value: 'capture_payment_intent', + }, + }, + { + id: 'prorate', + title: 'Prorate', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: 'cancel_subscription', + }, + }, + { + id: 'invoice_now', + title: 'Invoice Now', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: 'cancel_subscription', + }, + }, + { + id: 'paid_out_of_band', + title: 'Paid Out of Band', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + condition: { + field: 'operation', + value: 'pay_invoice', + }, + }, + { + id: 'type', + title: 'Event Type', + type: 'short-input', + placeholder: 'e.g., payment_intent.succeeded', + condition: { + field: 'operation', + value: 'list_events', + }, + }, + ...getTrigger('stripe_webhook').subBlocks, + ], + tools: { + access: [ + // Payment Intents + 'stripe_create_payment_intent', + 'stripe_retrieve_payment_intent', + 'stripe_update_payment_intent', + 'stripe_confirm_payment_intent', + 'stripe_capture_payment_intent', + 'stripe_cancel_payment_intent', + 'stripe_list_payment_intents', + 'stripe_search_payment_intents', + // Customers + 'stripe_create_customer', + 'stripe_retrieve_customer', + 'stripe_update_customer', + 'stripe_delete_customer', + 'stripe_list_customers', + 'stripe_search_customers', + // Subscriptions + 'stripe_create_subscription', + 'stripe_retrieve_subscription', + 'stripe_update_subscription', + 'stripe_cancel_subscription', + 'stripe_resume_subscription', + 'stripe_list_subscriptions', + 'stripe_search_subscriptions', + // Invoices + 'stripe_create_invoice', + 'stripe_retrieve_invoice', + 'stripe_update_invoice', + 'stripe_delete_invoice', + 'stripe_finalize_invoice', + 'stripe_pay_invoice', + 'stripe_void_invoice', + 'stripe_send_invoice', + 'stripe_list_invoices', + 'stripe_search_invoices', + // Charges + 'stripe_create_charge', + 'stripe_retrieve_charge', + 'stripe_update_charge', + 'stripe_capture_charge', + 'stripe_list_charges', + 'stripe_search_charges', + // Products + 'stripe_create_product', + 'stripe_retrieve_product', + 'stripe_update_product', + 'stripe_delete_product', + 'stripe_list_products', + 'stripe_search_products', + // Prices + 'stripe_create_price', + 'stripe_retrieve_price', + 'stripe_update_price', + 'stripe_list_prices', + 'stripe_search_prices', + // Events + 'stripe_retrieve_event', + 'stripe_list_events', + ], + config: { + tool: (params) => { + return `stripe_${params.operation}` + }, + params: (params) => { + const { + operation, + apiKey, + address, + metadata, + items, + images, + recurring, + cancel_at_period_end, + auto_advance, + capture, + active, + prorate, + invoice_now, + paid_out_of_band, + ...rest + } = params + + // Parse JSON fields + let parsedAddress: any | undefined + let parsedMetadata: any | undefined + let parsedItems: any | undefined + let parsedImages: any | undefined + let parsedRecurring: any | undefined + + try { + if (address) parsedAddress = JSON.parse(address) + if (metadata) parsedMetadata = JSON.parse(metadata) + if (items) parsedItems = JSON.parse(items) + if (images) parsedImages = JSON.parse(images) + if (recurring) parsedRecurring = JSON.parse(recurring) + } catch (error: any) { + throw new Error(`Invalid JSON input: ${error.message}`) + } + + // Convert string booleans to actual booleans + const parsedBooleans: Record = {} + if (cancel_at_period_end !== undefined) + parsedBooleans.cancel_at_period_end = cancel_at_period_end === 'true' + if (auto_advance !== undefined) parsedBooleans.auto_advance = auto_advance === 'true' + if (capture !== undefined) parsedBooleans.capture = capture === 'true' + if (active !== undefined) parsedBooleans.active = active === 'true' + if (prorate !== undefined) parsedBooleans.prorate = prorate === 'true' + if (invoice_now !== undefined) parsedBooleans.invoice_now = invoice_now === 'true' + if (paid_out_of_band !== undefined) + parsedBooleans.paid_out_of_band = paid_out_of_band === 'true' + + return { + apiKey, + ...rest, + ...(parsedAddress && { address: parsedAddress }), + ...(parsedMetadata && { metadata: parsedMetadata }), + ...(parsedItems && { items: parsedItems }), + ...(parsedImages && { images: parsedImages }), + ...(parsedRecurring && { recurring: parsedRecurring }), + ...parsedBooleans, + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Stripe secret API key' }, + // Common inputs + id: { type: 'string', description: 'Resource ID' }, + amount: { type: 'number', description: 'Amount in cents' }, + currency: { type: 'string', description: 'Three-letter ISO currency code' }, + description: { type: 'string', description: 'Description of the resource' }, + metadata: { type: 'json', description: 'Set of key-value pairs' }, + // Customer inputs + customer: { type: 'string', description: 'Customer ID' }, + email: { type: 'string', description: 'Customer email address' }, + name: { type: 'string', description: 'Customer or product name' }, + phone: { type: 'string', description: 'Customer phone number' }, + address: { type: 'json', description: 'Customer address object' }, + // Payment inputs + payment_method: { type: 'string', description: 'Payment method ID' }, + source: { type: 'string', description: 'Payment source' }, + receipt_email: { type: 'string', description: 'Email for receipt' }, + // Subscription inputs + items: { type: 'json', description: 'Subscription items array' }, + trial_period_days: { type: 'number', description: 'Trial period in days' }, + cancel_at_period_end: { type: 'boolean', description: 'Cancel at period end' }, + prorate: { type: 'boolean', description: 'Prorate cancellation' }, + invoice_now: { type: 'boolean', description: 'Invoice immediately' }, + // Invoice inputs + collection_method: { type: 'string', description: 'Collection method' }, + auto_advance: { type: 'boolean', description: 'Auto-finalize invoice' }, + paid_out_of_band: { type: 'boolean', description: 'Paid outside Stripe' }, + // Charge inputs + capture: { type: 'boolean', description: 'Capture immediately' }, + amount_to_capture: { type: 'number', description: 'Amount to capture in cents' }, + cancellation_reason: { type: 'string', description: 'Cancellation reason' }, + // Product inputs + active: { type: 'boolean', description: 'Whether resource is active' }, + images: { type: 'json', description: 'Product images array' }, + // Price inputs + product: { type: 'string', description: 'Product ID' }, + unit_amount: { type: 'number', description: 'Unit amount in cents' }, + recurring: { type: 'json', description: 'Recurring billing configuration' }, + // List/Search inputs + limit: { type: 'number', description: 'Maximum results to return' }, + query: { type: 'string', description: 'Search query' }, + status: { type: 'string', description: 'Status filter' }, + type: { type: 'string', description: 'Event type filter' }, + }, + outputs: { + // Payment Intent outputs + payment_intent: { type: 'json', description: 'Payment intent object' }, + payment_intents: { type: 'json', description: 'Array of payment intents' }, + // Customer outputs + customer: { type: 'json', description: 'Customer object' }, + customers: { type: 'json', description: 'Array of customers' }, + // Subscription outputs + subscription: { type: 'json', description: 'Subscription object' }, + subscriptions: { type: 'json', description: 'Array of subscriptions' }, + // Invoice outputs + invoice: { type: 'json', description: 'Invoice object' }, + invoices: { type: 'json', description: 'Array of invoices' }, + // Charge outputs + charge: { type: 'json', description: 'Charge object' }, + charges: { type: 'json', description: 'Array of charges' }, + // Product outputs + product: { type: 'json', description: 'Product object' }, + products: { type: 'json', description: 'Array of products' }, + // Price outputs + price: { type: 'json', description: 'Price object' }, + prices: { type: 'json', description: 'Array of prices' }, + // Event outputs + event: { type: 'json', description: 'Event object' }, + events: { type: 'json', description: 'Array of events' }, + // Common outputs + metadata: { type: 'json', description: 'Operation metadata' }, + deleted: { type: 'boolean', description: 'Whether resource was deleted' }, + }, + triggers: { + enabled: true, + available: ['stripe_webhook'], + }, +} diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index b9bb04b1fb..1a3a75d5fe 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -11,7 +11,7 @@ export const SupabaseBlock: BlockConfig = { description: 'Use Supabase database', authMode: AuthMode.ApiKey, longDescription: - 'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.', + 'Integrate Supabase into the workflow. Supports database operations (query, insert, update, delete, upsert), full-text search, RPC functions, row counting, vector search, and complete storage management (upload, download, list, move, copy, delete files and buckets).', docsLink: 'https://docs.sim.ai/tools/supabase', category: 'tools', bgColor: '#1C1C1C', @@ -23,13 +23,31 @@ export const SupabaseBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ + // Database Operations { label: 'Get Many Rows', id: 'query' }, { label: 'Get a Row', id: 'get_row' }, { label: 'Create a Row', id: 'insert' }, { label: 'Update a Row', id: 'update' }, { label: 'Delete a Row', id: 'delete' }, { label: 'Upsert a Row', id: 'upsert' }, + { label: 'Count Rows', id: 'count' }, + // Advanced Database Operations + { label: 'Full-Text Search', id: 'text_search' }, { label: 'Vector Search', id: 'vector_search' }, + { label: 'Call RPC Function', id: 'rpc' }, + // Storage - File Operations + { label: 'Storage: Upload File', id: 'storage_upload' }, + { label: 'Storage: Download File', id: 'storage_download' }, + { label: 'Storage: List Files', id: 'storage_list' }, + { label: 'Storage: Delete Files', id: 'storage_delete' }, + { label: 'Storage: Move File', id: 'storage_move' }, + { label: 'Storage: Copy File', id: 'storage_copy' }, + { label: 'Storage: Get Public URL', id: 'storage_get_public_url' }, + { label: 'Storage: Create Signed URL', id: 'storage_create_signed_url' }, + // Storage - Bucket Operations + { label: 'Storage: Create Bucket', id: 'storage_create_bucket' }, + { label: 'Storage: List Buckets', id: 'storage_list_buckets' }, + { label: 'Storage: Delete Bucket', id: 'storage_delete_bucket' }, ], value: () => 'query', }, @@ -49,6 +67,10 @@ export const SupabaseBlock: BlockConfig = { layout: 'full', placeholder: 'Name of the table', required: true, + condition: { + field: 'operation', + value: ['query', 'get_row', 'insert', 'update', 'delete', 'upsert', 'count', 'text_search'], + }, }, { id: 'apiKey', @@ -417,6 +439,355 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e placeholder: '10', condition: { field: 'operation', value: 'vector_search' }, }, + // RPC operation fields + { + id: 'functionName', + title: 'Function Name', + type: 'short-input', + layout: 'full', + placeholder: 'my_function_name', + condition: { field: 'operation', value: 'rpc' }, + required: true, + }, + { + id: 'params', + title: 'Parameters (JSON)', + type: 'code', + layout: 'full', + placeholder: '{\n "param1": "value1",\n "param2": "value2"\n}', + condition: { field: 'operation', value: 'rpc' }, + }, + // Text Search operation fields + { + id: 'column', + title: 'Column to Search', + type: 'short-input', + layout: 'full', + placeholder: 'content', + condition: { field: 'operation', value: 'text_search' }, + required: true, + }, + { + id: 'query', + title: 'Search Query', + type: 'short-input', + layout: 'full', + placeholder: 'search terms', + condition: { field: 'operation', value: 'text_search' }, + required: true, + }, + { + id: 'searchType', + title: 'Search Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Websearch (natural language)', id: 'websearch' }, + { label: 'Plain', id: 'plain' }, + { label: 'Phrase', id: 'phrase' }, + ], + value: () => 'websearch', + condition: { field: 'operation', value: 'text_search' }, + }, + { + id: 'language', + title: 'Language', + type: 'short-input', + layout: 'full', + placeholder: 'english', + condition: { field: 'operation', value: 'text_search' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + layout: 'full', + placeholder: '100', + condition: { field: 'operation', value: 'text_search' }, + }, + // Count operation fields + { + id: 'filter', + title: 'Filter (PostgREST syntax)', + type: 'short-input', + layout: 'full', + placeholder: 'status=eq.active', + condition: { field: 'operation', value: 'count' }, + }, + { + id: 'countType', + title: 'Count Type', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Exact', id: 'exact' }, + { label: 'Planned', id: 'planned' }, + { label: 'Estimated', id: 'estimated' }, + ], + value: () => 'exact', + condition: { field: 'operation', value: 'count' }, + }, + // Storage bucket field (for all storage operations except list_buckets) + { + id: 'bucket', + title: 'Bucket Name', + type: 'short-input', + layout: 'full', + placeholder: 'my-bucket', + condition: { + field: 'operation', + value: [ + 'storage_upload', + 'storage_download', + 'storage_list', + 'storage_delete', + 'storage_move', + 'storage_copy', + 'storage_create_bucket', + 'storage_delete_bucket', + 'storage_get_public_url', + 'storage_create_signed_url', + ], + }, + required: true, + }, + // Storage Upload fields + { + id: 'path', + title: 'File Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/file.jpg', + condition: { field: 'operation', value: 'storage_upload' }, + required: true, + }, + { + id: 'fileContent', + title: 'File Content', + type: 'code', + layout: 'full', + placeholder: 'Base64 encoded for binary files, or plain text', + condition: { field: 'operation', value: 'storage_upload' }, + required: true, + }, + { + id: 'contentType', + title: 'Content Type (MIME)', + type: 'short-input', + layout: 'full', + placeholder: 'image/jpeg', + condition: { field: 'operation', value: 'storage_upload' }, + }, + { + id: 'upsert', + title: 'Upsert (overwrite if exists)', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'False', id: 'false' }, + { label: 'True', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'storage_upload' }, + }, + // Storage Download fields + { + id: 'path', + title: 'File Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/file.jpg', + condition: { field: 'operation', value: 'storage_download' }, + required: true, + }, + // Storage List fields + { + id: 'path', + title: 'Folder Path (optional)', + type: 'short-input', + layout: 'full', + placeholder: 'folder/', + condition: { field: 'operation', value: 'storage_list' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + layout: 'full', + placeholder: '100', + condition: { field: 'operation', value: 'storage_list' }, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + layout: 'full', + placeholder: '0', + condition: { field: 'operation', value: 'storage_list' }, + }, + { + id: 'sortBy', + title: 'Sort By', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Name', id: 'name' }, + { label: 'Created At', id: 'created_at' }, + { label: 'Updated At', id: 'updated_at' }, + ], + value: () => 'name', + condition: { field: 'operation', value: 'storage_list' }, + }, + { + id: 'sortOrder', + title: 'Sort Order', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + value: () => 'asc', + condition: { field: 'operation', value: 'storage_list' }, + }, + { + id: 'search', + title: 'Search', + type: 'short-input', + layout: 'full', + placeholder: 'search term', + condition: { field: 'operation', value: 'storage_list' }, + }, + // Storage Delete fields + { + id: 'paths', + title: 'File Paths (JSON array)', + type: 'code', + layout: 'full', + placeholder: '["folder/file1.jpg", "folder/file2.jpg"]', + condition: { field: 'operation', value: 'storage_delete' }, + required: true, + }, + // Storage Move fields + { + id: 'fromPath', + title: 'From Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/old.jpg', + condition: { field: 'operation', value: 'storage_move' }, + required: true, + }, + { + id: 'toPath', + title: 'To Path', + type: 'short-input', + layout: 'full', + placeholder: 'newfolder/new.jpg', + condition: { field: 'operation', value: 'storage_move' }, + required: true, + }, + // Storage Copy fields + { + id: 'fromPath', + title: 'From Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/source.jpg', + condition: { field: 'operation', value: 'storage_copy' }, + required: true, + }, + { + id: 'toPath', + title: 'To Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/copy.jpg', + condition: { field: 'operation', value: 'storage_copy' }, + required: true, + }, + // Storage Get Public URL fields + { + id: 'path', + title: 'File Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/file.jpg', + condition: { field: 'operation', value: 'storage_get_public_url' }, + required: true, + }, + { + id: 'download', + title: 'Force Download', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'False', id: 'false' }, + { label: 'True', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'storage_get_public_url' }, + }, + // Storage Create Signed URL fields + { + id: 'path', + title: 'File Path', + type: 'short-input', + layout: 'full', + placeholder: 'folder/file.jpg', + condition: { field: 'operation', value: 'storage_create_signed_url' }, + required: true, + }, + { + id: 'expiresIn', + title: 'Expires In (seconds)', + type: 'short-input', + layout: 'full', + placeholder: '3600', + condition: { field: 'operation', value: 'storage_create_signed_url' }, + required: true, + }, + { + id: 'download', + title: 'Force Download', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'False', id: 'false' }, + { label: 'True', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'storage_create_signed_url' }, + }, + // Storage Create Bucket fields + { + id: 'isPublic', + title: 'Public Bucket', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'False (Private)', id: 'false' }, + { label: 'True (Public)', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'storage_create_bucket' }, + }, + { + id: 'fileSizeLimit', + title: 'File Size Limit (bytes)', + type: 'short-input', + layout: 'full', + placeholder: '52428800', + condition: { field: 'operation', value: 'storage_create_bucket' }, + }, + { + id: 'allowedMimeTypes', + title: 'Allowed MIME Types (JSON array)', + type: 'code', + layout: 'full', + placeholder: '["image/png", "image/jpeg"]', + condition: { field: 'operation', value: 'storage_create_bucket' }, + }, ], tools: { access: [ @@ -426,7 +797,21 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e 'supabase_update', 'supabase_delete', 'supabase_upsert', + 'supabase_count', + 'supabase_text_search', 'supabase_vector_search', + 'supabase_rpc', + 'supabase_storage_upload', + 'supabase_storage_download', + 'supabase_storage_list', + 'supabase_storage_delete', + 'supabase_storage_move', + 'supabase_storage_copy', + 'supabase_storage_create_bucket', + 'supabase_storage_list_buckets', + 'supabase_storage_delete_bucket', + 'supabase_storage_get_public_url', + 'supabase_storage_create_signed_url', ], config: { tool: (params) => { @@ -443,14 +828,53 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e return 'supabase_delete' case 'upsert': return 'supabase_upsert' + case 'count': + return 'supabase_count' + case 'text_search': + return 'supabase_text_search' case 'vector_search': return 'supabase_vector_search' + case 'rpc': + return 'supabase_rpc' + case 'storage_upload': + return 'supabase_storage_upload' + case 'storage_download': + return 'supabase_storage_download' + case 'storage_list': + return 'supabase_storage_list' + case 'storage_delete': + return 'supabase_storage_delete' + case 'storage_move': + return 'supabase_storage_move' + case 'storage_copy': + return 'supabase_storage_copy' + case 'storage_create_bucket': + return 'supabase_storage_create_bucket' + case 'storage_list_buckets': + return 'supabase_storage_list_buckets' + case 'storage_delete_bucket': + return 'supabase_storage_delete_bucket' + case 'storage_get_public_url': + return 'supabase_storage_get_public_url' + case 'storage_create_signed_url': + return 'supabase_storage_create_signed_url' default: throw new Error(`Invalid Supabase operation: ${params.operation}`) } }, params: (params) => { - const { operation, data, filter, queryEmbedding, ...rest } = params + const { + operation, + data, + filter, + queryEmbedding, + params: rpcParams, + paths, + allowedMimeTypes, + upsert, + download, + ...rest + } = params // Parse JSON data if it's a string let parsedData @@ -489,6 +913,56 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e parsedQueryEmbedding = queryEmbedding } + // Handle RPC params + let parsedRpcParams + if (rpcParams && typeof rpcParams === 'string' && rpcParams.trim()) { + try { + parsedRpcParams = JSON.parse(rpcParams) + } catch (parseError) { + const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown JSON error' + throw new Error( + `Invalid RPC params format: ${errorMsg}. Please provide a valid JSON object.` + ) + } + } else if (rpcParams && typeof rpcParams === 'object') { + parsedRpcParams = rpcParams + } + + // Handle paths array for storage delete + let parsedPaths + if (paths && typeof paths === 'string' && paths.trim()) { + try { + parsedPaths = JSON.parse(paths) + } catch (parseError) { + const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown JSON error' + throw new Error( + `Invalid paths format: ${errorMsg}. Please provide a valid JSON array like ["path1", "path2"].` + ) + } + } else if (paths && Array.isArray(paths)) { + parsedPaths = paths + } + + // Handle allowedMimeTypes array + let parsedAllowedMimeTypes + if (allowedMimeTypes && typeof allowedMimeTypes === 'string' && allowedMimeTypes.trim()) { + try { + parsedAllowedMimeTypes = JSON.parse(allowedMimeTypes) + } catch (parseError) { + const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown JSON error' + throw new Error( + `Invalid allowedMimeTypes format: ${errorMsg}. Please provide a valid JSON array.` + ) + } + } else if (allowedMimeTypes && Array.isArray(allowedMimeTypes)) { + parsedAllowedMimeTypes = allowedMimeTypes + } + + // Convert string booleans to actual booleans + const parsedUpsert = upsert === 'true' || upsert === true + const parsedDownload = download === 'true' || download === true + const parsedIsPublic = rest.isPublic === 'true' || rest.isPublic === true + // Build params object, only including defined values const result = { ...rest } @@ -504,6 +978,30 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e result.queryEmbedding = parsedQueryEmbedding } + if (parsedRpcParams !== undefined) { + result.params = parsedRpcParams + } + + if (parsedPaths !== undefined) { + result.paths = parsedPaths + } + + if (parsedAllowedMimeTypes !== undefined) { + result.allowedMimeTypes = parsedAllowedMimeTypes + } + + if (upsert !== undefined) { + result.upsert = parsedUpsert + } + + if (download !== undefined) { + result.download = parsedDownload + } + + if (rest.isPublic !== undefined) { + result.isPublic = parsedIsPublic + } + return result }, }, @@ -520,11 +1018,41 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e // Query operation inputs orderBy: { type: 'string', description: 'Sort column' }, limit: { type: 'number', description: 'Result limit' }, + offset: { type: 'number', description: 'Number of rows to skip' }, // Vector search operation inputs - functionName: { type: 'string', description: 'PostgreSQL function name for vector search' }, + functionName: { + type: 'string', + description: 'PostgreSQL function name for vector search or RPC', + }, queryEmbedding: { type: 'array', description: 'Query vector/embedding for similarity search' }, matchThreshold: { type: 'number', description: 'Minimum similarity threshold (0-1)' }, matchCount: { type: 'number', description: 'Maximum number of similar results to return' }, + // RPC operation inputs + params: { type: 'json', description: 'Parameters to pass to RPC function' }, + // Text search inputs + column: { type: 'string', description: 'Column name to search in' }, + query: { type: 'string', description: 'Search query' }, + searchType: { type: 'string', description: 'Search type: plain, phrase, or websearch' }, + language: { type: 'string', description: 'Language for text search' }, + // Count operation inputs + countType: { type: 'string', description: 'Count type: exact, planned, or estimated' }, + // Storage operation inputs + bucket: { type: 'string', description: 'Storage bucket name' }, + path: { type: 'string', description: 'File path in storage' }, + fileContent: { type: 'string', description: 'File content (base64 for binary)' }, + contentType: { type: 'string', description: 'MIME type of the file' }, + upsert: { type: 'boolean', description: 'Whether to overwrite existing file' }, + download: { type: 'boolean', description: 'Whether to force download' }, + paths: { type: 'array', description: 'Array of file paths' }, + fromPath: { type: 'string', description: 'Source file path for move/copy' }, + toPath: { type: 'string', description: 'Destination file path for move/copy' }, + sortBy: { type: 'string', description: 'Column to sort by' }, + sortOrder: { type: 'string', description: 'Sort order: asc or desc' }, + search: { type: 'string', description: 'Search term for filtering' }, + expiresIn: { type: 'number', description: 'Expiration time in seconds for signed URL' }, + isPublic: { type: 'boolean', description: 'Whether bucket should be public' }, + fileSizeLimit: { type: 'number', description: 'Maximum file size in bytes' }, + allowedMimeTypes: { type: 'array', description: 'Array of allowed MIME types' }, }, outputs: { message: { @@ -533,7 +1061,32 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e }, results: { type: 'json', - description: 'Database records returned from query, insert, update, or delete operations', + description: + 'Database records, storage objects, or operation results depending on the operation type', + }, + count: { + type: 'number', + description: 'Row count for count operations', + }, + fileContent: { + type: 'string', + description: 'Downloaded file content (base64 for binary files)', + }, + contentType: { + type: 'string', + description: 'MIME type of downloaded file', + }, + isBase64: { + type: 'boolean', + description: 'Whether file content is base64 encoded', + }, + publicUrl: { + type: 'string', + description: 'Public URL for storage file', + }, + signedUrl: { + type: 'string', + description: 'Temporary signed URL for storage file', }, }, } diff --git a/apps/sim/blocks/blocks/tavily.ts b/apps/sim/blocks/blocks/tavily.ts index a75013df9f..9bd050bd08 100644 --- a/apps/sim/blocks/blocks/tavily.ts +++ b/apps/sim/blocks/blocks/tavily.ts @@ -23,6 +23,8 @@ export const TavilyBlock: BlockConfig = { options: [ { label: 'Search', id: 'tavily_search' }, { label: 'Extract Content', id: 'tavily_extract' }, + { label: 'Crawl Website', id: 'tavily_crawl' }, + { label: 'Map Website', id: 'tavily_map' }, ], value: () => 'tavily_search', }, @@ -36,13 +38,127 @@ export const TavilyBlock: BlockConfig = { required: true, }, { - id: 'maxResults', + id: 'max_results', title: 'Max Results', type: 'short-input', layout: 'full', placeholder: '5', condition: { field: 'operation', value: 'tavily_search' }, }, + { + id: 'topic', + title: 'Topic', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'General', id: 'general' }, + { label: 'News', id: 'news' }, + { label: 'Finance', id: 'finance' }, + ], + value: () => 'general', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'search_depth', + title: 'Search Depth', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Basic', id: 'basic' }, + { label: 'Advanced', id: 'advanced' }, + ], + value: () => 'basic', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'include_answer', + title: 'Include Answer', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'None', id: 'false' }, + { label: 'Basic', id: 'basic' }, + { label: 'Advanced', id: 'advanced' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'include_raw_content', + title: 'Include Raw Content', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'None', id: 'false' }, + { label: 'Markdown', id: 'markdown' }, + { label: 'Text', id: 'text' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'include_images', + title: 'Include Images', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'include_image_descriptions', + title: 'Include Image Descriptions', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'include_favicon', + title: 'Include Favicon', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'time_range', + title: 'Time Range', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'All Time', id: '' }, + { label: 'Day', id: 'd' }, + { label: 'Week', id: 'w' }, + { label: 'Month', id: 'm' }, + { label: 'Year', id: 'y' }, + ], + value: () => '', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'include_domains', + title: 'Include Domains', + type: 'long-input', + layout: 'full', + placeholder: 'example.com, another.com (comma-separated)', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'exclude_domains', + title: 'Exclude Domains', + type: 'long-input', + layout: 'full', + placeholder: 'example.com, another.com (comma-separated)', + condition: { field: 'operation', value: 'tavily_search' }, + }, + { + id: 'country', + title: 'Country', + type: 'short-input', + layout: 'full', + placeholder: 'US', + condition: { field: 'operation', value: 'tavily_search' }, + }, { id: 'urls', title: 'URL', @@ -58,12 +174,161 @@ export const TavilyBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ - { label: 'basic', id: 'basic' }, - { label: 'advanced', id: 'advanced' }, + { label: 'Basic', id: 'basic' }, + { label: 'Advanced', id: 'advanced' }, ], value: () => 'basic', condition: { field: 'operation', value: 'tavily_extract' }, }, + { + id: 'format', + title: 'Format', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Markdown', id: 'markdown' }, + { label: 'Text', id: 'text' }, + ], + value: () => 'markdown', + condition: { field: 'operation', value: 'tavily_extract' }, + }, + { + id: 'include_images', + title: 'Include Images', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_extract' }, + }, + { + id: 'include_favicon', + title: 'Include Favicon', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_extract' }, + }, + { + id: 'url', + title: 'Website URL', + type: 'short-input', + layout: 'full', + placeholder: 'https://example.com', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + required: true, + }, + { + id: 'instructions', + title: 'Instructions', + type: 'long-input', + layout: 'full', + placeholder: 'Natural language directions for the crawler...', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'max_depth', + title: 'Max Depth', + type: 'short-input', + layout: 'full', + placeholder: '1', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'max_breadth', + title: 'Max Breadth', + type: 'short-input', + layout: 'full', + placeholder: '20', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + layout: 'full', + placeholder: '50', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'select_paths', + title: 'Select Paths', + type: 'long-input', + layout: 'full', + placeholder: '/docs/.*, /api/.* (regex patterns, comma-separated)', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'select_domains', + title: 'Select Domains', + type: 'long-input', + layout: 'full', + placeholder: '^docs\\.example\\.com$ (regex patterns, comma-separated)', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'exclude_paths', + title: 'Exclude Paths', + type: 'long-input', + layout: 'full', + placeholder: '/private/.*, /admin/.* (regex patterns, comma-separated)', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'exclude_domains', + title: 'Exclude Domains', + type: 'long-input', + layout: 'full', + placeholder: '^private\\.example\\.com$ (regex patterns, comma-separated)', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'allow_external', + title: 'Allow External Links', + type: 'switch', + layout: 'full', + value: () => 'true', + condition: { field: 'operation', value: ['tavily_crawl', 'tavily_map'] }, + }, + { + id: 'include_images', + title: 'Include Images', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_crawl' }, + }, + { + id: 'extract_depth', + title: 'Extract Depth', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Basic', id: 'basic' }, + { label: 'Advanced', id: 'advanced' }, + ], + value: () => 'basic', + condition: { field: 'operation', value: 'tavily_crawl' }, + }, + { + id: 'format', + title: 'Format', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Markdown', id: 'markdown' }, + { label: 'Text', id: 'text' }, + ], + value: () => 'markdown', + condition: { field: 'operation', value: 'tavily_crawl' }, + }, + { + id: 'include_favicon', + title: 'Include Favicon', + type: 'switch', + layout: 'full', + value: () => 'false', + condition: { field: 'operation', value: 'tavily_crawl' }, + }, { id: 'apiKey', title: 'API Key', @@ -75,7 +340,7 @@ export const TavilyBlock: BlockConfig = { }, ], tools: { - access: ['tavily_search', 'tavily_extract'], + access: ['tavily_search', 'tavily_extract', 'tavily_crawl', 'tavily_map'], config: { tool: (params) => { switch (params.operation) { @@ -83,6 +348,10 @@ export const TavilyBlock: BlockConfig = { return 'tavily_search' case 'tavily_extract': return 'tavily_extract' + case 'tavily_crawl': + return 'tavily_crawl' + case 'tavily_map': + return 'tavily_map' default: return 'tavily_search' } @@ -92,17 +361,50 @@ export const TavilyBlock: BlockConfig = { inputs: { operation: { type: 'string', description: 'Operation to perform' }, apiKey: { type: 'string', description: 'Tavily API key' }, + // Search params query: { type: 'string', description: 'Search query terms' }, - maxResults: { type: 'number', description: 'Maximum search results' }, + max_results: { type: 'number', description: 'Maximum search results' }, + topic: { type: 'string', description: 'Search topic category' }, + search_depth: { type: 'string', description: 'Search depth level' }, + include_answer: { type: 'string', description: 'Include LLM-generated answer' }, + include_raw_content: { type: 'string', description: 'Include raw content format' }, + include_images: { type: 'boolean', description: 'Include images in results' }, + include_image_descriptions: { type: 'boolean', description: 'Include image descriptions' }, + include_favicon: { type: 'boolean', description: 'Include favicon URLs' }, + time_range: { type: 'string', description: 'Time range filter' }, + include_domains: { type: 'string', description: 'Domains to include' }, + exclude_domains: { type: 'string', description: 'Domains to exclude' }, + country: { type: 'string', description: 'Country filter' }, + // Extract params urls: { type: 'string', description: 'URL to extract' }, extract_depth: { type: 'string', description: 'Extraction depth level' }, + format: { type: 'string', description: 'Output format' }, + // Crawl/Map params + url: { type: 'string', description: 'Root URL for crawl/map' }, + instructions: { type: 'string', description: 'Natural language instructions' }, + max_depth: { type: 'number', description: 'Maximum crawl depth' }, + max_breadth: { type: 'number', description: 'Maximum breadth per level' }, + limit: { type: 'number', description: 'Total links limit' }, + select_paths: { type: 'string', description: 'Path patterns to include' }, + select_domains: { type: 'string', description: 'Domain patterns to include' }, + exclude_paths: { type: 'string', description: 'Path patterns to exclude' }, + allow_external: { type: 'boolean', description: 'Allow external links' }, }, outputs: { - results: { type: 'json', description: 'Search results data' }, - answer: { type: 'string', description: 'Search answer' }, - query: { type: 'string', description: 'Query used' }, + // Search outputs + results: { type: 'json', description: 'Search/extract/crawl results data' }, + answer: { type: 'string', description: 'LLM-generated answer (search)' }, + query: { type: 'string', description: 'Search query used' }, + images: { type: 'array', description: 'Image URLs (search)' }, + auto_parameters: { type: 'json', description: 'Auto-selected parameters (search)' }, + // Extract outputs content: { type: 'string', description: 'Extracted content' }, title: { type: 'string', description: 'Page title' }, url: { type: 'string', description: 'Source URL' }, + failed_results: { type: 'array', description: 'Failed extraction URLs' }, + // Crawl/Map outputs + base_url: { type: 'string', description: 'Base URL that was crawled/mapped' }, + response_time: { type: 'number', description: 'Request duration in seconds' }, + request_id: { type: 'string', description: 'Request identifier for support' }, }, } diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 1cfd7de245..f5554208f8 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -68,6 +68,7 @@ import { StagehandBlock } from '@/blocks/blocks/stagehand' import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent' import { StartTriggerBlock } from '@/blocks/blocks/start_trigger' import { StarterBlock } from '@/blocks/blocks/starter' +import { StripeBlock } from '@/blocks/blocks/stripe' import { SupabaseBlock } from '@/blocks/blocks/supabase' import { TavilyBlock } from '@/blocks/blocks/tavily' import { TelegramBlock } from '@/blocks/blocks/telegram' @@ -165,6 +166,7 @@ export const registry: Record = { chat_trigger: ChatTriggerBlock, manual_trigger: ManualTriggerBlock, api_trigger: ApiTriggerBlock, + stripe: StripeBlock, supabase: SupabaseBlock, tavily: TavilyBlock, telegram: TelegramBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 18e2207f84..f5437e9bfa 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1772,15 +1772,48 @@ export function StripeIcon(props: SVGProps) { return ( + + + + + + ) diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index a698dc7470..a7681f3b45 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -509,14 +509,19 @@ export const auth = betterAuth({ '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', ], responseType: 'code', accessType: 'offline', @@ -773,7 +778,20 @@ export const auth = betterAuth({ authorizationUrl: 'https://auth.atlassian.com/authorize', tokenUrl: 'https://auth.atlassian.com/oauth/token', userInfoUrl: 'https://api.atlassian.com/me', - scopes: ['read:page:confluence', 'write:page:confluence', 'read:me', 'offline_access'], + scopes: [ + 'read:confluence-content.all', + 'read:confluence-space.summary', + 'write:confluence-content', + 'write:confluence-space', + 'write:confluence-file', + 'write:comment:confluence', + 'write:attachment:confluence', + 'write:label:confluence', + 'search:confluence', + 'readonly:content.attachment:confluence', + 'read:me', + 'offline_access', + ], responseType: 'code', pkce: true, accessType: 'offline', @@ -894,6 +912,19 @@ export const auth = betterAuth({ 'read:user:jira', 'read:field-configuration:jira', 'read:issue-details:jira', + 'read:issue-event:jira', + // New scopes for expanded Jira operations + 'delete:issue:jira', + 'write:comment:jira', + 'read:comment:jira', + 'delete:comment:jira', + 'read:attachment:jira', + 'delete:attachment:jira', + 'write:issue-worklog:jira', + 'read:issue-worklog:jira', + 'delete:issue-worklog:jira', + 'write:issue-link:jira', + 'delete:issue-link:jira', ], responseType: 'code', pkce: true, @@ -1044,7 +1075,24 @@ export const auth = betterAuth({ authorizationUrl: 'https://www.reddit.com/api/v1/authorize?duration=permanent', tokenUrl: 'https://www.reddit.com/api/v1/access_token', userInfoUrl: 'https://oauth.reddit.com/api/v1/me', - scopes: ['identity', 'read'], + scopes: [ + 'identity', + 'read', + 'submit', + 'vote', + 'save', + 'edit', + 'subscribe', + 'history', + 'privatemessages', + 'account', + 'mysubreddits', + 'flair', + 'report', + 'modposts', + 'modflair', + 'modmail', + ], responseType: 'code', pkce: false, accessType: 'offline', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 5658509c59..ef044859e0 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -356,7 +356,20 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'confluence', icon: (props) => ConfluenceIcon(props), baseProviderIcon: (props) => ConfluenceIcon(props), - scopes: ['read:page:confluence', 'write:page:confluence', 'read:me', 'offline_access'], + scopes: [ + 'read:confluence-content.all', + 'read:confluence-space.summary', + 'write:confluence-content', + 'write:confluence-space', + 'write:confluence-file', + 'write:comment:confluence', + 'write:attachment:confluence', + 'write:label:confluence', + 'search:confluence', + 'readonly:content.attachment:confluence', + 'read:me', + 'offline_access', + ], }, }, defaultService: 'confluence', diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index 0c0358ae3b..067257f1f4 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -1233,6 +1233,23 @@ export async function formatWebhookInput( } } + if (foundWebhook.provider === 'stripe') { + return { + ...body, + webhook: { + data: { + provider: 'stripe', + path: foundWebhook.path, + providerConfig: foundWebhook.providerConfig, + payload: body, + headers: Object.fromEntries(request.headers.entries()), + method: request.method, + }, + }, + workflowId: foundWorkflow.id, + } + } + // Generic format for other providers return { webhook: { diff --git a/apps/sim/tools/airtable/list_records.ts b/apps/sim/tools/airtable/list_records.ts index c159b0aa8e..cc72352ec9 100644 --- a/apps/sim/tools/airtable/list_records.ts +++ b/apps/sim/tools/airtable/list_records.ts @@ -49,7 +49,7 @@ export const airtableListRecordsTool: ToolConfig { const url = `https://api.airtable.com/v0/${params.baseId}/${params.tableId}` const queryParams = new URLSearchParams() - if (params.maxRecords) queryParams.append('maxRecords', params.maxRecords.toString()) + if (params.maxRecords) queryParams.append('maxRecords', Number(params.maxRecords).toString()) if (params.filterFormula) { // Airtable formulas often contain characters needing encoding, // but standard encodeURIComponent might over-encode. diff --git a/apps/sim/tools/arxiv/get_author_papers.ts b/apps/sim/tools/arxiv/get_author_papers.ts index 6860a5d61c..e3d78fb623 100644 --- a/apps/sim/tools/arxiv/get_author_papers.ts +++ b/apps/sim/tools/arxiv/get_author_papers.ts @@ -34,7 +34,7 @@ export const getAuthorPapersTool: ToolConfig< searchParams.append('search_query', `au:"${params.authorName}"`) searchParams.append( 'max_results', - (params.maxResults ? Math.min(params.maxResults, 2000) : 10).toString() + (params.maxResults ? Math.min(Number(params.maxResults), 2000) : 10).toString() ) searchParams.append('sortBy', 'submittedDate') searchParams.append('sortOrder', 'descending') diff --git a/apps/sim/tools/arxiv/search.ts b/apps/sim/tools/arxiv/search.ts index 4ea1046ebd..a285a39136 100644 --- a/apps/sim/tools/arxiv/search.ts +++ b/apps/sim/tools/arxiv/search.ts @@ -56,7 +56,7 @@ export const searchTool: ToolConfig = { // Add optional parameters if (params.maxResults) { - searchParams.append('max_results', Math.min(params.maxResults, 2000).toString()) + searchParams.append('max_results', Math.min(Number(params.maxResults), 2000).toString()) } else { searchParams.append('max_results', '10') } diff --git a/apps/sim/tools/confluence/add_label.ts b/apps/sim/tools/confluence/add_label.ts new file mode 100644 index 0000000000..2f0a24040d --- /dev/null +++ b/apps/sim/tools/confluence/add_label.ts @@ -0,0 +1,109 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceAddLabelParams { + accessToken: string + domain: string + pageId: string + labelName: string + cloudId?: string +} + +export interface ConfluenceAddLabelResponse { + success: boolean + output: { + ts: string + pageId: string + labelName: string + added: boolean + } +} + +export const confluenceAddLabelTool: ToolConfig< + ConfluenceAddLabelParams, + ConfluenceAddLabelResponse +> = { + id: 'confluence_add_label', + name: 'Confluence Add Label', + description: 'Add a label to a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to add label to', + }, + labelName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label name to add', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/labels', + method: 'POST', + headers: (params: ConfluenceAddLabelParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceAddLabelParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + labelName: params.labelName, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId || '', + labelName: data.labelName || '', + added: true, + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of operation' }, + pageId: { type: 'string', description: 'Page ID' }, + labelName: { type: 'string', description: 'Label name' }, + added: { type: 'boolean', description: 'Addition status' }, + }, +} diff --git a/apps/sim/tools/confluence/create_comment.ts b/apps/sim/tools/confluence/create_comment.ts new file mode 100644 index 0000000000..aa18a5c4a0 --- /dev/null +++ b/apps/sim/tools/confluence/create_comment.ts @@ -0,0 +1,106 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreateCommentParams { + accessToken: string + domain: string + pageId: string + comment: string + cloudId?: string +} + +export interface ConfluenceCreateCommentResponse { + success: boolean + output: { + ts: string + commentId: string + pageId: string + } +} + +export const confluenceCreateCommentTool: ToolConfig< + ConfluenceCreateCommentParams, + ConfluenceCreateCommentResponse +> = { + id: 'confluence_create_comment', + name: 'Confluence Create Comment', + description: 'Add a comment to a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to comment on', + }, + comment: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment text in Confluence storage format', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/comments', + method: 'POST', + headers: (params: ConfluenceCreateCommentParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceCreateCommentParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + comment: params.comment, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + commentId: data.id, + pageId: data.pageId || '', + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of creation' }, + commentId: { type: 'string', description: 'Created comment ID' }, + pageId: { type: 'string', description: 'Page ID' }, + }, +} diff --git a/apps/sim/tools/confluence/create_page.ts b/apps/sim/tools/confluence/create_page.ts new file mode 100644 index 0000000000..69b83dc1e9 --- /dev/null +++ b/apps/sim/tools/confluence/create_page.ts @@ -0,0 +1,125 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreatePageParams { + accessToken: string + domain: string + spaceId: string + title: string + content: string + parentId?: string + cloudId?: string +} + +export interface ConfluenceCreatePageResponse { + success: boolean + output: { + ts: string + pageId: string + title: string + url: string + } +} + +export const confluenceCreatePageTool: ToolConfig< + ConfluenceCreatePageParams, + ConfluenceCreatePageResponse +> = { + id: 'confluence_create_page', + name: 'Confluence Create Page', + description: 'Create a new page in a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence space ID where the page will be created', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Title of the new page', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Page content in Confluence storage format (HTML)', + }, + parentId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Parent page ID if creating a child page', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/create-page', + method: 'POST', + headers: (params: ConfluenceCreatePageParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceCreatePageParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + title: params.title, + content: params.content, + parentId: params.parentId, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.id, + title: data.title, + url: data.url || data._links?.webui || '', + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of creation' }, + pageId: { type: 'string', description: 'Created page ID' }, + title: { type: 'string', description: 'Page title' }, + url: { type: 'string', description: 'Page URL' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_attachment.ts b/apps/sim/tools/confluence/delete_attachment.ts new file mode 100644 index 0000000000..37d2d093d8 --- /dev/null +++ b/apps/sim/tools/confluence/delete_attachment.ts @@ -0,0 +1,97 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteAttachmentParams { + accessToken: string + domain: string + attachmentId: string + cloudId?: string +} + +export interface ConfluenceDeleteAttachmentResponse { + success: boolean + output: { + ts: string + attachmentId: string + deleted: boolean + } +} + +export const confluenceDeleteAttachmentTool: ToolConfig< + ConfluenceDeleteAttachmentParams, + ConfluenceDeleteAttachmentResponse +> = { + id: 'confluence_delete_attachment', + name: 'Confluence Delete Attachment', + description: 'Delete an attachment from a Confluence page (moves to trash).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + attachmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence attachment ID to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/attachment', + method: 'DELETE', + headers: (params: ConfluenceDeleteAttachmentParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceDeleteAttachmentParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + attachmentId: params.attachmentId, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + attachmentId: data.attachmentId || '', + deleted: true, + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of deletion' }, + attachmentId: { type: 'string', description: 'Deleted attachment ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_comment.ts b/apps/sim/tools/confluence/delete_comment.ts new file mode 100644 index 0000000000..6564181dfe --- /dev/null +++ b/apps/sim/tools/confluence/delete_comment.ts @@ -0,0 +1,97 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteCommentParams { + accessToken: string + domain: string + commentId: string + cloudId?: string +} + +export interface ConfluenceDeleteCommentResponse { + success: boolean + output: { + ts: string + commentId: string + deleted: boolean + } +} + +export const confluenceDeleteCommentTool: ToolConfig< + ConfluenceDeleteCommentParams, + ConfluenceDeleteCommentResponse +> = { + id: 'confluence_delete_comment', + name: 'Confluence Delete Comment', + description: 'Delete a comment from a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence comment ID to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/comment', + method: 'DELETE', + headers: (params: ConfluenceDeleteCommentParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceDeleteCommentParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + commentId: params.commentId, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + commentId: data.commentId || '', + deleted: true, + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of deletion' }, + commentId: { type: 'string', description: 'Deleted comment ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_page.ts b/apps/sim/tools/confluence/delete_page.ts new file mode 100644 index 0000000000..a9b35c33f1 --- /dev/null +++ b/apps/sim/tools/confluence/delete_page.ts @@ -0,0 +1,97 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeletePageParams { + accessToken: string + domain: string + pageId: string + cloudId?: string +} + +export interface ConfluenceDeletePageResponse { + success: boolean + output: { + ts: string + pageId: string + deleted: boolean + } +} + +export const confluenceDeletePageTool: ToolConfig< + ConfluenceDeletePageParams, + ConfluenceDeletePageResponse +> = { + id: 'confluence_delete_page', + name: 'Confluence Delete Page', + description: 'Delete a Confluence page (moves it to trash where it can be restored).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: ConfluenceDeletePageParams) => '/api/tools/confluence/page', + method: 'DELETE', + headers: (params: ConfluenceDeletePageParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceDeletePageParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + pageId: params.pageId, + cloudId: params.cloudId, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId || '', + deleted: true, + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of deletion' }, + pageId: { type: 'string', description: 'Deleted page ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/get_space.ts b/apps/sim/tools/confluence/get_space.ts new file mode 100644 index 0000000000..25057cd818 --- /dev/null +++ b/apps/sim/tools/confluence/get_space.ts @@ -0,0 +1,109 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetSpaceParams { + accessToken: string + domain: string + spaceId: string + cloudId?: string +} + +export interface ConfluenceGetSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + name: string + key: string + type: string + status: string + url: string + } +} + +export const confluenceGetSpaceTool: ToolConfig< + ConfluenceGetSpaceParams, + ConfluenceGetSpaceResponse +> = { + id: 'confluence_get_space', + name: 'Confluence Get Space', + description: 'Get details about a specific Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence space ID to retrieve', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'GET', + headers: (params: ConfluenceGetSpaceParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceGetSpaceParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.id, + name: data.name, + key: data.key, + type: data.type, + status: data.status, + url: data._links?.webui || '', + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of retrieval' }, + spaceId: { type: 'string', description: 'Space ID' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space type' }, + status: { type: 'string', description: 'Space status' }, + url: { type: 'string', description: 'Space URL' }, + }, +} diff --git a/apps/sim/tools/confluence/index.ts b/apps/sim/tools/confluence/index.ts index 97f01e249c..89a30d6c80 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -1,5 +1,51 @@ +// Page operations + +// Label operations +import { confluenceAddLabelTool } from '@/tools/confluence/add_label' +// Comment operations +import { confluenceCreateCommentTool } from '@/tools/confluence/create_comment' +import { confluenceCreatePageTool } from '@/tools/confluence/create_page' +import { confluenceDeleteAttachmentTool } from '@/tools/confluence/delete_attachment' +import { confluenceDeleteCommentTool } from '@/tools/confluence/delete_comment' +import { confluenceDeletePageTool } from '@/tools/confluence/delete_page' +// Space operations +import { confluenceGetSpaceTool } from '@/tools/confluence/get_space' +// Attachment operations +import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments' +import { confluenceListCommentsTool } from '@/tools/confluence/list_comments' +import { confluenceListLabelsTool } from '@/tools/confluence/list_labels' +import { confluenceListSpacesTool } from '@/tools/confluence/list_spaces' +import { confluenceRemoveLabelTool } from '@/tools/confluence/remove_label' import { confluenceRetrieveTool } from '@/tools/confluence/retrieve' +// Search operations +import { confluenceSearchTool } from '@/tools/confluence/search' import { confluenceUpdateTool } from '@/tools/confluence/update' +import { confluenceUpdateCommentTool } from '@/tools/confluence/update_comment' +// Page operations exports export { confluenceRetrieveTool } export { confluenceUpdateTool } +export { confluenceCreatePageTool } +export { confluenceDeletePageTool } + +// Search operations exports +export { confluenceSearchTool } + +// Comment operations exports +export { confluenceCreateCommentTool } +export { confluenceListCommentsTool } +export { confluenceUpdateCommentTool } +export { confluenceDeleteCommentTool } + +// Attachment operations exports +export { confluenceListAttachmentsTool } +export { confluenceDeleteAttachmentTool } + +// Label operations exports +export { confluenceAddLabelTool } +export { confluenceListLabelsTool } +export { confluenceRemoveLabelTool } + +// Space operations exports +export { confluenceGetSpaceTool } +export { confluenceListSpacesTool } diff --git a/apps/sim/tools/confluence/list_attachments.ts b/apps/sim/tools/confluence/list_attachments.ts new file mode 100644 index 0000000000..3dd2a2b0e7 --- /dev/null +++ b/apps/sim/tools/confluence/list_attachments.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListAttachmentsParams { + accessToken: string + domain: string + pageId: string + limit?: number + cloudId?: string +} + +export interface ConfluenceListAttachmentsResponse { + success: boolean + output: { + ts: string + attachments: Array<{ + id: string + title: string + fileSize: number + mediaType: string + downloadUrl: string + }> + } +} + +export const confluenceListAttachmentsTool: ToolConfig< + ConfluenceListAttachmentsParams, + ConfluenceListAttachmentsResponse +> = { + id: 'confluence_list_attachments', + name: 'Confluence List Attachments', + description: 'List all attachments on a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to list attachments from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of attachments to return (default: 25)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/attachments', + method: 'GET', + headers: (params: ConfluenceListAttachmentsParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceListAttachmentsParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + limit: params.limit ? Number(params.limit) : 25, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + attachments: data.attachments || [], + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of retrieval' }, + attachments: { type: 'array', description: 'List of attachments' }, + }, +} diff --git a/apps/sim/tools/confluence/list_comments.ts b/apps/sim/tools/confluence/list_comments.ts new file mode 100644 index 0000000000..e1d3538297 --- /dev/null +++ b/apps/sim/tools/confluence/list_comments.ts @@ -0,0 +1,107 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListCommentsParams { + accessToken: string + domain: string + pageId: string + limit?: number + cloudId?: string +} + +export interface ConfluenceListCommentsResponse { + success: boolean + output: { + ts: string + comments: Array<{ + id: string + body: string + createdAt: string + authorId: string + }> + } +} + +export const confluenceListCommentsTool: ToolConfig< + ConfluenceListCommentsParams, + ConfluenceListCommentsResponse +> = { + id: 'confluence_list_comments', + name: 'Confluence List Comments', + description: 'List all comments on a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to list comments from', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of comments to return (default: 25)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/comments', + method: 'GET', + headers: (params: ConfluenceListCommentsParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceListCommentsParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + limit: params.limit ? Number(params.limit) : 25, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + comments: data.comments || [], + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of retrieval' }, + comments: { type: 'array', description: 'List of comments' }, + }, +} diff --git a/apps/sim/tools/confluence/list_labels.ts b/apps/sim/tools/confluence/list_labels.ts new file mode 100644 index 0000000000..638cdebf27 --- /dev/null +++ b/apps/sim/tools/confluence/list_labels.ts @@ -0,0 +1,98 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListLabelsParams { + accessToken: string + domain: string + pageId: string + cloudId?: string +} + +export interface ConfluenceListLabelsResponse { + success: boolean + output: { + ts: string + labels: Array<{ + id: string + name: string + prefix: string + }> + } +} + +export const confluenceListLabelsTool: ToolConfig< + ConfluenceListLabelsParams, + ConfluenceListLabelsResponse +> = { + id: 'confluence_list_labels', + name: 'Confluence List Labels', + description: 'List all labels on a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to list labels from', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/labels', + method: 'GET', + headers: (params: ConfluenceListLabelsParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceListLabelsParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + labels: data.labels || [], + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of retrieval' }, + labels: { type: 'array', description: 'List of labels' }, + }, +} diff --git a/apps/sim/tools/confluence/list_spaces.ts b/apps/sim/tools/confluence/list_spaces.ts new file mode 100644 index 0000000000..3d4c4844a6 --- /dev/null +++ b/apps/sim/tools/confluence/list_spaces.ts @@ -0,0 +1,100 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListSpacesParams { + accessToken: string + domain: string + limit?: number + cloudId?: string +} + +export interface ConfluenceListSpacesResponse { + success: boolean + output: { + ts: string + spaces: Array<{ + id: string + name: string + key: string + type: string + status: string + }> + } +} + +export const confluenceListSpacesTool: ToolConfig< + ConfluenceListSpacesParams, + ConfluenceListSpacesResponse +> = { + id: 'confluence_list_spaces', + name: 'Confluence List Spaces', + description: 'List all Confluence spaces accessible to the user.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of spaces to return (default: 25)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/spaces', + method: 'GET', + headers: (params: ConfluenceListSpacesParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceListSpacesParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + limit: params.limit ? Number(params.limit) : 25, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaces: data.spaces || [], + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of retrieval' }, + spaces: { type: 'array', description: 'List of spaces' }, + }, +} diff --git a/apps/sim/tools/confluence/remove_label.ts b/apps/sim/tools/confluence/remove_label.ts new file mode 100644 index 0000000000..51f9263c9a --- /dev/null +++ b/apps/sim/tools/confluence/remove_label.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceRemoveLabelParams { + accessToken: string + domain: string + pageId: string + labelName: string + cloudId?: string +} + +export interface ConfluenceRemoveLabelResponse { + success: boolean + output: { + ts: string + pageId: string + labelName: string + removed: boolean + } +} + +export const confluenceRemoveLabelTool: ToolConfig< + ConfluenceRemoveLabelParams, + ConfluenceRemoveLabelResponse +> = { + id: 'confluence_remove_label', + name: 'Confluence Remove Label', + description: 'Remove a label from a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence page ID to remove label from', + }, + labelName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label name to remove', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/label', + method: 'DELETE', + headers: (params: ConfluenceRemoveLabelParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceRemoveLabelParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + labelName: params.labelName, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + pageId: data.pageId || '', + labelName: data.labelName || '', + removed: true, + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of operation' }, + pageId: { type: 'string', description: 'Page ID' }, + labelName: { type: 'string', description: 'Label name' }, + removed: { type: 'boolean', description: 'Removal status' }, + }, +} diff --git a/apps/sim/tools/confluence/search.ts b/apps/sim/tools/confluence/search.ts new file mode 100644 index 0000000000..336b328d01 --- /dev/null +++ b/apps/sim/tools/confluence/search.ts @@ -0,0 +1,106 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceSearchParams { + accessToken: string + domain: string + query: string + limit?: number + cloudId?: string +} + +export interface ConfluenceSearchResponse { + success: boolean + output: { + ts: string + results: Array<{ + id: string + title: string + type: string + url: string + excerpt: string + }> + } +} + +export const confluenceSearchTool: ToolConfig = { + id: 'confluence_search', + name: 'Confluence Search', + description: 'Search for content across Confluence pages, blog posts, and other content.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query string', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (default: 25)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/search', + method: 'POST', + headers: (params: ConfluenceSearchParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceSearchParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + query: params.query, + limit: params.limit ? Number(params.limit) : 25, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + results: data.results || [], + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of search' }, + results: { type: 'array', description: 'Search results' }, + }, +} diff --git a/apps/sim/tools/confluence/types.ts b/apps/sim/tools/confluence/types.ts index 617e31a473..b0078966f9 100644 --- a/apps/sim/tools/confluence/types.ts +++ b/apps/sim/tools/confluence/types.ts @@ -1,5 +1,6 @@ import type { ToolResponse } from '@/tools/types' +// Page operations export interface ConfluenceRetrieveParams { accessToken: string pageId: string @@ -43,4 +44,162 @@ export interface ConfluenceUpdateResponse extends ToolResponse { } } -export type ConfluenceResponse = ConfluenceRetrieveResponse | ConfluenceUpdateResponse +export interface ConfluenceCreatePageParams { + accessToken: string + domain: string + spaceId: string + title: string + content: string + parentId?: string + cloudId?: string +} + +export interface ConfluenceCreatePageResponse extends ToolResponse { + output: { + ts: string + pageId: string + title: string + url: string + } +} + +export interface ConfluenceDeletePageParams { + accessToken: string + domain: string + pageId: string + cloudId?: string +} + +export interface ConfluenceDeletePageResponse extends ToolResponse { + output: { + ts: string + pageId: string + deleted: boolean + } +} + +// Search operations +export interface ConfluenceSearchParams { + accessToken: string + domain: string + query: string + limit?: number + cloudId?: string +} + +export interface ConfluenceSearchResponse extends ToolResponse { + output: { + ts: string + results: Array<{ + id: string + title: string + type: string + url: string + excerpt: string + }> + } +} + +// Comment operations +export interface ConfluenceCommentParams { + accessToken: string + domain: string + pageId: string + comment: string + cloudId?: string +} + +export interface ConfluenceCommentResponse extends ToolResponse { + output: { + ts: string + commentId: string + pageId: string + } +} + +// Attachment operations +export interface ConfluenceAttachmentParams { + accessToken: string + domain: string + pageId?: string + attachmentId?: string + limit?: number + cloudId?: string +} + +export interface ConfluenceAttachmentResponse extends ToolResponse { + output: { + ts: string + attachments?: Array<{ + id: string + title: string + fileSize: number + mediaType: string + downloadUrl: string + }> + attachmentId?: string + deleted?: boolean + } +} + +// Label operations +export interface ConfluenceLabelParams { + accessToken: string + domain: string + pageId: string + labelName?: string + cloudId?: string +} + +export interface ConfluenceLabelResponse extends ToolResponse { + output: { + ts: string + labels?: Array<{ + id: string + name: string + prefix: string + }> + pageId?: string + labelName?: string + added?: boolean + removed?: boolean + } +} + +// Space operations +export interface ConfluenceSpaceParams { + accessToken: string + domain: string + spaceId?: string + limit?: number + cloudId?: string +} + +export interface ConfluenceSpaceResponse extends ToolResponse { + output: { + ts: string + spaces?: Array<{ + id: string + name: string + key: string + type: string + status: string + }> + spaceId?: string + name?: string + key?: string + type?: string + status?: string + } +} + +export type ConfluenceResponse = + | ConfluenceRetrieveResponse + | ConfluenceUpdateResponse + | ConfluenceCreatePageResponse + | ConfluenceDeletePageResponse + | ConfluenceSearchResponse + | ConfluenceCommentResponse + | ConfluenceAttachmentResponse + | ConfluenceLabelResponse + | ConfluenceSpaceResponse diff --git a/apps/sim/tools/confluence/update_comment.ts b/apps/sim/tools/confluence/update_comment.ts new file mode 100644 index 0000000000..897517f8b6 --- /dev/null +++ b/apps/sim/tools/confluence/update_comment.ts @@ -0,0 +1,106 @@ +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateCommentParams { + accessToken: string + domain: string + commentId: string + comment: string + cloudId?: string +} + +export interface ConfluenceUpdateCommentResponse { + success: boolean + output: { + ts: string + commentId: string + updated: boolean + } +} + +export const confluenceUpdateCommentTool: ToolConfig< + ConfluenceUpdateCommentParams, + ConfluenceUpdateCommentResponse +> = { + id: 'confluence_update_comment', + name: 'Confluence Update Comment', + description: 'Update an existing comment on a Confluence page.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Confluence comment ID to update', + }, + comment: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Updated comment text in Confluence storage format', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/comment', + method: 'PUT', + headers: (params: ConfluenceUpdateCommentParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: ConfluenceUpdateCommentParams) => { + return { + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + commentId: params.commentId, + comment: params.comment, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + commentId: data.id || data.commentId, + updated: true, + }, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of update' }, + commentId: { type: 'string', description: 'Updated comment ID' }, + updated: { type: 'boolean', description: 'Update status' }, + }, +} diff --git a/apps/sim/tools/discord/add_reaction.ts b/apps/sim/tools/discord/add_reaction.ts new file mode 100644 index 0000000000..2d3077d781 --- /dev/null +++ b/apps/sim/tools/discord/add_reaction.ts @@ -0,0 +1,69 @@ +import type { DiscordAddReactionParams, DiscordAddReactionResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordAddReactionTool: ToolConfig< + DiscordAddReactionParams, + DiscordAddReactionResponse +> = { + id: 'discord_add_reaction', + name: 'Discord Add Reaction', + description: 'Add a reaction emoji to a Discord message', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to react to', + }, + emoji: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The emoji to react with (unicode emoji or custom emoji in name:id format)', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordAddReactionParams) => { + const encodedEmoji = encodeURIComponent(params.emoji) + return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}/reactions/${encodedEmoji}/@me` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Reaction added successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/archive_thread.ts b/apps/sim/tools/discord/archive_thread.ts new file mode 100644 index 0000000000..0fc48679eb --- /dev/null +++ b/apps/sim/tools/discord/archive_thread.ts @@ -0,0 +1,81 @@ +import type { + DiscordArchiveThreadParams, + DiscordArchiveThreadResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordArchiveThreadTool: ToolConfig< + DiscordArchiveThreadParams, + DiscordArchiveThreadResponse +> = { + id: 'discord_archive_thread', + name: 'Discord Archive Thread', + description: 'Archive or unarchive a thread in Discord', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The thread ID to archive/unarchive', + }, + archived: { + type: 'boolean', + required: true, + visibility: 'user-or-llm', + description: 'Whether to archive (true) or unarchive (false) the thread', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordArchiveThreadParams) => { + return `https://discord.com/api/v10/channels/${params.threadId}` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordArchiveThreadParams) => { + return { + archived: params.archived, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: data.archived ? 'Thread archived successfully' : 'Thread unarchived successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Updated thread data', + properties: { + id: { type: 'string', description: 'Thread ID' }, + archived: { type: 'boolean', description: 'Whether thread is archived' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/assign_role.ts b/apps/sim/tools/discord/assign_role.ts new file mode 100644 index 0000000000..a79169ed63 --- /dev/null +++ b/apps/sim/tools/discord/assign_role.ts @@ -0,0 +1,60 @@ +import type { DiscordAssignRoleParams, DiscordAssignRoleResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordAssignRoleTool: ToolConfig = + { + id: 'discord_assign_role', + name: 'Discord Assign Role', + description: 'Assign a role to a member in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to assign the role to', + }, + roleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The role ID to assign', + }, + }, + + request: { + url: (params: DiscordAssignRoleParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}/roles/${params.roleId}` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Role assigned successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, + } diff --git a/apps/sim/tools/discord/ban_member.ts b/apps/sim/tools/discord/ban_member.ts new file mode 100644 index 0000000000..038275d508 --- /dev/null +++ b/apps/sim/tools/discord/ban_member.ts @@ -0,0 +1,79 @@ +import type { DiscordBanMemberParams, DiscordBanMemberResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordBanMemberTool: ToolConfig = { + id: 'discord_ban_member', + name: 'Discord Ban Member', + description: 'Ban a member from a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to ban', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for banning the member', + }, + deleteMessageDays: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of days to delete messages for (0-7)', + }, + }, + + request: { + url: (params: DiscordBanMemberParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/bans/${params.userId}` + }, + method: 'PUT', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + } + if (params.reason) { + headers['X-Audit-Log-Reason'] = encodeURIComponent(params.reason) + } + return headers + }, + body: (params: DiscordBanMemberParams) => { + const body: any = {} + if (params.deleteMessageDays !== undefined) { + body.delete_message_days = Number(params.deleteMessageDays) + } + return body + }, + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Member banned successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/create_channel.ts b/apps/sim/tools/discord/create_channel.ts new file mode 100644 index 0000000000..943275e482 --- /dev/null +++ b/apps/sim/tools/discord/create_channel.ts @@ -0,0 +1,99 @@ +import type { + DiscordCreateChannelParams, + DiscordCreateChannelResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordCreateChannelTool: ToolConfig< + DiscordCreateChannelParams, + DiscordCreateChannelResponse +> = { + id: 'discord_create_channel', + name: 'Discord Create Channel', + description: 'Create a new channel in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the channel (1-100 characters)', + }, + type: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Channel type (0=text, 2=voice, 4=category, 5=announcement, 13=stage)', + }, + topic: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Channel topic (0-1024 characters)', + }, + parentId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Parent category ID for the channel', + }, + }, + + request: { + url: (params: DiscordCreateChannelParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/channels` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordCreateChannelParams) => { + const body: any = { + name: params.name, + } + if (params.type !== undefined) body.type = Number(params.type) + if (params.topic) body.topic = params.topic + if (params.parentId) body.parent_id = params.parentId + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Channel created successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Created channel data', + properties: { + id: { type: 'string', description: 'Channel ID' }, + name: { type: 'string', description: 'Channel name' }, + type: { type: 'number', description: 'Channel type' }, + guild_id: { type: 'string', description: 'Server ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/create_invite.ts b/apps/sim/tools/discord/create_invite.ts new file mode 100644 index 0000000000..0febb8bb52 --- /dev/null +++ b/apps/sim/tools/discord/create_invite.ts @@ -0,0 +1,95 @@ +import type { DiscordCreateInviteParams, DiscordCreateInviteResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordCreateInviteTool: ToolConfig< + DiscordCreateInviteParams, + DiscordCreateInviteResponse +> = { + id: 'discord_create_invite', + name: 'Discord Create Invite', + description: 'Create an invite link for a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID to create an invite for', + }, + maxAge: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Duration of invite in seconds (0 = never expires, default 86400)', + }, + maxUses: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Max number of uses (0 = unlimited, default 0)', + }, + temporary: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether invite grants temporary membership', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordCreateInviteParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}/invites` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordCreateInviteParams) => { + const body: any = {} + if (params.maxAge !== undefined) body.max_age = Number(params.maxAge) + if (params.maxUses !== undefined) body.max_uses = Number(params.maxUses) + if (params.temporary !== undefined) body.temporary = params.temporary + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Invite created successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Created invite data', + properties: { + code: { type: 'string', description: 'Invite code' }, + url: { type: 'string', description: 'Full invite URL' }, + max_age: { type: 'number', description: 'Max age in seconds' }, + max_uses: { type: 'number', description: 'Max uses' }, + temporary: { type: 'boolean', description: 'Whether temporary' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/create_role.ts b/apps/sim/tools/discord/create_role.ts new file mode 100644 index 0000000000..7d310b8573 --- /dev/null +++ b/apps/sim/tools/discord/create_role.ts @@ -0,0 +1,95 @@ +import type { DiscordCreateRoleParams, DiscordCreateRoleResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordCreateRoleTool: ToolConfig = + { + id: 'discord_create_role', + name: 'Discord Create Role', + description: 'Create a new role in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the role', + }, + color: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'RGB color value as integer (e.g., 0xFF0000 for red)', + }, + hoist: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to display role members separately from online members', + }, + mentionable: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the role can be mentioned', + }, + }, + + request: { + url: (params: DiscordCreateRoleParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/roles` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordCreateRoleParams) => { + const body: any = { + name: params.name, + } + if (params.color !== undefined) body.color = Number(params.color) + if (params.hoist !== undefined) body.hoist = params.hoist + if (params.mentionable !== undefined) body.mentionable = params.mentionable + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Role created successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Created role data', + properties: { + id: { type: 'string', description: 'Role ID' }, + name: { type: 'string', description: 'Role name' }, + color: { type: 'number', description: 'Role color' }, + hoist: { type: 'boolean', description: 'Whether role is hoisted' }, + mentionable: { type: 'boolean', description: 'Whether role is mentionable' }, + }, + }, + }, + } diff --git a/apps/sim/tools/discord/create_thread.ts b/apps/sim/tools/discord/create_thread.ts new file mode 100644 index 0000000000..972a88f28d --- /dev/null +++ b/apps/sim/tools/discord/create_thread.ts @@ -0,0 +1,100 @@ +import type { DiscordCreateThreadParams, DiscordCreateThreadResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordCreateThreadTool: ToolConfig< + DiscordCreateThreadParams, + DiscordCreateThreadResponse +> = { + id: 'discord_create_thread', + name: 'Discord Create Thread', + description: 'Create a thread in a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID to create the thread in', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the thread (1-100 characters)', + }, + messageId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The message ID to create a thread from (if creating from existing message)', + }, + autoArchiveDuration: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Duration in minutes to auto-archive the thread (60, 1440, 4320, 10080)', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordCreateThreadParams) => { + if (params.messageId) { + return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}/threads` + } + return `https://discord.com/api/v10/channels/${params.channelId}/threads` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordCreateThreadParams) => { + const body: any = { + name: params.name, + } + if (params.autoArchiveDuration) { + body.auto_archive_duration = Number(params.autoArchiveDuration) + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Thread created successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Created thread data', + properties: { + id: { type: 'string', description: 'Thread ID' }, + name: { type: 'string', description: 'Thread name' }, + type: { type: 'number', description: 'Thread channel type' }, + guild_id: { type: 'string', description: 'Server ID' }, + parent_id: { type: 'string', description: 'Parent channel ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/create_webhook.ts b/apps/sim/tools/discord/create_webhook.ts new file mode 100644 index 0000000000..9ce5225dd7 --- /dev/null +++ b/apps/sim/tools/discord/create_webhook.ts @@ -0,0 +1,84 @@ +import type { + DiscordCreateWebhookParams, + DiscordCreateWebhookResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordCreateWebhookTool: ToolConfig< + DiscordCreateWebhookParams, + DiscordCreateWebhookResponse +> = { + id: 'discord_create_webhook', + name: 'Discord Create Webhook', + description: 'Create a webhook in a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID to create the webhook in', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the webhook (1-80 characters)', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordCreateWebhookParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}/webhooks` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordCreateWebhookParams) => { + return { + name: params.name, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Webhook created successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Created webhook data', + properties: { + id: { type: 'string', description: 'Webhook ID' }, + name: { type: 'string', description: 'Webhook name' }, + token: { type: 'string', description: 'Webhook token' }, + url: { type: 'string', description: 'Webhook URL' }, + channel_id: { type: 'string', description: 'Channel ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/delete_channel.ts b/apps/sim/tools/discord/delete_channel.ts new file mode 100644 index 0000000000..ae70d7b861 --- /dev/null +++ b/apps/sim/tools/discord/delete_channel.ts @@ -0,0 +1,59 @@ +import type { + DiscordDeleteChannelParams, + DiscordDeleteChannelResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordDeleteChannelTool: ToolConfig< + DiscordDeleteChannelParams, + DiscordDeleteChannelResponse +> = { + id: 'discord_delete_channel', + name: 'Discord Delete Channel', + description: 'Delete a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Discord channel ID to delete', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordDeleteChannelParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Channel deleted successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/delete_invite.ts b/apps/sim/tools/discord/delete_invite.ts new file mode 100644 index 0000000000..60e241b643 --- /dev/null +++ b/apps/sim/tools/discord/delete_invite.ts @@ -0,0 +1,56 @@ +import type { DiscordDeleteInviteParams, DiscordDeleteInviteResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordDeleteInviteTool: ToolConfig< + DiscordDeleteInviteParams, + DiscordDeleteInviteResponse +> = { + id: 'discord_delete_invite', + name: 'Discord Delete Invite', + description: 'Delete a Discord invite', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + inviteCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The invite code to delete', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordDeleteInviteParams) => { + return `https://discord.com/api/v10/invites/${params.inviteCode}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Invite deleted successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/delete_message.ts b/apps/sim/tools/discord/delete_message.ts new file mode 100644 index 0000000000..b2bb2f7f04 --- /dev/null +++ b/apps/sim/tools/discord/delete_message.ts @@ -0,0 +1,65 @@ +import type { + DiscordDeleteMessageParams, + DiscordDeleteMessageResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordDeleteMessageTool: ToolConfig< + DiscordDeleteMessageParams, + DiscordDeleteMessageResponse +> = { + id: 'discord_delete_message', + name: 'Discord Delete Message', + description: 'Delete a message from a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to delete', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordDeleteMessageParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Message deleted successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/delete_role.ts b/apps/sim/tools/discord/delete_role.ts new file mode 100644 index 0000000000..f9dcde3385 --- /dev/null +++ b/apps/sim/tools/discord/delete_role.ts @@ -0,0 +1,54 @@ +import type { DiscordDeleteRoleParams, DiscordDeleteRoleResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordDeleteRoleTool: ToolConfig = + { + id: 'discord_delete_role', + name: 'Discord Delete Role', + description: 'Delete a role from a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + roleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The role ID to delete', + }, + }, + + request: { + url: (params: DiscordDeleteRoleParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/roles/${params.roleId}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Role deleted successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, + } diff --git a/apps/sim/tools/discord/delete_webhook.ts b/apps/sim/tools/discord/delete_webhook.ts new file mode 100644 index 0000000000..69e6307bcb --- /dev/null +++ b/apps/sim/tools/discord/delete_webhook.ts @@ -0,0 +1,59 @@ +import type { + DiscordDeleteWebhookParams, + DiscordDeleteWebhookResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordDeleteWebhookTool: ToolConfig< + DiscordDeleteWebhookParams, + DiscordDeleteWebhookResponse +> = { + id: 'discord_delete_webhook', + name: 'Discord Delete Webhook', + description: 'Delete a Discord webhook', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + webhookId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The webhook ID to delete', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordDeleteWebhookParams) => { + return `https://discord.com/api/v10/webhooks/${params.webhookId}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Webhook deleted successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/edit_message.ts b/apps/sim/tools/discord/edit_message.ts new file mode 100644 index 0000000000..edd28c7fa7 --- /dev/null +++ b/apps/sim/tools/discord/edit_message.ts @@ -0,0 +1,86 @@ +import type { DiscordEditMessageParams, DiscordEditMessageResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordEditMessageTool: ToolConfig< + DiscordEditMessageParams, + DiscordEditMessageResponse +> = { + id: 'discord_edit_message', + name: 'Discord Edit Message', + description: 'Edit an existing message in a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to edit', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new text content for the message', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordEditMessageParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordEditMessageParams) => { + return { + content: params.content, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Message edited successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Updated Discord message data', + properties: { + id: { type: 'string', description: 'Message ID' }, + content: { type: 'string', description: 'Updated message content' }, + channel_id: { type: 'string', description: 'Channel ID' }, + edited_timestamp: { type: 'string', description: 'Message edited timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/execute_webhook.ts b/apps/sim/tools/discord/execute_webhook.ts new file mode 100644 index 0000000000..1531084ecc --- /dev/null +++ b/apps/sim/tools/discord/execute_webhook.ts @@ -0,0 +1,90 @@ +import type { + DiscordExecuteWebhookParams, + DiscordExecuteWebhookResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordExecuteWebhookTool: ToolConfig< + DiscordExecuteWebhookParams, + DiscordExecuteWebhookResponse +> = { + id: 'discord_execute_webhook', + name: 'Discord Execute Webhook', + description: 'Execute a Discord webhook to send a message', + version: '1.0.0', + + params: { + webhookId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The webhook ID', + }, + webhookToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The webhook token', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The message content to send', + }, + username: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Override the default username of the webhook', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordExecuteWebhookParams) => { + return `https://discord.com/api/v10/webhooks/${params.webhookId}/${params.webhookToken}?wait=true` + }, + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: DiscordExecuteWebhookParams) => { + const body: any = { + content: params.content, + } + if (params.username) body.username = params.username + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Webhook executed successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Message sent via webhook', + properties: { + id: { type: 'string', description: 'Message ID' }, + content: { type: 'string', description: 'Message content' }, + channel_id: { type: 'string', description: 'Channel ID' }, + timestamp: { type: 'string', description: 'Message timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/get_channel.ts b/apps/sim/tools/discord/get_channel.ts new file mode 100644 index 0000000000..1925479fe4 --- /dev/null +++ b/apps/sim/tools/discord/get_channel.ts @@ -0,0 +1,67 @@ +import type { DiscordGetChannelParams, DiscordGetChannelResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordGetChannelTool: ToolConfig = + { + id: 'discord_get_channel', + name: 'Discord Get Channel', + description: 'Get information about a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID to retrieve', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordGetChannelParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Channel information retrieved successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Channel data', + properties: { + id: { type: 'string', description: 'Channel ID' }, + name: { type: 'string', description: 'Channel name' }, + type: { type: 'number', description: 'Channel type' }, + topic: { type: 'string', description: 'Channel topic' }, + guild_id: { type: 'string', description: 'Server ID' }, + }, + }, + }, + } diff --git a/apps/sim/tools/discord/get_invite.ts b/apps/sim/tools/discord/get_invite.ts new file mode 100644 index 0000000000..49f4ee168a --- /dev/null +++ b/apps/sim/tools/discord/get_invite.ts @@ -0,0 +1,66 @@ +import type { DiscordGetInviteParams, DiscordGetInviteResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordGetInviteTool: ToolConfig = { + id: 'discord_get_invite', + name: 'Discord Get Invite', + description: 'Get information about a Discord invite', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + inviteCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The invite code to retrieve', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordGetInviteParams) => { + return `https://discord.com/api/v10/invites/${params.inviteCode}?with_counts=true` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Invite information retrieved successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Invite data', + properties: { + code: { type: 'string', description: 'Invite code' }, + guild: { type: 'object', description: 'Server information' }, + channel: { type: 'object', description: 'Channel information' }, + approximate_member_count: { type: 'number', description: 'Approximate member count' }, + approximate_presence_count: { type: 'number', description: 'Approximate online count' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/get_member.ts b/apps/sim/tools/discord/get_member.ts new file mode 100644 index 0000000000..d6189de7a6 --- /dev/null +++ b/apps/sim/tools/discord/get_member.ts @@ -0,0 +1,73 @@ +import type { DiscordGetMemberParams, DiscordGetMemberResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordGetMemberTool: ToolConfig = { + id: 'discord_get_member', + name: 'Discord Get Member', + description: 'Get information about a member in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to retrieve', + }, + }, + + request: { + url: (params: DiscordGetMemberParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Member information retrieved successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Member data', + properties: { + user: { + type: 'object', + description: 'User information', + properties: { + id: { type: 'string', description: 'User ID' }, + username: { type: 'string', description: 'Username' }, + avatar: { type: 'string', description: 'Avatar hash' }, + }, + }, + nick: { type: 'string', description: 'Server nickname' }, + roles: { type: 'array', description: 'Array of role IDs' }, + joined_at: { type: 'string', description: 'When the member joined' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/get_messages.ts b/apps/sim/tools/discord/get_messages.ts index fb46edaf6d..576df7f1a3 100644 --- a/apps/sim/tools/discord/get_messages.ts +++ b/apps/sim/tools/discord/get_messages.ts @@ -33,8 +33,8 @@ export const discordGetMessagesTool: ToolConfig< request: { url: (params: DiscordGetMessagesParams) => { - const limit = Math.min(params.limit || 10, 100) - return `https://discord.com/api/v10/channels/${params.channelId}/messages?limit=${limit}` + const limit = params.limit ? Number(params.limit) : 10 + return `https://discord.com/api/v10/channels/${params.channelId}/messages?limit=${Math.min(limit, 100)}` }, method: 'GET', headers: (params) => { @@ -66,33 +66,43 @@ export const discordGetMessagesTool: ToolConfig< outputs: { message: { type: 'string', description: 'Success or error message' }, - messages: { - type: 'array', - description: 'Array of Discord messages with full metadata', - items: { - type: 'object', - properties: { - id: { type: 'string', description: 'Message ID' }, - content: { type: 'string', description: 'Message content' }, - channel_id: { type: 'string', description: 'Channel ID' }, - author: { + data: { + type: 'object', + description: 'Container for messages data', + properties: { + messages: { + type: 'array', + description: 'Array of Discord messages with full metadata', + items: { type: 'object', - description: 'Message author information', properties: { - id: { type: 'string', description: 'Author user ID' }, - username: { type: 'string', description: 'Author username' }, - avatar: { type: 'string', description: 'Author avatar hash' }, - bot: { type: 'boolean', description: 'Whether author is a bot' }, + id: { type: 'string', description: 'Message ID' }, + content: { type: 'string', description: 'Message content' }, + channel_id: { type: 'string', description: 'Channel ID' }, + author: { + type: 'object', + description: 'Message author information', + properties: { + id: { type: 'string', description: 'Author user ID' }, + username: { type: 'string', description: 'Author username' }, + avatar: { type: 'string', description: 'Author avatar hash' }, + bot: { type: 'boolean', description: 'Whether author is a bot' }, + }, + }, + timestamp: { type: 'string', description: 'Message timestamp' }, + edited_timestamp: { type: 'string', description: 'Message edited timestamp' }, + embeds: { type: 'array', description: 'Message embeds' }, + attachments: { type: 'array', description: 'Message attachments' }, + mentions: { type: 'array', description: 'User mentions in message' }, + mention_roles: { type: 'array', description: 'Role mentions in message' }, + mention_everyone: { + type: 'boolean', + description: 'Whether message mentions everyone', + }, }, }, - timestamp: { type: 'string', description: 'Message timestamp' }, - edited_timestamp: { type: 'string', description: 'Message edited timestamp' }, - embeds: { type: 'array', description: 'Message embeds' }, - attachments: { type: 'array', description: 'Message attachments' }, - mentions: { type: 'array', description: 'User mentions in message' }, - mention_roles: { type: 'array', description: 'Role mentions in message' }, - mention_everyone: { type: 'boolean', description: 'Whether message mentions everyone' }, }, + channel_id: { type: 'string', description: 'Channel ID' }, }, }, }, diff --git a/apps/sim/tools/discord/get_webhook.ts b/apps/sim/tools/discord/get_webhook.ts new file mode 100644 index 0000000000..b2536887ae --- /dev/null +++ b/apps/sim/tools/discord/get_webhook.ts @@ -0,0 +1,67 @@ +import type { DiscordGetWebhookParams, DiscordGetWebhookResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordGetWebhookTool: ToolConfig = + { + id: 'discord_get_webhook', + name: 'Discord Get Webhook', + description: 'Get information about a Discord webhook', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + webhookId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The webhook ID to retrieve', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordGetWebhookParams) => { + return `https://discord.com/api/v10/webhooks/${params.webhookId}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Webhook information retrieved successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Webhook data', + properties: { + id: { type: 'string', description: 'Webhook ID' }, + name: { type: 'string', description: 'Webhook name' }, + channel_id: { type: 'string', description: 'Channel ID' }, + guild_id: { type: 'string', description: 'Server ID' }, + token: { type: 'string', description: 'Webhook token' }, + }, + }, + }, + } diff --git a/apps/sim/tools/discord/index.ts b/apps/sim/tools/discord/index.ts index ec9e992344..5ef5143653 100644 --- a/apps/sim/tools/discord/index.ts +++ b/apps/sim/tools/discord/index.ts @@ -1,6 +1,90 @@ +// Existing tools + +import { discordAddReactionTool } from '@/tools/discord/add_reaction' +import { discordArchiveThreadTool } from '@/tools/discord/archive_thread' +import { discordAssignRoleTool } from '@/tools/discord/assign_role' +import { discordBanMemberTool } from '@/tools/discord/ban_member' +// Channel operations +import { discordCreateChannelTool } from '@/tools/discord/create_channel' +// Invite operations +import { discordCreateInviteTool } from '@/tools/discord/create_invite' +// Role operations +import { discordCreateRoleTool } from '@/tools/discord/create_role' +// Thread operations +import { discordCreateThreadTool } from '@/tools/discord/create_thread' +// Webhook operations +import { discordCreateWebhookTool } from '@/tools/discord/create_webhook' +import { discordDeleteChannelTool } from '@/tools/discord/delete_channel' +import { discordDeleteInviteTool } from '@/tools/discord/delete_invite' +import { discordDeleteMessageTool } from '@/tools/discord/delete_message' +import { discordDeleteRoleTool } from '@/tools/discord/delete_role' +import { discordDeleteWebhookTool } from '@/tools/discord/delete_webhook' +// Message operations +import { discordEditMessageTool } from '@/tools/discord/edit_message' +import { discordExecuteWebhookTool } from '@/tools/discord/execute_webhook' +import { discordGetChannelTool } from '@/tools/discord/get_channel' +import { discordGetInviteTool } from '@/tools/discord/get_invite' +import { discordGetMemberTool } from '@/tools/discord/get_member' import { discordGetMessagesTool } from '@/tools/discord/get_messages' import { discordGetServerTool } from '@/tools/discord/get_server' import { discordGetUserTool } from '@/tools/discord/get_user' +import { discordGetWebhookTool } from '@/tools/discord/get_webhook' +import { discordJoinThreadTool } from '@/tools/discord/join_thread' +// Member operations +import { discordKickMemberTool } from '@/tools/discord/kick_member' +import { discordLeaveThreadTool } from '@/tools/discord/leave_thread' +import { discordPinMessageTool } from '@/tools/discord/pin_message' +import { discordRemoveReactionTool } from '@/tools/discord/remove_reaction' +import { discordRemoveRoleTool } from '@/tools/discord/remove_role' import { discordSendMessageTool } from '@/tools/discord/send_message' +import { discordUnbanMemberTool } from '@/tools/discord/unban_member' +import { discordUnpinMessageTool } from '@/tools/discord/unpin_message' +import { discordUpdateChannelTool } from '@/tools/discord/update_channel' +import { discordUpdateMemberTool } from '@/tools/discord/update_member' +import { discordUpdateRoleTool } from '@/tools/discord/update_role' -export { discordSendMessageTool, discordGetMessagesTool, discordGetServerTool, discordGetUserTool } +export { + // Existing tools + discordSendMessageTool, + discordGetMessagesTool, + discordGetServerTool, + discordGetUserTool, + // Message operations + discordEditMessageTool, + discordDeleteMessageTool, + discordAddReactionTool, + discordRemoveReactionTool, + discordPinMessageTool, + discordUnpinMessageTool, + // Thread operations + discordCreateThreadTool, + discordJoinThreadTool, + discordLeaveThreadTool, + discordArchiveThreadTool, + // Channel operations + discordCreateChannelTool, + discordUpdateChannelTool, + discordDeleteChannelTool, + discordGetChannelTool, + // Role operations + discordCreateRoleTool, + discordUpdateRoleTool, + discordDeleteRoleTool, + discordAssignRoleTool, + discordRemoveRoleTool, + // Member operations + discordKickMemberTool, + discordBanMemberTool, + discordUnbanMemberTool, + discordGetMemberTool, + discordUpdateMemberTool, + // Invite operations + discordCreateInviteTool, + discordGetInviteTool, + discordDeleteInviteTool, + // Webhook operations + discordCreateWebhookTool, + discordExecuteWebhookTool, + discordGetWebhookTool, + discordDeleteWebhookTool, +} diff --git a/apps/sim/tools/discord/join_thread.ts b/apps/sim/tools/discord/join_thread.ts new file mode 100644 index 0000000000..ebe5e7ba76 --- /dev/null +++ b/apps/sim/tools/discord/join_thread.ts @@ -0,0 +1,54 @@ +import type { DiscordJoinThreadParams, DiscordJoinThreadResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordJoinThreadTool: ToolConfig = + { + id: 'discord_join_thread', + name: 'Discord Join Thread', + description: 'Join a thread in Discord', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The thread ID to join', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordJoinThreadParams) => { + return `https://discord.com/api/v10/channels/${params.threadId}/thread-members/@me` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Joined thread successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, + } diff --git a/apps/sim/tools/discord/kick_member.ts b/apps/sim/tools/discord/kick_member.ts new file mode 100644 index 0000000000..4d1fd7e128 --- /dev/null +++ b/apps/sim/tools/discord/kick_member.ts @@ -0,0 +1,66 @@ +import type { DiscordKickMemberParams, DiscordKickMemberResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordKickMemberTool: ToolConfig = + { + id: 'discord_kick_member', + name: 'Discord Kick Member', + description: 'Kick a member from a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to kick', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for kicking the member', + }, + }, + + request: { + url: (params: DiscordKickMemberParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}` + }, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + Authorization: `Bot ${params.botToken}`, + } + if (params.reason) { + headers['X-Audit-Log-Reason'] = encodeURIComponent(params.reason) + } + return headers + }, + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Member kicked successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, + } diff --git a/apps/sim/tools/discord/leave_thread.ts b/apps/sim/tools/discord/leave_thread.ts new file mode 100644 index 0000000000..718b99d8d7 --- /dev/null +++ b/apps/sim/tools/discord/leave_thread.ts @@ -0,0 +1,56 @@ +import type { DiscordLeaveThreadParams, DiscordLeaveThreadResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordLeaveThreadTool: ToolConfig< + DiscordLeaveThreadParams, + DiscordLeaveThreadResponse +> = { + id: 'discord_leave_thread', + name: 'Discord Leave Thread', + description: 'Leave a thread in Discord', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + threadId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The thread ID to leave', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordLeaveThreadParams) => { + return `https://discord.com/api/v10/channels/${params.threadId}/thread-members/@me` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Left thread successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/pin_message.ts b/apps/sim/tools/discord/pin_message.ts new file mode 100644 index 0000000000..1b91bac714 --- /dev/null +++ b/apps/sim/tools/discord/pin_message.ts @@ -0,0 +1,60 @@ +import type { DiscordPinMessageParams, DiscordPinMessageResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordPinMessageTool: ToolConfig = + { + id: 'discord_pin_message', + name: 'Discord Pin Message', + description: 'Pin a message in a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to pin', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordPinMessageParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}/pins/${params.messageId}` + }, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Message pinned successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, + } diff --git a/apps/sim/tools/discord/remove_reaction.ts b/apps/sim/tools/discord/remove_reaction.ts new file mode 100644 index 0000000000..bfe9a85cc8 --- /dev/null +++ b/apps/sim/tools/discord/remove_reaction.ts @@ -0,0 +1,79 @@ +import type { + DiscordRemoveReactionParams, + DiscordRemoveReactionResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordRemoveReactionTool: ToolConfig< + DiscordRemoveReactionParams, + DiscordRemoveReactionResponse +> = { + id: 'discord_remove_reaction', + name: 'Discord Remove Reaction', + description: 'Remove a reaction from a Discord message', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message with the reaction', + }, + emoji: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The emoji to remove (unicode emoji or custom emoji in name:id format)', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: "The user ID whose reaction to remove (omit to remove bot's own reaction)", + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordRemoveReactionParams) => { + const encodedEmoji = encodeURIComponent(params.emoji) + const userPart = params.userId ? `/${params.userId}` : '/@me' + return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}/reactions/${encodedEmoji}${userPart}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Reaction removed successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/remove_role.ts b/apps/sim/tools/discord/remove_role.ts new file mode 100644 index 0000000000..65120bbd30 --- /dev/null +++ b/apps/sim/tools/discord/remove_role.ts @@ -0,0 +1,60 @@ +import type { DiscordRemoveRoleParams, DiscordRemoveRoleResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordRemoveRoleTool: ToolConfig = + { + id: 'discord_remove_role', + name: 'Discord Remove Role', + description: 'Remove a role from a member in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to remove the role from', + }, + roleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The role ID to remove', + }, + }, + + request: { + url: (params: DiscordRemoveRoleParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}/roles/${params.roleId}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Role removed successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, + } diff --git a/apps/sim/tools/discord/types.ts b/apps/sim/tools/discord/types.ts index 835fe431fc..cad8f68c7f 100644 --- a/apps/sim/tools/discord/types.ts +++ b/apps/sim/tools/discord/types.ts @@ -111,8 +111,413 @@ export interface DiscordGetUserResponse extends BaseDiscordResponse { } } +// Message operations +export interface DiscordEditMessageParams extends DiscordAuthParams { + channelId: string + messageId: string + content?: string +} + +export interface DiscordEditMessageResponse extends BaseDiscordResponse { + output: { + message: string + data?: DiscordMessage + } +} + +export interface DiscordDeleteMessageParams extends DiscordAuthParams { + channelId: string + messageId: string +} + +export interface DiscordDeleteMessageResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordAddReactionParams extends DiscordAuthParams { + channelId: string + messageId: string + emoji: string +} + +export interface DiscordAddReactionResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordRemoveReactionParams extends DiscordAuthParams { + channelId: string + messageId: string + emoji: string + userId?: string +} + +export interface DiscordRemoveReactionResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordPinMessageParams extends DiscordAuthParams { + channelId: string + messageId: string +} + +export interface DiscordPinMessageResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordUnpinMessageParams extends DiscordAuthParams { + channelId: string + messageId: string +} + +export interface DiscordUnpinMessageResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +// Thread operations +export interface DiscordCreateThreadParams extends DiscordAuthParams { + channelId: string + name: string + messageId?: string + autoArchiveDuration?: number +} + +export interface DiscordCreateThreadResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordJoinThreadParams extends DiscordAuthParams { + threadId: string +} + +export interface DiscordJoinThreadResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordLeaveThreadParams extends DiscordAuthParams { + threadId: string +} + +export interface DiscordLeaveThreadResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordArchiveThreadParams extends DiscordAuthParams { + threadId: string + archived: boolean +} + +export interface DiscordArchiveThreadResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +// Channel operations +export interface DiscordCreateChannelParams extends DiscordAuthParams { + name: string + type?: number + topic?: string + parentId?: string +} + +export interface DiscordCreateChannelResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordUpdateChannelParams extends DiscordAuthParams { + channelId: string + name?: string + topic?: string +} + +export interface DiscordUpdateChannelResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordDeleteChannelParams extends DiscordAuthParams { + channelId: string +} + +export interface DiscordDeleteChannelResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordGetChannelParams extends DiscordAuthParams { + channelId: string +} + +export interface DiscordGetChannelResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +// Role operations +export interface DiscordCreateRoleParams extends DiscordAuthParams { + name: string + color?: number + hoist?: boolean + mentionable?: boolean +} + +export interface DiscordCreateRoleResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordUpdateRoleParams extends DiscordAuthParams { + roleId: string + name?: string + color?: number + hoist?: boolean + mentionable?: boolean +} + +export interface DiscordUpdateRoleResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordDeleteRoleParams extends DiscordAuthParams { + roleId: string +} + +export interface DiscordDeleteRoleResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordAssignRoleParams extends DiscordAuthParams { + userId: string + roleId: string +} + +export interface DiscordAssignRoleResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordRemoveRoleParams extends DiscordAuthParams { + userId: string + roleId: string +} + +export interface DiscordRemoveRoleResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +// Member operations +export interface DiscordKickMemberParams extends DiscordAuthParams { + userId: string + reason?: string +} + +export interface DiscordKickMemberResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordBanMemberParams extends DiscordAuthParams { + userId: string + reason?: string + deleteMessageDays?: number +} + +export interface DiscordBanMemberResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordUnbanMemberParams extends DiscordAuthParams { + userId: string + reason?: string +} + +export interface DiscordUnbanMemberResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +export interface DiscordGetMemberParams extends DiscordAuthParams { + userId: string +} + +export interface DiscordGetMemberResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordUpdateMemberParams extends DiscordAuthParams { + userId: string + nick?: string + mute?: boolean + deaf?: boolean +} + +export interface DiscordUpdateMemberResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +// Invite operations +export interface DiscordCreateInviteParams extends DiscordAuthParams { + channelId: string + maxAge?: number + maxUses?: number + temporary?: boolean +} + +export interface DiscordCreateInviteResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordGetInviteParams extends DiscordAuthParams { + inviteCode: string +} + +export interface DiscordGetInviteResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordDeleteInviteParams extends DiscordAuthParams { + inviteCode: string +} + +export interface DiscordDeleteInviteResponse extends BaseDiscordResponse { + output: { + message: string + } +} + +// Webhook operations +export interface DiscordCreateWebhookParams extends DiscordAuthParams { + channelId: string + name: string +} + +export interface DiscordCreateWebhookResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordExecuteWebhookParams extends DiscordAuthParams { + webhookId: string + webhookToken: string + content: string + username?: string +} + +export interface DiscordExecuteWebhookResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordGetWebhookParams extends DiscordAuthParams { + webhookId: string +} + +export interface DiscordGetWebhookResponse extends BaseDiscordResponse { + output: { + message: string + data?: any + } +} + +export interface DiscordDeleteWebhookParams extends DiscordAuthParams { + webhookId: string +} + +export interface DiscordDeleteWebhookResponse extends BaseDiscordResponse { + output: { + message: string + } +} + export type DiscordResponse = | DiscordSendMessageResponse | DiscordGetMessagesResponse | DiscordGetServerResponse | DiscordGetUserResponse + | DiscordEditMessageResponse + | DiscordDeleteMessageResponse + | DiscordAddReactionResponse + | DiscordRemoveReactionResponse + | DiscordPinMessageResponse + | DiscordUnpinMessageResponse + | DiscordCreateThreadResponse + | DiscordJoinThreadResponse + | DiscordLeaveThreadResponse + | DiscordArchiveThreadResponse + | DiscordCreateChannelResponse + | DiscordUpdateChannelResponse + | DiscordDeleteChannelResponse + | DiscordGetChannelResponse + | DiscordCreateRoleResponse + | DiscordUpdateRoleResponse + | DiscordDeleteRoleResponse + | DiscordAssignRoleResponse + | DiscordRemoveRoleResponse + | DiscordKickMemberResponse + | DiscordBanMemberResponse + | DiscordUnbanMemberResponse + | DiscordGetMemberResponse + | DiscordUpdateMemberResponse + | DiscordCreateInviteResponse + | DiscordGetInviteResponse + | DiscordDeleteInviteResponse + | DiscordCreateWebhookResponse + | DiscordExecuteWebhookResponse + | DiscordGetWebhookResponse + | DiscordDeleteWebhookResponse diff --git a/apps/sim/tools/discord/unban_member.ts b/apps/sim/tools/discord/unban_member.ts new file mode 100644 index 0000000000..6db2e2af78 --- /dev/null +++ b/apps/sim/tools/discord/unban_member.ts @@ -0,0 +1,68 @@ +import type { DiscordUnbanMemberParams, DiscordUnbanMemberResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordUnbanMemberTool: ToolConfig< + DiscordUnbanMemberParams, + DiscordUnbanMemberResponse +> = { + id: 'discord_unban_member', + name: 'Discord Unban Member', + description: 'Unban a member from a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to unban', + }, + reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reason for unbanning the member', + }, + }, + + request: { + url: (params: DiscordUnbanMemberParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/bans/${params.userId}` + }, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + Authorization: `Bot ${params.botToken}`, + } + if (params.reason) { + headers['X-Audit-Log-Reason'] = encodeURIComponent(params.reason) + } + return headers + }, + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Member unbanned successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/unpin_message.ts b/apps/sim/tools/discord/unpin_message.ts new file mode 100644 index 0000000000..3274d03514 --- /dev/null +++ b/apps/sim/tools/discord/unpin_message.ts @@ -0,0 +1,62 @@ +import type { DiscordUnpinMessageParams, DiscordUnpinMessageResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordUnpinMessageTool: ToolConfig< + DiscordUnpinMessageParams, + DiscordUnpinMessageResponse +> = { + id: 'discord_unpin_message', + name: 'Discord Unpin Message', + description: 'Unpin a message in a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to unpin', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordUnpinMessageParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}/pins/${params.messageId}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bot ${params.botToken}`, + }), + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + message: 'Message unpinned successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/update_channel.ts b/apps/sim/tools/discord/update_channel.ts new file mode 100644 index 0000000000..cb782a7bb0 --- /dev/null +++ b/apps/sim/tools/discord/update_channel.ts @@ -0,0 +1,90 @@ +import type { + DiscordUpdateChannelParams, + DiscordUpdateChannelResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordUpdateChannelTool: ToolConfig< + DiscordUpdateChannelParams, + DiscordUpdateChannelResponse +> = { + id: 'discord_update_channel', + name: 'Discord Update Channel', + description: 'Update a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord channel ID to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new name for the channel', + }, + topic: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new topic for the channel', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + }, + + request: { + url: (params: DiscordUpdateChannelParams) => { + return `https://discord.com/api/v10/channels/${params.channelId}` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordUpdateChannelParams) => { + const body: any = {} + if (params.name) body.name = params.name + if (params.topic !== undefined) body.topic = params.topic + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Channel updated successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Updated channel data', + properties: { + id: { type: 'string', description: 'Channel ID' }, + name: { type: 'string', description: 'Channel name' }, + type: { type: 'number', description: 'Channel type' }, + topic: { type: 'string', description: 'Channel topic' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/update_member.ts b/apps/sim/tools/discord/update_member.ts new file mode 100644 index 0000000000..81511d4d89 --- /dev/null +++ b/apps/sim/tools/discord/update_member.ts @@ -0,0 +1,93 @@ +import type { DiscordUpdateMemberParams, DiscordUpdateMemberResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordUpdateMemberTool: ToolConfig< + DiscordUpdateMemberParams, + DiscordUpdateMemberResponse +> = { + id: 'discord_update_member', + name: 'Discord Update Member', + description: 'Update a member in a Discord server (e.g., change nickname)', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to update', + }, + nick: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New nickname for the member (null to remove)', + }, + mute: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to mute the member in voice channels', + }, + deaf: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to deafen the member in voice channels', + }, + }, + + request: { + url: (params: DiscordUpdateMemberParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordUpdateMemberParams) => { + const body: any = {} + if (params.nick !== undefined) body.nick = params.nick + if (params.mute !== undefined) body.mute = params.mute + if (params.deaf !== undefined) body.deaf = params.deaf + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Member updated successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Updated member data', + properties: { + nick: { type: 'string', description: 'Server nickname' }, + mute: { type: 'boolean', description: 'Voice mute status' }, + deaf: { type: 'boolean', description: 'Voice deaf status' }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/update_role.ts b/apps/sim/tools/discord/update_role.ts new file mode 100644 index 0000000000..917abeafd3 --- /dev/null +++ b/apps/sim/tools/discord/update_role.ts @@ -0,0 +1,98 @@ +import type { DiscordUpdateRoleParams, DiscordUpdateRoleResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordUpdateRoleTool: ToolConfig = + { + id: 'discord_update_role', + name: 'Discord Update Role', + description: 'Update a role in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Discord server ID (guild ID)', + }, + roleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The role ID to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new name for the role', + }, + color: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'RGB color value as integer', + }, + hoist: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to display role members separately', + }, + mentionable: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the role can be mentioned', + }, + }, + + request: { + url: (params: DiscordUpdateRoleParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId}/roles/${params.roleId}` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken}`, + }), + body: (params: DiscordUpdateRoleParams) => { + const body: any = {} + if (params.name) body.name = params.name + if (params.color !== undefined) body.color = Number(params.color) + if (params.hoist !== undefined) body.hoist = params.hoist + if (params.mentionable !== undefined) body.mentionable = params.mentionable + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + message: 'Role updated successfully', + data, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'Updated role data', + properties: { + id: { type: 'string', description: 'Role ID' }, + name: { type: 'string', description: 'Role name' }, + color: { type: 'number', description: 'Role color' }, + }, + }, + }, + } diff --git a/apps/sim/tools/exa/find_similar_links.ts b/apps/sim/tools/exa/find_similar_links.ts index d817b8fa06..9e4f5084b7 100644 --- a/apps/sim/tools/exa/find_similar_links.ts +++ b/apps/sim/tools/exa/find_similar_links.ts @@ -30,6 +30,73 @@ export const findSimilarLinksTool: ToolConfig< visibility: 'user-or-llm', description: 'Whether to include the full text of the similar pages', }, + includeDomains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to include in results', + }, + excludeDomains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to exclude from results', + }, + excludeSourceDomain: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Exclude the source domain from results (default: false)', + }, + startPublishedDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results published after this date (ISO 8601 format, e.g., 2024-01-01)', + }, + endPublishedDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results published before this date (ISO 8601 format)', + }, + startCrawlDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results crawled after this date (ISO 8601 format)', + }, + endCrawlDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results crawled before this date (ISO 8601 format)', + }, + category: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Filter by category: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site', + }, + highlights: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include highlighted snippets in results (default: false)', + }, + summary: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include AI-generated summaries in results (default: false)', + }, + livecrawl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Live crawling mode: always, fallback, or never (default: never)', + }, apiKey: { type: 'string', required: true, @@ -51,13 +118,45 @@ export const findSimilarLinksTool: ToolConfig< } // Add optional parameters if provided - if (params.numResults) body.numResults = params.numResults + if (params.numResults) body.numResults = Number(params.numResults) + + // Domain filtering + if (params.includeDomains) { + body.includeDomains = params.includeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + } + if (params.excludeDomains) { + body.excludeDomains = params.excludeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + } + if (params.excludeSourceDomain !== undefined) { + body.excludeSourceDomain = params.excludeSourceDomain + } + + // Date filtering + if (params.startPublishedDate) body.startPublishedDate = params.startPublishedDate + if (params.endPublishedDate) body.endPublishedDate = params.endPublishedDate + if (params.startCrawlDate) body.startCrawlDate = params.startCrawlDate + if (params.endCrawlDate) body.endCrawlDate = params.endCrawlDate + + // Category filtering + if (params.category) body.category = params.category + + // Content options - build contents object + const contents: Record = {} + if (params.text !== undefined) contents.text = params.text + if (params.highlights !== undefined) contents.highlights = params.highlights + if (params.summary !== undefined) contents.summary = params.summary + + // Live crawl mode should be inside contents + if (params.livecrawl) contents.livecrawl = params.livecrawl - // Add contents.text parameter if text is true - if (params.text) { - body.contents = { - text: true, - } + if (Object.keys(contents).length > 0) { + body.contents = contents } return body @@ -74,6 +173,8 @@ export const findSimilarLinksTool: ToolConfig< title: result.title || '', url: result.url, text: result.text || '', + summary: result.summary, + highlights: result.highlights, score: result.score || 0, })), }, diff --git a/apps/sim/tools/exa/get_contents.ts b/apps/sim/tools/exa/get_contents.ts index f5d3b1e500..31f359daea 100644 --- a/apps/sim/tools/exa/get_contents.ts +++ b/apps/sim/tools/exa/get_contents.ts @@ -28,6 +28,31 @@ export const getContentsTool: ToolConfig target.trim()) + .filter((target: string) => target.length > 0) + } + + // Content options + if (params.highlights !== undefined) { + body.highlights = params.highlights + } + + // Live crawl mode + if (params.livecrawl) { + body.livecrawl = params.livecrawl + } + return body }, }, @@ -82,6 +129,7 @@ export const getContentsTool: ToolConfig = visibility: 'user-or-llm', description: 'Research query or topic', }, - includeText: { - type: 'boolean', + model: { + type: 'string', required: false, visibility: 'user-only', - description: 'Include full text content in results', + description: 'Research model: exa-research-fast, exa-research (default), or exa-research-pro', }, apiKey: { type: 'string', @@ -35,7 +35,7 @@ export const researchTool: ToolConfig = }, request: { - url: 'https://api.exa.ai/research/v0/tasks', + url: 'https://api.exa.ai/research/v1/', method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', @@ -44,30 +44,11 @@ export const researchTool: ToolConfig = body: (params) => { const body: any = { instructions: params.query, - model: 'exa-research', - output: { - schema: { - type: 'object', - properties: { - results: { - type: 'array', - items: { - type: 'object', - properties: { - title: { type: 'string' }, - url: { type: 'string' }, - summary: { type: 'string' }, - text: { type: 'string' }, - publishedDate: { type: 'string' }, - author: { type: 'string' }, - score: { type: 'number' }, - }, - }, - }, - }, - required: ['results'], - }, - }, + } + + // Add model if specified, otherwise use default + if (params.model) { + body.model = params.model } return body diff --git a/apps/sim/tools/exa/search.ts b/apps/sim/tools/exa/search.ts index 429fa475c5..d6c1040ef4 100644 --- a/apps/sim/tools/exa/search.ts +++ b/apps/sim/tools/exa/search.ts @@ -33,6 +33,73 @@ export const searchTool: ToolConfig = { visibility: 'user-only', description: 'Search type: neural, keyword, auto or fast (default: auto)', }, + includeDomains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to include in results', + }, + excludeDomains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to exclude from results', + }, + startPublishedDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results published after this date (ISO 8601 format, e.g., 2024-01-01)', + }, + endPublishedDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results published before this date (ISO 8601 format)', + }, + startCrawlDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results crawled after this date (ISO 8601 format)', + }, + endCrawlDate: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter results crawled before this date (ISO 8601 format)', + }, + category: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Filter by category: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site', + }, + text: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include full text content in results (default: false)', + }, + highlights: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include highlighted snippets in results (default: false)', + }, + summary: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include AI-generated summaries in results (default: false)', + }, + livecrawl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Live crawling mode: always, fallback, or never (default: never)', + }, apiKey: { type: 'string', required: true, @@ -54,10 +121,57 @@ export const searchTool: ToolConfig = { } // Add optional parameters if provided - if (params.numResults) body.numResults = params.numResults + if (params.numResults) body.numResults = Number(params.numResults) if (params.useAutoprompt !== undefined) body.useAutoprompt = params.useAutoprompt if (params.type) body.type = params.type + // Domain filtering + if (params.includeDomains) { + body.includeDomains = params.includeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + } + if (params.excludeDomains) { + body.excludeDomains = params.excludeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + } + + // Date filtering + if (params.startPublishedDate) body.startPublishedDate = params.startPublishedDate + if (params.endPublishedDate) body.endPublishedDate = params.endPublishedDate + if (params.startCrawlDate) body.startCrawlDate = params.startCrawlDate + if (params.endCrawlDate) body.endCrawlDate = params.endCrawlDate + + // Category filtering + if (params.category) body.category = params.category + + // Build contents object for content options + const contents: Record = {} + + if (params.text !== undefined) { + contents.text = params.text + } + + if (params.highlights !== undefined) { + contents.highlights = params.highlights + } + + if (params.summary !== undefined) { + contents.summary = params.summary + } + + if (params.livecrawl) { + contents.livecrawl = params.livecrawl + } + + // Add contents to body if not empty + if (Object.keys(contents).length > 0) { + body.contents = contents + } + return body }, }, @@ -77,6 +191,7 @@ export const searchTool: ToolConfig = { favicon: result.favicon, image: result.image, text: result.text, + highlights: result.highlights, score: result.score, })), }, diff --git a/apps/sim/tools/exa/types.ts b/apps/sim/tools/exa/types.ts index bc4bd64202..ae76e36d0f 100644 --- a/apps/sim/tools/exa/types.ts +++ b/apps/sim/tools/exa/types.ts @@ -12,6 +12,31 @@ export interface ExaSearchParams extends ExaBaseParams { numResults?: number useAutoprompt?: boolean type?: 'auto' | 'neural' | 'keyword' | 'fast' + // Domain filtering + includeDomains?: string + excludeDomains?: string + // Date filtering + startPublishedDate?: string + endPublishedDate?: string + startCrawlDate?: string + endCrawlDate?: string + // Category filtering + category?: + | 'company' + | 'research_paper' + | 'news_article' + | 'pdf' + | 'github' + | 'tweet' + | 'movie' + | 'song' + | 'personal_site' + // Content options + text?: boolean | { maxCharacters?: number } + highlights?: boolean | { query?: string; numSentences?: number; highlightsPerUrl?: number } + summary?: boolean | { query?: string } + // Live crawl mode + livecrawl?: 'always' | 'fallback' | 'never' } export interface ExaSearchResult { @@ -22,7 +47,8 @@ export interface ExaSearchResult { summary?: string favicon?: string image?: string - text: string + text?: string + highlights?: string[] score: number } @@ -35,15 +61,23 @@ export interface ExaSearchResponse extends ToolResponse { // Get Contents tool types export interface ExaGetContentsParams extends ExaBaseParams { urls: string - text?: boolean + text?: boolean | { maxCharacters?: number } summaryQuery?: string + // Subpages crawling + subpages?: number + subpageTarget?: string + // Content options + highlights?: boolean | { query?: string; numSentences?: number; highlightsPerUrl?: number } + // Live crawl mode + livecrawl?: 'always' | 'fallback' | 'never' } export interface ExaGetContentsResult { url: string title: string - text: string + text?: string summary?: string + highlights?: string[] } export interface ExaGetContentsResponse extends ToolResponse { @@ -56,13 +90,40 @@ export interface ExaGetContentsResponse extends ToolResponse { export interface ExaFindSimilarLinksParams extends ExaBaseParams { url: string numResults?: number - text?: boolean + text?: boolean | { maxCharacters?: number } + // Domain filtering + includeDomains?: string + excludeDomains?: string + excludeSourceDomain?: boolean + // Date filtering + startPublishedDate?: string + endPublishedDate?: string + startCrawlDate?: string + endCrawlDate?: string + // Category filtering + category?: + | 'company' + | 'research_paper' + | 'news_article' + | 'pdf' + | 'github' + | 'tweet' + | 'movie' + | 'song' + | 'personal_site' + // Content options + highlights?: boolean | { query?: string; numSentences?: number; highlightsPerUrl?: number } + summary?: boolean | { query?: string } + // Live crawl mode + livecrawl?: 'always' | 'fallback' | 'never' } export interface ExaSimilarLink { title: string url: string - text: string + text?: string + summary?: string + highlights?: string[] score: number } @@ -92,7 +153,7 @@ export interface ExaAnswerResponse extends ToolResponse { // Research tool types export interface ExaResearchParams extends ExaBaseParams { query: string - includeText?: boolean + model?: 'exa-research-fast' | 'exa-research' | 'exa-research-pro' } export interface ExaResearchResponse extends ToolResponse { diff --git a/apps/sim/tools/firecrawl/crawl.ts b/apps/sim/tools/firecrawl/crawl.ts index 581e913b3c..043d6547c7 100644 --- a/apps/sim/tools/firecrawl/crawl.ts +++ b/apps/sim/tools/firecrawl/crawl.ts @@ -31,6 +31,90 @@ export const crawlTool: ToolConfig visibility: 'user-only', description: 'Extract only main content from pages', }, + prompt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Natural language instruction to auto-generate crawler options', + }, + maxDiscoveryDepth: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Depth limit for URL discovery (root pages have depth 0)', + }, + sitemap: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Whether to use sitemap data: "skip" or "include" (default: "include")', + }, + crawlEntireDomain: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Follow sibling/parent URLs or only child paths (default: false)', + }, + allowExternalLinks: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Follow external website links (default: false)', + }, + allowSubdomains: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Follow subdomain links (default: false)', + }, + ignoreQueryParameters: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Prevent re-scraping same path with different query params (default: false)', + }, + delay: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Seconds between scrapes for rate limit compliance', + }, + maxConcurrency: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Concurrent scrape limit', + }, + excludePaths: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Array of regex patterns for URLs to exclude', + }, + includePaths: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Array of regex patterns for URLs to include exclusively', + }, + webhook: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Webhook configuration for crawl notifications', + }, + scrapeOptions: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Advanced scraping configuration', + }, + zeroDataRetention: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Enable zero data retention (default: false)', + }, apiKey: { type: 'string', required: true, @@ -45,14 +129,36 @@ export const crawlTool: ToolConfig 'Content-Type': 'application/json', Authorization: `Bearer ${params.apiKey}`, }), - body: (params) => ({ - url: params.url, - limit: Number(params.limit) || 100, - scrapeOptions: { - formats: ['markdown'], - onlyMainContent: params.onlyMainContent || false, - }, - }), + body: (params) => { + const body: Record = { + url: params.url, + limit: Number(params.limit) || 100, + scrapeOptions: params.scrapeOptions || { + formats: ['markdown'], + onlyMainContent: params.onlyMainContent || false, + }, + } + + // Add all optional crawl-specific parameters if provided + if (params.prompt !== undefined) body.prompt = params.prompt + if (params.maxDiscoveryDepth !== undefined) + body.maxDiscoveryDepth = Number(params.maxDiscoveryDepth) + if (params.sitemap !== undefined) body.sitemap = params.sitemap + if (params.crawlEntireDomain !== undefined) body.crawlEntireDomain = params.crawlEntireDomain + if (params.allowExternalLinks !== undefined) + body.allowExternalLinks = params.allowExternalLinks + if (params.allowSubdomains !== undefined) body.allowSubdomains = params.allowSubdomains + if (params.ignoreQueryParameters !== undefined) + body.ignoreQueryParameters = params.ignoreQueryParameters + if (params.delay !== undefined) body.delay = Number(params.delay) + if (params.maxConcurrency !== undefined) body.maxConcurrency = Number(params.maxConcurrency) + if (params.excludePaths !== undefined) body.excludePaths = params.excludePaths + if (params.includePaths !== undefined) body.includePaths = params.includePaths + if (params.webhook !== undefined) body.webhook = params.webhook + if (params.zeroDataRetention !== undefined) body.zeroDataRetention = params.zeroDataRetention + + return body + }, }, transformResponse: async (response: Response) => { const data = await response.json() diff --git a/apps/sim/tools/firecrawl/extract.ts b/apps/sim/tools/firecrawl/extract.ts new file mode 100644 index 0000000000..a58678557e --- /dev/null +++ b/apps/sim/tools/firecrawl/extract.ts @@ -0,0 +1,138 @@ +import type { ExtractParams, ExtractResponse } from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const extractTool: ToolConfig = { + id: 'firecrawl_extract', + name: 'Firecrawl Extract', + description: + 'Extract structured data from entire webpages using natural language prompts and JSON schema. Powerful agentic feature for intelligent data extraction.', + version: '1.0.0', + + params: { + urls: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Array of URLs to extract data from (supports glob format)', + }, + prompt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Natural language guidance for the extraction process', + }, + schema: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'JSON Schema defining the structure of data to extract', + }, + enableWebSearch: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Enable web search to find supplementary information (default: false)', + }, + ignoreSitemap: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Ignore sitemap.xml files during scanning (default: false)', + }, + includeSubdomains: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Extend scanning to subdomains (default: true)', + }, + showSources: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Return data sources in the response (default: false)', + }, + ignoreInvalidURLs: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Skip invalid URLs in the array (default: true)', + }, + scrapeOptions: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Advanced scraping configuration options', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'POST', + url: 'https://api.firecrawl.dev/v1/extract', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = { + urls: params.urls, + } + + if (params.prompt !== undefined) body.prompt = params.prompt + if (params.schema !== undefined) body.schema = params.schema + if (params.enableWebSearch !== undefined) body.enableWebSearch = params.enableWebSearch + if (params.ignoreSitemap !== undefined) body.ignoreSitemap = params.ignoreSitemap + if (params.includeSubdomains !== undefined) body.includeSubdomains = params.includeSubdomains + if (params.showSources !== undefined) body.showSources = params.showSources + if (params.ignoreInvalidURLs !== undefined) body.ignoreInvalidURLs = params.ignoreInvalidURLs + if (params.scrapeOptions !== undefined) body.scrapeOptions = params.scrapeOptions + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: data.success, + output: { + success: data.success, + data: data.data || {}, + sources: data.sources, + warning: data.warning, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the extraction operation was successful', + }, + data: { + type: 'object', + description: 'Extracted structured data according to the schema or prompt', + }, + sources: { + type: 'array', + description: 'Data sources (only if showSources is enabled)', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'Source URL' }, + title: { type: 'string', description: 'Source title' }, + }, + }, + }, + warning: { + type: 'string', + description: 'Warning messages from the extraction operation', + }, + }, +} diff --git a/apps/sim/tools/firecrawl/index.ts b/apps/sim/tools/firecrawl/index.ts index c7c173f34d..870b1e820a 100644 --- a/apps/sim/tools/firecrawl/index.ts +++ b/apps/sim/tools/firecrawl/index.ts @@ -1,5 +1,7 @@ import { crawlTool } from '@/tools/firecrawl/crawl' +import { extractTool } from '@/tools/firecrawl/extract' +import { mapTool } from '@/tools/firecrawl/map' import { scrapeTool } from '@/tools/firecrawl/scrape' import { searchTool } from '@/tools/firecrawl/search' -export { scrapeTool, searchTool, crawlTool } +export { scrapeTool, searchTool, crawlTool, mapTool, extractTool } diff --git a/apps/sim/tools/firecrawl/map.ts b/apps/sim/tools/firecrawl/map.ts new file mode 100644 index 0000000000..335851a1f6 --- /dev/null +++ b/apps/sim/tools/firecrawl/map.ts @@ -0,0 +1,118 @@ +import type { MapParams, MapResponse } from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const mapTool: ToolConfig = { + id: 'firecrawl_map', + name: 'Firecrawl Map', + description: + 'Get a complete list of URLs from any website quickly and reliably. Useful for discovering all pages on a site without crawling them.', + version: '1.0.0', + + params: { + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The base URL to map and discover links from', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter results by relevance to a search term (e.g., "blog")', + }, + sitemap: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Controls sitemap usage: "skip", "include" (default), or "only"', + }, + includeSubdomains: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Whether to include URLs from subdomains (default: true)', + }, + ignoreQueryParameters: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Exclude URLs containing query strings (default: true)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of links to return (max: 100,000, default: 5,000)', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Request timeout in milliseconds', + }, + location: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Geographic context for proxying (country, languages)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'POST', + url: 'https://api.firecrawl.dev/v1/map', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = { + url: params.url, + } + + if (params.search !== undefined) body.search = params.search + if (params.sitemap !== undefined) body.sitemap = params.sitemap + if (params.includeSubdomains !== undefined) body.includeSubdomains = params.includeSubdomains + if (params.ignoreQueryParameters !== undefined) + body.ignoreQueryParameters = params.ignoreQueryParameters + if (params.limit !== undefined) body.limit = Number(params.limit) + if (params.timeout !== undefined) body.timeout = Number(params.timeout) + if (params.location !== undefined) body.location = params.location + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: data.success, + output: { + success: data.success, + links: data.links || [], + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the mapping operation was successful', + }, + links: { + type: 'array', + description: 'Array of discovered URLs from the website', + items: { + type: 'string', + }, + }, + }, +} diff --git a/apps/sim/tools/firecrawl/scrape.ts b/apps/sim/tools/firecrawl/scrape.ts index 52527c02ef..b5bea55450 100644 --- a/apps/sim/tools/firecrawl/scrape.ts +++ b/apps/sim/tools/firecrawl/scrape.ts @@ -15,11 +15,120 @@ export const scrapeTool: ToolConfig = { visibility: 'user-or-llm', description: 'The URL to scrape content from', }, + formats: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Output formats (markdown, html, rawHtml, links, images, screenshot). Default: ["markdown"]', + }, + onlyMainContent: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Extract only main content, excluding headers, navs, footers (default: true)', + }, + includeTags: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'HTML tags to retain in the output', + }, + excludeTags: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'HTML tags to remove from the output', + }, + maxAge: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Return cached version if younger than this age in ms (default: 172800000)', + }, + headers: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Custom request headers (cookies, user-agent, etc.)', + }, + waitFor: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Delay in milliseconds before fetching (default: 0)', + }, + mobile: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Emulate mobile device (default: false)', + }, + skipTlsVerification: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Skip TLS certificate verification (default: true)', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Request timeout in milliseconds', + }, + parsers: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'File processing controls (e.g., ["pdf"])', + }, + actions: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Pre-scrape operations (wait, click, scroll, screenshot, etc.)', + }, + location: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Geographic settings (country, languages)', + }, + removeBase64Images: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Strip base64 images from output (default: true)', + }, + blockAds: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Enable ad and popup blocking (default: true)', + }, + proxy: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'Proxy type: basic, stealth, or auto (default: auto)', + }, + storeInCache: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Cache the page (default: true)', + }, + zeroDataRetention: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Enable zero data retention mode (default: false)', + }, scrapeOptions: { type: 'json', required: false, visibility: 'hidden', - description: 'Options for content scraping', + description: 'Options for content scraping (legacy, prefer top-level params)', }, apiKey: { type: 'string', @@ -36,10 +145,40 @@ export const scrapeTool: ToolConfig = { 'Content-Type': 'application/json', Authorization: `Bearer ${params.apiKey}`, }), - body: (params) => ({ - url: params.url, - formats: params.scrapeOptions?.formats || ['markdown'], - }), + body: (params) => { + const body: Record = { + url: params.url, + formats: params.formats || params.scrapeOptions?.formats || ['markdown'], + } + + // Add all optional top-level parameters if provided + if (params.onlyMainContent !== undefined) body.onlyMainContent = params.onlyMainContent + if (params.includeTags !== undefined) body.includeTags = params.includeTags + if (params.excludeTags !== undefined) body.excludeTags = params.excludeTags + if (params.maxAge !== undefined) body.maxAge = Number(params.maxAge) + if (params.headers !== undefined) body.headers = params.headers + if (params.waitFor !== undefined) body.waitFor = Number(params.waitFor) + if (params.mobile !== undefined) body.mobile = params.mobile + if (params.skipTlsVerification !== undefined) + body.skipTlsVerification = params.skipTlsVerification + if (params.timeout !== undefined) body.timeout = Number(params.timeout) + if (params.parsers !== undefined) body.parsers = params.parsers + if (params.actions !== undefined) body.actions = params.actions + if (params.location !== undefined) body.location = params.location + if (params.removeBase64Images !== undefined) + body.removeBase64Images = params.removeBase64Images + if (params.blockAds !== undefined) body.blockAds = params.blockAds + if (params.proxy !== undefined) body.proxy = params.proxy + if (params.storeInCache !== undefined) body.storeInCache = params.storeInCache + if (params.zeroDataRetention !== undefined) body.zeroDataRetention = params.zeroDataRetention + + // Support legacy scrapeOptions for backwards compatibility + if (params.scrapeOptions) { + Object.assign(body, params.scrapeOptions) + } + + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/firecrawl/search.ts b/apps/sim/tools/firecrawl/search.ts index d172f5a895..516f75d211 100644 --- a/apps/sim/tools/firecrawl/search.ts +++ b/apps/sim/tools/firecrawl/search.ts @@ -14,6 +14,62 @@ export const searchTool: ToolConfig = { visibility: 'user-or-llm', description: 'The search query to use', }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (1-100, default: 5)', + }, + sources: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Search sources: ["web"], ["images"], or ["news"] (default: ["web"])', + }, + categories: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Filter by categories: ["github"], ["research"], or ["pdf"]', + }, + tbs: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Time-based search: qdr:h (hour), qdr:d (day), qdr:w (week), qdr:m (month), qdr:y (year)', + }, + location: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Geographic location for results (e.g., "San Francisco, California, United States")', + }, + country: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'ISO country code for geo-targeting (default: US)', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Timeout in milliseconds (default: 60000)', + }, + ignoreInvalidURLs: { + type: 'boolean', + required: false, + visibility: 'hidden', + description: 'Exclude invalid URLs from results (default: false)', + }, + scrapeOptions: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Advanced scraping configuration for search results', + }, apiKey: { type: 'string', required: true, @@ -29,9 +85,24 @@ export const searchTool: ToolConfig = { 'Content-Type': 'application/json', Authorization: `Bearer ${params.apiKey}`, }), - body: (params) => ({ - query: params.query, - }), + body: (params) => { + const body: Record = { + query: params.query, + } + + // Add all optional parameters if provided + if (params.limit !== undefined) body.limit = Number(params.limit) + if (params.sources !== undefined) body.sources = params.sources + if (params.categories !== undefined) body.categories = params.categories + if (params.tbs !== undefined) body.tbs = params.tbs + if (params.location !== undefined) body.location = params.location + if (params.country !== undefined) body.country = params.country + if (params.timeout !== undefined) body.timeout = Number(params.timeout) + if (params.ignoreInvalidURLs !== undefined) body.ignoreInvalidURLs = params.ignoreInvalidURLs + if (params.scrapeOptions !== undefined) body.scrapeOptions = params.scrapeOptions + + return body + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/firecrawl/types.ts b/apps/sim/tools/firecrawl/types.ts index d61a268770..15338fd656 100644 --- a/apps/sim/tools/firecrawl/types.ts +++ b/apps/sim/tools/firecrawl/types.ts @@ -1,17 +1,74 @@ import type { ToolResponse } from '@/tools/types' +// Common types +export interface LocationConfig { + country?: string + languages?: string[] +} + +export interface ScrapeOptions { + formats?: string[] + onlyMainContent?: boolean + includeTags?: string[] + excludeTags?: string[] + maxAge?: number + headers?: Record + waitFor?: number + mobile?: boolean + skipTlsVerification?: boolean + timeout?: number + parsers?: string[] + actions?: Array<{ + type: string + [key: string]: any + }> + location?: LocationConfig + removeBase64Images?: boolean + blockAds?: boolean + proxy?: 'basic' | 'stealth' | 'auto' + storeInCache?: boolean +} + export interface ScrapeParams { apiKey: string url: string - scrapeOptions?: { - onlyMainContent?: boolean - formats?: string[] - } + scrapeOptions?: ScrapeOptions + // Additional top-level scrape params + onlyMainContent?: boolean + formats?: string[] + includeTags?: string[] + excludeTags?: string[] + maxAge?: number + headers?: Record + waitFor?: number + mobile?: boolean + skipTlsVerification?: boolean + timeout?: number + parsers?: string[] + actions?: Array<{ + type: string + [key: string]: any + }> + location?: LocationConfig + removeBase64Images?: boolean + blockAds?: boolean + proxy?: 'basic' | 'stealth' | 'auto' + storeInCache?: boolean + zeroDataRetention?: boolean } export interface SearchParams { apiKey: string query: string + limit?: number + sources?: ('web' | 'images' | 'news')[] + categories?: ('github' | 'research' | 'pdf')[] + tbs?: string + location?: string + country?: string + timeout?: number + ignoreInvalidURLs?: boolean + scrapeOptions?: ScrapeOptions } export interface FirecrawlCrawlParams { @@ -19,6 +76,50 @@ export interface FirecrawlCrawlParams { url: string limit?: number onlyMainContent?: boolean + prompt?: string + maxDiscoveryDepth?: number + sitemap?: 'skip' | 'include' + crawlEntireDomain?: boolean + allowExternalLinks?: boolean + allowSubdomains?: boolean + ignoreQueryParameters?: boolean + delay?: number + maxConcurrency?: number + excludePaths?: string[] + includePaths?: string[] + webhook?: { + url: string + headers?: Record + metadata?: Record + events?: ('completed' | 'page' | 'failed' | 'started')[] + } + scrapeOptions?: ScrapeOptions + zeroDataRetention?: boolean +} + +export interface MapParams { + apiKey: string + url: string + search?: string + sitemap?: 'skip' | 'include' | 'only' + includeSubdomains?: boolean + ignoreQueryParameters?: boolean + limit?: number + timeout?: number + location?: LocationConfig +} + +export interface ExtractParams { + apiKey: string + urls: string[] + prompt?: string + schema?: Record + enableWebSearch?: boolean + ignoreSitemap?: boolean + includeSubdomains?: boolean + showSources?: boolean + ignoreInvalidURLs?: boolean + scrapeOptions?: ScrapeOptions } export interface ScrapeResponse extends ToolResponse { @@ -85,4 +186,28 @@ export interface FirecrawlCrawlResponse extends ToolResponse { } } -export type FirecrawlResponse = ScrapeResponse | SearchResponse | FirecrawlCrawlResponse +export interface MapResponse extends ToolResponse { + output: { + success: boolean + links: string[] + } +} + +export interface ExtractResponse extends ToolResponse { + output: { + success: boolean + data: Record + sources?: Array<{ + url: string + title?: string + }> + warning?: string + } +} + +export type FirecrawlResponse = + | ScrapeResponse + | SearchResponse + | FirecrawlCrawlResponse + | MapResponse + | ExtractResponse diff --git a/apps/sim/tools/github/get_pr_files.ts b/apps/sim/tools/github/get_pr_files.ts index e0fab19e24..b0409433b8 100644 --- a/apps/sim/tools/github/get_pr_files.ts +++ b/apps/sim/tools/github/get_pr_files.ts @@ -53,8 +53,8 @@ export const getPRFilesTool: ToolConfig = const url = new URL( `https://api.github.com/repos/${params.owner}/${params.repo}/pulls/${params.pullNumber}/files` ) - if (params.per_page) url.searchParams.append('per_page', params.per_page.toString()) - if (params.page) url.searchParams.append('page', params.page.toString()) + if (params.per_page) url.searchParams.append('per_page', Number(params.per_page).toString()) + if (params.page) url.searchParams.append('page', Number(params.page).toString()) return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/github/list_branches.ts b/apps/sim/tools/github/list_branches.ts index a5ef3ef538..fa35c39b28 100644 --- a/apps/sim/tools/github/list_branches.ts +++ b/apps/sim/tools/github/list_branches.ts @@ -56,10 +56,10 @@ export const listBranchesTool: ToolConfig = if (params.labels) url.searchParams.append('labels', params.labels) if (params.sort) url.searchParams.append('sort', params.sort) if (params.direction) url.searchParams.append('direction', params.direction) - if (params.per_page) url.searchParams.append('per_page', params.per_page.toString()) - if (params.page) url.searchParams.append('page', params.page.toString()) + if (params.per_page) url.searchParams.append('per_page', Number(params.per_page).toString()) + if (params.page) url.searchParams.append('page', Number(params.page).toString()) return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/github/list_pr_comments.ts b/apps/sim/tools/github/list_pr_comments.ts index 8767daae0f..0d0808021b 100644 --- a/apps/sim/tools/github/list_pr_comments.ts +++ b/apps/sim/tools/github/list_pr_comments.ts @@ -76,8 +76,8 @@ export const listPRCommentsTool: ToolConfig = { if (params.base) url.searchParams.append('base', params.base) if (params.sort) url.searchParams.append('sort', params.sort) if (params.direction) url.searchParams.append('direction', params.direction) - if (params.per_page) url.searchParams.append('per_page', params.per_page.toString()) - if (params.page) url.searchParams.append('page', params.page.toString()) + if (params.per_page) url.searchParams.append('per_page', Number(params.per_page).toString()) + if (params.page) url.searchParams.append('page', Number(params.page).toString()) return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/github/list_releases.ts b/apps/sim/tools/github/list_releases.ts index 3e9b0889ae..e1048cf0b7 100644 --- a/apps/sim/tools/github/list_releases.ts +++ b/apps/sim/tools/github/list_releases.ts @@ -47,10 +47,10 @@ export const listReleasesTool: ToolConfig { const url = new URL(`https://api.github.com/repos/${params.owner}/${params.repo}/releases`) if (params.per_page) { - url.searchParams.append('per_page', params.per_page.toString()) + url.searchParams.append('per_page', Number(params.per_page).toString()) } if (params.page) { - url.searchParams.append('page', params.page.toString()) + url.searchParams.append('page', Number(params.page).toString()) } return url.toString() }, diff --git a/apps/sim/tools/github/list_workflow_runs.ts b/apps/sim/tools/github/list_workflow_runs.ts index 28057bd7c0..94abda4087 100644 --- a/apps/sim/tools/github/list_workflow_runs.ts +++ b/apps/sim/tools/github/list_workflow_runs.ts @@ -85,10 +85,10 @@ export const listWorkflowRunsTool: ToolConfig = { } // Set max results (default to 1 for simplicity, max 10) - const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1 + const maxResults = params.maxResults ? Math.min(Number(params.maxResults), 10) : 1 url.searchParams.append('maxResults', maxResults.toString()) return url.toString() @@ -135,7 +135,7 @@ export const gmailReadTool: ToolConfig = { // For agentic workflows, we'll fetch the first message by default // If maxResults > 1, we'll return a summary of messages found - const maxResults = params?.maxResults ? Math.min(params.maxResults, 10) : 1 + const maxResults = params?.maxResults ? Math.min(Number(params.maxResults), 10) : 1 if (maxResults === 1) { try { diff --git a/apps/sim/tools/gmail/search.ts b/apps/sim/tools/gmail/search.ts index b9f3d91ec9..2d93f80c50 100644 --- a/apps/sim/tools/gmail/search.ts +++ b/apps/sim/tools/gmail/search.ts @@ -47,7 +47,7 @@ export const gmailSearchTool: ToolConfig = const searchParams = new URLSearchParams() searchParams.append('q', params.query) if (params.maxResults) { - searchParams.append('maxResults', params.maxResults.toString()) + searchParams.append('maxResults', Number(params.maxResults).toString()) } return `${GMAIL_API_BASE}/messages?${searchParams.toString()}` }, diff --git a/apps/sim/tools/google_drive/list.ts b/apps/sim/tools/google_drive/list.ts index 9fa24c6efe..5c5e14b481 100644 --- a/apps/sim/tools/google_drive/list.ts +++ b/apps/sim/tools/google_drive/list.ts @@ -83,7 +83,7 @@ export const listTool: ToolConfig = { url: (params: GoogleFormsGetResponsesParams) => params.responseId ? buildGetResponseUrl({ formId: params.formId, responseId: params.responseId }) - : buildListResponsesUrl({ formId: params.formId, pageSize: params.pageSize }), + : buildListResponsesUrl({ + formId: params.formId, + pageSize: params.pageSize ? Number(params.pageSize) : undefined, + }), method: 'GET', headers: (params: GoogleFormsGetResponsesParams) => ({ Authorization: `Bearer ${params.accessToken}`, diff --git a/apps/sim/tools/hunter/domain_search.ts b/apps/sim/tools/hunter/domain_search.ts index 92aa6af190..73f12922a0 100644 --- a/apps/sim/tools/hunter/domain_search.ts +++ b/apps/sim/tools/hunter/domain_search.ts @@ -58,8 +58,8 @@ export const domainSearchTool: ToolConfig = { type: 'boolean', required: false, visibility: 'user-only', - description: 'Whether to use ReaderLM-v2 for better quality', + description: 'Whether to use ReaderLM-v2 for better quality (3x token cost)', }, gatherLinks: { type: 'boolean', @@ -39,6 +39,142 @@ export const readUrlTool: ToolConfig = { visibility: 'user-only', description: 'Your Jina AI API key', }, + // Content extraction params + targetSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'CSS selector to target specific page elements (e.g., "#main-content")', + }, + waitForSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'CSS selector to wait for before extracting content (useful for dynamic pages)', + }, + removeSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'CSS selector for elements to exclude (e.g., "header, footer, .ad")', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum seconds to wait for page load', + }, + withImagesummary: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Gather all images from the page with metadata', + }, + retainImages: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Control image inclusion: "none" removes all, "all" keeps all', + }, + returnFormat: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Output format: markdown, html, text, screenshot, or pageshot', + }, + withIframe: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include iframe content in extraction', + }, + withShadowDom: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Extract Shadow DOM content', + }, + // Authentication & proxy + setCookie: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Forward authentication cookies (disables caching)', + }, + proxyUrl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'HTTP proxy URL for request routing', + }, + proxy: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Country code for proxy (e.g., "US", "UK") or "auto"/"none"', + }, + // Performance & caching + engine: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Rendering engine: browser, direct, or cf-browser-rendering', + }, + tokenBudget: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum tokens for the request (cost control)', + }, + noCache: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Bypass cached content for real-time retrieval', + }, + cacheTolerance: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Custom cache lifetime in seconds', + }, + // Advanced options + withGeneratedAlt: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Generate alt text for images using VLM', + }, + baseUrl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Set to "final" to follow redirect chain', + }, + locale: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Browser locale for rendering (e.g., "en-US")', + }, + robotsTxt: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot User-Agent for robots.txt checking', + }, + dnt: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Do Not Track - prevents caching/tracking', + }, + noGfm: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Disable GitHub Flavored Markdown', + }, }, request: { @@ -53,7 +189,7 @@ export const readUrlTool: ToolConfig = { Authorization: `Bearer ${params.apiKey}`, } - // Add conditional headers based on boolean values + // Legacy params (backward compatible) if (params.useReaderLMv2 === true) { headers['X-Respond-With'] = 'readerlm-v2' } @@ -61,11 +197,99 @@ export const readUrlTool: ToolConfig = { headers['X-With-Links-Summary'] = 'true' } + // Content extraction headers + if (params.targetSelector) { + headers['X-Target-Selector'] = params.targetSelector + } + if (params.waitForSelector) { + headers['X-Wait-For-Selector'] = params.waitForSelector + } + if (params.removeSelector) { + headers['X-Remove-Selector'] = params.removeSelector + } + if (params.timeout) { + headers['X-Timeout'] = Number(params.timeout).toString() + } + if (params.withImagesummary === true) { + headers['X-With-Images-Summary'] = 'true' + } + if (params.retainImages) { + headers['X-Retain-Images'] = params.retainImages + } + if (params.returnFormat) { + headers['X-Return-Format'] = params.returnFormat + } + if (params.withIframe === true) { + headers['X-With-Iframe'] = 'true' + } + if (params.withShadowDom === true) { + headers['X-With-Shadow-Dom'] = 'true' + } + + // Authentication & proxy headers + if (params.setCookie) { + headers['X-Set-Cookie'] = params.setCookie + } + if (params.proxyUrl) { + headers['X-Proxy-Url'] = params.proxyUrl + } + if (params.proxy) { + headers['X-Proxy'] = params.proxy + } + + // Performance & caching headers + if (params.engine) { + headers['X-Engine'] = params.engine + } + if (params.tokenBudget) { + headers['X-Token-Budget'] = Number(params.tokenBudget).toString() + } + if (params.noCache === true) { + headers['X-No-Cache'] = 'true' + } + if (params.cacheTolerance) { + headers['X-Cache-Tolerance'] = Number(params.cacheTolerance).toString() + } + + // Advanced options + if (params.withGeneratedAlt === true) { + headers['X-With-Generated-Alt'] = 'true' + } + if (params.baseUrl) { + headers['X-Base'] = params.baseUrl + } + if (params.locale) { + headers['X-Locale'] = params.locale + } + if (params.robotsTxt) { + headers['X-Robots-Txt'] = params.robotsTxt + } + if (params.dnt === true) { + headers.DNT = '1' + } + if (params.noGfm === true) { + headers['X-No-Gfm'] = 'true' + } + return headers }, }, transformResponse: async (response: Response) => { + const contentType = response.headers.get('content-type') + + if (contentType?.includes('application/json')) { + const data = await response.json() + return { + success: response.ok, + output: { + content: data.data?.content || data.content || JSON.stringify(data), + links: data.data?.links || undefined, + images: data.data?.images || undefined, + }, + } + } + const content = await response.text() return { success: response.ok, @@ -80,5 +304,14 @@ export const readUrlTool: ToolConfig = { type: 'string', description: 'The extracted content from the URL, processed into clean, LLM-friendly text', }, + links: { + type: 'array', + description: + 'List of links found on the page (when gatherLinks or withLinksummary is enabled)', + }, + images: { + type: 'array', + description: 'List of images found on the page (when withImagesummary is enabled)', + }, }, } diff --git a/apps/sim/tools/jina/search.ts b/apps/sim/tools/jina/search.ts new file mode 100644 index 0000000000..89d6ef1443 --- /dev/null +++ b/apps/sim/tools/jina/search.ts @@ -0,0 +1,259 @@ +import type { SearchParams, SearchResponse } from '@/tools/jina/types' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'jina_search', + name: 'Jina Search', + description: + 'Search the web and return top 5 results with LLM-friendly content. Each result is automatically processed through Jina Reader API. Supports geographic filtering, site restrictions, and pagination.', + version: '1.0.0', + + params: { + q: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query string', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jina AI API key', + }, + // Geographic & language params + gl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Two-letter country code for geo-specific results (e.g., "US", "UK", "JP")', + }, + location: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'City-level location for localized search results', + }, + hl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Two-letter language code for results (e.g., "en", "es", "fr")', + }, + // Pagination + num: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results per page (default: 5)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination (offset)', + }, + // Site restriction + site: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Restrict results to specific domain(s). Can be comma-separated for multiple sites (e.g., "jina.ai,github.com")', + }, + // Content options + withFavicon: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include website favicons in results', + }, + withImagesummary: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Gather all images from result pages with metadata', + }, + withLinksummary: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Gather all links from result pages', + }, + retainImages: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Control image inclusion: "none" removes all, "all" keeps all', + }, + noCache: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Bypass cached content for real-time retrieval', + }, + withGeneratedAlt: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Generate alt text for images using VLM', + }, + respondWith: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Set to "no-content" to get only metadata without page content', + }, + returnFormat: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Output format: markdown, html, text, screenshot, or pageshot', + }, + engine: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Rendering engine: browser or direct', + }, + timeout: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum seconds to wait for page load', + }, + setCookie: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Forward authentication cookies', + }, + proxyUrl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'HTTP proxy URL for request routing', + }, + locale: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Browser locale for rendering (e.g., "en-US")', + }, + }, + + request: { + url: (params: SearchParams) => { + const baseUrl = 'https://s.jina.ai/' + const query = encodeURIComponent(params.q) + + // Build query params + const queryParams: string[] = [] + + // Handle site parameter (can be string or array) + if (params.site) { + const sites = typeof params.site === 'string' ? params.site.split(',') : params.site + sites.forEach((s) => queryParams.push(`site=${encodeURIComponent(s.trim())}`)) + } + + const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '' + return `${baseUrl}${query}${queryString}` + }, + method: 'GET', + headers: (params: SearchParams) => { + const headers: Record = { + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + + // Content options + if (params.withFavicon === true) { + headers['X-With-Favicon'] = 'true' + } + if (params.withImagesummary === true) { + headers['X-With-Images-Summary'] = 'true' + } + if (params.withLinksummary === true) { + headers['X-With-Links-Summary'] = 'true' + } + if (params.retainImages) { + headers['X-Retain-Images'] = params.retainImages + } + if (params.noCache === true) { + headers['X-No-Cache'] = 'true' + } + if (params.withGeneratedAlt === true) { + headers['X-With-Generated-Alt'] = 'true' + } + if (params.respondWith) { + headers['X-Respond-With'] = params.respondWith + } + if (params.returnFormat) { + headers['X-Return-Format'] = params.returnFormat + } + if (params.engine) { + headers['X-Engine'] = params.engine + } + if (params.timeout) { + headers['X-Timeout'] = Number(params.timeout).toString() + } + if (params.setCookie) { + headers['X-Set-Cookie'] = params.setCookie + } + if (params.proxyUrl) { + headers['X-Proxy-Url'] = params.proxyUrl + } + if (params.locale) { + headers['X-Locale'] = params.locale + } + + // Geographic & language headers + if (params.gl) { + headers['X-Gl'] = params.gl + } + if (params.location) { + headers['X-Location'] = params.location + } + if (params.hl) { + headers['X-Hl'] = params.hl + } + + // Pagination headers + if (params.num) { + headers['X-Num'] = Number(params.num).toString() + } + if (params.page) { + headers['X-Page'] = Number(params.page).toString() + } + + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + // The API returns an array of results or a data object with results + const results = Array.isArray(data) ? data : data.data || [] + + return { + success: response.ok, + output: { + results: results.map((result: any) => ({ + title: result.title || '', + description: result.description || '', + url: result.url || '', + content: result.content || '', + })), + }, + } + }, + + outputs: { + results: { + type: 'array', + description: + 'Array of search results, each containing title, description, url, and LLM-friendly content', + }, + }, +} diff --git a/apps/sim/tools/jina/types.ts b/apps/sim/tools/jina/types.ts index a76330e171..2747546673 100644 --- a/apps/sim/tools/jina/types.ts +++ b/apps/sim/tools/jina/types.ts @@ -2,14 +2,84 @@ import type { ToolResponse } from '@/tools/types' export interface ReadUrlParams { url: string + // Existing params (backward compatible) useReaderLMv2?: boolean gatherLinks?: boolean jsonResponse?: boolean apiKey?: string + // New content extraction params + targetSelector?: string + waitForSelector?: string + removeSelector?: string + timeout?: number + withImagesummary?: boolean + retainImages?: 'none' | 'all' + returnFormat?: 'markdown' | 'html' | 'text' | 'screenshot' | 'pageshot' + withIframe?: boolean + withShadowDom?: boolean + // Authentication & proxy + setCookie?: string + proxyUrl?: string + proxy?: string + // Performance & caching + engine?: 'browser' | 'direct' | 'cf-browser-rendering' + tokenBudget?: number + noCache?: boolean + cacheTolerance?: number + // Advanced options + withGeneratedAlt?: boolean + baseUrl?: 'final' + locale?: string + robotsTxt?: string + dnt?: boolean + noGfm?: boolean } export interface ReadUrlResponse extends ToolResponse { output: { content: string + links?: string[] + images?: string[] + } +} + +export interface SearchParams { + q: string + apiKey?: string + // Geographic & language params + gl?: string + location?: string + hl?: string + // Pagination + num?: number + page?: number + // Site restriction + site?: string | string[] + // Content options + withFavicon?: boolean + withImagesummary?: boolean + withLinksummary?: boolean + retainImages?: 'none' | 'all' + noCache?: boolean + withGeneratedAlt?: boolean + respondWith?: 'no-content' + returnFormat?: 'markdown' | 'html' | 'text' | 'screenshot' | 'pageshot' + engine?: 'browser' | 'direct' + timeout?: number + setCookie?: string + proxyUrl?: string + locale?: string +} + +export interface SearchResult { + title: string + description: string + url: string + content: string +} + +export interface SearchResponse extends ToolResponse { + output: { + results: SearchResult[] } } diff --git a/apps/sim/tools/jira/add_comment.ts b/apps/sim/tools/jira/add_comment.ts new file mode 100644 index 0000000000..cfb658cafb --- /dev/null +++ b/apps/sim/tools/jira/add_comment.ts @@ -0,0 +1,176 @@ +import type { JiraAddCommentParams, JiraAddCommentResponse } from '@/tools/jira/types' +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +export const jiraAddCommentTool: ToolConfig = { + id: 'jira_add_comment', + name: 'Jira Add Comment', + description: 'Add a comment to a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:comment:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to add comment to (e.g., PROJ-123)', + }, + body: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment body text', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraAddCommentParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'POST', + headers: (params: JiraAddCommentParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraAddCommentParams) => { + return { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params?.body || '', + }, + ], + }, + ], + }, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraAddCommentParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/comment` + const commentResponse = await fetch(commentUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + body: JSON.stringify({ + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params?.body || '', + }, + ], + }, + ], + }, + }), + }) + + if (!commentResponse.ok) { + let message = `Failed to add comment to Jira issue (${commentResponse.status})` + try { + const err = await commentResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await commentResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + commentId: data?.id || 'unknown', + body: params?.body || '', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to add comment to Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + commentId: data?.id || 'unknown', + body: params?.body || '', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Comment details with timestamp, issue key, comment ID, body, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/add_watcher.ts b/apps/sim/tools/jira/add_watcher.ts new file mode 100644 index 0000000000..5c522d00ae --- /dev/null +++ b/apps/sim/tools/jira/add_watcher.ts @@ -0,0 +1,154 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraAddWatcherParams { + accessToken: string + domain: string + issueKey: string + accountId: string + cloudId?: string +} + +export interface JiraAddWatcherResponse extends ToolResponse { + output: { + ts: string + issueKey: string + watcherAccountId: string + success: boolean + } +} + +export const jiraAddWatcherTool: ToolConfig = { + id: 'jira_add_watcher', + name: 'Jira Add Watcher', + description: 'Add a watcher to a Jira issue to receive notifications about updates', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to add watcher to (e.g., PROJ-123)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID of the user to add as watcher', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraAddWatcherParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/watchers` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'POST', + headers: (params: JiraAddWatcherParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraAddWatcherParams) => { + return { accountId: params.accountId } + }, + }, + + transformResponse: async (response: Response, params?: JiraAddWatcherParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/watchers` + const watcherResponse = await fetch(watcherUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + body: JSON.stringify({ accountId: params?.accountId }), + }) + + if (!watcherResponse.ok) { + let message = `Failed to add watcher to Jira issue (${watcherResponse.status})` + try { + const err = await watcherResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + watcherAccountId: params?.accountId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to add watcher to Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + watcherAccountId: params?.accountId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Watcher details with timestamp, issue key, watcher account ID, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/add_worklog.ts b/apps/sim/tools/jira/add_worklog.ts new file mode 100644 index 0000000000..f593155ee6 --- /dev/null +++ b/apps/sim/tools/jira/add_worklog.ts @@ -0,0 +1,196 @@ +import type { JiraAddWorklogParams, JiraAddWorklogResponse } from '@/tools/jira/types' +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +export const jiraAddWorklogTool: ToolConfig = { + id: 'jira_add_worklog', + name: 'Jira Add Worklog', + description: 'Add a time tracking worklog entry to a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:issue-worklog:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to add worklog to (e.g., PROJ-123)', + }, + timeSpentSeconds: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Time spent in seconds', + }, + comment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional comment for the worklog entry', + }, + started: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional start time in ISO format (defaults to current time)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraAddWorklogParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'POST', + headers: (params: JiraAddWorklogParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraAddWorklogParams) => { + return { + timeSpentSeconds: Number(params.timeSpentSeconds), + comment: params.comment + ? { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + } + : undefined, + started: params.started || new Date().toISOString().replace(/\.\d{3}Z$/, '+0000'), + } + }, + }, + + transformResponse: async (response: Response, params?: JiraAddWorklogParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/worklog` + const worklogResponse = await fetch(worklogUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + body: JSON.stringify({ + timeSpentSeconds: params?.timeSpentSeconds ? Number(params.timeSpentSeconds) : 0, + comment: params?.comment + ? { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + } + : undefined, + started: params?.started || new Date().toISOString().replace(/\.\d{3}Z$/, '+0000'), + }), + }) + + if (!worklogResponse.ok) { + let message = `Failed to add worklog to Jira issue (${worklogResponse.status})` + try { + const err = await worklogResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await worklogResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + worklogId: data?.id || 'unknown', + timeSpentSeconds: params?.timeSpentSeconds ? Number(params.timeSpentSeconds) : 0 || 0, + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to add worklog to Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + worklogId: data?.id || 'unknown', + timeSpentSeconds: params?.timeSpentSeconds ? Number(params.timeSpentSeconds) : 0 || 0, + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Worklog details with timestamp, issue key, worklog ID, time spent in seconds, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/assign_issue.ts b/apps/sim/tools/jira/assign_issue.ts new file mode 100644 index 0000000000..98715871c1 --- /dev/null +++ b/apps/sim/tools/jira/assign_issue.ts @@ -0,0 +1,156 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraAssignIssueParams { + accessToken: string + domain: string + issueKey: string + accountId: string + cloudId?: string +} + +export interface JiraAssignIssueResponse extends ToolResponse { + output: { + ts: string + issueKey: string + assigneeId: string + success: boolean + } +} + +export const jiraAssignIssueTool: ToolConfig = { + id: 'jira_assign_issue', + name: 'Jira Assign Issue', + description: 'Assign a Jira issue to a user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to assign (e.g., PROJ-123)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Account ID of the user to assign the issue to. Use "-1" for automatic assignment or null to unassign.', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraAssignIssueParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/assignee` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'PUT', + headers: (params: JiraAssignIssueParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraAssignIssueParams) => { + return { + accountId: params.accountId === 'null' ? null : params.accountId, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraAssignIssueParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/assignee` + const assignResponse = await fetch(assignUrl, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + body: JSON.stringify({ + accountId: params!.accountId === 'null' ? null : params!.accountId, + }), + }) + + if (!assignResponse.ok) { + let message = `Failed to assign Jira issue (${assignResponse.status})` + try { + const err = await assignResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + assigneeId: params?.accountId || 'unknown', + success: true, + }, + } + } + + if (!response.ok) { + let message = `Failed to assign Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + assigneeId: params?.accountId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Assignment details with timestamp, issue key, assignee ID, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/create_issue_link.ts b/apps/sim/tools/jira/create_issue_link.ts new file mode 100644 index 0000000000..a5b3424944 --- /dev/null +++ b/apps/sim/tools/jira/create_issue_link.ts @@ -0,0 +1,232 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraCreateIssueLinkParams { + accessToken: string + domain: string + inwardIssueKey: string + outwardIssueKey: string + linkType: string + comment?: string + cloudId?: string +} + +export interface JiraCreateIssueLinkResponse extends ToolResponse { + output: { + ts: string + inwardIssue: string + outwardIssue: string + linkType: string + success: boolean + } +} + +export const jiraCreateIssueLinkTool: ToolConfig< + JiraCreateIssueLinkParams, + JiraCreateIssueLinkResponse +> = { + id: 'jira_create_issue_link', + name: 'Jira Create Issue Link', + description: 'Create a link relationship between two Jira issues', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:issue-link:jira', 'read:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + inwardIssueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key for the inward issue (e.g., PROJ-123)', + }, + outwardIssueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key for the outward issue (e.g., PROJ-456)', + }, + linkType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The type of link relationship (e.g., "Blocks", "Relates to", "Duplicates")', + }, + comment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional comment to add to the issue link', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraCreateIssueLinkParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issueLink` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'POST', + headers: (params: JiraCreateIssueLinkParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraCreateIssueLinkParams) => { + return { + type: { + name: params?.linkType, + }, + inwardIssue: { + key: params?.inwardIssueKey, + }, + outwardIssue: { + key: params?.outwardIssueKey, + }, + comment: params?.comment + ? { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + }, + } + : undefined, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraCreateIssueLinkParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const linkUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issueLink` + const linkResponse = await fetch(linkUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + body: JSON.stringify({ + type: { + name: params?.linkType, + }, + inwardIssue: { + key: params?.inwardIssueKey, + }, + outwardIssue: { + key: params?.outwardIssueKey, + }, + comment: params?.comment + ? { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + }, + } + : undefined, + }), + }) + + if (!linkResponse.ok) { + let message = `Failed to create issue link (${linkResponse.status})` + try { + const err = await linkResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + inwardIssue: params?.inwardIssueKey || 'unknown', + outwardIssue: params?.outwardIssueKey || 'unknown', + linkType: params?.linkType || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to create issue link (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + inwardIssue: params?.inwardIssueKey || 'unknown', + outwardIssue: params?.outwardIssueKey || 'unknown', + linkType: params?.linkType || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Issue link details with timestamp, inward issue key, outward issue key, link type, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/delete_attachment.ts b/apps/sim/tools/jira/delete_attachment.ts new file mode 100644 index 0000000000..c5c3bc98a4 --- /dev/null +++ b/apps/sim/tools/jira/delete_attachment.ts @@ -0,0 +1,140 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraDeleteAttachmentParams { + accessToken: string + domain: string + attachmentId: string + cloudId?: string +} + +export interface JiraDeleteAttachmentResponse extends ToolResponse { + output: { + ts: string + attachmentId: string + success: boolean + } +} + +export const jiraDeleteAttachmentTool: ToolConfig< + JiraDeleteAttachmentParams, + JiraDeleteAttachmentResponse +> = { + id: 'jira_delete_attachment', + name: 'Jira Delete Attachment', + description: 'Delete an attachment from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['delete:attachment:jira', 'read:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + attachmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the attachment to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraDeleteAttachmentParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/attachment/${params.attachmentId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'DELETE', + headers: (params: JiraDeleteAttachmentParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraDeleteAttachmentParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const attachmentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/attachment/${params?.attachmentId}` + const attachmentResponse = await fetch(attachmentUrl, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!attachmentResponse.ok) { + let message = `Failed to delete attachment from Jira issue (${attachmentResponse.status})` + try { + const err = await attachmentResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + attachmentId: params?.attachmentId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to delete attachment from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + attachmentId: params?.attachmentId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Deletion details with timestamp, attachment ID, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/delete_comment.ts b/apps/sim/tools/jira/delete_comment.ts new file mode 100644 index 0000000000..e4e114fe29 --- /dev/null +++ b/apps/sim/tools/jira/delete_comment.ts @@ -0,0 +1,148 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraDeleteCommentParams { + accessToken: string + domain: string + issueKey: string + commentId: string + cloudId?: string +} + +export interface JiraDeleteCommentResponse extends ToolResponse { + output: { + ts: string + issueKey: string + commentId: string + success: boolean + } +} + +export const jiraDeleteCommentTool: ToolConfig = + { + id: 'jira_delete_comment', + name: 'Jira Delete Comment', + description: 'Delete a comment from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['delete:comment:jira', 'read:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key containing the comment (e.g., PROJ-123)', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the comment to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraDeleteCommentParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment/${params.commentId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'DELETE', + headers: (params: JiraDeleteCommentParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraDeleteCommentParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/comment/${params?.commentId}` + const commentResponse = await fetch(commentUrl, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!commentResponse.ok) { + let message = `Failed to delete comment from Jira issue (${commentResponse.status})` + try { + const err = await commentResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + commentId: params?.commentId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to delete comment from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + commentId: params?.commentId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Deletion details with timestamp, issue key, comment ID, and success status', + }, + }, + } diff --git a/apps/sim/tools/jira/delete_issue.ts b/apps/sim/tools/jira/delete_issue.ts new file mode 100644 index 0000000000..580de04477 --- /dev/null +++ b/apps/sim/tools/jira/delete_issue.ts @@ -0,0 +1,145 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraDeleteIssueParams { + accessToken: string + domain: string + issueKey: string + cloudId?: string + deleteSubtasks?: boolean +} + +export interface JiraDeleteIssueResponse extends ToolResponse { + output: { + ts: string + issueKey: string + success: boolean + } +} + +export const jiraDeleteIssueTool: ToolConfig = { + id: 'jira_delete_issue', + name: 'Jira Delete Issue', + description: 'Delete a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['delete:issue:jira', 'read:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to delete (e.g., PROJ-123)', + }, + deleteSubtasks: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether to delete subtasks. If false, parent issues with subtasks cannot be deleted.', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraDeleteIssueParams) => { + if (params.cloudId) { + const deleteSubtasksParam = params.deleteSubtasks ? '?deleteSubtasks=true' : '' + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}${deleteSubtasksParam}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'DELETE', + headers: (params: JiraDeleteIssueParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraDeleteIssueParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + const deleteSubtasksParam = params!.deleteSubtasks ? '?deleteSubtasks=true' : '' + const deleteUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}${deleteSubtasksParam}` + const deleteResponse = await fetch(deleteUrl, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + }) + + if (!deleteResponse.ok) { + let message = `Failed to delete Jira issue (${deleteResponse.status})` + try { + const err = await deleteResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + success: true, + }, + } + } + + if (!response.ok) { + let message = `Failed to delete Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Deleted issue details with timestamp, issue key, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/delete_issue_link.ts b/apps/sim/tools/jira/delete_issue_link.ts new file mode 100644 index 0000000000..4501f46827 --- /dev/null +++ b/apps/sim/tools/jira/delete_issue_link.ts @@ -0,0 +1,140 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraDeleteIssueLinkParams { + accessToken: string + domain: string + linkId: string + cloudId?: string +} + +export interface JiraDeleteIssueLinkResponse extends ToolResponse { + output: { + ts: string + linkId: string + success: boolean + } +} + +export const jiraDeleteIssueLinkTool: ToolConfig< + JiraDeleteIssueLinkParams, + JiraDeleteIssueLinkResponse +> = { + id: 'jira_delete_issue_link', + name: 'Jira Delete Issue Link', + description: 'Delete a link between two Jira issues', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['delete:issue-link:jira', 'read:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + linkId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the issue link to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraDeleteIssueLinkParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issueLink/${params.linkId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'DELETE', + headers: (params: JiraDeleteIssueLinkParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraDeleteIssueLinkParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const issueLinkUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issueLink/${params?.linkId}` + const issueLinkResponse = await fetch(issueLinkUrl, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!issueLinkResponse.ok) { + let message = `Failed to delete issue link (${issueLinkResponse.status})` + try { + const err = await issueLinkResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + linkId: params?.linkId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to delete issue link (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + linkId: params?.linkId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Deletion details with timestamp, link ID, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/delete_worklog.ts b/apps/sim/tools/jira/delete_worklog.ts new file mode 100644 index 0000000000..aead0e8bc4 --- /dev/null +++ b/apps/sim/tools/jira/delete_worklog.ts @@ -0,0 +1,148 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraDeleteWorklogParams { + accessToken: string + domain: string + issueKey: string + worklogId: string + cloudId?: string +} + +export interface JiraDeleteWorklogResponse extends ToolResponse { + output: { + ts: string + issueKey: string + worklogId: string + success: boolean + } +} + +export const jiraDeleteWorklogTool: ToolConfig = + { + id: 'jira_delete_worklog', + name: 'Jira Delete Worklog', + description: 'Delete a worklog entry from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['delete:issue-worklog:jira', 'read:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key containing the worklog (e.g., PROJ-123)', + }, + worklogId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the worklog entry to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraDeleteWorklogParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog/${params.worklogId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'DELETE', + headers: (params: JiraDeleteWorklogParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraDeleteWorklogParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/worklog/${params?.worklogId}` + const worklogResponse = await fetch(worklogUrl, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!worklogResponse.ok) { + let message = `Failed to delete worklog from Jira issue (${worklogResponse.status})` + try { + const err = await worklogResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + worklogId: params?.worklogId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to delete worklog from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + worklogId: params?.worklogId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Deletion details with timestamp, issue key, worklog ID, and success status', + }, + }, + } diff --git a/apps/sim/tools/jira/get_attachments.ts b/apps/sim/tools/jira/get_attachments.ts new file mode 100644 index 0000000000..3a5043448d --- /dev/null +++ b/apps/sim/tools/jira/get_attachments.ts @@ -0,0 +1,144 @@ +import type { JiraGetAttachmentsParams, JiraGetAttachmentsResponse } from '@/tools/jira/types' +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +export const jiraGetAttachmentsTool: ToolConfig< + JiraGetAttachmentsParams, + JiraGetAttachmentsResponse +> = { + id: 'jira_get_attachments', + name: 'Jira Get Attachments', + description: 'Get all attachments from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['read:attachment:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to get attachments from (e.g., PROJ-123)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraGetAttachmentsParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}?fields=attachment` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraGetAttachmentsParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraGetAttachmentsParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const attachmentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}?fields=attachment` + const attachmentsResponse = await fetch(attachmentsUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!attachmentsResponse.ok) { + let message = `Failed to get attachments from Jira issue (${attachmentsResponse.status})` + try { + const err = await attachmentsResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await attachmentsResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + attachments: (data?.fields?.attachment || []).map((att: any) => ({ + id: att.id, + filename: att.filename, + size: att.size, + mimeType: att.mimeType, + created: att.created, + author: att.author?.displayName || att.author?.accountId || 'Unknown', + })), + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to get attachments from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + attachments: (data?.fields?.attachment || []).map((att: any) => ({ + id: att.id, + filename: att.filename, + size: att.size, + mimeType: att.mimeType, + created: att.created, + author: att.author?.displayName || att.author?.accountId || 'Unknown', + })), + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Attachments data with timestamp, issue key, and array of attachments', + }, + }, +} diff --git a/apps/sim/tools/jira/get_comments.ts b/apps/sim/tools/jira/get_comments.ts new file mode 100644 index 0000000000..838540dff8 --- /dev/null +++ b/apps/sim/tools/jira/get_comments.ts @@ -0,0 +1,192 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraGetCommentsParams { + accessToken: string + domain: string + issueKey: string + startAt?: number + maxResults?: number + cloudId?: string +} + +export interface JiraGetCommentsResponse extends ToolResponse { + output: { + ts: string + issueKey: string + total: number + comments: Array<{ + id: string + author: string + body: string + created: string + updated: string + }> + } +} + +export const jiraGetCommentsTool: ToolConfig = { + id: 'jira_get_comments', + name: 'Jira Get Comments', + description: 'Get all comments from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['read:comment:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to get comments from (e.g., PROJ-123)', + }, + startAt: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Index of the first comment to return (default: 0)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of comments to return (default: 50)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraGetCommentsParams) => { + if (params.cloudId) { + const startAt = params.startAt || 0 + const maxResults = params.maxResults || 50 + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraGetCommentsParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraGetCommentsParams) => { + // Extract text from Atlassian Document Format + const extractText = (content: any): string => { + if (!content) return '' + if (typeof content === 'string') return content + if (Array.isArray(content)) { + return content.map(extractText).join(' ') + } + if (content.type === 'text') return content.text || '' + if (content.content) return extractText(content.content) + return '' + } + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const startAt = params?.startAt || 0 + const maxResults = params?.maxResults || 50 + const commentsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/comment?startAt=${startAt}&maxResults=${maxResults}` + const commentsResponse = await fetch(commentsUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!commentsResponse.ok) { + let message = `Failed to get comments from Jira issue (${commentsResponse.status})` + try { + const err = await commentsResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await commentsResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + total: data.total || 0, + comments: (data.comments || []).map((comment: any) => ({ + id: comment.id, + author: comment.author?.displayName || comment.author?.accountId || 'Unknown', + body: extractText(comment.body), + created: comment.created, + updated: comment.updated, + })), + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to get comments from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + total: data.total || 0, + comments: (data.comments || []).map((comment: any) => ({ + id: comment.id, + author: comment.author?.displayName || comment.author?.accountId || 'Unknown', + body: extractText(comment.body), + created: comment.created, + updated: comment.updated, + })), + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Comments data with timestamp, issue key, total count, and array of comments', + }, + }, +} diff --git a/apps/sim/tools/jira/get_worklogs.ts b/apps/sim/tools/jira/get_worklogs.ts new file mode 100644 index 0000000000..687e7b99eb --- /dev/null +++ b/apps/sim/tools/jira/get_worklogs.ts @@ -0,0 +1,201 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraGetWorklogsParams { + accessToken: string + domain: string + issueKey: string + startAt?: number + maxResults?: number + cloudId?: string +} + +export interface JiraGetWorklogsResponse extends ToolResponse { + output: { + ts: string + issueKey: string + total: number + worklogs: Array<{ + id: string + author: string + timeSpentSeconds: number + timeSpent: string + comment?: string + created: string + updated: string + started: string + }> + } +} + +export const jiraGetWorklogsTool: ToolConfig = { + id: 'jira_get_worklogs', + name: 'Jira Get Worklogs', + description: 'Get all worklog entries from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['read:issue-worklog:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to get worklogs from (e.g., PROJ-123)', + }, + startAt: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Index of the first worklog to return (default: 0)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of worklogs to return (default: 50)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraGetWorklogsParams) => { + if (params.cloudId) { + const startAt = params.startAt || 0 + const maxResults = params.maxResults || 50 + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog?startAt=${startAt}&maxResults=${maxResults}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'GET', + headers: (params: JiraGetWorklogsParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraGetWorklogsParams) => { + // Extract text from Atlassian Document Format + const extractText = (content: any): string => { + if (!content) return '' + if (typeof content === 'string') return content + if (Array.isArray(content)) { + return content.map(extractText).join(' ') + } + if (content.type === 'text') return content.text || '' + if (content.content) return extractText(content.content) + return '' + } + + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const startAt = params?.startAt || 0 + const maxResults = params?.maxResults || 50 + const worklogsUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/worklog?startAt=${startAt}&maxResults=${maxResults}` + const worklogsResponse = await fetch(worklogsUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!worklogsResponse.ok) { + let message = `Failed to get worklogs from Jira issue (${worklogsResponse.status})` + try { + const err = await worklogsResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await worklogsResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + total: data.total || 0, + worklogs: (data.worklogs || []).map((worklog: any) => ({ + id: worklog.id, + author: worklog.author?.displayName || worklog.author?.accountId || 'Unknown', + timeSpentSeconds: worklog.timeSpentSeconds, + timeSpent: worklog.timeSpent, + comment: worklog.comment ? extractText(worklog.comment) : undefined, + created: worklog.created, + updated: worklog.updated, + started: worklog.started, + })), + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to get worklogs from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + total: data.total || 0, + worklogs: (data.worklogs || []).map((worklog: any) => ({ + id: worklog.id, + author: worklog.author?.displayName || worklog.author?.accountId || 'Unknown', + timeSpentSeconds: worklog.timeSpentSeconds, + timeSpent: worklog.timeSpent, + comment: worklog.comment ? extractText(worklog.comment) : undefined, + created: worklog.created, + updated: worklog.updated, + started: worklog.started, + })), + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: 'Worklogs data with timestamp, issue key, total count, and array of worklogs', + }, + }, +} diff --git a/apps/sim/tools/jira/index.ts b/apps/sim/tools/jira/index.ts index 918f6be59c..6a9dd7f6b1 100644 --- a/apps/sim/tools/jira/index.ts +++ b/apps/sim/tools/jira/index.ts @@ -1,9 +1,66 @@ +// Core CRUD operations + +// Comment operations +import { jiraAddCommentTool } from '@/tools/jira/add_comment' +// Watcher operations +import { jiraAddWatcherTool } from '@/tools/jira/add_watcher' +// Worklog operations +import { jiraAddWorklogTool } from '@/tools/jira/add_worklog' +import { jiraAssignIssueTool } from '@/tools/jira/assign_issue' import { jiraBulkRetrieveTool } from '@/tools/jira/bulk_read' +// Issue link operations +import { jiraCreateIssueLinkTool } from '@/tools/jira/create_issue_link' +import { jiraDeleteAttachmentTool } from '@/tools/jira/delete_attachment' +import { jiraDeleteCommentTool } from '@/tools/jira/delete_comment' +// Issue operations +import { jiraDeleteIssueTool } from '@/tools/jira/delete_issue' +import { jiraDeleteIssueLinkTool } from '@/tools/jira/delete_issue_link' +import { jiraDeleteWorklogTool } from '@/tools/jira/delete_worklog' +// Attachment operations +import { jiraGetAttachmentsTool } from '@/tools/jira/get_attachments' +import { jiraGetCommentsTool } from '@/tools/jira/get_comments' +import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs' +import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher' import { jiraRetrieveTool } from '@/tools/jira/retrieve' +import { jiraSearchIssuesTool } from '@/tools/jira/search_issues' +import { jiraTransitionIssueTool } from '@/tools/jira/transition_issue' import { jiraUpdateTool } from '@/tools/jira/update' +import { jiraUpdateCommentTool } from '@/tools/jira/update_comment' +import { jiraUpdateWorklogTool } from '@/tools/jira/update_worklog' import { jiraWriteTool } from '@/tools/jira/write' +// Core CRUD operations export { jiraRetrieveTool } export { jiraUpdateTool } export { jiraWriteTool } export { jiraBulkRetrieveTool } + +// Issue operations +export { jiraDeleteIssueTool } +export { jiraAssignIssueTool } +export { jiraTransitionIssueTool } +export { jiraSearchIssuesTool } + +// Comment operations +export { jiraAddCommentTool } +export { jiraGetCommentsTool } +export { jiraUpdateCommentTool } +export { jiraDeleteCommentTool } + +// Attachment operations +export { jiraGetAttachmentsTool } +export { jiraDeleteAttachmentTool } + +// Worklog operations +export { jiraAddWorklogTool } +export { jiraGetWorklogsTool } +export { jiraUpdateWorklogTool } +export { jiraDeleteWorklogTool } + +// Issue link operations +export { jiraCreateIssueLinkTool } +export { jiraDeleteIssueLinkTool } + +// Watcher operations +export { jiraAddWatcherTool } +export { jiraRemoveWatcherTool } diff --git a/apps/sim/tools/jira/remove_watcher.ts b/apps/sim/tools/jira/remove_watcher.ts new file mode 100644 index 0000000000..6d7396e23c --- /dev/null +++ b/apps/sim/tools/jira/remove_watcher.ts @@ -0,0 +1,149 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export interface JiraRemoveWatcherParams { + accessToken: string + domain: string + issueKey: string + accountId: string + cloudId?: string +} + +export interface JiraRemoveWatcherResponse extends ToolResponse { + output: { + ts: string + issueKey: string + watcherAccountId: string + success: boolean + } +} + +export const jiraRemoveWatcherTool: ToolConfig = + { + id: 'jira_remove_watcher', + name: 'Jira Remove Watcher', + description: 'Remove a watcher from a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to remove watcher from (e.g., PROJ-123)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Account ID of the user to remove as watcher', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraRemoveWatcherParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/watchers?accountId=${params.accountId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'DELETE', + headers: (params: JiraRemoveWatcherParams) => { + return { + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraRemoveWatcherParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const watcherUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/watchers?accountId=${params?.accountId}` + const watcherResponse = await fetch(watcherUrl, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + }) + + if (!watcherResponse.ok) { + let message = `Failed to remove watcher from Jira issue (${watcherResponse.status})` + try { + const err = await watcherResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + watcherAccountId: params?.accountId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to remove watcher from Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + watcherAccountId: params?.accountId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Removal details with timestamp, issue key, watcher account ID, and success status', + }, + }, + } diff --git a/apps/sim/tools/jira/search_issues.ts b/apps/sim/tools/jira/search_issues.ts new file mode 100644 index 0000000000..588fba4a5f --- /dev/null +++ b/apps/sim/tools/jira/search_issues.ts @@ -0,0 +1,180 @@ +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' +import type { JiraSearchIssuesParams, JiraSearchIssuesResponse } from './types' + +export const jiraSearchIssuesTool: ToolConfig = { + id: 'jira_search_issues', + name: 'Jira Search Issues', + description: 'Search for Jira issues using JQL (Jira Query Language)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + jql: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JQL query string to search for issues (e.g., "project = PROJ AND status = Open")', + }, + startAt: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'The index of the first result to return (for pagination)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (default: 50)', + }, + fields: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: + "Array of field names to return (default: ['summary', 'status', 'assignee', 'created', 'updated'])", + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraSearchIssuesParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/search` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'POST', + headers: (params: JiraSearchIssuesParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraSearchIssuesParams) => { + return { + jql: params.jql, + startAt: params.startAt ? Number(params.startAt) : 0, + maxResults: params.maxResults ? Number(params.maxResults) : 50, + fields: params.fields || ['summary', 'status', 'assignee', 'created', 'updated'], + } + }, + }, + + transformResponse: async (response: Response, params?: JiraSearchIssuesParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search` + const searchResponse = await fetch(searchUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + body: JSON.stringify({ + jql: params?.jql, + startAt: params?.startAt ? Number(params.startAt) : 0, + maxResults: params?.maxResults ? Number(params.maxResults) : 50, + fields: params?.fields || ['summary', 'status', 'assignee', 'created', 'updated'], + }), + }) + + if (!searchResponse.ok) { + let message = `Failed to search Jira issues (${searchResponse.status})` + try { + const err = await searchResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await searchResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + total: data?.total || 0, + startAt: data?.startAt || 0, + maxResults: data?.maxResults || 0, + issues: (data?.issues || []).map((issue: any) => ({ + key: issue.key, + summary: issue.fields?.summary, + status: issue.fields?.status?.name, + assignee: issue.fields?.assignee?.displayName || issue.fields?.assignee?.accountId, + created: issue.fields?.created, + updated: issue.fields?.updated, + })), + }, + } + } + + if (!response.ok) { + let message = `Failed to search Jira issues (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + total: data?.total || 0, + startAt: data?.startAt || 0, + maxResults: data?.maxResults || 0, + issues: (data?.issues || []).map((issue: any) => ({ + key: issue.key, + summary: issue.fields?.summary, + status: issue.fields?.status?.name, + assignee: issue.fields?.assignee?.displayName || issue.fields?.assignee?.accountId, + created: issue.fields?.created, + updated: issue.fields?.updated, + })), + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Search results with timestamp, total count, pagination details, and array of matching issues', + }, + }, +} diff --git a/apps/sim/tools/jira/transition_issue.ts b/apps/sim/tools/jira/transition_issue.ts new file mode 100644 index 0000000000..8351941fde --- /dev/null +++ b/apps/sim/tools/jira/transition_issue.ts @@ -0,0 +1,213 @@ +import type { JiraTransitionIssueParams, JiraTransitionIssueResponse } from '@/tools/jira/types' +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +export const jiraTransitionIssueTool: ToolConfig< + JiraTransitionIssueParams, + JiraTransitionIssueResponse +> = { + id: 'jira_transition_issue', + name: 'Jira Transition Issue', + description: 'Move a Jira issue between workflow statuses (e.g., To Do -> In Progress)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:jira-work'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to transition (e.g., PROJ-123)', + }, + transitionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'ID of the transition to execute (e.g., "11" for "To Do", "21" for "In Progress")', + }, + comment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional comment to add when transitioning the issue', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraTransitionIssueParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/transitions` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'POST', + headers: (params: JiraTransitionIssueParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraTransitionIssueParams) => { + const body: any = { + transition: { + id: params.transitionId, + }, + } + + if (params.comment) { + body.update = { + comment: [ + { + add: { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + }, + }, + }, + ], + } + } + + return body + }, + }, + + transformResponse: async (response: Response, params?: JiraTransitionIssueParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + const transitionUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params!.issueKey}/transitions` + + const body: any = { + transition: { + id: params!.transitionId, + }, + } + + if (params!.comment) { + body.update = { + comment: [ + { + add: { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params!.comment, + }, + ], + }, + ], + }, + }, + }, + ], + } + } + + const transitionResponse = await fetch(transitionUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params!.accessToken}`, + }, + body: JSON.stringify(body), + }) + + if (!transitionResponse.ok) { + let message = `Failed to transition Jira issue (${transitionResponse.status})` + try { + const err = await transitionResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + // Transition endpoint returns 204 No Content on success + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params!.issueKey, + transitionId: params!.transitionId, + success: true, + }, + } + } + + if (!response.ok) { + let message = `Failed to transition Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + // Transition endpoint returns 204 No Content on success + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + transitionId: params?.transitionId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Transition details with timestamp, issue key, transition ID, and success status', + }, + }, +} diff --git a/apps/sim/tools/jira/types.ts b/apps/sim/tools/jira/types.ts index 810980a038..f0705f21c3 100644 --- a/apps/sim/tools/jira/types.ts +++ b/apps/sim/tools/jira/types.ts @@ -105,8 +105,383 @@ export interface JiraCloudResource { avatarUrl: string } +// Delete Issue +export interface JiraDeleteIssueParams { + accessToken: string + domain: string + issueKey: string + cloudId?: string + deleteSubtasks?: boolean +} + +export interface JiraDeleteIssueResponse extends ToolResponse { + output: { + ts: string + issueKey: string + success: boolean + } +} + +// Assign Issue +export interface JiraAssignIssueParams { + accessToken: string + domain: string + issueKey: string + accountId: string + cloudId?: string +} + +export interface JiraAssignIssueResponse extends ToolResponse { + output: { + ts: string + issueKey: string + assigneeId: string + success: boolean + } +} + +// Transition Issue +export interface JiraTransitionIssueParams { + accessToken: string + domain: string + issueKey: string + transitionId: string + comment?: string + cloudId?: string +} + +export interface JiraTransitionIssueResponse extends ToolResponse { + output: { + ts: string + issueKey: string + transitionId: string + success: boolean + } +} + +// Search Issues +export interface JiraSearchIssuesParams { + accessToken: string + domain: string + jql: string + startAt?: number + maxResults?: number + fields?: string[] + cloudId?: string +} + +export interface JiraSearchIssuesResponse extends ToolResponse { + output: { + ts: string + total: number + startAt: number + maxResults: number + issues: Array<{ + key: string + summary: string + status: string + assignee?: string + priority?: string + created: string + updated: string + }> + } +} + +// Comments +export interface JiraAddCommentParams { + accessToken: string + domain: string + issueKey: string + body: string + cloudId?: string +} + +export interface JiraAddCommentResponse extends ToolResponse { + output: { + ts: string + issueKey: string + commentId: string + body: string + success: boolean + } +} + +export interface JiraGetCommentsParams { + accessToken: string + domain: string + issueKey: string + startAt?: number + maxResults?: number + cloudId?: string +} + +export interface JiraGetCommentsResponse extends ToolResponse { + output: { + ts: string + issueKey: string + total: number + comments: Array<{ + id: string + author: string + body: string + created: string + updated: string + }> + } +} + +export interface JiraUpdateCommentParams { + accessToken: string + domain: string + issueKey: string + commentId: string + body: string + cloudId?: string +} + +export interface JiraUpdateCommentResponse extends ToolResponse { + output: { + ts: string + issueKey: string + commentId: string + body: string + success: boolean + } +} + +export interface JiraDeleteCommentParams { + accessToken: string + domain: string + issueKey: string + commentId: string + cloudId?: string +} + +export interface JiraDeleteCommentResponse extends ToolResponse { + output: { + ts: string + issueKey: string + commentId: string + success: boolean + } +} + +// Attachments +export interface JiraGetAttachmentsParams { + accessToken: string + domain: string + issueKey: string + cloudId?: string +} + +export interface JiraGetAttachmentsResponse extends ToolResponse { + output: { + ts: string + issueKey: string + attachments: Array<{ + id: string + filename: string + author: string + created: string + size: number + mimeType: string + content: string + }> + } +} + +export interface JiraDeleteAttachmentParams { + accessToken: string + domain: string + attachmentId: string + cloudId?: string +} + +export interface JiraDeleteAttachmentResponse extends ToolResponse { + output: { + ts: string + attachmentId: string + success: boolean + } +} + +// Worklogs +export interface JiraAddWorklogParams { + accessToken: string + domain: string + issueKey: string + timeSpentSeconds: number + comment?: string + started?: string + cloudId?: string +} + +export interface JiraAddWorklogResponse extends ToolResponse { + output: { + ts: string + issueKey: string + worklogId: string + timeSpentSeconds: number + success: boolean + } +} + +export interface JiraGetWorklogsParams { + accessToken: string + domain: string + issueKey: string + startAt?: number + maxResults?: number + cloudId?: string +} + +export interface JiraGetWorklogsResponse extends ToolResponse { + output: { + ts: string + issueKey: string + total: number + worklogs: Array<{ + id: string + author: string + timeSpentSeconds: number + timeSpent: string + comment?: string + created: string + updated: string + started: string + }> + } +} + +export interface JiraUpdateWorklogParams { + accessToken: string + domain: string + issueKey: string + worklogId: string + timeSpentSeconds?: number + comment?: string + started?: string + cloudId?: string +} + +export interface JiraUpdateWorklogResponse extends ToolResponse { + output: { + ts: string + issueKey: string + worklogId: string + success: boolean + } +} + +export interface JiraDeleteWorklogParams { + accessToken: string + domain: string + issueKey: string + worklogId: string + cloudId?: string +} + +export interface JiraDeleteWorklogResponse extends ToolResponse { + output: { + ts: string + issueKey: string + worklogId: string + success: boolean + } +} + +// Issue Links +export interface JiraCreateIssueLinkParams { + accessToken: string + domain: string + inwardIssueKey: string + outwardIssueKey: string + linkType: string + comment?: string + cloudId?: string +} + +export interface JiraCreateIssueLinkResponse extends ToolResponse { + output: { + ts: string + inwardIssue: string + outwardIssue: string + linkType: string + success: boolean + } +} + +export interface JiraDeleteIssueLinkParams { + accessToken: string + domain: string + linkId: string + cloudId?: string +} + +export interface JiraDeleteIssueLinkResponse extends ToolResponse { + output: { + ts: string + linkId: string + success: boolean + } +} + +// Watchers +export interface JiraAddWatcherParams { + accessToken: string + domain: string + issueKey: string + accountId: string + cloudId?: string +} + +export interface JiraAddWatcherResponse extends ToolResponse { + output: { + ts: string + issueKey: string + watcherAccountId: string + success: boolean + } +} + +export interface JiraRemoveWatcherParams { + accessToken: string + domain: string + issueKey: string + accountId: string + cloudId?: string +} + +export interface JiraRemoveWatcherResponse extends ToolResponse { + output: { + ts: string + issueKey: string + watcherAccountId: string + success: boolean + } +} + export type JiraResponse = | JiraRetrieveResponse | JiraUpdateResponse | JiraWriteResponse | JiraRetrieveResponseBulk + | JiraDeleteIssueResponse + | JiraAssignIssueResponse + | JiraTransitionIssueResponse + | JiraSearchIssuesResponse + | JiraAddCommentResponse + | JiraGetCommentsResponse + | JiraUpdateCommentResponse + | JiraDeleteCommentResponse + | JiraGetAttachmentsResponse + | JiraDeleteAttachmentResponse + | JiraAddWorklogResponse + | JiraGetWorklogsResponse + | JiraUpdateWorklogResponse + | JiraDeleteWorklogResponse + | JiraCreateIssueLinkResponse + | JiraDeleteIssueLinkResponse + | JiraAddWatcherResponse + | JiraRemoveWatcherResponse diff --git a/apps/sim/tools/jira/update_comment.ts b/apps/sim/tools/jira/update_comment.ts new file mode 100644 index 0000000000..fe89085ac1 --- /dev/null +++ b/apps/sim/tools/jira/update_comment.ts @@ -0,0 +1,183 @@ +import type { JiraUpdateCommentParams, JiraUpdateCommentResponse } from '@/tools/jira/types' +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +export const jiraUpdateCommentTool: ToolConfig = + { + id: 'jira_update_comment', + name: 'Jira Update Comment', + description: 'Update an existing comment on a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:comment:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key containing the comment (e.g., PROJ-123)', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the comment to update', + }, + body: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Updated comment text', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraUpdateCommentParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/comment/${params.commentId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'PUT', + headers: (params: JiraUpdateCommentParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraUpdateCommentParams) => { + return { + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.body, + }, + ], + }, + ], + }, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraUpdateCommentParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const commentUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/comment/${params?.commentId}` + const commentResponse = await fetch(commentUrl, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + body: JSON.stringify({ + body: { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params?.body, + }, + ], + }, + ], + }, + }), + }) + + if (!commentResponse.ok) { + let message = `Failed to update comment on Jira issue (${commentResponse.status})` + try { + const err = await commentResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await commentResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + commentId: data?.id || params?.commentId || 'unknown', + body: params?.body || '', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to update comment on Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + commentId: data?.id || params?.commentId || 'unknown', + body: params?.body || '', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Updated comment details with timestamp, issue key, comment ID, body text, and success status', + }, + }, + } diff --git a/apps/sim/tools/jira/update_worklog.ts b/apps/sim/tools/jira/update_worklog.ts new file mode 100644 index 0000000000..ae5f59ef18 --- /dev/null +++ b/apps/sim/tools/jira/update_worklog.ts @@ -0,0 +1,201 @@ +import type { JiraUpdateWorklogParams, JiraUpdateWorklogResponse } from '@/tools/jira/types' +import { getJiraCloudId } from '@/tools/jira/utils' +import type { ToolConfig } from '@/tools/types' + +export const jiraUpdateWorklogTool: ToolConfig = + { + id: 'jira_update_worklog', + name: 'Jira Update Worklog', + description: 'Update an existing worklog entry on a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + additionalScopes: ['write:issue-worklog:jira', 'read:jira-work', 'read:jira-user'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key containing the worklog (e.g., PROJ-123)', + }, + worklogId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the worklog entry to update', + }, + timeSpentSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Time spent in seconds', + }, + comment: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional comment for the worklog entry', + }, + started: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional start time in ISO format', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: JiraUpdateWorklogParams) => { + if (params.cloudId) { + return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/issue/${params.issueKey}/worklog/${params.worklogId}` + } + return 'https://api.atlassian.com/oauth/token/accessible-resources' + }, + method: 'PUT', + headers: (params: JiraUpdateWorklogParams) => { + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params: JiraUpdateWorklogParams) => { + return { + timeSpentSeconds: Number(params.timeSpentSeconds), + comment: params.comment + ? { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + } + : undefined, + started: params.started, + } + }, + }, + + transformResponse: async (response: Response, params?: JiraUpdateWorklogParams) => { + if (!params?.cloudId) { + const cloudId = await getJiraCloudId(params!.domain, params!.accessToken) + // Make the actual request with the resolved cloudId + const worklogUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${params?.issueKey}/worklog/${params?.worklogId}` + const worklogResponse = await fetch(worklogUrl, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params?.accessToken}`, + }, + body: JSON.stringify({ + timeSpentSeconds: params?.timeSpentSeconds ? Number(params.timeSpentSeconds) : 0, + comment: params?.comment + ? { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: params.comment, + }, + ], + }, + ], + } + : undefined, + started: params?.started, + }), + }) + + if (!worklogResponse.ok) { + let message = `Failed to update worklog on Jira issue (${worklogResponse.status})` + try { + const err = await worklogResponse.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await worklogResponse.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + worklogId: data?.id || params?.worklogId || 'unknown', + success: true, + }, + } + } + + // If cloudId was provided, process the response + if (!response.ok) { + let message = `Failed to update worklog on Jira issue (${response.status})` + try { + const err = await response.json() + message = err?.errorMessages?.join(', ') || err?.message || message + } catch (_e) {} + throw new Error(message) + } + + const data = await response.json() + + return { + success: true, + output: { + ts: new Date().toISOString(), + issueKey: params?.issueKey || 'unknown', + worklogId: data?.id || params?.worklogId || 'unknown', + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Operation success status', + }, + output: { + type: 'object', + description: + 'Worklog update details with timestamp, issue key, worklog ID, and success status', + }, + }, + } diff --git a/apps/sim/tools/knowledge/search.ts b/apps/sim/tools/knowledge/search.ts index 8008dfcfe4..c8e35f2830 100644 --- a/apps/sim/tools/knowledge/search.ts +++ b/apps/sim/tools/knowledge/search.ts @@ -78,9 +78,7 @@ export const knowledgeSearchTool: ToolConfig = { const requestBody = { knowledgeBaseIds, query: params.query, - topK: params.topK - ? Math.max(1, Math.min(100, Number.parseInt(params.topK.toString()) || 10)) - : 10, + topK: params.topK ? Math.max(1, Math.min(100, Number(params.topK))) : 10, ...(Object.keys(filters).length > 0 && { filters }), ...(workflowId && { workflowId }), } diff --git a/apps/sim/tools/linear/add_label_to_issue.ts b/apps/sim/tools/linear/add_label_to_issue.ts new file mode 100644 index 0000000000..6c9fe1b6f7 --- /dev/null +++ b/apps/sim/tools/linear/add_label_to_issue.ts @@ -0,0 +1,90 @@ +import type { LinearAddLabelResponse, LinearAddLabelToIssueParams } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearAddLabelToIssueTool: ToolConfig< + LinearAddLabelToIssueParams, + LinearAddLabelResponse +> = { + id: 'linear_add_label_to_issue', + name: 'Linear Add Label to Issue', + description: 'Add a label to an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID', + }, + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label ID to add to the issue', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation AddLabelToIssue($issueId: String!, $labelId: String!) { + issueAddLabel(id: $issueId, labelId: $labelId) { + success + } + } + `, + variables: { + issueId: params.issueId, + labelId: params.labelId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to add label to issue', + output: {}, + } + } + + return { + success: data.data.issueAddLabel.success, + output: { + success: data.data.issueAddLabel.success, + issueId: response.ok ? data.data.issueAddLabel.success : '', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the label was successfully added', + }, + issueId: { + type: 'string', + description: 'The ID of the issue', + }, + }, +} diff --git a/apps/sim/tools/linear/archive_issue.ts b/apps/sim/tools/linear/archive_issue.ts new file mode 100644 index 0000000000..506875fe97 --- /dev/null +++ b/apps/sim/tools/linear/archive_issue.ts @@ -0,0 +1,83 @@ +import type { LinearArchiveIssueParams, LinearArchiveIssueResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearArchiveIssueTool: ToolConfig< + LinearArchiveIssueParams, + LinearArchiveIssueResponse +> = { + id: 'linear_archive_issue', + name: 'Linear Archive Issue', + description: 'Archive an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID to archive', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation ArchiveIssue($id: String!) { + issueArchive(id: $id) { + success + } + } + `, + variables: { + id: params.issueId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to archive issue', + output: {}, + } + } + + return { + success: data.data.issueArchive.success, + output: { + success: data.data.issueArchive.success, + issueId: response.ok ? data.data.issueArchive.success : '', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the archive operation was successful', + }, + issueId: { + type: 'string', + description: 'The ID of the archived issue', + }, + }, +} diff --git a/apps/sim/tools/linear/archive_label.ts b/apps/sim/tools/linear/archive_label.ts new file mode 100644 index 0000000000..e24250b6e2 --- /dev/null +++ b/apps/sim/tools/linear/archive_label.ts @@ -0,0 +1,83 @@ +import type { LinearArchiveLabelParams, LinearArchiveLabelResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearArchiveLabelTool: ToolConfig< + LinearArchiveLabelParams, + LinearArchiveLabelResponse +> = { + id: 'linear_archive_label', + name: 'Linear Archive Label', + description: 'Archive a label in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label ID to archive', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation ArchiveLabel($id: String!) { + issueLabelArchive(id: $id) { + success + } + } + `, + variables: { + id: params.labelId, + }, + }), + }, + + transformResponse: async (response, params) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to archive label', + output: {}, + } + } + + return { + success: data.data.issueLabelArchive.success, + output: { + success: data.data.issueLabelArchive.success, + labelId: params?.labelId, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the archive operation was successful', + }, + labelId: { + type: 'string', + description: 'The ID of the archived label', + }, + }, +} diff --git a/apps/sim/tools/linear/archive_project.ts b/apps/sim/tools/linear/archive_project.ts new file mode 100644 index 0000000000..82d1c65aff --- /dev/null +++ b/apps/sim/tools/linear/archive_project.ts @@ -0,0 +1,83 @@ +import type { LinearArchiveProjectParams, LinearArchiveProjectResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearArchiveProjectTool: ToolConfig< + LinearArchiveProjectParams, + LinearArchiveProjectResponse +> = { + id: 'linear_archive_project', + name: 'Linear Archive Project', + description: 'Archive a project in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID to archive', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation ArchiveProject($id: String!) { + projectArchive(id: $id) { + success + } + } + `, + variables: { + id: params.projectId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to archive project', + output: {}, + } + } + + return { + success: data.data.projectArchive.success, + output: { + success: data.data.projectArchive.success, + projectId: response.ok ? data.data.projectArchive.success : '', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the archive operation was successful', + }, + projectId: { + type: 'string', + description: 'The ID of the archived project', + }, + }, +} diff --git a/apps/sim/tools/linear/create_attachment.ts b/apps/sim/tools/linear/create_attachment.ts new file mode 100644 index 0000000000..25e9e5bb62 --- /dev/null +++ b/apps/sim/tools/linear/create_attachment.ts @@ -0,0 +1,133 @@ +import type { + LinearCreateAttachmentParams, + LinearCreateAttachmentResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateAttachmentTool: ToolConfig< + LinearCreateAttachmentParams, + LinearCreateAttachmentResponse +> = { + id: 'linear_create_attachment', + name: 'Linear Create Attachment', + description: 'Add an attachment to an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Issue ID to attach to', + }, + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'URL of the attachment', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Attachment title', + }, + subtitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Attachment subtitle/description', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + issueId: params.issueId, + url: params.url, + } + + if (params.title !== undefined) input.title = params.title + if (params.subtitle !== undefined) input.subtitle = params.subtitle + + return { + query: ` + mutation CreateAttachment($input: AttachmentCreateInput!) { + attachmentCreate(input: $input) { + success + attachment { + id + title + subtitle + url + createdAt + updatedAt + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create attachment', + output: {}, + } + } + + const result = data.data.attachmentCreate + if (!result.success) { + return { + success: false, + error: 'Attachment creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + attachment: result.attachment, + }, + } + }, + + outputs: { + attachment: { + type: 'object', + description: 'The created attachment', + properties: { + id: { type: 'string', description: 'Attachment ID' }, + title: { type: 'string', description: 'Attachment title' }, + subtitle: { type: 'string', description: 'Attachment subtitle' }, + url: { type: 'string', description: 'Attachment URL' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_comment.ts b/apps/sim/tools/linear/create_comment.ts new file mode 100644 index 0000000000..d94bf785e3 --- /dev/null +++ b/apps/sim/tools/linear/create_comment.ts @@ -0,0 +1,118 @@ +import type { LinearCreateCommentParams, LinearCreateCommentResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateCommentTool: ToolConfig< + LinearCreateCommentParams, + LinearCreateCommentResponse +> = { + id: 'linear_create_comment', + name: 'Linear Create Comment', + description: 'Add a comment to an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID to comment on', + }, + body: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment text (supports Markdown)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation CreateComment($input: CommentCreateInput!) { + commentCreate(input: $input) { + success + comment { + id + body + createdAt + updatedAt + user { + id + name + email + } + issue { + id + title + } + } + } + } + `, + variables: { + input: { + issueId: params.issueId, + body: params.body, + }, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create comment', + output: {}, + } + } + + const result = data.data.commentCreate + if (!result.success) { + return { + success: false, + error: 'Comment creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + comment: result.comment, + }, + } + }, + + outputs: { + comment: { + type: 'object', + description: 'The created comment', + properties: { + id: { type: 'string', description: 'Comment ID' }, + body: { type: 'string', description: 'Comment text' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + user: { type: 'object', description: 'User who created the comment' }, + issue: { type: 'object', description: 'Associated issue' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_cycle.ts b/apps/sim/tools/linear/create_cycle.ts new file mode 100644 index 0000000000..3f3428e8a3 --- /dev/null +++ b/apps/sim/tools/linear/create_cycle.ts @@ -0,0 +1,133 @@ +import type { LinearCreateCycleParams, LinearCreateCycleResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateCycleTool: ToolConfig = + { + id: 'linear_create_cycle', + name: 'Linear Create Cycle', + description: 'Create a new cycle (sprint/iteration) in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Team ID to create the cycle in', + }, + startsAt: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cycle start date (ISO format)', + }, + endsAt: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cycle end date (ISO format)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cycle name (optional, will be auto-generated if not provided)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + teamId: params.teamId, + startsAt: params.startsAt, + endsAt: params.endsAt, + } + + if (params.name !== undefined) input.name = params.name + + return { + query: ` + mutation CreateCycle($input: CycleCreateInput!) { + cycleCreate(input: $input) { + success + cycle { + id + number + name + startsAt + endsAt + progress + team { + id + name + } + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create cycle', + output: {}, + } + } + + const result = data.data.cycleCreate + if (!result.success) { + return { + success: false, + error: 'Cycle creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + cycle: result.cycle, + }, + } + }, + + outputs: { + cycle: { + type: 'object', + description: 'The created cycle', + properties: { + id: { type: 'string', description: 'Cycle ID' }, + number: { type: 'number', description: 'Cycle number' }, + name: { type: 'string', description: 'Cycle name' }, + startsAt: { type: 'string', description: 'Start date' }, + endsAt: { type: 'string', description: 'End date' }, + team: { type: 'object', description: 'Team this cycle belongs to' }, + }, + }, + }, + } diff --git a/apps/sim/tools/linear/create_favorite.ts b/apps/sim/tools/linear/create_favorite.ts new file mode 100644 index 0000000000..62a4b32c4c --- /dev/null +++ b/apps/sim/tools/linear/create_favorite.ts @@ -0,0 +1,141 @@ +import type { LinearCreateFavoriteParams, LinearCreateFavoriteResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateFavoriteTool: ToolConfig< + LinearCreateFavoriteParams, + LinearCreateFavoriteResponse +> = { + id: 'linear_create_favorite', + name: 'Linear Create Favorite', + description: 'Bookmark an issue, project, cycle, or label in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Issue ID to favorite', + }, + projectId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project ID to favorite', + }, + cycleId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cycle ID to favorite', + }, + labelId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Label ID to favorite', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + if (params.issueId !== undefined) input.issueId = params.issueId + if (params.projectId !== undefined) input.projectId = params.projectId + if (params.cycleId !== undefined) input.cycleId = params.cycleId + if (params.labelId !== undefined) input.labelId = params.labelId + + if (Object.keys(input).length === 0) { + throw new Error('At least one ID (issue, project, cycle, or label) must be provided') + } + + return { + query: ` + mutation CreateFavorite($input: FavoriteCreateInput!) { + favoriteCreate(input: $input) { + success + favorite { + id + type + issue { + id + title + } + project { + id + name + } + cycle { + id + name + } + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create favorite', + output: {}, + } + } + + const result = data.data.favoriteCreate + if (!result.success) { + return { + success: false, + error: 'Favorite creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + favorite: result.favorite, + }, + } + }, + + outputs: { + favorite: { + type: 'object', + description: 'The created favorite', + properties: { + id: { type: 'string', description: 'Favorite ID' }, + type: { type: 'string', description: 'Favorite type' }, + issue: { type: 'object', description: 'Favorited issue (if applicable)' }, + project: { type: 'object', description: 'Favorited project (if applicable)' }, + cycle: { type: 'object', description: 'Favorited cycle (if applicable)' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_issue_relation.ts b/apps/sim/tools/linear/create_issue_relation.ts new file mode 100644 index 0000000000..924f37ce6a --- /dev/null +++ b/apps/sim/tools/linear/create_issue_relation.ts @@ -0,0 +1,124 @@ +import type { + LinearCreateIssueRelationParams, + LinearCreateIssueRelationResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateIssueRelationTool: ToolConfig< + LinearCreateIssueRelationParams, + LinearCreateIssueRelationResponse +> = { + id: 'linear_create_issue_relation', + name: 'Linear Create Issue Relation', + description: 'Link two issues together in Linear (blocks, relates to, duplicates)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Source issue ID', + }, + relatedIssueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Target issue ID to link to', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Relation type: "blocks", "blocked", "duplicate", "related"', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation CreateIssueRelation($input: IssueRelationCreateInput!) { + issueRelationCreate(input: $input) { + success + issueRelation { + id + type + issue { + id + title + } + relatedIssue { + id + title + } + } + } + } + `, + variables: { + input: { + issueId: params.issueId, + relatedIssueId: params.relatedIssueId, + type: params.type, + }, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create issue relation', + output: {}, + } + } + + const result = data.data.issueRelationCreate + if (!result.success) { + return { + success: false, + error: 'Issue relation creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + relation: result.issueRelation, + }, + } + }, + + outputs: { + relation: { + type: 'object', + description: 'The created issue relation', + properties: { + id: { type: 'string', description: 'Relation ID' }, + type: { type: 'string', description: 'Relation type' }, + issue: { type: 'object', description: 'Source issue' }, + relatedIssue: { type: 'object', description: 'Target issue' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_label.ts b/apps/sim/tools/linear/create_label.ts new file mode 100644 index 0000000000..22a06029d8 --- /dev/null +++ b/apps/sim/tools/linear/create_label.ts @@ -0,0 +1,130 @@ +import type { LinearCreateLabelParams, LinearCreateLabelResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateLabelTool: ToolConfig = + { + id: 'linear_create_label', + name: 'Linear Create Label', + description: 'Create a new label in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label name', + }, + color: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Label color (hex format, e.g., "#ff0000")', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Label description', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Team ID (if omitted, creates workspace label)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + name: params.name, + } + + if (params.color !== undefined) input.color = params.color + if (params.description !== undefined) input.description = params.description + if (params.teamId !== undefined) input.teamId = params.teamId + + return { + query: ` + mutation CreateLabel($input: IssueLabelCreateInput!) { + issueLabelCreate(input: $input) { + success + issueLabel { + id + name + color + description + team { + id + name + } + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create label', + output: {}, + } + } + + const result = data.data.issueLabelCreate + if (!result.success) { + return { + success: false, + error: 'Label creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + label: result.issueLabel, + }, + } + }, + + outputs: { + label: { + type: 'object', + description: 'The created label', + properties: { + id: { type: 'string', description: 'Label ID' }, + name: { type: 'string', description: 'Label name' }, + color: { type: 'string', description: 'Label color' }, + description: { type: 'string', description: 'Label description' }, + team: { type: 'object', description: 'Team this label belongs to' }, + }, + }, + }, + } diff --git a/apps/sim/tools/linear/create_project.ts b/apps/sim/tools/linear/create_project.ts new file mode 100644 index 0000000000..abf6317876 --- /dev/null +++ b/apps/sim/tools/linear/create_project.ts @@ -0,0 +1,177 @@ +import type { LinearCreateProjectParams, LinearCreateProjectResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateProjectTool: ToolConfig< + LinearCreateProjectParams, + LinearCreateProjectResponse +> = { + id: 'linear_create_project', + name: 'Linear Create Project', + description: 'Create a new project in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Team ID to create the project in', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project description', + }, + leadId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID of the project lead', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project start date (ISO format)', + }, + targetDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project target date (ISO format)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Project priority (0-4)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + teamIds: [params.teamId], + name: params.name, + } + + if (params.description !== undefined) input.description = params.description + if (params.leadId !== undefined) input.leadId = params.leadId + if (params.startDate !== undefined) input.startDate = params.startDate + if (params.targetDate !== undefined) input.targetDate = params.targetDate + if (params.priority !== undefined) input.priority = Number(params.priority) + + return { + query: ` + mutation CreateProject($input: ProjectCreateInput!) { + projectCreate(input: $input) { + success + project { + id + name + description + state + priority + startDate + targetDate + url + lead { + id + name + } + teams { + nodes { + id + name + } + } + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create project', + output: {}, + } + } + + const result = data.data.projectCreate + if (!result.success) { + return { + success: false, + error: 'Project creation was not successful', + output: {}, + } + } + + const project = result.project + return { + success: true, + output: { + project: { + id: project.id, + name: project.name, + description: project.description, + state: project.state, + priority: project.priority, + startDate: project.startDate, + targetDate: project.targetDate, + url: project.url, + lead: project.lead, + teams: project.teams?.nodes || [], + }, + }, + } + }, + + outputs: { + project: { + type: 'object', + description: 'The created project', + properties: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + description: { type: 'string', description: 'Project description' }, + state: { type: 'string', description: 'Project state' }, + priority: { type: 'number', description: 'Project priority' }, + lead: { type: 'object', description: 'Project lead' }, + teams: { type: 'array', description: 'Associated teams' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_project_link.ts b/apps/sim/tools/linear/create_project_link.ts new file mode 100644 index 0000000000..c5242d8009 --- /dev/null +++ b/apps/sim/tools/linear/create_project_link.ts @@ -0,0 +1,123 @@ +import type { + LinearCreateProjectLinkParams, + LinearCreateProjectLinkResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateProjectLinkTool: ToolConfig< + LinearCreateProjectLinkParams, + LinearCreateProjectLinkResponse +> = { + id: 'linear_create_project_link', + name: 'Linear Create Project Link', + description: 'Add an external link to a project in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID to add link to', + }, + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'URL of the external link', + }, + label: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Link label/title', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + projectId: params.projectId, + url: params.url, + } + + if (params.label !== undefined) input.label = params.label + + return { + query: ` + mutation CreateProjectLink($input: ProjectLinkCreateInput!) { + projectLinkCreate(input: $input) { + success + projectLink { + id + url + label + createdAt + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create project link', + output: {}, + } + } + + const result = data.data.projectLinkCreate + if (!result.success) { + return { + success: false, + error: 'Project link creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + link: result.projectLink, + }, + } + }, + + outputs: { + link: { + type: 'object', + description: 'The created project link', + properties: { + id: { type: 'string', description: 'Link ID' }, + url: { type: 'string', description: 'Link URL' }, + label: { type: 'string', description: 'Link label' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_project_update.ts b/apps/sim/tools/linear/create_project_update.ts new file mode 100644 index 0000000000..33954c11ed --- /dev/null +++ b/apps/sim/tools/linear/create_project_update.ts @@ -0,0 +1,128 @@ +import type { + LinearCreateProjectUpdateParams, + LinearCreateProjectUpdateResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateProjectUpdateTool: ToolConfig< + LinearCreateProjectUpdateParams, + LinearCreateProjectUpdateResponse +> = { + id: 'linear_create_project_update', + name: 'Linear Create Project Update', + description: 'Post a status update for a project in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID to post update for', + }, + body: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Update message (supports Markdown)', + }, + health: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project health: "onTrack", "atRisk", or "offTrack"', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + projectId: params.projectId, + body: params.body, + } + + if (params.health !== undefined) input.health = params.health + + return { + query: ` + mutation CreateProjectUpdate($input: ProjectUpdateCreateInput!) { + projectUpdateCreate(input: $input) { + success + projectUpdate { + id + body + health + createdAt + user { + id + name + } + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create project update', + output: {}, + } + } + + const result = data.data.projectUpdateCreate + if (!result.success) { + return { + success: false, + error: 'Project update creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + update: result.projectUpdate, + }, + } + }, + + outputs: { + update: { + type: 'object', + description: 'The created project update', + properties: { + id: { type: 'string', description: 'Update ID' }, + body: { type: 'string', description: 'Update message' }, + health: { type: 'string', description: 'Project health status' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + user: { type: 'object', description: 'User who created the update' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/create_workflow_state.ts b/apps/sim/tools/linear/create_workflow_state.ts new file mode 100644 index 0000000000..06d70c3a55 --- /dev/null +++ b/apps/sim/tools/linear/create_workflow_state.ts @@ -0,0 +1,151 @@ +import type { + LinearCreateWorkflowStateParams, + LinearCreateWorkflowStateResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearCreateWorkflowStateTool: ToolConfig< + LinearCreateWorkflowStateParams, + LinearCreateWorkflowStateResponse +> = { + id: 'linear_create_workflow_state', + name: 'Linear Create Workflow State', + description: 'Create a new workflow state (status) in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Team ID to create the state in', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'State name (e.g., "In Review")', + }, + color: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'State color (hex format)', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'State type: "backlog", "unstarted", "started", "completed", or "canceled"', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State description', + }, + position: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Position in the workflow', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = { + teamId: params.teamId, + name: params.name, + color: params.color, + type: params.type, + } + + if (params.description !== undefined) input.description = params.description + if (params.position !== undefined) input.position = Number(params.position) + + return { + query: ` + mutation CreateWorkflowState($input: WorkflowStateCreateInput!) { + workflowStateCreate(input: $input) { + success + workflowState { + id + name + type + color + position + team { + id + name + } + } + } + } + `, + variables: { + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to create workflow state', + output: {}, + } + } + + const result = data.data.workflowStateCreate + if (!result.success) { + return { + success: false, + error: 'Workflow state creation was not successful', + output: {}, + } + } + + return { + success: true, + output: { + state: result.workflowState, + }, + } + }, + + outputs: { + state: { + type: 'object', + description: 'The created workflow state', + properties: { + id: { type: 'string', description: 'State ID' }, + name: { type: 'string', description: 'State name' }, + type: { type: 'string', description: 'State type' }, + color: { type: 'string', description: 'State color' }, + position: { type: 'number', description: 'State position' }, + team: { type: 'object', description: 'Team this state belongs to' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/delete_attachment.ts b/apps/sim/tools/linear/delete_attachment.ts new file mode 100644 index 0000000000..44043c741e --- /dev/null +++ b/apps/sim/tools/linear/delete_attachment.ts @@ -0,0 +1,81 @@ +import type { + LinearDeleteAttachmentParams, + LinearDeleteAttachmentResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearDeleteAttachmentTool: ToolConfig< + LinearDeleteAttachmentParams, + LinearDeleteAttachmentResponse +> = { + id: 'linear_delete_attachment', + name: 'Linear Delete Attachment', + description: 'Delete an attachment from Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + attachmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Attachment ID to delete', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation DeleteAttachment($id: String!) { + attachmentDelete(id: $id) { + success + } + } + `, + variables: { + id: params.attachmentId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to delete attachment', + output: {}, + } + } + + return { + success: data.data.attachmentDelete.success, + output: { + success: data.data.attachmentDelete.success, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the delete operation was successful', + }, + }, +} diff --git a/apps/sim/tools/linear/delete_comment.ts b/apps/sim/tools/linear/delete_comment.ts new file mode 100644 index 0000000000..92c8acebb2 --- /dev/null +++ b/apps/sim/tools/linear/delete_comment.ts @@ -0,0 +1,78 @@ +import type { LinearDeleteCommentParams, LinearDeleteCommentResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearDeleteCommentTool: ToolConfig< + LinearDeleteCommentParams, + LinearDeleteCommentResponse +> = { + id: 'linear_delete_comment', + name: 'Linear Delete Comment', + description: 'Delete a comment from Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment ID to delete', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation DeleteComment($id: String!) { + commentDelete(id: $id) { + success + } + } + `, + variables: { + id: params.commentId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to delete comment', + output: {}, + } + } + + return { + success: data.data.commentDelete.success, + output: { + success: data.data.commentDelete.success, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the delete operation was successful', + }, + }, +} diff --git a/apps/sim/tools/linear/delete_issue.ts b/apps/sim/tools/linear/delete_issue.ts new file mode 100644 index 0000000000..1e558666e6 --- /dev/null +++ b/apps/sim/tools/linear/delete_issue.ts @@ -0,0 +1,76 @@ +import type { LinearDeleteIssueParams, LinearDeleteIssueResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearDeleteIssueTool: ToolConfig = + { + id: 'linear_delete_issue', + name: 'Linear Delete Issue', + description: 'Delete (trash) an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID to delete', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation DeleteIssue($id: String!) { + issueDelete(id: $id) { + success + } + } + `, + variables: { + id: params.issueId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to delete issue', + output: {}, + } + } + + return { + success: data.data.issueDelete.success, + output: { + success: data.data.issueDelete.success, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the delete operation was successful', + }, + }, + } diff --git a/apps/sim/tools/linear/delete_issue_relation.ts b/apps/sim/tools/linear/delete_issue_relation.ts new file mode 100644 index 0000000000..5ebb8708b7 --- /dev/null +++ b/apps/sim/tools/linear/delete_issue_relation.ts @@ -0,0 +1,81 @@ +import type { + LinearDeleteIssueRelationParams, + LinearDeleteIssueRelationResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearDeleteIssueRelationTool: ToolConfig< + LinearDeleteIssueRelationParams, + LinearDeleteIssueRelationResponse +> = { + id: 'linear_delete_issue_relation', + name: 'Linear Delete Issue Relation', + description: 'Remove a relation between two issues in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + relationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Relation ID to delete', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation DeleteIssueRelation($id: String!) { + issueRelationDelete(id: $id) { + success + } + } + `, + variables: { + id: params.relationId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to delete issue relation', + output: {}, + } + } + + return { + success: data.data.issueRelationDelete.success, + output: { + success: data.data.issueRelationDelete.success, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the delete operation was successful', + }, + }, +} diff --git a/apps/sim/tools/linear/get_active_cycle.ts b/apps/sim/tools/linear/get_active_cycle.ts new file mode 100644 index 0000000000..6a44bd07fe --- /dev/null +++ b/apps/sim/tools/linear/get_active_cycle.ts @@ -0,0 +1,99 @@ +import type { LinearGetActiveCycleParams, LinearGetActiveCycleResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearGetActiveCycleTool: ToolConfig< + LinearGetActiveCycleParams, + LinearGetActiveCycleResponse +> = { + id: 'linear_get_active_cycle', + name: 'Linear Get Active Cycle', + description: 'Get the currently active cycle for a team', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Team ID', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query GetActiveCycle($id: String!) { + team(id: $id) { + activeCycle { + id + number + name + startsAt + endsAt + completedAt + progress + team { + id + name + } + } + } + } + `, + variables: { + id: params.teamId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to fetch active cycle', + output: {}, + } + } + + return { + success: true, + output: { + cycle: data.data.team.activeCycle || null, + }, + } + }, + + outputs: { + cycle: { + type: 'object', + description: 'The active cycle (null if no active cycle)', + properties: { + id: { type: 'string', description: 'Cycle ID' }, + number: { type: 'number', description: 'Cycle number' }, + name: { type: 'string', description: 'Cycle name' }, + startsAt: { type: 'string', description: 'Start date' }, + endsAt: { type: 'string', description: 'End date' }, + progress: { type: 'number', description: 'Progress percentage' }, + team: { type: 'object', description: 'Team this cycle belongs to' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/get_cycle.ts b/apps/sim/tools/linear/get_cycle.ts new file mode 100644 index 0000000000..4d34baea2b --- /dev/null +++ b/apps/sim/tools/linear/get_cycle.ts @@ -0,0 +1,94 @@ +import type { LinearGetCycleParams, LinearGetCycleResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearGetCycleTool: ToolConfig = { + id: 'linear_get_cycle', + name: 'Linear Get Cycle', + description: 'Get a single cycle by ID from Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + cycleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Cycle ID', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query GetCycle($id: String!) { + cycle(id: $id) { + id + number + name + startsAt + endsAt + completedAt + progress + team { + id + name + } + } + } + `, + variables: { + id: params.cycleId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to fetch cycle', + output: {}, + } + } + + return { + success: true, + output: { + cycle: data.data.cycle, + }, + } + }, + + outputs: { + cycle: { + type: 'object', + description: 'The cycle with full details', + properties: { + id: { type: 'string', description: 'Cycle ID' }, + number: { type: 'number', description: 'Cycle number' }, + name: { type: 'string', description: 'Cycle name' }, + startsAt: { type: 'string', description: 'Start date' }, + endsAt: { type: 'string', description: 'End date' }, + progress: { type: 'number', description: 'Progress percentage' }, + team: { type: 'object', description: 'Team this cycle belongs to' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/get_issue.ts b/apps/sim/tools/linear/get_issue.ts new file mode 100644 index 0000000000..15bd2406d6 --- /dev/null +++ b/apps/sim/tools/linear/get_issue.ts @@ -0,0 +1,141 @@ +import type { LinearGetIssueParams, LinearGetIssueResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearGetIssueTool: ToolConfig = { + id: 'linear_get_issue', + name: 'Linear Get Issue', + description: 'Get a single issue by ID from Linear with full details', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query GetIssue($id: String!) { + issue(id: $id) { + id + title + description + priority + estimate + url + createdAt + updatedAt + completedAt + canceledAt + archivedAt + state { + id + name + type + } + assignee { + id + name + email + } + team { + id + name + } + project { + id + name + } + labels { + nodes { + id + name + color + } + } + } + } + `, + variables: { + id: params.issueId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to fetch issue', + output: {}, + } + } + + const issue = data.data.issue + return { + success: true, + output: { + issue: { + id: issue.id, + title: issue.title, + description: issue.description, + priority: issue.priority, + estimate: issue.estimate, + url: issue.url, + createdAt: issue.createdAt, + updatedAt: issue.updatedAt, + completedAt: issue.completedAt, + canceledAt: issue.canceledAt, + archivedAt: issue.archivedAt, + state: issue.state, + assignee: issue.assignee, + teamId: issue.team?.id, + projectId: issue.project?.id, + labels: issue.labels?.nodes || [], + }, + }, + } + }, + + outputs: { + issue: { + type: 'object', + description: 'The issue with full details', + properties: { + id: { type: 'string', description: 'Issue ID' }, + title: { type: 'string', description: 'Issue title' }, + description: { type: 'string', description: 'Issue description' }, + priority: { type: 'number', description: 'Issue priority (0-4)' }, + estimate: { type: 'number', description: 'Issue estimate in points' }, + url: { type: 'string', description: 'Issue URL' }, + state: { type: 'object', description: 'Issue state/status' }, + assignee: { type: 'object', description: 'Assigned user' }, + labels: { type: 'array', description: 'Issue labels' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/get_project.ts b/apps/sim/tools/linear/get_project.ts new file mode 100644 index 0000000000..a7438aced3 --- /dev/null +++ b/apps/sim/tools/linear/get_project.ts @@ -0,0 +1,121 @@ +import type { LinearGetProjectParams, LinearGetProjectResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearGetProjectTool: ToolConfig = { + id: 'linear_get_project', + name: 'Linear Get Project', + description: 'Get a single project by ID from Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear project ID', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query GetProject($id: String!) { + project(id: $id) { + id + name + description + state + priority + startDate + targetDate + completedAt + canceledAt + archivedAt + url + lead { + id + name + } + teams { + nodes { + id + name + } + } + } + } + `, + variables: { + id: params.projectId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to fetch project', + output: {}, + } + } + + const project = data.data.project + return { + success: true, + output: { + project: { + id: project.id, + name: project.name, + description: project.description, + state: project.state, + priority: project.priority, + startDate: project.startDate, + targetDate: project.targetDate, + completedAt: project.completedAt, + canceledAt: project.canceledAt, + archivedAt: project.archivedAt, + url: project.url, + lead: project.lead, + teams: project.teams?.nodes || [], + }, + }, + } + }, + + outputs: { + project: { + type: 'object', + description: 'The project with full details', + properties: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + description: { type: 'string', description: 'Project description' }, + state: { type: 'string', description: 'Project state' }, + priority: { type: 'number', description: 'Project priority' }, + startDate: { type: 'string', description: 'Start date' }, + targetDate: { type: 'string', description: 'Target completion date' }, + lead: { type: 'object', description: 'Project lead' }, + teams: { type: 'array', description: 'Associated teams' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/get_viewer.ts b/apps/sim/tools/linear/get_viewer.ts new file mode 100644 index 0000000000..f090e55a24 --- /dev/null +++ b/apps/sim/tools/linear/get_viewer.ts @@ -0,0 +1,80 @@ +import type { LinearGetViewerParams, LinearGetViewerResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearGetViewerTool: ToolConfig = { + id: 'linear_get_viewer', + name: 'Linear Get Current User', + description: 'Get the currently authenticated user (viewer) information', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: {}, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: () => ({ + query: ` + query GetViewer { + viewer { + id + name + email + displayName + active + admin + avatarUrl + } + } + `, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to get viewer', + output: {}, + } + } + + return { + success: true, + output: { + user: data.data.viewer, + }, + } + }, + + outputs: { + user: { + type: 'object', + description: 'The currently authenticated user', + properties: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'User name' }, + email: { type: 'string', description: 'User email' }, + displayName: { type: 'string', description: 'Display name' }, + active: { type: 'boolean', description: 'Whether user is active' }, + admin: { type: 'boolean', description: 'Whether user is admin' }, + avatarUrl: { type: 'string', description: 'Avatar URL' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/index.ts b/apps/sim/tools/linear/index.ts index 83ceb95ea9..52eb80711d 100644 --- a/apps/sim/tools/linear/index.ts +++ b/apps/sim/tools/linear/index.ts @@ -1,4 +1,122 @@ +// Issue operations + +import { linearAddLabelToIssueTool } from '@/tools/linear/add_label_to_issue' +import { linearArchiveIssueTool } from '@/tools/linear/archive_issue' +import { linearArchiveLabelTool } from '@/tools/linear/archive_label' +import { linearArchiveProjectTool } from '@/tools/linear/archive_project' +// Attachment operations +import { linearCreateAttachmentTool } from '@/tools/linear/create_attachment' +// Comment operations +import { linearCreateCommentTool } from '@/tools/linear/create_comment' +import { linearCreateCycleTool } from '@/tools/linear/create_cycle' +// Favorite operations +import { linearCreateFavoriteTool } from '@/tools/linear/create_favorite' import { linearCreateIssueTool } from '@/tools/linear/create_issue' +// Issue Relation operations +import { linearCreateIssueRelationTool } from '@/tools/linear/create_issue_relation' +import { linearCreateLabelTool } from '@/tools/linear/create_label' +import { linearCreateProjectTool } from '@/tools/linear/create_project' +import { linearCreateProjectLinkTool } from '@/tools/linear/create_project_link' +// Project Update operations +import { linearCreateProjectUpdateTool } from '@/tools/linear/create_project_update' +import { linearCreateWorkflowStateTool } from '@/tools/linear/create_workflow_state' +import { linearDeleteAttachmentTool } from '@/tools/linear/delete_attachment' +import { linearDeleteCommentTool } from '@/tools/linear/delete_comment' +import { linearDeleteIssueTool } from '@/tools/linear/delete_issue' +import { linearDeleteIssueRelationTool } from '@/tools/linear/delete_issue_relation' +import { linearGetActiveCycleTool } from '@/tools/linear/get_active_cycle' +import { linearGetCycleTool } from '@/tools/linear/get_cycle' +import { linearGetIssueTool } from '@/tools/linear/get_issue' +import { linearGetProjectTool } from '@/tools/linear/get_project' +import { linearGetViewerTool } from '@/tools/linear/get_viewer' +import { linearListAttachmentsTool } from '@/tools/linear/list_attachments' +import { linearListCommentsTool } from '@/tools/linear/list_comments' +// Cycle operations +import { linearListCyclesTool } from '@/tools/linear/list_cycles' +import { linearListFavoritesTool } from '@/tools/linear/list_favorites' +import { linearListIssueRelationsTool } from '@/tools/linear/list_issue_relations' +// Label operations +import { linearListLabelsTool } from '@/tools/linear/list_labels' +// Notification operations +import { linearListNotificationsTool } from '@/tools/linear/list_notifications' +import { linearListProjectUpdatesTool } from '@/tools/linear/list_project_updates' +// Project operations +import { linearListProjectsTool } from '@/tools/linear/list_projects' +import { linearListTeamsTool } from '@/tools/linear/list_teams' +// User and Team operations +import { linearListUsersTool } from '@/tools/linear/list_users' +// Workflow State operations +import { linearListWorkflowStatesTool } from '@/tools/linear/list_workflow_states' import { linearReadIssuesTool } from '@/tools/linear/read_issues' +import { linearRemoveLabelFromIssueTool } from '@/tools/linear/remove_label_from_issue' +import { linearSearchIssuesTool } from '@/tools/linear/search_issues' +import { linearUnarchiveIssueTool } from '@/tools/linear/unarchive_issue' +import { linearUpdateAttachmentTool } from '@/tools/linear/update_attachment' +import { linearUpdateCommentTool } from '@/tools/linear/update_comment' +import { linearUpdateIssueTool } from '@/tools/linear/update_issue' +import { linearUpdateLabelTool } from '@/tools/linear/update_label' +import { linearUpdateNotificationTool } from '@/tools/linear/update_notification' +import { linearUpdateProjectTool } from '@/tools/linear/update_project' +import { linearUpdateWorkflowStateTool } from '@/tools/linear/update_workflow_state' -export { linearReadIssuesTool, linearCreateIssueTool } +export { + // Issue operations + linearReadIssuesTool, + linearCreateIssueTool, + linearGetIssueTool, + linearUpdateIssueTool, + linearArchiveIssueTool, + linearUnarchiveIssueTool, + linearDeleteIssueTool, + linearAddLabelToIssueTool, + linearRemoveLabelFromIssueTool, + linearSearchIssuesTool, + // Comment operations + linearCreateCommentTool, + linearUpdateCommentTool, + linearDeleteCommentTool, + linearListCommentsTool, + // Project operations + linearListProjectsTool, + linearGetProjectTool, + linearCreateProjectTool, + linearUpdateProjectTool, + linearArchiveProjectTool, + // User and Team operations + linearListUsersTool, + linearListTeamsTool, + linearGetViewerTool, + // Label operations + linearListLabelsTool, + linearCreateLabelTool, + linearUpdateLabelTool, + linearArchiveLabelTool, + // Workflow State operations + linearListWorkflowStatesTool, + linearCreateWorkflowStateTool, + linearUpdateWorkflowStateTool, + // Cycle operations + linearListCyclesTool, + linearGetCycleTool, + linearCreateCycleTool, + linearGetActiveCycleTool, + // Attachment operations + linearCreateAttachmentTool, + linearListAttachmentsTool, + linearUpdateAttachmentTool, + linearDeleteAttachmentTool, + // Issue Relation operations + linearCreateIssueRelationTool, + linearListIssueRelationsTool, + linearDeleteIssueRelationTool, + // Favorite operations + linearCreateFavoriteTool, + linearListFavoritesTool, + // Project Update operations + linearCreateProjectUpdateTool, + linearListProjectUpdatesTool, + linearCreateProjectLinkTool, + // Notification operations + linearListNotificationsTool, + linearUpdateNotificationTool, +} diff --git a/apps/sim/tools/linear/list_attachments.ts b/apps/sim/tools/linear/list_attachments.ts new file mode 100644 index 0000000000..20e40bf550 --- /dev/null +++ b/apps/sim/tools/linear/list_attachments.ts @@ -0,0 +1,127 @@ +import type { + LinearListAttachmentsParams, + LinearListAttachmentsResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListAttachmentsTool: ToolConfig< + LinearListAttachmentsParams, + LinearListAttachmentsResponse +> = { + id: 'linear_list_attachments', + name: 'Linear List Attachments', + description: 'List all attachments on an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Issue ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of attachments to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListAttachments($issueId: String!, $first: Int, $after: String) { + issue(id: $issueId) { + attachments(first: $first, after: $after) { + nodes { + id + title + subtitle + url + createdAt + updatedAt + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + variables: { + issueId: params.issueId, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list attachments', + output: {}, + } + } + + const result = data.data.issue.attachments + return { + success: true, + output: { + attachments: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + attachments: { + type: 'array', + description: 'Array of attachments', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Attachment ID' }, + title: { type: 'string', description: 'Attachment title' }, + subtitle: { type: 'string', description: 'Attachment subtitle' }, + url: { type: 'string', description: 'Attachment URL' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_comments.ts b/apps/sim/tools/linear/list_comments.ts new file mode 100644 index 0000000000..1f9db2e5c6 --- /dev/null +++ b/apps/sim/tools/linear/list_comments.ts @@ -0,0 +1,131 @@ +import type { LinearListCommentsParams, LinearListCommentsResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListCommentsTool: ToolConfig< + LinearListCommentsParams, + LinearListCommentsResponse +> = { + id: 'linear_list_comments', + name: 'Linear List Comments', + description: 'List all comments on an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of comments to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListComments($issueId: String!, $first: Int, $after: String) { + issue(id: $issueId) { + comments(first: $first, after: $after) { + nodes { + id + body + createdAt + updatedAt + user { + id + name + email + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + variables: { + issueId: params.issueId, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list comments', + output: {}, + } + } + + const result = data.data.issue.comments + return { + success: true, + output: { + comments: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + comments: { + type: 'array', + description: 'Array of comments on the issue', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Comment ID' }, + body: { type: 'string', description: 'Comment text' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + user: { type: 'object', description: 'User who created the comment' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + properties: { + hasNextPage: { type: 'boolean', description: 'Whether there are more results' }, + endCursor: { type: 'string', description: 'Cursor for next page' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/list_cycles.ts b/apps/sim/tools/linear/list_cycles.ts new file mode 100644 index 0000000000..efc9e446ce --- /dev/null +++ b/apps/sim/tools/linear/list_cycles.ts @@ -0,0 +1,134 @@ +import type { LinearListCyclesParams, LinearListCyclesResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListCyclesTool: ToolConfig = { + id: 'linear_list_cycles', + name: 'Linear List Cycles', + description: 'List cycles (sprints/iterations) in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by team ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of cycles to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const filter: Record = {} + if (params.teamId) { + filter.team = { id: { eq: params.teamId } } + } + + return { + query: ` + query ListCycles($filter: CycleFilter, $first: Int, $after: String) { + cycles(filter: $filter, first: $first, after: $after) { + nodes { + id + number + name + startsAt + endsAt + completedAt + progress + team { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + filter: Object.keys(filter).length > 0 ? filter : undefined, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list cycles', + output: {}, + } + } + + const result = data.data.cycles + return { + success: true, + output: { + cycles: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + cycles: { + type: 'array', + description: 'Array of cycles', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Cycle ID' }, + number: { type: 'number', description: 'Cycle number' }, + name: { type: 'string', description: 'Cycle name' }, + startsAt: { type: 'string', description: 'Start date' }, + endsAt: { type: 'string', description: 'End date' }, + completedAt: { type: 'string', description: 'Completion date' }, + progress: { type: 'number', description: 'Progress percentage (0-1)' }, + team: { type: 'object', description: 'Team this cycle belongs to' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_favorites.ts b/apps/sim/tools/linear/list_favorites.ts new file mode 100644 index 0000000000..b26a9deb93 --- /dev/null +++ b/apps/sim/tools/linear/list_favorites.ts @@ -0,0 +1,123 @@ +import type { LinearListFavoritesParams, LinearListFavoritesResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListFavoritesTool: ToolConfig< + LinearListFavoritesParams, + LinearListFavoritesResponse +> = { + id: 'linear_list_favorites', + name: 'Linear List Favorites', + description: 'List all bookmarked items for the current user in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of favorites to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListFavorites($first: Int, $after: String) { + favorites(first: $first, after: $after) { + nodes { + id + type + issue { + id + title + } + project { + id + name + } + cycle { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list favorites', + output: {}, + } + } + + const result = data.data.favorites + return { + success: true, + output: { + favorites: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + favorites: { + type: 'array', + description: 'Array of favorited items', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Favorite ID' }, + type: { type: 'string', description: 'Favorite type' }, + issue: { type: 'object', description: 'Favorited issue' }, + project: { type: 'object', description: 'Favorited project' }, + cycle: { type: 'object', description: 'Favorited cycle' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_issue_relations.ts b/apps/sim/tools/linear/list_issue_relations.ts new file mode 100644 index 0000000000..26caa99ef0 --- /dev/null +++ b/apps/sim/tools/linear/list_issue_relations.ts @@ -0,0 +1,130 @@ +import type { + LinearListIssueRelationsParams, + LinearListIssueRelationsResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListIssueRelationsTool: ToolConfig< + LinearListIssueRelationsParams, + LinearListIssueRelationsResponse +> = { + id: 'linear_list_issue_relations', + name: 'Linear List Issue Relations', + description: 'List all relations (dependencies) for an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Issue ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of relations to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListIssueRelations($issueId: String!, $first: Int, $after: String) { + issue(id: $issueId) { + relations(first: $first, after: $after) { + nodes { + id + type + issue { + id + title + } + relatedIssue { + id + title + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + variables: { + issueId: params.issueId, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list issue relations', + output: {}, + } + } + + const result = data.data.issue.relations + return { + success: true, + output: { + relations: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + relations: { + type: 'array', + description: 'Array of issue relations', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Relation ID' }, + type: { type: 'string', description: 'Relation type' }, + issue: { type: 'object', description: 'Source issue' }, + relatedIssue: { type: 'object', description: 'Target issue' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_labels.ts b/apps/sim/tools/linear/list_labels.ts new file mode 100644 index 0000000000..55a7f4b74f --- /dev/null +++ b/apps/sim/tools/linear/list_labels.ts @@ -0,0 +1,128 @@ +import type { LinearListLabelsParams, LinearListLabelsResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListLabelsTool: ToolConfig = { + id: 'linear_list_labels', + name: 'Linear List Labels', + description: 'List all labels in Linear workspace or team', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by team ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of labels to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const filter: Record = {} + if (params.teamId) { + filter.team = { id: { eq: params.teamId } } + } + + return { + query: ` + query ListLabels($filter: IssueLabelFilter, $first: Int, $after: String) { + issueLabels(filter: $filter, first: $first, after: $after) { + nodes { + id + name + color + description + team { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + filter: Object.keys(filter).length > 0 ? filter : undefined, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list labels', + output: {}, + } + } + + const result = data.data.issueLabels + return { + success: true, + output: { + labels: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + labels: { + type: 'array', + description: 'Array of labels', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Label ID' }, + name: { type: 'string', description: 'Label name' }, + color: { type: 'string', description: 'Label color (hex)' }, + description: { type: 'string', description: 'Label description' }, + team: { type: 'object', description: 'Team this label belongs to' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_notifications.ts b/apps/sim/tools/linear/list_notifications.ts new file mode 100644 index 0000000000..56755e4e34 --- /dev/null +++ b/apps/sim/tools/linear/list_notifications.ts @@ -0,0 +1,120 @@ +import type { + LinearListNotificationsParams, + LinearListNotificationsResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListNotificationsTool: ToolConfig< + LinearListNotificationsParams, + LinearListNotificationsResponse +> = { + id: 'linear_list_notifications', + name: 'Linear List Notifications', + description: 'List notifications for the current user in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of notifications to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListNotifications($first: Int, $after: String) { + notifications(first: $first, after: $after) { + nodes { + id + type + createdAt + readAt + issue { + id + title + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list notifications', + output: {}, + } + } + + const result = data.data.notifications + return { + success: true, + output: { + notifications: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + notifications: { + type: 'array', + description: 'Array of notifications', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Notification ID' }, + type: { type: 'string', description: 'Notification type' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + readAt: { type: 'string', description: 'Read timestamp (null if unread)' }, + issue: { type: 'object', description: 'Related issue' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_project_updates.ts b/apps/sim/tools/linear/list_project_updates.ts new file mode 100644 index 0000000000..7c125a5c0c --- /dev/null +++ b/apps/sim/tools/linear/list_project_updates.ts @@ -0,0 +1,129 @@ +import type { + LinearListProjectUpdatesParams, + LinearListProjectUpdatesResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListProjectUpdatesTool: ToolConfig< + LinearListProjectUpdatesParams, + LinearListProjectUpdatesResponse +> = { + id: 'linear_list_project_updates', + name: 'Linear List Project Updates', + description: 'List all status updates for a project in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of updates to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListProjectUpdates($projectId: String!, $first: Int, $after: String) { + project(id: $projectId) { + projectUpdates(first: $first, after: $after) { + nodes { + id + body + health + createdAt + user { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `, + variables: { + projectId: params.projectId, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list project updates', + output: {}, + } + } + + const result = data.data.project.projectUpdates + return { + success: true, + output: { + updates: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + updates: { + type: 'array', + description: 'Array of project updates', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Update ID' }, + body: { type: 'string', description: 'Update message' }, + health: { type: 'string', description: 'Project health' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + user: { type: 'object', description: 'User who created the update' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_projects.ts b/apps/sim/tools/linear/list_projects.ts new file mode 100644 index 0000000000..69778f165f --- /dev/null +++ b/apps/sim/tools/linear/list_projects.ts @@ -0,0 +1,167 @@ +import type { LinearListProjectsParams, LinearListProjectsResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListProjectsTool: ToolConfig< + LinearListProjectsParams, + LinearListProjectsResponse +> = { + id: 'linear_list_projects', + name: 'Linear List Projects', + description: 'List projects in Linear with optional filtering', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by team ID', + }, + includeArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include archived projects', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of projects to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const filter: Record = {} + if (params.teamId) { + filter.team = { id: { eq: params.teamId } } + } + + return { + query: ` + query ListProjects($filter: ProjectFilter, $first: Int, $after: String, $includeArchived: Boolean) { + projects(filter: $filter, first: $first, after: $after, includeArchived: $includeArchived) { + nodes { + id + name + description + state + priority + startDate + targetDate + completedAt + canceledAt + archivedAt + url + lead { + id + name + } + teams { + nodes { + id + name + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + filter: Object.keys(filter).length > 0 ? filter : undefined, + first: params.first ? Number(params.first) : 50, + after: params.after, + includeArchived: params.includeArchived || false, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list projects', + output: {}, + } + } + + const result = data.data.projects + return { + success: true, + output: { + projects: result.nodes.map((project: any) => ({ + id: project.id, + name: project.name, + description: project.description, + state: project.state, + priority: project.priority, + startDate: project.startDate, + targetDate: project.targetDate, + completedAt: project.completedAt, + canceledAt: project.canceledAt, + archivedAt: project.archivedAt, + url: project.url, + lead: project.lead, + teams: project.teams?.nodes || [], + })), + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + projects: { + type: 'array', + description: 'Array of projects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + description: { type: 'string', description: 'Project description' }, + state: { type: 'string', description: 'Project state' }, + priority: { type: 'number', description: 'Project priority' }, + lead: { type: 'object', description: 'Project lead' }, + teams: { type: 'array', description: 'Teams associated with project' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_teams.ts b/apps/sim/tools/linear/list_teams.ts new file mode 100644 index 0000000000..e93055d556 --- /dev/null +++ b/apps/sim/tools/linear/list_teams.ts @@ -0,0 +1,109 @@ +import type { LinearListTeamsParams, LinearListTeamsResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListTeamsTool: ToolConfig = { + id: 'linear_list_teams', + name: 'Linear List Teams', + description: 'List all teams in the Linear workspace', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of teams to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListTeams($first: Int, $after: String) { + teams(first: $first, after: $after) { + nodes { + id + name + key + description + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list teams', + output: {}, + } + } + + const result = data.data.teams + return { + success: true, + output: { + teams: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + teams: { + type: 'array', + description: 'Array of teams', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Team ID' }, + name: { type: 'string', description: 'Team name' }, + key: { type: 'string', description: 'Team key (used in issue identifiers)' }, + description: { type: 'string', description: 'Team description' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_users.ts b/apps/sim/tools/linear/list_users.ts new file mode 100644 index 0000000000..ac1f3762bd --- /dev/null +++ b/apps/sim/tools/linear/list_users.ts @@ -0,0 +1,122 @@ +import type { LinearListUsersParams, LinearListUsersResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListUsersTool: ToolConfig = { + id: 'linear_list_users', + name: 'Linear List Users', + description: 'List all users in the Linear workspace', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + includeDisabled: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include disabled/inactive users', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of users to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + query ListUsers($includeDisabled: Boolean, $first: Int, $after: String) { + users(includeDisabled: $includeDisabled, first: $first, after: $after) { + nodes { + id + name + email + displayName + active + admin + avatarUrl + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + includeDisabled: params.includeDisabled || false, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list users', + output: {}, + } + } + + const result = data.data.users + return { + success: true, + output: { + users: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + users: { + type: 'array', + description: 'Array of workspace users', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'User name' }, + email: { type: 'string', description: 'User email' }, + displayName: { type: 'string', description: 'Display name' }, + active: { type: 'boolean', description: 'Whether user is active' }, + admin: { type: 'boolean', description: 'Whether user is admin' }, + avatarUrl: { type: 'string', description: 'Avatar URL' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/list_workflow_states.ts b/apps/sim/tools/linear/list_workflow_states.ts new file mode 100644 index 0000000000..ca8ade3980 --- /dev/null +++ b/apps/sim/tools/linear/list_workflow_states.ts @@ -0,0 +1,139 @@ +import type { + LinearListWorkflowStatesParams, + LinearListWorkflowStatesResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearListWorkflowStatesTool: ToolConfig< + LinearListWorkflowStatesParams, + LinearListWorkflowStatesResponse +> = { + id: 'linear_list_workflow_states', + name: 'Linear List Workflow States', + description: 'List all workflow states (statuses) in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by team ID', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of states to return (default: 50)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor for pagination', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const filter: Record = {} + if (params.teamId) { + filter.team = { id: { eq: params.teamId } } + } + + return { + query: ` + query ListWorkflowStates($filter: WorkflowStateFilter, $first: Int, $after: String) { + workflowStates(filter: $filter, first: $first, after: $after) { + nodes { + id + name + type + color + position + team { + id + name + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + filter: Object.keys(filter).length > 0 ? filter : undefined, + first: params.first ? Number(params.first) : 50, + after: params.after, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to list workflow states', + output: {}, + } + } + + const result = data.data.workflowStates + return { + success: true, + output: { + states: result.nodes, + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + states: { + type: 'array', + description: 'Array of workflow states', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'State ID' }, + name: { type: 'string', description: 'State name (e.g., "Todo", "In Progress")' }, + type: { + type: 'string', + description: 'State type (e.g., "unstarted", "started", "completed")', + }, + color: { type: 'string', description: 'State color' }, + position: { type: 'number', description: 'State position in workflow' }, + team: { type: 'object', description: 'Team this state belongs to' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + }, + }, +} diff --git a/apps/sim/tools/linear/remove_label_from_issue.ts b/apps/sim/tools/linear/remove_label_from_issue.ts new file mode 100644 index 0000000000..a6fb85f125 --- /dev/null +++ b/apps/sim/tools/linear/remove_label_from_issue.ts @@ -0,0 +1,93 @@ +import type { + LinearRemoveLabelFromIssueParams, + LinearRemoveLabelResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearRemoveLabelFromIssueTool: ToolConfig< + LinearRemoveLabelFromIssueParams, + LinearRemoveLabelResponse +> = { + id: 'linear_remove_label_from_issue', + name: 'Linear Remove Label from Issue', + description: 'Remove a label from an issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID', + }, + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label ID to remove from the issue', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation RemoveLabelFromIssue($issueId: String!, $labelId: String!) { + issueRemoveLabel(id: $issueId, labelId: $labelId) { + success + } + } + `, + variables: { + issueId: params.issueId, + labelId: params.labelId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to remove label from issue', + output: {}, + } + } + + return { + success: data.data.issueRemoveLabel.success, + output: { + success: data.data.issueRemoveLabel.success, + issueId: response.ok ? data.data.issueRemoveLabel.success : '', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the label was successfully removed', + }, + issueId: { + type: 'string', + description: 'The ID of the issue', + }, + }, +} diff --git a/apps/sim/tools/linear/search_issues.ts b/apps/sim/tools/linear/search_issues.ts new file mode 100644 index 0000000000..ac2b726641 --- /dev/null +++ b/apps/sim/tools/linear/search_issues.ts @@ -0,0 +1,185 @@ +import type { LinearSearchIssuesParams, LinearSearchIssuesResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearSearchIssuesTool: ToolConfig< + LinearSearchIssuesParams, + LinearSearchIssuesResponse +> = { + id: 'linear_search_issues', + name: 'Linear Search Issues', + description: 'Search for issues in Linear using full-text search', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query string', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by team ID', + }, + includeArchived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Include archived issues in search results', + }, + first: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default: 50)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const filter: Record = {} + if (params.teamId) { + filter.team = { id: { eq: params.teamId } } + } + + return { + query: ` + query SearchIssues($query: String!, $filter: IssueFilter, $first: Int, $includeArchived: Boolean) { + issueSearch(query: $query, filter: $filter, first: $first, includeArchived: $includeArchived) { + nodes { + id + title + description + priority + estimate + url + createdAt + updatedAt + archivedAt + state { + id + name + type + } + assignee { + id + name + email + } + team { + id + name + } + project { + id + name + } + labels { + nodes { + id + name + color + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `, + variables: { + query: params.query, + filter: Object.keys(filter).length > 0 ? filter : undefined, + first: params.first ? Number(params.first) : 50, + includeArchived: params.includeArchived || false, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to search issues', + output: {}, + } + } + + const result = data.data.issueSearch + return { + success: true, + output: { + issues: result.nodes.map((issue: any) => ({ + id: issue.id, + title: issue.title, + description: issue.description, + priority: issue.priority, + estimate: issue.estimate, + url: issue.url, + createdAt: issue.createdAt, + updatedAt: issue.updatedAt, + archivedAt: issue.archivedAt, + state: issue.state, + assignee: issue.assignee, + teamId: issue.team?.id, + projectId: issue.project?.id, + labels: issue.labels?.nodes || [], + })), + pageInfo: { + hasNextPage: result.pageInfo.hasNextPage, + endCursor: result.pageInfo.endCursor, + }, + }, + } + }, + + outputs: { + issues: { + type: 'array', + description: 'Array of matching issues', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Issue ID' }, + title: { type: 'string', description: 'Issue title' }, + description: { type: 'string', description: 'Issue description' }, + priority: { type: 'number', description: 'Issue priority' }, + state: { type: 'object', description: 'Issue state' }, + assignee: { type: 'object', description: 'Assigned user' }, + labels: { type: 'array', description: 'Issue labels' }, + }, + }, + }, + pageInfo: { + type: 'object', + description: 'Pagination information', + properties: { + hasNextPage: { type: 'boolean', description: 'Whether there are more results' }, + endCursor: { type: 'string', description: 'Cursor for next page' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts index b56e78d1f1..a55975d340 100644 --- a/apps/sim/tools/linear/types.ts +++ b/apps/sim/tools/linear/types.ts @@ -1,20 +1,143 @@ import type { ToolResponse } from '@/tools/types' +// ===== Core Types ===== + export interface LinearIssue { id: string title: string description?: string - state?: string - teamId: string - projectId: string + state?: { + id: string + name: string + type: string + } + assignee?: { + id: string + name: string + email: string + } + priority?: number + estimate?: number + teamId?: string + projectId?: string + labels?: Array<{ + id: string + name: string + color: string + }> + createdAt?: string + updatedAt?: string + completedAt?: string + canceledAt?: string + archivedAt?: string + url?: string +} + +export interface LinearComment { + id: string + body: string + createdAt: string + updatedAt?: string + user: { + id: string + name: string + email?: string + } + issue?: { + id: string + title: string + } +} + +export interface LinearProject { + id: string + name: string + description?: string + state: string + priority: number + startDate?: string + targetDate?: string + completedAt?: string + canceledAt?: string + archivedAt?: string + lead?: { + id: string + name: string + } + teams?: Array<{ + id: string + name: string + }> + url?: string +} + +export interface LinearUser { + id: string + name: string + email: string + displayName: string + active: boolean + admin: boolean + avatarUrl?: string +} + +export interface LinearTeam { + id: string + name: string + key: string + description?: string +} + +export interface LinearLabel { + id: string + name: string + color: string + description?: string + team?: { + id: string + name: string + } +} + +export interface LinearWorkflowState { + id: string + name: string + type: string + color: string + position: number + team: { + id: string + name: string + } } +export interface LinearCycle { + id: string + number: number + name?: string + startsAt: string + endsAt: string + completedAt?: string + progress: number + team: { + id: string + name: string + } +} + +// ===== Request Params ===== + export interface LinearReadIssuesParams { teamId: string projectId: string accessToken?: string } +export interface LinearGetIssueParams { + issueId: string + accessToken?: string +} + export interface LinearCreateIssueParams { teamId: string projectId: string @@ -23,16 +146,762 @@ export interface LinearCreateIssueParams { accessToken?: string } +export interface LinearUpdateIssueParams { + issueId: string + title?: string + description?: string + stateId?: string + assigneeId?: string + priority?: number + estimate?: number + labelIds?: string[] + accessToken?: string +} + +export interface LinearArchiveIssueParams { + issueId: string + accessToken?: string +} + +export interface LinearUnarchiveIssueParams { + issueId: string + accessToken?: string +} + +export interface LinearDeleteIssueParams { + issueId: string + accessToken?: string +} + +export interface LinearAddLabelToIssueParams { + issueId: string + labelId: string + accessToken?: string +} + +export interface LinearRemoveLabelFromIssueParams { + issueId: string + labelId: string + accessToken?: string +} + +export interface LinearSearchIssuesParams { + query: string + teamId?: string + includeArchived?: boolean + first?: number + accessToken?: string +} + +export interface LinearCreateCommentParams { + issueId: string + body: string + accessToken?: string +} + +export interface LinearUpdateCommentParams { + commentId: string + body: string + accessToken?: string +} + +export interface LinearDeleteCommentParams { + commentId: string + accessToken?: string +} + +export interface LinearListCommentsParams { + issueId: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearListProjectsParams { + teamId?: string + includeArchived?: boolean + first?: number + after?: string + accessToken?: string +} + +export interface LinearGetProjectParams { + projectId: string + accessToken?: string +} + +export interface LinearCreateProjectParams { + teamId: string + name: string + description?: string + leadId?: string + startDate?: string + targetDate?: string + priority?: number + accessToken?: string +} + +export interface LinearUpdateProjectParams { + projectId: string + name?: string + description?: string + state?: string + leadId?: string + startDate?: string + targetDate?: string + priority?: number + accessToken?: string +} + +export interface LinearArchiveProjectParams { + projectId: string + accessToken?: string +} + +export interface LinearListUsersParams { + includeDisabled?: boolean + first?: number + after?: string + accessToken?: string +} + +export interface LinearListTeamsParams { + first?: number + after?: string + accessToken?: string +} + +export interface LinearGetViewerParams { + accessToken?: string +} + +export interface LinearListLabelsParams { + teamId?: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearCreateLabelParams { + name: string + color?: string + description?: string + teamId?: string + accessToken?: string +} + +export interface LinearUpdateLabelParams { + labelId: string + name?: string + color?: string + description?: string + accessToken?: string +} + +export interface LinearArchiveLabelParams { + labelId: string + accessToken?: string +} + +export interface LinearListWorkflowStatesParams { + teamId?: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearCreateWorkflowStateParams { + teamId: string + name: string + color: string + type: string + description?: string + position?: number + accessToken?: string +} + +export interface LinearUpdateWorkflowStateParams { + stateId: string + name?: string + color?: string + description?: string + position?: number + accessToken?: string +} + +export interface LinearListCyclesParams { + teamId?: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearGetCycleParams { + cycleId: string + accessToken?: string +} + +export interface LinearCreateCycleParams { + teamId: string + startsAt: string + endsAt: string + name?: string + accessToken?: string +} + +export interface LinearGetActiveCycleParams { + teamId: string + accessToken?: string +} + +export interface LinearCreateAttachmentParams { + issueId: string + url: string + title?: string + subtitle?: string + accessToken?: string +} + +export interface LinearListAttachmentsParams { + issueId: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearUpdateAttachmentParams { + attachmentId: string + title?: string + subtitle?: string + accessToken?: string +} + +export interface LinearDeleteAttachmentParams { + attachmentId: string + accessToken?: string +} + +export interface LinearCreateIssueRelationParams { + issueId: string + relatedIssueId: string + type: string + accessToken?: string +} + +export interface LinearListIssueRelationsParams { + issueId: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearDeleteIssueRelationParams { + relationId: string + accessToken?: string +} + +export interface LinearCreateFavoriteParams { + issueId?: string + projectId?: string + cycleId?: string + labelId?: string + accessToken?: string +} + +export interface LinearListFavoritesParams { + first?: number + after?: string + accessToken?: string +} + +export interface LinearCreateProjectUpdateParams { + projectId: string + body: string + health?: string + accessToken?: string +} + +export interface LinearListProjectUpdatesParams { + projectId: string + first?: number + after?: string + accessToken?: string +} + +export interface LinearCreateProjectLinkParams { + projectId: string + url: string + label?: string + accessToken?: string +} + +export interface LinearListNotificationsParams { + first?: number + after?: string + accessToken?: string +} + +export interface LinearUpdateNotificationParams { + notificationId: string + readAt?: string + accessToken?: string +} + +// ===== Response Types ===== + export interface LinearReadIssuesResponse extends ToolResponse { output: { - issues: LinearIssue[] + issues?: LinearIssue[] + } +} + +export interface LinearGetIssueResponse extends ToolResponse { + output: { + issue?: LinearIssue } } export interface LinearCreateIssueResponse extends ToolResponse { output: { - issue: LinearIssue + issue?: LinearIssue + } +} + +export interface LinearUpdateIssueResponse extends ToolResponse { + output: { + issue?: LinearIssue + } +} + +export interface LinearArchiveIssueResponse extends ToolResponse { + output: { + success?: boolean + issueId?: string + } +} + +export interface LinearUnarchiveIssueResponse extends ToolResponse { + output: { + success?: boolean + issueId?: string + } +} + +export interface LinearDeleteIssueResponse extends ToolResponse { + output: { + success?: boolean + } +} + +export interface LinearAddLabelResponse extends ToolResponse { + output: { + success?: boolean + issueId?: string + } +} + +export interface LinearRemoveLabelResponse extends ToolResponse { + output: { + success?: boolean + issueId?: string + } +} + +export interface LinearSearchIssuesResponse extends ToolResponse { + output: { + issues?: LinearIssue[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearCreateCommentResponse extends ToolResponse { + output: { + comment?: LinearComment + } +} + +export interface LinearUpdateCommentResponse extends ToolResponse { + output: { + comment?: LinearComment + } +} + +export interface LinearDeleteCommentResponse extends ToolResponse { + output: { + success?: boolean + } +} + +export interface LinearListCommentsResponse extends ToolResponse { + output: { + comments?: LinearComment[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearListProjectsResponse extends ToolResponse { + output: { + projects?: LinearProject[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearGetProjectResponse extends ToolResponse { + output: { + project?: LinearProject + } +} + +export interface LinearCreateProjectResponse extends ToolResponse { + output: { + project?: LinearProject + } +} + +export interface LinearUpdateProjectResponse extends ToolResponse { + output: { + project?: LinearProject + } +} + +export interface LinearArchiveProjectResponse extends ToolResponse { + output: { + success?: boolean + projectId?: string + } +} + +export interface LinearListUsersResponse extends ToolResponse { + output: { + users?: LinearUser[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearListTeamsResponse extends ToolResponse { + output: { + teams?: LinearTeam[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearGetViewerResponse extends ToolResponse { + output: { + user?: LinearUser + } +} + +export interface LinearListLabelsResponse extends ToolResponse { + output: { + labels?: LinearLabel[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearCreateLabelResponse extends ToolResponse { + output: { + label?: LinearLabel + } +} + +export interface LinearUpdateLabelResponse extends ToolResponse { + output: { + label?: LinearLabel + } +} + +export interface LinearArchiveLabelResponse extends ToolResponse { + output: { + success?: boolean + labelId?: string + } +} + +export interface LinearListWorkflowStatesResponse extends ToolResponse { + output: { + states?: LinearWorkflowState[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearCreateWorkflowStateResponse extends ToolResponse { + output: { + state?: LinearWorkflowState + } +} + +export interface LinearUpdateWorkflowStateResponse extends ToolResponse { + output: { + state?: LinearWorkflowState + } +} + +export interface LinearListCyclesResponse extends ToolResponse { + output: { + cycles?: LinearCycle[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearGetCycleResponse extends ToolResponse { + output: { + cycle?: LinearCycle + } +} + +export interface LinearCreateCycleResponse extends ToolResponse { + output: { + cycle?: LinearCycle + } +} + +export interface LinearGetActiveCycleResponse extends ToolResponse { + output: { + cycle?: LinearCycle | null + } +} + +export interface LinearAttachment { + id: string + title?: string + subtitle?: string + url: string + createdAt: string + updatedAt?: string +} + +export interface LinearCreateAttachmentResponse extends ToolResponse { + output: { + attachment?: LinearAttachment + } +} + +export interface LinearListAttachmentsResponse extends ToolResponse { + output: { + attachments?: LinearAttachment[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearUpdateAttachmentResponse extends ToolResponse { + output: { + attachment?: LinearAttachment + } +} + +export interface LinearDeleteAttachmentResponse extends ToolResponse { + output: { + success?: boolean + } +} + +export interface LinearIssueRelation { + id: string + type: string + issue: { + id: string + title: string + } + relatedIssue: { + id: string + title: string + } +} + +export interface LinearCreateIssueRelationResponse extends ToolResponse { + output: { + relation?: LinearIssueRelation + } +} + +export interface LinearListIssueRelationsResponse extends ToolResponse { + output: { + relations?: LinearIssueRelation[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearDeleteIssueRelationResponse extends ToolResponse { + output: { + success?: boolean + } +} + +export interface LinearFavorite { + id: string + type: string + issue?: { + id: string + title: string + } + project?: { + id: string + name: string + } + cycle?: { + id: string + name: string + } +} + +export interface LinearCreateFavoriteResponse extends ToolResponse { + output: { + favorite?: LinearFavorite + } +} + +export interface LinearListFavoritesResponse extends ToolResponse { + output: { + favorites?: LinearFavorite[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearProjectUpdate { + id: string + body: string + health: string + createdAt: string + user: { + id: string + name: string + } +} + +export interface LinearCreateProjectUpdateResponse extends ToolResponse { + output: { + update?: LinearProjectUpdate + } +} + +export interface LinearListProjectUpdatesResponse extends ToolResponse { + output: { + updates?: LinearProjectUpdate[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearProjectLink { + id: string + url: string + label: string + createdAt: string +} + +export interface LinearCreateProjectLinkResponse extends ToolResponse { + output: { + link?: LinearProjectLink + } +} + +export interface LinearNotification { + id: string + type: string + createdAt: string + readAt?: string + issue?: { + id: string + title: string + } +} + +export interface LinearListNotificationsResponse extends ToolResponse { + output: { + notifications?: LinearNotification[] + pageInfo?: { + hasNextPage: boolean + endCursor?: string + } + } +} + +export interface LinearUpdateNotificationResponse extends ToolResponse { + output: { + notification?: LinearNotification } } -export type LinearResponse = LinearReadIssuesResponse | LinearCreateIssueResponse +export type LinearResponse = + | LinearReadIssuesResponse + | LinearGetIssueResponse + | LinearCreateIssueResponse + | LinearUpdateIssueResponse + | LinearArchiveIssueResponse + | LinearUnarchiveIssueResponse + | LinearDeleteIssueResponse + | LinearAddLabelResponse + | LinearRemoveLabelResponse + | LinearSearchIssuesResponse + | LinearCreateCommentResponse + | LinearUpdateCommentResponse + | LinearDeleteCommentResponse + | LinearListCommentsResponse + | LinearListProjectsResponse + | LinearGetProjectResponse + | LinearCreateProjectResponse + | LinearUpdateProjectResponse + | LinearArchiveProjectResponse + | LinearListUsersResponse + | LinearListTeamsResponse + | LinearGetViewerResponse + | LinearListLabelsResponse + | LinearCreateLabelResponse + | LinearUpdateLabelResponse + | LinearArchiveLabelResponse + | LinearListWorkflowStatesResponse + | LinearCreateWorkflowStateResponse + | LinearUpdateWorkflowStateResponse + | LinearListCyclesResponse + | LinearGetCycleResponse + | LinearCreateCycleResponse + | LinearGetActiveCycleResponse + | LinearCreateAttachmentResponse + | LinearListAttachmentsResponse + | LinearUpdateAttachmentResponse + | LinearDeleteAttachmentResponse + | LinearCreateIssueRelationResponse + | LinearListIssueRelationsResponse + | LinearDeleteIssueRelationResponse + | LinearCreateFavoriteResponse + | LinearListFavoritesResponse + | LinearCreateProjectUpdateResponse + | LinearListProjectUpdatesResponse + | LinearCreateProjectLinkResponse + | LinearListNotificationsResponse + | LinearUpdateNotificationResponse diff --git a/apps/sim/tools/linear/unarchive_issue.ts b/apps/sim/tools/linear/unarchive_issue.ts new file mode 100644 index 0000000000..8adc8a0cd7 --- /dev/null +++ b/apps/sim/tools/linear/unarchive_issue.ts @@ -0,0 +1,83 @@ +import type { LinearUnarchiveIssueParams, LinearUnarchiveIssueResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUnarchiveIssueTool: ToolConfig< + LinearUnarchiveIssueParams, + LinearUnarchiveIssueResponse +> = { + id: 'linear_unarchive_issue', + name: 'Linear Unarchive Issue', + description: 'Unarchive (restore) an archived issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID to unarchive', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation UnarchiveIssue($id: String!) { + issueUnarchive(id: $id) { + success + } + } + `, + variables: { + id: params.issueId, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to unarchive issue', + output: {}, + } + } + + return { + success: data.data.issueUnarchive.success, + output: { + success: data.data.issueUnarchive.success, + issueId: response.ok ? data.data.issueUnarchive.success : '', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the unarchive operation was successful', + }, + issueId: { + type: 'string', + description: 'The ID of the unarchived issue', + }, + }, +} diff --git a/apps/sim/tools/linear/update_attachment.ts b/apps/sim/tools/linear/update_attachment.ts new file mode 100644 index 0000000000..5ec7dcc9d4 --- /dev/null +++ b/apps/sim/tools/linear/update_attachment.ts @@ -0,0 +1,124 @@ +import type { + LinearUpdateAttachmentParams, + LinearUpdateAttachmentResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateAttachmentTool: ToolConfig< + LinearUpdateAttachmentParams, + LinearUpdateAttachmentResponse +> = { + id: 'linear_update_attachment', + name: 'Linear Update Attachment', + description: 'Update an attachment metadata in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + attachmentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Attachment ID to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New attachment title', + }, + subtitle: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New attachment subtitle', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + if (params.title !== undefined) input.title = params.title + if (params.subtitle !== undefined) input.subtitle = params.subtitle + + return { + query: ` + mutation UpdateAttachment($id: String!, $input: AttachmentUpdateInput!) { + attachmentUpdate(id: $id, input: $input) { + success + attachment { + id + title + subtitle + url + updatedAt + } + } + } + `, + variables: { + id: params.attachmentId, + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update attachment', + output: {}, + } + } + + const result = data.data.attachmentUpdate + if (!result.success) { + return { + success: false, + error: 'Attachment update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + attachment: result.attachment, + }, + } + }, + + outputs: { + attachment: { + type: 'object', + description: 'The updated attachment', + properties: { + id: { type: 'string', description: 'Attachment ID' }, + title: { type: 'string', description: 'Attachment title' }, + subtitle: { type: 'string', description: 'Attachment subtitle' }, + url: { type: 'string', description: 'Attachment URL' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/update_comment.ts b/apps/sim/tools/linear/update_comment.ts new file mode 100644 index 0000000000..27666ecacd --- /dev/null +++ b/apps/sim/tools/linear/update_comment.ts @@ -0,0 +1,113 @@ +import type { LinearUpdateCommentParams, LinearUpdateCommentResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateCommentTool: ToolConfig< + LinearUpdateCommentParams, + LinearUpdateCommentResponse +> = { + id: 'linear_update_comment', + name: 'Linear Update Comment', + description: 'Edit a comment in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment ID to update', + }, + body: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New comment text (supports Markdown)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => ({ + query: ` + mutation UpdateComment($id: String!, $input: CommentUpdateInput!) { + commentUpdate(id: $id, input: $input) { + success + comment { + id + body + createdAt + updatedAt + user { + id + name + email + } + } + } + } + `, + variables: { + id: params.commentId, + input: { + body: params.body, + }, + }, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update comment', + output: {}, + } + } + + const result = data.data.commentUpdate + if (!result.success) { + return { + success: false, + error: 'Comment update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + comment: result.comment, + }, + } + }, + + outputs: { + comment: { + type: 'object', + description: 'The updated comment', + properties: { + id: { type: 'string', description: 'Comment ID' }, + body: { type: 'string', description: 'Comment text' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + user: { type: 'object', description: 'User who created the comment' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/update_issue.ts b/apps/sim/tools/linear/update_issue.ts new file mode 100644 index 0000000000..6a1c414512 --- /dev/null +++ b/apps/sim/tools/linear/update_issue.ts @@ -0,0 +1,197 @@ +import type { LinearUpdateIssueParams, LinearUpdateIssueResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateIssueTool: ToolConfig = + { + id: 'linear_update_issue', + name: 'Linear Update Issue', + description: 'Update an existing issue in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + issueId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Linear issue ID to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New issue title', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New issue description', + }, + stateId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Workflow state ID (status)', + }, + assigneeId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID to assign the issue to', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Priority (0=No priority, 1=Urgent, 2=High, 3=Normal, 4=Low)', + }, + estimate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Estimate in points', + }, + labelIds: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Array of label IDs to set on the issue', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + if (params.title !== undefined) input.title = params.title + if (params.description !== undefined) input.description = params.description + if (params.stateId !== undefined) input.stateId = params.stateId + if (params.assigneeId !== undefined) input.assigneeId = params.assigneeId + if (params.priority !== undefined) input.priority = Number(params.priority) + if (params.estimate !== undefined) input.estimate = Number(params.estimate) + if (params.labelIds !== undefined) input.labelIds = params.labelIds + + return { + query: ` + mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) { + issueUpdate(id: $id, input: $input) { + success + issue { + id + title + description + priority + estimate + url + updatedAt + state { + id + name + type + } + assignee { + id + name + email + } + team { + id + } + project { + id + } + labels { + nodes { + id + name + color + } + } + } + } + } + `, + variables: { + id: params.issueId, + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update issue', + output: {}, + } + } + + const result = data.data.issueUpdate + if (!result.success) { + return { + success: false, + error: 'Issue update was not successful', + output: {}, + } + } + + const issue = result.issue + return { + success: true, + output: { + issue: { + id: issue.id, + title: issue.title, + description: issue.description, + priority: issue.priority, + estimate: issue.estimate, + url: issue.url, + updatedAt: issue.updatedAt, + state: issue.state, + assignee: issue.assignee, + teamId: issue.team?.id, + projectId: issue.project?.id, + labels: issue.labels?.nodes || [], + }, + }, + } + }, + + outputs: { + issue: { + type: 'object', + description: 'The updated issue', + properties: { + id: { type: 'string', description: 'Issue ID' }, + title: { type: 'string', description: 'Issue title' }, + description: { type: 'string', description: 'Issue description' }, + priority: { type: 'number', description: 'Issue priority' }, + estimate: { type: 'number', description: 'Issue estimate' }, + state: { type: 'object', description: 'Issue state' }, + assignee: { type: 'object', description: 'Assigned user' }, + labels: { type: 'array', description: 'Issue labels' }, + updatedAt: { type: 'string', description: 'Last update timestamp' }, + }, + }, + }, + } diff --git a/apps/sim/tools/linear/update_label.ts b/apps/sim/tools/linear/update_label.ts new file mode 100644 index 0000000000..de710ad6f7 --- /dev/null +++ b/apps/sim/tools/linear/update_label.ts @@ -0,0 +1,128 @@ +import type { LinearUpdateLabelParams, LinearUpdateLabelResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateLabelTool: ToolConfig = + { + id: 'linear_update_label', + name: 'Linear Update Label', + description: 'Update an existing label in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + labelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Label ID to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New label name', + }, + color: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New label color (hex format)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New label description', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + if (params.name !== undefined) input.name = params.name + if (params.color !== undefined) input.color = params.color + if (params.description !== undefined) input.description = params.description + + return { + query: ` + mutation UpdateLabel($id: String!, $input: IssueLabelUpdateInput!) { + issueLabelUpdate(id: $id, input: $input) { + success + issueLabel { + id + name + color + description + team { + id + name + } + } + } + } + `, + variables: { + id: params.labelId, + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update label', + output: {}, + } + } + + const result = data.data.issueLabelUpdate + if (!result.success) { + return { + success: false, + error: 'Label update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + label: result.issueLabel, + }, + } + }, + + outputs: { + label: { + type: 'object', + description: 'The updated label', + properties: { + id: { type: 'string', description: 'Label ID' }, + name: { type: 'string', description: 'Label name' }, + color: { type: 'string', description: 'Label color' }, + description: { type: 'string', description: 'Label description' }, + }, + }, + }, + } diff --git a/apps/sim/tools/linear/update_notification.ts b/apps/sim/tools/linear/update_notification.ts new file mode 100644 index 0000000000..b297682141 --- /dev/null +++ b/apps/sim/tools/linear/update_notification.ts @@ -0,0 +1,125 @@ +import type { + LinearUpdateNotificationParams, + LinearUpdateNotificationResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateNotificationTool: ToolConfig< + LinearUpdateNotificationParams, + LinearUpdateNotificationResponse +> = { + id: 'linear_update_notification', + name: 'Linear Update Notification', + description: 'Mark a notification as read or unread in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + notificationId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Notification ID to update', + }, + readAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Timestamp to mark as read (ISO format). Pass null or omit to mark as unread', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + // If readAt is provided, use it; if explicitly null, mark as unread; if omitted, mark as read now + if (params.readAt !== undefined) { + input.readAt = params.readAt + } else { + input.readAt = new Date().toISOString() + } + + return { + query: ` + mutation UpdateNotification($id: String!, $input: NotificationUpdateInput!) { + notificationUpdate(id: $id, input: $input) { + success + notification { + id + type + createdAt + readAt + issue { + id + title + } + } + } + } + `, + variables: { + id: params.notificationId, + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update notification', + output: {}, + } + } + + const result = data.data.notificationUpdate + if (!result.success) { + return { + success: false, + error: 'Notification update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + notification: result.notification, + }, + } + }, + + outputs: { + notification: { + type: 'object', + description: 'The updated notification', + properties: { + id: { type: 'string', description: 'Notification ID' }, + type: { type: 'string', description: 'Notification type' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + readAt: { type: 'string', description: 'Read timestamp' }, + issue: { type: 'object', description: 'Related issue' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/update_project.ts b/apps/sim/tools/linear/update_project.ts new file mode 100644 index 0000000000..5e091ff41e --- /dev/null +++ b/apps/sim/tools/linear/update_project.ts @@ -0,0 +1,183 @@ +import type { LinearUpdateProjectParams, LinearUpdateProjectResponse } from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateProjectTool: ToolConfig< + LinearUpdateProjectParams, + LinearUpdateProjectResponse +> = { + id: 'linear_update_project', + name: 'Linear Update Project', + description: 'Update an existing project in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New project name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New project description', + }, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project state (planned, started, completed, canceled)', + }, + leadId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID of the project lead', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project start date (ISO format)', + }, + targetDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Project target date (ISO format)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Project priority (0-4)', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + if (params.name !== undefined) input.name = params.name + if (params.description !== undefined) input.description = params.description + if (params.state !== undefined) input.state = params.state + if (params.leadId !== undefined) input.leadId = params.leadId + if (params.startDate !== undefined) input.startDate = params.startDate + if (params.targetDate !== undefined) input.targetDate = params.targetDate + if (params.priority !== undefined) input.priority = Number(params.priority) + + return { + query: ` + mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) { + projectUpdate(id: $id, input: $input) { + success + project { + id + name + description + state + priority + startDate + targetDate + url + lead { + id + name + } + teams { + nodes { + id + name + } + } + } + } + } + `, + variables: { + id: params.projectId, + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update project', + output: {}, + } + } + + const result = data.data.projectUpdate + if (!result.success) { + return { + success: false, + error: 'Project update was not successful', + output: {}, + } + } + + const project = result.project + return { + success: true, + output: { + project: { + id: project.id, + name: project.name, + description: project.description, + state: project.state, + priority: project.priority, + startDate: project.startDate, + targetDate: project.targetDate, + url: project.url, + lead: project.lead, + teams: project.teams?.nodes || [], + }, + }, + } + }, + + outputs: { + project: { + type: 'object', + description: 'The updated project', + properties: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + description: { type: 'string', description: 'Project description' }, + state: { type: 'string', description: 'Project state' }, + priority: { type: 'number', description: 'Project priority' }, + lead: { type: 'object', description: 'Project lead' }, + teams: { type: 'array', description: 'Associated teams' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linear/update_workflow_state.ts b/apps/sim/tools/linear/update_workflow_state.ts new file mode 100644 index 0000000000..1ba608d6d5 --- /dev/null +++ b/apps/sim/tools/linear/update_workflow_state.ts @@ -0,0 +1,142 @@ +import type { + LinearUpdateWorkflowStateParams, + LinearUpdateWorkflowStateResponse, +} from '@/tools/linear/types' +import type { ToolConfig } from '@/tools/types' + +export const linearUpdateWorkflowStateTool: ToolConfig< + LinearUpdateWorkflowStateParams, + LinearUpdateWorkflowStateResponse +> = { + id: 'linear_update_workflow_state', + name: 'Linear Update Workflow State', + description: 'Update an existing workflow state in Linear', + version: '1.0.0', + + oauth: { + required: true, + provider: 'linear', + }, + + params: { + stateId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Workflow state ID to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New state name', + }, + color: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New state color (hex format)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New state description', + }, + position: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'New position in workflow', + }, + }, + + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + const input: Record = {} + + if (params.name !== undefined) input.name = params.name + if (params.color !== undefined) input.color = params.color + if (params.description !== undefined) input.description = params.description + if (params.position !== undefined) input.position = Number(params.position) + + return { + query: ` + mutation UpdateWorkflowState($id: String!, $input: WorkflowStateUpdateInput!) { + workflowStateUpdate(id: $id, input: $input) { + success + workflowState { + id + name + type + color + position + team { + id + name + } + } + } + } + `, + variables: { + id: params.stateId, + input, + }, + } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (data.errors) { + return { + success: false, + error: data.errors[0]?.message || 'Failed to update workflow state', + output: {}, + } + } + + const result = data.data.workflowStateUpdate + if (!result.success) { + return { + success: false, + error: 'Workflow state update was not successful', + output: {}, + } + } + + return { + success: true, + output: { + state: result.workflowState, + }, + } + }, + + outputs: { + state: { + type: 'object', + description: 'The updated workflow state', + properties: { + id: { type: 'string', description: 'State ID' }, + name: { type: 'string', description: 'State name' }, + type: { type: 'string', description: 'State type' }, + color: { type: 'string', description: 'State color' }, + position: { type: 'number', description: 'State position' }, + }, + }, + }, +} diff --git a/apps/sim/tools/linkup/search.ts b/apps/sim/tools/linkup/search.ts index 91fdc1d046..908ace1db4 100644 --- a/apps/sim/tools/linkup/search.ts +++ b/apps/sim/tools/linkup/search.ts @@ -28,7 +28,7 @@ export const searchTool: ToolConfig { const body: Record = { q: params.q, + depth: params.depth, + outputType: params.outputType, } - if (params.depth) body.depth = params.depth - if (params.outputType) body.outputType = params.outputType - body.includeImages = false + if (params.includeImages !== undefined) body.includeImages = params.includeImages + if (params.fromDate) body.fromDate = params.fromDate + if (params.toDate) body.toDate = params.toDate + + if (params.excludeDomains) { + body.excludeDomains = params.excludeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + } + + if (params.includeDomains) { + body.includeDomains = params.includeDomains + .split(',') + .map((d: string) => d.trim()) + .filter((d: string) => d.length > 0) + } + + if (params.includeInlineCitations !== undefined) + body.includeInlineCitations = params.includeInlineCitations + + if (params.includeSources !== undefined) body.includeSources = params.includeSources return body }, @@ -63,10 +127,7 @@ export const searchTool: ToolConfig = { - page_size: params.limit || 10, + page_size: Number(params.limit || 10), } // Only add filters if we have any conditions diff --git a/apps/sim/tools/mem0/search_memories.ts b/apps/sim/tools/mem0/search_memories.ts index 26a8f2d502..156e43407b 100644 --- a/apps/sim/tools/mem0/search_memories.ts +++ b/apps/sim/tools/mem0/search_memories.ts @@ -50,7 +50,7 @@ export const mem0SearchMemoriesTool: ToolConfig = { filters: { user_id: params.userId, }, - top_k: params.limit || 10, + top_k: Number(params.limit || 10), } return body diff --git a/apps/sim/tools/microsoft_planner/create_bucket.ts b/apps/sim/tools/microsoft_planner/create_bucket.ts new file mode 100644 index 0000000000..8133da371a --- /dev/null +++ b/apps/sim/tools/microsoft_planner/create_bucket.ts @@ -0,0 +1,101 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerCreateBucketResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerCreateBucket') + +export const createBucketTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerCreateBucketResponse +> = { + id: 'microsoft_planner_create_bucket', + name: 'Create Microsoft Planner Bucket', + description: 'Create a new bucket in a Microsoft Planner plan', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the plan where the bucket will be created', + }, + name: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The name of the bucket', + }, + }, + + request: { + url: () => 'https://graph.microsoft.com/v1.0/planner/buckets', + 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) => { + if (!params.planId) { + throw new Error('Plan ID is required') + } + if (!params.name) { + throw new Error('Bucket name is required') + } + + const body = { + name: params.name, + planId: params.planId, + orderHint: ' !', + } + + logger.info('Creating bucket with body:', body) + return body + }, + }, + + transformResponse: async (response: Response) => { + const bucket = await response.json() + logger.info('Created bucket:', bucket) + + const result: MicrosoftPlannerCreateBucketResponse = { + success: true, + output: { + bucket, + metadata: { + bucketId: bucket.id, + planId: bucket.planId, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the bucket was created successfully' }, + bucket: { type: 'object', description: 'The created bucket object with all properties' }, + metadata: { type: 'object', description: 'Metadata including bucketId and planId' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/delete_bucket.ts b/apps/sim/tools/microsoft_planner/delete_bucket.ts new file mode 100644 index 0000000000..40abc27926 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/delete_bucket.ts @@ -0,0 +1,88 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerDeleteBucketResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerDeleteBucket') + +export const deleteBucketTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerDeleteBucketResponse +> = { + id: 'microsoft_planner_delete_bucket', + name: 'Delete Microsoft Planner Bucket', + description: 'Delete a bucket from Microsoft Planner', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + bucketId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the bucket to delete', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the bucket to delete (If-Match header)', + }, + }, + + request: { + url: (params) => { + if (!params.bucketId) { + throw new Error('Bucket ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for delete operations') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'If-Match': params.etag, + } + }, + }, + + transformResponse: async (response: Response) => { + logger.info('Bucket deleted successfully') + + const result: MicrosoftPlannerDeleteBucketResponse = { + success: true, + output: { + deleted: true, + metadata: {}, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the bucket was deleted successfully' }, + deleted: { type: 'boolean', description: 'Confirmation of deletion' }, + metadata: { type: 'object', description: 'Additional metadata' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/delete_task.ts b/apps/sim/tools/microsoft_planner/delete_task.ts new file mode 100644 index 0000000000..32dc365c64 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/delete_task.ts @@ -0,0 +1,88 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerDeleteTaskResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerDeleteTask') + +export const deleteTaskTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerDeleteTaskResponse +> = { + id: 'microsoft_planner_delete_task', + name: 'Delete Microsoft Planner Task', + description: 'Delete a task from Microsoft Planner', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the task to delete', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the task to delete (If-Match header)', + }, + }, + + request: { + url: (params) => { + if (!params.taskId) { + throw new Error('Task ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for delete operations') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'If-Match': params.etag, + } + }, + }, + + transformResponse: async (response: Response) => { + logger.info('Task deleted successfully') + + const result: MicrosoftPlannerDeleteTaskResponse = { + success: true, + output: { + deleted: true, + metadata: {}, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the task was deleted successfully' }, + deleted: { type: 'boolean', description: 'Confirmation of deletion' }, + metadata: { type: 'object', description: 'Additional metadata' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/get_task_details.ts b/apps/sim/tools/microsoft_planner/get_task_details.ts new file mode 100644 index 0000000000..ef841088a1 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/get_task_details.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerGetTaskDetailsResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerGetTaskDetails') + +export const getTaskDetailsTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerGetTaskDetailsResponse +> = { + id: 'microsoft_planner_get_task_details', + name: 'Get Microsoft Planner Task Details', + description: 'Get detailed information about a task including checklist and references', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the task', + }, + }, + + request: { + url: (params) => { + if (!params.taskId) { + throw new Error('Task ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}/details` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const taskDetails = await response.json() + logger.info('Task details retrieved:', taskDetails) + + const result: MicrosoftPlannerGetTaskDetailsResponse = { + success: true, + output: { + taskDetails, + metadata: { + taskId: taskDetails.id, + }, + }, + } + + return result + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the task details were retrieved successfully', + }, + taskDetails: { + type: 'object', + description: 'The task details including description, checklist, and references', + }, + metadata: { type: 'object', description: 'Metadata including taskId' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/index.ts b/apps/sim/tools/microsoft_planner/index.ts index 145034f094..6450e28016 100644 --- a/apps/sim/tools/microsoft_planner/index.ts +++ b/apps/sim/tools/microsoft_planner/index.ts @@ -1,5 +1,27 @@ +import { createBucketTool } from '@/tools/microsoft_planner/create_bucket' import { createTaskTool } from '@/tools/microsoft_planner/create_task' +import { deleteBucketTool } from '@/tools/microsoft_planner/delete_bucket' +import { deleteTaskTool } from '@/tools/microsoft_planner/delete_task' +import { getTaskDetailsTool } from '@/tools/microsoft_planner/get_task_details' +import { listBucketsTool } from '@/tools/microsoft_planner/list_buckets' +import { listPlansTool } from '@/tools/microsoft_planner/list_plans' +import { readBucketTool } from '@/tools/microsoft_planner/read_bucket' +import { readPlanTool } from '@/tools/microsoft_planner/read_plan' import { readTaskTool } from '@/tools/microsoft_planner/read_task' +import { updateBucketTool } from '@/tools/microsoft_planner/update_bucket' +import { updateTaskTool } from '@/tools/microsoft_planner/update_task' +import { updateTaskDetailsTool } from '@/tools/microsoft_planner/update_task_details' export const microsoftPlannerCreateTaskTool = createTaskTool export const microsoftPlannerReadTaskTool = readTaskTool +export const microsoftPlannerUpdateTaskTool = updateTaskTool +export const microsoftPlannerDeleteTaskTool = deleteTaskTool +export const microsoftPlannerListPlansTool = listPlansTool +export const microsoftPlannerReadPlanTool = readPlanTool +export const microsoftPlannerListBucketsTool = listBucketsTool +export const microsoftPlannerReadBucketTool = readBucketTool +export const microsoftPlannerCreateBucketTool = createBucketTool +export const microsoftPlannerUpdateBucketTool = updateBucketTool +export const microsoftPlannerDeleteBucketTool = deleteBucketTool +export const microsoftPlannerGetTaskDetailsTool = getTaskDetailsTool +export const microsoftPlannerUpdateTaskDetailsTool = updateTaskDetailsTool diff --git a/apps/sim/tools/microsoft_planner/list_buckets.ts b/apps/sim/tools/microsoft_planner/list_buckets.ts new file mode 100644 index 0000000000..8d529bcfd6 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/list_buckets.ts @@ -0,0 +1,84 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerListBucketsResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerListBuckets') + +export const listBucketsTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerListBucketsResponse +> = { + id: 'microsoft_planner_list_buckets', + name: 'List Microsoft Planner Buckets', + description: 'List all buckets in a Microsoft Planner plan', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the plan', + }, + }, + + request: { + url: (params) => { + if (!params.planId) { + throw new Error('Plan ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/buckets` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + logger.info('List buckets response:', data) + + const buckets = data.value || [] + + const result: MicrosoftPlannerListBucketsResponse = { + success: true, + output: { + buckets, + metadata: { + planId: buckets.length > 0 ? buckets[0].planId : undefined, + count: buckets.length, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether buckets were retrieved successfully' }, + buckets: { type: 'array', description: 'Array of bucket objects' }, + metadata: { type: 'object', description: 'Metadata including planId and count' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/list_plans.ts b/apps/sim/tools/microsoft_planner/list_plans.ts new file mode 100644 index 0000000000..ec1576856a --- /dev/null +++ b/apps/sim/tools/microsoft_planner/list_plans.ts @@ -0,0 +1,84 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerListPlansResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerListPlans') + +export const listPlansTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerListPlansResponse +> = { + id: 'microsoft_planner_list_plans', + name: 'List Microsoft Planner Plans', + description: 'List all plans in a Microsoft 365 group', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the Microsoft 365 group', + }, + }, + + request: { + url: (params) => { + if (!params.groupId) { + throw new Error('Group ID is required') + } + return `https://graph.microsoft.com/v1.0/groups/${params.groupId}/planner/plans` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + logger.info('List plans response:', data) + + const plans = data.value || [] + + const result: MicrosoftPlannerListPlansResponse = { + success: true, + output: { + plans, + metadata: { + groupId: plans.length > 0 ? plans[0].container?.containerId : undefined, + count: plans.length, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether plans were retrieved successfully' }, + plans: { type: 'array', description: 'Array of plan objects' }, + metadata: { type: 'object', description: 'Metadata including groupId and count' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/read_bucket.ts b/apps/sim/tools/microsoft_planner/read_bucket.ts new file mode 100644 index 0000000000..227aef34ea --- /dev/null +++ b/apps/sim/tools/microsoft_planner/read_bucket.ts @@ -0,0 +1,82 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerReadBucketResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerReadBucket') + +export const readBucketTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerReadBucketResponse +> = { + id: 'microsoft_planner_read_bucket', + name: 'Read Microsoft Planner Bucket', + description: 'Get details of a specific bucket', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + bucketId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the bucket to retrieve', + }, + }, + + request: { + url: (params) => { + if (!params.bucketId) { + throw new Error('Bucket ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const bucket = await response.json() + logger.info('Read bucket response:', bucket) + + const result: MicrosoftPlannerReadBucketResponse = { + success: true, + output: { + bucket, + metadata: { + bucketId: bucket.id, + planId: bucket.planId, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the bucket was retrieved successfully' }, + bucket: { type: 'object', description: 'The bucket object with all properties' }, + metadata: { type: 'object', description: 'Metadata including bucketId and planId' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/read_plan.ts b/apps/sim/tools/microsoft_planner/read_plan.ts new file mode 100644 index 0000000000..e73a4bc0ee --- /dev/null +++ b/apps/sim/tools/microsoft_planner/read_plan.ts @@ -0,0 +1,82 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerReadPlanResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerReadPlan') + +export const readPlanTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerReadPlanResponse +> = { + id: 'microsoft_planner_read_plan', + name: 'Read Microsoft Planner Plan', + description: 'Get details of a specific Microsoft Planner plan', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the plan to retrieve', + }, + }, + + request: { + url: (params) => { + if (!params.planId) { + throw new Error('Plan ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const plan = await response.json() + logger.info('Read plan response:', plan) + + const result: MicrosoftPlannerReadPlanResponse = { + success: true, + output: { + plan, + metadata: { + planId: plan.id, + planUrl: `https://graph.microsoft.com/v1.0/planner/plans/${plan.id}`, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the plan was retrieved successfully' }, + plan: { type: 'object', description: 'The plan object with all properties' }, + metadata: { type: 'object', description: 'Metadata including planId and planUrl' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/types.ts b/apps/sim/tools/microsoft_planner/types.ts index 01aea1632f..5da08e607a 100644 --- a/apps/sim/tools/microsoft_planner/types.ts +++ b/apps/sim/tools/microsoft_planner/types.ts @@ -69,12 +69,30 @@ export interface PlannerTask { } } +export interface PlannerBucket { + id: string + name: string + planId: string + orderHint?: string + '@odata.etag'?: string +} + export interface PlannerPlan { id: string title: string owner?: string createdDateTime?: string container?: PlannerContainer + '@odata.etag'?: string +} + +export interface PlannerTaskDetails { + id: string + description?: string + previewType?: string + references?: Record + checklist?: Record + '@odata.etag'?: string } export interface MicrosoftPlannerMetadata { @@ -83,6 +101,9 @@ export interface MicrosoftPlannerMetadata { userId?: string planUrl?: string taskUrl?: string + bucketId?: string + groupId?: string + count?: number } export interface MicrosoftPlannerReadResponse extends ToolResponse { @@ -101,6 +122,83 @@ export interface MicrosoftPlannerCreateResponse extends ToolResponse { } } +export interface MicrosoftPlannerUpdateTaskResponse extends ToolResponse { + output: { + task: PlannerTask + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerDeleteTaskResponse extends ToolResponse { + output: { + deleted: boolean + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerListPlansResponse extends ToolResponse { + output: { + plans: PlannerPlan[] + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerReadPlanResponse extends ToolResponse { + output: { + plan: PlannerPlan + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerListBucketsResponse extends ToolResponse { + output: { + buckets: PlannerBucket[] + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerReadBucketResponse extends ToolResponse { + output: { + bucket: PlannerBucket + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerCreateBucketResponse extends ToolResponse { + output: { + bucket: PlannerBucket + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerUpdateBucketResponse extends ToolResponse { + output: { + bucket: PlannerBucket + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerDeleteBucketResponse extends ToolResponse { + output: { + deleted: boolean + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerGetTaskDetailsResponse extends ToolResponse { + output: { + taskDetails: PlannerTaskDetails + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerUpdateTaskDetailsResponse extends ToolResponse { + output: { + taskDetails: PlannerTaskDetails + metadata: MicrosoftPlannerMetadata + } +} + export interface MicrosoftPlannerToolParams { accessToken: string planId?: string @@ -108,10 +206,30 @@ export interface MicrosoftPlannerToolParams { title?: string description?: string dueDateTime?: string + startDateTime?: string assigneeUserId?: string bucketId?: string priority?: number percentComplete?: number + groupId?: string + name?: string + etag?: string + checklist?: Record + references?: Record + previewType?: string } -export type MicrosoftPlannerResponse = MicrosoftPlannerReadResponse | MicrosoftPlannerCreateResponse +export type MicrosoftPlannerResponse = + | MicrosoftPlannerReadResponse + | MicrosoftPlannerCreateResponse + | MicrosoftPlannerUpdateTaskResponse + | MicrosoftPlannerDeleteTaskResponse + | MicrosoftPlannerListPlansResponse + | MicrosoftPlannerReadPlanResponse + | MicrosoftPlannerListBucketsResponse + | MicrosoftPlannerReadBucketResponse + | MicrosoftPlannerCreateBucketResponse + | MicrosoftPlannerUpdateBucketResponse + | MicrosoftPlannerDeleteBucketResponse + | MicrosoftPlannerGetTaskDetailsResponse + | MicrosoftPlannerUpdateTaskDetailsResponse diff --git a/apps/sim/tools/microsoft_planner/update_bucket.ts b/apps/sim/tools/microsoft_planner/update_bucket.ts new file mode 100644 index 0000000000..f66c45da95 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/update_bucket.ts @@ -0,0 +1,113 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdateBucketResponse, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerUpdateBucket') + +export const updateBucketTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdateBucketResponse +> = { + id: 'microsoft_planner_update_bucket', + name: 'Update Microsoft Planner Bucket', + description: 'Update a bucket in Microsoft Planner', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + bucketId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the bucket to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The new name of the bucket', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the bucket to update (If-Match header)', + }, + }, + + request: { + url: (params) => { + if (!params.bucketId) { + throw new Error('Bucket ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}` + }, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for update operations') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'If-Match': params.etag, + } + }, + body: (params) => { + const body: Record = {} + + if (params.name) { + body.name = params.name + } + + if (Object.keys(body).length === 0) { + throw new Error('At least one field must be provided to update') + } + + logger.info('Updating bucket with body:', body) + return body + }, + }, + + transformResponse: async (response: Response) => { + const bucket = await response.json() + logger.info('Updated bucket:', bucket) + + const result: MicrosoftPlannerUpdateBucketResponse = { + success: true, + output: { + bucket, + metadata: { + bucketId: bucket.id, + planId: bucket.planId, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the bucket was updated successfully' }, + bucket: { type: 'object', description: 'The updated bucket object with all properties' }, + metadata: { type: 'object', description: 'Metadata including bucketId and planId' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/update_task.ts b/apps/sim/tools/microsoft_planner/update_task.ts new file mode 100644 index 0000000000..34a8ec5c4e --- /dev/null +++ b/apps/sim/tools/microsoft_planner/update_task.ts @@ -0,0 +1,180 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdateTaskResponse, + PlannerTask, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerUpdateTask') + +export const updateTaskTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdateTaskResponse +> = { + id: 'microsoft_planner_update_task', + name: 'Update Microsoft Planner Task', + description: 'Update a task in Microsoft Planner', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the task to update', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the task to update (If-Match header)', + }, + title: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The new title of the task', + }, + bucketId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The bucket ID to move the task to', + }, + dueDateTime: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The due date and time for the task (ISO 8601 format)', + }, + startDateTime: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The start date and time for the task (ISO 8601 format)', + }, + percentComplete: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'The percentage of task completion (0-100)', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'The priority of the task (0-10)', + }, + assigneeUserId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The user ID to assign the task to', + }, + }, + + request: { + url: (params) => { + if (!params.taskId) { + throw new Error('Task ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + }, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for update operations') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'If-Match': params.etag, + } + }, + body: (params) => { + const body: Partial = {} + + if (params.title) { + body.title = params.title + } + + if (params.bucketId) { + body.bucketId = params.bucketId + } + + if (params.dueDateTime) { + body.dueDateTime = params.dueDateTime + } + + if (params.startDateTime) { + body.startDateTime = params.startDateTime + } + + if (params.percentComplete !== undefined) { + body.percentComplete = params.percentComplete + } + + if (params.priority !== undefined) { + body.priority = Number(params.priority) + } + + if (params.assigneeUserId) { + body.assignments = { + [params.assigneeUserId]: { + '@odata.type': 'microsoft.graph.plannerAssignment', + orderHint: ' !', + }, + } + } + + if (Object.keys(body).length === 0) { + throw new Error('At least one field must be provided to update') + } + + logger.info('Updating task with body:', body) + return body + }, + }, + + transformResponse: async (response: Response) => { + const task = await response.json() + logger.info('Updated task:', task) + + const result: MicrosoftPlannerUpdateTaskResponse = { + success: true, + output: { + task, + metadata: { + taskId: task.id, + planId: task.planId, + taskUrl: `https://graph.microsoft.com/v1.0/planner/tasks/${task.id}`, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the task was updated successfully' }, + task: { type: 'object', description: 'The updated task object with all properties' }, + metadata: { type: 'object', description: 'Metadata including taskId, planId, and taskUrl' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/update_task_details.ts b/apps/sim/tools/microsoft_planner/update_task_details.ts new file mode 100644 index 0000000000..fda426e1e8 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/update_task_details.ts @@ -0,0 +1,149 @@ +import { createLogger } from '@/lib/logs/console/logger' +import type { + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdateTaskDetailsResponse, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerUpdateTaskDetails') + +export const updateTaskDetailsTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdateTaskDetailsResponse +> = { + id: 'microsoft_planner_update_task_details', + name: 'Update Microsoft Planner Task Details', + description: + 'Update task details including description, checklist items, and references in Microsoft Planner', + version: '1.0', + + oauth: { + required: true, + provider: 'microsoft-planner', + additionalScopes: [], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the task', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the task details to update (If-Match header)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The description of the task', + }, + checklist: { + type: 'object', + required: false, + visibility: 'user-only', + description: 'Checklist items as a JSON object', + }, + references: { + type: 'object', + required: false, + visibility: 'user-only', + description: 'References as a JSON object', + }, + previewType: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Preview type: automatic, noPreview, checklist, description, or reference', + }, + }, + + request: { + url: (params) => { + if (!params.taskId) { + throw new Error('Task ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}/details` + }, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for update operations') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'If-Match': params.etag, + } + }, + body: (params) => { + const body: Record = {} + + if (params.description !== undefined) { + body.description = params.description + } + + if (params.checklist) { + body.checklist = params.checklist + } + + if (params.references) { + body.references = params.references + } + + if (params.previewType) { + body.previewType = params.previewType + } + + if (Object.keys(body).length === 0) { + throw new Error('At least one field must be provided to update') + } + + logger.info('Updating task details with body:', body) + return body + }, + }, + + transformResponse: async (response: Response) => { + const taskDetails = await response.json() + logger.info('Updated task details:', taskDetails) + + const result: MicrosoftPlannerUpdateTaskDetailsResponse = { + success: true, + output: { + taskDetails, + metadata: { + taskId: taskDetails.id, + }, + }, + } + + return result + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the task details were updated successfully', + }, + taskDetails: { + type: 'object', + description: 'The updated task details object with all properties', + }, + metadata: { type: 'object', description: 'Metadata including taskId' }, + }, +} diff --git a/apps/sim/tools/microsoft_teams/delete_channel_message.ts b/apps/sim/tools/microsoft_teams/delete_channel_message.ts new file mode 100644 index 0000000000..276d4e2b1c --- /dev/null +++ b/apps/sim/tools/microsoft_teams/delete_channel_message.ts @@ -0,0 +1,89 @@ +import type { + MicrosoftTeamsDeleteMessageParams, + MicrosoftTeamsDeleteResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteChannelMessageTool: ToolConfig< + MicrosoftTeamsDeleteMessageParams, + MicrosoftTeamsDeleteResponse +> = { + id: 'microsoft_teams_delete_channel_message', + name: 'Delete Microsoft Teams Channel Message', + description: 'Soft delete a message in a Microsoft Teams channel', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the team', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the channel containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to delete', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the deletion was successful' }, + deleted: { type: 'boolean', description: 'Confirmation of deletion' }, + messageId: { type: 'string', description: 'ID of the deleted message' }, + }, + + request: { + url: (params) => { + const teamId = params.teamId?.trim() + const channelId = params.channelId?.trim() + const messageId = params.messageId?.trim() + if (!teamId || !channelId || !messageId) { + throw new Error('Team ID, Channel ID, and Message ID are required') + } + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/softDelete` + }, + method: 'POST', + 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?: MicrosoftTeamsDeleteMessageParams) => { + // Soft delete returns 204 No Content on success + return { + success: true, + output: { + deleted: true, + messageId: params?.messageId || '', + metadata: { + messageId: params?.messageId || '', + teamId: params?.teamId || '', + channelId: params?.channelId || '', + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/delete_chat_message.ts b/apps/sim/tools/microsoft_teams/delete_chat_message.ts new file mode 100644 index 0000000000..403d32334c --- /dev/null +++ b/apps/sim/tools/microsoft_teams/delete_chat_message.ts @@ -0,0 +1,81 @@ +import type { + MicrosoftTeamsDeleteMessageParams, + MicrosoftTeamsDeleteResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteChatMessageTool: ToolConfig< + MicrosoftTeamsDeleteMessageParams, + MicrosoftTeamsDeleteResponse +> = { + id: 'microsoft_teams_delete_chat_message', + name: 'Delete Microsoft Teams Chat Message', + description: 'Soft delete a message in a Microsoft Teams chat', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the chat containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to delete', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the deletion was successful' }, + deleted: { type: 'boolean', description: 'Confirmation of deletion' }, + messageId: { type: 'string', description: 'ID of the deleted message' }, + }, + + request: { + url: (params) => { + const chatId = params.chatId?.trim() + const messageId = params.messageId?.trim() + if (!chatId || !messageId) { + throw new Error('Chat ID and Message ID are required') + } + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}/softDelete` + }, + method: 'POST', + 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?: MicrosoftTeamsDeleteMessageParams) => { + // Soft delete returns 204 No Content on success + return { + success: true, + output: { + deleted: true, + messageId: params?.messageId || '', + metadata: { + messageId: params?.messageId || '', + chatId: params?.chatId || '', + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/get_message.ts b/apps/sim/tools/microsoft_teams/get_message.ts new file mode 100644 index 0000000000..1eaec8afd9 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/get_message.ts @@ -0,0 +1,121 @@ +import type { + MicrosoftTeamsGetMessageParams, + MicrosoftTeamsReadResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const getMessageTool: ToolConfig< + MicrosoftTeamsGetMessageParams, + MicrosoftTeamsReadResponse +> = { + id: 'microsoft_teams_get_message', + name: 'Get Microsoft Teams Message', + description: 'Get a specific message from a Microsoft Teams chat or channel', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the team (for channel messages)', + }, + channelId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the channel (for channel messages)', + }, + chatId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the chat (for chat messages)', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to retrieve', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the retrieval was successful' }, + content: { type: 'string', description: 'The message content' }, + metadata: { type: 'object', description: 'Message metadata including sender, timestamp, etc.' }, + }, + + request: { + url: (params) => { + const messageId = params.messageId?.trim() + if (!messageId) { + throw new Error('Message ID is required') + } + + // Check if it's a channel or chat message + if (params.teamId && params.channelId) { + const teamId = params.teamId.trim() + const channelId = params.channelId.trim() + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}` + } + if (params.chatId) { + const chatId = params.chatId.trim() + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}` + } + + throw new Error('Either (teamId and channelId) or chatId is required') + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsGetMessageParams) => { + const data = await response.json() + + const metadata = { + messageId: data.id || params?.messageId || '', + content: data.body?.content || '', + createdTime: data.createdDateTime || '', + url: data.webUrl || '', + teamId: params?.teamId, + channelId: params?.channelId, + chatId: params?.chatId, + messages: [ + { + id: data.id || '', + content: data.body?.content || '', + sender: data.from?.user?.displayName || 'Unknown', + timestamp: data.createdDateTime || '', + messageType: data.messageType || 'message', + attachments: data.attachments || [], + }, + ], + messageCount: 1, + } + + return { + success: true, + output: { + content: data.body?.content || '', + metadata, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/index.ts b/apps/sim/tools/microsoft_teams/index.ts index b06a2e912f..50628ed2c6 100644 --- a/apps/sim/tools/microsoft_teams/index.ts +++ b/apps/sim/tools/microsoft_teams/index.ts @@ -1,9 +1,50 @@ +// Read operations + +import { deleteChannelMessageTool } from '@/tools/microsoft_teams/delete_channel_message' +// Delete operations +import { deleteChatMessageTool } from '@/tools/microsoft_teams/delete_chat_message' +import { getMessageTool } from '@/tools/microsoft_teams/get_message' +import { listChannelMembersTool } from '@/tools/microsoft_teams/list_channel_members' +// Member operations +import { listTeamMembersTool } from '@/tools/microsoft_teams/list_team_members' import { readChannelTool } from '@/tools/microsoft_teams/read_channel' import { readChatTool } from '@/tools/microsoft_teams/read_chat' +// Reply operations +import { replyToMessageTool } from '@/tools/microsoft_teams/reply_to_message' +// Reaction operations +import { setReactionTool } from '@/tools/microsoft_teams/set_reaction' +import { unsetReactionTool } from '@/tools/microsoft_teams/unset_reaction' +import { updateChannelMessageTool } from '@/tools/microsoft_teams/update_channel_message' +// Update operations +import { updateChatMessageTool } from '@/tools/microsoft_teams/update_chat_message' +// Write operations import { writeChannelTool } from '@/tools/microsoft_teams/write_channel' import { writeChatTool } from '@/tools/microsoft_teams/write_chat' +// Read operations export const microsoftTeamsReadChannelTool = readChannelTool -export const microsoftTeamsWriteChannelTool = writeChannelTool export const microsoftTeamsReadChatTool = readChatTool +export const microsoftTeamsGetMessageTool = getMessageTool + +// Write operations +export const microsoftTeamsWriteChannelTool = writeChannelTool export const microsoftTeamsWriteChatTool = writeChatTool + +// Update operations +export const microsoftTeamsUpdateChatMessageTool = updateChatMessageTool +export const microsoftTeamsUpdateChannelMessageTool = updateChannelMessageTool + +// Delete operations +export const microsoftTeamsDeleteChatMessageTool = deleteChatMessageTool +export const microsoftTeamsDeleteChannelMessageTool = deleteChannelMessageTool + +// Reply operations +export const microsoftTeamsReplyToMessageTool = replyToMessageTool + +// Reaction operations +export const microsoftTeamsSetReactionTool = setReactionTool +export const microsoftTeamsUnsetReactionTool = unsetReactionTool + +// Member operations +export const microsoftTeamsListTeamMembersTool = listTeamMembersTool +export const microsoftTeamsListChannelMembersTool = listChannelMembersTool diff --git a/apps/sim/tools/microsoft_teams/list_channel_members.ts b/apps/sim/tools/microsoft_teams/list_channel_members.ts new file mode 100644 index 0000000000..cf8cde134d --- /dev/null +++ b/apps/sim/tools/microsoft_teams/list_channel_members.ts @@ -0,0 +1,89 @@ +import type { + MicrosoftTeamsListMembersResponse, + MicrosoftTeamsToolParams, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const listChannelMembersTool: ToolConfig< + MicrosoftTeamsToolParams, + MicrosoftTeamsListMembersResponse +> = { + id: 'microsoft_teams_list_channel_members', + name: 'List Microsoft Teams Channel Members', + description: 'List all members of a Microsoft Teams channel', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the team', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the channel', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the listing was successful' }, + members: { type: 'array', description: 'Array of channel members' }, + memberCount: { type: 'number', description: 'Total number of members' }, + }, + + request: { + url: (params) => { + const teamId = params.teamId?.trim() + const channelId = params.channelId?.trim() + if (!teamId || !channelId) { + throw new Error('Team ID and Channel ID are required') + } + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/members` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => { + const data = await response.json() + + const members = (data.value || []).map((member: any) => ({ + id: member.id || '', + displayName: member.displayName || '', + email: member.email || member.userId || '', + userId: member.userId || '', + roles: member.roles || [], + })) + + return { + success: true, + output: { + members, + memberCount: members.length, + metadata: { + teamId: params?.teamId || '', + channelId: params?.channelId || '', + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/list_team_members.ts b/apps/sim/tools/microsoft_teams/list_team_members.ts new file mode 100644 index 0000000000..b57886670e --- /dev/null +++ b/apps/sim/tools/microsoft_teams/list_team_members.ts @@ -0,0 +1,81 @@ +import type { + MicrosoftTeamsListMembersResponse, + MicrosoftTeamsToolParams, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const listTeamMembersTool: ToolConfig< + MicrosoftTeamsToolParams, + MicrosoftTeamsListMembersResponse +> = { + id: 'microsoft_teams_list_team_members', + name: 'List Microsoft Teams Team Members', + description: 'List all members of a Microsoft Teams team', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the team', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the listing was successful' }, + members: { type: 'array', description: 'Array of team members' }, + memberCount: { type: 'number', description: 'Total number of members' }, + }, + + request: { + url: (params) => { + const teamId = params.teamId?.trim() + if (!teamId) { + throw new Error('Team ID is required') + } + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/members` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => { + const data = await response.json() + + const members = (data.value || []).map((member: any) => ({ + id: member.id || '', + displayName: member.displayName || '', + email: member.email || member.userId || '', + userId: member.userId || '', + roles: member.roles || [], + })) + + return { + success: true, + output: { + members, + memberCount: members.length, + metadata: { + teamId: params?.teamId || '', + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/reply_to_message.ts b/apps/sim/tools/microsoft_teams/reply_to_message.ts new file mode 100644 index 0000000000..b7d444f5ef --- /dev/null +++ b/apps/sim/tools/microsoft_teams/reply_to_message.ts @@ -0,0 +1,111 @@ +import type { + MicrosoftTeamsReplyParams, + MicrosoftTeamsWriteResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const replyToMessageTool: ToolConfig< + MicrosoftTeamsReplyParams, + MicrosoftTeamsWriteResponse +> = { + id: 'microsoft_teams_reply_to_message', + name: 'Reply to Microsoft Teams Channel Message', + description: 'Reply to an existing message in a Microsoft Teams channel', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the team', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the channel', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to reply to', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The reply content', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the reply was successful' }, + messageId: { type: 'string', description: 'ID of the reply message' }, + updatedContent: { type: 'boolean', description: 'Whether content was successfully sent' }, + }, + + request: { + url: (params) => { + const teamId = params.teamId?.trim() + const channelId = params.channelId?.trim() + const messageId = params.messageId?.trim() + if (!teamId || !channelId || !messageId) { + throw new Error('Team ID, Channel ID, and Message ID are required') + } + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/replies` + }, + 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) => { + if (!params.content) { + throw new Error('Content is required') + } + return { + body: { + contentType: 'text', + content: params.content, + }, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsReplyParams) => { + const data = await response.json() + + const metadata = { + messageId: data.id || '', + teamId: params?.teamId || '', + channelId: params?.channelId || '', + content: data.body?.content || params?.content || '', + createdTime: data.createdDateTime || '', + url: data.webUrl || '', + } + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/set_reaction.ts b/apps/sim/tools/microsoft_teams/set_reaction.ts new file mode 100644 index 0000000000..463f25a866 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/set_reaction.ts @@ -0,0 +1,121 @@ +import type { + MicrosoftTeamsReactionParams, + MicrosoftTeamsReactionResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const setReactionTool: ToolConfig< + MicrosoftTeamsReactionParams, + MicrosoftTeamsReactionResponse +> = { + id: 'microsoft_teams_set_reaction', + name: 'Add Reaction to Microsoft Teams Message', + description: 'Add an emoji reaction to a message in Microsoft Teams', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the team (for channel messages)', + }, + channelId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the channel (for channel messages)', + }, + chatId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the chat (for chat messages)', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to react to', + }, + reactionType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The emoji reaction (e.g., ❤️, 👍, 😊)', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the reaction was added successfully' }, + reactionType: { type: 'string', description: 'The emoji that was added' }, + messageId: { type: 'string', description: 'ID of the message' }, + }, + + request: { + url: (params) => { + const messageId = params.messageId?.trim() + if (!messageId) { + throw new Error('Message ID is required') + } + + // Check if it's a channel or chat message + if (params.teamId && params.channelId) { + const teamId = params.teamId.trim() + const channelId = params.channelId.trim() + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/setReaction` + } + if (params.chatId) { + const chatId = params.chatId.trim() + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}/setReaction` + } + + throw new Error('Either (teamId and channelId) or chatId is required') + }, + 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) => { + if (!params.reactionType) { + throw new Error('Reaction type is required') + } + return { + reactionType: params.reactionType, + } + }, + }, + + transformResponse: async (_response: Response, params?: MicrosoftTeamsReactionParams) => { + // setReaction returns 204 No Content on success + return { + success: true, + output: { + success: true, + reactionType: params?.reactionType || '', + messageId: params?.messageId || '', + metadata: { + messageId: params?.messageId || '', + teamId: params?.teamId, + channelId: params?.channelId, + chatId: params?.chatId, + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/types.ts b/apps/sim/tools/microsoft_teams/types.ts index a58786ef0b..da72817807 100644 --- a/apps/sim/tools/microsoft_teams/types.ts +++ b/apps/sim/tools/microsoft_teams/types.ts @@ -72,6 +72,79 @@ export interface MicrosoftTeamsToolParams { content?: string includeAttachments?: boolean files?: any[] // UserFile array for attachments + reactionType?: string // For reaction operations } -export type MicrosoftTeamsResponse = MicrosoftTeamsReadResponse | MicrosoftTeamsWriteResponse +// Update message params +export interface MicrosoftTeamsUpdateMessageParams extends MicrosoftTeamsToolParams { + messageId: string + content: string +} + +// Delete message params +export interface MicrosoftTeamsDeleteMessageParams extends MicrosoftTeamsToolParams { + messageId: string +} + +// Reply to message params +export interface MicrosoftTeamsReplyParams extends MicrosoftTeamsToolParams { + messageId: string + content: string +} + +// Reaction params +export interface MicrosoftTeamsReactionParams extends MicrosoftTeamsToolParams { + messageId: string + reactionType: string +} + +// Get message params +export interface MicrosoftTeamsGetMessageParams extends MicrosoftTeamsToolParams { + messageId: string +} + +// Member list response +export interface MicrosoftTeamsMember { + id: string + displayName: string + email?: string + userId?: string + roles?: string[] +} + +export interface MicrosoftTeamsListMembersResponse extends ToolResponse { + output: { + members: MicrosoftTeamsMember[] + memberCount: number + metadata: { + teamId?: string + channelId?: string + } + } +} + +// Delete response +export interface MicrosoftTeamsDeleteResponse extends ToolResponse { + output: { + deleted: boolean + messageId: string + metadata: MicrosoftTeamsMetadata + } +} + +// Reaction response +export interface MicrosoftTeamsReactionResponse extends ToolResponse { + output: { + success: boolean + reactionType: string + messageId: string + metadata: MicrosoftTeamsMetadata + } +} + +export type MicrosoftTeamsResponse = + | MicrosoftTeamsReadResponse + | MicrosoftTeamsWriteResponse + | MicrosoftTeamsDeleteResponse + | MicrosoftTeamsListMembersResponse + | MicrosoftTeamsReactionResponse diff --git a/apps/sim/tools/microsoft_teams/unset_reaction.ts b/apps/sim/tools/microsoft_teams/unset_reaction.ts new file mode 100644 index 0000000000..56a0aa53d8 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/unset_reaction.ts @@ -0,0 +1,121 @@ +import type { + MicrosoftTeamsReactionParams, + MicrosoftTeamsReactionResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const unsetReactionTool: ToolConfig< + MicrosoftTeamsReactionParams, + MicrosoftTeamsReactionResponse +> = { + id: 'microsoft_teams_unset_reaction', + name: 'Remove Reaction from Microsoft Teams Message', + description: 'Remove an emoji reaction from a message in Microsoft Teams', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the team (for channel messages)', + }, + channelId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the channel (for channel messages)', + }, + chatId: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The ID of the chat (for chat messages)', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message', + }, + reactionType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The emoji reaction to remove (e.g., ❤️, 👍, 😊)', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the reaction was removed successfully' }, + reactionType: { type: 'string', description: 'The emoji that was removed' }, + messageId: { type: 'string', description: 'ID of the message' }, + }, + + request: { + url: (params) => { + const messageId = params.messageId?.trim() + if (!messageId) { + throw new Error('Message ID is required') + } + + // Check if it's a channel or chat message + if (params.teamId && params.channelId) { + const teamId = params.teamId.trim() + const channelId = params.channelId.trim() + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/unsetReaction` + } + if (params.chatId) { + const chatId = params.chatId.trim() + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}/unsetReaction` + } + + throw new Error('Either (teamId and channelId) or chatId is required') + }, + 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) => { + if (!params.reactionType) { + throw new Error('Reaction type is required') + } + return { + reactionType: params.reactionType, + } + }, + }, + + transformResponse: async (_response: Response, params?: MicrosoftTeamsReactionParams) => { + // unsetReaction returns 204 No Content on success + return { + success: true, + output: { + success: true, + reactionType: params?.reactionType || '', + messageId: params?.messageId || '', + metadata: { + messageId: params?.messageId || '', + teamId: params?.teamId, + channelId: params?.channelId, + chatId: params?.chatId, + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/update_channel_message.ts b/apps/sim/tools/microsoft_teams/update_channel_message.ts new file mode 100644 index 0000000000..92d50a64f7 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/update_channel_message.ts @@ -0,0 +1,111 @@ +import type { + MicrosoftTeamsUpdateMessageParams, + MicrosoftTeamsWriteResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const updateChannelMessageTool: ToolConfig< + MicrosoftTeamsUpdateMessageParams, + MicrosoftTeamsWriteResponse +> = { + id: 'microsoft_teams_update_channel_message', + name: 'Update Microsoft Teams Channel Message', + description: 'Update an existing message in a Microsoft Teams channel', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the team', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the channel containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to update', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The new content for the message', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the update was successful' }, + messageId: { type: 'string', description: 'ID of the updated message' }, + updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' }, + }, + + request: { + url: (params) => { + const teamId = params.teamId?.trim() + const channelId = params.channelId?.trim() + const messageId = params.messageId?.trim() + if (!teamId || !channelId || !messageId) { + throw new Error('Team ID, Channel ID, and Message ID are required') + } + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}` + }, + 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) => { + if (!params.content) { + throw new Error('Content is required') + } + return { + body: { + contentType: 'text', + content: params.content, + }, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsUpdateMessageParams) => { + const data = await response.json() + + const metadata = { + messageId: data.id || params?.messageId || '', + teamId: params?.teamId || '', + channelId: params?.channelId || '', + content: data.body?.content || params?.content || '', + createdTime: data.createdDateTime || '', + url: data.webUrl || '', + } + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/update_chat_message.ts b/apps/sim/tools/microsoft_teams/update_chat_message.ts new file mode 100644 index 0000000000..a33bf50230 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/update_chat_message.ts @@ -0,0 +1,103 @@ +import type { + MicrosoftTeamsUpdateMessageParams, + MicrosoftTeamsWriteResponse, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const updateChatMessageTool: ToolConfig< + MicrosoftTeamsUpdateMessageParams, + MicrosoftTeamsWriteResponse +> = { + id: 'microsoft_teams_update_chat_message', + name: 'Update Microsoft Teams Chat Message', + description: 'Update an existing message in a Microsoft Teams chat', + version: '1.0', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the chat containing the message', + }, + messageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the message to update', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The new content for the message', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the update was successful' }, + messageId: { type: 'string', description: 'ID of the updated message' }, + updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' }, + }, + + request: { + url: (params) => { + const chatId = params.chatId?.trim() + const messageId = params.messageId?.trim() + if (!chatId || !messageId) { + throw new Error('Chat ID and Message ID are required') + } + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages/${encodeURIComponent(messageId)}` + }, + 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) => { + if (!params.content) { + throw new Error('Content is required') + } + return { + body: { + contentType: 'text', + content: params.content, + }, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsUpdateMessageParams) => { + const data = await response.json() + + const metadata = { + messageId: data.id || params?.messageId || '', + chatId: params?.chatId || '', + content: data.body?.content || params?.content || '', + createdTime: data.createdDateTime || '', + url: data.webUrl || '', + } + + return { + success: true, + output: { + updatedContent: true, + metadata, + }, + } + }, +} diff --git a/apps/sim/tools/mongodb/delete.ts b/apps/sim/tools/mongodb/delete.ts index ddc915c4d3..3b392830a0 100644 --- a/apps/sim/tools/mongodb/delete.ts +++ b/apps/sim/tools/mongodb/delete.ts @@ -78,7 +78,7 @@ export const deleteTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mongodb/execute.ts b/apps/sim/tools/mongodb/execute.ts index 980712560a..ec0b85d95d 100644 --- a/apps/sim/tools/mongodb/execute.ts +++ b/apps/sim/tools/mongodb/execute.ts @@ -72,7 +72,7 @@ export const executeTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mongodb/insert.ts b/apps/sim/tools/mongodb/insert.ts index 18b8f9ab5a..853414cc2c 100644 --- a/apps/sim/tools/mongodb/insert.ts +++ b/apps/sim/tools/mongodb/insert.ts @@ -72,7 +72,7 @@ export const insertTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mongodb/query.ts b/apps/sim/tools/mongodb/query.ts index 82bb00f496..41ec1f97fd 100644 --- a/apps/sim/tools/mongodb/query.ts +++ b/apps/sim/tools/mongodb/query.ts @@ -84,7 +84,7 @@ export const queryTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, @@ -92,7 +92,7 @@ export const queryTool: ToolConfig = { ssl: params.ssl || 'preferred', collection: params.collection, query: params.query, - limit: params.limit, + limit: params.limit ? Number(params.limit) : undefined, sort: params.sort, }), }, diff --git a/apps/sim/tools/mongodb/update.ts b/apps/sim/tools/mongodb/update.ts index 4c163fa296..505995ea00 100644 --- a/apps/sim/tools/mongodb/update.ts +++ b/apps/sim/tools/mongodb/update.ts @@ -90,7 +90,7 @@ export const updateTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mysql/delete.ts b/apps/sim/tools/mysql/delete.ts index 35bcb53410..2f4ad28ad6 100644 --- a/apps/sim/tools/mysql/delete.ts +++ b/apps/sim/tools/mysql/delete.ts @@ -66,7 +66,7 @@ export const deleteTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mysql/execute.ts b/apps/sim/tools/mysql/execute.ts index 933162e565..5e912ab447 100644 --- a/apps/sim/tools/mysql/execute.ts +++ b/apps/sim/tools/mysql/execute.ts @@ -60,7 +60,7 @@ export const executeTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mysql/insert.ts b/apps/sim/tools/mysql/insert.ts index 00f1dba7e7..e837249275 100644 --- a/apps/sim/tools/mysql/insert.ts +++ b/apps/sim/tools/mysql/insert.ts @@ -66,7 +66,7 @@ export const insertTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mysql/query.ts b/apps/sim/tools/mysql/query.ts index 4da7040571..85804e7c06 100644 --- a/apps/sim/tools/mysql/query.ts +++ b/apps/sim/tools/mysql/query.ts @@ -60,7 +60,7 @@ export const queryTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/mysql/update.ts b/apps/sim/tools/mysql/update.ts index 17fbf9ae6c..4d53a20fa8 100644 --- a/apps/sim/tools/mysql/update.ts +++ b/apps/sim/tools/mysql/update.ts @@ -72,7 +72,7 @@ export const updateTool: ToolConfig = { }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/notion/query_database.ts b/apps/sim/tools/notion/query_database.ts index 9cb8ce1b3b..e5b77b3a62 100644 --- a/apps/sim/tools/notion/query_database.ts +++ b/apps/sim/tools/notion/query_database.ts @@ -94,7 +94,7 @@ export const notionQueryDatabaseTool: ToolConfig = // Add page size if provided if (params.pageSize) { - body.page_size = Math.min(params.pageSize, 100) + body.page_size = Math.min(Number(params.pageSize), 100) } return body diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index 4464b263c7..cdc0a342a2 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -81,7 +81,7 @@ export const listTool: ToolConfig = { // Add pagination if (params.pageSize) { - url.searchParams.append('$top', params.pageSize.toString()) + url.searchParams.append('$top', Number(params.pageSize).toString()) } return url.toString() diff --git a/apps/sim/tools/openai/image.ts b/apps/sim/tools/openai/image.ts index 90cf2a1f42..141c9a3aea 100644 --- a/apps/sim/tools/openai/image.ts +++ b/apps/sim/tools/openai/image.ts @@ -74,7 +74,7 @@ export const imageTool: ToolConfig = { model: params.model, prompt: params.prompt, size: params.size || '1024x1024', - n: params.n || 1, + n: params.n ? Number(params.n) : 1, } // Add model-specific parameters diff --git a/apps/sim/tools/outlook/read.ts b/apps/sim/tools/outlook/read.ts index 07ac14476a..1b905e650f 100644 --- a/apps/sim/tools/outlook/read.ts +++ b/apps/sim/tools/outlook/read.ts @@ -105,7 +105,7 @@ export const outlookReadTool: ToolConfig url: (params) => { // Set max results (default to 1 for simplicity, max 10) with no negative values const maxResults = params.maxResults - ? Math.max(1, Math.min(Math.abs(params.maxResults), 10)) + ? Math.max(1, Math.min(Math.abs(Number(params.maxResults)), 10)) : 1 // If folder is provided, read from that specific folder diff --git a/apps/sim/tools/parallel/deep_research.ts b/apps/sim/tools/parallel/deep_research.ts new file mode 100644 index 0000000000..97b121a66a --- /dev/null +++ b/apps/sim/tools/parallel/deep_research.ts @@ -0,0 +1,176 @@ +import type { ParallelDeepResearchParams } from '@/tools/parallel/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const deepResearchTool: ToolConfig = { + id: 'parallel_deep_research', + name: 'Parallel AI Deep Research', + description: + 'Conduct comprehensive deep research across the web using Parallel AI. Synthesizes information from multiple sources with citations. Can take up to 15 minutes to complete.', + version: '1.0.0', + + params: { + input: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Research query or question (up to 15,000 characters)', + }, + processor: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Compute level: base, lite, pro, ultra, ultra2x, ultra4x, ultra8x (default: base)', + }, + output_schema: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Desired output format description. Use "text" for markdown reports with citations, or describe structured JSON format', + }, + include_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to restrict research to (source policy)', + }, + exclude_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to exclude from research (source policy)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Parallel AI API Key', + }, + }, + + request: { + url: 'https://api.parallel.ai/v1/tasks/runs', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + }), + body: (params) => { + const body: Record = { + input: params.input, + processor: params.processor || 'base', + } + + // Build task_spec + const taskSpec: Record = {} + + // Handle output_schema - can be "text" or a custom description + if (params.output_schema) { + if (params.output_schema.toLowerCase() === 'text') { + taskSpec.output_schema = 'text' + } else { + taskSpec.output_schema = { + description: params.output_schema, + type: 'text', + } + } + } else { + // Default to auto mode for deep research + taskSpec.output_schema = 'auto' + } + + body.task_spec = taskSpec + + // Handle source policy (include/exclude domains) + if (params.include_domains || params.exclude_domains) { + const sourcePolicy: Record = {} + + if (params.include_domains) { + sourcePolicy.include_domains = params.include_domains + .split(',') + .map((d) => d.trim()) + .filter((d) => d.length > 0) + } + + if (params.exclude_domains) { + sourcePolicy.exclude_domains = params.exclude_domains + .split(',') + .map((d) => d.trim()) + .filter((d) => d.length > 0) + } + + if (Object.keys(sourcePolicy).length > 0) { + body.source_policy = sourcePolicy + } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + // Check if the task is still running + if (data.status === 'running') { + return { + success: true, + output: { + status: 'running', + run_id: data.run_id, + message: + 'Deep research task is running. This can take up to 15 minutes. Use the run_id to check status.', + }, + } + } + + // Task completed + return { + success: true, + output: { + status: data.status, + run_id: data.run_id, + content: data.content || {}, + basis: data.basis || [], + metadata: data.metadata || {}, + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Task status (running, completed, failed)', + }, + run_id: { + type: 'string', + description: 'Unique ID for this research task', + }, + message: { + type: 'string', + description: 'Status message (for running tasks)', + }, + content: { + type: 'object', + description: 'Research results (structured based on output_schema)', + }, + basis: { + type: 'array', + description: 'Citations and sources with excerpts and confidence levels', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'Source URL' }, + title: { type: 'string', description: 'Source title' }, + excerpt: { type: 'string', description: 'Relevant excerpt' }, + confidence: { type: 'number', description: 'Confidence level' }, + }, + }, + }, + metadata: { + type: 'object', + description: 'Additional task metadata', + }, + }, +} diff --git a/apps/sim/tools/parallel/extract.ts b/apps/sim/tools/parallel/extract.ts new file mode 100644 index 0000000000..196d3eb299 --- /dev/null +++ b/apps/sim/tools/parallel/extract.ts @@ -0,0 +1,102 @@ +import type { ParallelExtractParams } from '@/tools/parallel/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const extractTool: ToolConfig = { + id: 'parallel_extract', + name: 'Parallel AI Extract', + description: + 'Extract targeted information from specific URLs using Parallel AI. Processes provided URLs to pull relevant content based on your objective.', + version: '1.0.0', + + params: { + urls: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated list of URLs to extract information from', + }, + objective: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'What information to extract from the provided URLs', + }, + excerpts: { + type: 'boolean', + required: true, + visibility: 'user-only', + description: 'Include relevant excerpts from the content', + }, + full_content: { + type: 'boolean', + required: true, + visibility: 'user-only', + description: 'Include full page content', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Parallel AI API Key', + }, + }, + + request: { + url: 'https://api.parallel.ai/v1beta/extract', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + 'parallel-beta': 'search-extract-2025-10-10', + }), + body: (params) => { + // Convert comma-separated URLs to array + const urlArray = params.urls + .split(',') + .map((url) => url.trim()) + .filter((url) => url.length > 0) + + const body: Record = { + urls: urlArray, + objective: params.objective, + } + + // Add optional parameters if provided + if (params.excerpts !== undefined) body.excerpts = params.excerpts + if (params.full_content !== undefined) body.full_content = params.full_content + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + results: data.results || [], + }, + } + }, + + outputs: { + results: { + type: 'array', + description: 'Extracted information from the provided URLs', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'The source URL' }, + title: { type: 'string', description: 'The title of the page' }, + content: { type: 'string', description: 'Extracted content' }, + excerpts: { + type: 'array', + description: 'Relevant text excerpts', + items: { type: 'string' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/parallel/index.ts b/apps/sim/tools/parallel/index.ts index 471319e217..7e9c9abb74 100644 --- a/apps/sim/tools/parallel/index.ts +++ b/apps/sim/tools/parallel/index.ts @@ -1,3 +1,7 @@ +import { deepResearchTool } from '@/tools/parallel/deep_research' +import { extractTool } from '@/tools/parallel/extract' import { searchTool } from '@/tools/parallel/search' export const parallelSearchTool = searchTool +export const parallelExtractTool = extractTool +export const parallelDeepResearchTool = deepResearchTool diff --git a/apps/sim/tools/parallel/search.ts b/apps/sim/tools/parallel/search.ts index 3059d65d09..b536c7d439 100644 --- a/apps/sim/tools/parallel/search.ts +++ b/apps/sim/tools/parallel/search.ts @@ -53,6 +53,7 @@ export const searchTool: ToolConfig = { headers: (params) => ({ 'Content-Type': 'application/json', 'x-api-key': params.apiKey, + 'parallel-beta': 'search-extract-2025-10-10', }), body: (params) => { const body: Record = { @@ -62,8 +63,9 @@ export const searchTool: ToolConfig = { // Add optional parameters if provided if (params.processor) body.processor = params.processor - if (params.max_results) body.max_results = params.max_results - if (params.max_chars_per_result) body.max_chars_per_result = params.max_chars_per_result + if (params.max_results) body.max_results = Number(params.max_results) + if (params.max_chars_per_result) + body.max_chars_per_result = Number(params.max_chars_per_result) return body }, diff --git a/apps/sim/tools/parallel/types.ts b/apps/sim/tools/parallel/types.ts index 48720ec796..92fa1e4d49 100644 --- a/apps/sim/tools/parallel/types.ts +++ b/apps/sim/tools/parallel/types.ts @@ -16,3 +16,47 @@ export interface ParallelSearchResult { export interface ParallelSearchResponse { results: ParallelSearchResult[] } + +export interface ParallelExtractParams { + urls: string + objective: string + excerpts: boolean + full_content: boolean + apiKey: string +} + +export interface ParallelExtractResult { + url: string + title: string + content?: string + excerpts?: string[] +} + +export interface ParallelExtractResponse { + results: ParallelExtractResult[] +} + +export interface ParallelDeepResearchParams { + input: string + processor?: string + output_schema?: string + include_domains?: string + exclude_domains?: string + apiKey: string +} + +export interface ParallelDeepResearchBasis { + url: string + title: string + excerpt: string + confidence?: number +} + +export interface ParallelDeepResearchResponse { + status: string + run_id: string + message?: string + content?: Record + basis?: ParallelDeepResearchBasis[] + metadata?: Record +} diff --git a/apps/sim/tools/pinecone/search_text.ts b/apps/sim/tools/pinecone/search_text.ts index cc1d2929cb..4ef6bb7285 100644 --- a/apps/sim/tools/pinecone/search_text.ts +++ b/apps/sim/tools/pinecone/search_text.ts @@ -75,7 +75,7 @@ export const searchTextTool: ToolConfig ({ namespace: params.namespace, vector: typeof params.vector === 'string' ? JSON.parse(params.vector) : params.vector, - topK: params.topK ? Number.parseInt(params.topK.toString()) : 10, + topK: params.topK ? Number(params.topK) : 10, filter: params.filter ? typeof params.filter === 'string' ? JSON.parse(params.filter) diff --git a/apps/sim/tools/postgresql/delete.ts b/apps/sim/tools/postgresql/delete.ts index 7a629d746b..51e253342f 100644 --- a/apps/sim/tools/postgresql/delete.ts +++ b/apps/sim/tools/postgresql/delete.ts @@ -66,7 +66,7 @@ export const deleteTool: ToolConfig ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/postgresql/execute.ts b/apps/sim/tools/postgresql/execute.ts index 7a6d28ee45..df8b9c356f 100644 --- a/apps/sim/tools/postgresql/execute.ts +++ b/apps/sim/tools/postgresql/execute.ts @@ -60,7 +60,7 @@ export const executeTool: ToolConfig ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/postgresql/insert.ts b/apps/sim/tools/postgresql/insert.ts index 4f26dda3cd..805dfac9e1 100644 --- a/apps/sim/tools/postgresql/insert.ts +++ b/apps/sim/tools/postgresql/insert.ts @@ -66,7 +66,7 @@ export const insertTool: ToolConfig ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/postgresql/query.ts b/apps/sim/tools/postgresql/query.ts index 3f4e794343..45e9fd1d23 100644 --- a/apps/sim/tools/postgresql/query.ts +++ b/apps/sim/tools/postgresql/query.ts @@ -60,7 +60,7 @@ export const queryTool: ToolConfig = }), body: (params) => ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/postgresql/update.ts b/apps/sim/tools/postgresql/update.ts index 9bd09cc5ac..8044c8770b 100644 --- a/apps/sim/tools/postgresql/update.ts +++ b/apps/sim/tools/postgresql/update.ts @@ -72,7 +72,7 @@ export const updateTool: ToolConfig ({ host: params.host, - port: params.port, + port: Number(params.port), database: params.database, username: params.username, password: params.password, diff --git a/apps/sim/tools/qdrant/search_vector.ts b/apps/sim/tools/qdrant/search_vector.ts index f68001913f..509657a65d 100644 --- a/apps/sim/tools/qdrant/search_vector.ts +++ b/apps/sim/tools/qdrant/search_vector.ts @@ -100,7 +100,7 @@ export const searchVectorTool: ToolConfig = return { query: params.vector, - limit: params.limit ? Number.parseInt(params.limit.toString()) : 10, + limit: params.limit ? Number(params.limit) : 10, filter: params.filter, with_payload: withPayload, with_vector: withVector, diff --git a/apps/sim/tools/reddit/delete.ts b/apps/sim/tools/reddit/delete.ts new file mode 100644 index 0000000000..5304263622 --- /dev/null +++ b/apps/sim/tools/reddit/delete.ts @@ -0,0 +1,87 @@ +import type { RedditDeleteParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteTool: ToolConfig = { + id: 'reddit_delete', + name: 'Delete Reddit Post/Comment', + description: 'Delete your own Reddit post or comment', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['edit'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thing fullname to delete (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/del', + method: 'POST', + headers: (params: RedditDeleteParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditDeleteParams) => { + const formData = new URLSearchParams({ + id: params.id, + }) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditDeleteParams) => { + // Reddit delete API returns empty JSON {} on success + await response.json() + + if (response.ok) { + return { + success: true, + output: { + success: true, + message: `Successfully deleted ${requestParams?.id}`, + }, + } + } + + return { + success: false, + output: { + success: false, + message: 'Failed to delete item', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the deletion was successful', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + }, +} diff --git a/apps/sim/tools/reddit/edit.ts b/apps/sim/tools/reddit/edit.ts new file mode 100644 index 0000000000..c9e165cb4a --- /dev/null +++ b/apps/sim/tools/reddit/edit.ts @@ -0,0 +1,107 @@ +import type { RedditEditParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const editTool: ToolConfig = { + id: 'reddit_edit', + name: 'Edit Reddit Post/Comment', + description: 'Edit the text of your own Reddit post or comment', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['edit'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + thing_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thing fullname to edit (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New text content in markdown format', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/editusertext', + method: 'POST', + headers: (params: RedditEditParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditEditParams) => { + const formData = new URLSearchParams({ + thing_id: params.thing_id, + text: params.text, + api_type: 'json', + }) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditEditParams) => { + const data = await response.json() + + // Reddit API returns errors in json.errors array + if (data.json?.errors && data.json.errors.length > 0) { + const errors = data.json.errors.map((err: any) => err.join(': ')).join(', ') + return { + success: false, + output: { + success: false, + message: `Failed to edit: ${errors}`, + }, + } + } + + // Success response + const thingData = data.json?.data?.things?.[0]?.data + return { + success: true, + output: { + success: true, + message: `Successfully edited ${requestParams?.thing_id}`, + data: { + id: thingData?.id, + body: thingData?.body, + selftext: thingData?.selftext, + }, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the edit was successful', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + data: { + type: 'object', + description: 'Updated content data', + }, + }, +} diff --git a/apps/sim/tools/reddit/get_comments.ts b/apps/sim/tools/reddit/get_comments.ts index 9e952efffd..e08d010dd6 100644 --- a/apps/sim/tools/reddit/get_comments.ts +++ b/apps/sim/tools/reddit/get_comments.ts @@ -45,6 +45,66 @@ export const getCommentsTool: ToolConfig { diff --git a/apps/sim/tools/reddit/get_controversial.ts b/apps/sim/tools/reddit/get_controversial.ts new file mode 100644 index 0000000000..26bbe80496 --- /dev/null +++ b/apps/sim/tools/reddit/get_controversial.ts @@ -0,0 +1,178 @@ +import type { RedditControversialParams, RedditPostsResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const getControversialTool: ToolConfig = { + id: 'reddit_get_controversial', + name: 'Get Reddit Controversial Posts', + description: 'Fetch controversial posts from a subreddit', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['read'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + subreddit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the subreddit to fetch posts from (without the r/ prefix)', + }, + time: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Time filter for controversial posts: "hour", "day", "week", "month", "year", or "all" (default: "all")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of posts to return (default: 10, max: 100)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items after (for pagination)', + }, + before: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items before (for pagination)', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'A count of items already seen in the listing (used for numbering)', + }, + show: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Show items that would normally be filtered (e.g., "all")', + }, + sr_detail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Expand subreddit details in the response', + }, + }, + + request: { + url: (params: RedditControversialParams) => { + // Sanitize inputs + const subreddit = params.subreddit.trim().replace(/^r\//, '') + const limit = Math.min(Math.max(1, params.limit || 10), 100) + + // Build URL with appropriate parameters using OAuth endpoint + const urlParams = new URLSearchParams({ + limit: limit.toString(), + raw_json: '1', + }) + + // Add time filter + if (params.time) { + urlParams.append('t', params.time) + } + + // Add pagination parameters if provided + if (params.after) urlParams.append('after', params.after) + if (params.before) urlParams.append('before', params.before) + if (params.count !== undefined) urlParams.append('count', params.count.toString()) + if (params.show) urlParams.append('show', params.show) + if (params.sr_detail !== undefined) urlParams.append('sr_detail', params.sr_detail.toString()) + + return `https://oauth.reddit.com/r/${subreddit}/controversial?${urlParams.toString()}` + }, + method: 'GET', + headers: (params: RedditControversialParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditControversialParams) => { + const data = await response.json() + + // Extract subreddit name from response (with fallback) + const subredditName = + data.data?.children[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' + + // Transform posts data + const posts = + data.data?.children?.map((child: any) => { + const post = child.data || {} + return { + id: post.id || '', + title: post.title || '', + author: post.author || '[deleted]', + url: post.url || '', + permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', + created_utc: post.created_utc || 0, + score: post.score || 0, + num_comments: post.num_comments || 0, + is_self: !!post.is_self, + selftext: post.selftext || '', + thumbnail: post.thumbnail || '', + subreddit: post.subreddit || subredditName, + } + }) || [] + + return { + success: true, + output: { + subreddit: subredditName, + posts, + }, + } + }, + + outputs: { + subreddit: { + type: 'string', + description: 'Name of the subreddit where posts were fetched from', + }, + posts: { + type: 'array', + description: + 'Array of controversial posts with title, author, URL, score, comments count, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Post ID' }, + title: { type: 'string', description: 'Post title' }, + author: { type: 'string', description: 'Author username' }, + url: { type: 'string', description: 'Post URL' }, + permalink: { type: 'string', description: 'Reddit permalink' }, + score: { type: 'number', description: 'Post score (upvotes - downvotes)' }, + num_comments: { type: 'number', description: 'Number of comments' }, + created_utc: { type: 'number', description: 'Creation timestamp (UTC)' }, + is_self: { type: 'boolean', description: 'Whether this is a text post' }, + selftext: { type: 'string', description: 'Text content for self posts' }, + thumbnail: { type: 'string', description: 'Thumbnail URL' }, + subreddit: { type: 'string', description: 'Subreddit name' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/reddit/get_gilded.ts b/apps/sim/tools/reddit/get_gilded.ts new file mode 100644 index 0000000000..5db74fd3a6 --- /dev/null +++ b/apps/sim/tools/reddit/get_gilded.ts @@ -0,0 +1,166 @@ +import type { RedditGildedParams, RedditPostsResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const getGildedTool: ToolConfig = { + id: 'reddit_get_gilded', + name: 'Get Reddit Gilded Posts', + description: 'Fetch gilded/awarded posts from a subreddit', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['read'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + subreddit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the subreddit to fetch posts from (without the r/ prefix)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of posts to return (default: 10, max: 100)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items after (for pagination)', + }, + before: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items before (for pagination)', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'A count of items already seen in the listing (used for numbering)', + }, + show: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Show items that would normally be filtered (e.g., "all")', + }, + sr_detail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Expand subreddit details in the response', + }, + }, + + request: { + url: (params: RedditGildedParams) => { + // Sanitize inputs + const subreddit = params.subreddit.trim().replace(/^r\//, '') + const limit = Math.min(Math.max(1, params.limit || 10), 100) + + // Build URL with appropriate parameters using OAuth endpoint + const urlParams = new URLSearchParams({ + limit: limit.toString(), + raw_json: '1', + }) + + // Add pagination parameters if provided + if (params.after) urlParams.append('after', params.after) + if (params.before) urlParams.append('before', params.before) + if (params.count !== undefined) urlParams.append('count', params.count.toString()) + if (params.show) urlParams.append('show', params.show) + if (params.sr_detail !== undefined) urlParams.append('sr_detail', params.sr_detail.toString()) + + return `https://oauth.reddit.com/r/${subreddit}/gilded?${urlParams.toString()}` + }, + method: 'GET', + headers: (params: RedditGildedParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditGildedParams) => { + const data = await response.json() + + // Extract subreddit name from response (with fallback) + const subredditName = + data.data?.children[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' + + // Transform posts data + const posts = + data.data?.children?.map((child: any) => { + const post = child.data || {} + return { + id: post.id || '', + title: post.title || '', + author: post.author || '[deleted]', + url: post.url || '', + permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', + created_utc: post.created_utc || 0, + score: post.score || 0, + num_comments: post.num_comments || 0, + is_self: !!post.is_self, + selftext: post.selftext || '', + thumbnail: post.thumbnail || '', + subreddit: post.subreddit || subredditName, + } + }) || [] + + return { + success: true, + output: { + subreddit: subredditName, + posts, + }, + } + }, + + outputs: { + subreddit: { + type: 'string', + description: 'Name of the subreddit where posts were fetched from', + }, + posts: { + type: 'array', + description: + 'Array of gilded/awarded posts with title, author, URL, score, comments count, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Post ID' }, + title: { type: 'string', description: 'Post title' }, + author: { type: 'string', description: 'Author username' }, + url: { type: 'string', description: 'Post URL' }, + permalink: { type: 'string', description: 'Reddit permalink' }, + score: { type: 'number', description: 'Post score (upvotes - downvotes)' }, + num_comments: { type: 'number', description: 'Number of comments' }, + created_utc: { type: 'number', description: 'Creation timestamp (UTC)' }, + is_self: { type: 'boolean', description: 'Whether this is a text post' }, + selftext: { type: 'string', description: 'Text content for self posts' }, + thumbnail: { type: 'string', description: 'Thumbnail URL' }, + subreddit: { type: 'string', description: 'Subreddit name' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/reddit/get_posts.ts b/apps/sim/tools/reddit/get_posts.ts index bec69bc60c..877e303b9a 100644 --- a/apps/sim/tools/reddit/get_posts.ts +++ b/apps/sim/tools/reddit/get_posts.ts @@ -45,6 +45,36 @@ export const getPostsTool: ToolConfig = description: 'Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" (default: "day")', }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items after (for pagination)', + }, + before: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items before (for pagination)', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'A count of items already seen in the listing (used for numbering)', + }, + show: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Show items that would normally be filtered (e.g., "all")', + }, + sr_detail: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Expand subreddit details in the response', + }, }, request: { @@ -55,14 +85,24 @@ export const getPostsTool: ToolConfig = const limit = Math.min(Math.max(1, params.limit || 10), 100) // Build URL with appropriate parameters using OAuth endpoint - let url = `https://oauth.reddit.com/r/${subreddit}/${sort}?limit=${limit}&raw_json=1` + const urlParams = new URLSearchParams({ + limit: limit.toString(), + raw_json: '1', + }) // Add time parameter only for 'top' sorting if (sort === 'top' && params.time) { - url += `&t=${params.time}` + urlParams.append('t', params.time) } - return url + // Add pagination parameters if provided + if (params.after) urlParams.append('after', params.after) + if (params.before) urlParams.append('before', params.before) + if (params.count !== undefined) urlParams.append('count', params.count.toString()) + if (params.show) urlParams.append('show', params.show) + if (params.sr_detail !== undefined) urlParams.append('sr_detail', params.sr_detail.toString()) + + return `https://oauth.reddit.com/r/${subreddit}/${sort}?${urlParams.toString()}` }, method: 'GET', headers: (params: RedditPostsParams) => { diff --git a/apps/sim/tools/reddit/index.ts b/apps/sim/tools/reddit/index.ts index e3facb252a..a25a46acc0 100644 --- a/apps/sim/tools/reddit/index.ts +++ b/apps/sim/tools/reddit/index.ts @@ -1,7 +1,31 @@ +import { deleteTool } from '@/tools/reddit/delete' +import { editTool } from '@/tools/reddit/edit' import { getCommentsTool } from '@/tools/reddit/get_comments' +import { getControversialTool } from '@/tools/reddit/get_controversial' +import { getGildedTool } from '@/tools/reddit/get_gilded' import { getPostsTool } from '@/tools/reddit/get_posts' import { hotPostsTool } from '@/tools/reddit/hot_posts' +import { replyTool } from '@/tools/reddit/reply' +import { saveTool, unsaveTool } from '@/tools/reddit/save' +import { searchTool } from '@/tools/reddit/search' +import { submitPostTool } from '@/tools/reddit/submit_post' +import { subscribeTool } from '@/tools/reddit/subscribe' +import { voteTool } from '@/tools/reddit/vote' +// Read operations export const redditHotPostsTool = hotPostsTool export const redditGetPostsTool = getPostsTool export const redditGetCommentsTool = getCommentsTool +export const redditGetControversialTool = getControversialTool +export const redditGetGildedTool = getGildedTool +export const redditSearchTool = searchTool + +// Write operations +export const redditSubmitPostTool = submitPostTool +export const redditVoteTool = voteTool +export const redditSaveTool = saveTool +export const redditUnsaveTool = unsaveTool +export const redditReplyTool = replyTool +export const redditEditTool = editTool +export const redditDeleteTool = deleteTool +export const redditSubscribeTool = subscribeTool diff --git a/apps/sim/tools/reddit/reply.ts b/apps/sim/tools/reddit/reply.ts new file mode 100644 index 0000000000..eeee10ed94 --- /dev/null +++ b/apps/sim/tools/reddit/reply.ts @@ -0,0 +1,110 @@ +import type { RedditReplyParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const replyTool: ToolConfig = { + id: 'reddit_reply', + name: 'Reply to Reddit Post/Comment', + description: 'Add a comment reply to a Reddit post or comment', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['submit'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + parent_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thing fullname to reply to (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comment text in markdown format', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/comment', + method: 'POST', + headers: (params: RedditReplyParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditReplyParams) => { + const formData = new URLSearchParams({ + thing_id: params.parent_id, + text: params.text, + api_type: 'json', + }) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + // Reddit API returns errors in json.errors array + if (data.json?.errors && data.json.errors.length > 0) { + const errors = data.json.errors.map((err: any) => err.join(': ')).join(', ') + return { + success: false, + output: { + success: false, + message: `Failed to post reply: ${errors}`, + }, + } + } + + // Success response includes comment data + const commentData = data.json?.data?.things?.[0]?.data + return { + success: true, + output: { + success: true, + message: 'Reply posted successfully', + data: { + id: commentData?.id, + name: commentData?.name, + permalink: commentData?.permalink + ? `https://www.reddit.com${commentData.permalink}` + : undefined, + body: commentData?.body, + }, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the reply was posted successfully', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + data: { + type: 'object', + description: 'Comment data including ID, name, permalink, and body', + }, + }, +} diff --git a/apps/sim/tools/reddit/save.ts b/apps/sim/tools/reddit/save.ts new file mode 100644 index 0000000000..99cb684700 --- /dev/null +++ b/apps/sim/tools/reddit/save.ts @@ -0,0 +1,182 @@ +import type { RedditSaveParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const saveTool: ToolConfig = { + id: 'reddit_save', + name: 'Save Reddit Post/Comment', + description: 'Save a Reddit post or comment to your saved items', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['save'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thing fullname to save (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + }, + category: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Category to save under (Reddit Gold feature)', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/save', + method: 'POST', + headers: (params: RedditSaveParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditSaveParams) => { + const formData = new URLSearchParams({ + id: params.id, + }) + + if (params.category) { + formData.append('category', params.category) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditSaveParams) => { + // Reddit save API returns empty JSON {} on success + await response.json() + + if (response.ok) { + return { + success: true, + output: { + success: true, + message: `Successfully saved ${requestParams?.id}`, + }, + } + } + + return { + success: false, + output: { + success: false, + message: 'Failed to save item', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the save was successful', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + }, +} + +export const unsaveTool: ToolConfig = { + id: 'reddit_unsave', + name: 'Unsave Reddit Post/Comment', + description: 'Remove a Reddit post or comment from your saved items', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['save'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thing fullname to unsave (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/unsave', + method: 'POST', + headers: (params: RedditSaveParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditSaveParams) => { + const formData = new URLSearchParams({ + id: params.id, + }) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditSaveParams) => { + // Reddit unsave API returns empty JSON {} on success + await response.json() + + if (response.ok) { + return { + success: true, + output: { + success: true, + message: `Successfully unsaved ${requestParams?.id}`, + }, + } + } + + return { + success: false, + output: { + success: false, + message: 'Failed to unsave item', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the unsave was successful', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + }, +} diff --git a/apps/sim/tools/reddit/search.ts b/apps/sim/tools/reddit/search.ts new file mode 100644 index 0000000000..0833b39388 --- /dev/null +++ b/apps/sim/tools/reddit/search.ts @@ -0,0 +1,195 @@ +import type { RedditPostsResponse, RedditSearchParams } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'reddit_search', + name: 'Search Reddit', + description: 'Search for posts within a subreddit', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['read'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + subreddit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the subreddit to search in (without the r/ prefix)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query text', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Sort method for search results: "relevance", "hot", "top", "new", or "comments" (default: "relevance")', + }, + time: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Time filter for search results: "hour", "day", "week", "month", "year", or "all" (default: "all")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of posts to return (default: 10, max: 100)', + }, + restrict_sr: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Restrict search to the specified subreddit only (default: true)', + }, + after: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items after (for pagination)', + }, + before: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Fullname of a thing to fetch items before (for pagination)', + }, + count: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'A count of items already seen in the listing (used for numbering)', + }, + show: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Show items that would normally be filtered (e.g., "all")', + }, + }, + + request: { + url: (params: RedditSearchParams) => { + // Sanitize inputs + const subreddit = params.subreddit.trim().replace(/^r\//, '') + const sort = params.sort || 'relevance' + const limit = Math.min(Math.max(1, params.limit || 10), 100) + const restrict_sr = params.restrict_sr !== false // Default to true + + // Build URL with appropriate parameters using OAuth endpoint + const urlParams = new URLSearchParams({ + q: params.query, + sort: sort, + limit: limit.toString(), + restrict_sr: restrict_sr.toString(), + raw_json: '1', + }) + + // Add time filter if provided + if (params.time) { + urlParams.append('t', params.time) + } + + // Add pagination parameters if provided + if (params.after) urlParams.append('after', params.after) + if (params.before) urlParams.append('before', params.before) + if (params.count !== undefined) urlParams.append('count', Number(params.count).toString()) + if (params.show) urlParams.append('show', params.show) + + return `https://oauth.reddit.com/r/${subreddit}/search?${urlParams.toString()}` + }, + method: 'GET', + headers: (params: RedditSearchParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + Accept: 'application/json', + } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditSearchParams) => { + const data = await response.json() + + // Extract subreddit name from response (with fallback) + const subredditName = + data.data?.children[0]?.data?.subreddit || requestParams?.subreddit || 'unknown' + + // Transform posts data + const posts = + data.data?.children?.map((child: any) => { + const post = child.data || {} + return { + id: post.id || '', + title: post.title || '', + author: post.author || '[deleted]', + url: post.url || '', + permalink: post.permalink ? `https://www.reddit.com${post.permalink}` : '', + created_utc: post.created_utc || 0, + score: post.score || 0, + num_comments: post.num_comments || 0, + is_self: !!post.is_self, + selftext: post.selftext || '', + thumbnail: post.thumbnail || '', + subreddit: post.subreddit || subredditName, + } + }) || [] + + return { + success: true, + output: { + subreddit: subredditName, + posts, + }, + } + }, + + outputs: { + subreddit: { + type: 'string', + description: 'Name of the subreddit where search was performed', + }, + posts: { + type: 'array', + description: + 'Array of search result posts with title, author, URL, score, comments count, and metadata', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Post ID' }, + title: { type: 'string', description: 'Post title' }, + author: { type: 'string', description: 'Author username' }, + url: { type: 'string', description: 'Post URL' }, + permalink: { type: 'string', description: 'Reddit permalink' }, + score: { type: 'number', description: 'Post score (upvotes - downvotes)' }, + num_comments: { type: 'number', description: 'Number of comments' }, + created_utc: { type: 'number', description: 'Creation timestamp (UTC)' }, + is_self: { type: 'boolean', description: 'Whether this is a text post' }, + selftext: { type: 'string', description: 'Text content for self posts' }, + thumbnail: { type: 'string', description: 'Thumbnail URL' }, + subreddit: { type: 'string', description: 'Subreddit name' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/reddit/submit_post.ts b/apps/sim/tools/reddit/submit_post.ts new file mode 100644 index 0000000000..1fdfc8b5fa --- /dev/null +++ b/apps/sim/tools/reddit/submit_post.ts @@ -0,0 +1,160 @@ +import type { RedditSubmitParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const submitPostTool: ToolConfig = { + id: 'reddit_submit_post', + name: 'Submit Reddit Post', + description: 'Submit a new post to a subreddit (text or link)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['submit'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + subreddit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the subreddit to post to (without the r/ prefix)', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Title of the submission (max 300 characters)', + }, + text: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Text content for a self post (markdown supported)', + }, + url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL for a link post (cannot be used with text)', + }, + nsfw: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Mark post as NSFW', + }, + spoiler: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Mark post as spoiler', + }, + send_replies: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Send reply notifications to inbox (default: true)', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/submit', + method: 'POST', + headers: (params: RedditSubmitParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditSubmitParams) => { + // Sanitize subreddit + const subreddit = params.subreddit.trim().replace(/^r\//, '') + + // Build form data + const formData = new URLSearchParams({ + sr: subreddit, + title: params.title, + api_type: 'json', + }) + + // Determine post kind (self or link) + if (params.text) { + formData.append('kind', 'self') + formData.append('text', params.text) + } else if (params.url) { + formData.append('kind', 'link') + formData.append('url', params.url) + } else { + formData.append('kind', 'self') + formData.append('text', '') + } + + // Add optional parameters + if (params.nsfw !== undefined) formData.append('nsfw', params.nsfw.toString()) + if (params.spoiler !== undefined) formData.append('spoiler', params.spoiler.toString()) + if (params.send_replies !== undefined) + formData.append('sendreplies', params.send_replies.toString()) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + // Reddit API returns errors in json.errors array + if (data.json?.errors && data.json.errors.length > 0) { + const errors = data.json.errors.map((err: any) => err.join(': ')).join(', ') + return { + success: false, + output: { + success: false, + message: `Failed to submit post: ${errors}`, + }, + } + } + + // Success response includes post data + const postData = data.json?.data + return { + success: true, + output: { + success: true, + message: 'Post submitted successfully', + data: { + id: postData?.id, + name: postData?.name, + url: postData?.url, + permalink: `https://www.reddit.com${postData?.url}`, + }, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the post was submitted successfully', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + data: { + type: 'object', + description: 'Post data including ID, name, URL, and permalink', + }, + }, +} diff --git a/apps/sim/tools/reddit/subscribe.ts b/apps/sim/tools/reddit/subscribe.ts new file mode 100644 index 0000000000..319d7c3dfd --- /dev/null +++ b/apps/sim/tools/reddit/subscribe.ts @@ -0,0 +1,107 @@ +import type { RedditSubscribeParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const subscribeTool: ToolConfig = { + id: 'reddit_subscribe', + name: 'Subscribe/Unsubscribe from Subreddit', + description: 'Subscribe or unsubscribe from a subreddit', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['subscribe'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + subreddit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the subreddit (without the r/ prefix)', + }, + action: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Action to perform: "sub" to subscribe or "unsub" to unsubscribe', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/subscribe', + method: 'POST', + headers: (params: RedditSubscribeParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditSubscribeParams) => { + // Validate action + if (!['sub', 'unsub'].includes(params.action)) { + throw new Error('action must be "sub" or "unsub"') + } + + // Sanitize subreddit + const subreddit = params.subreddit.trim().replace(/^r\//, '') + + const formData = new URLSearchParams({ + action: params.action, + sr_name: subreddit, + }) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditSubscribeParams) => { + // Reddit subscribe API returns empty JSON {} on success + await response.json() + + if (response.ok) { + const actionText = + requestParams?.action === 'sub' + ? `subscribed to r/${requestParams?.subreddit || 'subreddit'}` + : `unsubscribed from r/${requestParams?.subreddit || 'subreddit'}` + + return { + success: true, + output: { + success: true, + message: `Successfully ${actionText}`, + }, + } + } + + return { + success: false, + output: { + success: false, + message: 'Failed to update subscription', + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the subscription action was successful', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + }, +} diff --git a/apps/sim/tools/reddit/types.ts b/apps/sim/tools/reddit/types.ts index d57620b9d0..94fbd24f82 100644 --- a/apps/sim/tools/reddit/types.ts +++ b/apps/sim/tools/reddit/types.ts @@ -39,6 +39,12 @@ export interface RedditPostsParams { sort?: 'hot' | 'new' | 'top' | 'rising' limit?: number time?: 'day' | 'week' | 'month' | 'year' | 'all' + // Pagination parameters + after?: string + before?: string + count?: number + show?: string + sr_detail?: boolean accessToken?: string } @@ -56,6 +62,18 @@ export interface RedditCommentsParams { subreddit: string sort?: 'confidence' | 'top' | 'new' | 'controversial' | 'old' | 'random' | 'qa' limit?: number + // Comment-specific parameters + depth?: number + context?: number + showedits?: boolean + showmore?: boolean + showtitle?: boolean + threaded?: boolean + truncate?: number + // Pagination parameters + after?: string + before?: string + count?: number accessToken?: string } @@ -75,4 +93,110 @@ export interface RedditCommentsResponse extends ToolResponse { } } -export type RedditResponse = RedditHotPostsResponse | RedditPostsResponse | RedditCommentsResponse +// Parameters for controversial posts +export interface RedditControversialParams { + subreddit: string + time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all' + limit?: number + after?: string + before?: string + count?: number + show?: string + sr_detail?: boolean + accessToken?: string +} + +// Parameters for gilded posts +export interface RedditGildedParams { + subreddit: string + limit?: number + after?: string + before?: string + count?: number + show?: string + sr_detail?: boolean + accessToken?: string +} + +// Parameters for search +export interface RedditSearchParams { + subreddit: string + query: string + sort?: 'relevance' | 'hot' | 'top' | 'new' | 'comments' + time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all' + limit?: number + after?: string + before?: string + count?: number + show?: string + restrict_sr?: boolean + accessToken?: string +} + +// Parameters for submit post +export interface RedditSubmitParams { + subreddit: string + title: string + text?: string + url?: string + nsfw?: boolean + spoiler?: boolean + send_replies?: boolean + accessToken?: string +} + +// Parameters for vote +export interface RedditVoteParams { + id: string // Thing fullname (e.g., t3_xxxxx for post, t1_xxxxx for comment) + dir: 1 | 0 | -1 // 1 = upvote, 0 = unvote, -1 = downvote + accessToken?: string +} + +// Parameters for save/unsave +export interface RedditSaveParams { + id: string // Thing fullname + category?: string // Save category + accessToken?: string +} + +// Parameters for reply +export interface RedditReplyParams { + parent_id: string // Thing fullname to reply to + text: string // Comment text in markdown + accessToken?: string +} + +// Parameters for edit +export interface RedditEditParams { + thing_id: string // Thing fullname to edit + text: string // New text in markdown + accessToken?: string +} + +// Parameters for delete +export interface RedditDeleteParams { + id: string // Thing fullname to delete + accessToken?: string +} + +// Parameters for subscribe/unsubscribe +export interface RedditSubscribeParams { + subreddit: string + action: 'sub' | 'unsub' + accessToken?: string +} + +// Generic success response for write operations +export interface RedditWriteResponse extends ToolResponse { + output: { + success: boolean + message?: string + data?: any + } +} + +export type RedditResponse = + | RedditHotPostsResponse + | RedditPostsResponse + | RedditCommentsResponse + | RedditWriteResponse diff --git a/apps/sim/tools/reddit/vote.ts b/apps/sim/tools/reddit/vote.ts new file mode 100644 index 0000000000..fe93a55c50 --- /dev/null +++ b/apps/sim/tools/reddit/vote.ts @@ -0,0 +1,103 @@ +import type { RedditVoteParams, RedditWriteResponse } from '@/tools/reddit/types' +import type { ToolConfig } from '@/tools/types' + +export const voteTool: ToolConfig = { + id: 'reddit_vote', + name: 'Vote on Reddit Post/Comment', + description: 'Upvote, downvote, or unvote a Reddit post or comment', + version: '1.0.0', + + oauth: { + required: true, + provider: 'reddit', + additionalScopes: ['vote'], + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Reddit API', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Thing fullname to vote on (e.g., t3_xxxxx for post, t1_xxxxx for comment)', + }, + dir: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Vote direction: 1 (upvote), 0 (unvote), or -1 (downvote)', + }, + }, + + request: { + url: () => 'https://oauth.reddit.com/api/vote', + method: 'POST', + headers: (params: RedditVoteParams) => { + if (!params.accessToken) { + throw new Error('Access token is required for Reddit API') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'User-Agent': 'sim-studio/1.0 (https://github.com/simstudioai/sim)', + 'Content-Type': 'application/x-www-form-urlencoded', + } + }, + body: (params: RedditVoteParams) => { + // Validate dir parameter + if (![1, 0, -1].includes(params.dir)) { + throw new Error('dir must be 1 (upvote), 0 (unvote), or -1 (downvote)') + } + + const formData = new URLSearchParams({ + id: params.id, + dir: params.dir.toString(), + }) + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response: Response, requestParams?: RedditVoteParams) => { + // Reddit vote API returns empty JSON {} on success + const data = await response.json() + + if (response.ok) { + const action = + requestParams?.dir === 1 ? 'upvoted' : requestParams?.dir === -1 ? 'downvoted' : 'unvoted' + + return { + success: true, + output: { + success: true, + message: `Successfully ${action} ${requestParams?.id}`, + }, + } + } + + return { + success: false, + output: { + success: false, + message: 'Failed to vote', + data, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the vote was successful', + }, + message: { + type: 'string', + description: 'Success or error message', + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 274f21786d..fcbb64d5af 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -8,12 +8,60 @@ import { import { arxivGetAuthorPapersTool, arxivGetPaperTool, arxivSearchTool } from '@/tools/arxiv' import { browserUseRunTaskTool } from '@/tools/browser_use' import { clayPopulateTool } from '@/tools/clay' -import { confluenceRetrieveTool, confluenceUpdateTool } from '@/tools/confluence' import { + confluenceAddLabelTool, + confluenceCreateCommentTool, + confluenceCreatePageTool, + confluenceDeleteAttachmentTool, + confluenceDeleteCommentTool, + confluenceDeletePageTool, + confluenceGetSpaceTool, + confluenceListAttachmentsTool, + confluenceListCommentsTool, + confluenceListLabelsTool, + confluenceListSpacesTool, + confluenceRemoveLabelTool, + confluenceRetrieveTool, + confluenceSearchTool, + confluenceUpdateCommentTool, + confluenceUpdateTool, +} from '@/tools/confluence' +import { + discordAddReactionTool, + discordArchiveThreadTool, + discordAssignRoleTool, + discordBanMemberTool, + discordCreateChannelTool, + discordCreateInviteTool, + discordCreateRoleTool, + discordCreateThreadTool, + discordCreateWebhookTool, + discordDeleteChannelTool, + discordDeleteInviteTool, + discordDeleteMessageTool, + discordDeleteRoleTool, + discordDeleteWebhookTool, + discordEditMessageTool, + discordExecuteWebhookTool, + discordGetChannelTool, + discordGetInviteTool, + discordGetMemberTool, discordGetMessagesTool, discordGetServerTool, discordGetUserTool, + discordGetWebhookTool, + discordJoinThreadTool, + discordKickMemberTool, + discordLeaveThreadTool, + discordPinMessageTool, + discordRemoveReactionTool, + discordRemoveRoleTool, discordSendMessageTool, + discordUnbanMemberTool, + discordUnpinMessageTool, + discordUpdateChannelTool, + discordUpdateMemberTool, + discordUpdateRoleTool, } from '@/tools/discord' import { elevenLabsTtsTool } from '@/tools/elevenlabs' import { @@ -24,7 +72,7 @@ import { exaSearchTool, } from '@/tools/exa' import { fileParseTool } from '@/tools/file' -import { crawlTool, scrapeTool, searchTool } from '@/tools/firecrawl' +import { crawlTool, extractTool, mapTool, scrapeTool, searchTool } from '@/tools/firecrawl' import { functionExecuteTool } from '@/tools/function' import { githubAddAssigneesTool, @@ -137,14 +185,85 @@ import { hunterEmailFinderTool, hunterEmailVerifierTool, } from '@/tools/hunter' -import { readUrlTool } from '@/tools/jina' -import { jiraBulkRetrieveTool, jiraRetrieveTool, jiraUpdateTool, jiraWriteTool } from '@/tools/jira' +import { searchTool as jinaSearchTool, readUrlTool } from '@/tools/jina' +import { + jiraAddCommentTool, + jiraAddWatcherTool, + jiraAddWorklogTool, + jiraAssignIssueTool, + jiraBulkRetrieveTool, + jiraCreateIssueLinkTool, + jiraDeleteAttachmentTool, + jiraDeleteCommentTool, + jiraDeleteIssueLinkTool, + jiraDeleteIssueTool, + jiraDeleteWorklogTool, + jiraGetAttachmentsTool, + jiraGetCommentsTool, + jiraGetWorklogsTool, + jiraRemoveWatcherTool, + jiraRetrieveTool, + jiraSearchIssuesTool, + jiraTransitionIssueTool, + jiraUpdateCommentTool, + jiraUpdateTool, + jiraUpdateWorklogTool, + jiraWriteTool, +} from '@/tools/jira' import { knowledgeCreateDocumentTool, knowledgeSearchTool, knowledgeUploadChunkTool, } from '@/tools/knowledge' -import { linearCreateIssueTool, linearReadIssuesTool } from '@/tools/linear' +import { + linearAddLabelToIssueTool, + linearArchiveIssueTool, + linearArchiveLabelTool, + linearArchiveProjectTool, + linearCreateAttachmentTool, + linearCreateCommentTool, + linearCreateCycleTool, + linearCreateFavoriteTool, + linearCreateIssueRelationTool, + linearCreateIssueTool, + linearCreateLabelTool, + linearCreateProjectLinkTool, + linearCreateProjectTool, + linearCreateProjectUpdateTool, + linearCreateWorkflowStateTool, + linearDeleteAttachmentTool, + linearDeleteCommentTool, + linearDeleteIssueRelationTool, + linearDeleteIssueTool, + linearGetActiveCycleTool, + linearGetCycleTool, + linearGetIssueTool, + linearGetProjectTool, + linearGetViewerTool, + linearListAttachmentsTool, + linearListCommentsTool, + linearListCyclesTool, + linearListFavoritesTool, + linearListIssueRelationsTool, + linearListLabelsTool, + linearListNotificationsTool, + linearListProjectsTool, + linearListProjectUpdatesTool, + linearListTeamsTool, + linearListUsersTool, + linearListWorkflowStatesTool, + linearReadIssuesTool, + linearRemoveLabelFromIssueTool, + linearSearchIssuesTool, + linearUnarchiveIssueTool, + linearUpdateAttachmentTool, + linearUpdateCommentTool, + linearUpdateIssueTool, + linearUpdateLabelTool, + linearUpdateNotificationTool, + linearUpdateProjectTool, + linearUpdateWorkflowStateTool, +} from '@/tools/linear' import { linkupSearchTool } from '@/tools/linkup' import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from '@/tools/mem0' import { memoryAddTool, memoryDeleteTool, memoryGetAllTool, memoryGetTool } from '@/tools/memory' @@ -154,12 +273,33 @@ import { microsoftExcelWriteTool, } from '@/tools/microsoft_excel' import { + microsoftPlannerCreateBucketTool, microsoftPlannerCreateTaskTool, + microsoftPlannerDeleteBucketTool, + microsoftPlannerDeleteTaskTool, + microsoftPlannerGetTaskDetailsTool, + microsoftPlannerListBucketsTool, + microsoftPlannerListPlansTool, + microsoftPlannerReadBucketTool, + microsoftPlannerReadPlanTool, microsoftPlannerReadTaskTool, + microsoftPlannerUpdateBucketTool, + microsoftPlannerUpdateTaskDetailsTool, + microsoftPlannerUpdateTaskTool, } from '@/tools/microsoft_planner' import { + microsoftTeamsDeleteChannelMessageTool, + microsoftTeamsDeleteChatMessageTool, + microsoftTeamsGetMessageTool, + microsoftTeamsListChannelMembersTool, + microsoftTeamsListTeamMembersTool, microsoftTeamsReadChannelTool, microsoftTeamsReadChatTool, + microsoftTeamsReplyToMessageTool, + microsoftTeamsSetReactionTool, + microsoftTeamsUnsetReactionTool, + microsoftTeamsUpdateChannelMessageTool, + microsoftTeamsUpdateChatMessageTool, microsoftTeamsWriteChannelTool, microsoftTeamsWriteChatTool, } from '@/tools/microsoft_teams' @@ -205,7 +345,7 @@ import { outlookReadTool, outlookSendTool, } from '@/tools/outlook' -import { parallelSearchTool } from '@/tools/parallel' +import { parallelDeepResearchTool, parallelExtractTool, parallelSearchTool } from '@/tools/parallel' import { perplexityChatTool, perplexitySearchTool } from '@/tools/perplexity' import { pineconeFetchTool, @@ -222,7 +362,22 @@ import { updateTool as postgresUpdateTool, } from '@/tools/postgresql' import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdrant' -import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from '@/tools/reddit' +import { + redditDeleteTool, + redditEditTool, + redditGetCommentsTool, + redditGetControversialTool, + redditGetGildedTool, + redditGetPostsTool, + redditHotPostsTool, + redditReplyTool, + redditSaveTool, + redditSearchTool, + redditSubmitPostTool, + redditSubscribeTool, + redditUnsaveTool, + redditVoteTool, +} from '@/tools/reddit' import { mailSendTool } from '@/tools/resend' import { s3CopyObjectTool, @@ -254,15 +409,81 @@ import { import { smsSendTool } from '@/tools/sms' import { stagehandAgentTool, stagehandExtractTool } from '@/tools/stagehand' import { + stripeCancelPaymentIntentTool, + stripeCancelSubscriptionTool, + stripeCaptureChargeTool, + stripeCapturePaymentIntentTool, + stripeConfirmPaymentIntentTool, + stripeCreateChargeTool, + stripeCreateCustomerTool, + stripeCreateInvoiceTool, + stripeCreatePaymentIntentTool, + stripeCreatePriceTool, + stripeCreateProductTool, + stripeCreateSubscriptionTool, + stripeDeleteCustomerTool, + stripeDeleteInvoiceTool, + stripeDeleteProductTool, + stripeFinalizeInvoiceTool, + stripeListChargesTool, + stripeListCustomersTool, + stripeListEventsTool, + stripeListInvoicesTool, + stripeListPaymentIntentsTool, + stripeListPricesTool, + stripeListProductsTool, + stripeListSubscriptionsTool, + stripePayInvoiceTool, + stripeResumeSubscriptionTool, + stripeRetrieveChargeTool, + stripeRetrieveCustomerTool, + stripeRetrieveEventTool, + stripeRetrieveInvoiceTool, + stripeRetrievePaymentIntentTool, + stripeRetrievePriceTool, + stripeRetrieveProductTool, + stripeRetrieveSubscriptionTool, + stripeSearchChargesTool, + stripeSearchCustomersTool, + stripeSearchInvoicesTool, + stripeSearchPaymentIntentsTool, + stripeSearchPricesTool, + stripeSearchProductsTool, + stripeSearchSubscriptionsTool, + stripeSendInvoiceTool, + stripeUpdateChargeTool, + stripeUpdateCustomerTool, + stripeUpdateInvoiceTool, + stripeUpdatePaymentIntentTool, + stripeUpdatePriceTool, + stripeUpdateProductTool, + stripeUpdateSubscriptionTool, + stripeVoidInvoiceTool, +} from '@/tools/stripe' +import { + supabaseCountTool, supabaseDeleteTool, supabaseGetRowTool, supabaseInsertTool, supabaseQueryTool, + supabaseRpcTool, + supabaseStorageCopyTool, + supabaseStorageCreateBucketTool, + supabaseStorageCreateSignedUrlTool, + supabaseStorageDeleteBucketTool, + supabaseStorageDeleteTool, + supabaseStorageDownloadTool, + supabaseStorageGetPublicUrlTool, + supabaseStorageListBucketsTool, + supabaseStorageListTool, + supabaseStorageMoveTool, + supabaseStorageUploadTool, + supabaseTextSearchTool, supabaseUpdateTool, supabaseUpsertTool, supabaseVectorSearchTool, } from '@/tools/supabase' -import { tavilyExtractTool, tavilySearchTool } from '@/tools/tavily' +import { tavilyCrawlTool, tavilyExtractTool, tavilyMapTool, tavilySearchTool } from '@/tools/tavily' import { telegramDeleteMessageTool, telegramMessageTool, @@ -348,9 +569,12 @@ export const tools: Record = { firecrawl_scrape: scrapeTool, firecrawl_search: searchTool, firecrawl_crawl: crawlTool, + firecrawl_map: mapTool, + firecrawl_extract: extractTool, google_search: googleSearchTool, guardrails_validate: guardrailsValidateTool, jina_read_url: readUrlTool, + jina_search: jinaSearchTool, linkup_search: linkupSearchTool, resend_send: mailSendTool, sms_send: smsSendTool, @@ -358,6 +582,24 @@ export const tools: Record = { jira_update: jiraUpdateTool, jira_write: jiraWriteTool, jira_bulk_read: jiraBulkRetrieveTool, + jira_delete_issue: jiraDeleteIssueTool, + jira_assign_issue: jiraAssignIssueTool, + jira_transition_issue: jiraTransitionIssueTool, + jira_search_issues: jiraSearchIssuesTool, + jira_add_comment: jiraAddCommentTool, + jira_get_comments: jiraGetCommentsTool, + jira_update_comment: jiraUpdateCommentTool, + jira_delete_comment: jiraDeleteCommentTool, + jira_get_attachments: jiraGetAttachmentsTool, + jira_delete_attachment: jiraDeleteAttachmentTool, + jira_add_worklog: jiraAddWorklogTool, + jira_get_worklogs: jiraGetWorklogsTool, + jira_update_worklog: jiraUpdateWorklogTool, + jira_delete_worklog: jiraDeleteWorklogTool, + jira_create_issue_link: jiraCreateIssueLinkTool, + jira_delete_issue_link: jiraDeleteIssueLinkTool, + jira_add_watcher: jiraAddWatcherTool, + jira_remove_watcher: jiraRemoveWatcherTool, slack_message: slackMessageTool, slack_message_reader: slackMessageReaderTool, slack_canvas: slackCanvasTool, @@ -370,13 +612,29 @@ export const tools: Record = { serper_search: serperSearch, tavily_search: tavilySearchTool, tavily_extract: tavilyExtractTool, + tavily_crawl: tavilyCrawlTool, + tavily_map: tavilyMapTool, supabase_query: supabaseQueryTool, supabase_insert: supabaseInsertTool, supabase_get_row: supabaseGetRowTool, supabase_update: supabaseUpdateTool, supabase_delete: supabaseDeleteTool, supabase_upsert: supabaseUpsertTool, + supabase_count: supabaseCountTool, + supabase_text_search: supabaseTextSearchTool, supabase_vector_search: supabaseVectorSearchTool, + supabase_rpc: supabaseRpcTool, + supabase_storage_upload: supabaseStorageUploadTool, + supabase_storage_download: supabaseStorageDownloadTool, + supabase_storage_list: supabaseStorageListTool, + supabase_storage_delete: supabaseStorageDeleteTool, + supabase_storage_move: supabaseStorageMoveTool, + supabase_storage_copy: supabaseStorageCopyTool, + supabase_storage_create_bucket: supabaseStorageCreateBucketTool, + supabase_storage_list_buckets: supabaseStorageListBucketsTool, + supabase_storage_delete_bucket: supabaseStorageDeleteBucketTool, + supabase_storage_get_public_url: supabaseStorageGetPublicUrlTool, + supabase_storage_create_signed_url: supabaseStorageCreateSignedUrlTool, typeform_responses: typeformResponsesTool, typeform_files: typeformFilesTool, typeform_insights: typeformInsightsTool, @@ -493,9 +751,22 @@ export const tools: Record = { exa_answer: exaAnswerTool, exa_research: exaResearchTool, parallel_search: parallelSearchTool, + parallel_extract: parallelExtractTool, + parallel_deep_research: parallelDeepResearchTool, reddit_hot_posts: redditHotPostsTool, reddit_get_posts: redditGetPostsTool, reddit_get_comments: redditGetCommentsTool, + reddit_get_controversial: redditGetControversialTool, + reddit_get_gilded: redditGetGildedTool, + reddit_search: redditSearchTool, + reddit_submit_post: redditSubmitPostTool, + reddit_vote: redditVoteTool, + reddit_save: redditSaveTool, + reddit_unsave: redditUnsaveTool, + reddit_reply: redditReplyTool, + reddit_edit: redditEditTool, + reddit_delete: redditDeleteTool, + reddit_subscribe: redditSubscribeTool, google_drive_get_content: googleDriveGetContentTool, google_drive_list: googleDriveListTool, google_drive_upload: googleDriveUploadTool, @@ -512,6 +783,20 @@ export const tools: Record = { perplexity_search: perplexitySearchTool, confluence_retrieve: confluenceRetrieveTool, confluence_update: confluenceUpdateTool, + confluence_create_page: confluenceCreatePageTool, + confluence_delete_page: confluenceDeletePageTool, + confluence_search: confluenceSearchTool, + confluence_create_comment: confluenceCreateCommentTool, + confluence_list_comments: confluenceListCommentsTool, + confluence_update_comment: confluenceUpdateCommentTool, + confluence_delete_comment: confluenceDeleteCommentTool, + confluence_list_attachments: confluenceListAttachmentsTool, + confluence_delete_attachment: confluenceDeleteAttachmentTool, + confluence_add_label: confluenceAddLabelTool, + confluence_list_labels: confluenceListLabelsTool, + confluence_remove_label: confluenceRemoveLabelTool, + confluence_get_space: confluenceGetSpaceTool, + confluence_list_spaces: confluenceListSpacesTool, twilio_send_sms: sendSMSTool, twilio_voice_make_call: makeCallTool, twilio_voice_list_calls: listCallsTool, @@ -561,11 +846,52 @@ export const tools: Record = { discord_get_messages: discordGetMessagesTool, discord_get_server: discordGetServerTool, discord_get_user: discordGetUserTool, + discord_edit_message: discordEditMessageTool, + discord_delete_message: discordDeleteMessageTool, + discord_add_reaction: discordAddReactionTool, + discord_remove_reaction: discordRemoveReactionTool, + discord_pin_message: discordPinMessageTool, + discord_unpin_message: discordUnpinMessageTool, + discord_create_thread: discordCreateThreadTool, + discord_join_thread: discordJoinThreadTool, + discord_leave_thread: discordLeaveThreadTool, + discord_archive_thread: discordArchiveThreadTool, + discord_create_channel: discordCreateChannelTool, + discord_update_channel: discordUpdateChannelTool, + discord_delete_channel: discordDeleteChannelTool, + discord_get_channel: discordGetChannelTool, + discord_create_role: discordCreateRoleTool, + discord_update_role: discordUpdateRoleTool, + discord_delete_role: discordDeleteRoleTool, + discord_assign_role: discordAssignRoleTool, + discord_remove_role: discordRemoveRoleTool, + discord_kick_member: discordKickMemberTool, + discord_ban_member: discordBanMemberTool, + discord_unban_member: discordUnbanMemberTool, + discord_get_member: discordGetMemberTool, + discord_update_member: discordUpdateMemberTool, + discord_create_invite: discordCreateInviteTool, + discord_get_invite: discordGetInviteTool, + discord_delete_invite: discordDeleteInviteTool, + discord_create_webhook: discordCreateWebhookTool, + discord_execute_webhook: discordExecuteWebhookTool, + discord_get_webhook: discordGetWebhookTool, + discord_delete_webhook: discordDeleteWebhookTool, openai_image: imageTool, microsoft_teams_read_chat: microsoftTeamsReadChatTool, microsoft_teams_write_chat: microsoftTeamsWriteChatTool, microsoft_teams_read_channel: microsoftTeamsReadChannelTool, microsoft_teams_write_channel: microsoftTeamsWriteChannelTool, + microsoft_teams_update_chat_message: microsoftTeamsUpdateChatMessageTool, + microsoft_teams_update_channel_message: microsoftTeamsUpdateChannelMessageTool, + microsoft_teams_delete_chat_message: microsoftTeamsDeleteChatMessageTool, + microsoft_teams_delete_channel_message: microsoftTeamsDeleteChannelMessageTool, + microsoft_teams_reply_to_message: microsoftTeamsReplyToMessageTool, + microsoft_teams_set_reaction: microsoftTeamsSetReactionTool, + microsoft_teams_unset_reaction: microsoftTeamsUnsetReactionTool, + microsoft_teams_get_message: microsoftTeamsGetMessageTool, + microsoft_teams_list_team_members: microsoftTeamsListTeamMembersTool, + microsoft_teams_list_channel_members: microsoftTeamsListChannelMembersTool, outlook_read: outlookReadTool, outlook_send: outlookSendTool, outlook_draft: outlookDraftTool, @@ -577,6 +903,51 @@ export const tools: Record = { outlook_copy: outlookCopyTool, linear_read_issues: linearReadIssuesTool, linear_create_issue: linearCreateIssueTool, + linear_get_issue: linearGetIssueTool, + linear_update_issue: linearUpdateIssueTool, + linear_archive_issue: linearArchiveIssueTool, + linear_unarchive_issue: linearUnarchiveIssueTool, + linear_delete_issue: linearDeleteIssueTool, + linear_add_label_to_issue: linearAddLabelToIssueTool, + linear_remove_label_from_issue: linearRemoveLabelFromIssueTool, + linear_search_issues: linearSearchIssuesTool, + linear_create_comment: linearCreateCommentTool, + linear_update_comment: linearUpdateCommentTool, + linear_delete_comment: linearDeleteCommentTool, + linear_list_comments: linearListCommentsTool, + linear_list_projects: linearListProjectsTool, + linear_get_project: linearGetProjectTool, + linear_create_project: linearCreateProjectTool, + linear_update_project: linearUpdateProjectTool, + linear_archive_project: linearArchiveProjectTool, + linear_list_users: linearListUsersTool, + linear_list_teams: linearListTeamsTool, + linear_get_viewer: linearGetViewerTool, + linear_list_labels: linearListLabelsTool, + linear_create_label: linearCreateLabelTool, + linear_update_label: linearUpdateLabelTool, + linear_archive_label: linearArchiveLabelTool, + linear_list_workflow_states: linearListWorkflowStatesTool, + linear_create_workflow_state: linearCreateWorkflowStateTool, + linear_update_workflow_state: linearUpdateWorkflowStateTool, + linear_list_cycles: linearListCyclesTool, + linear_get_cycle: linearGetCycleTool, + linear_create_cycle: linearCreateCycleTool, + linear_get_active_cycle: linearGetActiveCycleTool, + linear_create_attachment: linearCreateAttachmentTool, + linear_list_attachments: linearListAttachmentsTool, + linear_update_attachment: linearUpdateAttachmentTool, + linear_delete_attachment: linearDeleteAttachmentTool, + linear_create_issue_relation: linearCreateIssueRelationTool, + linear_list_issue_relations: linearListIssueRelationsTool, + linear_delete_issue_relation: linearDeleteIssueRelationTool, + linear_create_favorite: linearCreateFavoriteTool, + linear_list_favorites: linearListFavoritesTool, + linear_create_project_update: linearCreateProjectUpdateTool, + linear_list_project_updates: linearListProjectUpdatesTool, + linear_create_project_link: linearCreateProjectLinkTool, + linear_list_notifications: linearListNotificationsTool, + linear_update_notification: linearUpdateNotificationTool, onedrive_create_folder: onedriveCreateFolderTool, onedrive_download: onedriveDownloadTool, onedrive_list: onedriveListTool, @@ -586,6 +957,17 @@ export const tools: Record = { microsoft_excel_table_add: microsoftExcelTableAddTool, microsoft_planner_create_task: microsoftPlannerCreateTaskTool, microsoft_planner_read_task: microsoftPlannerReadTaskTool, + microsoft_planner_update_task: microsoftPlannerUpdateTaskTool, + microsoft_planner_delete_task: microsoftPlannerDeleteTaskTool, + microsoft_planner_list_plans: microsoftPlannerListPlansTool, + microsoft_planner_read_plan: microsoftPlannerReadPlanTool, + microsoft_planner_list_buckets: microsoftPlannerListBucketsTool, + microsoft_planner_read_bucket: microsoftPlannerReadBucketTool, + microsoft_planner_create_bucket: microsoftPlannerCreateBucketTool, + microsoft_planner_update_bucket: microsoftPlannerUpdateBucketTool, + microsoft_planner_delete_bucket: microsoftPlannerDeleteBucketTool, + microsoft_planner_get_task_details: microsoftPlannerGetTaskDetailsTool, + microsoft_planner_update_task_details: microsoftPlannerUpdateTaskDetailsTool, google_calendar_create: googleCalendarCreateTool, google_calendar_get: googleCalendarGetTool, google_calendar_list: googleCalendarListTool, @@ -632,4 +1014,54 @@ export const tools: Record = { sharepoint_update_list: sharepointUpdateListItemTool, sharepoint_add_list_items: sharepointAddListItemTool, sharepoint_upload_file: sharepointUploadFileTool, + stripe_create_payment_intent: stripeCreatePaymentIntentTool, + stripe_retrieve_payment_intent: stripeRetrievePaymentIntentTool, + stripe_update_payment_intent: stripeUpdatePaymentIntentTool, + stripe_confirm_payment_intent: stripeConfirmPaymentIntentTool, + stripe_capture_payment_intent: stripeCapturePaymentIntentTool, + stripe_cancel_payment_intent: stripeCancelPaymentIntentTool, + stripe_list_payment_intents: stripeListPaymentIntentsTool, + stripe_search_payment_intents: stripeSearchPaymentIntentsTool, + stripe_create_customer: stripeCreateCustomerTool, + stripe_retrieve_customer: stripeRetrieveCustomerTool, + stripe_update_customer: stripeUpdateCustomerTool, + stripe_delete_customer: stripeDeleteCustomerTool, + stripe_list_customers: stripeListCustomersTool, + stripe_search_customers: stripeSearchCustomersTool, + stripe_create_subscription: stripeCreateSubscriptionTool, + stripe_retrieve_subscription: stripeRetrieveSubscriptionTool, + stripe_update_subscription: stripeUpdateSubscriptionTool, + stripe_cancel_subscription: stripeCancelSubscriptionTool, + stripe_resume_subscription: stripeResumeSubscriptionTool, + stripe_list_subscriptions: stripeListSubscriptionsTool, + stripe_search_subscriptions: stripeSearchSubscriptionsTool, + stripe_create_invoice: stripeCreateInvoiceTool, + stripe_retrieve_invoice: stripeRetrieveInvoiceTool, + stripe_update_invoice: stripeUpdateInvoiceTool, + stripe_delete_invoice: stripeDeleteInvoiceTool, + stripe_finalize_invoice: stripeFinalizeInvoiceTool, + stripe_pay_invoice: stripePayInvoiceTool, + stripe_void_invoice: stripeVoidInvoiceTool, + stripe_send_invoice: stripeSendInvoiceTool, + stripe_list_invoices: stripeListInvoicesTool, + stripe_search_invoices: stripeSearchInvoicesTool, + stripe_create_charge: stripeCreateChargeTool, + stripe_retrieve_charge: stripeRetrieveChargeTool, + stripe_update_charge: stripeUpdateChargeTool, + stripe_capture_charge: stripeCaptureChargeTool, + stripe_list_charges: stripeListChargesTool, + stripe_search_charges: stripeSearchChargesTool, + stripe_create_product: stripeCreateProductTool, + stripe_retrieve_product: stripeRetrieveProductTool, + stripe_update_product: stripeUpdateProductTool, + stripe_delete_product: stripeDeleteProductTool, + stripe_list_products: stripeListProductsTool, + stripe_search_products: stripeSearchProductsTool, + stripe_create_price: stripeCreatePriceTool, + stripe_retrieve_price: stripeRetrievePriceTool, + stripe_update_price: stripeUpdatePriceTool, + stripe_list_prices: stripeListPricesTool, + stripe_search_prices: stripeSearchPricesTool, + stripe_retrieve_event: stripeRetrieveEventTool, + stripe_list_events: stripeListEventsTool, } diff --git a/apps/sim/tools/s3/list_objects.ts b/apps/sim/tools/s3/list_objects.ts index 7c4d284f44..cc5e9a2ae6 100644 --- a/apps/sim/tools/s3/list_objects.ts +++ b/apps/sim/tools/s3/list_objects.ts @@ -63,7 +63,7 @@ export const s3ListObjectsTool: ToolConfig = { region: params.region, bucketName: params.bucketName, prefix: params.prefix, - maxKeys: params.maxKeys, + maxKeys: params.maxKeys !== undefined ? Number(params.maxKeys) : undefined, continuationToken: params.continuationToken, }), }, diff --git a/apps/sim/tools/serper/search.ts b/apps/sim/tools/serper/search.ts index 6e56d36d47..a1dc11144f 100644 --- a/apps/sim/tools/serper/search.ts +++ b/apps/sim/tools/serper/search.ts @@ -60,7 +60,7 @@ export const searchTool: ToolConfig = { } // Only include optional parameters if they are explicitly set - if (params.num) body.num = params.num + if (params.num) body.num = Number(params.num) if (params.gl) body.gl = params.gl if (params.hl) body.hl = params.hl diff --git a/apps/sim/tools/slack/message_reader.ts b/apps/sim/tools/slack/message_reader.ts index 022e5a5a85..93f70a0cb7 100644 --- a/apps/sim/tools/slack/message_reader.ts +++ b/apps/sim/tools/slack/message_reader.ts @@ -73,7 +73,8 @@ export const slackMessageReaderTool: ToolConfig< const url = new URL('https://slack.com/api/conversations.history') url.searchParams.append('channel', params.channel) // Cap limit at 15 due to Slack API restrictions for non-Marketplace apps - url.searchParams.append('limit', String(Math.min(params.limit || 10, 15))) + const limit = params.limit ? Number(params.limit) : 10 + url.searchParams.append('limit', String(Math.min(limit, 15))) if (params.oldest) { url.searchParams.append('oldest', params.oldest) diff --git a/apps/sim/tools/stripe/cancel_payment_intent.ts b/apps/sim/tools/stripe/cancel_payment_intent.ts new file mode 100644 index 0000000000..4487c11150 --- /dev/null +++ b/apps/sim/tools/stripe/cancel_payment_intent.ts @@ -0,0 +1,77 @@ +import type { CancelPaymentIntentParams, PaymentIntentResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCancelPaymentIntentTool: ToolConfig< + CancelPaymentIntentParams, + PaymentIntentResponse +> = { + id: 'stripe_cancel_payment_intent', + name: 'Stripe Cancel Payment Intent', + description: 'Cancel a Payment Intent', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment Intent ID (e.g., pi_1234567890)', + }, + cancellation_reason: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Reason for cancellation (duplicate, fraudulent, requested_by_customer, abandoned)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/payment_intents/${params.id}/cancel`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + if (params.cancellation_reason) { + formData.append('cancellation_reason', params.cancellation_reason) + } + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intent: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + payment_intent: { + type: 'json', + description: 'The canceled Payment Intent object', + }, + metadata: { + type: 'json', + description: 'Payment Intent metadata including ID, status, amount, and currency', + }, + }, +} diff --git a/apps/sim/tools/stripe/cancel_subscription.ts b/apps/sim/tools/stripe/cancel_subscription.ts new file mode 100644 index 0000000000..1d9722abf7 --- /dev/null +++ b/apps/sim/tools/stripe/cancel_subscription.ts @@ -0,0 +1,86 @@ +import type { CancelSubscriptionParams, SubscriptionResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCancelSubscriptionTool: ToolConfig< + CancelSubscriptionParams, + SubscriptionResponse +> = { + id: 'stripe_cancel_subscription', + name: 'Stripe Cancel Subscription', + description: 'Cancel a subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Subscription ID (e.g., sub_1234567890)', + }, + prorate: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to prorate the cancellation', + }, + invoice_now: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to invoice immediately', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/subscriptions/${params.id}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.prorate !== undefined) { + formData.append('prorate', String(params.prorate)) + } + if (params.invoice_now !== undefined) { + formData.append('invoice_now', String(params.invoice_now)) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscription: data, + metadata: { + id: data.id, + status: data.status, + customer: data.customer, + }, + }, + } + }, + + outputs: { + subscription: { + type: 'json', + description: 'The canceled subscription object', + }, + metadata: { + type: 'json', + description: 'Subscription metadata including ID, status, and customer', + }, + }, +} diff --git a/apps/sim/tools/stripe/capture_charge.ts b/apps/sim/tools/stripe/capture_charge.ts new file mode 100644 index 0000000000..f5086fcf1f --- /dev/null +++ b/apps/sim/tools/stripe/capture_charge.ts @@ -0,0 +1,74 @@ +import type { CaptureChargeParams, ChargeResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCaptureChargeTool: ToolConfig = { + id: 'stripe_capture_charge', + name: 'Stripe Capture Charge', + description: 'Capture an uncaptured charge', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Charge ID (e.g., ch_1234567890)', + }, + amount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Amount to capture in cents (defaults to full amount)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/charges/${params.id}/capture`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + if (params.amount) { + formData.append('amount', Number(params.amount).toString()) + } + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + charge: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + paid: data.paid, + }, + }, + } + }, + + outputs: { + charge: { + type: 'json', + description: 'The captured Charge object', + }, + metadata: { + type: 'json', + description: 'Charge metadata including ID, status, amount, currency, and paid status', + }, + }, +} diff --git a/apps/sim/tools/stripe/capture_payment_intent.ts b/apps/sim/tools/stripe/capture_payment_intent.ts new file mode 100644 index 0000000000..5cd034942d --- /dev/null +++ b/apps/sim/tools/stripe/capture_payment_intent.ts @@ -0,0 +1,76 @@ +import type { CapturePaymentIntentParams, PaymentIntentResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCapturePaymentIntentTool: ToolConfig< + CapturePaymentIntentParams, + PaymentIntentResponse +> = { + id: 'stripe_capture_payment_intent', + name: 'Stripe Capture Payment Intent', + description: 'Capture an authorized Payment Intent', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment Intent ID (e.g., pi_1234567890)', + }, + amount_to_capture: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Amount to capture in cents (defaults to full amount)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/payment_intents/${params.id}/capture`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + if (params.amount_to_capture) { + formData.append('amount_to_capture', Number(params.amount_to_capture).toString()) + } + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intent: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + payment_intent: { + type: 'json', + description: 'The captured Payment Intent object', + }, + metadata: { + type: 'json', + description: 'Payment Intent metadata including ID, status, amount, and currency', + }, + }, +} diff --git a/apps/sim/tools/stripe/confirm_payment_intent.ts b/apps/sim/tools/stripe/confirm_payment_intent.ts new file mode 100644 index 0000000000..f4dc6d8997 --- /dev/null +++ b/apps/sim/tools/stripe/confirm_payment_intent.ts @@ -0,0 +1,74 @@ +import type { ConfirmPaymentIntentParams, PaymentIntentResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeConfirmPaymentIntentTool: ToolConfig< + ConfirmPaymentIntentParams, + PaymentIntentResponse +> = { + id: 'stripe_confirm_payment_intent', + name: 'Stripe Confirm Payment Intent', + description: 'Confirm a Payment Intent to complete the payment', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment Intent ID (e.g., pi_1234567890)', + }, + payment_method: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment method ID to confirm with', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/payment_intents/${params.id}/confirm`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + if (params.payment_method) formData.append('payment_method', params.payment_method) + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intent: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + payment_intent: { + type: 'json', + description: 'The confirmed Payment Intent object', + }, + metadata: { + type: 'json', + description: 'Payment Intent metadata including ID, status, amount, and currency', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_charge.ts b/apps/sim/tools/stripe/create_charge.ts new file mode 100644 index 0000000000..1ad87dab8b --- /dev/null +++ b/apps/sim/tools/stripe/create_charge.ts @@ -0,0 +1,115 @@ +import type { ChargeResponse, CreateChargeParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreateChargeTool: ToolConfig = { + id: 'stripe_create_charge', + name: 'Stripe Create Charge', + description: 'Create a new charge to process a payment', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + amount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Amount in cents (e.g., 2000 for $20.00)', + }, + currency: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Three-letter ISO currency code (e.g., usd, eur)', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer ID to associate with this charge', + }, + source: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment source ID (e.g., card token or saved card ID)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the charge', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs for storing additional information', + }, + capture: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to immediately capture the charge (defaults to true)', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/charges', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + formData.append('amount', Number(params.amount).toString()) + formData.append('currency', params.currency) + + if (params.customer) formData.append('customer', params.customer) + if (params.source) formData.append('source', params.source) + if (params.description) formData.append('description', params.description) + if (params.capture !== undefined) formData.append('capture', String(params.capture)) + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + charge: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + paid: data.paid, + }, + }, + } + }, + + outputs: { + charge: { + type: 'json', + description: 'The created Charge object', + }, + metadata: { + type: 'json', + description: 'Charge metadata including ID, status, amount, currency, and paid status', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_customer.ts b/apps/sim/tools/stripe/create_customer.ts new file mode 100644 index 0000000000..8a6e3319cb --- /dev/null +++ b/apps/sim/tools/stripe/create_customer.ts @@ -0,0 +1,118 @@ +import type { CreateCustomerParams, CustomerResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreateCustomerTool: ToolConfig = { + id: 'stripe_create_customer', + name: 'Stripe Create Customer', + description: 'Create a new customer object', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer email address', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer full name', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer phone number', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the customer', + }, + address: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Customer address object', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs', + }, + payment_method: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment method ID to attach', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/customers', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.email) formData.append('email', params.email) + if (params.name) formData.append('name', params.name) + if (params.phone) formData.append('phone', params.phone) + if (params.description) formData.append('description', params.description) + if (params.payment_method) formData.append('payment_method', params.payment_method) + + if (params.address) { + Object.entries(params.address).forEach(([key, value]) => { + if (value) formData.append(`address[${key}]`, String(value)) + }) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + customer: data, + metadata: { + id: data.id, + email: data.email, + name: data.name, + }, + }, + } + }, + + outputs: { + customer: { + type: 'json', + description: 'The created customer object', + }, + metadata: { + type: 'json', + description: 'Customer metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_invoice.ts b/apps/sim/tools/stripe/create_invoice.ts new file mode 100644 index 0000000000..04427f76d7 --- /dev/null +++ b/apps/sim/tools/stripe/create_invoice.ts @@ -0,0 +1,102 @@ +import type { CreateInvoiceParams, InvoiceResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreateInvoiceTool: ToolConfig = { + id: 'stripe_create_invoice', + name: 'Stripe Create Invoice', + description: 'Create a new invoice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + customer: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID (e.g., cus_1234567890)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the invoice', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs', + }, + auto_advance: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Auto-finalize the invoice', + }, + collection_method: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Collection method: charge_automatically or send_invoice', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/invoices', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + formData.append('customer', params.customer) + + if (params.description) formData.append('description', params.description) + if (params.auto_advance !== undefined) { + formData.append('auto_advance', String(params.auto_advance)) + } + if (params.collection_method) formData.append('collection_method', params.collection_method) + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The created invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_payment_intent.ts b/apps/sim/tools/stripe/create_payment_intent.ts new file mode 100644 index 0000000000..537ab6c827 --- /dev/null +++ b/apps/sim/tools/stripe/create_payment_intent.ts @@ -0,0 +1,128 @@ +import type { CreatePaymentIntentParams, PaymentIntentResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreatePaymentIntentTool: ToolConfig< + CreatePaymentIntentParams, + PaymentIntentResponse +> = { + id: 'stripe_create_payment_intent', + name: 'Stripe Create Payment Intent', + description: 'Create a new Payment Intent to process a payment', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + amount: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Amount in cents (e.g., 2000 for $20.00)', + }, + currency: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Three-letter ISO currency code (e.g., usd, eur)', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer ID to associate with this payment', + }, + payment_method: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment method ID', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the payment', + }, + receipt_email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address to send receipt to', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs for storing additional information', + }, + automatic_payment_methods: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Enable automatic payment methods (e.g., {"enabled": true})', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/payment_intents', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + formData.append('amount', Number(params.amount).toString()) + formData.append('currency', params.currency) + + if (params.customer) formData.append('customer', params.customer) + if (params.payment_method) formData.append('payment_method', params.payment_method) + if (params.description) formData.append('description', params.description) + if (params.receipt_email) formData.append('receipt_email', params.receipt_email) + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + if (params.automatic_payment_methods?.enabled) { + formData.append('automatic_payment_methods[enabled]', 'true') + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intent: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + payment_intent: { + type: 'json', + description: 'The created Payment Intent object', + }, + metadata: { + type: 'json', + description: 'Payment Intent metadata including ID, status, amount, and currency', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_price.ts b/apps/sim/tools/stripe/create_price.ts new file mode 100644 index 0000000000..da0cada1d1 --- /dev/null +++ b/apps/sim/tools/stripe/create_price.ts @@ -0,0 +1,114 @@ +import type { CreatePriceParams, PriceResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreatePriceTool: ToolConfig = { + id: 'stripe_create_price', + name: 'Stripe Create Price', + description: 'Create a new price for a product', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + product: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID (e.g., prod_1234567890)', + }, + currency: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Three-letter ISO currency code (e.g., usd, eur)', + }, + unit_amount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Amount in cents (e.g., 1000 for $10.00)', + }, + recurring: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Recurring billing configuration (interval: day/week/month/year)', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs', + }, + billing_scheme: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Billing scheme (per_unit or tiered)', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/prices', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + formData.append('product', params.product) + formData.append('currency', params.currency) + + if (params.unit_amount !== undefined) + formData.append('unit_amount', Number(params.unit_amount).toString()) + if (params.billing_scheme) formData.append('billing_scheme', params.billing_scheme) + + if (params.recurring) { + Object.entries(params.recurring).forEach(([key, value]) => { + if (value) formData.append(`recurring[${key}]`, String(value)) + }) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + price: data, + metadata: { + id: data.id, + product: data.product, + unit_amount: data.unit_amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + price: { + type: 'json', + description: 'The created price object', + }, + metadata: { + type: 'json', + description: 'Price metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_product.ts b/apps/sim/tools/stripe/create_product.ts new file mode 100644 index 0000000000..aa7ba507f7 --- /dev/null +++ b/apps/sim/tools/stripe/create_product.ts @@ -0,0 +1,104 @@ +import type { CreateProductParams, ProductResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreateProductTool: ToolConfig = { + id: 'stripe_create_product', + name: 'Stripe Create Product', + description: 'Create a new product object', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Product description', + }, + active: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the product is active', + }, + images: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Array of image URLs for the product', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/products', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + formData.append('name', params.name) + if (params.description) formData.append('description', params.description) + if (params.active !== undefined) formData.append('active', String(params.active)) + + if (params.images) { + params.images.forEach((image: string, index: number) => { + formData.append(`images[${index}]`, image) + }) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + product: data, + metadata: { + id: data.id, + name: data.name, + active: data.active, + }, + }, + } + }, + + outputs: { + product: { + type: 'json', + description: 'The created product object', + }, + metadata: { + type: 'json', + description: 'Product metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/create_subscription.ts b/apps/sim/tools/stripe/create_subscription.ts new file mode 100644 index 0000000000..de24e9e3dd --- /dev/null +++ b/apps/sim/tools/stripe/create_subscription.ts @@ -0,0 +1,124 @@ +import type { CreateSubscriptionParams, SubscriptionResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeCreateSubscriptionTool: ToolConfig< + CreateSubscriptionParams, + SubscriptionResponse +> = { + id: 'stripe_create_subscription', + name: 'Stripe Create Subscription', + description: 'Create a new subscription for a customer', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + customer: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID to subscribe', + }, + items: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Array of items with price IDs (e.g., [{"price": "price_xxx", "quantity": 1}])', + }, + trial_period_days: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of trial days', + }, + default_payment_method: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Payment method ID', + }, + cancel_at_period_end: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Cancel subscription at period end', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs for storing additional information', + }, + }, + + request: { + url: () => 'https://api.stripe.com/v1/subscriptions', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + formData.append('customer', params.customer) + + if (params.items && Array.isArray(params.items)) { + params.items.forEach((item, index) => { + formData.append(`items[${index}][price]`, item.price) + if (item.quantity) { + formData.append(`items[${index}][quantity]`, Number(item.quantity).toString()) + } + }) + } + + if (params.trial_period_days !== undefined) { + formData.append('trial_period_days', Number(params.trial_period_days).toString()) + } + if (params.default_payment_method) { + formData.append('default_payment_method', params.default_payment_method) + } + if (params.cancel_at_period_end !== undefined) { + formData.append('cancel_at_period_end', String(params.cancel_at_period_end)) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscription: data, + metadata: { + id: data.id, + status: data.status, + customer: data.customer, + }, + }, + } + }, + + outputs: { + subscription: { + type: 'json', + description: 'The created subscription object', + }, + metadata: { + type: 'json', + description: 'Subscription metadata including ID, status, and customer', + }, + }, +} diff --git a/apps/sim/tools/stripe/delete_customer.ts b/apps/sim/tools/stripe/delete_customer.ts new file mode 100644 index 0000000000..14a5ec0619 --- /dev/null +++ b/apps/sim/tools/stripe/delete_customer.ts @@ -0,0 +1,63 @@ +import type { CustomerDeleteResponse, DeleteCustomerParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeDeleteCustomerTool: ToolConfig = { + id: 'stripe_delete_customer', + name: 'Stripe Delete Customer', + description: 'Permanently delete a customer', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID (e.g., cus_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/customers/${params.id}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + deleted: data.deleted, + id: data.id, + metadata: { + id: data.id, + deleted: data.deleted, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the customer was deleted', + }, + id: { + type: 'string', + description: 'The ID of the deleted customer', + }, + metadata: { + type: 'json', + description: 'Deletion metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/delete_invoice.ts b/apps/sim/tools/stripe/delete_invoice.ts new file mode 100644 index 0000000000..80e5db160e --- /dev/null +++ b/apps/sim/tools/stripe/delete_invoice.ts @@ -0,0 +1,63 @@ +import type { DeleteInvoiceParams, InvoiceDeleteResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeDeleteInvoiceTool: ToolConfig = { + id: 'stripe_delete_invoice', + name: 'Stripe Delete Invoice', + description: 'Permanently delete a draft invoice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + deleted: data.deleted, + id: data.id, + metadata: { + id: data.id, + deleted: data.deleted, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the invoice was deleted', + }, + id: { + type: 'string', + description: 'The ID of the deleted invoice', + }, + metadata: { + type: 'json', + description: 'Deletion metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/delete_product.ts b/apps/sim/tools/stripe/delete_product.ts new file mode 100644 index 0000000000..2d205f4803 --- /dev/null +++ b/apps/sim/tools/stripe/delete_product.ts @@ -0,0 +1,63 @@ +import type { DeleteProductParams, ProductDeleteResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeDeleteProductTool: ToolConfig = { + id: 'stripe_delete_product', + name: 'Stripe Delete Product', + description: 'Permanently delete a product', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID (e.g., prod_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/products/${params.id}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + deleted: data.deleted, + id: data.id, + metadata: { + id: data.id, + deleted: data.deleted, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the product was deleted', + }, + id: { + type: 'string', + description: 'The ID of the deleted product', + }, + metadata: { + type: 'json', + description: 'Deletion metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/finalize_invoice.ts b/apps/sim/tools/stripe/finalize_invoice.ts new file mode 100644 index 0000000000..bd327fea5a --- /dev/null +++ b/apps/sim/tools/stripe/finalize_invoice.ts @@ -0,0 +1,75 @@ +import type { FinalizeInvoiceParams, InvoiceResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeFinalizeInvoiceTool: ToolConfig = { + id: 'stripe_finalize_invoice', + name: 'Stripe Finalize Invoice', + description: 'Finalize a draft invoice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + auto_advance: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Auto-advance the invoice', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}/finalize`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.auto_advance !== undefined) { + formData.append('auto_advance', String(params.auto_advance)) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The finalized invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/index.ts b/apps/sim/tools/stripe/index.ts new file mode 100644 index 0000000000..26a150e8de --- /dev/null +++ b/apps/sim/tools/stripe/index.ts @@ -0,0 +1,51 @@ +export { stripeCancelPaymentIntentTool } from './cancel_payment_intent' +export { stripeCancelSubscriptionTool } from './cancel_subscription' +export { stripeCaptureChargeTool } from './capture_charge' +export { stripeCapturePaymentIntentTool } from './capture_payment_intent' +export { stripeConfirmPaymentIntentTool } from './confirm_payment_intent' +export { stripeCreateChargeTool } from './create_charge' +export { stripeCreateCustomerTool } from './create_customer' +export { stripeCreateInvoiceTool } from './create_invoice' +export { stripeCreatePaymentIntentTool } from './create_payment_intent' +export { stripeCreatePriceTool } from './create_price' +export { stripeCreateProductTool } from './create_product' +export { stripeCreateSubscriptionTool } from './create_subscription' +export { stripeDeleteCustomerTool } from './delete_customer' +export { stripeDeleteInvoiceTool } from './delete_invoice' +export { stripeDeleteProductTool } from './delete_product' +export { stripeFinalizeInvoiceTool } from './finalize_invoice' +export { stripeListChargesTool } from './list_charges' +export { stripeListCustomersTool } from './list_customers' +export { stripeListEventsTool } from './list_events' +export { stripeListInvoicesTool } from './list_invoices' +export { stripeListPaymentIntentsTool } from './list_payment_intents' +export { stripeListPricesTool } from './list_prices' +export { stripeListProductsTool } from './list_products' +export { stripeListSubscriptionsTool } from './list_subscriptions' +export { stripePayInvoiceTool } from './pay_invoice' +export { stripeResumeSubscriptionTool } from './resume_subscription' +export { stripeRetrieveChargeTool } from './retrieve_charge' +export { stripeRetrieveCustomerTool } from './retrieve_customer' +export { stripeRetrieveEventTool } from './retrieve_event' +export { stripeRetrieveInvoiceTool } from './retrieve_invoice' +export { stripeRetrievePaymentIntentTool } from './retrieve_payment_intent' +export { stripeRetrievePriceTool } from './retrieve_price' +export { stripeRetrieveProductTool } from './retrieve_product' +export { stripeRetrieveSubscriptionTool } from './retrieve_subscription' +export { stripeSearchChargesTool } from './search_charges' +export { stripeSearchCustomersTool } from './search_customers' +export { stripeSearchInvoicesTool } from './search_invoices' +export { stripeSearchPaymentIntentsTool } from './search_payment_intents' +export { stripeSearchPricesTool } from './search_prices' +export { stripeSearchProductsTool } from './search_products' +export { stripeSearchSubscriptionsTool } from './search_subscriptions' +export { stripeSendInvoiceTool } from './send_invoice' +export * from './types' +export { stripeUpdateChargeTool } from './update_charge' +export { stripeUpdateCustomerTool } from './update_customer' +export { stripeUpdateInvoiceTool } from './update_invoice' +export { stripeUpdatePaymentIntentTool } from './update_payment_intent' +export { stripeUpdatePriceTool } from './update_price' +export { stripeUpdateProductTool } from './update_product' +export { stripeUpdateSubscriptionTool } from './update_subscription' +export { stripeVoidInvoiceTool } from './void_invoice' diff --git a/apps/sim/tools/stripe/list_charges.ts b/apps/sim/tools/stripe/list_charges.ts new file mode 100644 index 0000000000..493c55d461 --- /dev/null +++ b/apps/sim/tools/stripe/list_charges.ts @@ -0,0 +1,80 @@ +import type { ChargeListResponse, ListChargesParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListChargesTool: ToolConfig = { + id: 'stripe_list_charges', + name: 'Stripe List Charges', + description: 'List all charges', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by customer ID', + }, + created: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Filter by creation date (e.g., {"gt": 1633024800})', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/charges') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.customer) url.searchParams.append('customer', params.customer) + if (params.created) { + Object.entries(params.created).forEach(([key, value]) => { + url.searchParams.append(`created[${key}]`, String(value)) + }) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + charges: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + charges: { + type: 'json', + description: 'Array of Charge objects', + }, + metadata: { + type: 'json', + description: 'List metadata including count and has_more', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_customers.ts b/apps/sim/tools/stripe/list_customers.ts new file mode 100644 index 0000000000..02f4ed7b97 --- /dev/null +++ b/apps/sim/tools/stripe/list_customers.ts @@ -0,0 +1,80 @@ +import type { CustomerListResponse, ListCustomersParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListCustomersTool: ToolConfig = { + id: 'stripe_list_customers', + name: 'Stripe List Customers', + description: 'List all customers', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by email address', + }, + created: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Filter by creation date', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/customers') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.email) url.searchParams.append('email', params.email) + if (params.created) { + Object.entries(params.created).forEach(([key, value]) => { + url.searchParams.append(`created[${key}]`, String(value)) + }) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + customers: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + customers: { + type: 'json', + description: 'Array of customer objects', + }, + metadata: { + type: 'json', + description: 'List metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_events.ts b/apps/sim/tools/stripe/list_events.ts new file mode 100644 index 0000000000..8dc230c57a --- /dev/null +++ b/apps/sim/tools/stripe/list_events.ts @@ -0,0 +1,80 @@ +import type { EventListResponse, ListEventsParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListEventsTool: ToolConfig = { + id: 'stripe_list_events', + name: 'Stripe List Events', + description: 'List all Events', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + type: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by event type (e.g., payment_intent.created)', + }, + created: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Filter by creation date (e.g., {"gt": 1633024800})', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/events') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.type) url.searchParams.append('type', params.type) + if (params.created) { + Object.entries(params.created).forEach(([key, value]) => { + url.searchParams.append(`created[${key}]`, String(value)) + }) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + events: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + events: { + type: 'json', + description: 'Array of Event objects', + }, + metadata: { + type: 'json', + description: 'List metadata including count and has_more', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_invoices.ts b/apps/sim/tools/stripe/list_invoices.ts new file mode 100644 index 0000000000..a7c876d832 --- /dev/null +++ b/apps/sim/tools/stripe/list_invoices.ts @@ -0,0 +1,76 @@ +import type { InvoiceListResponse, ListInvoicesParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListInvoicesTool: ToolConfig = { + id: 'stripe_list_invoices', + name: 'Stripe List Invoices', + description: 'List all invoices', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by customer ID', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by invoice status', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/invoices') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.customer) url.searchParams.append('customer', params.customer) + if (params.status) url.searchParams.append('status', params.status) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoices: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + invoices: { + type: 'json', + description: 'Array of invoice objects', + }, + metadata: { + type: 'json', + description: 'List metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_payment_intents.ts b/apps/sim/tools/stripe/list_payment_intents.ts new file mode 100644 index 0000000000..35c46677e4 --- /dev/null +++ b/apps/sim/tools/stripe/list_payment_intents.ts @@ -0,0 +1,83 @@ +import type { ListPaymentIntentsParams, PaymentIntentListResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListPaymentIntentsTool: ToolConfig< + ListPaymentIntentsParams, + PaymentIntentListResponse +> = { + id: 'stripe_list_payment_intents', + name: 'Stripe List Payment Intents', + description: 'List all Payment Intents', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by customer ID', + }, + created: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Filter by creation date (e.g., {"gt": 1633024800})', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/payment_intents') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.customer) url.searchParams.append('customer', params.customer) + if (params.created) { + Object.entries(params.created).forEach(([key, value]) => { + url.searchParams.append(`created[${key}]`, String(value)) + }) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intents: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + payment_intents: { + type: 'json', + description: 'Array of Payment Intent objects', + }, + metadata: { + type: 'json', + description: 'List metadata including count and has_more', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_prices.ts b/apps/sim/tools/stripe/list_prices.ts new file mode 100644 index 0000000000..8be6800ad2 --- /dev/null +++ b/apps/sim/tools/stripe/list_prices.ts @@ -0,0 +1,76 @@ +import type { ListPricesParams, PriceListResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListPricesTool: ToolConfig = { + id: 'stripe_list_prices', + name: 'Stripe List Prices', + description: 'List all prices', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + product: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by product ID', + }, + active: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Filter by active status', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/prices') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.product) url.searchParams.append('product', params.product) + if (params.active !== undefined) url.searchParams.append('active', params.active.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + prices: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + prices: { + type: 'json', + description: 'Array of price objects', + }, + metadata: { + type: 'json', + description: 'List metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_products.ts b/apps/sim/tools/stripe/list_products.ts new file mode 100644 index 0000000000..65262608da --- /dev/null +++ b/apps/sim/tools/stripe/list_products.ts @@ -0,0 +1,69 @@ +import type { ListProductsParams, ProductListResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListProductsTool: ToolConfig = { + id: 'stripe_list_products', + name: 'Stripe List Products', + description: 'List all products', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + active: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Filter by active status', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/products') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.active !== undefined) url.searchParams.append('active', String(params.active)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + products: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + products: { + type: 'json', + description: 'Array of product objects', + }, + metadata: { + type: 'json', + description: 'List metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/list_subscriptions.ts b/apps/sim/tools/stripe/list_subscriptions.ts new file mode 100644 index 0000000000..08b9eac22e --- /dev/null +++ b/apps/sim/tools/stripe/list_subscriptions.ts @@ -0,0 +1,87 @@ +import type { ListSubscriptionsParams, SubscriptionListResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeListSubscriptionsTool: ToolConfig< + ListSubscriptionsParams, + SubscriptionListResponse +> = { + id: 'stripe_list_subscriptions', + name: 'Stripe List Subscriptions', + description: 'List all subscriptions', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by customer ID', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by status (active, past_due, unpaid, canceled, incomplete, incomplete_expired, trialing, all)', + }, + price: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by price ID', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/subscriptions') + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + if (params.customer) url.searchParams.append('customer', params.customer) + if (params.status) url.searchParams.append('status', params.status) + if (params.price) url.searchParams.append('price', params.price) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscriptions: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + subscriptions: { + type: 'json', + description: 'Array of subscription objects', + }, + metadata: { + type: 'json', + description: 'List metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/pay_invoice.ts b/apps/sim/tools/stripe/pay_invoice.ts new file mode 100644 index 0000000000..56ef0fbc29 --- /dev/null +++ b/apps/sim/tools/stripe/pay_invoice.ts @@ -0,0 +1,73 @@ +import type { InvoiceResponse, PayInvoiceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripePayInvoiceTool: ToolConfig = { + id: 'stripe_pay_invoice', + name: 'Stripe Pay Invoice', + description: 'Pay an invoice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + paid_out_of_band: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Mark invoice as paid out of band', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}/pay`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + if (params.paid_out_of_band !== undefined) { + formData.append('paid_out_of_band', String(params.paid_out_of_band)) + } + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The paid invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/resume_subscription.ts b/apps/sim/tools/stripe/resume_subscription.ts new file mode 100644 index 0000000000..a2512be07e --- /dev/null +++ b/apps/sim/tools/stripe/resume_subscription.ts @@ -0,0 +1,66 @@ +import type { ResumeSubscriptionParams, SubscriptionResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeResumeSubscriptionTool: ToolConfig< + ResumeSubscriptionParams, + SubscriptionResponse +> = { + id: 'stripe_resume_subscription', + name: 'Stripe Resume Subscription', + description: 'Resume a subscription that was scheduled for cancellation', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Subscription ID (e.g., sub_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/subscriptions/${params.id}/resume`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: () => { + const formData = new URLSearchParams() + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscription: data, + metadata: { + id: data.id, + status: data.status, + customer: data.customer, + }, + }, + } + }, + + outputs: { + subscription: { + type: 'json', + description: 'The resumed subscription object', + }, + metadata: { + type: 'json', + description: 'Subscription metadata including ID, status, and customer', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_charge.ts b/apps/sim/tools/stripe/retrieve_charge.ts new file mode 100644 index 0000000000..8d2d719857 --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_charge.ts @@ -0,0 +1,61 @@ +import type { ChargeResponse, RetrieveChargeParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrieveChargeTool: ToolConfig = { + id: 'stripe_retrieve_charge', + name: 'Stripe Retrieve Charge', + description: 'Retrieve an existing charge by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Charge ID (e.g., ch_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/charges/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + charge: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + paid: data.paid, + }, + }, + } + }, + + outputs: { + charge: { + type: 'json', + description: 'The retrieved Charge object', + }, + metadata: { + type: 'json', + description: 'Charge metadata including ID, status, amount, currency, and paid status', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_customer.ts b/apps/sim/tools/stripe/retrieve_customer.ts new file mode 100644 index 0000000000..7188e31d8c --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_customer.ts @@ -0,0 +1,59 @@ +import type { CustomerResponse, RetrieveCustomerParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrieveCustomerTool: ToolConfig = { + id: 'stripe_retrieve_customer', + name: 'Stripe Retrieve Customer', + description: 'Retrieve an existing customer by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID (e.g., cus_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/customers/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + customer: data, + metadata: { + id: data.id, + email: data.email, + name: data.name, + }, + }, + } + }, + + outputs: { + customer: { + type: 'json', + description: 'The retrieved customer object', + }, + metadata: { + type: 'json', + description: 'Customer metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_event.ts b/apps/sim/tools/stripe/retrieve_event.ts new file mode 100644 index 0000000000..870b93839b --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_event.ts @@ -0,0 +1,59 @@ +import type { EventResponse, RetrieveEventParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrieveEventTool: ToolConfig = { + id: 'stripe_retrieve_event', + name: 'Stripe Retrieve Event', + description: 'Retrieve an existing Event by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Event ID (e.g., evt_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/events/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + event: data, + metadata: { + id: data.id, + type: data.type, + created: data.created, + }, + }, + } + }, + + outputs: { + event: { + type: 'json', + description: 'The retrieved Event object', + }, + metadata: { + type: 'json', + description: 'Event metadata including ID, type, and created timestamp', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_invoice.ts b/apps/sim/tools/stripe/retrieve_invoice.ts new file mode 100644 index 0000000000..69fad20cad --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_invoice.ts @@ -0,0 +1,60 @@ +import type { InvoiceResponse, RetrieveInvoiceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrieveInvoiceTool: ToolConfig = { + id: 'stripe_retrieve_invoice', + name: 'Stripe Retrieve Invoice', + description: 'Retrieve an existing invoice by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The retrieved invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_payment_intent.ts b/apps/sim/tools/stripe/retrieve_payment_intent.ts new file mode 100644 index 0000000000..1c70cabac6 --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_payment_intent.ts @@ -0,0 +1,63 @@ +import type { PaymentIntentResponse, RetrievePaymentIntentParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrievePaymentIntentTool: ToolConfig< + RetrievePaymentIntentParams, + PaymentIntentResponse +> = { + id: 'stripe_retrieve_payment_intent', + name: 'Stripe Retrieve Payment Intent', + description: 'Retrieve an existing Payment Intent by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment Intent ID (e.g., pi_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/payment_intents/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intent: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + payment_intent: { + type: 'json', + description: 'The retrieved Payment Intent object', + }, + metadata: { + type: 'json', + description: 'Payment Intent metadata including ID, status, amount, and currency', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_price.ts b/apps/sim/tools/stripe/retrieve_price.ts new file mode 100644 index 0000000000..67cbd6fed8 --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_price.ts @@ -0,0 +1,60 @@ +import type { PriceResponse, RetrievePriceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrievePriceTool: ToolConfig = { + id: 'stripe_retrieve_price', + name: 'Stripe Retrieve Price', + description: 'Retrieve an existing price by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Price ID (e.g., price_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/prices/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + price: data, + metadata: { + id: data.id, + product: data.product, + unit_amount: data.unit_amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + price: { + type: 'json', + description: 'The retrieved price object', + }, + metadata: { + type: 'json', + description: 'Price metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_product.ts b/apps/sim/tools/stripe/retrieve_product.ts new file mode 100644 index 0000000000..b8c496c156 --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_product.ts @@ -0,0 +1,59 @@ +import type { ProductResponse, RetrieveProductParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrieveProductTool: ToolConfig = { + id: 'stripe_retrieve_product', + name: 'Stripe Retrieve Product', + description: 'Retrieve an existing product by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID (e.g., prod_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/products/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + product: data, + metadata: { + id: data.id, + name: data.name, + active: data.active, + }, + }, + } + }, + + outputs: { + product: { + type: 'json', + description: 'The retrieved product object', + }, + metadata: { + type: 'json', + description: 'Product metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/retrieve_subscription.ts b/apps/sim/tools/stripe/retrieve_subscription.ts new file mode 100644 index 0000000000..1ce90a6a76 --- /dev/null +++ b/apps/sim/tools/stripe/retrieve_subscription.ts @@ -0,0 +1,62 @@ +import type { RetrieveSubscriptionParams, SubscriptionResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeRetrieveSubscriptionTool: ToolConfig< + RetrieveSubscriptionParams, + SubscriptionResponse +> = { + id: 'stripe_retrieve_subscription', + name: 'Stripe Retrieve Subscription', + description: 'Retrieve an existing subscription by ID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Subscription ID (e.g., sub_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/subscriptions/${params.id}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscription: data, + metadata: { + id: data.id, + status: data.status, + customer: data.customer, + }, + }, + } + }, + + outputs: { + subscription: { + type: 'json', + description: 'The retrieved subscription object', + }, + metadata: { + type: 'json', + description: 'Subscription metadata including ID, status, and customer', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_charges.ts b/apps/sim/tools/stripe/search_charges.ts new file mode 100644 index 0000000000..1e32e4c7c0 --- /dev/null +++ b/apps/sim/tools/stripe/search_charges.ts @@ -0,0 +1,69 @@ +import type { ChargeListResponse, SearchChargesParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchChargesTool: ToolConfig = { + id: 'stripe_search_charges', + name: 'Stripe Search Charges', + description: 'Search for charges using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Search query (e.g., \"status:'succeeded' AND currency:'usd'\")", + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/charges/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + charges: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + charges: { + type: 'json', + description: 'Array of matching Charge objects', + }, + metadata: { + type: 'json', + description: 'Search metadata including count and has_more', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_customers.ts b/apps/sim/tools/stripe/search_customers.ts new file mode 100644 index 0000000000..f5d730246e --- /dev/null +++ b/apps/sim/tools/stripe/search_customers.ts @@ -0,0 +1,69 @@ +import type { CustomerListResponse, SearchCustomersParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchCustomersTool: ToolConfig = { + id: 'stripe_search_customers', + name: 'Stripe Search Customers', + description: 'Search for customers using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query (e.g., "email:\'customer@example.com\'")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/customers/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + customers: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + customers: { + type: 'json', + description: 'Array of matching customer objects', + }, + metadata: { + type: 'json', + description: 'Search metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_invoices.ts b/apps/sim/tools/stripe/search_invoices.ts new file mode 100644 index 0000000000..2a20511ac3 --- /dev/null +++ b/apps/sim/tools/stripe/search_invoices.ts @@ -0,0 +1,69 @@ +import type { InvoiceListResponse, SearchInvoicesParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchInvoicesTool: ToolConfig = { + id: 'stripe_search_invoices', + name: 'Stripe Search Invoices', + description: 'Search for invoices using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query (e.g., "customer:\'cus_1234567890\'")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/invoices/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoices: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + invoices: { + type: 'json', + description: 'Array of matching invoice objects', + }, + metadata: { + type: 'json', + description: 'Search metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_payment_intents.ts b/apps/sim/tools/stripe/search_payment_intents.ts new file mode 100644 index 0000000000..9223d3dc9d --- /dev/null +++ b/apps/sim/tools/stripe/search_payment_intents.ts @@ -0,0 +1,72 @@ +import type { PaymentIntentListResponse, SearchPaymentIntentsParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchPaymentIntentsTool: ToolConfig< + SearchPaymentIntentsParams, + PaymentIntentListResponse +> = { + id: 'stripe_search_payment_intents', + name: 'Stripe Search Payment Intents', + description: 'Search for Payment Intents using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Search query (e.g., \"status:'succeeded' AND currency:'usd'\")", + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/payment_intents/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intents: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + payment_intents: { + type: 'json', + description: 'Array of matching Payment Intent objects', + }, + metadata: { + type: 'json', + description: 'Search metadata including count and has_more', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_prices.ts b/apps/sim/tools/stripe/search_prices.ts new file mode 100644 index 0000000000..b0b5ce553e --- /dev/null +++ b/apps/sim/tools/stripe/search_prices.ts @@ -0,0 +1,69 @@ +import type { PriceListResponse, SearchPricesParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchPricesTool: ToolConfig = { + id: 'stripe_search_prices', + name: 'Stripe Search Prices', + description: 'Search for prices using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Search query (e.g., \"active:'true' AND currency:'usd'\")", + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/prices/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + prices: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + prices: { + type: 'json', + description: 'Array of matching price objects', + }, + metadata: { + type: 'json', + description: 'Search metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_products.ts b/apps/sim/tools/stripe/search_products.ts new file mode 100644 index 0000000000..5ef20a9be6 --- /dev/null +++ b/apps/sim/tools/stripe/search_products.ts @@ -0,0 +1,69 @@ +import type { ProductListResponse, SearchProductsParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchProductsTool: ToolConfig = { + id: 'stripe_search_products', + name: 'Stripe Search Products', + description: 'Search for products using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search query (e.g., "name:\'shirt\'")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/products/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + products: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + products: { + type: 'json', + description: 'Array of matching product objects', + }, + metadata: { + type: 'json', + description: 'Search metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/search_subscriptions.ts b/apps/sim/tools/stripe/search_subscriptions.ts new file mode 100644 index 0000000000..afc74306e0 --- /dev/null +++ b/apps/sim/tools/stripe/search_subscriptions.ts @@ -0,0 +1,72 @@ +import type { SearchSubscriptionsParams, SubscriptionListResponse } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSearchSubscriptionsTool: ToolConfig< + SearchSubscriptionsParams, + SubscriptionListResponse +> = { + id: 'stripe_search_subscriptions', + name: 'Stripe Search Subscriptions', + description: 'Search for subscriptions using query syntax', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Search query (e.g., \"status:'active' AND customer:'cus_xxx'\")", + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to return (default 10, max 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.stripe.com/v1/subscriptions/search') + url.searchParams.append('query', params.query) + if (params.limit) url.searchParams.append('limit', params.limit.toString()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscriptions: data.data || [], + metadata: { + count: (data.data || []).length, + has_more: data.has_more || false, + }, + }, + } + }, + + outputs: { + subscriptions: { + type: 'json', + description: 'Array of matching subscription objects', + }, + metadata: { + type: 'json', + description: 'Search metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/send_invoice.ts b/apps/sim/tools/stripe/send_invoice.ts new file mode 100644 index 0000000000..3179d3c7f0 --- /dev/null +++ b/apps/sim/tools/stripe/send_invoice.ts @@ -0,0 +1,60 @@ +import type { InvoiceResponse, SendInvoiceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeSendInvoiceTool: ToolConfig = { + id: 'stripe_send_invoice', + name: 'Stripe Send Invoice', + description: 'Send an invoice to the customer', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}/send`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The sent invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/types.ts b/apps/sim/tools/stripe/types.ts new file mode 100644 index 0000000000..5a4a2d471f --- /dev/null +++ b/apps/sim/tools/stripe/types.ts @@ -0,0 +1,746 @@ +import type { ToolResponse } from '@/tools/types' + +export interface StripeAddress { + line1?: string + line2?: string + city?: string + state?: string + postal_code?: string + country?: string +} + +export interface StripeMetadata { + [key: string]: string +} + +// ============================================================================ +// Payment Intent Types +// ============================================================================ + +export interface PaymentIntentObject { + id: string + object: 'payment_intent' + amount: number + currency: string + status: string + customer?: string + payment_method?: string + description?: string + receipt_email?: string + metadata?: StripeMetadata + created: number + [key: string]: any +} + +export interface CreatePaymentIntentParams { + apiKey: string + amount: number + currency: string + customer?: string + payment_method?: string + description?: string + receipt_email?: string + metadata?: StripeMetadata + automatic_payment_methods?: { enabled: boolean } +} + +export interface RetrievePaymentIntentParams { + apiKey: string + id: string +} + +export interface UpdatePaymentIntentParams { + apiKey: string + id: string + amount?: number + currency?: string + customer?: string + description?: string + metadata?: StripeMetadata +} + +export interface ConfirmPaymentIntentParams { + apiKey: string + id: string + payment_method?: string +} + +export interface CapturePaymentIntentParams { + apiKey: string + id: string + amount_to_capture?: number +} + +export interface CancelPaymentIntentParams { + apiKey: string + id: string + cancellation_reason?: string +} + +export interface ListPaymentIntentsParams { + apiKey: string + limit?: number + customer?: string + created?: any +} + +export interface SearchPaymentIntentsParams { + apiKey: string + query: string + limit?: number +} + +export interface PaymentIntentResponse extends ToolResponse { + output: { + payment_intent: PaymentIntentObject + metadata: { + id: string + status: string + amount: number + currency: string + } + } +} + +export interface PaymentIntentListResponse extends ToolResponse { + output: { + payment_intents: PaymentIntentObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +// ============================================================================ +// Customer Types +// ============================================================================ + +export interface CustomerObject { + id: string + object: 'customer' + email?: string + name?: string + phone?: string + description?: string + address?: StripeAddress + metadata?: StripeMetadata + created: number + [key: string]: any +} + +export interface CreateCustomerParams { + apiKey: string + email?: string + name?: string + phone?: string + description?: string + address?: StripeAddress + metadata?: StripeMetadata + payment_method?: string +} + +export interface RetrieveCustomerParams { + apiKey: string + id: string +} + +export interface UpdateCustomerParams { + apiKey: string + id: string + email?: string + name?: string + phone?: string + description?: string + address?: StripeAddress + metadata?: StripeMetadata +} + +export interface DeleteCustomerParams { + apiKey: string + id: string +} + +export interface ListCustomersParams { + apiKey: string + limit?: number + email?: string + created?: any +} + +export interface SearchCustomersParams { + apiKey: string + query: string + limit?: number +} + +export interface CustomerResponse extends ToolResponse { + output: { + customer: CustomerObject + metadata: { + id: string + email?: string + name?: string + } + } +} + +export interface CustomerListResponse extends ToolResponse { + output: { + customers: CustomerObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +export interface CustomerDeleteResponse extends ToolResponse { + output: { + deleted: boolean + id: string + metadata: { + id: string + deleted: boolean + } + } +} + +// ============================================================================ +// Subscription Types +// ============================================================================ + +export interface SubscriptionObject { + id: string + object: 'subscription' + customer: string + status: string + items: { + data: Array<{ + id: string + price: { + id: string + [key: string]: any + } + [key: string]: any + }> + } + current_period_start: number + current_period_end: number + cancel_at_period_end: boolean + metadata?: StripeMetadata + created: number + [key: string]: any +} + +export interface CreateSubscriptionParams { + apiKey: string + customer: string + items: Array<{ price: string; quantity?: number }> + trial_period_days?: number + default_payment_method?: string + cancel_at_period_end?: boolean + metadata?: StripeMetadata +} + +export interface RetrieveSubscriptionParams { + apiKey: string + id: string +} + +export interface UpdateSubscriptionParams { + apiKey: string + id: string + items?: Array<{ price: string; quantity?: number }> + cancel_at_period_end?: boolean + metadata?: StripeMetadata +} + +export interface CancelSubscriptionParams { + apiKey: string + id: string + prorate?: boolean + invoice_now?: boolean +} + +export interface ResumeSubscriptionParams { + apiKey: string + id: string +} + +export interface ListSubscriptionsParams { + apiKey: string + limit?: number + customer?: string + status?: string + price?: string +} + +export interface SearchSubscriptionsParams { + apiKey: string + query: string + limit?: number +} + +export interface SubscriptionResponse extends ToolResponse { + output: { + subscription: SubscriptionObject + metadata: { + id: string + status: string + customer: string + } + } +} + +export interface SubscriptionListResponse extends ToolResponse { + output: { + subscriptions: SubscriptionObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +// ============================================================================ +// Invoice Types +// ============================================================================ + +export interface InvoiceObject { + id: string + object: 'invoice' + customer: string + amount_due: number + amount_paid: number + amount_remaining: number + currency: string + status: string + description?: string + metadata?: StripeMetadata + created: number + [key: string]: any +} + +export interface CreateInvoiceParams { + apiKey: string + customer: string + description?: string + metadata?: StripeMetadata + auto_advance?: boolean + collection_method?: 'charge_automatically' | 'send_invoice' +} + +export interface RetrieveInvoiceParams { + apiKey: string + id: string +} + +export interface UpdateInvoiceParams { + apiKey: string + id: string + description?: string + metadata?: StripeMetadata + auto_advance?: boolean +} + +export interface DeleteInvoiceParams { + apiKey: string + id: string +} + +export interface FinalizeInvoiceParams { + apiKey: string + id: string + auto_advance?: boolean +} + +export interface PayInvoiceParams { + apiKey: string + id: string + paid_out_of_band?: boolean +} + +export interface VoidInvoiceParams { + apiKey: string + id: string +} + +export interface SendInvoiceParams { + apiKey: string + id: string +} + +export interface ListInvoicesParams { + apiKey: string + limit?: number + customer?: string + status?: string +} + +export interface SearchInvoicesParams { + apiKey: string + query: string + limit?: number +} + +export interface InvoiceResponse extends ToolResponse { + output: { + invoice: InvoiceObject + metadata: { + id: string + status: string + amount_due: number + currency: string + } + } +} + +export interface InvoiceListResponse extends ToolResponse { + output: { + invoices: InvoiceObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +export interface InvoiceDeleteResponse extends ToolResponse { + output: { + deleted: boolean + id: string + metadata: { + id: string + deleted: boolean + } + } +} + +// ============================================================================ +// Charge Types +// ============================================================================ + +export interface ChargeObject { + id: string + object: 'charge' + amount: number + currency: string + status: string + customer?: string + description?: string + paid: boolean + refunded: boolean + metadata?: StripeMetadata + created: number + [key: string]: any +} + +export interface CreateChargeParams { + apiKey: string + amount: number + currency: string + customer?: string + source?: string + description?: string + metadata?: StripeMetadata + capture?: boolean +} + +export interface RetrieveChargeParams { + apiKey: string + id: string +} + +export interface UpdateChargeParams { + apiKey: string + id: string + description?: string + metadata?: StripeMetadata +} + +export interface CaptureChargeParams { + apiKey: string + id: string + amount?: number +} + +export interface ListChargesParams { + apiKey: string + limit?: number + customer?: string + created?: any +} + +export interface SearchChargesParams { + apiKey: string + query: string + limit?: number +} + +export interface ChargeResponse extends ToolResponse { + output: { + charge: ChargeObject + metadata: { + id: string + status: string + amount: number + currency: string + paid: boolean + } + } +} + +export interface ChargeListResponse extends ToolResponse { + output: { + charges: ChargeObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +// ============================================================================ +// Product Types +// ============================================================================ + +export interface ProductObject { + id: string + object: 'product' + name: string + description?: string + active: boolean + images?: string[] + metadata?: StripeMetadata + created: number + [key: string]: any +} + +export interface CreateProductParams { + apiKey: string + name: string + description?: string + active?: boolean + images?: string[] + metadata?: StripeMetadata +} + +export interface RetrieveProductParams { + apiKey: string + id: string +} + +export interface UpdateProductParams { + apiKey: string + id: string + name?: string + description?: string + active?: boolean + images?: string[] + metadata?: StripeMetadata +} + +export interface DeleteProductParams { + apiKey: string + id: string +} + +export interface ListProductsParams { + apiKey: string + limit?: number + active?: boolean +} + +export interface SearchProductsParams { + apiKey: string + query: string + limit?: number +} + +export interface ProductResponse extends ToolResponse { + output: { + product: ProductObject + metadata: { + id: string + name: string + active: boolean + } + } +} + +export interface ProductListResponse extends ToolResponse { + output: { + products: ProductObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +export interface ProductDeleteResponse extends ToolResponse { + output: { + deleted: boolean + id: string + metadata: { + id: string + deleted: boolean + } + } +} + +// ============================================================================ +// Price Types +// ============================================================================ + +export interface PriceObject { + id: string + object: 'price' + product: string + unit_amount?: number + currency: string + recurring?: { + interval: string + interval_count: number + } + metadata?: StripeMetadata + active: boolean + created: number + [key: string]: any +} + +export interface CreatePriceParams { + apiKey: string + product: string + currency: string + unit_amount?: number + recurring?: { + interval: 'day' | 'week' | 'month' | 'year' + interval_count?: number + } + metadata?: StripeMetadata + billing_scheme?: 'per_unit' | 'tiered' +} + +export interface RetrievePriceParams { + apiKey: string + id: string +} + +export interface UpdatePriceParams { + apiKey: string + id: string + active?: boolean + metadata?: StripeMetadata +} + +export interface ListPricesParams { + apiKey: string + limit?: number + product?: string + active?: boolean +} + +export interface SearchPricesParams { + apiKey: string + query: string + limit?: number +} + +export interface PriceResponse extends ToolResponse { + output: { + price: PriceObject + metadata: { + id: string + product: string + unit_amount?: number + currency: string + } + } +} + +export interface PriceListResponse extends ToolResponse { + output: { + prices: PriceObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +// ============================================================================ +// Event Types +// ============================================================================ + +export interface EventObject { + id: string + object: 'event' + type: string + data: { + object: any + } + created: number + livemode: boolean + api_version?: string + request?: { + id: string + idempotency_key?: string + } + [key: string]: any +} + +export interface RetrieveEventParams { + apiKey: string + id: string +} + +export interface ListEventsParams { + apiKey: string + limit?: number + type?: string + created?: any +} + +export interface EventResponse extends ToolResponse { + output: { + event: EventObject + metadata: { + id: string + type: string + created: number + } + } +} + +export interface EventListResponse extends ToolResponse { + output: { + events: EventObject[] + metadata: { + count: number + has_more: boolean + } + } +} + +export type StripeResponse = + | PaymentIntentResponse + | PaymentIntentListResponse + | CustomerResponse + | CustomerListResponse + | CustomerDeleteResponse + | SubscriptionResponse + | SubscriptionListResponse + | InvoiceResponse + | InvoiceListResponse + | InvoiceDeleteResponse + | ChargeResponse + | ChargeListResponse + | ProductResponse + | ProductListResponse + | ProductDeleteResponse + | PriceResponse + | PriceListResponse + | EventResponse + | EventListResponse diff --git a/apps/sim/tools/stripe/update_charge.ts b/apps/sim/tools/stripe/update_charge.ts new file mode 100644 index 0000000000..919384622c --- /dev/null +++ b/apps/sim/tools/stripe/update_charge.ts @@ -0,0 +1,86 @@ +import type { ChargeResponse, UpdateChargeParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdateChargeTool: ToolConfig = { + id: 'stripe_update_charge', + name: 'Stripe Update Charge', + description: 'Update an existing charge', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Charge ID (e.g., ch_1234567890)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated description', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated metadata', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/charges/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.description) formData.append('description', params.description) + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + charge: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + paid: data.paid, + }, + }, + } + }, + + outputs: { + charge: { + type: 'json', + description: 'The updated Charge object', + }, + metadata: { + type: 'json', + description: 'Charge metadata including ID, status, amount, currency, and paid status', + }, + }, +} diff --git a/apps/sim/tools/stripe/update_customer.ts b/apps/sim/tools/stripe/update_customer.ts new file mode 100644 index 0000000000..576d5b32ef --- /dev/null +++ b/apps/sim/tools/stripe/update_customer.ts @@ -0,0 +1,117 @@ +import type { CustomerResponse, UpdateCustomerParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdateCustomerTool: ToolConfig = { + id: 'stripe_update_customer', + name: 'Stripe Update Customer', + description: 'Update an existing customer', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Customer ID (e.g., cus_1234567890)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated email address', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated name', + }, + phone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated phone number', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated description', + }, + address: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated address object', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated metadata', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/customers/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.email) formData.append('email', params.email) + if (params.name) formData.append('name', params.name) + if (params.phone) formData.append('phone', params.phone) + if (params.description) formData.append('description', params.description) + + if (params.address) { + Object.entries(params.address).forEach(([key, value]) => { + if (value) formData.append(`address[${key}]`, String(value)) + }) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + customer: data, + metadata: { + id: data.id, + email: data.email, + name: data.name, + }, + }, + } + }, + + outputs: { + customer: { + type: 'json', + description: 'The updated customer object', + }, + metadata: { + type: 'json', + description: 'Customer metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/update_invoice.ts b/apps/sim/tools/stripe/update_invoice.ts new file mode 100644 index 0000000000..fe643201fc --- /dev/null +++ b/apps/sim/tools/stripe/update_invoice.ts @@ -0,0 +1,94 @@ +import type { InvoiceResponse, UpdateInvoiceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdateInvoiceTool: ToolConfig = { + id: 'stripe_update_invoice', + name: 'Stripe Update Invoice', + description: 'Update an existing invoice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the invoice', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Set of key-value pairs', + }, + auto_advance: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Auto-finalize the invoice', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.description) formData.append('description', params.description) + if (params.auto_advance !== undefined) { + formData.append('auto_advance', String(params.auto_advance)) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The updated invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/update_payment_intent.ts b/apps/sim/tools/stripe/update_payment_intent.ts new file mode 100644 index 0000000000..f9035a0a76 --- /dev/null +++ b/apps/sim/tools/stripe/update_payment_intent.ts @@ -0,0 +1,109 @@ +import type { PaymentIntentResponse, UpdatePaymentIntentParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdatePaymentIntentTool: ToolConfig< + UpdatePaymentIntentParams, + PaymentIntentResponse +> = { + id: 'stripe_update_payment_intent', + name: 'Stripe Update Payment Intent', + description: 'Update an existing Payment Intent', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Payment Intent ID (e.g., pi_1234567890)', + }, + amount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Updated amount in cents', + }, + currency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Three-letter ISO currency code', + }, + customer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Customer ID', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated description', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated metadata', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/payment_intents/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.amount) formData.append('amount', Number(params.amount).toString()) + if (params.currency) formData.append('currency', params.currency) + if (params.customer) formData.append('customer', params.customer) + if (params.description) formData.append('description', params.description) + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + payment_intent: data, + metadata: { + id: data.id, + status: data.status, + amount: data.amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + payment_intent: { + type: 'json', + description: 'The updated Payment Intent object', + }, + metadata: { + type: 'json', + description: 'Payment Intent metadata including ID, status, amount, and currency', + }, + }, +} diff --git a/apps/sim/tools/stripe/update_price.ts b/apps/sim/tools/stripe/update_price.ts new file mode 100644 index 0000000000..e0dff2a2d6 --- /dev/null +++ b/apps/sim/tools/stripe/update_price.ts @@ -0,0 +1,85 @@ +import type { PriceResponse, UpdatePriceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdatePriceTool: ToolConfig = { + id: 'stripe_update_price', + name: 'Stripe Update Price', + description: 'Update an existing price', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Price ID (e.g., price_1234567890)', + }, + active: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the price is active', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated metadata', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/prices/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.active !== undefined) formData.append('active', String(params.active)) + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + price: data, + metadata: { + id: data.id, + product: data.product, + unit_amount: data.unit_amount, + currency: data.currency, + }, + }, + } + }, + + outputs: { + price: { + type: 'json', + description: 'The updated price object', + }, + metadata: { + type: 'json', + description: 'Price metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/update_product.ts b/apps/sim/tools/stripe/update_product.ts new file mode 100644 index 0000000000..3eafb4b1d7 --- /dev/null +++ b/apps/sim/tools/stripe/update_product.ts @@ -0,0 +1,110 @@ +import type { ProductResponse, UpdateProductParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdateProductTool: ToolConfig = { + id: 'stripe_update_product', + name: 'Stripe Update Product', + description: 'Update an existing product', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Product ID (e.g., prod_1234567890)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated product name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated product description', + }, + active: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Updated active status', + }, + images: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated array of image URLs', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated metadata', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/products/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.name) formData.append('name', params.name) + if (params.description) formData.append('description', params.description) + if (params.active !== undefined) formData.append('active', String(params.active)) + + if (params.images) { + params.images.forEach((image: string, index: number) => { + formData.append(`images[${index}]`, image) + }) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + product: data, + metadata: { + id: data.id, + name: data.name, + active: data.active, + }, + }, + } + }, + + outputs: { + product: { + type: 'json', + description: 'The updated product object', + }, + metadata: { + type: 'json', + description: 'Product metadata', + }, + }, +} diff --git a/apps/sim/tools/stripe/update_subscription.ts b/apps/sim/tools/stripe/update_subscription.ts new file mode 100644 index 0000000000..0ded075d97 --- /dev/null +++ b/apps/sim/tools/stripe/update_subscription.ts @@ -0,0 +1,104 @@ +import type { SubscriptionResponse, UpdateSubscriptionParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeUpdateSubscriptionTool: ToolConfig< + UpdateSubscriptionParams, + SubscriptionResponse +> = { + id: 'stripe_update_subscription', + name: 'Stripe Update Subscription', + description: 'Update an existing subscription', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Subscription ID (e.g., sub_1234567890)', + }, + items: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated array of items with price IDs', + }, + cancel_at_period_end: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Cancel subscription at period end', + }, + metadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Updated metadata', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/subscriptions/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + body: (params) => { + const formData = new URLSearchParams() + + if (params.items && Array.isArray(params.items)) { + params.items.forEach((item, index) => { + formData.append(`items[${index}][price]`, item.price) + if (item.quantity) { + formData.append(`items[${index}][quantity]`, String(item.quantity)) + } + }) + } + + if (params.cancel_at_period_end !== undefined) { + formData.append('cancel_at_period_end', String(params.cancel_at_period_end)) + } + + if (params.metadata) { + Object.entries(params.metadata).forEach(([key, value]) => { + formData.append(`metadata[${key}]`, String(value)) + }) + } + + return { body: formData.toString() } + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + subscription: data, + metadata: { + id: data.id, + status: data.status, + customer: data.customer, + }, + }, + } + }, + + outputs: { + subscription: { + type: 'json', + description: 'The updated subscription object', + }, + metadata: { + type: 'json', + description: 'Subscription metadata including ID, status, and customer', + }, + }, +} diff --git a/apps/sim/tools/stripe/void_invoice.ts b/apps/sim/tools/stripe/void_invoice.ts new file mode 100644 index 0000000000..b7064cd326 --- /dev/null +++ b/apps/sim/tools/stripe/void_invoice.ts @@ -0,0 +1,60 @@ +import type { InvoiceResponse, VoidInvoiceParams } from '@/tools/stripe/types' +import type { ToolConfig } from '@/tools/types' + +export const stripeVoidInvoiceTool: ToolConfig = { + id: 'stripe_void_invoice', + name: 'Stripe Void Invoice', + description: 'Void an invoice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Stripe API key (secret key)', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Invoice ID (e.g., in_1234567890)', + }, + }, + + request: { + url: (params) => `https://api.stripe.com/v1/invoices/${params.id}/void`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + invoice: data, + metadata: { + id: data.id, + status: data.status, + amount_due: data.amount_due, + currency: data.currency, + }, + }, + } + }, + + outputs: { + invoice: { + type: 'json', + description: 'The voided invoice object', + }, + metadata: { + type: 'json', + description: 'Invoice metadata', + }, + }, +} diff --git a/apps/sim/tools/supabase/count.ts b/apps/sim/tools/supabase/count.ts new file mode 100644 index 0000000000..800c1da4e5 --- /dev/null +++ b/apps/sim/tools/supabase/count.ts @@ -0,0 +1,96 @@ +import type { SupabaseCountParams, SupabaseCountResponse } from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const countTool: ToolConfig = { + id: 'supabase_count', + name: 'Supabase Count', + description: 'Count rows in a Supabase table', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the Supabase table to count rows from', + }, + filter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'PostgREST filter (e.g., "status=eq.active")', + }, + countType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Count type: exact, planned, or estimated (default: exact)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*` + + // Add filters if provided + if (params.filter?.trim()) { + url += `&${params.filter.trim()}` + } + + return url + }, + method: 'HEAD', + headers: (params) => { + const countType = params.countType || 'exact' + return { + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + Prefer: `count=${countType}`, + } + }, + }, + + transformResponse: async (response: Response) => { + // Extract count from Content-Range header + const contentRange = response.headers.get('content-range') + + if (!contentRange) { + throw new Error('No content-range header found in response') + } + + // Parse the content-range header (format: "0-9/100" or "*/100") + const countMatch = contentRange.match(/\/(\d+)$/) + + if (!countMatch) { + throw new Error(`Invalid content-range header format: ${contentRange}`) + } + + const count = Number.parseInt(countMatch[1], 10) + + return { + success: true, + output: { + message: `Successfully counted ${count} row${count === 1 ? '' : 's'}`, + count: count, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + count: { type: 'number', description: 'Number of rows matching the filter' }, + }, +} diff --git a/apps/sim/tools/supabase/index.ts b/apps/sim/tools/supabase/index.ts index 2a266f012b..c7be85af4e 100644 --- a/apps/sim/tools/supabase/index.ts +++ b/apps/sim/tools/supabase/index.ts @@ -1,7 +1,21 @@ +import { countTool } from '@/tools/supabase/count' import { deleteTool } from '@/tools/supabase/delete' import { getRowTool } from '@/tools/supabase/get_row' import { insertTool } from '@/tools/supabase/insert' import { queryTool } from '@/tools/supabase/query' +import { rpcTool } from '@/tools/supabase/rpc' +import { storageCopyTool } from '@/tools/supabase/storage_copy' +import { storageCreateBucketTool } from '@/tools/supabase/storage_create_bucket' +import { storageCreateSignedUrlTool } from '@/tools/supabase/storage_create_signed_url' +import { storageDeleteTool } from '@/tools/supabase/storage_delete' +import { storageDeleteBucketTool } from '@/tools/supabase/storage_delete_bucket' +import { storageDownloadTool } from '@/tools/supabase/storage_download' +import { storageGetPublicUrlTool } from '@/tools/supabase/storage_get_public_url' +import { storageListTool } from '@/tools/supabase/storage_list' +import { storageListBucketsTool } from '@/tools/supabase/storage_list_buckets' +import { storageMoveTool } from '@/tools/supabase/storage_move' +import { storageUploadTool } from '@/tools/supabase/storage_upload' +import { textSearchTool } from '@/tools/supabase/text_search' import { updateTool } from '@/tools/supabase/update' import { upsertTool } from '@/tools/supabase/upsert' import { vectorSearchTool } from '@/tools/supabase/vector_search' @@ -13,3 +27,17 @@ export const supabaseUpdateTool = updateTool export const supabaseDeleteTool = deleteTool export const supabaseUpsertTool = upsertTool export const supabaseVectorSearchTool = vectorSearchTool +export const supabaseRpcTool = rpcTool +export const supabaseTextSearchTool = textSearchTool +export const supabaseCountTool = countTool +export const supabaseStorageUploadTool = storageUploadTool +export const supabaseStorageDownloadTool = storageDownloadTool +export const supabaseStorageListTool = storageListTool +export const supabaseStorageDeleteTool = storageDeleteTool +export const supabaseStorageMoveTool = storageMoveTool +export const supabaseStorageCopyTool = storageCopyTool +export const supabaseStorageCreateBucketTool = storageCreateBucketTool +export const supabaseStorageListBucketsTool = storageListBucketsTool +export const supabaseStorageDeleteBucketTool = storageDeleteBucketTool +export const supabaseStorageGetPublicUrlTool = storageGetPublicUrlTool +export const supabaseStorageCreateSignedUrlTool = storageCreateSignedUrlTool diff --git a/apps/sim/tools/supabase/query.ts b/apps/sim/tools/supabase/query.ts index 978e498245..3a56e99505 100644 --- a/apps/sim/tools/supabase/query.ts +++ b/apps/sim/tools/supabase/query.ts @@ -78,7 +78,7 @@ export const queryTool: ToolConfig = // Add limit if provided if (params.limit) { - url += `&limit=${params.limit}` + url += `&limit=${Number(params.limit)}` } return url diff --git a/apps/sim/tools/supabase/rpc.ts b/apps/sim/tools/supabase/rpc.ts new file mode 100644 index 0000000000..a57d36a158 --- /dev/null +++ b/apps/sim/tools/supabase/rpc.ts @@ -0,0 +1,74 @@ +import type { SupabaseRpcParams, SupabaseRpcResponse } from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const rpcTool: ToolConfig = { + id: 'supabase_rpc', + name: 'Supabase RPC', + description: 'Call a PostgreSQL function in Supabase', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + functionName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the PostgreSQL function to call', + }, + params: { + type: 'object', + required: false, + visibility: 'user-or-llm', + description: 'Parameters to pass to the function as a JSON object', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/rest/v1/rpc/${params.functionName}` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + return params.params || {} + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase RPC response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully executed PostgreSQL function', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { type: 'json', description: 'Result returned from the function' }, + }, +} diff --git a/apps/sim/tools/supabase/storage_copy.ts b/apps/sim/tools/supabase/storage_copy.ts new file mode 100644 index 0000000000..98a03b40f4 --- /dev/null +++ b/apps/sim/tools/supabase/storage_copy.ts @@ -0,0 +1,87 @@ +import type { SupabaseStorageCopyParams, SupabaseStorageCopyResponse } from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageCopyTool: ToolConfig = { + id: 'supabase_storage_copy', + name: 'Supabase Storage Copy', + description: 'Copy a file within a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + fromPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the source file (e.g., "folder/source.jpg")', + }, + toPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path for the copied file (e.g., "folder/copy.jpg")', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/copy` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + return { + bucketId: params.bucket, + sourceKey: params.fromPath, + destinationKey: params.toPath, + } + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage copy response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully copied file in storage', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Copy operation result', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_create_bucket.ts b/apps/sim/tools/supabase/storage_create_bucket.ts new file mode 100644 index 0000000000..c394ece24c --- /dev/null +++ b/apps/sim/tools/supabase/storage_create_bucket.ts @@ -0,0 +1,109 @@ +import type { + SupabaseStorageCreateBucketParams, + SupabaseStorageCreateBucketResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageCreateBucketTool: ToolConfig< + SupabaseStorageCreateBucketParams, + SupabaseStorageCreateBucketResponse +> = { + id: 'supabase_storage_create_bucket', + name: 'Supabase Storage Create Bucket', + description: 'Create a new storage bucket in Supabase', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the bucket to create', + }, + isPublic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the bucket should be publicly accessible (default: false)', + }, + fileSizeLimit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum file size in bytes (optional)', + }, + allowedMimeTypes: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Array of allowed MIME types (e.g., ["image/png", "image/jpeg"])', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/bucket` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const payload: any = { + id: params.bucket, + name: params.bucket, + public: params.isPublic || false, + } + + if (params.fileSizeLimit) { + payload.file_size_limit = Number(params.fileSizeLimit) + } + + if (params.allowedMimeTypes && params.allowedMimeTypes.length > 0) { + payload.allowed_mime_types = params.allowedMimeTypes + } + + return payload + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage create bucket response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully created storage bucket', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Created bucket information', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_create_signed_url.ts b/apps/sim/tools/supabase/storage_create_signed_url.ts new file mode 100644 index 0000000000..49543a7d96 --- /dev/null +++ b/apps/sim/tools/supabase/storage_create_signed_url.ts @@ -0,0 +1,103 @@ +import type { + SupabaseStorageCreateSignedUrlParams, + SupabaseStorageCreateSignedUrlResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageCreateSignedUrlTool: ToolConfig< + SupabaseStorageCreateSignedUrlParams, + SupabaseStorageCreateSignedUrlResponse +> = { + id: 'supabase_storage_create_signed_url', + name: 'Supabase Storage Create Signed URL', + description: 'Create a temporary signed URL for a file in a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path to the file (e.g., "folder/file.jpg")', + }, + expiresIn: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Number of seconds until the URL expires (e.g., 3600 for 1 hour)', + }, + download: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'If true, forces download instead of inline display (default: false)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/sign/${params.bucket}/${params.path}` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const payload: any = { + expiresIn: Number(params.expiresIn), + } + + if (params.download !== undefined) { + payload.download = params.download + } + + return payload + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage create signed URL response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully created signed URL', + signedUrl: data.signedURL || data.signedUrl, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + signedUrl: { + type: 'string', + description: 'The temporary signed URL to access the file', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_delete.ts b/apps/sim/tools/supabase/storage_delete.ts new file mode 100644 index 0000000000..d6ebaae02d --- /dev/null +++ b/apps/sim/tools/supabase/storage_delete.ts @@ -0,0 +1,85 @@ +import type { + SupabaseStorageDeleteParams, + SupabaseStorageDeleteResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageDeleteTool: ToolConfig< + SupabaseStorageDeleteParams, + SupabaseStorageDeleteResponse +> = { + id: 'supabase_storage_delete', + name: 'Supabase Storage Delete', + description: 'Delete files from a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + paths: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Array of file paths to delete (e.g., ["folder/file1.jpg", "folder/file2.jpg"])', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/${params.bucket}` + }, + method: 'DELETE', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + return { + prefixes: params.paths, + } + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage delete response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully deleted files from storage', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'array', + description: 'Array of deleted file objects', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_delete_bucket.ts b/apps/sim/tools/supabase/storage_delete_bucket.ts new file mode 100644 index 0000000000..251e510e81 --- /dev/null +++ b/apps/sim/tools/supabase/storage_delete_bucket.ts @@ -0,0 +1,73 @@ +import type { + SupabaseStorageDeleteBucketParams, + SupabaseStorageDeleteBucketResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageDeleteBucketTool: ToolConfig< + SupabaseStorageDeleteBucketParams, + SupabaseStorageDeleteBucketResponse +> = { + id: 'supabase_storage_delete_bucket', + name: 'Supabase Storage Delete Bucket', + description: 'Delete a storage bucket in Supabase', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the bucket to delete', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/bucket/${params.bucket}` + }, + method: 'DELETE', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage delete bucket response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully deleted storage bucket', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Delete operation result', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_download.ts b/apps/sim/tools/supabase/storage_download.ts new file mode 100644 index 0000000000..38e9bc6723 --- /dev/null +++ b/apps/sim/tools/supabase/storage_download.ts @@ -0,0 +1,105 @@ +import type { + SupabaseStorageDownloadParams, + SupabaseStorageDownloadResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageDownloadTool: ToolConfig< + SupabaseStorageDownloadParams, + SupabaseStorageDownloadResponse +> = { + id: 'supabase_storage_download', + name: 'Supabase Storage Download', + description: 'Download a file from a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path to the file to download (e.g., "folder/file.jpg")', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/${params.bucket}/${params.path}` + }, + method: 'GET', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + // Get the content type + const contentType = response.headers.get('content-type') || 'application/octet-stream' + + // Check if it's a text-based file + const isText = + contentType.startsWith('text/') || + contentType.includes('json') || + contentType.includes('xml') || + contentType.includes('javascript') || + contentType.includes('html') + + let fileContent: string + if (isText) { + // Return text content as-is + fileContent = await response.text() + } else { + // Return binary content as base64 + const buffer = await response.arrayBuffer() + const bytes = new Uint8Array(buffer) + let binary = '' + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]) + } + fileContent = btoa(binary) + } + + return { + success: true, + output: { + message: 'Successfully downloaded file from storage', + fileContent: fileContent, + contentType: contentType, + isBase64: !isText, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + fileContent: { + type: 'string', + description: 'File content (base64 encoded if binary, plain text otherwise)', + }, + contentType: { type: 'string', description: 'MIME type of the file' }, + isBase64: { + type: 'boolean', + description: 'Whether the file content is base64 encoded', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_get_public_url.ts b/apps/sim/tools/supabase/storage_get_public_url.ts new file mode 100644 index 0000000000..16426f0910 --- /dev/null +++ b/apps/sim/tools/supabase/storage_get_public_url.ts @@ -0,0 +1,91 @@ +import type { + SupabaseStorageGetPublicUrlParams, + SupabaseStorageGetPublicUrlResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageGetPublicUrlTool: ToolConfig< + SupabaseStorageGetPublicUrlParams, + SupabaseStorageGetPublicUrlResponse +> = { + id: 'supabase_storage_get_public_url', + name: 'Supabase Storage Get Public URL', + description: 'Get the public URL for a file in a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path to the file (e.g., "folder/file.jpg")', + }, + download: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'If true, forces download instead of inline display (default: false)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + // For public URL, we don't actually need to make a request + // We can construct it directly + let url = `https://${params.projectId}.supabase.co/storage/v1/object/public/${params.bucket}/${params.path}` + + if (params.download) { + url += '?download=true' + } + + // Return a dummy URL that won't be called + return url + }, + method: 'GET', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + // The URL is already constructed in the request.url + // We just need to return it + const publicUrl = response.url + + return { + success: true, + output: { + message: 'Successfully generated public URL', + publicUrl: publicUrl, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + publicUrl: { + type: 'string', + description: 'The public URL to access the file', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_list.ts b/apps/sim/tools/supabase/storage_list.ts new file mode 100644 index 0000000000..a49638434a --- /dev/null +++ b/apps/sim/tools/supabase/storage_list.ts @@ -0,0 +1,126 @@ +import type { SupabaseStorageListParams, SupabaseStorageListResponse } from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageListTool: ToolConfig = { + id: 'supabase_storage_list', + name: 'Supabase Storage List', + description: 'List files in a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + path: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The folder path to list files from (default: root)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of files to return (default: 100)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of files to skip (for pagination)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Column to sort by: name, created_at, updated_at (default: name)', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc or desc (default: asc)', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter files by name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/list/${params.bucket}` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const payload: any = { + prefix: params.path || '', + limit: params.limit ? Number(params.limit) : 100, + offset: params.offset ? Number(params.offset) : 0, + } + + if (params.sortBy) { + payload.sortBy = { + column: params.sortBy, + order: params.sortOrder || 'asc', + } + } + + if (params.search) { + payload.search = params.search + } + + return payload + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage list response: ${parseError}`) + } + + const fileCount = Array.isArray(data) ? data.length : 0 + + return { + success: true, + output: { + message: `Successfully listed ${fileCount} file${fileCount === 1 ? '' : 's'}`, + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'array', + description: 'Array of file objects with metadata', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_list_buckets.ts b/apps/sim/tools/supabase/storage_list_buckets.ts new file mode 100644 index 0000000000..3bb27879ca --- /dev/null +++ b/apps/sim/tools/supabase/storage_list_buckets.ts @@ -0,0 +1,69 @@ +import type { + SupabaseStorageListBucketsParams, + SupabaseStorageListBucketsResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageListBucketsTool: ToolConfig< + SupabaseStorageListBucketsParams, + SupabaseStorageListBucketsResponse +> = { + id: 'supabase_storage_list_buckets', + name: 'Supabase Storage List Buckets', + description: 'List all storage buckets in Supabase', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/bucket` + }, + method: 'GET', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage list buckets response: ${parseError}`) + } + + const bucketCount = Array.isArray(data) ? data.length : 0 + + return { + success: true, + output: { + message: `Successfully listed ${bucketCount} bucket${bucketCount === 1 ? '' : 's'}`, + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'array', + description: 'Array of bucket objects', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_move.ts b/apps/sim/tools/supabase/storage_move.ts new file mode 100644 index 0000000000..b86c89dab9 --- /dev/null +++ b/apps/sim/tools/supabase/storage_move.ts @@ -0,0 +1,87 @@ +import type { SupabaseStorageMoveParams, SupabaseStorageMoveResponse } from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageMoveTool: ToolConfig = { + id: 'supabase_storage_move', + name: 'Supabase Storage Move', + description: 'Move a file within a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + fromPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The current path of the file (e.g., "folder/old.jpg")', + }, + toPath: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The new path for the file (e.g., "newfolder/new.jpg")', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/move` + }, + method: 'POST', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + return { + bucketId: params.bucket, + sourceKey: params.fromPath, + destinationKey: params.toPath, + } + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage move response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully moved file in storage', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Move operation result', + }, + }, +} diff --git a/apps/sim/tools/supabase/storage_upload.ts b/apps/sim/tools/supabase/storage_upload.ts new file mode 100644 index 0000000000..43c5986f43 --- /dev/null +++ b/apps/sim/tools/supabase/storage_upload.ts @@ -0,0 +1,116 @@ +import type { + SupabaseStorageUploadParams, + SupabaseStorageUploadResponse, +} from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const storageUploadTool: ToolConfig< + SupabaseStorageUploadParams, + SupabaseStorageUploadResponse +> = { + id: 'supabase_storage_upload', + name: 'Supabase Storage Upload', + description: 'Upload a file to a Supabase storage bucket', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + bucket: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the storage bucket', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path where the file will be stored (e.g., "folder/file.jpg")', + }, + fileContent: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The file content (base64 encoded for binary files, or plain text)', + }, + contentType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'MIME type of the file (e.g., "image/jpeg", "text/plain")', + }, + upsert: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'If true, overwrites existing file (default: false)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + return `https://${params.projectId}.supabase.co/storage/v1/object/${params.bucket}/${params.path}` + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + } + + if (params.contentType) { + headers['Content-Type'] = params.contentType + } + + if (params.upsert) { + headers['x-upsert'] = 'true' + } + + return headers + }, + body: (params) => { + // Return the file content wrapped in an object + // The actual upload will need to handle this appropriately + return { + content: params.fileContent, + } + }, + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase storage upload response: ${parseError}`) + } + + return { + success: true, + output: { + message: 'Successfully uploaded file to storage', + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { + type: 'object', + description: 'Upload result including file path and metadata', + }, + }, +} diff --git a/apps/sim/tools/supabase/text_search.ts b/apps/sim/tools/supabase/text_search.ts new file mode 100644 index 0000000000..2e47908b1a --- /dev/null +++ b/apps/sim/tools/supabase/text_search.ts @@ -0,0 +1,121 @@ +import type { SupabaseTextSearchParams, SupabaseTextSearchResponse } from '@/tools/supabase/types' +import type { ToolConfig } from '@/tools/types' + +export const textSearchTool: ToolConfig = { + id: 'supabase_text_search', + name: 'Supabase Text Search', + description: 'Perform full-text search on a Supabase table', + version: '1.0', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the Supabase table to search', + }, + column: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The column to search in', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The search query', + }, + searchType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search type: plain, phrase, or websearch (default: websearch)', + }, + language: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Language for text search configuration (default: english)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of rows to return', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Supabase service role secret key', + }, + }, + + request: { + url: (params) => { + const searchType = params.searchType || 'websearch' + const language = params.language || 'english' + + // Build the text search filter + let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*` + + // Add text search filter using PostgREST syntax + url += `&${params.column}=${searchType}fts(${language}).${encodeURIComponent(params.query)}` + + // Add limit if provided + if (params.limit) { + url += `&limit=${Number(params.limit)}` + } + + return url + }, + method: 'GET', + headers: (params) => ({ + apikey: params.apiKey, + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + let data + try { + data = await response.json() + } catch (parseError) { + throw new Error(`Failed to parse Supabase text search response: ${parseError}`) + } + + const rowCount = Array.isArray(data) ? data.length : 0 + + if (rowCount === 0) { + return { + success: true, + output: { + message: 'No results found matching the search query', + results: data, + }, + error: undefined, + } + } + + return { + success: true, + output: { + message: `Successfully found ${rowCount} result${rowCount === 1 ? '' : 's'}`, + results: data, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + results: { type: 'array', description: 'Array of records matching the search query' }, + }, +} diff --git a/apps/sim/tools/supabase/types.ts b/apps/sim/tools/supabase/types.ts index 660c2ae14d..8ceaf2d321 100644 --- a/apps/sim/tools/supabase/types.ts +++ b/apps/sim/tools/supabase/types.ts @@ -77,3 +77,186 @@ export interface SupabaseUpsertResponse extends SupabaseBaseResponse {} export interface SupabaseVectorSearchResponse extends SupabaseBaseResponse {} export interface SupabaseResponse extends SupabaseBaseResponse {} + +// RPC types +export interface SupabaseRpcParams { + apiKey: string + projectId: string + functionName: string + params?: any +} + +export interface SupabaseRpcResponse extends SupabaseBaseResponse {} + +// Text Search types +export interface SupabaseTextSearchParams { + apiKey: string + projectId: string + table: string + column: string + query: string + searchType?: string + language?: string + limit?: number +} + +export interface SupabaseTextSearchResponse extends SupabaseBaseResponse {} + +// Count types +export interface SupabaseCountParams { + apiKey: string + projectId: string + table: string + filter?: string + countType?: string +} + +export interface SupabaseCountResponse extends ToolResponse { + output: { + message: string + count: number + } + error?: string +} + +// Storage Upload types +export interface SupabaseStorageUploadParams { + apiKey: string + projectId: string + bucket: string + path: string + fileContent: string + contentType?: string + upsert?: boolean +} + +export interface SupabaseStorageUploadResponse extends SupabaseBaseResponse {} + +// Storage Download types +export interface SupabaseStorageDownloadParams { + apiKey: string + projectId: string + bucket: string + path: string +} + +export interface SupabaseStorageDownloadResponse extends ToolResponse { + output: { + message: string + fileContent: string + contentType: string + isBase64: boolean + } + error?: string +} + +// Storage List types +export interface SupabaseStorageListParams { + apiKey: string + projectId: string + bucket: string + path?: string + limit?: number + offset?: number + sortBy?: string + sortOrder?: string + search?: string +} + +export interface SupabaseStorageListResponse extends SupabaseBaseResponse {} + +// Storage Delete types +export interface SupabaseStorageDeleteParams { + apiKey: string + projectId: string + bucket: string + paths: string[] +} + +export interface SupabaseStorageDeleteResponse extends SupabaseBaseResponse {} + +// Storage Move types +export interface SupabaseStorageMoveParams { + apiKey: string + projectId: string + bucket: string + fromPath: string + toPath: string +} + +export interface SupabaseStorageMoveResponse extends SupabaseBaseResponse {} + +// Storage Copy types +export interface SupabaseStorageCopyParams { + apiKey: string + projectId: string + bucket: string + fromPath: string + toPath: string +} + +export interface SupabaseStorageCopyResponse extends SupabaseBaseResponse {} + +// Storage Create Bucket types +export interface SupabaseStorageCreateBucketParams { + apiKey: string + projectId: string + bucket: string + isPublic?: boolean + fileSizeLimit?: number + allowedMimeTypes?: string[] +} + +export interface SupabaseStorageCreateBucketResponse extends SupabaseBaseResponse {} + +// Storage List Buckets types +export interface SupabaseStorageListBucketsParams { + apiKey: string + projectId: string +} + +export interface SupabaseStorageListBucketsResponse extends SupabaseBaseResponse {} + +// Storage Delete Bucket types +export interface SupabaseStorageDeleteBucketParams { + apiKey: string + projectId: string + bucket: string +} + +export interface SupabaseStorageDeleteBucketResponse extends SupabaseBaseResponse {} + +// Storage Get Public URL types +export interface SupabaseStorageGetPublicUrlParams { + apiKey: string + projectId: string + bucket: string + path: string + download?: boolean +} + +export interface SupabaseStorageGetPublicUrlResponse extends ToolResponse { + output: { + message: string + publicUrl: string + } + error?: string +} + +// Storage Create Signed URL types +export interface SupabaseStorageCreateSignedUrlParams { + apiKey: string + projectId: string + bucket: string + path: string + expiresIn: number + download?: boolean +} + +export interface SupabaseStorageCreateSignedUrlResponse extends ToolResponse { + output: { + message: string + signedUrl: string + } + error?: string +} diff --git a/apps/sim/tools/supabase/vector_search.ts b/apps/sim/tools/supabase/vector_search.ts index c0f33992d0..51a461ca78 100644 --- a/apps/sim/tools/supabase/vector_search.ts +++ b/apps/sim/tools/supabase/vector_search.ts @@ -72,11 +72,11 @@ export const vectorSearchTool: ToolConfig< // Add optional parameters if provided if (params.matchThreshold !== undefined) { - rpcParams.match_threshold = params.matchThreshold + rpcParams.match_threshold = Number(params.matchThreshold) } if (params.matchCount !== undefined) { - rpcParams.match_count = params.matchCount + rpcParams.match_count = Number(params.matchCount) } return rpcParams diff --git a/apps/sim/tools/tavily/crawl.ts b/apps/sim/tools/tavily/crawl.ts new file mode 100644 index 0000000000..d14b8a4e91 --- /dev/null +++ b/apps/sim/tools/tavily/crawl.ts @@ -0,0 +1,173 @@ +import type { CrawlResponse, TavilyCrawlParams } from '@/tools/tavily/types' +import type { ToolConfig } from '@/tools/types' + +export const crawlTool: ToolConfig = { + id: 'tavily_crawl', + name: 'Tavily Crawl', + description: + "Systematically crawl and extract content from websites using Tavily's crawl API. Supports depth control, path filtering, domain restrictions, and natural language instructions for targeted crawling.", + version: '1.0.0', + + params: { + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The root URL to begin the crawl', + }, + instructions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Natural language directions for the crawler (costs 2 credits per 10 pages)', + }, + max_depth: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'How far from base URL to explore (1-5, default: 1)', + }, + max_breadth: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Links followed per page level (≥1, default: 20)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Total links processed before stopping (≥1, default: 50)', + }, + select_paths: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to include specific URL paths (e.g., /docs/.*)', + }, + select_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to restrict crawling to certain domains', + }, + exclude_paths: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to skip specific URL paths', + }, + exclude_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to block certain domains', + }, + allow_external: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include external domain links in results (default: true)', + }, + include_images: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Incorporate images in crawl output', + }, + extract_depth: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Extraction depth: basic (1 credit/5 pages) or advanced (2 credits/5 pages)', + }, + format: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Output format: markdown or text (default: markdown)', + }, + include_favicon: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Add favicon URL for each result', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tavily API Key', + }, + }, + + request: { + url: 'https://api.tavily.com/crawl', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + url: params.url, + } + + if (params.instructions) body.instructions = params.instructions + if (params.max_depth) body.max_depth = Number(params.max_depth) + if (params.max_breadth) body.max_breadth = Number(params.max_breadth) + if (params.limit) body.limit = Number(params.limit) + if (params.select_paths) { + body.select_paths = params.select_paths.split(',').map((p) => p.trim()) + } + if (params.select_domains) { + body.select_domains = params.select_domains.split(',').map((d) => d.trim()) + } + if (params.exclude_paths) { + body.exclude_paths = params.exclude_paths.split(',').map((p) => p.trim()) + } + if (params.exclude_domains) { + body.exclude_domains = params.exclude_domains.split(',').map((d) => d.trim()) + } + if (params.allow_external !== undefined) body.allow_external = params.allow_external + if (params.include_images !== undefined) body.include_images = params.include_images + if (params.extract_depth) body.extract_depth = params.extract_depth + if (params.format) body.format = params.format + if (params.include_favicon !== undefined) body.include_favicon = params.include_favicon + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + base_url: data.base_url, + results: data.results || [], + response_time: data.response_time, + ...(data.request_id && { request_id: data.request_id }), + }, + } + }, + + outputs: { + base_url: { type: 'string', description: 'The base URL that was crawled' }, + results: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'The crawled page URL' }, + raw_content: { type: 'string', description: 'Extracted content from the page' }, + favicon: { type: 'string', description: 'Favicon URL (if requested)' }, + }, + }, + description: 'Array of crawled pages with extracted content', + }, + response_time: { type: 'number', description: 'Time taken for the crawl request in seconds' }, + request_id: { type: 'string', description: 'Unique identifier for support reference' }, + }, +} diff --git a/apps/sim/tools/tavily/extract.ts b/apps/sim/tools/tavily/extract.ts index d7699f0ef7..5920f2c9ef 100644 --- a/apps/sim/tools/tavily/extract.ts +++ b/apps/sim/tools/tavily/extract.ts @@ -21,6 +21,24 @@ export const extractTool: ToolConfig visibility: 'user-only', description: 'The depth of extraction (basic=1 credit/5 URLs, advanced=2 credits/5 URLs)', }, + format: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Output format: markdown or text (default: markdown)', + }, + include_images: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Incorporate images in extraction output', + }, + include_favicon: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Add favicon URL for each result', + }, apiKey: { type: 'string', required: true, @@ -42,6 +60,9 @@ export const extractTool: ToolConfig } if (params.extract_depth) body.extract_depth = params.extract_depth + if (params.format) body.format = params.format + if (params.include_images !== undefined) body.include_images = params.include_images + if (params.include_favicon !== undefined) body.include_favicon = params.include_favicon return body }, @@ -68,6 +89,7 @@ export const extractTool: ToolConfig properties: { url: { type: 'string', description: 'The URL that was extracted' }, raw_content: { type: 'string', description: 'The raw text content from the webpage' }, + favicon: { type: 'string', description: 'Favicon URL (if requested)' }, }, }, description: 'Successfully extracted content from URLs', diff --git a/apps/sim/tools/tavily/index.ts b/apps/sim/tools/tavily/index.ts index f301820c8e..e688eade7f 100644 --- a/apps/sim/tools/tavily/index.ts +++ b/apps/sim/tools/tavily/index.ts @@ -1,5 +1,9 @@ +import { crawlTool } from '@/tools/tavily/crawl' import { extractTool } from '@/tools/tavily/extract' +import { mapTool } from '@/tools/tavily/map' import { searchTool } from '@/tools/tavily/search' export const tavilyExtractTool = extractTool export const tavilySearchTool = searchTool +export const tavilyCrawlTool = crawlTool +export const tavilyMapTool = mapTool diff --git a/apps/sim/tools/tavily/map.ts b/apps/sim/tools/tavily/map.ts new file mode 100644 index 0000000000..2b09be9653 --- /dev/null +++ b/apps/sim/tools/tavily/map.ts @@ -0,0 +1,143 @@ +import type { MapResponse, TavilyMapParams } from '@/tools/tavily/types' +import type { ToolConfig } from '@/tools/types' + +export const mapTool: ToolConfig = { + id: 'tavily_map', + name: 'Tavily Map', + description: + "Discover and visualize website structure using Tavily's map API. Maps out all accessible URLs from a base URL with depth control, path filtering, and domain restrictions.", + version: '1.0.0', + + params: { + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The root URL to begin mapping', + }, + instructions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Natural language guidance for mapping behavior (costs 2 credits per 10 pages)', + }, + max_depth: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'How far from base URL to explore (1-5, default: 1)', + }, + max_breadth: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Links to follow per level (default: 20)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Total links to process (default: 50)', + }, + select_paths: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns for URL path filtering (e.g., /docs/.*)', + }, + select_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to restrict mapping to specific domains', + }, + exclude_paths: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to exclude specific URL paths', + }, + exclude_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated regex patterns to exclude domains', + }, + allow_external: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include external domain links in results (default: true)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tavily API Key', + }, + }, + + request: { + url: 'https://api.tavily.com/map', + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + url: params.url, + } + + if (params.instructions) body.instructions = params.instructions + if (params.max_depth) body.max_depth = Number(params.max_depth) + if (params.max_breadth) body.max_breadth = Number(params.max_breadth) + if (params.limit) body.limit = Number(params.limit) + if (params.select_paths) { + body.select_paths = params.select_paths.split(',').map((p) => p.trim()) + } + if (params.select_domains) { + body.select_domains = params.select_domains.split(',').map((d) => d.trim()) + } + if (params.exclude_paths) { + body.exclude_paths = params.exclude_paths.split(',').map((p) => p.trim()) + } + if (params.exclude_domains) { + body.exclude_domains = params.exclude_domains.split(',').map((d) => d.trim()) + } + if (params.allow_external !== undefined) body.allow_external = params.allow_external + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + base_url: data.base_url, + results: data.results || [], + response_time: data.response_time, + ...(data.request_id && { request_id: data.request_id }), + }, + } + }, + + outputs: { + base_url: { type: 'string', description: 'The base URL that was mapped' }, + results: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'Discovered URL' }, + }, + }, + description: 'Array of discovered URLs during mapping', + }, + response_time: { type: 'number', description: 'Time taken for the map request in seconds' }, + request_id: { type: 'string', description: 'Unique identifier for support reference' }, + }, +} diff --git a/apps/sim/tools/tavily/search.ts b/apps/sim/tools/tavily/search.ts index fe1dbf07c8..603d846a60 100644 --- a/apps/sim/tools/tavily/search.ts +++ b/apps/sim/tools/tavily/search.ts @@ -21,6 +21,96 @@ export const searchTool: ToolConfig = visibility: 'user-only', description: 'Maximum number of results (1-20)', }, + topic: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Category type: general, news, or finance (default: general)', + }, + search_depth: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Search scope: basic (1 credit) or advanced (2 credits) (default: basic)', + }, + include_answer: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'LLM-generated response: true/basic for quick answer or advanced for detailed', + }, + include_raw_content: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Parsed HTML content: true/markdown or text format', + }, + include_images: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include image search results', + }, + include_image_descriptions: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Add descriptive text for images', + }, + include_favicon: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Include favicon URLs', + }, + chunks_per_source: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of relevant chunks per source (1-3, default: 3)', + }, + time_range: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Filter by recency: day/d, week/w, month/m, year/y', + }, + start_date: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Earliest publication date (YYYY-MM-DD format)', + }, + end_date: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Latest publication date (YYYY-MM-DD format)', + }, + include_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to whitelist (max 300)', + }, + exclude_domains: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Comma-separated list of domains to blacklist (max 150)', + }, + country: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Boost results from specified country (general topic only)', + }, + auto_parameters: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Automatic parameter configuration based on query intent', + }, apiKey: { type: 'string', required: true, @@ -42,7 +132,27 @@ export const searchTool: ToolConfig = } // Only include optional parameters if explicitly set - if (params.max_results) body.max_results = params.max_results + if (params.max_results) body.max_results = Number(params.max_results) + if (params.topic) body.topic = params.topic + if (params.search_depth) body.search_depth = params.search_depth + if (params.include_answer) body.include_answer = params.include_answer + if (params.include_raw_content) body.include_raw_content = params.include_raw_content + if (params.include_images !== undefined) body.include_images = params.include_images + if (params.include_image_descriptions !== undefined) + body.include_image_descriptions = params.include_image_descriptions + if (params.include_favicon !== undefined) body.include_favicon = params.include_favicon + if (params.chunks_per_source) body.chunks_per_source = Number(params.chunks_per_source) + if (params.time_range) body.time_range = params.time_range + if (params.start_date) body.start_date = params.start_date + if (params.end_date) body.end_date = params.end_date + if (params.include_domains) { + body.include_domains = params.include_domains.split(',').map((d) => d.trim()) + } + if (params.exclude_domains) { + body.exclude_domains = params.exclude_domains.split(',').map((d) => d.trim()) + } + if (params.country) body.country = params.country + if (params.auto_parameters !== undefined) body.auto_parameters = params.auto_parameters return body }, @@ -59,8 +169,13 @@ export const searchTool: ToolConfig = title: result.title, url: result.url, snippet: result.snippet, + ...(result.score !== undefined && { score: result.score }), ...(result.raw_content && { raw_content: result.raw_content }), + ...(result.favicon && { favicon: result.favicon }), })), + ...(data.answer && { answer: data.answer }), + ...(data.images && { images: data.images }), + ...(data.auto_parameters && { auto_parameters: data.auto_parameters }), response_time: data.response_time, }, } @@ -76,10 +191,22 @@ export const searchTool: ToolConfig = title: { type: 'string' }, url: { type: 'string' }, snippet: { type: 'string' }, + score: { type: 'number' }, raw_content: { type: 'string' }, + favicon: { type: 'string' }, }, }, - description: 'Search results with titles, URLs, and content snippets', + description: 'Search results with titles, URLs, content snippets, and optional metadata', + }, + answer: { type: 'string', description: 'LLM-generated answer to the query (if requested)' }, + images: { + type: 'array', + items: { type: 'string' }, + description: 'Query-related images (if requested)', + }, + auto_parameters: { + type: 'object', + description: 'Automatically selected parameters based on query intent (if enabled)', }, response_time: { type: 'number', description: 'Time taken for the search request in seconds' }, }, diff --git a/apps/sim/tools/tavily/types.ts b/apps/sim/tools/tavily/types.ts index 658be45e88..0b5fc2ccbb 100644 --- a/apps/sim/tools/tavily/types.ts +++ b/apps/sim/tools/tavily/types.ts @@ -32,6 +32,9 @@ export interface TavilyExtractParams { urls: string | string[] apiKey: string extract_depth?: 'basic' | 'advanced' + format?: string + include_images?: boolean + include_favicon?: boolean } interface ExtractResult { @@ -54,6 +57,21 @@ export interface TavilySearchParams { query: string apiKey: string max_results?: number + topic?: string + search_depth?: string + include_answer?: string + include_raw_content?: string + include_images?: boolean + include_image_descriptions?: boolean + include_favicon?: boolean + chunks_per_source?: number + time_range?: string + start_date?: string + end_date?: string + include_domains?: string + exclude_domains?: string + country?: string + auto_parameters?: boolean } interface SearchResult { @@ -72,3 +90,83 @@ export interface SearchResponse extends ToolResponse { } export type TavilyResponse = TavilySearchResponse | TavilyExtractResponse + +// Crawl API types +export interface TavilyCrawlParams { + url: string + apiKey: string + instructions?: string + max_depth?: number + max_breadth?: number + limit?: number + select_paths?: string + select_domains?: string + exclude_paths?: string + exclude_domains?: string + allow_external?: boolean + include_images?: boolean + extract_depth?: string + format?: string + include_favicon?: boolean +} + +interface CrawlResult { + url: string + raw_content: string + favicon?: string +} + +export interface CrawlResponse extends ToolResponse { + output: { + base_url: string + results: CrawlResult[] + response_time: number + request_id?: string + } +} + +export interface TavilyCrawlResponse extends ToolResponse { + output: { + base_url: string + results: CrawlResult[] + response_time: number + request_id?: string + } +} + +// Map API types +export interface TavilyMapParams { + url: string + apiKey: string + instructions?: string + max_depth?: number + max_breadth?: number + limit?: number + select_paths?: string + select_domains?: string + exclude_paths?: string + exclude_domains?: string + allow_external?: boolean +} + +interface MapResult { + url: string +} + +export interface MapResponse extends ToolResponse { + output: { + base_url: string + results: MapResult[] + response_time: number + request_id?: string + } +} + +export interface TavilyMapResponse extends ToolResponse { + output: { + base_url: string + results: MapResult[] + response_time: number + request_id?: string + } +} diff --git a/apps/sim/tools/twilio_voice/list_calls.ts b/apps/sim/tools/twilio_voice/list_calls.ts index a1ed2581ee..49828e250d 100644 --- a/apps/sim/tools/twilio_voice/list_calls.ts +++ b/apps/sim/tools/twilio_voice/list_calls.ts @@ -80,7 +80,7 @@ export const listCallsTool: ToolConfig', params.startTimeAfter) if (params.startTimeBefore) queryParams.append('StartTime<', params.startTimeBefore) - if (params.pageSize) queryParams.append('PageSize', params.pageSize.toString()) + if (params.pageSize) queryParams.append('PageSize', Number(params.pageSize).toString()) const queryString = queryParams.toString() return queryString ? `${baseUrl}?${queryString}` : baseUrl diff --git a/apps/sim/tools/twilio_voice/make_call.ts b/apps/sim/tools/twilio_voice/make_call.ts index 326dbfce06..eb791e2225 100644 --- a/apps/sim/tools/twilio_voice/make_call.ts +++ b/apps/sim/tools/twilio_voice/make_call.ts @@ -155,7 +155,7 @@ export const makeCallTool: ToolConfig = formData.append('RecordingStatusCallback', params.recordingStatusCallback) } if (params.timeout) { - formData.append('Timeout', params.timeout.toString()) + formData.append('Timeout', Number(params.timeout).toString()) } if (params.machineDetection) { formData.append('MachineDetection', params.machineDetection) diff --git a/apps/sim/tools/typeform/list_forms.ts b/apps/sim/tools/typeform/list_forms.ts index 35cd199c5c..8a6204947f 100644 --- a/apps/sim/tools/typeform/list_forms.ts +++ b/apps/sim/tools/typeform/list_forms.ts @@ -50,11 +50,11 @@ export const listFormsTool: ToolConfig = { 'user.fields': 'name,username,description,profile_image_url,verified,public_metrics', }) - if (params.maxResults && params.maxResults < 10) { + if (params.maxResults && Number(params.maxResults) < 10) { queryParams.append('max_results', '10') } else if (params.maxResults) { - queryParams.append('max_results', params.maxResults.toString()) + queryParams.append('max_results', Number(params.maxResults).toString()) } if (params.startTime) queryParams.append('start_time', params.startTime) if (params.endTime) queryParams.append('end_time', params.endTime) diff --git a/apps/sim/tools/youtube/channel_playlists.ts b/apps/sim/tools/youtube/channel_playlists.ts index fc13c7a36d..c2339a0854 100644 --- a/apps/sim/tools/youtube/channel_playlists.ts +++ b/apps/sim/tools/youtube/channel_playlists.ts @@ -45,7 +45,7 @@ export const youtubeChannelPlaylistsTool: ToolConfig< let url = `https://www.googleapis.com/youtube/v3/playlists?part=snippet,contentDetails&channelId=${encodeURIComponent( params.channelId )}&key=${params.apiKey}` - url += `&maxResults=${params.maxResults || 10}` + url += `&maxResults=${Number(params.maxResults || 10)}` if (params.pageToken) { url += `&pageToken=${params.pageToken}` } diff --git a/apps/sim/tools/youtube/channel_videos.ts b/apps/sim/tools/youtube/channel_videos.ts index 1a6871417a..f0de06210c 100644 --- a/apps/sim/tools/youtube/channel_videos.ts +++ b/apps/sim/tools/youtube/channel_videos.ts @@ -51,7 +51,7 @@ export const youtubeChannelVideosTool: ToolConfig< let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&channelId=${encodeURIComponent( params.channelId )}&key=${params.apiKey}` - url += `&maxResults=${params.maxResults || 10}` + url += `&maxResults=${Number(params.maxResults || 10)}` if (params.order) { url += `&order=${params.order}` } diff --git a/apps/sim/tools/youtube/comments.ts b/apps/sim/tools/youtube/comments.ts index ccd20b8460..9f9f6674e8 100644 --- a/apps/sim/tools/youtube/comments.ts +++ b/apps/sim/tools/youtube/comments.ts @@ -44,7 +44,7 @@ export const youtubeCommentsTool: ToolConfig { let url = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${params.videoId}&key=${params.apiKey}` - url += `&maxResults=${params.maxResults || 20}` + url += `&maxResults=${Number(params.maxResults || 20)}` url += `&order=${params.order || 'relevance'}` if (params.pageToken) { url += `&pageToken=${params.pageToken}` diff --git a/apps/sim/tools/youtube/playlist_items.ts b/apps/sim/tools/youtube/playlist_items.ts index e6ad8e974a..5a75902164 100644 --- a/apps/sim/tools/youtube/playlist_items.ts +++ b/apps/sim/tools/youtube/playlist_items.ts @@ -43,7 +43,7 @@ export const youtubePlaylistItemsTool: ToolConfig< request: { url: (params: YouTubePlaylistItemsParams) => { let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${params.playlistId}&key=${params.apiKey}` - url += `&maxResults=${params.maxResults || 10}` + url += `&maxResults=${Number(params.maxResults || 10)}` if (params.pageToken) { url += `&pageToken=${params.pageToken}` } diff --git a/apps/sim/tools/youtube/related_videos.ts b/apps/sim/tools/youtube/related_videos.ts index 708e8809bd..e2d1603961 100644 --- a/apps/sim/tools/youtube/related_videos.ts +++ b/apps/sim/tools/youtube/related_videos.ts @@ -45,7 +45,7 @@ export const youtubeRelatedVideosTool: ToolConfig< let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&relatedToVideoId=${encodeURIComponent( params.videoId )}&key=${params.apiKey}` - url += `&maxResults=${params.maxResults || 10}` + url += `&maxResults=${Number(params.maxResults || 10)}` if (params.pageToken) { url += `&pageToken=${params.pageToken}` } diff --git a/apps/sim/tools/youtube/search.ts b/apps/sim/tools/youtube/search.ts index d3ae3dc754..0bb464492d 100644 --- a/apps/sim/tools/youtube/search.ts +++ b/apps/sim/tools/youtube/search.ts @@ -108,7 +108,7 @@ export const youtubeSearchTool: ToolConfig = { const queryParams = new URLSearchParams() const mode = params.mode || 'summary' queryParams.append('mode', mode) - if (params.minRating !== undefined) queryParams.append('minRating', String(params.minRating)) + if (params.minRating !== undefined) + queryParams.append('minRating', String(Number(params.minRating))) return `https://api.getzep.com/api/v2/threads/${params.threadId}/context?${queryParams.toString()}` }, method: 'GET', diff --git a/apps/sim/tools/zep/get_messages.ts b/apps/sim/tools/zep/get_messages.ts index 3af99c1c99..14cf433080 100644 --- a/apps/sim/tools/zep/get_messages.ts +++ b/apps/sim/tools/zep/get_messages.ts @@ -44,9 +44,9 @@ export const zepGetMessagesTool: ToolConfig = { request: { url: (params) => { const queryParams = new URLSearchParams() - if (params.limit) queryParams.append('limit', String(params.limit)) + if (params.limit) queryParams.append('limit', String(Number(params.limit))) if (params.cursor) queryParams.append('cursor', params.cursor) - if (params.lastn) queryParams.append('lastn', String(params.lastn)) + if (params.lastn) queryParams.append('lastn', String(Number(params.lastn))) const queryString = queryParams.toString() return `https://api.getzep.com/api/v2/threads/${params.threadId}/messages${queryString ? `?${queryString}` : ''}` diff --git a/apps/sim/tools/zep/get_threads.ts b/apps/sim/tools/zep/get_threads.ts index 322e36fa54..c6dbc14010 100644 --- a/apps/sim/tools/zep/get_threads.ts +++ b/apps/sim/tools/zep/get_threads.ts @@ -47,8 +47,8 @@ export const zepGetThreadsTool: ToolConfig = { request: { url: (params) => { const queryParams = new URLSearchParams() - queryParams.append('page_size', String(params.pageSize || 10)) - queryParams.append('page_number', String(params.pageNumber || 1)) + queryParams.append('page_size', String(Number(params.pageSize || 10))) + queryParams.append('page_number', String(Number(params.pageNumber || 1))) if (params.orderBy) queryParams.append('order_by', params.orderBy) if (params.asc !== undefined) queryParams.append('asc', String(params.asc)) return `https://api.getzep.com/api/v2/threads?${queryParams.toString()}` diff --git a/apps/sim/tools/zep/get_user_threads.ts b/apps/sim/tools/zep/get_user_threads.ts index 67c4b79920..a28d7ae7d2 100644 --- a/apps/sim/tools/zep/get_user_threads.ts +++ b/apps/sim/tools/zep/get_user_threads.ts @@ -32,7 +32,7 @@ export const zepGetUserThreadsTool: ToolConfig = { request: { url: (params) => { - const limit = params.limit || 10 + const limit = Number(params.limit || 10) return `https://api.getzep.com/api/v2/users/${params.userId}/threads?limit=${limit}` }, method: 'GET', diff --git a/apps/sim/triggers/stripe/webhook.ts b/apps/sim/triggers/stripe/webhook.ts index 6e72e4d1e1..aa4fca34b7 100644 --- a/apps/sim/triggers/stripe/webhook.ts +++ b/apps/sim/triggers/stripe/webhook.ts @@ -1,13 +1,13 @@ -import { ShieldCheck } from 'lucide-react' +import { StripeIcon } from '@/components/icons' import type { TriggerConfig } from '@/triggers/types' export const stripeWebhookTrigger: TriggerConfig = { id: 'stripe_webhook', name: 'Stripe Webhook', provider: 'stripe', - description: 'Triggers when Stripe events occur (payments, subscriptions, etc.)', + description: 'Triggers when Stripe events occur (payments, subscriptions, invoices, etc.)', version: '1.0.0', - icon: ShieldCheck, + icon: StripeIcon, subBlocks: [ { @@ -20,18 +20,165 @@ export const stripeWebhookTrigger: TriggerConfig = { placeholder: 'Webhook URL will be generated', mode: 'trigger', }, + { + id: 'eventTypes', + title: 'Event Types to Listen For', + type: 'dropdown', + multiSelect: true, + layout: 'full', + options: [ + // Payment Intents + { label: 'payment_intent.succeeded', id: 'payment_intent.succeeded' }, + { label: 'payment_intent.created', id: 'payment_intent.created' }, + { label: 'payment_intent.payment_failed', id: 'payment_intent.payment_failed' }, + { label: 'payment_intent.canceled', id: 'payment_intent.canceled' }, + { + label: 'payment_intent.amount_capturable_updated', + id: 'payment_intent.amount_capturable_updated', + }, + { label: 'payment_intent.processing', id: 'payment_intent.processing' }, + { label: 'payment_intent.requires_action', id: 'payment_intent.requires_action' }, + + // Charges + { label: 'charge.succeeded', id: 'charge.succeeded' }, + { label: 'charge.failed', id: 'charge.failed' }, + { label: 'charge.captured', id: 'charge.captured' }, + { label: 'charge.refunded', id: 'charge.refunded' }, + { label: 'charge.updated', id: 'charge.updated' }, + { label: 'charge.dispute.created', id: 'charge.dispute.created' }, + { label: 'charge.dispute.closed', id: 'charge.dispute.closed' }, + { label: 'charge.expired', id: 'charge.expired' }, + { label: 'charge.dispute.funds_withdrawn', id: 'charge.dispute.funds_withdrawn' }, + { label: 'charge.dispute.funds_reinstated', id: 'charge.dispute.funds_reinstated' }, + + // Customers + { label: 'customer.created', id: 'customer.created' }, + { label: 'customer.updated', id: 'customer.updated' }, + { label: 'customer.deleted', id: 'customer.deleted' }, + { label: 'customer.source.created', id: 'customer.source.created' }, + { label: 'customer.source.updated', id: 'customer.source.updated' }, + { label: 'customer.source.deleted', id: 'customer.source.deleted' }, + { label: 'customer.subscription.created', id: 'customer.subscription.created' }, + { label: 'customer.subscription.updated', id: 'customer.subscription.updated' }, + { label: 'customer.subscription.deleted', id: 'customer.subscription.deleted' }, + { label: 'customer.discount.created', id: 'customer.discount.created' }, + { label: 'customer.discount.deleted', id: 'customer.discount.deleted' }, + { label: 'customer.discount.updated', id: 'customer.discount.updated' }, + + // Subscriptions + { + label: 'customer.subscription.trial_will_end', + id: 'customer.subscription.trial_will_end', + }, + { label: 'customer.subscription.paused', id: 'customer.subscription.paused' }, + { label: 'customer.subscription.resumed', id: 'customer.subscription.resumed' }, + + // Invoices + { label: 'invoice.created', id: 'invoice.created' }, + { label: 'invoice.finalized', id: 'invoice.finalized' }, + { label: 'invoice.finalization_failed', id: 'invoice.finalization_failed' }, + { label: 'invoice.paid', id: 'invoice.paid' }, + { label: 'invoice.payment_failed', id: 'invoice.payment_failed' }, + { label: 'invoice.payment_succeeded', id: 'invoice.payment_succeeded' }, + { label: 'invoice.payment_action_required', id: 'invoice.payment_action_required' }, + { label: 'invoice.sent', id: 'invoice.sent' }, + { label: 'invoice.upcoming', id: 'invoice.upcoming' }, + { label: 'invoice.updated', id: 'invoice.updated' }, + { label: 'invoice.voided', id: 'invoice.voided' }, + { label: 'invoice.marked_uncollectible', id: 'invoice.marked_uncollectible' }, + { label: 'invoice.overdue', id: 'invoice.overdue' }, + + // Products & Prices + { label: 'product.created', id: 'product.created' }, + { label: 'product.updated', id: 'product.updated' }, + { label: 'product.deleted', id: 'product.deleted' }, + { label: 'price.created', id: 'price.created' }, + { label: 'price.updated', id: 'price.updated' }, + { label: 'price.deleted', id: 'price.deleted' }, + + // Payment Methods + { label: 'payment_method.attached', id: 'payment_method.attached' }, + { label: 'payment_method.detached', id: 'payment_method.detached' }, + { label: 'payment_method.updated', id: 'payment_method.updated' }, + { + label: 'payment_method.automatically_updated', + id: 'payment_method.automatically_updated', + }, + + // Setup Intents + { label: 'setup_intent.succeeded', id: 'setup_intent.succeeded' }, + { label: 'setup_intent.setup_failed', id: 'setup_intent.setup_failed' }, + { label: 'setup_intent.canceled', id: 'setup_intent.canceled' }, + + // Refunds + { label: 'refund.created', id: 'refund.created' }, + { label: 'refund.updated', id: 'refund.updated' }, + { label: 'refund.failed', id: 'refund.failed' }, + + // Checkout Sessions + { label: 'checkout.session.completed', id: 'checkout.session.completed' }, + { label: 'checkout.session.expired', id: 'checkout.session.expired' }, + { + label: 'checkout.session.async_payment_succeeded', + id: 'checkout.session.async_payment_succeeded', + }, + { + label: 'checkout.session.async_payment_failed', + id: 'checkout.session.async_payment_failed', + }, + + // Payouts + { label: 'payout.created', id: 'payout.created' }, + { label: 'payout.updated', id: 'payout.updated' }, + { label: 'payout.paid', id: 'payout.paid' }, + { label: 'payout.failed', id: 'payout.failed' }, + { label: 'payout.canceled', id: 'payout.canceled' }, + + // Coupons + { label: 'coupon.created', id: 'coupon.created' }, + { label: 'coupon.updated', id: 'coupon.updated' }, + { label: 'coupon.deleted', id: 'coupon.deleted' }, + + // Credit Notes + { label: 'credit_note.created', id: 'credit_note.created' }, + { label: 'credit_note.updated', id: 'credit_note.updated' }, + { label: 'credit_note.voided', id: 'credit_note.voided' }, + + // Account + { label: 'account.updated', id: 'account.updated' }, + { label: 'account.application.deauthorized', id: 'account.application.deauthorized' }, + + // Balance + { label: 'balance.available', id: 'balance.available' }, + ], + placeholder: 'Leave empty to receive all events', + description: + 'Select specific Stripe events to filter. Leave empty to receive all events from Stripe.', + mode: 'trigger', + }, + { + id: 'webhookSecret', + title: 'Webhook Signing Secret', + type: 'short-input', + placeholder: 'whsec_...', + description: + 'Your webhook signing secret from Stripe Dashboard. Used to verify webhook authenticity.', + password: true, + mode: 'trigger', + }, { id: 'triggerInstructions', title: 'Setup Instructions', type: 'text', defaultValue: [ - 'Go to your Stripe Dashboard at https://dashboard.stripe.com/', - 'Navigate to Developers > Webhooks', - 'Click "Add endpoint"', - 'Paste the Webhook URL above into the "Endpoint URL" field', - 'Select the events you want to listen to (e.g., charge.succeeded)', - 'Click "Add endpoint"', - 'Stripe will send a test event to verify your webhook endpoint', + 'Go to your Stripe Dashboard at https://dashboard.stripe.com/webhooks', + 'Click "Add destination" button', + 'In "Events to send", select the events you want to listen to (must match the events selected above, or select "Select all events" to receive everything)', + 'Select `Webhook Endpoint`, press continue, and paste the Webhook URL above into the "Endpoint URL" field', + 'Click "Create Destination" to save', + 'After creating the endpoint, click "Reveal" next to "Signing secret" and copy it', + 'Paste the signing secret into the Webhook Signing Secret field above', + 'Click "Save" to activate your webhook trigger', ] .map( (instruction, index) => @@ -54,28 +201,34 @@ export const stripeWebhookTrigger: TriggerConfig = { language: 'json', defaultValue: JSON.stringify( { - id: 'evt_1234567890', - type: 'charge.succeeded', - created: 1641234567, + id: 'evt_1234567890abcdef', + object: 'event', + api_version: '2023-10-16', + created: 1677649261, + type: 'payment_intent.succeeded', + livemode: false, data: { object: { - id: 'ch_1234567890', - object: 'charge', + id: 'pi_1234567890abcdef', + object: 'payment_intent', amount: 2500, + amount_capturable: 0, + amount_received: 2500, currency: 'usd', - description: 'Sample charge', - paid: true, - status: 'succeeded', - customer: 'cus_1234567890', + customer: 'cus_1234567890abcdef', + description: 'Example payment', + metadata: { + order_id: '6735', + }, + payment_method: 'pm_1234567890abcdef', receipt_email: 'customer@example.com', + status: 'succeeded', }, }, - object: 'event', - livemode: false, - api_version: '2020-08-27', + pending_webhooks: 1, request: { - id: 'req_1234567890', - idempotency_key: null, + id: 'req_1234567890abcdef', + idempotency_key: '00000000-0000-0000-0000-000000000000', }, }, null, @@ -91,34 +244,38 @@ export const stripeWebhookTrigger: TriggerConfig = { outputs: { id: { type: 'string', - description: 'Event ID from Stripe', + description: 'Unique identifier for the event', }, type: { type: 'string', - description: 'Event type (e.g., charge.succeeded, payment_intent.succeeded)', + description: 'Event type (e.g., payment_intent.succeeded, customer.created, invoice.paid)', }, - created: { + object: { type: 'string', - description: 'Timestamp when the event was created', + description: 'Always "event"', }, - data: { + api_version: { type: 'string', - description: 'Event data containing the affected Stripe object', + description: 'Stripe API version used to render the event', }, - object: { - type: 'string', - description: 'The Stripe object that was updated (e.g., charge, payment_intent)', + created: { + type: 'number', + description: 'Unix timestamp when the event was created', + }, + data: { + type: 'json', + description: 'Event data containing the affected Stripe object', }, livemode: { - type: 'string', - description: 'Whether this event occurred in live mode or test mode', + type: 'boolean', + description: 'Whether this event occurred in live mode (true) or test mode (false)', }, - apiVersion: { - type: 'string', - description: 'API version used to render this event', + pending_webhooks: { + type: 'number', + description: 'Number of webhooks yet to be delivered for this event', }, request: { - type: 'string', + type: 'json', description: 'Information about the request that triggered this event', }, },