diff --git a/apps/docs/content/docs/en/integrations/elevenlabs.mdx b/apps/docs/content/docs/en/integrations/elevenlabs.mdx index 0099b3a1fd8..ca8fef8cd3a 100644 --- a/apps/docs/content/docs/en/integrations/elevenlabs.mdx +++ b/apps/docs/content/docs/en/integrations/elevenlabs.mdx @@ -1,6 +1,6 @@ --- title: ElevenLabs -description: Convert text to speech with ElevenLabs +description: Generate and transform audio with ElevenLabs --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -27,7 +27,7 @@ In Sim, the ElevenLabs integration enables your agents to convert text to lifeli ## Usage Instructions -Integrate ElevenLabs into the workflow. Can convert text to speech. +Integrate ElevenLabs into the workflow. Convert text to speech, generate sound effects, transform voices, isolate audio, and manage voices, models, and account settings. @@ -55,4 +55,214 @@ Convert text to speech using ElevenLabs voices | `audioUrl` | string | The URL of the generated audio | | `audioFile` | file | The generated audio file | +### `elevenlabs_sound_effects` + +Generate a sound effect from a text prompt using ElevenLabs + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `text` | string | Yes | The prompt describing the sound effect \(e.g., "thunder rumbling in the distance"\) | +| `modelId` | string | No | The model to use \(defaults to eleven_text_to_sound_v2\) | +| `durationSeconds` | number | No | Length of the sound in seconds \(0.5-30\). Omit to auto-determine | +| `promptInfluence` | number | No | How closely to follow the prompt from 0.0 to 1.0 \(default 0.3\) | +| `loop` | boolean | No | Whether to generate a seamlessly looping sound effect \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `audioUrl` | string | URL of the generated sound effect | +| `audioFile` | file | The generated sound effect file | + +### `elevenlabs_speech_to_speech` + +Convert audio into a chosen ElevenLabs voice while preserving content and emotion + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `voiceId` | string | Yes | The ID of the target voice to convert the audio into | +| `audioFile` | file | Yes | The source audio file to convert \(e.g., MP3, WAV, M4A\) | +| `modelId` | string | No | The model to use \(defaults to eleven_english_sts_v2\) | +| `removeBackgroundNoise` | boolean | No | Whether to isolate the voice and remove background noise \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `audioUrl` | string | URL of the converted audio | +| `audioFile` | file | The converted audio file | + +### `elevenlabs_audio_isolation` + +Remove background noise from an audio file, isolating the speech using ElevenLabs + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `audioFile` | file | Yes | The audio file to isolate speech from \(e.g., MP3, WAV, M4A\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `audioUrl` | string | URL of the isolated audio | +| `audioFile` | file | The isolated audio file | + +### `elevenlabs_list_voices` + +List the voices available in your ElevenLabs account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `search` | string | No | Search term to filter voices by name, description, labels, or category | +| `category` | string | No | Filter by category: premade, cloned, generated, or professional | +| `pageSize` | number | No | Number of voices to return \(1-100, default 10\) | +| `nextPageToken` | string | No | Page token from a previous response to fetch the next page of voices | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `voices` | array | List of voices | +| ↳ `voiceId` | string | Unique voice identifier | +| ↳ `name` | string | Voice name | +| ↳ `category` | string | Voice category | +| ↳ `description` | string | Voice description | +| ↳ `labels` | json | Voice labels \(accent, gender, age, use case\) | +| ↳ `previewUrl` | string | URL to a preview audio sample | +| ↳ `settings` | json | Default voice settings | +| `totalCount` | number | Total number of matching voices | +| `hasMore` | boolean | Whether more voices are available | +| `nextPageToken` | string | Token to fetch the next page | + +### `elevenlabs_get_voice` + +Get metadata and settings for a specific ElevenLabs voice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `voiceId` | string | Yes | The ID of the voice to retrieve \(e.g., "21m00Tcm4TlvDq8ikWAM"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `voiceId` | string | Unique voice identifier | +| `name` | string | Voice name | +| `category` | string | Voice category | +| `description` | string | Voice description | +| `labels` | json | Voice labels \(accent, gender, age, use case\) | +| `previewUrl` | string | URL to a preview audio sample | +| `settings` | json | Default voice settings | +| `availableForTiers` | array | Subscription tiers the voice is available on | +| `highQualityBaseModelIds` | array | Model IDs that support high-quality output for this voice | +| `isOwner` | boolean | Whether the current user owns this voice | + +### `elevenlabs_get_voice_settings` + +Get the configured settings for a specific ElevenLabs voice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `voiceId` | string | Yes | The ID of the voice whose settings to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stability` | number | Voice stability \(0.0-1.0\) | +| `similarityBoost` | number | Similarity boost \(0.0-1.0\) | +| `style` | number | Style exaggeration \(0.0-1.0\) | +| `useSpeakerBoost` | boolean | Whether speaker boost is enabled | +| `speed` | number | Speech speed \(1.0 = normal\) | + +### `elevenlabs_edit_voice_settings` + +Update the settings for a specific ElevenLabs voice + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | +| `voiceId` | string | Yes | The ID of the voice to update | +| `stability` | number | No | Voice stability from 0.0 to 1.0 \(default 0.5\) | +| `similarityBoost` | number | No | Similarity boost from 0.0 to 1.0 \(default 0.75\) | +| `style` | number | No | Style exaggeration from 0.0 to 1.0 \(default 0\) | +| `useSpeakerBoost` | boolean | No | Whether to enhance similarity to the original speaker \(default true\) | +| `speed` | number | No | Speech speed where 1.0 is normal \(default 1.0\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Request outcome \("ok" on success\) | + +### `elevenlabs_list_models` + +List the models available in ElevenLabs + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `models` | array | List of available models | +| ↳ `modelId` | string | Unique model identifier | +| ↳ `name` | string | Model name | +| ↳ `description` | string | Model description | +| ↳ `canDoTextToSpeech` | boolean | Supports text-to-speech | +| ↳ `canDoVoiceConversion` | boolean | Supports voice conversion | +| ↳ `canUseStyle` | boolean | Supports the style parameter | +| ↳ `canUseSpeakerBoost` | boolean | Supports speaker boost | +| ↳ `languages` | array | Languages supported by the model | +| ↳ `languageId` | string | Language code | +| ↳ `name` | string | Language name | + +### `elevenlabs_get_user` + +Get account and subscription information for the ElevenLabs user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Your ElevenLabs API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | Unique user identifier | +| `isNewUser` | boolean | Whether the user is new | +| `subscription` | object | Subscription and usage details | +| ↳ `tier` | string | Subscription tier | +| ↳ `characterCount` | number | Characters used this period | +| ↳ `characterLimit` | number | Character quota for this period | +| ↳ `canExtendCharacterLimit` | boolean | Whether the character limit can be extended | +| ↳ `status` | string | Subscription status | +| ↳ `nextCharacterCountResetUnix` | number | Unix timestamp when the character count resets | + diff --git a/apps/docs/content/docs/en/integrations/firecrawl.mdx b/apps/docs/content/docs/en/integrations/firecrawl.mdx index adde3194056..64be2dac640 100644 --- a/apps/docs/content/docs/en/integrations/firecrawl.mdx +++ b/apps/docs/content/docs/en/integrations/firecrawl.mdx @@ -78,6 +78,81 @@ Extract structured content from web pages with comprehensive metadata support. C | ↳ `ogSiteName` | string | Open Graph site name | | ↳ `error` | string | Error message if scrape failed | +### `firecrawl_batch_scrape` + +Scrape multiple URLs in a single batch job and retrieve structured content from each page. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `urls` | json | Yes | Array of URLs to scrape \(e.g., \["https://example.com/page1", "https://example.com/page2"\]\) | +| `formats` | json | No | Output formats for scraped content \(e.g., \["markdown"\], \["markdown", "html"\]\) | +| `onlyMainContent` | boolean | No | Extract only main content from pages | +| `maxConcurrency` | number | No | Maximum number of concurrent scrapes | +| `ignoreInvalidURLs` | boolean | No | Skip invalid URLs instead of failing the batch \(default: true\) | +| `scrapeOptions` | json | No | Advanced scraping configuration options | +| `zeroDataRetention` | boolean | No | Enable zero data retention | +| `apiKey` | string | Yes | Firecrawl API key | +| `pricing` | custom | No | No description | +| `rateLimit` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pages` | array | Array of scraped pages with their content and metadata | +| ↳ `markdown` | string | Page content in markdown format | +| ↳ `html` | string | Processed HTML content of the page | +| ↳ `rawHtml` | string | Unprocessed raw HTML content | +| ↳ `links` | array | Array of links found on the page | +| ↳ `screenshot` | string | Screenshot URL \(expires after 24 hours\) | +| ↳ `metadata` | object | Page metadata from crawl operation | +| ↳ `title` | string | Page title | +| ↳ `description` | string | Page meta description | +| ↳ `language` | string | Page language code | +| ↳ `sourceURL` | string | Original source URL | +| ↳ `statusCode` | number | HTTP status code | +| ↳ `ogLocaleAlternate` | array | Alternate locale versions | +| `total` | number | Total number of pages attempted | +| `completed` | number | Number of pages successfully scraped | +| `invalidURLs` | array | URLs that were skipped because they were invalid | + +### `firecrawl_batch_scrape_status` + +Check the status and retrieve results of a previously started Firecrawl batch scrape job by its job ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `jobId` | string | Yes | The ID of the batch scrape job to check | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Current batch scrape status \(scraping, completed, or failed\) | +| `total` | number | Total number of pages attempted | +| `completed` | number | Number of pages successfully scraped | +| `creditsUsed` | number | Credits consumed by the batch scrape | +| `expiresAt` | string | ISO timestamp when the batch scrape results expire | +| `next` | string | URL to retrieve the next page of results when present | +| `pages` | array | Array of scraped pages with their content and metadata | +| ↳ `markdown` | string | Page content in markdown format | +| ↳ `html` | string | Processed HTML content of the page | +| ↳ `rawHtml` | string | Unprocessed raw HTML content | +| ↳ `links` | array | Array of links found on the page | +| ↳ `screenshot` | string | Screenshot URL \(expires after 24 hours\) | +| ↳ `metadata` | object | Page metadata from crawl operation | +| ↳ `title` | string | Page title | +| ↳ `description` | string | Page meta description | +| ↳ `language` | string | Page language code | +| ↳ `sourceURL` | string | Original source URL | +| ↳ `statusCode` | number | HTTP status code | +| ↳ `ogLocaleAlternate` | array | Alternate locale versions | + ### `firecrawl_search` Search for information on the web using Firecrawl @@ -149,6 +224,58 @@ Crawl entire websites and extract structured content from all accessible pages | ↳ `ogLocaleAlternate` | array | Alternate locale versions | | `total` | number | Total number of pages found during crawl | +### `firecrawl_crawl_status` + +Check the status and retrieve results of a previously started Firecrawl crawl job by its job ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `jobId` | string | Yes | The ID of the crawl job to check | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Current crawl status \(scraping, completed, or failed\) | +| `total` | number | Total number of pages attempted | +| `completed` | number | Number of pages successfully crawled | +| `creditsUsed` | number | Credits consumed by the crawl | +| `expiresAt` | string | ISO timestamp when the crawl results expire | +| `next` | string | URL to retrieve the next page of results when present | +| `pages` | array | Array of crawled pages with their content and metadata | +| ↳ `markdown` | string | Page content in markdown format | +| ↳ `html` | string | Processed HTML content of the page | +| ↳ `rawHtml` | string | Unprocessed raw HTML content | +| ↳ `links` | array | Array of links found on the page | +| ↳ `screenshot` | string | Screenshot URL \(expires after 24 hours\) | +| ↳ `metadata` | object | Page metadata from crawl operation | +| ↳ `title` | string | Page title | +| ↳ `description` | string | Page meta description | +| ↳ `language` | string | Page language code | +| ↳ `sourceURL` | string | Original source URL | +| ↳ `statusCode` | number | HTTP status code | +| ↳ `ogLocaleAlternate` | array | Alternate locale versions | + +### `firecrawl_cancel_crawl` + +Cancel an in-progress Firecrawl crawl job by its job ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `jobId` | string | Yes | The ID of the crawl job to cancel | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Status of the cancelled crawl job \(e.g., "cancelled"\) | + ### `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. @@ -204,6 +331,27 @@ Extract structured data from entire webpages using natural language prompts and | `success` | boolean | Whether the extraction operation was successful | | `data` | object | Extracted structured data according to the schema or prompt | +### `firecrawl_extract_status` + +Check the status and retrieve results of a previously started Firecrawl extract job by its job ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `jobId` | string | Yes | The ID of the extract job to check | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Current extract status \(processing, completed, failed, or cancelled\) | +| `data` | json | Extracted structured data according to the schema or prompt | +| `expiresAt` | string | ISO timestamp when the extract results expire | +| `creditsUsed` | number | Number of credits used by the extract job | +| `tokensUsed` | number | Number of tokens used by the extract job | + ### `firecrawl_agent` Autonomous web data extraction agent. Searches and gathers information based on natural language prompts without requiring specific URLs. @@ -274,4 +422,23 @@ Parse uploaded documents (PDF, DOCX, HTML, etc.) into clean markdown using Firec | ↳ `error` | string | Error message if parse failed | | `warning` | string | Warning message from the parse operation | +### `firecrawl_credit_usage` + +Retrieve the remaining and allocated Firecrawl credits for the team. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Firecrawl API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `remainingCredits` | number | Number of credits remaining for the team | +| `planCredits` | number | Credits allocated in the current plan | +| `billingPeriodStart` | string | Start of the current billing period | +| `billingPeriodEnd` | string | End of the current billing period | + diff --git a/apps/docs/content/docs/en/integrations/google_drive.mdx b/apps/docs/content/docs/en/integrations/google_drive.mdx index 5b2d22b43f8..139bd86ab2e 100644 --- a/apps/docs/content/docs/en/integrations/google_drive.mdx +++ b/apps/docs/content/docs/en/integrations/google_drive.mdx @@ -694,6 +694,163 @@ List all permissions (who has access) for a file in Google Drive | ↳ `permissionDetails` | json | Details about inherited permissions | | `nextPageToken` | string | Token for fetching the next page of permissions | +### `google_drive_export` + +Export a Google Workspace file (Docs, Sheets, Slides, Drawings) to a chosen format such as PDF, DOCX, XLSX, or CSV + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the Google Workspace file to export | +| `mimeType` | string | Yes | The target MIME type to export to \(e.g. application/pdf, text/csv\) | +| `fileName` | string | No | Optional filename override for the exported file | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | file | Exported file stored in execution files | +| `exportedMimeType` | string | The MIME type the file was exported to | + +### `google_drive_list_revisions` + +List the revision history of a file in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to list revisions for | +| `pageSize` | number | No | Maximum number of revisions to return \(1-1000, default 200\) | +| `pageToken` | string | No | The page token to use for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `revisions` | array | List of revisions for the file \(most recent last\) | +| ↳ `id` | string | Revision ID | +| ↳ `mimeType` | string | MIME type of the revision | +| ↳ `modifiedTime` | string | When this revision was created | +| ↳ `keepForever` | boolean | Whether this revision is preserved forever | +| ↳ `published` | boolean | Whether this revision is published | +| ↳ `publishedLink` | string | Public link to the published revision | +| ↳ `lastModifyingUser` | json | User who created this revision | +| ↳ `originalFilename` | string | Original filename for binary revisions | +| ↳ `md5Checksum` | string | MD5 checksum for binary revisions | +| ↳ `size` | string | Size of the revision in bytes | +| ↳ `exportLinks` | json | Export format links for the revision | +| `nextPageToken` | string | Token for fetching the next page of revisions | + +### `google_drive_get_revision` + +Get metadata for a specific revision of a file in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file the revision belongs to | +| `revisionId` | string | Yes | The ID of the revision to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `revision` | json | The revision metadata | +| ↳ `id` | string | Revision ID | +| ↳ `mimeType` | string | MIME type of the revision | +| ↳ `modifiedTime` | string | When this revision was created | +| ↳ `keepForever` | boolean | Whether this revision is preserved forever | +| ↳ `published` | boolean | Whether this revision is published | +| ↳ `publishedLink` | string | Public link to the published revision | +| ↳ `lastModifyingUser` | json | User who created this revision | +| ↳ `originalFilename` | string | Original filename for binary revisions | +| ↳ `md5Checksum` | string | MD5 checksum for binary revisions | +| ↳ `size` | string | Size of the revision in bytes | +| ↳ `exportLinks` | json | Export format links for the revision | + +### `google_drive_list_comments` + +List comments on a file in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to list comments for | +| `includeDeleted` | boolean | No | Whether to include deleted comments \(their content is stripped\) | +| `pageSize` | number | No | Maximum number of comments to return \(1-100, default 20\) | +| `startModifiedTime` | string | No | Only return comments modified after this RFC 3339 timestamp | +| `pageToken` | string | No | The page token to use for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comments` | array | List of comments on the file | +| ↳ `id` | string | Comment ID | +| ↳ `content` | string | Plain text content of the comment | +| ↳ `htmlContent` | string | HTML-formatted content of the comment | +| ↳ `author` | json | User who authored the comment | +| ↳ `createdTime` | string | When the comment was created | +| ↳ `modifiedTime` | string | When the comment was last modified | +| ↳ `resolved` | boolean | Whether the comment has been resolved | +| ↳ `deleted` | boolean | Whether the comment has been deleted | +| ↳ `anchor` | string | Region of the document the comment refers to | +| ↳ `quotedFileContent` | json | The file content the comment quotes | +| ↳ `replies` | json | Threaded replies to the comment | +| `nextPageToken` | string | Token for fetching the next page of comments | + +### `google_drive_create_comment` + +Add a comment to a file in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file to comment on | +| `content` | string | Yes | The plain text content of the comment | +| `anchor` | string | No | A region of the document the comment refers to \(JSON anchor string\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | json | The created comment | +| ↳ `id` | string | Comment ID | +| ↳ `content` | string | Plain text content of the comment | +| ↳ `htmlContent` | string | HTML-formatted content of the comment | +| ↳ `author` | json | User who authored the comment | +| ↳ `createdTime` | string | When the comment was created | +| ↳ `modifiedTime` | string | When the comment was last modified | +| ↳ `resolved` | boolean | Whether the comment has been resolved | +| ↳ `deleted` | boolean | Whether the comment has been deleted | +| ↳ `anchor` | string | Region of the document the comment refers to | +| ↳ `quotedFileContent` | json | The file content the comment quotes | +| ↳ `replies` | json | Threaded replies to the comment | + +### `google_drive_delete_comment` + +Delete a comment from a file in Google Drive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file the comment belongs to | +| `commentId` | string | Yes | The ID of the comment to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the comment was successfully deleted | +| `fileId` | string | The ID of the file | +| `commentId` | string | The ID of the deleted comment | + ### `google_drive_get_about` Get information about the user and their Google Drive (storage quota, capabilities) diff --git a/apps/docs/content/docs/en/integrations/pinecone.mdx b/apps/docs/content/docs/en/integrations/pinecone.mdx index c7fab08fc7e..d3b443f1d11 100644 --- a/apps/docs/content/docs/en/integrations/pinecone.mdx +++ b/apps/docs/content/docs/en/integrations/pinecone.mdx @@ -29,7 +29,7 @@ In Sim, the Pinecone integration enables your agents to leverage vector search c ## Usage Instructions -Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors. +Integrate Pinecone into the workflow. Generate embeddings, upsert and update text records, delete vectors, search with text or vectors, fetch and list vectors, inspect index statistics, and manage indexes. @@ -75,6 +75,49 @@ Insert or update text records in a Pinecone index | --------- | ---- | ----------- | | `statusText` | string | Status of the upsert operation | +### `pinecone_update_vector` + +Update the values, sparse values, or metadata of a vector in a Pinecone namespace + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `indexHost` | string | Yes | Full Pinecone index host URL \(e.g., "https://my-index-abc123.svc.pinecone.io"\) | +| `id` | string | Yes | Unique ID of the vector to update | +| `namespace` | string | No | Namespace containing the vector \(e.g., "documents", "embeddings"\) | +| `values` | array | No | New dense vector values to overwrite the existing values | +| `sparseValues` | object | No | New sparse vector values with indices and values arrays | +| `setMetadata` | object | No | Metadata key-value pairs to add or overwrite on the vector | +| `apiKey` | string | Yes | Pinecone API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `statusText` | string | Status of the update operation | + +### `pinecone_delete_vectors` + +Delete vectors from a Pinecone namespace by IDs, by metadata filter, or delete all + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `indexHost` | string | Yes | Full Pinecone index host URL \(e.g., "https://my-index-abc123.svc.pinecone.io"\) | +| `namespace` | string | No | Namespace to delete vectors from \(e.g., "documents", "embeddings"\) | +| `ids` | array | No | Vector IDs to delete \(1-1000 items\). Mutually exclusive with deleteAll and filter | +| `deleteAll` | boolean | No | Delete all vectors in the namespace. Mutually exclusive with ids and filter | +| `filter` | object | No | Metadata filter selecting vectors to delete \(e.g., \{"category": \{"$eq": "product"\}\}\). Mutually exclusive with ids and deleteAll | +| `apiKey` | string | Yes | Pinecone API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `statusText` | string | Status of the delete operation | + ### `pinecone_search_text` Search for similar text in a Pinecone index @@ -157,4 +200,102 @@ Fetch vectors by ID from a Pinecone index | `usage` | object | Usage statistics including total read units | | ↳ `total_tokens` | number | Read units consumed | +### `pinecone_list_vector_ids` + +List vector IDs in a Pinecone namespace by prefix (serverless indexes only) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `indexHost` | string | Yes | Full Pinecone index host URL \(e.g., "https://my-index-abc123.svc.pinecone.io"\) | +| `namespace` | string | Yes | Namespace to list vector IDs from \(e.g., "documents", "embeddings"\) | +| `prefix` | string | No | Filter vector IDs by a common prefix \(e.g., "doc1#"\) | +| `limit` | number | No | Maximum number of IDs to return per page \(default 100\) | +| `paginationToken` | string | No | Pagination token from a previous response to fetch the next page | +| `apiKey` | string | Yes | Pinecone API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `vectorIds` | array | Vector IDs in the namespace | +| `pagination` | object | Pagination info with a next token when more results exist | +| ↳ `next` | string | Token to fetch the next page | +| `namespace` | string | Namespace the IDs were listed from | +| `usage` | object | Usage statistics including read units | +| ↳ `total_tokens` | number | Read units consumed | + +### `pinecone_describe_index_stats` + +Get statistics about a Pinecone index, including per-namespace vector counts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `indexHost` | string | Yes | Full Pinecone index host URL \(e.g., "https://my-index-abc123.svc.pinecone.io"\) | +| `filter` | object | No | Metadata filter to limit which vectors are counted \(pod-based indexes only, e.g., \{"category": \{"$eq": "product"\}\}\) | +| `apiKey` | string | Yes | Pinecone API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `namespaces` | json | Map of namespace name to its summary including vectorCount | +| `dimension` | number | Dimensionality of the indexed vectors | +| `indexFullness` | number | Fullness of the index \(pod-based indexes only\) | +| `totalVectorCount` | number | Total number of vectors across all namespaces | + +### `pinecone_list_indexes` + +List all Pinecone indexes in the project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Pinecone API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `indexes` | array | List of indexes with name, dimension, metric, host, spec, and status | +| ↳ `name` | string | Index name | +| ↳ `dimension` | number | Vector dimensionality | +| ↳ `metric` | string | Distance metric \(cosine, euclidean, dotproduct\) | +| ↳ `host` | string | Index host URL for data-plane operations | +| ↳ `vectorType` | string | Vector type \(dense or sparse\) | +| ↳ `deletionProtection` | string | Deletion protection \(enabled or disabled\) | +| ↳ `tags` | object | Custom user tags on the index | +| ↳ `spec` | object | Index spec \(serverless or pod configuration\) | +| ↳ `status` | object | Index status with ready and state | + +### `pinecone_describe_index` + +Get the configuration and status of a Pinecone index by name + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `indexName` | string | Yes | Name of the index to describe | +| `apiKey` | string | Yes | Pinecone API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `index` | object | Index configuration and status | +| ↳ `name` | string | Index name | +| ↳ `dimension` | number | Vector dimensionality | +| ↳ `metric` | string | Distance metric \(cosine, euclidean, dotproduct\) | +| ↳ `host` | string | Index host URL for data-plane operations | +| ↳ `vectorType` | string | Vector type \(dense or sparse\) | +| ↳ `deletionProtection` | string | Deletion protection \(enabled or disabled\) | +| ↳ `tags` | object | Custom user tags on the index | +| ↳ `spec` | object | Index spec \(serverless or pod configuration\) | +| ↳ `status` | object | Index status with ready and state | + diff --git a/apps/docs/content/docs/en/integrations/resend.mdx b/apps/docs/content/docs/en/integrations/resend.mdx index 53671593923..0adc6415400 100644 --- a/apps/docs/content/docs/en/integrations/resend.mdx +++ b/apps/docs/content/docs/en/integrations/resend.mdx @@ -94,6 +94,23 @@ Retrieve details of a previously sent email by its ID | ↳ `name` | string | Tag name | | ↳ `value` | string | Tag value | +### `resend_cancel_email` + +Cancel a scheduled email before it is sent + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `cancelEmailId` | string | Yes | The ID of the scheduled email to cancel | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Canceled email ID | + ### `resend_create_contact` Create a new contact in Resend @@ -197,6 +214,151 @@ Delete a contact from Resend by ID or email | `id` | string | Deleted contact ID | | `deleted` | boolean | Whether the contact was successfully deleted | +### `resend_create_audience` + +Create a new audience in Resend + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audienceName` | string | Yes | The name of the audience to create | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Created audience ID | +| `name` | string | Audience name | + +### `resend_get_audience` + +Retrieve details of an audience by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audienceId` | string | Yes | The ID of the audience to retrieve | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Audience ID | +| `name` | string | Audience name | +| `createdAt` | string | Audience creation timestamp | + +### `resend_list_audiences` + +List all audiences in Resend + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `audiences` | array | Array of audiences | +| ↳ `id` | string | Audience ID | +| ↳ `name` | string | Audience name | +| ↳ `created_at` | string | Audience creation timestamp | +| `hasMore` | boolean | Whether there are more audiences to retrieve | + +### `resend_delete_audience` + +Delete an audience from Resend by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audienceId` | string | Yes | The ID of the audience to delete | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Deleted audience ID | +| `deleted` | boolean | Whether the audience was successfully deleted | + +### `resend_create_broadcast` + +Create a broadcast email for an audience in Resend + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `audienceId` | string | Yes | The ID of the audience to send the broadcast to | +| `broadcastFrom` | string | Yes | Sender email address \(e.g., "sender@example.com" or "Sender Name <sender@example.com>"\) | +| `broadcastSubject` | string | Yes | Broadcast email subject line | +| `broadcastHtml` | string | No | HTML content of the broadcast | +| `broadcastText` | string | No | Plain text content of the broadcast | +| `broadcastReplyTo` | string | No | Reply-to email address | +| `broadcastName` | string | No | Friendly internal name for the broadcast | +| `broadcastPreviewText` | string | No | Preview text shown in the inbox before the email is opened | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Created broadcast ID | + +### `resend_send_broadcast` + +Send a broadcast immediately or schedule it for later + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `broadcastId` | string | Yes | The ID of the broadcast to send | +| `broadcastScheduledAt` | string | No | Schedule delivery in natural language \(e.g., "in 1 min"\) or ISO 8601 format. Sends immediately if omitted | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Broadcast ID | + +### `resend_get_broadcast` + +Retrieve details of a broadcast by ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `broadcastId` | string | Yes | The ID of the broadcast to retrieve | +| `resendApiKey` | string | Yes | Resend API key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Broadcast ID | +| `name` | string | Broadcast name | +| `audienceId` | string | Audience ID \(legacy\) | +| `segmentId` | string | Segment ID \(the current recipient field\) | +| `from` | string | Sender email address | +| `subject` | string | Broadcast subject | +| `replyTo` | string | Reply-to email address | +| `previewText` | string | Inbox preview text | +| `status` | string | Broadcast status \(e.g., draft, sent\) | +| `createdAt` | string | Broadcast creation timestamp | +| `scheduledAt` | string | Scheduled send timestamp | +| `sentAt` | string | Timestamp the broadcast was sent | + ### `resend_list_domains` List all verified domains in your Resend account diff --git a/apps/docs/content/docs/en/integrations/s3.mdx b/apps/docs/content/docs/en/integrations/s3.mdx index 0678a74bc1b..63f8429c0a6 100644 --- a/apps/docs/content/docs/en/integrations/s3.mdx +++ b/apps/docs/content/docs/en/integrations/s3.mdx @@ -1,6 +1,6 @@ --- title: S3 -description: Upload, download, list, and manage S3 files +description: Upload, download, list, and manage S3 files and buckets --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -29,7 +29,7 @@ In Sim, the S3 integration enables your agents to retrieve and access files stor ## Usage Instructions -Integrate S3 into the workflow. Upload files, download objects, list bucket contents, delete objects, and copy objects between buckets. Requires AWS access key and secret access key. +Integrate S3 into the workflow. Upload, download, copy, and delete objects (individually or in batches), inspect object metadata, generate time-limited presigned URLs, list bucket contents, and create, list, or delete buckets. Requires AWS access key and secret access key. @@ -155,4 +155,144 @@ Copy an object within or between AWS S3 buckets | `uri` | string | S3 URI of the copied object \(s3://bucket/key\) | | `metadata` | object | Copy operation metadata | +### `s3_list_buckets` + +List the S3 buckets owned by the authenticated AWS account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region to address the request to \(e.g., us-east-1\) | +| `prefix` | string | No | Limit the response to bucket names that begin with this prefix | +| `maxBuckets` | number | No | Maximum number of buckets to return \(1-10000\) | +| `continuationToken` | string | No | Token for pagination from a previous list buckets response | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `buckets` | array | List of S3 buckets owned by the account | +| ↳ `name` | string | Bucket name | +| ↳ `creationDate` | string | Bucket creation timestamp | +| ↳ `region` | string | AWS region where the bucket is located | +| `metadata` | object | Listing metadata including owner and pagination info | + +### `s3_head_object` + +Retrieve metadata for an S3 object without downloading its body + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | +| `objectKey` | string | Yes | Object key/path to inspect \(e.g., folder/file.txt\) | +| `versionId` | string | No | Specific object version ID to inspect \(for versioned buckets\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `exists` | boolean | Whether the object exists and was reachable | +| `metadata` | object | Object metadata including size, content type, ETag, and last modified date | + +### `s3_create_bucket` + +Create a new AWS S3 bucket in the specified region + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region to create the bucket in \(e.g., us-east-1\) | +| `bucketName` | string | Yes | Name for the new S3 bucket \(must be globally unique\) | +| `acl` | string | No | Canned ACL for the bucket \(e.g., private, public-read\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `metadata` | object | Created bucket metadata including name and location | + +### `s3_delete_bucket` + +Delete an empty AWS S3 bucket + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region where the bucket is located \(e.g., us-east-1\) | +| `bucketName` | string | Yes | Name of the S3 bucket to delete \(must be empty\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the bucket was successfully deleted | +| `metadata` | object | Deletion metadata including bucket name | + +### `s3_presigned_url` + +Generate a time-limited presigned URL to download or upload an S3 object + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region where the bucket is located \(e.g., us-east-1\) | +| `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | +| `objectKey` | string | Yes | Object key/path for the presigned URL \(e.g., folder/file.txt\) | +| `method` | string | Yes | Operation the URL grants: get \(download\) or put \(upload\) | +| `expiresIn` | number | No | URL validity in seconds \(1-604800, default 3600\) | +| `contentType` | string | No | Content-Type the upload must use \(only applies to put URLs\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `url` | string | The generated presigned URL | +| `metadata` | object | Presigned URL metadata including method and expiration | + +### `s3_delete_objects` + +Delete multiple objects from an AWS S3 bucket in a single batch request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | Your AWS Access Key ID | +| `secretAccessKey` | string | Yes | Your AWS Secret Access Key | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `bucketName` | string | Yes | S3 bucket name \(e.g., my-bucket\) | +| `keys` | json | Yes | Array of object keys to delete \(e.g., \["a.txt", "folder/b.txt"\]\). Max 1000. | +| `quiet` | boolean | No | Return only deletion errors, omitting successfully deleted keys | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | array | Objects that were successfully deleted | +| ↳ `key` | string | Deleted object key | +| ↳ `versionId` | string | Version ID of the deleted object | +| ↳ `deleteMarker` | boolean | Whether a delete marker was created | +| `errors` | array | Objects that failed to delete | +| ↳ `key` | string | Object key that failed | +| ↳ `code` | string | Error code | +| ↳ `message` | string | Error message | +| `metadata` | object | Batch deletion summary including counts | + diff --git a/apps/sim/app/api/tools/elevenlabs/audio/route.ts b/apps/sim/app/api/tools/elevenlabs/audio/route.ts new file mode 100644 index 00000000000..f4f83f399a5 --- /dev/null +++ b/apps/sim/app/api/tools/elevenlabs/audio/route.ts @@ -0,0 +1,209 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' +import type { NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { elevenLabsAudioToolContract } from '@/lib/api/contracts/tools/media/elevenlabs' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { + isPayloadSizeLimitError, + readResponseToBufferWithLimit, +} from '@/lib/core/utils/stream-limits' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { StorageService } from '@/lib/uploads' +import { getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' + +const logger = createLogger('ElevenLabsAudioAPI') +const MAX_AUDIO_BYTES = 25 * 1024 * 1024 +const BASE_URL = 'https://api.elevenlabs.io/v1' + +type AudioOperation = 'sound_effects' | 'speech_to_speech' | 'audio_isolation' + +interface SourceAudio { + buffer: Buffer + fileName: string + mimeType: string +} + +/** Builds the upstream ElevenLabs request for an audio-producing operation. */ +function buildElevenLabsRequest( + operation: AudioOperation, + body: { + apiKey: string + voiceId?: string + text?: string + modelId?: string + durationSeconds?: number + promptInfluence?: number + loop?: boolean + removeBackgroundNoise?: boolean + }, + source: SourceAudio | null +): { url: string; init: RequestInit } { + const headers: Record = { 'xi-api-key': body.apiKey, Accept: 'audio/mpeg' } + const signal = AbortSignal.timeout(DEFAULT_EXECUTION_TIMEOUT_MS) + + if (operation === 'sound_effects') { + const payload: Record = { text: body.text } + if (body.modelId) payload.model_id = body.modelId + if (body.durationSeconds !== undefined) payload.duration_seconds = body.durationSeconds + if (body.promptInfluence !== undefined) payload.prompt_influence = body.promptInfluence + if (body.loop !== undefined) payload.loop = body.loop + return { + url: `${BASE_URL}/sound-generation`, + init: { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal, + }, + } + } + + const formData = new FormData() + const file = source as SourceAudio + formData.append( + 'audio', + new Blob([new Uint8Array(file.buffer)], { type: file.mimeType }), + file.fileName + ) + + if (operation === 'speech_to_speech') { + if (body.modelId) formData.append('model_id', body.modelId) + if (body.removeBackgroundNoise !== undefined) { + formData.append('remove_background_noise', String(body.removeBackgroundNoise)) + } + return { + url: `${BASE_URL}/speech-to-speech/${body.voiceId}`, + init: { method: 'POST', headers, body: formData, signal }, + } + } + + return { + url: `${BASE_URL}/audio-isolation`, + init: { method: 'POST', headers, body: formData, signal }, + } +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateId() + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + const userId = authResult.userId + + const parsed = await parseRequest( + elevenLabsAudioToolContract, + request, + {}, + { + validationErrorResponse: (error) => + NextResponse.json( + { error: getValidationErrorMessage(error, 'Missing required parameters') }, + { status: 400 } + ), + } + ) + if (!parsed.success) return parsed.response + + const body = parsed.data.body + const operation = body.operation as AudioOperation + + if (operation === 'sound_effects' && !body.text) { + return NextResponse.json({ error: 'text is required' }, { status: 400 }) + } + + let source: SourceAudio | null = null + if (operation === 'speech_to_speech' || operation === 'audio_isolation') { + if (!body.audioFile) { + return NextResponse.json({ error: 'audioFile is required' }, { status: 400 }) + } + const file = body.audioFile + const denied = await assertToolFileAccess(file.key, userId, requestId, logger) + if (denied) return denied + const buffer = await downloadFileFromStorage(file, requestId, logger) + const ext = file.name.split('.').pop()?.toLowerCase() || '' + source = { + buffer, + fileName: file.name, + mimeType: file.type || getMimeTypeFromExtension(ext), + } + } + + if (operation === 'speech_to_speech') { + if (!body.voiceId) { + return NextResponse.json({ error: 'voiceId is required' }, { status: 400 }) + } + const voiceIdValidation = validateAlphanumericId(body.voiceId, 'voiceId', 255) + if (!voiceIdValidation.isValid) { + return NextResponse.json({ error: voiceIdValidation.error }, { status: 400 }) + } + } + + const { url, init } = buildElevenLabsRequest(operation, body, source) + const response = await fetch(url, init) + + if (!response.ok) { + const errorBody = await response.text().catch(() => '') + logger.error(`[${requestId}] ElevenLabs ${operation} failed: ${response.status}`, errorBody) + return NextResponse.json( + { error: `ElevenLabs request failed: ${response.status} ${response.statusText}` }, + { status: response.status } + ) + } + + const outputBuffer = await readResponseToBufferWithLimit(response, { + maxBytes: MAX_AUDIO_BYTES, + label: `ElevenLabs ${operation} response`, + signal: request.signal, + }) + + if (outputBuffer.length === 0) { + return NextResponse.json({ error: 'Empty audio received' }, { status: 422 }) + } + + const fileName = `elevenlabs-${operation}-${Date.now()}.mp3` + const executionContext = + body.workspaceId && body.workflowId && body.executionId + ? { + workspaceId: body.workspaceId, + workflowId: body.workflowId, + executionId: body.executionId, + } + : null + + if (executionContext) { + const { uploadExecutionFile } = await import('@/lib/uploads/contexts/execution') + const userFile = await uploadExecutionFile( + executionContext, + outputBuffer, + fileName, + 'audio/mpeg', + userId + ) + return NextResponse.json({ audioFile: userFile, audioUrl: userFile.url }) + } + + const fileInfo = await StorageService.uploadFile({ + file: outputBuffer, + fileName, + contentType: 'audio/mpeg', + context: 'copilot', + }) + return NextResponse.json({ audioUrl: `${getBaseUrl()}${fileInfo.path}`, size: fileInfo.size }) + } catch (error) { + logger.error(`[${requestId}] ElevenLabs audio proxy error:`, error) + return NextResponse.json( + { error: `Internal Server Error: ${getErrorMessage(error, 'Unknown error')}` }, + { status: isPayloadSizeLimitError(error) ? 413 : 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/google_drive/export/route.ts b/apps/sim/app/api/tools/google_drive/export/route.ts new file mode 100644 index 00000000000..40b48057def --- /dev/null +++ b/apps/sim/app/api/tools/google_drive/export/route.ts @@ -0,0 +1,209 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { googleDriveExportContract } from '@/lib/api/contracts/tools/google' +import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import type { GoogleDriveFile } from '@/tools/google_drive/types' +import { + ALL_FILE_FIELDS, + GOOGLE_WORKSPACE_MIME_TYPES, + MAX_EXPORT_BYTES, + VALID_EXPORT_FORMATS, +} from '@/tools/google_drive/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('GoogleDriveExportAPI') + +/** Google API error response structure */ +interface GoogleApiErrorResponse { + error?: { + message?: string + code?: number + status?: string + } +} + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized Google Drive export attempt: ${authResult.error}`) + return NextResponse.json( + { success: false, error: authResult.error || 'Authentication required' }, + { status: 401 } + ) + } + + const parsed = await parseRequest( + googleDriveExportContract, + request, + {}, + { + validationErrorResponse: (error) => + NextResponse.json( + { success: false, error: getValidationErrorMessage(error, 'Invalid request') }, + { status: 400 } + ), + } + ) + if (!parsed.success) return parsed.response + + const { accessToken, fileId, mimeType: exportMimeType, fileName } = parsed.data.body + const authHeader = `Bearer ${accessToken}` + + logger.info(`[${requestId}] Getting file metadata from Google Drive`, { fileId }) + + const metadataUrl = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true` + const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl') + if (!metadataUrlValidation.isValid) { + return NextResponse.json( + { success: false, error: metadataUrlValidation.error }, + { status: 400 } + ) + } + + const metadataResponse = await secureFetchWithPinnedIP( + metadataUrl, + metadataUrlValidation.resolvedIP!, + { headers: { Authorization: authHeader } } + ) + + if (!metadataResponse.ok) { + const errorDetails = (await metadataResponse + .json() + .catch(() => ({}))) as GoogleApiErrorResponse + logger.error(`[${requestId}] Failed to get file metadata`, { + status: metadataResponse.status, + error: errorDetails, + }) + return NextResponse.json( + { success: false, error: errorDetails.error?.message || 'Failed to get file metadata' }, + { status: 400 } + ) + } + + const metadata = (await metadataResponse.json()) as GoogleDriveFile + const fileMimeType = metadata.mimeType + + if (!GOOGLE_WORKSPACE_MIME_TYPES.includes(fileMimeType)) { + return NextResponse.json( + { + success: false, + error: `Export only supports Google Workspace files (Docs, Sheets, Slides, Drawings). This file is "${fileMimeType}" — use the Download operation instead.`, + }, + { status: 400 } + ) + } + + const validFormats = VALID_EXPORT_FORMATS[fileMimeType] + if (validFormats && !validFormats.includes(exportMimeType)) { + return NextResponse.json( + { + success: false, + error: `Export format "${exportMimeType}" is not supported for this file type. Supported formats: ${validFormats.join(', ')}`, + }, + { status: 400 } + ) + } + + logger.info(`[${requestId}] Exporting Google Workspace file`, { + fileId, + mimeType: fileMimeType, + exportFormat: exportMimeType, + }) + + const exportUrl = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(exportMimeType)}` + const exportUrlValidation = await validateUrlWithDNS(exportUrl, 'exportUrl') + if (!exportUrlValidation.isValid) { + return NextResponse.json( + { success: false, error: exportUrlValidation.error }, + { status: 400 } + ) + } + + const exportResponse = await secureFetchWithPinnedIP( + exportUrl, + exportUrlValidation.resolvedIP!, + { + headers: { Authorization: authHeader }, + } + ) + + if (!exportResponse.ok) { + const exportError = (await exportResponse.json().catch(() => ({}))) as GoogleApiErrorResponse + logger.error(`[${requestId}] Failed to export file`, { + status: exportResponse.status, + error: exportError, + }) + return NextResponse.json( + { + success: false, + error: exportError.error?.message || 'Failed to export Google Workspace file', + }, + { status: 400 } + ) + } + + const declaredSize = Number(exportResponse.headers.get('content-length')) + if (Number.isFinite(declaredSize) && declaredSize > MAX_EXPORT_BYTES) { + return NextResponse.json( + { + success: false, + error: `Exported content (${declaredSize} bytes) exceeds the ${MAX_EXPORT_BYTES}-byte export limit.`, + }, + { status: 400 } + ) + } + + const arrayBuffer = await exportResponse.arrayBuffer() + if (arrayBuffer.byteLength > MAX_EXPORT_BYTES) { + return NextResponse.json( + { + success: false, + error: `Exported content (${arrayBuffer.byteLength} bytes) exceeds the ${MAX_EXPORT_BYTES}-byte export limit.`, + }, + { status: 400 } + ) + } + const fileBuffer = Buffer.from(arrayBuffer) + + const resolvedName = fileName || metadata.name || 'export' + + logger.info(`[${requestId}] File exported successfully`, { + fileId, + name: resolvedName, + size: fileBuffer.length, + mimeType: exportMimeType, + }) + + return NextResponse.json({ + success: true, + output: { + file: { + name: resolvedName, + mimeType: exportMimeType, + data: fileBuffer.toString('base64'), + size: fileBuffer.length, + }, + exportedMimeType: exportMimeType, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error exporting Google Drive file:`, error) + return NextResponse.json( + { success: false, error: getErrorMessage(error, 'Unknown error occurred') }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/s3/create-bucket/route.ts b/apps/sim/app/api/tools/s3/create-bucket/route.ts new file mode 100644 index 00000000000..055ee8bf509 --- /dev/null +++ b/apps/sim/app/api/tools/s3/create-bucket/route.ts @@ -0,0 +1,99 @@ +import { + type BucketCannedACL, + type BucketLocationConstraint, + CreateBucketCommand, + S3Client, +} from '@aws-sdk/client-s3' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsS3CreateBucketContract } from '@/lib/api/contracts/tools/aws/s3-create-bucket' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3CreateBucketAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 create bucket attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated S3 create bucket request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const parsed = await parseToolRequest(awsS3CreateBucketContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`[${requestId}] Creating S3 bucket`, { + bucket: validatedData.bucketName, + region: validatedData.region, + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const createCommand = new CreateBucketCommand({ + Bucket: validatedData.bucketName, + ACL: (validatedData.acl as BucketCannedACL | undefined) || undefined, + CreateBucketConfiguration: + validatedData.region === 'us-east-1' + ? undefined + : { LocationConstraint: validatedData.region as BucketLocationConstraint }, + }) + + const result = await s3Client.send(createCommand) + + logger.info(`[${requestId}] Bucket created successfully`, { + bucket: validatedData.bucketName, + location: result.Location, + }) + + return NextResponse.json({ + success: true, + output: { + bucket: validatedData.bucketName, + location: result.Location ?? null, + bucketArn: result.BucketArn ?? null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error creating S3 bucket:`, error) + + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Internal server error'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/s3/delete-bucket/route.ts b/apps/sim/app/api/tools/s3/delete-bucket/route.ts new file mode 100644 index 00000000000..c3d89e92cd2 --- /dev/null +++ b/apps/sim/app/api/tools/s3/delete-bucket/route.ts @@ -0,0 +1,86 @@ +import { DeleteBucketCommand, S3Client } from '@aws-sdk/client-s3' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsS3DeleteBucketContract } from '@/lib/api/contracts/tools/aws/s3-delete-bucket' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3DeleteBucketAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 delete bucket attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated S3 delete bucket request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const parsed = await parseToolRequest(awsS3DeleteBucketContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`[${requestId}] Deleting S3 bucket`, { + bucket: validatedData.bucketName, + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const deleteCommand = new DeleteBucketCommand({ + Bucket: validatedData.bucketName, + }) + + await s3Client.send(deleteCommand) + + logger.info(`[${requestId}] Bucket deleted successfully`, { + bucket: validatedData.bucketName, + }) + + return NextResponse.json({ + success: true, + output: { + deleted: true, + bucket: validatedData.bucketName, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error deleting S3 bucket:`, error) + + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Internal server error'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/s3/delete-objects/route.ts b/apps/sim/app/api/tools/s3/delete-objects/route.ts new file mode 100644 index 00000000000..f8b74278427 --- /dev/null +++ b/apps/sim/app/api/tools/s3/delete-objects/route.ts @@ -0,0 +1,105 @@ +import { DeleteObjectsCommand, S3Client } from '@aws-sdk/client-s3' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsS3DeleteObjectsContract } from '@/lib/api/contracts/tools/aws/s3-delete-objects' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3DeleteObjectsAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 delete objects attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated S3 delete objects request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const parsed = await parseToolRequest(awsS3DeleteObjectsContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`[${requestId}] Deleting S3 objects`, { + bucket: validatedData.bucketName, + count: validatedData.keys.length, + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const deleteCommand = new DeleteObjectsCommand({ + Bucket: validatedData.bucketName, + Delete: { + Objects: validatedData.keys.map((key) => ({ Key: key })), + Quiet: validatedData.quiet ?? false, + }, + }) + + const result = await s3Client.send(deleteCommand) + + const deleted = (result.Deleted || []).map((obj) => ({ + key: obj.Key ?? null, + versionId: obj.VersionId ?? null, + deleteMarker: obj.DeleteMarker ?? null, + })) + + const errors = (result.Errors || []).map((err) => ({ + key: err.Key ?? null, + code: err.Code ?? null, + message: err.Message ?? null, + })) + + logger.info(`[${requestId}] Delete objects completed`, { + bucket: validatedData.bucketName, + deleted: deleted.length, + errors: errors.length, + }) + + return NextResponse.json({ + success: true, + output: { + deleted, + errors, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error deleting S3 objects:`, error) + + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Internal server error'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/s3/head-object/route.ts b/apps/sim/app/api/tools/s3/head-object/route.ts new file mode 100644 index 00000000000..898a29c7167 --- /dev/null +++ b/apps/sim/app/api/tools/s3/head-object/route.ts @@ -0,0 +1,114 @@ +import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsS3HeadObjectContract } from '@/lib/api/contracts/tools/aws/s3-head-object' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3HeadObjectAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 head object attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated S3 head object request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const parsed = await parseToolRequest(awsS3HeadObjectContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`[${requestId}] Fetching S3 object metadata`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const headCommand = new HeadObjectCommand({ + Bucket: validatedData.bucketName, + Key: validatedData.objectKey, + VersionId: validatedData.versionId || undefined, + }) + + const result = await s3Client.send(headCommand) + + logger.info(`[${requestId}] Object metadata retrieved`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + }) + + return NextResponse.json({ + success: true, + output: { + exists: true, + contentLength: result.ContentLength ?? null, + contentType: result.ContentType ?? null, + etag: result.ETag ?? null, + lastModified: result.LastModified?.toISOString() ?? null, + versionId: result.VersionId ?? null, + storageClass: result.StorageClass ?? null, + serverSideEncryption: result.ServerSideEncryption ?? null, + deleteMarker: result.DeleteMarker ?? null, + metadata: result.Metadata ?? {}, + }, + }) + } catch (error) { + const metadata = error as { name?: string; $metadata?: { httpStatusCode?: number } } + if (metadata?.name === 'NotFound' || metadata?.$metadata?.httpStatusCode === 404) { + return NextResponse.json({ + success: true, + output: { + exists: false, + contentLength: null, + contentType: null, + etag: null, + lastModified: null, + versionId: null, + storageClass: null, + serverSideEncryption: null, + deleteMarker: null, + metadata: {}, + }, + }) + } + + logger.error(`[${requestId}] Error fetching S3 object metadata:`, error) + + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Internal server error'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/s3/list-buckets/route.ts b/apps/sim/app/api/tools/s3/list-buckets/route.ts new file mode 100644 index 00000000000..1a951bf5632 --- /dev/null +++ b/apps/sim/app/api/tools/s3/list-buckets/route.ts @@ -0,0 +1,97 @@ +import { ListBucketsCommand, S3Client } from '@aws-sdk/client-s3' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsS3ListBucketsContract } from '@/lib/api/contracts/tools/aws/s3-list-buckets' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3ListBucketsAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 list buckets attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info(`[${requestId}] Authenticated S3 list buckets request via ${authResult.authType}`, { + userId: authResult.userId, + }) + + const parsed = await parseToolRequest(awsS3ListBucketsContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`[${requestId}] Listing S3 buckets`, { + prefix: validatedData.prefix || '(none)', + maxBuckets: validatedData.maxBuckets || '(all)', + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const listCommand = new ListBucketsCommand({ + Prefix: validatedData.prefix || undefined, + MaxBuckets: validatedData.maxBuckets || undefined, + ContinuationToken: validatedData.continuationToken || undefined, + }) + + const result = await s3Client.send(listCommand) + + const buckets = (result.Buckets || []).map((bucket) => ({ + name: bucket.Name || '', + creationDate: bucket.CreationDate?.toISOString() ?? null, + region: bucket.BucketRegion ?? null, + })) + + logger.info(`[${requestId}] Listed ${buckets.length} buckets`) + + return NextResponse.json({ + success: true, + output: { + buckets, + owner: result.Owner + ? { + displayName: result.Owner.DisplayName ?? null, + id: result.Owner.ID ?? null, + } + : null, + continuationToken: result.ContinuationToken ?? null, + prefix: result.Prefix ?? null, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error listing S3 buckets:`, error) + + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Internal server error'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/s3/presigned-url/route.ts b/apps/sim/app/api/tools/s3/presigned-url/route.ts new file mode 100644 index 00000000000..72b443d974d --- /dev/null +++ b/apps/sim/app/api/tools/s3/presigned-url/route.ts @@ -0,0 +1,105 @@ +import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3' +import { getSignedUrl } from '@aws-sdk/s3-request-presigner' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsS3PresignedUrlContract } from '@/lib/api/contracts/tools/aws/s3-presigned-url' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('S3PresignedUrlAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized S3 presigned URL attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + logger.info( + `[${requestId}] Authenticated S3 presigned URL request via ${authResult.authType}`, + { + userId: authResult.userId, + } + ) + + const parsed = await parseToolRequest(awsS3PresignedUrlContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`[${requestId}] Generating S3 presigned URL`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + method: validatedData.method, + expiresIn: validatedData.expiresIn, + }) + + const s3Client = new S3Client({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const command = + validatedData.method === 'put' + ? new PutObjectCommand({ + Bucket: validatedData.bucketName, + Key: validatedData.objectKey, + ContentType: validatedData.contentType || undefined, + }) + : new GetObjectCommand({ + Bucket: validatedData.bucketName, + Key: validatedData.objectKey, + }) + + const url = await getSignedUrl(s3Client, command, { + expiresIn: validatedData.expiresIn, + }) + + const expiresAt = new Date(Date.now() + validatedData.expiresIn * 1000).toISOString() + + logger.info(`[${requestId}] Presigned URL generated`, { + bucket: validatedData.bucketName, + key: validatedData.objectKey, + }) + + return NextResponse.json({ + success: true, + output: { + url, + method: validatedData.method, + expiresIn: validatedData.expiresIn, + expiresAt, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error generating S3 presigned URL:`, error) + + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Internal server error'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/elevenlabs.ts b/apps/sim/blocks/blocks/elevenlabs.ts index 20b564007a1..b981d3f94ff 100644 --- a/apps/sim/blocks/blocks/elevenlabs.ts +++ b/apps/sim/blocks/blocks/elevenlabs.ts @@ -1,13 +1,36 @@ import { ElevenLabsIcon } from '@/components/icons' import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types' +import { normalizeFileInput } from '@/blocks/utils' import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types' +const VOICE_OPERATIONS = [ + 'tts', + 'speech_to_speech', + 'get_voice', + 'get_voice_settings', + 'edit_voice_settings', +] +const AUDIO_INPUT_OPERATIONS = ['speech_to_speech', 'audio_isolation'] + +const toNumber = (value: unknown): number | undefined => { + if (value === undefined || value === null || value === '') return undefined + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +const toBoolean = (value: unknown): boolean | undefined => { + if (value === undefined || value === null || value === '') return undefined + if (typeof value === 'boolean') return value + return String(value).toLowerCase() === 'true' +} + export const ElevenLabsBlock: BlockConfig = { type: 'elevenlabs', name: 'ElevenLabs', - description: 'Convert text to speech with ElevenLabs', + description: 'Generate and transform audio with ElevenLabs', authMode: AuthMode.ApiKey, - longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.', + longDescription: + 'Integrate ElevenLabs into the workflow. Convert text to speech, generate sound effects, transform voices, isolate audio, and manage voices, models, and account settings.', docsLink: 'https://docs.sim.ai/integrations/elevenlabs', category: 'tools', integrationType: IntegrationType.AI, @@ -15,20 +38,63 @@ export const ElevenLabsBlock: BlockConfig = { icon: ElevenLabsIcon, subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Text to Speech', id: 'tts' }, + { label: 'Sound Effects', id: 'sound_effects' }, + { label: 'Speech to Speech', id: 'speech_to_speech' }, + { label: 'Audio Isolation', id: 'audio_isolation' }, + { label: 'List Voices', id: 'list_voices' }, + { label: 'Get Voice', id: 'get_voice' }, + { label: 'Get Voice Settings', id: 'get_voice_settings' }, + { label: 'Edit Voice Settings', id: 'edit_voice_settings' }, + { label: 'List Models', id: 'list_models' }, + { label: 'Get User Info', id: 'get_user' }, + ], + value: () => 'tts', + required: true, + }, + { id: 'text', title: 'Text', type: 'long-input', placeholder: 'Enter the text to convert to speech', - required: true, + condition: { field: 'operation', value: 'tts' }, + required: { field: 'operation', value: 'tts' }, }, + { + id: 'text', + title: 'Sound Prompt', + type: 'long-input', + placeholder: 'Describe the sound effect (e.g., "thunder rumbling in the distance")', + condition: { field: 'operation', value: 'sound_effects' }, + required: { field: 'operation', value: 'sound_effects' }, + }, + { id: 'voiceId', title: 'Voice ID', type: 'short-input', placeholder: 'Enter the voice ID', - required: true, + condition: { field: 'operation', value: VOICE_OPERATIONS }, + required: { field: 'operation', value: VOICE_OPERATIONS }, + }, + + { + id: 'audioFile', + title: 'Audio File', + type: 'file-upload', + placeholder: 'Upload an audio file', + multiple: false, + acceptedTypes: '.mp3,.m4a,.wav,.webm,.ogg,.flac,.aac,.opus', + condition: { field: 'operation', value: AUDIO_INPUT_OPERATIONS }, + required: { field: 'operation', value: AUDIO_INPUT_OPERATIONS }, }, + { id: 'modelId', title: 'Model ID', @@ -42,13 +108,35 @@ export const ElevenLabsBlock: BlockConfig = { { label: 'eleven_v3', id: 'eleven_v3' }, ], value: () => 'eleven_monolingual_v1', + condition: { field: 'operation', value: 'tts' }, }, + { + id: 'modelId', + title: 'Model ID', + type: 'dropdown', + options: [{ label: 'eleven_text_to_sound_v2', id: 'eleven_text_to_sound_v2' }], + value: () => 'eleven_text_to_sound_v2', + condition: { field: 'operation', value: 'sound_effects' }, + }, + { + id: 'modelId', + title: 'Model ID', + type: 'dropdown', + options: [ + { label: 'eleven_english_sts_v2', id: 'eleven_english_sts_v2' }, + { label: 'eleven_multilingual_sts_v2', id: 'eleven_multilingual_sts_v2' }, + ], + value: () => 'eleven_english_sts_v2', + condition: { field: 'operation', value: 'speech_to_speech' }, + }, + { id: 'stability', title: 'Stability', type: 'short-input', placeholder: '0.0 to 1.0 (e.g., 0.5)', mode: 'advanced', + condition: { field: 'operation', value: 'tts' }, }, { id: 'similarityBoost', @@ -56,7 +144,118 @@ export const ElevenLabsBlock: BlockConfig = { type: 'short-input', placeholder: '0.0 to 1.0 (e.g., 0.75)', mode: 'advanced', + condition: { field: 'operation', value: 'tts' }, + }, + + { + id: 'durationSeconds', + title: 'Duration (seconds)', + type: 'short-input', + placeholder: '0.5 to 30 (leave empty to auto-determine)', + mode: 'advanced', + condition: { field: 'operation', value: 'sound_effects' }, + }, + { + id: 'promptInfluence', + title: 'Prompt Influence', + type: 'short-input', + placeholder: '0.0 to 1.0 (e.g., 0.3)', + mode: 'advanced', + condition: { field: 'operation', value: 'sound_effects' }, + }, + { + id: 'loop', + title: 'Loop', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'sound_effects' }, + }, + + { + id: 'removeBackgroundNoise', + title: 'Remove Background Noise', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'speech_to_speech' }, + }, + + { + id: 'editStability', + title: 'Stability', + type: 'short-input', + placeholder: '0.0 to 1.0 (e.g., 0.5)', + condition: { field: 'operation', value: 'edit_voice_settings' }, + }, + { + id: 'editSimilarityBoost', + title: 'Similarity Boost', + type: 'short-input', + placeholder: '0.0 to 1.0 (e.g., 0.75)', + condition: { field: 'operation', value: 'edit_voice_settings' }, + }, + { + id: 'editStyle', + title: 'Style', + type: 'short-input', + placeholder: '0.0 to 1.0 (e.g., 0.0)', + mode: 'advanced', + condition: { field: 'operation', value: 'edit_voice_settings' }, + }, + { + id: 'editSpeed', + title: 'Speed', + type: 'short-input', + placeholder: '1.0 = normal', + mode: 'advanced', + condition: { field: 'operation', value: 'edit_voice_settings' }, + }, + { + id: 'editUseSpeakerBoost', + title: 'Use Speaker Boost', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'edit_voice_settings' }, }, + + { + id: 'search', + title: 'Search', + type: 'short-input', + placeholder: 'Filter voices by name, description, labels, or category', + condition: { field: 'operation', value: 'list_voices' }, + }, + { + id: 'category', + title: 'Category', + type: 'dropdown', + options: [ + { label: 'Any', id: '' }, + { label: 'Premade', id: 'premade' }, + { label: 'Cloned', id: 'cloned' }, + { label: 'Generated', id: 'generated' }, + { label: 'Professional', id: 'professional' }, + ], + value: () => '', + mode: 'advanced', + condition: { field: 'operation', value: 'list_voices' }, + }, + { + id: 'pageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '1 to 100 (default 10)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_voices' }, + }, + { + id: 'nextPageToken', + title: 'Next Page Token', + type: 'short-input', + placeholder: 'Token from a previous response to fetch the next page', + mode: 'advanced', + condition: { field: 'operation', value: 'list_voices' }, + }, + { id: 'apiKey', title: 'API Key', @@ -68,39 +267,212 @@ export const ElevenLabsBlock: BlockConfig = { ], tools: { - access: ['elevenlabs_tts'], + access: [ + 'elevenlabs_tts', + 'elevenlabs_sound_effects', + 'elevenlabs_speech_to_speech', + 'elevenlabs_audio_isolation', + 'elevenlabs_list_voices', + 'elevenlabs_get_voice', + 'elevenlabs_get_voice_settings', + 'elevenlabs_edit_voice_settings', + 'elevenlabs_list_models', + 'elevenlabs_get_user', + ], config: { - tool: () => 'elevenlabs_tts', + tool: (params) => `elevenlabs_${params.operation || 'tts'}`, params: (params) => { - const parseUnitInterval = (value: unknown): number | undefined => { - if (value === undefined || value === null || value === '') return undefined - const n = Number(value) - return Number.isFinite(n) ? n : undefined - } + const audioFile = normalizeFileInput(params.audioFile, { single: true }) return { apiKey: params.apiKey, text: params.text, voiceId: params.voiceId, modelId: params.modelId, - stability: parseUnitInterval(params.stability), - similarityBoost: parseUnitInterval(params.similarityBoost), + audioFile, + search: params.search, + category: params.category || undefined, + nextPageToken: params.nextPageToken || undefined, + pageSize: toNumber(params.pageSize), + stability: toNumber( + params.operation === 'edit_voice_settings' ? params.editStability : params.stability + ), + similarityBoost: toNumber( + params.operation === 'edit_voice_settings' + ? params.editSimilarityBoost + : params.similarityBoost + ), + style: toNumber(params.editStyle), + speed: toNumber(params.editSpeed), + useSpeakerBoost: toBoolean(params.editUseSpeakerBoost), + durationSeconds: toNumber(params.durationSeconds), + promptInfluence: toNumber(params.promptInfluence), + loop: toBoolean(params.loop), + removeBackgroundNoise: toBoolean(params.removeBackgroundNoise), } }, }, }, inputs: { - text: { type: 'string', description: 'Text to convert' }, + operation: { type: 'string', description: 'Operation to perform' }, + text: { type: 'string', description: 'Text to convert or sound prompt' }, voiceId: { type: 'string', description: 'Voice identifier' }, + audioFile: { type: 'json', description: 'Source audio file (UserFile)' }, modelId: { type: 'string', description: 'Model identifier' }, stability: { type: 'number', description: 'Voice stability 0.0-1.0' }, similarityBoost: { type: 'number', description: 'Similarity boost 0.0-1.0' }, + durationSeconds: { type: 'number', description: 'Sound effect length in seconds (0.5-30)' }, + promptInfluence: { type: 'number', description: 'Sound prompt influence 0.0-1.0' }, + loop: { type: 'boolean', description: 'Generate a seamlessly looping sound effect' }, + removeBackgroundNoise: { type: 'boolean', description: 'Isolate the voice during conversion' }, + editStability: { type: 'number', description: 'Voice stability to set 0.0-1.0' }, + editSimilarityBoost: { type: 'number', description: 'Similarity boost to set 0.0-1.0' }, + editStyle: { type: 'number', description: 'Style exaggeration to set 0.0-1.0' }, + editSpeed: { type: 'number', description: 'Speech speed to set (1.0 = normal)' }, + editUseSpeakerBoost: { type: 'boolean', description: 'Enable speaker boost' }, + search: { type: 'string', description: 'Voice search filter' }, + category: { type: 'string', description: 'Voice category filter' }, + pageSize: { type: 'number', description: 'Number of voices to return (1-100)' }, apiKey: { type: 'string', description: 'ElevenLabs API key' }, }, outputs: { - audioUrl: { type: 'string', description: 'Generated audio URL' }, - audioFile: { type: 'file', description: 'Generated audio file' }, + audioUrl: { + type: 'string', + description: 'Generated audio URL', + condition: { + field: 'operation', + value: ['tts', 'sound_effects', 'speech_to_speech', 'audio_isolation'], + }, + }, + audioFile: { + type: 'file', + description: 'Generated audio file', + condition: { + field: 'operation', + value: ['tts', 'sound_effects', 'speech_to_speech', 'audio_isolation'], + }, + }, + voices: { + type: 'array', + description: 'List of voices', + condition: { field: 'operation', value: 'list_voices' }, + }, + totalCount: { + type: 'number', + description: 'Total number of matching voices', + condition: { field: 'operation', value: 'list_voices' }, + }, + hasMore: { + type: 'boolean', + description: 'Whether more voices are available', + condition: { field: 'operation', value: 'list_voices' }, + }, + nextPageToken: { + type: 'string', + description: 'Token to fetch the next page', + condition: { field: 'operation', value: 'list_voices' }, + }, + voiceId: { + type: 'string', + description: 'Voice identifier', + condition: { field: 'operation', value: 'get_voice' }, + }, + name: { + type: 'string', + description: 'Voice name', + condition: { field: 'operation', value: 'get_voice' }, + }, + category: { + type: 'string', + description: 'Voice category', + condition: { field: 'operation', value: 'get_voice' }, + }, + description: { + type: 'string', + description: 'Voice description', + condition: { field: 'operation', value: 'get_voice' }, + }, + labels: { + type: 'json', + description: 'Voice labels', + condition: { field: 'operation', value: 'get_voice' }, + }, + previewUrl: { + type: 'string', + description: 'Preview audio URL', + condition: { field: 'operation', value: 'get_voice' }, + }, + settings: { + type: 'json', + description: 'Voice settings', + condition: { field: 'operation', value: 'get_voice' }, + }, + availableForTiers: { + type: 'array', + description: 'Subscription tiers the voice is available on', + condition: { field: 'operation', value: 'get_voice' }, + }, + highQualityBaseModelIds: { + type: 'array', + description: 'Model IDs supporting high-quality output for this voice', + condition: { field: 'operation', value: 'get_voice' }, + }, + isOwner: { + type: 'boolean', + description: 'Whether the current user owns this voice', + condition: { field: 'operation', value: 'get_voice' }, + }, + stability: { + type: 'number', + description: 'Voice stability', + condition: { field: 'operation', value: 'get_voice_settings' }, + }, + similarityBoost: { + type: 'number', + description: 'Similarity boost', + condition: { field: 'operation', value: 'get_voice_settings' }, + }, + style: { + type: 'number', + description: 'Style exaggeration', + condition: { field: 'operation', value: 'get_voice_settings' }, + }, + useSpeakerBoost: { + type: 'boolean', + description: 'Whether speaker boost is enabled', + condition: { field: 'operation', value: 'get_voice_settings' }, + }, + speed: { + type: 'number', + description: 'Speech speed', + condition: { field: 'operation', value: 'get_voice_settings' }, + }, + status: { + type: 'string', + description: 'Edit outcome ("ok" on success)', + condition: { field: 'operation', value: 'edit_voice_settings' }, + }, + models: { + type: 'array', + description: 'List of available models', + condition: { field: 'operation', value: 'list_models' }, + }, + userId: { + type: 'string', + description: 'User identifier', + condition: { field: 'operation', value: 'get_user' }, + }, + isNewUser: { + type: 'boolean', + description: 'Whether the user is new', + condition: { field: 'operation', value: 'get_user' }, + }, + subscription: { + type: 'json', + description: 'Subscription and usage details', + condition: { field: 'operation', value: 'get_user' }, + }, }, } diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index af9ab4d4c07..9b22b80e7ef 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -23,12 +23,18 @@ export const FirecrawlBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Scrape', id: 'scrape' }, + { label: 'Batch Scrape', id: 'batch_scrape' }, + { label: 'Batch Scrape Status', id: 'batch_scrape_status' }, { label: 'Search', id: 'search' }, { label: 'Crawl', id: 'crawl' }, + { label: 'Crawl Status', id: 'crawl_status' }, + { label: 'Cancel Crawl', id: 'cancel_crawl' }, { label: 'Map', id: 'map' }, { label: 'Extract', id: 'extract' }, + { label: 'Extract Status', id: 'extract_status' }, { label: 'Agent', id: 'agent' }, { label: 'Parse Document', id: 'parse' }, + { label: 'Credit Usage', id: 'credit_usage' }, ], value: () => 'scrape', }, @@ -79,9 +85,26 @@ export const FirecrawlBlock: BlockConfig = { placeholder: '["https://example.com/page1", "https://example.com/page2"]', condition: { field: 'operation', - value: 'extract', + value: ['extract', 'batch_scrape'], + }, + required: { + field: 'operation', + value: ['extract', 'batch_scrape'], + }, + }, + { + id: 'jobId', + title: 'Job ID', + type: 'short-input', + placeholder: 'Enter the job ID', + condition: { + field: 'operation', + value: ['crawl_status', 'cancel_crawl', 'batch_scrape_status', 'extract_status'], + }, + required: { + field: 'operation', + value: ['crawl_status', 'cancel_crawl', 'batch_scrape_status', 'extract_status'], }, - required: true, }, { id: 'prompt', @@ -210,7 +233,7 @@ Example 2 - Product Data: type: 'switch', condition: { field: 'operation', - value: ['scrape', 'parse'], + value: ['scrape', 'parse', 'batch_scrape'], }, }, { @@ -220,9 +243,24 @@ Example 2 - Product Data: placeholder: '["markdown", "html"]', condition: { field: 'operation', - value: ['scrape', 'parse'], + value: ['scrape', 'parse', 'batch_scrape'], }, }, + { + id: 'maxConcurrency', + title: 'Max Concurrency', + type: 'short-input', + placeholder: 'Maximum number of concurrent scrapes', + mode: 'advanced', + condition: { field: 'operation', value: 'batch_scrape' }, + }, + { + id: 'ignoreInvalidURLs', + title: 'Ignore Invalid URLs', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'batch_scrape' }, + }, { id: 'waitFor', title: 'Wait For (ms)', @@ -380,30 +418,48 @@ Example 2 - Product Data: tools: { access: [ 'firecrawl_scrape', + 'firecrawl_batch_scrape', + 'firecrawl_batch_scrape_status', 'firecrawl_search', 'firecrawl_crawl', + 'firecrawl_crawl_status', + 'firecrawl_cancel_crawl', 'firecrawl_map', 'firecrawl_extract', + 'firecrawl_extract_status', 'firecrawl_agent', 'firecrawl_parse', + 'firecrawl_credit_usage', ], config: { tool: (params) => { switch (params.operation) { case 'scrape': return 'firecrawl_scrape' + case 'batch_scrape': + return 'firecrawl_batch_scrape' + case 'batch_scrape_status': + return 'firecrawl_batch_scrape_status' case 'search': return 'firecrawl_search' case 'crawl': return 'firecrawl_crawl' + case 'crawl_status': + return 'firecrawl_crawl_status' + case 'cancel_crawl': + return 'firecrawl_cancel_crawl' case 'map': return 'firecrawl_map' case 'extract': return 'firecrawl_extract' + case 'extract_status': + return 'firecrawl_extract_status' case 'agent': return 'firecrawl_agent' case 'parse': return 'firecrawl_parse' + case 'credit_usage': + return 'firecrawl_credit_usage' default: return 'firecrawl_scrape' } @@ -427,6 +483,7 @@ Example 2 - Product Data: schema, maxCredits, strictConstrainToURLs, + jobId, } = params const result: Record = { apiKey } @@ -575,6 +632,50 @@ Example 2 - Product Data: if (maxCredits) result.maxCredits = Number.parseInt(maxCredits) if (strictConstrainToURLs != null) result.strictConstrainToURLs = strictConstrainToURLs break + + case 'batch_scrape': + if (urls) { + if (Array.isArray(urls)) { + result.urls = urls + } else if (typeof urls === 'string') { + try { + const parsed = JSON.parse(urls) + result.urls = Array.isArray(parsed) ? parsed : [parsed] + } catch { + result.urls = urls + } + } + } + if (formats) { + if (Array.isArray(formats)) { + result.formats = formats + } else if (typeof formats === 'string') { + try { + const parsed = JSON.parse(formats) + result.formats = Array.isArray(parsed) ? parsed : ['markdown'] + } catch { + result.formats = ['markdown'] + } + } + } + if (onlyMainContent != null) result.onlyMainContent = onlyMainContent + if (params.maxConcurrency != null && params.maxConcurrency !== '') { + result.maxConcurrency = Number.parseInt(String(params.maxConcurrency)) + } + if (params.ignoreInvalidURLs != null) { + result.ignoreInvalidURLs = params.ignoreInvalidURLs + } + break + + case 'crawl_status': + case 'cancel_crawl': + case 'batch_scrape_status': + case 'extract_status': + if (jobId) result.jobId = jobId + break + + case 'credit_usage': + break } return result @@ -585,7 +686,8 @@ Example 2 - Product Data: apiKey: { type: 'string', description: 'Firecrawl API key' }, operation: { type: 'string', description: 'Operation to perform' }, url: { type: 'string', description: 'Target website URL' }, - urls: { type: 'json', description: 'Array of URLs for extraction' }, + urls: { type: 'json', description: 'Array of URLs for extraction or batch scraping' }, + jobId: { type: 'string', description: 'Job ID for status/cancel operations' }, query: { type: 'string', description: 'Search query terms' }, prompt: { type: 'string', description: 'Extraction prompt' }, limit: { type: 'string', description: 'Result/page limit' }, @@ -641,16 +743,25 @@ Example 2 - Product Data: data: { type: 'json', description: 'Search results or extracted data' }, warning: { type: 'string', description: 'Warning messages' }, // Crawl output - pages: { type: 'json', description: 'Crawled pages data' }, + pages: { type: 'json', description: 'Crawled or batch-scraped pages data' }, total: { type: 'number', description: 'Total pages found' }, + completed: { type: 'number', description: 'Number of pages completed' }, + creditsUsed: { type: 'number', description: 'Credits consumed by the job' }, + next: { type: 'string', description: 'URL to retrieve the next page of results' }, + invalidURLs: { type: 'json', description: 'URLs skipped because they were invalid' }, // Map output success: { type: 'boolean', description: 'Operation success status' }, links: { type: 'json', description: 'Discovered URLs array' }, // Extract output sources: { type: 'json', description: 'Data sources array' }, - // Agent output - status: { type: 'string', description: 'Agent job status' }, + tokensUsed: { type: 'number', description: 'Tokens consumed by the extract job' }, + jobId: { type: 'string', description: 'Job ID for the started operation' }, + status: { type: 'string', description: 'Job status' }, expiresAt: { type: 'string', description: 'Result expiration timestamp' }, + remainingCredits: { type: 'number', description: 'Credits remaining for the team' }, + planCredits: { type: 'number', description: 'Credits allocated in the current plan' }, + billingPeriodStart: { type: 'string', description: 'Start of the current billing period' }, + billingPeriodEnd: { type: 'string', description: 'End of the current billing period' }, // Parse output summary: { type: 'string', description: 'Generated summary of the parsed document' }, rawHtml: { type: 'string', description: 'Unprocessed raw HTML from the parsed document' }, diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 8e4b6b9beff..6bdb5d76398 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -43,6 +43,12 @@ export const GoogleDriveBlock: BlockConfig = { { label: 'Share File', id: 'share' }, { label: 'Remove Sharing', id: 'unshare' }, { label: 'List Permissions', id: 'list_permissions' }, + { label: 'Export File', id: 'export' }, + { label: 'List Revisions', id: 'list_revisions' }, + { label: 'Get Revision', id: 'get_revision' }, + { label: 'List Comments', id: 'list_comments' }, + { label: 'Create Comment', id: 'create_comment' }, + { label: 'Delete Comment', id: 'delete_comment' }, { label: 'Get Drive Info', id: 'get_about' }, ], value: () => 'list', @@ -917,6 +923,253 @@ Return ONLY the query string - no explanations, no quotes around the whole thing condition: { field: 'operation', value: 'untrash' }, required: true, }, + { + id: 'exportFileSelector', + title: 'Select File to Export', + type: 'file-selector', + canonicalParamId: 'exportFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a Google Workspace file to export', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'export' }, + required: true, + }, + { + id: 'exportManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'exportFileId', + placeholder: 'Enter file ID to export', + mode: 'advanced', + condition: { field: 'operation', value: 'export' }, + required: true, + }, + { + id: 'exportMimeType', + title: 'Export Format', + type: 'dropdown', + options: [ + { label: 'PDF (application/pdf)', id: 'application/pdf' }, + { label: 'Plain Text (text/plain)', id: 'text/plain' }, + { label: 'HTML (text/html)', id: 'text/html' }, + { + label: 'DOCX (MS Word)', + id: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + }, + { + label: 'XLSX (MS Excel)', + id: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + { + label: 'PPTX (MS PowerPoint)', + id: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + }, + { label: 'CSV (text/csv)', id: 'text/csv' }, + { label: 'PNG (image/png)', id: 'image/png' }, + { label: 'SVG (image/svg+xml)', id: 'image/svg+xml' }, + ], + placeholder: 'Select the format to export to', + condition: { field: 'operation', value: 'export' }, + required: true, + }, + { + id: 'fileName', + title: 'File Name Override', + type: 'short-input', + placeholder: 'Optional: Override the exported filename', + condition: { field: 'operation', value: 'export' }, + }, + { + id: 'listRevisionsFileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'listRevisionsFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file to list revisions for', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'list_revisions' }, + required: true, + }, + { + id: 'listRevisionsManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'listRevisionsFileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'list_revisions' }, + required: true, + }, + { + id: 'revisionsPageSize', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'Number of revisions (default: 200, max: 1000)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_revisions' }, + }, + { + id: 'getRevisionFileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'getRevisionFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'get_revision' }, + required: true, + }, + { + id: 'getRevisionManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'getRevisionFileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'get_revision' }, + required: true, + }, + { + id: 'revisionId', + title: 'Revision ID', + type: 'short-input', + placeholder: 'Enter the revision ID (use List Revisions to find)', + condition: { field: 'operation', value: 'get_revision' }, + required: true, + }, + { + id: 'listCommentsFileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'listCommentsFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file to list comments for', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'list_comments' }, + required: true, + }, + { + id: 'listCommentsManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'listCommentsFileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'list_comments' }, + required: true, + }, + { + id: 'commentsPageSize', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'Number of comments (default: 20, max: 100)', + mode: 'advanced', + condition: { field: 'operation', value: 'list_comments' }, + }, + { + id: 'includeDeleted', + title: 'Include Deleted Comments', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + placeholder: 'Include deleted comments', + mode: 'advanced', + condition: { field: 'operation', value: 'list_comments' }, + }, + { + id: 'createCommentFileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'createCommentFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select a file to comment on', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'create_comment' }, + required: true, + }, + { + id: 'createCommentManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'createCommentFileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'create_comment' }, + required: true, + }, + { + id: 'content', + title: 'Comment', + type: 'long-input', + placeholder: 'The text of your comment', + condition: { field: 'operation', value: 'create_comment' }, + required: true, + wandConfig: { + enabled: true, + prompt: `Generate a clear, professional comment for a Google Drive file based on the user's input. +Keep it concise and actionable - typically 1-3 sentences. + +Return ONLY the comment text - no explanations, no quotes, no extra formatting.`, + placeholder: 'Describe the comment you want to leave...', + }, + }, + { + id: 'anchor', + title: 'Anchor', + type: 'short-input', + placeholder: 'Optional: JSON anchor describing the region the comment refers to', + mode: 'advanced', + condition: { field: 'operation', value: 'create_comment' }, + }, + { + id: 'deleteCommentFileSelector', + title: 'Select File', + type: 'file-selector', + canonicalParamId: 'deleteCommentFileId', + serviceId: 'google-drive', + selectorKey: 'google.drive', + requiredScopes: getScopesForService('google-drive'), + placeholder: 'Select the file the comment belongs to', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'delete_comment' }, + required: true, + }, + { + id: 'deleteCommentManualFileId', + title: 'File ID', + type: 'short-input', + canonicalParamId: 'deleteCommentFileId', + placeholder: 'Enter file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'delete_comment' }, + required: true, + }, + { + id: 'commentId', + title: 'Comment ID', + type: 'short-input', + placeholder: 'Enter the comment ID to delete (use List Comments to find)', + condition: { field: 'operation', value: 'delete_comment' }, + required: true, + }, // Get Drive Info has no additional fields (just needs credential) ...getTrigger('google_drive_poller').subBlocks, ], @@ -938,6 +1191,12 @@ Return ONLY the query string - no explanations, no quotes around the whole thing 'google_drive_share', 'google_drive_unshare', 'google_drive_list_permissions', + 'google_drive_export', + 'google_drive_list_revisions', + 'google_drive_get_revision', + 'google_drive_list_comments', + 'google_drive_create_comment', + 'google_drive_delete_comment', 'google_drive_get_about', ], config: { @@ -976,6 +1235,18 @@ Return ONLY the query string - no explanations, no quotes around the whole thing return 'google_drive_unshare' case 'list_permissions': return 'google_drive_list_permissions' + case 'export': + return 'google_drive_export' + case 'list_revisions': + return 'google_drive_list_revisions' + case 'get_revision': + return 'google_drive_get_revision' + case 'list_comments': + return 'google_drive_list_comments' + case 'create_comment': + return 'google_drive_create_comment' + case 'delete_comment': + return 'google_drive_delete_comment' case 'get_about': return 'google_drive_get_about' default: @@ -1004,6 +1275,12 @@ Return ONLY the query string - no explanations, no quotes around the whole thing shareFileId, unshareFileId, listPermissionsFileId, + exportFileId, + listRevisionsFileId, + getRevisionFileId, + listCommentsFileId, + createCommentFileId, + deleteCommentFileId, // File upload file, mimeType, @@ -1012,11 +1289,15 @@ Return ONLY the query string - no explanations, no quotes around the whole thing sendNotification, removeFromCurrent, includeRevisions, + includeDeleted, pageSize, query, searchQuery, searchPageSize, + revisionsPageSize, + commentsPageSize, getContentExportMimeType, + exportMimeType, ...rest } = params @@ -1077,6 +1358,24 @@ Return ONLY the query string - no explanations, no quotes around the whole thing case 'list_permissions': effectiveFileId = listPermissionsFileId?.trim() || undefined break + case 'export': + effectiveFileId = exportFileId?.trim() || undefined + break + case 'list_revisions': + effectiveFileId = listRevisionsFileId?.trim() || undefined + break + case 'get_revision': + effectiveFileId = getRevisionFileId?.trim() || undefined + break + case 'list_comments': + effectiveFileId = listCommentsFileId?.trim() || undefined + break + case 'create_comment': + effectiveFileId = createCommentFileId?.trim() || undefined + break + case 'delete_comment': + effectiveFileId = deleteCommentFileId?.trim() || undefined + break } // Resolve destinationFolderId for copy/move operations @@ -1102,10 +1401,21 @@ Return ONLY the query string - no explanations, no quotes around the whole thing const includeRevisionsValue = includeRevisions === 'true' ? true : includeRevisions === 'false' ? false : undefined - const effectivePageSize = params.operation === 'search' ? searchPageSize : pageSize + const includeDeletedValue = + includeDeleted === 'true' ? true : includeDeleted === 'false' ? false : undefined + + let effectivePageSize: string | undefined = pageSize + if (params.operation === 'search') effectivePageSize = searchPageSize + else if (params.operation === 'list_revisions') effectivePageSize = revisionsPageSize + else if (params.operation === 'list_comments') effectivePageSize = commentsPageSize + const effectiveQuery = params.operation === 'search' ? searchQuery : query const effectiveMimeType = - params.operation === 'get_content' ? getContentExportMimeType : mimeType + params.operation === 'get_content' + ? getContentExportMimeType + : params.operation === 'export' + ? exportMimeType + : mimeType return { oauthCredential, @@ -1123,6 +1433,7 @@ Return ONLY the query string - no explanations, no quotes around the whole thing sendNotification: sendNotificationValue, removeFromCurrent: removeFromCurrentValue, includeRevisions: includeRevisionsValue, + includeDeleted: includeDeletedValue, transferOwnership: rest.role === 'owner' ? true : undefined, ...rest, } @@ -1151,6 +1462,12 @@ Return ONLY the query string - no explanations, no quotes around the whole thing shareFileId: { type: 'string', description: 'File to share' }, unshareFileId: { type: 'string', description: 'File to unshare' }, listPermissionsFileId: { type: 'string', description: 'File to list permissions for' }, + exportFileId: { type: 'string', description: 'File to export' }, + listRevisionsFileId: { type: 'string', description: 'File to list revisions for' }, + getRevisionFileId: { type: 'string', description: 'File the revision belongs to' }, + listCommentsFileId: { type: 'string', description: 'File to list comments for' }, + createCommentFileId: { type: 'string', description: 'File to comment on' }, + deleteCommentFileId: { type: 'string', description: 'File the comment belongs to' }, // Move operation inputs removeFromCurrent: { type: 'string', @@ -1183,6 +1500,13 @@ Return ONLY the query string - no explanations, no quotes around the whole thing emailMessage: { type: 'string', description: 'Custom notification message' }, // Unshare operation inputs permissionId: { type: 'string', description: 'Permission ID to remove' }, + exportMimeType: { type: 'string', description: 'Target MIME type to export to' }, + revisionId: { type: 'string', description: 'Revision ID to retrieve' }, + revisionsPageSize: { type: 'string', description: 'Results per page for revisions' }, + commentId: { type: 'string', description: 'Comment ID to delete' }, + anchor: { type: 'string', description: 'Anchor region for a new comment' }, + includeDeleted: { type: 'string', description: 'Include deleted comments when listing' }, + commentsPageSize: { type: 'string', description: 'Results per page for comments' }, }, outputs: { file: { type: 'file', description: 'Downloaded file stored in execution files' }, @@ -1201,8 +1525,14 @@ Return ONLY the query string - no explanations, no quotes around the whole thing description: 'Map of Google Workspace MIME types and export formats', }, maxUploadSize: { type: 'string', description: 'Maximum upload size in bytes' }, - deleted: { type: 'boolean', description: 'Whether file was deleted' }, + deleted: { type: 'boolean', description: 'Whether file or comment was deleted' }, removed: { type: 'boolean', description: 'Whether permission was removed' }, + exportedMimeType: { type: 'string', description: 'MIME type a file was exported to' }, + revisions: { type: 'json', description: 'List of file revisions' }, + revision: { type: 'json', description: 'A single file revision' }, + comments: { type: 'json', description: 'List of file comments' }, + comment: { type: 'json', description: 'A single file comment' }, + commentId: { type: 'string', description: 'ID of a deleted comment' }, }, triggers: { enabled: true, diff --git a/apps/sim/blocks/blocks/pinecone.ts b/apps/sim/blocks/blocks/pinecone.ts index 31007af65dd..3d01b42f702 100644 --- a/apps/sim/blocks/blocks/pinecone.ts +++ b/apps/sim/blocks/blocks/pinecone.ts @@ -9,7 +9,7 @@ export const PineconeBlock: BlockConfig = { description: 'Use Pinecone vector database', authMode: AuthMode.ApiKey, longDescription: - 'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors.', + 'Integrate Pinecone into the workflow. Generate embeddings, upsert and update text records, delete vectors, search with text or vectors, fetch and list vectors, inspect index statistics, and manage indexes.', docsLink: 'https://docs.sim.ai/integrations/pinecone', category: 'tools', integrationType: IntegrationType.Databases, @@ -24,9 +24,15 @@ export const PineconeBlock: BlockConfig = { options: [ { label: 'Generate Embeddings', id: 'generate' }, { label: 'Upsert Text', id: 'upsert_text' }, + { label: 'Update Vector', id: 'update_vector' }, + { label: 'Delete Vectors', id: 'delete_vectors' }, { label: 'Search With Text', id: 'search_text' }, { label: 'Search With Vector', id: 'search_vector' }, { label: 'Fetch Vectors', id: 'fetch' }, + { label: 'List Vector IDs', id: 'list_vector_ids' }, + { label: 'Describe Index Stats', id: 'describe_index_stats' }, + { label: 'List Indexes', id: 'list_indexes' }, + { label: 'Describe Index', id: 'describe_index' }, ], value: () => 'generate', }, @@ -255,6 +261,197 @@ export const PineconeBlock: BlockConfig = { ], condition: { field: 'operation', value: 'search_vector' }, }, + { + id: 'indexHost', + title: 'Index Host', + type: 'short-input', + placeholder: 'https://index-name-abc123.svc.project-id.pinecone.io', + condition: { field: 'operation', value: 'update_vector' }, + required: true, + }, + { + id: 'namespace', + title: 'Namespace', + type: 'short-input', + placeholder: 'default', + condition: { field: 'operation', value: 'update_vector' }, + }, + { + id: 'id', + title: 'Vector ID', + type: 'short-input', + placeholder: 'vec-001', + condition: { field: 'operation', value: 'update_vector' }, + required: true, + }, + { + id: 'values', + title: 'Vector Values', + type: 'long-input', + placeholder: '[0.1, 0.2, 0.3, ...]', + condition: { field: 'operation', value: 'update_vector' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of floating-point numbers representing the new dense vector values. Return ONLY a valid JSON array - no explanations.', + placeholder: 'Describe or paste the vector values...', + generationType: 'json-object', + }, + }, + { + id: 'setMetadata', + title: 'Set Metadata', + type: 'long-input', + placeholder: '{"category": "product", "year": 2024}', + condition: { field: 'operation', value: 'update_vector' }, + wandConfig: { + enabled: true, + prompt: + "Generate a JSON object of metadata key-value pairs to add or overwrite on a Pinecone vector based on the user's description. Return ONLY valid JSON - no explanations.", + placeholder: 'Describe the metadata to set...', + generationType: 'json-object', + }, + }, + { + id: 'sparseValues', + title: 'Sparse Values', + type: 'long-input', + placeholder: '{"indices": [1, 5], "values": [0.3, 0.7]}', + condition: { field: 'operation', value: 'update_vector' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a Pinecone sparse vector as a JSON object with "indices" (array of integers) and "values" (array of floats) of equal length. Return ONLY valid JSON - no explanations.', + placeholder: 'Describe the sparse vector...', + generationType: 'json-object', + }, + }, + { + id: 'indexHost', + title: 'Index Host', + type: 'short-input', + placeholder: 'https://index-name-abc123.svc.project-id.pinecone.io', + condition: { field: 'operation', value: 'delete_vectors' }, + required: true, + }, + { + id: 'namespace', + title: 'Namespace', + type: 'short-input', + placeholder: 'default', + condition: { field: 'operation', value: 'delete_vectors' }, + }, + { + id: 'ids', + title: 'Vector IDs', + type: 'long-input', + placeholder: '["vec1", "vec2"]', + condition: { field: 'operation', value: 'delete_vectors' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of vector IDs to delete from Pinecone based on the user\'s description. Example: ["vec1", "vec2", "vec3"]. Return ONLY a valid JSON array - no explanations.', + placeholder: 'Describe which vector IDs to delete...', + generationType: 'json-object', + }, + }, + { + id: 'deleteAll', + title: 'Delete All In Namespace', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + condition: { field: 'operation', value: 'delete_vectors' }, + value: () => 'false', + }, + { + id: 'filter', + title: 'Filter', + type: 'long-input', + placeholder: '{"category": "product"}', + condition: { field: 'operation', value: 'delete_vectors' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a Pinecone metadata filter object in JSON format that selects which vectors to delete based on the user\'s description. Use operators like $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, and $and, $or. Example: {"category": {"$eq": "product"}}. Return ONLY valid JSON - no explanations.', + placeholder: 'Describe which vectors to delete by metadata...', + generationType: 'json-object', + }, + }, + { + id: 'indexHost', + title: 'Index Host', + type: 'short-input', + placeholder: 'https://index-name-abc123.svc.project-id.pinecone.io', + condition: { field: 'operation', value: 'list_vector_ids' }, + required: true, + }, + { + id: 'namespace', + title: 'Namespace', + type: 'short-input', + placeholder: 'default', + condition: { field: 'operation', value: 'list_vector_ids' }, + required: true, + }, + { + id: 'prefix', + title: 'ID Prefix', + type: 'short-input', + placeholder: 'doc1#', + condition: { field: 'operation', value: 'list_vector_ids' }, + mode: 'advanced', + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { field: 'operation', value: 'list_vector_ids' }, + mode: 'advanced', + }, + { + id: 'paginationToken', + title: 'Pagination Token', + type: 'short-input', + placeholder: 'Token from a previous response', + condition: { field: 'operation', value: 'list_vector_ids' }, + mode: 'advanced', + }, + { + id: 'indexHost', + title: 'Index Host', + type: 'short-input', + placeholder: 'https://index-name-abc123.svc.project-id.pinecone.io', + condition: { field: 'operation', value: 'describe_index_stats' }, + required: true, + }, + { + id: 'filter', + title: 'Filter', + type: 'long-input', + placeholder: '{"category": "product"}', + condition: { field: 'operation', value: 'describe_index_stats' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a Pinecone metadata filter object in JSON format to limit which vectors are counted (pod-based indexes only). Example: {"category": {"$eq": "product"}}. Return ONLY valid JSON - no explanations.', + placeholder: 'Describe how to filter the counted vectors...', + generationType: 'json-object', + }, + }, + { + id: 'indexName', + title: 'Index Name', + type: 'short-input', + placeholder: 'my-index', + condition: { field: 'operation', value: 'describe_index' }, + required: true, + }, // Common fields { id: 'apiKey', @@ -270,9 +467,15 @@ export const PineconeBlock: BlockConfig = { access: [ 'pinecone_generate_embeddings', 'pinecone_upsert_text', + 'pinecone_update_vector', + 'pinecone_delete_vectors', 'pinecone_search_text', 'pinecone_search_vector', 'pinecone_fetch', + 'pinecone_list_vector_ids', + 'pinecone_describe_index_stats', + 'pinecone_list_indexes', + 'pinecone_describe_index', ], config: { tool: (params: Record) => { @@ -281,16 +484,38 @@ export const PineconeBlock: BlockConfig = { return 'pinecone_generate_embeddings' case 'upsert_text': return 'pinecone_upsert_text' + case 'update_vector': + return 'pinecone_update_vector' + case 'delete_vectors': + return 'pinecone_delete_vectors' case 'search_text': return 'pinecone_search_text' case 'fetch': return 'pinecone_fetch' case 'search_vector': return 'pinecone_search_vector' + case 'list_vector_ids': + return 'pinecone_list_vector_ids' + case 'describe_index_stats': + return 'pinecone_describe_index_stats' + case 'list_indexes': + return 'pinecone_list_indexes' + case 'describe_index': + return 'pinecone_describe_index' default: throw new Error('Invalid operation selected') } }, + params: (params: Record) => { + const result: Record = {} + if (params.limit != null && params.limit !== '') { + result.limit = Number(params.limit) + } + if (params.deleteAll != null && params.deleteAll !== '') { + result.deleteAll = params.deleteAll === true || params.deleteAll === 'true' + } + return result + }, }, }, @@ -316,15 +541,42 @@ export const PineconeBlock: BlockConfig = { vector: { type: 'json', description: 'Query vector' }, includeValues: { type: 'boolean', description: 'Include vector values' }, includeMetadata: { type: 'boolean', description: 'Include metadata' }, + id: { type: 'string', description: 'Vector identifier to update' }, + values: { type: 'json', description: 'New dense vector values' }, + sparseValues: { type: 'json', description: 'New sparse vector values' }, + setMetadata: { type: 'json', description: 'Metadata to set on the vector' }, + deleteAll: { type: 'boolean', description: 'Delete all vectors in the namespace' }, + prefix: { type: 'string', description: 'Vector ID prefix filter' }, + limit: { type: 'number', description: 'Maximum number of IDs to return' }, + paginationToken: { type: 'string', description: 'Pagination token for the next page' }, + indexName: { type: 'string', description: 'Index name' }, }, outputs: { matches: { type: 'json', description: 'Search matches' }, - statusText: { type: 'string', description: 'Status of the upsert operation' }, + statusText: { type: 'string', description: 'Status of the operation' }, data: { type: 'json', description: 'Response data' }, model: { type: 'string', description: 'Model information' }, vector_type: { type: 'string', description: 'Vector type' }, + namespace: { type: 'string', description: 'Namespace the operation targeted' }, usage: { type: 'json', description: 'Usage statistics' }, + indexes: { + type: 'json', + description: 'List of indexes [{name, dimension, metric, host, vectorType, spec, status}]', + }, + index: { + type: 'json', + description: 'Index configuration {name, dimension, metric, host, vectorType, spec, status}', + }, + namespaces: { + type: 'json', + description: 'Per-namespace stats keyed by name, each with vectorCount', + }, + dimension: { type: 'number', description: 'Index vector dimensionality' }, + indexFullness: { type: 'number', description: 'Index fullness (pod-based indexes only)' }, + totalVectorCount: { type: 'number', description: 'Total vectors across all namespaces' }, + vectorIds: { type: 'json', description: 'List of vector IDs in the namespace' }, + pagination: { type: 'json', description: 'Pagination info {next}' }, }, } diff --git a/apps/sim/blocks/blocks/resend.ts b/apps/sim/blocks/blocks/resend.ts index 0e1fe69f472..c399d3e7116 100644 --- a/apps/sim/blocks/blocks/resend.ts +++ b/apps/sim/blocks/blocks/resend.ts @@ -38,11 +38,19 @@ export const ResendBlock: BlockConfig = { options: [ { label: 'Send Email', id: 'send_email' }, { label: 'Get Email', id: 'get_email' }, + { label: 'Cancel Email', id: 'cancel_email' }, { label: 'Create Contact', id: 'create_contact' }, { label: 'List Contacts', id: 'list_contacts' }, { label: 'Get Contact', id: 'get_contact' }, { label: 'Update Contact', id: 'update_contact' }, { label: 'Delete Contact', id: 'delete_contact' }, + { label: 'Create Audience', id: 'create_audience' }, + { label: 'Get Audience', id: 'get_audience' }, + { label: 'List Audiences', id: 'list_audiences' }, + { label: 'Delete Audience', id: 'delete_audience' }, + { label: 'Create Broadcast', id: 'create_broadcast' }, + { label: 'Send Broadcast', id: 'send_broadcast' }, + { label: 'Get Broadcast', id: 'get_broadcast' }, { label: 'List Domains', id: 'list_domains' }, ], value: () => 'send_email', @@ -237,6 +245,118 @@ Return ONLY the email body - no explanations, no extra text.`, required: true, }, + { + id: 'cancelEmailId', + title: 'Email ID', + type: 'short-input', + placeholder: 'Scheduled email ID to cancel', + condition: { field: 'operation', value: 'cancel_email' }, + required: true, + }, + + { + id: 'audienceName', + title: 'Audience Name', + type: 'short-input', + placeholder: 'Registered Users', + condition: { field: 'operation', value: 'create_audience' }, + required: true, + }, + { + id: 'audienceId', + title: 'Audience ID', + type: 'short-input', + placeholder: 'Audience ID', + condition: { + field: 'operation', + value: ['get_audience', 'delete_audience', 'create_broadcast'], + }, + required: { + field: 'operation', + value: ['get_audience', 'delete_audience', 'create_broadcast'], + }, + }, + + { + id: 'broadcastFrom', + title: 'From Address', + type: 'short-input', + placeholder: 'sender@yourdomain.com', + condition: { field: 'operation', value: 'create_broadcast' }, + required: true, + }, + { + id: 'broadcastSubject', + title: 'Subject', + type: 'short-input', + placeholder: 'Broadcast subject', + condition: { field: 'operation', value: 'create_broadcast' }, + required: true, + }, + { + id: 'broadcastHtml', + title: 'HTML Body', + type: 'long-input', + placeholder: 'Broadcast HTML content', + condition: { field: 'operation', value: 'create_broadcast' }, + }, + { + id: 'broadcastText', + title: 'Text Body', + type: 'long-input', + placeholder: 'Broadcast plain text content', + condition: { field: 'operation', value: 'create_broadcast' }, + mode: 'advanced', + }, + { + id: 'broadcastReplyTo', + title: 'Reply To', + type: 'short-input', + placeholder: 'reply@example.com', + condition: { field: 'operation', value: 'create_broadcast' }, + mode: 'advanced', + }, + { + id: 'broadcastName', + title: 'Broadcast Name', + type: 'short-input', + placeholder: 'Internal reference name', + condition: { field: 'operation', value: 'create_broadcast' }, + mode: 'advanced', + }, + { + id: 'broadcastPreviewText', + title: 'Preview Text', + type: 'short-input', + placeholder: 'Shown in the inbox before opening', + condition: { field: 'operation', value: 'create_broadcast' }, + mode: 'advanced', + }, + + { + id: 'broadcastId', + title: 'Broadcast ID', + type: 'short-input', + placeholder: 'Broadcast ID', + condition: { field: 'operation', value: ['send_broadcast', 'get_broadcast'] }, + required: { field: 'operation', value: ['send_broadcast', 'get_broadcast'] }, + }, + { + id: 'broadcastScheduledAt', + title: 'Schedule At', + type: 'short-input', + placeholder: 'in 1 min or 2024-08-05T11:52:01.858Z', + condition: { field: 'operation', value: 'send_broadcast' }, + mode: 'advanced', + wandConfig: { + enabled: true, + generationType: 'timestamp', + prompt: + 'Generate an ISO 8601 timestamp for scheduling broadcast delivery. Return ONLY the timestamp - no explanations, no extra text.', + placeholder: 'Describe when to send (e.g., "tomorrow at 9am")...', + }, + }, + ...getTrigger('resend_email_sent').subBlocks, ...getTrigger('resend_email_delivered').subBlocks, ...getTrigger('resend_email_bounced').subBlocks, @@ -251,11 +371,19 @@ Return ONLY the email body - no explanations, no extra text.`, access: [ 'resend_send', 'resend_get_email', + 'resend_cancel_email', 'resend_create_contact', 'resend_list_contacts', 'resend_get_contact', 'resend_update_contact', 'resend_delete_contact', + 'resend_create_audience', + 'resend_get_audience', + 'resend_list_audiences', + 'resend_delete_audience', + 'resend_create_broadcast', + 'resend_send_broadcast', + 'resend_get_broadcast', 'resend_list_domains', ], config: { @@ -297,6 +425,18 @@ Return ONLY the email body - no explanations, no extra text.`, lastName: { type: 'string', description: 'Contact last name' }, unsubscribed: { type: 'string', description: 'Contact subscription status' }, contactId: { type: 'string', description: 'Contact ID or email address' }, + cancelEmailId: { type: 'string', description: 'Scheduled email ID to cancel' }, + audienceName: { type: 'string', description: 'Audience name' }, + audienceId: { type: 'string', description: 'Audience ID' }, + broadcastFrom: { type: 'string', description: 'Broadcast sender email address' }, + broadcastSubject: { type: 'string', description: 'Broadcast subject' }, + broadcastHtml: { type: 'string', description: 'Broadcast HTML content' }, + broadcastText: { type: 'string', description: 'Broadcast plain text content' }, + broadcastReplyTo: { type: 'string', description: 'Broadcast reply-to email address' }, + broadcastName: { type: 'string', description: 'Internal broadcast name' }, + broadcastPreviewText: { type: 'string', description: 'Broadcast inbox preview text' }, + broadcastId: { type: 'string', description: 'Broadcast ID' }, + broadcastScheduledAt: { type: 'string', description: 'Broadcast scheduled send time' }, }, outputs: { @@ -318,6 +458,14 @@ Return ONLY the email body - no explanations, no extra text.`, unsubscribed: { type: 'boolean', description: 'Whether the contact is unsubscribed' }, contacts: { type: 'json', description: 'Array of contacts' }, domains: { type: 'json', description: 'Array of domains' }, + audiences: { type: 'json', description: 'Array of audiences' }, + name: { type: 'string', description: 'Audience or broadcast name' }, + audienceId: { type: 'string', description: 'Audience ID (legacy)' }, + segmentId: { type: 'string', description: 'Broadcast segment ID (current recipient field)' }, + replyTo: { type: 'string', description: 'Broadcast reply-to email address' }, + previewText: { type: 'string', description: 'Broadcast inbox preview text' }, + status: { type: 'string', description: 'Broadcast status' }, + sentAt: { type: 'string', description: 'Broadcast sent timestamp' }, hasMore: { type: 'boolean', description: 'Whether more results are available' }, deleted: { type: 'boolean', description: 'Whether the resource was deleted' }, }, diff --git a/apps/sim/blocks/blocks/s3.ts b/apps/sim/blocks/blocks/s3.ts index 266331ff0ea..039aa391e1e 100644 --- a/apps/sim/blocks/blocks/s3.ts +++ b/apps/sim/blocks/blocks/s3.ts @@ -4,13 +4,39 @@ import { AuthMode, IntegrationType } from '@/blocks/types' import { normalizeFileInput } from '@/blocks/utils' import type { S3Response } from '@/tools/s3/types' +/** + * Normalize the batch-delete keys input into a string array. Accepts an array, + * a JSON array string, or newline/comma-separated keys. + */ +function parseObjectKeys(value: unknown): string[] { + if (Array.isArray(value)) { + return value.map((key) => String(key).trim()).filter(Boolean) + } + if (typeof value !== 'string') { + return [] + } + const trimmed = value.trim() + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed) + if (Array.isArray(parsed)) { + return parsed.map((key) => String(key).trim()).filter(Boolean) + } + } catch {} + } + return trimmed + .split(/[\n,]/) + .map((key) => key.trim()) + .filter(Boolean) +} + export const S3Block: BlockConfig = { type: 's3', name: 'S3', - description: 'Upload, download, list, and manage S3 files', + description: 'Upload, download, list, and manage S3 files and buckets', authMode: AuthMode.ApiKey, longDescription: - 'Integrate S3 into the workflow. Upload files, download objects, list bucket contents, delete objects, and copy objects between buckets. Requires AWS access key and secret access key.', + 'Integrate S3 into the workflow. Upload, download, copy, and delete objects (individually or in batches), inspect object metadata, generate time-limited presigned URLs, list bucket contents, and create, list, or delete buckets. Requires AWS access key and secret access key.', docsLink: 'https://docs.sim.ai/integrations/s3', category: 'tools', integrationType: IntegrationType.Documents, @@ -27,7 +53,13 @@ export const S3Block: BlockConfig = { { label: 'Upload File', id: 'put_object' }, { label: 'List Objects', id: 'list_objects' }, { label: 'Delete Object', id: 'delete_object' }, + { label: 'Delete Objects (Batch)', id: 'delete_objects' }, { label: 'Copy Object', id: 'copy_object' }, + { label: 'Head Object (Metadata)', id: 'head_object' }, + { label: 'Presigned URL', id: 'presigned_url' }, + { label: 'List Buckets', id: 'list_buckets' }, + { label: 'Create Bucket', id: 'create_bucket' }, + { label: 'Delete Bucket', id: 'delete_bucket' }, ], value: () => 'get_object', }, @@ -55,7 +87,18 @@ export const S3Block: BlockConfig = { placeholder: 'e.g., us-east-1, us-west-2', condition: { field: 'operation', - value: ['put_object', 'list_objects', 'delete_object', 'copy_object'], + value: [ + 'put_object', + 'list_objects', + 'delete_object', + 'delete_objects', + 'copy_object', + 'head_object', + 'presigned_url', + 'list_buckets', + 'create_bucket', + 'delete_bucket', + ], }, required: true, }, @@ -74,7 +117,19 @@ export const S3Block: BlockConfig = { title: 'Bucket Name', type: 'short-input', placeholder: 'Enter S3 bucket name', - condition: { field: 'operation', value: ['put_object', 'list_objects', 'delete_object'] }, + condition: { + field: 'operation', + value: [ + 'put_object', + 'list_objects', + 'delete_object', + 'delete_objects', + 'head_object', + 'presigned_url', + 'create_bucket', + 'delete_bucket', + ], + }, required: true, }, @@ -171,13 +226,15 @@ export const S3Block: BlockConfig = { mode: 'advanced', }, - // ===== DELETE OBJECT FIELDS ===== { id: 'objectKey', title: 'Object Key/Path', type: 'short-input', placeholder: 'e.g., myfile.pdf or documents/report.pdf', - condition: { field: 'operation', value: 'delete_object' }, + condition: { + field: 'operation', + value: ['delete_object', 'head_object', 'presigned_url'], + }, required: true, }, @@ -228,6 +285,98 @@ export const S3Block: BlockConfig = { condition: { field: 'operation', value: 'copy_object' }, mode: 'advanced', }, + + { + id: 'headVersionId', + title: 'Version ID', + type: 'short-input', + placeholder: 'Optional object version ID (for versioned buckets)', + condition: { field: 'operation', value: 'head_object' }, + mode: 'advanced', + }, + + { + id: 'objectKeys', + title: 'Object Keys', + type: 'long-input', + placeholder: 'One key per line, or a JSON array (e.g., ["a.txt", "folder/b.txt"])', + condition: { field: 'operation', value: 'delete_objects' }, + required: true, + }, + { + id: 'quiet', + title: 'Quiet Mode', + type: 'switch', + condition: { field: 'operation', value: 'delete_objects' }, + mode: 'advanced', + }, + + { + id: 'presignedMethod', + title: 'URL Type', + type: 'dropdown', + options: [ + { label: 'Download (GET)', id: 'get' }, + { label: 'Upload (PUT)', id: 'put' }, + ], + placeholder: 'Select URL type', + condition: { field: 'operation', value: 'presigned_url' }, + required: true, + }, + { + id: 'expiresIn', + title: 'Expires In (seconds)', + type: 'short-input', + placeholder: 'URL validity in seconds (1-604800, default: 3600)', + condition: { field: 'operation', value: 'presigned_url' }, + }, + { + id: 'presignedContentType', + title: 'Content Type', + type: 'short-input', + placeholder: 'Content-Type the upload must use (PUT only)', + condition: { field: 'operation', value: 'presigned_url' }, + mode: 'advanced', + }, + + { + id: 'bucketPrefix', + title: 'Bucket Prefix', + type: 'short-input', + placeholder: 'Filter buckets by name prefix (optional)', + condition: { field: 'operation', value: 'list_buckets' }, + }, + { + id: 'maxBuckets', + title: 'Max Buckets', + type: 'short-input', + placeholder: 'Maximum number of buckets to return (1-10000)', + condition: { field: 'operation', value: 'list_buckets' }, + mode: 'advanced', + }, + { + id: 'bucketsContinuationToken', + title: 'Continuation Token', + type: 'short-input', + placeholder: 'Token for pagination (from previous response)', + condition: { field: 'operation', value: 'list_buckets' }, + mode: 'advanced', + }, + + { + id: 'createBucketAcl', + title: 'Access Control', + type: 'dropdown', + options: [ + { label: 'Private', id: 'private' }, + { label: 'Public Read', id: 'public-read' }, + { label: 'Public Read/Write', id: 'public-read-write' }, + { label: 'Authenticated Read', id: 'authenticated-read' }, + ], + placeholder: 'Select ACL for the new bucket (default: private)', + condition: { field: 'operation', value: 'create_bucket' }, + mode: 'advanced', + }, ], tools: { access: [ @@ -236,6 +385,12 @@ export const S3Block: BlockConfig = { 's3_list_objects', 's3_delete_object', 's3_copy_object', + 's3_list_buckets', + 's3_head_object', + 's3_create_bucket', + 's3_delete_bucket', + 's3_presigned_url', + 's3_delete_objects', ], config: { tool: (params) => { @@ -253,6 +408,18 @@ export const S3Block: BlockConfig = { return 's3_delete_object' case 'copy_object': return 's3_copy_object' + case 'list_buckets': + return 's3_list_buckets' + case 'head_object': + return 's3_head_object' + case 'create_bucket': + return 's3_create_bucket' + case 'delete_bucket': + return 's3_delete_bucket' + case 'presigned_url': + return 's3_presigned_url' + case 'delete_objects': + return 's3_delete_objects' default: throw new Error(`Invalid S3 operation: ${operation}`) } @@ -369,6 +536,117 @@ export const S3Block: BlockConfig = { } } + case 'list_buckets': + if (!params.region) { + throw new Error('AWS Region is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + prefix: params.bucketPrefix, + maxBuckets: params.maxBuckets + ? Number.parseInt(params.maxBuckets as string, 10) + : undefined, + continuationToken: params.bucketsContinuationToken, + } + + case 'head_object': + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + if (!params.objectKey) { + throw new Error('Object Key is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + versionId: params.headVersionId, + } + + case 'create_bucket': + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + acl: params.createBucketAcl, + } + + case 'delete_bucket': + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + } + + case 'presigned_url': { + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + if (!params.objectKey) { + throw new Error('Object Key is required') + } + if (!params.presignedMethod) { + throw new Error('URL Type (get or put) is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + method: params.presignedMethod, + expiresIn: params.expiresIn + ? Number.parseInt(params.expiresIn as string, 10) + : undefined, + contentType: params.presignedContentType, + } + } + + case 'delete_objects': { + if (!params.region) { + throw new Error('AWS Region is required') + } + if (!params.bucketName) { + throw new Error('Bucket Name is required') + } + const keys = parseObjectKeys(params.objectKeys) + if (keys.length === 0) { + throw new Error('At least one object key is required') + } + return { + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + keys, + quiet: typeof params.quiet === 'string' ? params.quiet === 'true' : params.quiet, + } + } + default: throw new Error(`Unknown operation: ${operation}`) } @@ -400,6 +678,16 @@ export const S3Block: BlockConfig = { destinationBucket: { type: 'string', description: 'Destination bucket name' }, destinationKey: { type: 'string', description: 'Destination object key' }, copyAcl: { type: 'string', description: 'ACL for copied object' }, + headVersionId: { type: 'string', description: 'Object version ID for metadata lookup' }, + objectKeys: { type: 'string', description: 'Object keys to delete (batch)' }, + quiet: { type: 'boolean', description: 'Return only deletion errors' }, + presignedMethod: { type: 'string', description: 'Presigned URL type (get or put)' }, + expiresIn: { type: 'number', description: 'Presigned URL validity in seconds' }, + presignedContentType: { type: 'string', description: 'Content-Type for presigned upload' }, + bucketPrefix: { type: 'string', description: 'Bucket name prefix filter' }, + maxBuckets: { type: 'number', description: 'Maximum number of buckets to return' }, + bucketsContinuationToken: { type: 'string', description: 'Pagination token for buckets' }, + createBucketAcl: { type: 'string', description: 'ACL for the new bucket' }, }, outputs: { url: { type: 'string', description: 'URL of S3 object' }, @@ -409,7 +697,10 @@ export const S3Block: BlockConfig = { }, file: { type: 'file', description: 'Downloaded file stored in execution files' }, objects: { type: 'json', description: 'List of objects (for list operation)' }, - deleted: { type: 'boolean', description: 'Deletion status' }, + buckets: { type: 'json', description: 'List of buckets (for list buckets operation)' }, + deleted: { type: 'json', description: 'Deletion status (boolean) or deleted objects (array)' }, + errors: { type: 'json', description: 'Failed deletions (for batch delete operation)' }, + exists: { type: 'boolean', description: 'Whether the object exists (for head operation)' }, metadata: { type: 'json', description: 'Operation metadata' }, }, } @@ -509,5 +800,11 @@ export const S3BlockMeta = { content: '# Archive Object\n\nMove an object to an archive prefix or bucket.\n\n## Steps\n1. Run copy_object from the source key to the archive destination key.\n2. Verify the copy succeeded.\n3. Run delete_object on the original to complete the move.\n\n## Output\nConfirm the archived destination key and that the original was removed.', }, + { + name: 'share-file-via-presigned-url', + description: 'Generate a time-limited presigned URL so others can download an S3 object.', + content: + '# Share File Via Presigned URL\n\nHand someone a temporary download link without making the object public.\n\n## Steps\n1. Identify the bucket and object key to share.\n2. Run presigned_url with method get and an expiry window in seconds.\n3. Deliver the returned URL to the recipient (for example via email or Slack).\n\n## Output\nReturn the presigned URL and note when it expires.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/lib/api/contracts/tools/aws/s3-create-bucket.ts b/apps/sim/lib/api/contracts/tools/aws/s3-create-bucket.ts new file mode 100644 index 00000000000..6bcaf3e691a --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/s3-create-bucket.ts @@ -0,0 +1,34 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const S3CreateBucketSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + acl: z.string().optional().nullable(), +}) + +const S3CreateBucketResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + bucket: z.string(), + location: z.string().nullable(), + bucketArn: z.string().nullable(), + }), +}) + +export const awsS3CreateBucketContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/s3/create-bucket', + body: S3CreateBucketSchema, + response: { mode: 'json', schema: S3CreateBucketResponseSchema }, +}) +export type AwsS3CreateBucketRequest = ContractBodyInput +export type AwsS3CreateBucketBody = ContractBody +export type AwsS3CreateBucketResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/tools/aws/s3-delete-bucket.ts b/apps/sim/lib/api/contracts/tools/aws/s3-delete-bucket.ts new file mode 100644 index 00000000000..f591d7632f4 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/s3-delete-bucket.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const S3DeleteBucketSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), +}) + +const S3DeleteBucketResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + deleted: z.literal(true), + bucket: z.string(), + }), +}) + +export const awsS3DeleteBucketContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/s3/delete-bucket', + body: S3DeleteBucketSchema, + response: { mode: 'json', schema: S3DeleteBucketResponseSchema }, +}) +export type AwsS3DeleteBucketRequest = ContractBodyInput +export type AwsS3DeleteBucketBody = ContractBody +export type AwsS3DeleteBucketResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/tools/aws/s3-delete-objects.ts b/apps/sim/lib/api/contracts/tools/aws/s3-delete-objects.ts new file mode 100644 index 00000000000..cb4edac9ca7 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/s3-delete-objects.ts @@ -0,0 +1,49 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const S3DeleteObjectsSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + keys: z + .array(z.string().min(1, 'Object key cannot be empty')) + .min(1, 'At least one object key is required') + .max(1000, 'A maximum of 1000 keys can be deleted per request'), + quiet: z.boolean().optional().nullable(), +}) + +const S3DeleteObjectsResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + deleted: z.array( + z.object({ + key: z.string().nullable(), + versionId: z.string().nullable(), + deleteMarker: z.boolean().nullable(), + }) + ), + errors: z.array( + z.object({ + key: z.string().nullable(), + code: z.string().nullable(), + message: z.string().nullable(), + }) + ), + }), +}) + +export const awsS3DeleteObjectsContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/s3/delete-objects', + body: S3DeleteObjectsSchema, + response: { mode: 'json', schema: S3DeleteObjectsResponseSchema }, +}) +export type AwsS3DeleteObjectsRequest = ContractBodyInput +export type AwsS3DeleteObjectsBody = ContractBody +export type AwsS3DeleteObjectsResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/tools/aws/s3-head-object.ts b/apps/sim/lib/api/contracts/tools/aws/s3-head-object.ts new file mode 100644 index 00000000000..5ce4851b647 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/s3-head-object.ts @@ -0,0 +1,42 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const S3HeadObjectSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + objectKey: z.string().min(1, 'Object key is required'), + versionId: z.string().optional().nullable(), +}) + +const S3HeadObjectResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + exists: z.boolean(), + contentLength: z.number().nullable(), + contentType: z.string().nullable(), + etag: z.string().nullable(), + lastModified: z.string().nullable(), + versionId: z.string().nullable(), + storageClass: z.string().nullable(), + serverSideEncryption: z.string().nullable(), + deleteMarker: z.boolean().nullable(), + metadata: z.record(z.string(), z.string()), + }), +}) + +export const awsS3HeadObjectContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/s3/head-object', + body: S3HeadObjectSchema, + response: { mode: 'json', schema: S3HeadObjectResponseSchema }, +}) +export type AwsS3HeadObjectRequest = ContractBodyInput +export type AwsS3HeadObjectBody = ContractBody +export type AwsS3HeadObjectResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/tools/aws/s3-list-buckets.ts b/apps/sim/lib/api/contracts/tools/aws/s3-list-buckets.ts new file mode 100644 index 00000000000..5cf938d9d4d --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/s3-list-buckets.ts @@ -0,0 +1,47 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const S3ListBucketsSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + prefix: z.string().optional().nullable(), + maxBuckets: z.number().int().min(1).max(10000).optional().nullable(), + continuationToken: z.string().optional().nullable(), +}) + +const S3ListBucketsResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + buckets: z.array( + z.object({ + name: z.string(), + creationDate: z.string().nullable(), + region: z.string().nullable(), + }) + ), + owner: z + .object({ + displayName: z.string().nullable(), + id: z.string().nullable(), + }) + .nullable(), + continuationToken: z.string().nullable(), + prefix: z.string().nullable(), + }), +}) + +export const awsS3ListBucketsContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/s3/list-buckets', + body: S3ListBucketsSchema, + response: { mode: 'json', schema: S3ListBucketsResponseSchema }, +}) +export type AwsS3ListBucketsRequest = ContractBodyInput +export type AwsS3ListBucketsBody = ContractBody +export type AwsS3ListBucketsResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/tools/aws/s3-presigned-url.ts b/apps/sim/lib/api/contracts/tools/aws/s3-presigned-url.ts new file mode 100644 index 00000000000..6119cd15854 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/s3-presigned-url.ts @@ -0,0 +1,38 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const S3PresignedUrlSchema = z.object({ + accessKeyId: z.string().min(1, 'Access Key ID is required'), + secretAccessKey: z.string().min(1, 'Secret Access Key is required'), + region: z.string().min(1, 'Region is required'), + bucketName: z.string().min(1, 'Bucket name is required'), + objectKey: z.string().min(1, 'Object key is required'), + method: z.enum(['get', 'put']), + expiresIn: z.number().int().min(1).max(604800), + contentType: z.string().optional().nullable(), +}) + +const S3PresignedUrlResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + url: z.string(), + method: z.enum(['get', 'put']), + expiresIn: z.number(), + expiresAt: z.string(), + }), +}) + +export const awsS3PresignedUrlContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/s3/presigned-url', + body: S3PresignedUrlSchema, + response: { mode: 'json', schema: S3PresignedUrlResponseSchema }, +}) +export type AwsS3PresignedUrlRequest = ContractBodyInput +export type AwsS3PresignedUrlBody = ContractBody +export type AwsS3PresignedUrlResponse = ContractJsonResponse diff --git a/apps/sim/lib/api/contracts/tools/google.ts b/apps/sim/lib/api/contracts/tools/google.ts index d5af5ff6f64..17d79fe6bf1 100644 --- a/apps/sim/lib/api/contracts/tools/google.ts +++ b/apps/sim/lib/api/contracts/tools/google.ts @@ -53,6 +53,13 @@ export const googleDriveDownloadBodySchema = z.object({ includeRevisions: z.boolean().optional().default(true), }) +export const googleDriveExportBodySchema = z.object({ + accessToken: googleAccessTokenSchema, + fileId: z.string().min(1, 'File ID is required'), + mimeType: z.string().min(1, 'Target export MIME type is required'), + fileName: z.string().optional().nullable(), +}) + export const googleVaultDownloadExportFileBodySchema = z.object({ accessToken: googleAccessTokenSchema, bucketName: z.string().min(1, 'Bucket name is required'), @@ -175,6 +182,13 @@ export const googleDriveDownloadContract = defineRouteContract({ response: { mode: 'json', schema: toolJsonResponseSchema }, }) +export const googleDriveExportContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/google_drive/export', + body: googleDriveExportBodySchema, + response: { mode: 'json', schema: toolJsonResponseSchema }, +}) + export const googleVaultDownloadExportFileContract = defineRouteContract({ method: 'POST', path: '/api/tools/google_vault/download-export-file', @@ -202,6 +216,7 @@ export type GmailSendBody = ContractBodyInput export type GmailUnarchiveBody = ContractBodyInput export type GoogleDriveUploadBody = ContractBodyInput export type GoogleDriveDownloadBody = ContractBodyInput +export type GoogleDriveExportBody = ContractBodyInput export type GoogleVaultDownloadExportFileBody = ContractBodyInput< typeof googleVaultDownloadExportFileContract > @@ -222,6 +237,7 @@ export type GmailSendResponse = ContractJsonResponse export type GmailUnarchiveResponse = ContractJsonResponse export type GoogleDriveUploadResponse = ContractJsonResponse export type GoogleDriveDownloadResponse = ContractJsonResponse +export type GoogleDriveExportResponse = ContractJsonResponse export type GoogleVaultDownloadExportFileResponse = ContractJsonResponse< typeof googleVaultDownloadExportFileContract > diff --git a/apps/sim/lib/api/contracts/tools/media/elevenlabs.ts b/apps/sim/lib/api/contracts/tools/media/elevenlabs.ts new file mode 100644 index 00000000000..ca95e665a5a --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/media/elevenlabs.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' +import { userFileSchema } from '@/lib/api/contracts/primitives' +import { toolBooleanSchema, toolJsonResponseSchema } from '@/lib/api/contracts/tools/media/shared' +import { defineRouteContract } from '@/lib/api/contracts/types' + +const MISSING_FIELDS_ERROR = 'Missing required fields: operation and apiKey' + +export const elevenLabsAudioFileSchema = userFileSchema.extend({ + type: z.string().optional().default(''), +}) + +export const elevenLabsAudioToolBodySchema = z + .object({ + operation: z.enum(['sound_effects', 'speech_to_speech', 'audio_isolation'], { + error: MISSING_FIELDS_ERROR, + }), + apiKey: z.string({ error: MISSING_FIELDS_ERROR }).min(1, MISSING_FIELDS_ERROR), + voiceId: z.string().optional(), + text: z.string().optional(), + modelId: z.string().optional(), + durationSeconds: z.coerce.number().min(0.5).max(30).optional(), + promptInfluence: z.coerce.number().min(0).max(1).optional(), + loop: toolBooleanSchema.optional(), + removeBackgroundNoise: toolBooleanSchema.optional(), + audioFile: elevenLabsAudioFileSchema.optional(), + workspaceId: z.string().optional(), + workflowId: z.string().optional(), + executionId: z.string().optional(), + }) + .passthrough() + +export const elevenLabsAudioToolContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/elevenlabs/audio', + body: elevenLabsAudioToolBodySchema, + response: { mode: 'json', schema: toolJsonResponseSchema }, +}) diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 2868235c8a7..4649bc1cad4 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -4598,13 +4598,54 @@ "type": "elevenlabs", "slug": "elevenlabs", "name": "ElevenLabs", - "description": "Convert text to speech with ElevenLabs", - "longDescription": "Integrate ElevenLabs into the workflow. Can convert text to speech.", + "description": "Generate and transform audio with ElevenLabs", + "longDescription": "Integrate ElevenLabs into the workflow. Convert text to speech, generate sound effects, transform voices, isolate audio, and manage voices, models, and account settings.", "bgColor": "#181C1E", "iconName": "ElevenLabsIcon", "docsUrl": "https://docs.sim.ai/integrations/elevenlabs", - "operations": [], - "operationCount": 0, + "operations": [ + { + "name": "Text to Speech", + "description": "Convert text to speech using ElevenLabs voices" + }, + { + "name": "Sound Effects", + "description": "Generate a sound effect from a text prompt using ElevenLabs" + }, + { + "name": "Speech to Speech", + "description": "Convert audio into a chosen ElevenLabs voice while preserving content and emotion" + }, + { + "name": "Audio Isolation", + "description": "Remove background noise from an audio file, isolating the speech using ElevenLabs" + }, + { + "name": "List Voices", + "description": "List the voices available in your ElevenLabs account" + }, + { + "name": "Get Voice", + "description": "Get metadata and settings for a specific ElevenLabs voice" + }, + { + "name": "Get Voice Settings", + "description": "Get the configured settings for a specific ElevenLabs voice" + }, + { + "name": "Edit Voice Settings", + "description": "Update the settings for a specific ElevenLabs voice" + }, + { + "name": "List Models", + "description": "List the models available in ElevenLabs" + }, + { + "name": "Get User Info", + "description": "Get account and subscription information for the ElevenLabs user" + } + ], + "operationCount": 10, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -5122,6 +5163,14 @@ "name": "Scrape", "description": "Extract structured content from web pages with comprehensive metadata support. Converts content to markdown or HTML while capturing SEO metadata, Open Graph tags, and page information." }, + { + "name": "Batch Scrape", + "description": "Scrape multiple URLs in a single batch job and retrieve structured content from each page." + }, + { + "name": "Batch Scrape Status", + "description": "Check the status and retrieve results of a previously started Firecrawl batch scrape job by its job ID." + }, { "name": "Search", "description": "Search for information on the web using Firecrawl" @@ -5130,6 +5179,14 @@ "name": "Crawl", "description": "Crawl entire websites and extract structured content from all accessible pages" }, + { + "name": "Crawl Status", + "description": "Check the status and retrieve results of a previously started Firecrawl crawl job by its job ID." + }, + { + "name": "Cancel Crawl", + "description": "Cancel an in-progress Firecrawl crawl job by its job ID." + }, { "name": "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." @@ -5138,6 +5195,10 @@ "name": "Extract", "description": "Extract structured data from entire webpages using natural language prompts and JSON schema. Powerful agentic feature for intelligent data extraction." }, + { + "name": "Extract Status", + "description": "Check the status and retrieve results of a previously started Firecrawl extract job by its job ID." + }, { "name": "Agent", "description": "Autonomous web data extraction agent. Searches and gathers information based on natural language prompts without requiring specific URLs." @@ -5145,9 +5206,13 @@ { "name": "Parse Document", "description": "Parse uploaded documents (PDF, DOCX, HTML, etc.) into clean markdown using Firecrawl. Supports .html, .htm, .pdf, .docx, .doc, .odt, .rtf, .xlsx, .xls." + }, + { + "name": "Credit Usage", + "description": "Retrieve the remaining and allocated Firecrawl credits for the team." } ], - "operationCount": 7, + "operationCount": 13, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -6407,12 +6472,36 @@ "name": "List Permissions", "description": "List all permissions (who has access) for a file in Google Drive" }, + { + "name": "Export File", + "description": "Export a Google Workspace file (Docs, Sheets, Slides, Drawings) to a chosen format such as PDF, DOCX, XLSX, or CSV" + }, + { + "name": "List Revisions", + "description": "List the revision history of a file in Google Drive" + }, + { + "name": "Get Revision", + "description": "Get metadata for a specific revision of a file in Google Drive" + }, + { + "name": "List Comments", + "description": "List comments on a file in Google Drive" + }, + { + "name": "Create Comment", + "description": "Add a comment to a file in Google Drive" + }, + { + "name": "Delete Comment", + "description": "Delete a comment from a file in Google Drive" + }, { "name": "Get Drive Info", "description": "Get information about the user and their Google Drive (storage quota, capabilities)" } ], - "operationCount": 18, + "operationCount": 24, "triggers": [ { "id": "google_drive_poller", @@ -11860,7 +11949,7 @@ "slug": "pinecone", "name": "Pinecone", "description": "Use Pinecone vector database", - "longDescription": "Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors.", + "longDescription": "Integrate Pinecone into the workflow. Generate embeddings, upsert and update text records, delete vectors, search with text or vectors, fetch and list vectors, inspect index statistics, and manage indexes.", "bgColor": "#0D1117", "iconName": "PineconeIcon", "docsUrl": "https://docs.sim.ai/integrations/pinecone", @@ -11873,6 +11962,14 @@ "name": "Upsert Text", "description": "Insert or update text records in a Pinecone index" }, + { + "name": "Update Vector", + "description": "Update the values, sparse values, or metadata of a vector in a Pinecone namespace" + }, + { + "name": "Delete Vectors", + "description": "Delete vectors from a Pinecone namespace by IDs, by metadata filter, or delete all" + }, { "name": "Search With Text", "description": "Search for similar text in a Pinecone index" @@ -11884,9 +11981,25 @@ { "name": "Fetch Vectors", "description": "Fetch vectors by ID from a Pinecone index" + }, + { + "name": "List Vector IDs", + "description": "List vector IDs in a Pinecone namespace by prefix (serverless indexes only)" + }, + { + "name": "Describe Index Stats", + "description": "Get statistics about a Pinecone index, including per-namespace vector counts" + }, + { + "name": "List Indexes", + "description": "List all Pinecone indexes in the project" + }, + { + "name": "Describe Index", + "description": "Get the configuration and status of a Pinecone index by name" } ], - "operationCount": 5, + "operationCount": 11, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -13140,6 +13253,10 @@ "name": "Get Email", "description": "Retrieve details of a previously sent email by its ID" }, + { + "name": "Cancel Email", + "description": "Cancel a scheduled email before it is sent" + }, { "name": "Create Contact", "description": "Create a new contact in Resend" @@ -13160,12 +13277,40 @@ "name": "Delete Contact", "description": "Delete a contact from Resend by ID or email" }, + { + "name": "Create Audience", + "description": "Create a new audience in Resend" + }, + { + "name": "Get Audience", + "description": "Retrieve details of an audience by ID" + }, + { + "name": "List Audiences", + "description": "List all audiences in Resend" + }, + { + "name": "Delete Audience", + "description": "Delete an audience from Resend by ID" + }, + { + "name": "Create Broadcast", + "description": "Create a broadcast email for an audience in Resend" + }, + { + "name": "Send Broadcast", + "description": "Send a broadcast immediately or schedule it for later" + }, + { + "name": "Get Broadcast", + "description": "Retrieve details of a broadcast by ID" + }, { "name": "List Domains", "description": "List all verified domains in your Resend account" } ], - "operationCount": 8, + "operationCount": 16, "triggers": [ { "id": "resend_email_sent", @@ -13875,8 +14020,8 @@ "type": "s3", "slug": "s3", "name": "S3", - "description": "Upload, download, list, and manage S3 files", - "longDescription": "Integrate S3 into the workflow. Upload files, download objects, list bucket contents, delete objects, and copy objects between buckets. Requires AWS access key and secret access key.", + "description": "Upload, download, list, and manage S3 files and buckets", + "longDescription": "Integrate S3 into the workflow. Upload, download, copy, and delete objects (individually or in batches), inspect object metadata, generate time-limited presigned URLs, list bucket contents, and create, list, or delete buckets. Requires AWS access key and secret access key.", "bgColor": "linear-gradient(45deg, #1B660F 0%, #6CAE3E 100%)", "iconName": "S3Icon", "docsUrl": "https://docs.sim.ai/integrations/s3", @@ -13897,12 +14042,36 @@ "name": "Delete Object", "description": "Delete an object from an AWS S3 bucket" }, + { + "name": "Delete Objects (Batch)", + "description": "Delete multiple objects from an AWS S3 bucket in a single batch request" + }, { "name": "Copy Object", "description": "Copy an object within or between AWS S3 buckets" + }, + { + "name": "Head Object (Metadata)", + "description": "Retrieve metadata for an S3 object without downloading its body" + }, + { + "name": "Presigned URL", + "description": "Generate a time-limited presigned URL to download or upload an S3 object" + }, + { + "name": "List Buckets", + "description": "List the S3 buckets owned by the authenticated AWS account" + }, + { + "name": "Create Bucket", + "description": "Create a new AWS S3 bucket in the specified region" + }, + { + "name": "Delete Bucket", + "description": "Delete an empty AWS S3 bucket" } ], - "operationCount": 5, + "operationCount": 11, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/tools/elevenlabs/audio-isolation.ts b/apps/sim/tools/elevenlabs/audio-isolation.ts new file mode 100644 index 00000000000..1b956405833 --- /dev/null +++ b/apps/sim/tools/elevenlabs/audio-isolation.ts @@ -0,0 +1,73 @@ +import type { + ElevenLabsAudioIsolationParams, + ElevenLabsAudioResponse, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsAudioIsolationTool: ToolConfig< + ElevenLabsAudioIsolationParams, + ElevenLabsAudioResponse +> = { + id: 'elevenlabs_audio_isolation', + name: 'ElevenLabs Audio Isolation', + description: 'Remove background noise from an audio file, isolating the speech using ElevenLabs', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + audioFile: { + type: 'file', + required: true, + visibility: 'user-only', + description: 'The audio file to isolate speech from (e.g., MP3, WAV, M4A)', + }, + }, + + request: { + url: '/api/tools/elevenlabs/audio', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: ( + params: ElevenLabsAudioIsolationParams & { + _context?: { workspaceId?: string; workflowId?: string; executionId?: string } + } + ) => ({ + operation: 'audio_isolation', + apiKey: params.apiKey, + audioFile: params.audioFile, + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok || data.error) { + return { + success: false, + error: data.error || 'Audio isolation failed', + output: { audioUrl: '' }, + } + } + return { + success: true, + output: { + audioUrl: data.audioUrl, + audioFile: data.audioFile, + }, + } + }, + + outputs: { + audioUrl: { type: 'string', description: 'URL of the isolated audio' }, + audioFile: { type: 'file', description: 'The isolated audio file' }, + }, +} diff --git a/apps/sim/tools/elevenlabs/edit-voice-settings.ts b/apps/sim/tools/elevenlabs/edit-voice-settings.ts new file mode 100644 index 00000000000..15b6a7e4df2 --- /dev/null +++ b/apps/sim/tools/elevenlabs/edit-voice-settings.ts @@ -0,0 +1,92 @@ +import type { + ElevenLabsEditVoiceSettingsParams, + ElevenLabsEditVoiceSettingsResponse, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsEditVoiceSettingsTool: ToolConfig< + ElevenLabsEditVoiceSettingsParams, + ElevenLabsEditVoiceSettingsResponse +> = { + id: 'elevenlabs_edit_voice_settings', + name: 'ElevenLabs Edit Voice Settings', + description: 'Update the settings for a specific ElevenLabs voice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + voiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the voice to update', + }, + stability: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Voice stability from 0.0 to 1.0 (default 0.5)', + }, + similarityBoost: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Similarity boost from 0.0 to 1.0 (default 0.75)', + }, + style: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Style exaggeration from 0.0 to 1.0 (default 0)', + }, + useSpeakerBoost: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to enhance similarity to the original speaker (default true)', + }, + speed: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Speech speed where 1.0 is normal (default 1.0)', + }, + }, + + request: { + url: (params) => `https://api.elevenlabs.io/v1/voices/${params.voiceId.trim()}/settings/edit`, + method: 'POST', + headers: (params) => ({ + 'xi-api-key': params.apiKey, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + if (params.stability !== undefined) body.stability = params.stability + if (params.similarityBoost !== undefined) body.similarity_boost = params.similarityBoost + if (params.style !== undefined) body.style = params.style + if (params.useSpeakerBoost !== undefined) body.use_speaker_boost = params.useSpeakerBoost + if (params.speed !== undefined) body.speed = params.speed + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + status: data.status ?? 'ok', + }, + } + }, + + outputs: { + status: { type: 'string', description: 'Request outcome ("ok" on success)' }, + }, +} diff --git a/apps/sim/tools/elevenlabs/get-user.ts b/apps/sim/tools/elevenlabs/get-user.ts new file mode 100644 index 00000000000..88b6e527473 --- /dev/null +++ b/apps/sim/tools/elevenlabs/get-user.ts @@ -0,0 +1,72 @@ +import type { ElevenLabsGetUserParams, ElevenLabsGetUserResponse } from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsGetUserTool: ToolConfig = + { + id: 'elevenlabs_get_user', + name: 'ElevenLabs Get User', + description: 'Get account and subscription information for the ElevenLabs user', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + }, + + request: { + url: 'https://api.elevenlabs.io/v1/user', + method: 'GET', + headers: (params) => ({ + 'xi-api-key': params.apiKey, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const subscription = data.subscription + return { + success: true, + output: { + userId: data.user_id ?? null, + isNewUser: data.is_new_user ?? null, + subscription: subscription + ? { + tier: subscription.tier ?? null, + characterCount: subscription.character_count ?? null, + characterLimit: subscription.character_limit ?? null, + canExtendCharacterLimit: subscription.can_extend_character_limit ?? null, + status: subscription.status ?? null, + nextCharacterCountResetUnix: subscription.next_character_count_reset_unix ?? null, + } + : null, + }, + } + }, + + outputs: { + userId: { type: 'string', description: 'Unique user identifier' }, + isNewUser: { type: 'boolean', description: 'Whether the user is new' }, + subscription: { + type: 'object', + description: 'Subscription and usage details', + properties: { + tier: { type: 'string', description: 'Subscription tier' }, + characterCount: { type: 'number', description: 'Characters used this period' }, + characterLimit: { type: 'number', description: 'Character quota for this period' }, + canExtendCharacterLimit: { + type: 'boolean', + description: 'Whether the character limit can be extended', + }, + status: { type: 'string', description: 'Subscription status' }, + nextCharacterCountResetUnix: { + type: 'number', + description: 'Unix timestamp when the character count resets', + }, + }, + }, + }, + } diff --git a/apps/sim/tools/elevenlabs/get-voice-settings.ts b/apps/sim/tools/elevenlabs/get-voice-settings.ts new file mode 100644 index 00000000000..a1611d3d104 --- /dev/null +++ b/apps/sim/tools/elevenlabs/get-voice-settings.ts @@ -0,0 +1,60 @@ +import type { + ElevenLabsGetVoiceSettingsParams, + ElevenLabsGetVoiceSettingsResponse, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsGetVoiceSettingsTool: ToolConfig< + ElevenLabsGetVoiceSettingsParams, + ElevenLabsGetVoiceSettingsResponse +> = { + id: 'elevenlabs_get_voice_settings', + name: 'ElevenLabs Get Voice Settings', + description: 'Get the configured settings for a specific ElevenLabs voice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + voiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the voice whose settings to retrieve', + }, + }, + + request: { + url: (params) => `https://api.elevenlabs.io/v1/voices/${params.voiceId.trim()}/settings`, + method: 'GET', + headers: (params) => ({ + 'xi-api-key': params.apiKey, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + stability: data.stability ?? null, + similarityBoost: data.similarity_boost ?? null, + style: data.style ?? null, + useSpeakerBoost: data.use_speaker_boost ?? null, + speed: data.speed ?? null, + }, + } + }, + + outputs: { + stability: { type: 'number', description: 'Voice stability (0.0-1.0)' }, + similarityBoost: { type: 'number', description: 'Similarity boost (0.0-1.0)' }, + style: { type: 'number', description: 'Style exaggeration (0.0-1.0)' }, + useSpeakerBoost: { type: 'boolean', description: 'Whether speaker boost is enabled' }, + speed: { type: 'number', description: 'Speech speed (1.0 = normal)' }, + }, +} diff --git a/apps/sim/tools/elevenlabs/get-voice.ts b/apps/sim/tools/elevenlabs/get-voice.ts new file mode 100644 index 00000000000..2a620dbde94 --- /dev/null +++ b/apps/sim/tools/elevenlabs/get-voice.ts @@ -0,0 +1,73 @@ +import type { ElevenLabsGetVoiceParams, ElevenLabsGetVoiceResponse } from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsGetVoiceTool: ToolConfig< + ElevenLabsGetVoiceParams, + ElevenLabsGetVoiceResponse +> = { + id: 'elevenlabs_get_voice', + name: 'ElevenLabs Get Voice', + description: 'Get metadata and settings for a specific ElevenLabs voice', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + voiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the voice to retrieve (e.g., "21m00Tcm4TlvDq8ikWAM")', + }, + }, + + request: { + url: (params) => `https://api.elevenlabs.io/v1/voices/${params.voiceId.trim()}`, + method: 'GET', + headers: (params) => ({ + 'xi-api-key': params.apiKey, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + voiceId: data.voice_id, + name: data.name ?? null, + category: data.category ?? null, + description: data.description ?? null, + labels: data.labels ?? null, + previewUrl: data.preview_url ?? null, + settings: data.settings ?? null, + availableForTiers: data.available_for_tiers ?? [], + highQualityBaseModelIds: data.high_quality_base_model_ids ?? [], + isOwner: data.is_owner ?? null, + }, + } + }, + + outputs: { + voiceId: { type: 'string', description: 'Unique voice identifier' }, + name: { type: 'string', description: 'Voice name' }, + category: { type: 'string', description: 'Voice category' }, + description: { type: 'string', description: 'Voice description' }, + labels: { type: 'json', description: 'Voice labels (accent, gender, age, use case)' }, + previewUrl: { type: 'string', description: 'URL to a preview audio sample' }, + settings: { type: 'json', description: 'Default voice settings' }, + availableForTiers: { + type: 'array', + description: 'Subscription tiers the voice is available on', + }, + highQualityBaseModelIds: { + type: 'array', + description: 'Model IDs that support high-quality output for this voice', + }, + isOwner: { type: 'boolean', description: 'Whether the current user owns this voice' }, + }, +} diff --git a/apps/sim/tools/elevenlabs/index.ts b/apps/sim/tools/elevenlabs/index.ts index fe92c04d784..997bb764460 100644 --- a/apps/sim/tools/elevenlabs/index.ts +++ b/apps/sim/tools/elevenlabs/index.ts @@ -1,3 +1,11 @@ -import { elevenLabsTtsTool } from '@/tools/elevenlabs/tts' - -export { elevenLabsTtsTool } +export { elevenLabsAudioIsolationTool } from '@/tools/elevenlabs/audio-isolation' +export { elevenLabsEditVoiceSettingsTool } from '@/tools/elevenlabs/edit-voice-settings' +export { elevenLabsGetUserTool } from '@/tools/elevenlabs/get-user' +export { elevenLabsGetVoiceTool } from '@/tools/elevenlabs/get-voice' +export { elevenLabsGetVoiceSettingsTool } from '@/tools/elevenlabs/get-voice-settings' +export { elevenLabsListModelsTool } from '@/tools/elevenlabs/list-models' +export { elevenLabsListVoicesTool } from '@/tools/elevenlabs/list-voices' +export { elevenLabsSoundEffectsTool } from '@/tools/elevenlabs/sound-effects' +export { elevenLabsSpeechToSpeechTool } from '@/tools/elevenlabs/speech-to-speech' +export { elevenLabsTtsTool } from '@/tools/elevenlabs/tts' +export * from '@/tools/elevenlabs/types' diff --git a/apps/sim/tools/elevenlabs/list-models.ts b/apps/sim/tools/elevenlabs/list-models.ts new file mode 100644 index 00000000000..93cfcb566da --- /dev/null +++ b/apps/sim/tools/elevenlabs/list-models.ts @@ -0,0 +1,87 @@ +import type { + ElevenLabsListModelsParams, + ElevenLabsListModelsResponse, + ElevenLabsModelSummary, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsListModelsTool: ToolConfig< + ElevenLabsListModelsParams, + ElevenLabsListModelsResponse +> = { + id: 'elevenlabs_list_models', + name: 'ElevenLabs List Models', + description: 'List the models available in ElevenLabs', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + }, + + request: { + url: 'https://api.elevenlabs.io/v1/models', + method: 'GET', + headers: (params) => ({ + 'xi-api-key': params.apiKey, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const models: ElevenLabsModelSummary[] = (Array.isArray(data) ? data : []).map( + (model: any) => ({ + modelId: model.model_id, + name: model.name ?? null, + description: model.description ?? null, + canDoTextToSpeech: model.can_do_text_to_speech ?? null, + canDoVoiceConversion: model.can_do_voice_conversion ?? null, + canUseStyle: model.can_use_style ?? null, + canUseSpeakerBoost: model.can_use_speaker_boost ?? null, + languages: (model.languages ?? []).map((language: any) => ({ + languageId: language.language_id ?? null, + name: language.name ?? null, + })), + }) + ) + + return { + success: true, + output: { models }, + } + }, + + outputs: { + models: { + type: 'array', + description: 'List of available models', + items: { + type: 'object', + properties: { + modelId: { type: 'string', description: 'Unique model identifier' }, + name: { type: 'string', description: 'Model name' }, + description: { type: 'string', description: 'Model description' }, + canDoTextToSpeech: { type: 'boolean', description: 'Supports text-to-speech' }, + canDoVoiceConversion: { type: 'boolean', description: 'Supports voice conversion' }, + canUseStyle: { type: 'boolean', description: 'Supports the style parameter' }, + canUseSpeakerBoost: { type: 'boolean', description: 'Supports speaker boost' }, + languages: { + type: 'array', + description: 'Languages supported by the model', + items: { + type: 'object', + properties: { + languageId: { type: 'string', description: 'Language code' }, + name: { type: 'string', description: 'Language name' }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/elevenlabs/list-voices.ts b/apps/sim/tools/elevenlabs/list-voices.ts new file mode 100644 index 00000000000..533eaf46cd8 --- /dev/null +++ b/apps/sim/tools/elevenlabs/list-voices.ts @@ -0,0 +1,110 @@ +import type { + ElevenLabsListVoicesParams, + ElevenLabsListVoicesResponse, + ElevenLabsVoiceSummary, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsListVoicesTool: ToolConfig< + ElevenLabsListVoicesParams, + ElevenLabsListVoicesResponse +> = { + id: 'elevenlabs_list_voices', + name: 'ElevenLabs List Voices', + description: 'List the voices available in your ElevenLabs account', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search term to filter voices by name, description, labels, or category', + }, + category: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by category: premade, cloned, generated, or professional', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of voices to return (1-100, default 10)', + }, + nextPageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token from a previous response to fetch the next page of voices', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.search) query.set('search', params.search) + if (params.category) query.set('category', params.category) + if (params.pageSize !== undefined) query.set('page_size', String(params.pageSize)) + if (params.nextPageToken) query.set('next_page_token', params.nextPageToken) + const qs = query.toString() + return `https://api.elevenlabs.io/v2/voices${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'xi-api-key': params.apiKey, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const voices: ElevenLabsVoiceSummary[] = (data.voices ?? []).map((voice: any) => ({ + voiceId: voice.voice_id, + name: voice.name ?? null, + category: voice.category ?? null, + description: voice.description ?? null, + labels: voice.labels ?? null, + previewUrl: voice.preview_url ?? null, + settings: voice.settings ?? null, + })) + + return { + success: true, + output: { + voices, + totalCount: data.total_count ?? null, + hasMore: data.has_more ?? false, + nextPageToken: data.next_page_token ?? null, + }, + } + }, + + outputs: { + voices: { + type: 'array', + description: 'List of voices', + items: { + type: 'object', + properties: { + voiceId: { type: 'string', description: 'Unique voice identifier' }, + name: { type: 'string', description: 'Voice name' }, + category: { type: 'string', description: 'Voice category' }, + description: { type: 'string', description: 'Voice description' }, + labels: { type: 'json', description: 'Voice labels (accent, gender, age, use case)' }, + previewUrl: { type: 'string', description: 'URL to a preview audio sample' }, + settings: { type: 'json', description: 'Default voice settings' }, + }, + }, + }, + totalCount: { type: 'number', description: 'Total number of matching voices', optional: true }, + hasMore: { type: 'boolean', description: 'Whether more voices are available' }, + nextPageToken: { type: 'string', description: 'Token to fetch the next page', optional: true }, + }, +} diff --git a/apps/sim/tools/elevenlabs/sound-effects.ts b/apps/sim/tools/elevenlabs/sound-effects.ts new file mode 100644 index 00000000000..0bd95706523 --- /dev/null +++ b/apps/sim/tools/elevenlabs/sound-effects.ts @@ -0,0 +1,102 @@ +import type { + ElevenLabsAudioResponse, + ElevenLabsSoundEffectsParams, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsSoundEffectsTool: ToolConfig< + ElevenLabsSoundEffectsParams, + ElevenLabsAudioResponse +> = { + id: 'elevenlabs_sound_effects', + name: 'ElevenLabs Sound Effects', + description: 'Generate a sound effect from a text prompt using ElevenLabs', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + text: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The prompt describing the sound effect (e.g., "thunder rumbling in the distance")', + }, + modelId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The model to use (defaults to eleven_text_to_sound_v2)', + }, + durationSeconds: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Length of the sound in seconds (0.5-30). Omit to auto-determine', + }, + promptInfluence: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'How closely to follow the prompt from 0.0 to 1.0 (default 0.3)', + }, + loop: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to generate a seamlessly looping sound effect (default false)', + }, + }, + + request: { + url: '/api/tools/elevenlabs/audio', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: ( + params: ElevenLabsSoundEffectsParams & { + _context?: { workspaceId?: string; workflowId?: string; executionId?: string } + } + ) => ({ + operation: 'sound_effects', + apiKey: params.apiKey, + text: params.text, + modelId: params.modelId, + durationSeconds: params.durationSeconds, + promptInfluence: params.promptInfluence, + loop: params.loop, + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok || data.error) { + return { + success: false, + error: data.error || 'Sound effect generation failed', + output: { audioUrl: '' }, + } + } + return { + success: true, + output: { + audioUrl: data.audioUrl, + audioFile: data.audioFile, + }, + } + }, + + outputs: { + audioUrl: { type: 'string', description: 'URL of the generated sound effect' }, + audioFile: { type: 'file', description: 'The generated sound effect file' }, + }, +} diff --git a/apps/sim/tools/elevenlabs/speech-to-speech.ts b/apps/sim/tools/elevenlabs/speech-to-speech.ts new file mode 100644 index 00000000000..e926dd585b0 --- /dev/null +++ b/apps/sim/tools/elevenlabs/speech-to-speech.ts @@ -0,0 +1,94 @@ +import type { + ElevenLabsAudioResponse, + ElevenLabsSpeechToSpeechParams, +} from '@/tools/elevenlabs/types' +import type { ToolConfig } from '@/tools/types' + +export const elevenLabsSpeechToSpeechTool: ToolConfig< + ElevenLabsSpeechToSpeechParams, + ElevenLabsAudioResponse +> = { + id: 'elevenlabs_speech_to_speech', + name: 'ElevenLabs Speech to Speech', + description: 'Convert audio into a chosen ElevenLabs voice while preserving content and emotion', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your ElevenLabs API key', + }, + voiceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the target voice to convert the audio into', + }, + audioFile: { + type: 'file', + required: true, + visibility: 'user-only', + description: 'The source audio file to convert (e.g., MP3, WAV, M4A)', + }, + modelId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The model to use (defaults to eleven_english_sts_v2)', + }, + removeBackgroundNoise: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to isolate the voice and remove background noise (default false)', + }, + }, + + request: { + url: '/api/tools/elevenlabs/audio', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: ( + params: ElevenLabsSpeechToSpeechParams & { + _context?: { workspaceId?: string; workflowId?: string; executionId?: string } + } + ) => ({ + operation: 'speech_to_speech', + apiKey: params.apiKey, + voiceId: params.voiceId, + audioFile: params.audioFile, + modelId: params.modelId, + removeBackgroundNoise: params.removeBackgroundNoise, + workspaceId: params._context?.workspaceId, + workflowId: params._context?.workflowId, + executionId: params._context?.executionId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok || data.error) { + return { + success: false, + error: data.error || 'Speech-to-speech conversion failed', + output: { audioUrl: '' }, + } + } + return { + success: true, + output: { + audioUrl: data.audioUrl, + audioFile: data.audioFile, + }, + } + }, + + outputs: { + audioUrl: { type: 'string', description: 'URL of the converted audio' }, + audioFile: { type: 'file', description: 'The converted audio file' }, + }, +} diff --git a/apps/sim/tools/elevenlabs/types.ts b/apps/sim/tools/elevenlabs/types.ts index 7c00d48942f..7eb312c705f 100644 --- a/apps/sim/tools/elevenlabs/types.ts +++ b/apps/sim/tools/elevenlabs/types.ts @@ -17,4 +17,166 @@ export interface ElevenLabsTtsResponse extends ToolResponse { } } -export type ElevenLabsBlockResponse = ElevenLabsTtsResponse +/** Voice settings shared by get/edit settings and embedded voice objects. */ +export interface ElevenLabsVoiceSettings { + stability?: number | null + similarity_boost?: number | null + style?: number | null + use_speaker_boost?: boolean | null + speed?: number | null +} + +export interface ElevenLabsVoiceSummary { + voiceId: string + name: string | null + category: string | null + description: string | null + labels: Record | null + previewUrl: string | null + settings: ElevenLabsVoiceSettings | null +} + +export interface ElevenLabsListVoicesParams { + apiKey: string + search?: string + category?: string + pageSize?: number + nextPageToken?: string +} + +export interface ElevenLabsListVoicesResponse extends ToolResponse { + output: { + voices: ElevenLabsVoiceSummary[] + totalCount: number | null + hasMore: boolean + nextPageToken: string | null + } +} + +export interface ElevenLabsGetVoiceParams { + apiKey: string + voiceId: string +} + +export interface ElevenLabsGetVoiceResponse extends ToolResponse { + output: ElevenLabsVoiceSummary & { + availableForTiers: string[] + highQualityBaseModelIds: string[] + isOwner: boolean | null + } +} + +export interface ElevenLabsGetVoiceSettingsParams { + apiKey: string + voiceId: string +} + +export interface ElevenLabsGetVoiceSettingsResponse extends ToolResponse { + output: { + stability: number | null + similarityBoost: number | null + style: number | null + useSpeakerBoost: boolean | null + speed: number | null + } +} + +export interface ElevenLabsEditVoiceSettingsParams { + apiKey: string + voiceId: string + stability?: number + similarityBoost?: number + style?: number + useSpeakerBoost?: boolean + speed?: number +} + +export interface ElevenLabsEditVoiceSettingsResponse extends ToolResponse { + output: { + status: string + } +} + +export interface ElevenLabsModelLanguage { + languageId: string | null + name: string | null +} + +export interface ElevenLabsModelSummary { + modelId: string + name: string | null + description: string | null + canDoTextToSpeech: boolean | null + canDoVoiceConversion: boolean | null + canUseStyle: boolean | null + canUseSpeakerBoost: boolean | null + languages: ElevenLabsModelLanguage[] +} + +export interface ElevenLabsListModelsParams { + apiKey: string +} + +export interface ElevenLabsListModelsResponse extends ToolResponse { + output: { + models: ElevenLabsModelSummary[] + } +} + +export interface ElevenLabsGetUserParams { + apiKey: string +} + +export interface ElevenLabsGetUserResponse extends ToolResponse { + output: { + userId: string | null + isNewUser: boolean | null + subscription: { + tier: string | null + characterCount: number | null + characterLimit: number | null + canExtendCharacterLimit: boolean | null + status: string | null + nextCharacterCountResetUnix: number | null + } | null + } +} + +export interface ElevenLabsSoundEffectsParams { + apiKey: string + text: string + modelId?: string + durationSeconds?: number + promptInfluence?: number + loop?: boolean +} + +export interface ElevenLabsSpeechToSpeechParams { + apiKey: string + voiceId: string + audioFile?: UserFile + modelId?: string + removeBackgroundNoise?: boolean +} + +export interface ElevenLabsAudioIsolationParams { + apiKey: string + audioFile?: UserFile +} + +export interface ElevenLabsAudioResponse extends ToolResponse { + output: { + audioUrl: string + audioFile?: UserFile + } +} + +export type ElevenLabsBlockResponse = + | ElevenLabsTtsResponse + | ElevenLabsListVoicesResponse + | ElevenLabsGetVoiceResponse + | ElevenLabsGetVoiceSettingsResponse + | ElevenLabsEditVoiceSettingsResponse + | ElevenLabsListModelsResponse + | ElevenLabsGetUserResponse + | ElevenLabsAudioResponse diff --git a/apps/sim/tools/firecrawl/batch-scrape-status.ts b/apps/sim/tools/firecrawl/batch-scrape-status.ts new file mode 100644 index 00000000000..fd45a6dc8ca --- /dev/null +++ b/apps/sim/tools/firecrawl/batch-scrape-status.ts @@ -0,0 +1,87 @@ +import type { + FirecrawlBatchScrapeStatusParams, + FirecrawlBatchScrapeStatusResponse, +} from '@/tools/firecrawl/types' +import { CRAWLED_PAGE_OUTPUT_PROPERTIES } from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const batchScrapeStatusTool: ToolConfig< + FirecrawlBatchScrapeStatusParams, + FirecrawlBatchScrapeStatusResponse +> = { + id: 'firecrawl_batch_scrape_status', + name: 'Firecrawl Batch Scrape Status', + description: + 'Check the status and retrieve results of a previously started Firecrawl batch scrape job by its job ID.', + version: '1.0.0', + + params: { + jobId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the batch scrape job to check', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'GET', + url: (params) => + `https://api.firecrawl.dev/v2/batch/scrape/${encodeURIComponent(params.jobId.trim())}`, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + status: data.status, + total: data.total ?? 0, + completed: data.completed ?? 0, + creditsUsed: data.creditsUsed ?? 0, + expiresAt: data.expiresAt ?? null, + next: data.next ?? null, + pages: data.data ?? [], + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Current batch scrape status (scraping, completed, or failed)', + }, + total: { type: 'number', description: 'Total number of pages attempted' }, + completed: { type: 'number', description: 'Number of pages successfully scraped' }, + creditsUsed: { type: 'number', description: 'Credits consumed by the batch scrape' }, + expiresAt: { + type: 'string', + description: 'ISO timestamp when the batch scrape results expire', + optional: true, + }, + next: { + type: 'string', + description: 'URL to retrieve the next page of results when present', + optional: true, + }, + pages: { + type: 'array', + description: 'Array of scraped pages with their content and metadata', + items: { + type: 'object', + properties: CRAWLED_PAGE_OUTPUT_PROPERTIES, + }, + }, + }, +} diff --git a/apps/sim/tools/firecrawl/batch-scrape.ts b/apps/sim/tools/firecrawl/batch-scrape.ts new file mode 100644 index 00000000000..933c37d22ae --- /dev/null +++ b/apps/sim/tools/firecrawl/batch-scrape.ts @@ -0,0 +1,274 @@ +import { createLogger } from '@sim/logger' +import { sleep } from '@sim/utils/helpers' +import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' +import type { + FirecrawlBatchScrapeParams, + FirecrawlBatchScrapeResponse, +} from '@/tools/firecrawl/types' +import { CRAWLED_PAGE_OUTPUT_PROPERTIES } from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('FirecrawlBatchScrapeTool') + +const POLL_INTERVAL_MS = 5000 +const MAX_POLL_TIME_MS = DEFAULT_EXECUTION_TIMEOUT_MS + +/** + * Normalizes a list of URLs supplied as an array, a JSON-string array, or a + * newline-separated string into a trimmed string array. + */ +function normalizeUrls(value: unknown): string[] { + if (Array.isArray(value)) { + return value.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0) + } + + if (typeof value === 'string') { + const trimmed = value.trim() + if (trimmed === '') return [] + try { + const parsed = JSON.parse(trimmed) + if (Array.isArray(parsed)) { + return parsed.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0) + } + } catch {} + return trimmed + .split(/\r?\n/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + } + + return [] +} + +export const batchScrapeTool: ToolConfig = + { + id: 'firecrawl_batch_scrape', + name: 'Firecrawl Batch Scrape', + description: + 'Scrape multiple URLs in a single batch job and retrieve structured content from each page.', + version: '1.0.0', + + params: { + urls: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of URLs to scrape (e.g., ["https://example.com/page1", "https://example.com/page2"])', + }, + formats: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Output formats for scraped content (e.g., ["markdown"], ["markdown", "html"])', + }, + onlyMainContent: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Extract only main content from pages', + }, + maxConcurrency: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of concurrent scrapes', + }, + ignoreInvalidURLs: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Skip invalid URLs instead of failing the batch (default: true)', + }, + scrapeOptions: { + type: 'json', + required: false, + visibility: 'hidden', + description: 'Advanced scraping configuration options', + }, + zeroDataRetention: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Enable zero data retention', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + hosting: { + envKeyPrefix: 'FIRECRAWL_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'firecrawl', + pricing: { + type: 'custom', + getCost: (_params, output) => { + if (output.creditsUsed == null) { + throw new Error('Firecrawl response missing creditsUsed field') + } + + const creditsUsed = Number(output.creditsUsed) + if (Number.isNaN(creditsUsed)) { + throw new Error('Firecrawl response returned a non-numeric creditsUsed field') + } + + return { + cost: creditsUsed * 0.001, + metadata: { creditsUsed }, + } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 100, + }, + }, + + request: { + method: 'POST', + url: 'https://api.firecrawl.dev/v2/batch/scrape', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = { + urls: normalizeUrls(params.urls), + } + + const scrapeOptions: Record = { ...(params.scrapeOptions ?? {}) } + if (params.formats) scrapeOptions.formats = params.formats + if (typeof params.onlyMainContent === 'boolean') + scrapeOptions.onlyMainContent = params.onlyMainContent + if (Object.keys(scrapeOptions).length > 0) { + Object.assign(body, scrapeOptions) + } + + if (params.maxConcurrency != null) body.maxConcurrency = Number(params.maxConcurrency) + if (typeof params.ignoreInvalidURLs === 'boolean') + body.ignoreInvalidURLs = params.ignoreInvalidURLs + if (typeof params.zeroDataRetention === 'boolean') + body.zeroDataRetention = params.zeroDataRetention + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.id) { + throw new Error(data.error || 'Firecrawl did not return a batch scrape job id to poll') + } + + return { + success: true, + output: { + jobId: data.id, + invalidURLs: data.invalidURLs ?? [], + pages: [], + total: 0, + completed: 0, + creditsUsed: 0, + }, + } + }, + + postProcess: async (result, params) => { + if (!result.success) { + return result + } + + const jobId = result.output.jobId + const invalidURLs = result.output.invalidURLs ?? [] + logger.info(`Firecrawl batch scrape job ${jobId} created, polling for completion...`) + + let elapsedTime = 0 + + while (elapsedTime < MAX_POLL_TIME_MS) { + try { + const statusResponse = await fetch(`https://api.firecrawl.dev/v2/batch/scrape/${jobId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }, + }) + + if (!statusResponse.ok) { + throw new Error(`Failed to get batch scrape status: ${statusResponse.statusText}`) + } + + const batchData = await statusResponse.json() + logger.info(`Firecrawl batch scrape job ${jobId} status: ${batchData.status}`) + + if (batchData.status === 'completed') { + result.output = { + jobId, + invalidURLs, + pages: batchData.data ?? [], + total: batchData.total ?? 0, + completed: batchData.completed ?? 0, + creditsUsed: batchData.creditsUsed ?? 0, + } + return result + } + + if (batchData.status === 'failed') { + return { + ...result, + success: false, + error: `Batch scrape job failed: ${batchData.error || 'Unknown error'}`, + } + } + + await sleep(POLL_INTERVAL_MS) + elapsedTime += POLL_INTERVAL_MS + } catch (error: any) { + logger.error('Error polling for batch scrape job status:', { + message: error.message || 'Unknown error', + jobId, + }) + + return { + ...result, + success: false, + error: `Error polling for batch scrape job status: ${error.message || 'Unknown error'}`, + } + } + } + + logger.warn( + `Batch scrape job ${jobId} did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)` + ) + return { + ...result, + success: false, + error: `Batch scrape job did not complete within the maximum polling time (${MAX_POLL_TIME_MS / 1000}s)`, + } + }, + + outputs: { + pages: { + type: 'array', + description: 'Array of scraped pages with their content and metadata', + items: { + type: 'object', + properties: CRAWLED_PAGE_OUTPUT_PROPERTIES, + }, + }, + total: { type: 'number', description: 'Total number of pages attempted' }, + completed: { type: 'number', description: 'Number of pages successfully scraped' }, + invalidURLs: { + type: 'array', + description: 'URLs that were skipped because they were invalid', + optional: true, + items: { type: 'string', description: 'Invalid URL' }, + }, + }, + } diff --git a/apps/sim/tools/firecrawl/cancel-crawl.ts b/apps/sim/tools/firecrawl/cancel-crawl.ts new file mode 100644 index 00000000000..62e599bdba7 --- /dev/null +++ b/apps/sim/tools/firecrawl/cancel-crawl.ts @@ -0,0 +1,56 @@ +import type { + FirecrawlCancelCrawlParams, + FirecrawlCancelCrawlResponse, +} from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const cancelCrawlTool: ToolConfig = + { + id: 'firecrawl_cancel_crawl', + name: 'Firecrawl Cancel Crawl', + description: 'Cancel an in-progress Firecrawl crawl job by its job ID.', + version: '1.0.0', + + params: { + jobId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the crawl job to cancel', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'DELETE', + url: (params) => + `https://api.firecrawl.dev/v2/crawl/${encodeURIComponent(params.jobId.trim())}`, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + status: data.status ?? 'cancelled', + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Status of the cancelled crawl job (e.g., "cancelled")', + }, + }, + } diff --git a/apps/sim/tools/firecrawl/crawl-status.ts b/apps/sim/tools/firecrawl/crawl-status.ts new file mode 100644 index 00000000000..c29e9fe72b8 --- /dev/null +++ b/apps/sim/tools/firecrawl/crawl-status.ts @@ -0,0 +1,85 @@ +import type { + FirecrawlCrawlStatusParams, + FirecrawlCrawlStatusResponse, +} from '@/tools/firecrawl/types' +import { CRAWLED_PAGE_OUTPUT_PROPERTIES } from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const crawlStatusTool: ToolConfig = + { + id: 'firecrawl_crawl_status', + name: 'Firecrawl Crawl Status', + description: + 'Check the status and retrieve results of a previously started Firecrawl crawl job by its job ID.', + version: '1.0.0', + + params: { + jobId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the crawl job to check', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'GET', + url: (params) => + `https://api.firecrawl.dev/v2/crawl/${encodeURIComponent(params.jobId.trim())}`, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + status: data.status, + total: data.total ?? 0, + completed: data.completed ?? 0, + creditsUsed: data.creditsUsed ?? 0, + expiresAt: data.expiresAt ?? null, + next: data.next ?? null, + pages: data.data ?? [], + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Current crawl status (scraping, completed, or failed)', + }, + total: { type: 'number', description: 'Total number of pages attempted' }, + completed: { type: 'number', description: 'Number of pages successfully crawled' }, + creditsUsed: { type: 'number', description: 'Credits consumed by the crawl' }, + expiresAt: { + type: 'string', + description: 'ISO timestamp when the crawl results expire', + optional: true, + }, + next: { + type: 'string', + description: 'URL to retrieve the next page of results when present', + optional: true, + }, + pages: { + type: 'array', + description: 'Array of crawled pages with their content and metadata', + items: { + type: 'object', + properties: CRAWLED_PAGE_OUTPUT_PROPERTIES, + }, + }, + }, + } diff --git a/apps/sim/tools/firecrawl/credit-usage.ts b/apps/sim/tools/firecrawl/credit-usage.ts new file mode 100644 index 00000000000..540c3f5b95d --- /dev/null +++ b/apps/sim/tools/firecrawl/credit-usage.ts @@ -0,0 +1,68 @@ +import type { + FirecrawlCreditUsageParams, + FirecrawlCreditUsageResponse, +} from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const creditUsageTool: ToolConfig = + { + id: 'firecrawl_credit_usage', + name: 'Firecrawl Credit Usage', + description: 'Retrieve the remaining and allocated Firecrawl credits for the team.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'GET', + url: 'https://api.firecrawl.dev/v2/team/credit-usage', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const usage = data.data ?? {} + + return { + success: true, + output: { + remainingCredits: usage.remainingCredits ?? null, + planCredits: usage.planCredits ?? null, + billingPeriodStart: usage.billingPeriodStart ?? null, + billingPeriodEnd: usage.billingPeriodEnd ?? null, + }, + } + }, + + outputs: { + remainingCredits: { + type: 'number', + description: 'Number of credits remaining for the team', + }, + planCredits: { + type: 'number', + description: 'Credits allocated in the current plan', + optional: true, + }, + billingPeriodStart: { + type: 'string', + description: 'Start of the current billing period', + optional: true, + }, + billingPeriodEnd: { + type: 'string', + description: 'End of the current billing period', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/firecrawl/extract-status.ts b/apps/sim/tools/firecrawl/extract-status.ts new file mode 100644 index 00000000000..99cfcfadd24 --- /dev/null +++ b/apps/sim/tools/firecrawl/extract-status.ts @@ -0,0 +1,82 @@ +import type { + FirecrawlExtractStatusParams, + FirecrawlExtractStatusResponse, +} from '@/tools/firecrawl/types' +import type { ToolConfig } from '@/tools/types' + +export const extractStatusTool: ToolConfig< + FirecrawlExtractStatusParams, + FirecrawlExtractStatusResponse +> = { + id: 'firecrawl_extract_status', + name: 'Firecrawl Extract Status', + description: + 'Check the status and retrieve results of a previously started Firecrawl extract job by its job ID.', + version: '1.0.0', + + params: { + jobId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the extract job to check', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Firecrawl API key', + }, + }, + + request: { + method: 'GET', + url: (params) => + `https://api.firecrawl.dev/v2/extract/${encodeURIComponent(params.jobId.trim())}`, + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + status: data.status, + data: data.data ?? {}, + expiresAt: data.expiresAt ?? null, + creditsUsed: data.creditsUsed ?? null, + tokensUsed: data.tokensUsed ?? null, + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Current extract status (processing, completed, failed, or cancelled)', + }, + data: { + type: 'json', + description: 'Extracted structured data according to the schema or prompt', + }, + expiresAt: { + type: 'string', + description: 'ISO timestamp when the extract results expire', + optional: true, + }, + creditsUsed: { + type: 'number', + description: 'Number of credits used by the extract job', + optional: true, + }, + tokensUsed: { + type: 'number', + description: 'Number of tokens used by the extract job', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/firecrawl/index.ts b/apps/sim/tools/firecrawl/index.ts index 9d868ba7d37..4c1700044c7 100644 --- a/apps/sim/tools/firecrawl/index.ts +++ b/apps/sim/tools/firecrawl/index.ts @@ -1,6 +1,12 @@ import { agentTool } from '@/tools/firecrawl/agent' +import { batchScrapeTool } from '@/tools/firecrawl/batch-scrape' +import { batchScrapeStatusTool } from '@/tools/firecrawl/batch-scrape-status' +import { cancelCrawlTool } from '@/tools/firecrawl/cancel-crawl' import { crawlTool } from '@/tools/firecrawl/crawl' +import { crawlStatusTool } from '@/tools/firecrawl/crawl-status' +import { creditUsageTool } from '@/tools/firecrawl/credit-usage' import { extractTool } from '@/tools/firecrawl/extract' +import { extractStatusTool } from '@/tools/firecrawl/extract-status' import { mapTool } from '@/tools/firecrawl/map' import { parseTool } from '@/tools/firecrawl/parse' import { scrapeTool } from '@/tools/firecrawl/scrape' @@ -13,3 +19,9 @@ export const firecrawlMapTool = mapTool export const firecrawlExtractTool = extractTool export const firecrawlAgentTool = agentTool export const firecrawlParseTool = parseTool +export const firecrawlCrawlStatusTool = crawlStatusTool +export const firecrawlCancelCrawlTool = cancelCrawlTool +export const firecrawlBatchScrapeTool = batchScrapeTool +export const firecrawlBatchScrapeStatusTool = batchScrapeStatusTool +export const firecrawlExtractStatusTool = extractStatusTool +export const firecrawlCreditUsageTool = creditUsageTool diff --git a/apps/sim/tools/firecrawl/types.ts b/apps/sim/tools/firecrawl/types.ts index 736f5603824..8158233c2c8 100644 --- a/apps/sim/tools/firecrawl/types.ts +++ b/apps/sim/tools/firecrawl/types.ts @@ -559,6 +559,117 @@ export interface ParseResponse extends ToolResponse { } } +interface CrawledPage { + markdown: string + html?: string + rawHtml?: string + links?: string[] + screenshot?: string + metadata: { + title: string + description?: string + language?: string + sourceURL: string + statusCode: number + ogLocaleAlternate?: string[] + } +} + +export interface FirecrawlCrawlStatusParams { + apiKey: string + jobId: string +} + +export interface FirecrawlCrawlStatusResponse extends ToolResponse { + output: { + status: string + total: number + completed: number + creditsUsed: number + expiresAt?: string | null + next?: string | null + pages: CrawledPage[] + } +} + +export interface FirecrawlCancelCrawlParams { + apiKey: string + jobId: string +} + +export interface FirecrawlCancelCrawlResponse extends ToolResponse { + output: { + status: string + } +} + +export interface FirecrawlBatchScrapeParams { + apiKey: string + urls: string[] | string + formats?: string[] + onlyMainContent?: boolean + maxConcurrency?: number + ignoreInvalidURLs?: boolean + scrapeOptions?: ScrapeOptions + zeroDataRetention?: boolean +} + +export interface FirecrawlBatchScrapeResponse extends ToolResponse { + output: { + jobId?: string + invalidURLs?: string[] + pages: CrawledPage[] + total: number + completed: number + creditsUsed?: number + } +} + +export interface FirecrawlBatchScrapeStatusParams { + apiKey: string + jobId: string +} + +export interface FirecrawlBatchScrapeStatusResponse extends ToolResponse { + output: { + status: string + total: number + completed: number + creditsUsed: number + expiresAt?: string | null + next?: string | null + pages: CrawledPage[] + } +} + +export interface FirecrawlExtractStatusParams { + apiKey: string + jobId: string +} + +export interface FirecrawlExtractStatusResponse extends ToolResponse { + output: { + status: string + data: Record | unknown[] + expiresAt?: string | null + creditsUsed?: number | null + tokensUsed?: number | null + } +} + +export interface FirecrawlCreditUsageParams { + apiKey: string +} + +export interface FirecrawlCreditUsageResponse extends ToolResponse { + output: { + remainingCredits: number | null + planCredits?: number | null + billingPeriodStart?: string | null + billingPeriodEnd?: string | null + } +} + export type FirecrawlResponse = | ScrapeResponse | SearchResponse @@ -567,3 +678,9 @@ export type FirecrawlResponse = | ExtractResponse | AgentResponse | ParseResponse + | FirecrawlCrawlStatusResponse + | FirecrawlCancelCrawlResponse + | FirecrawlBatchScrapeResponse + | FirecrawlBatchScrapeStatusResponse + | FirecrawlExtractStatusResponse + | FirecrawlCreditUsageResponse diff --git a/apps/sim/tools/google_drive/create_comment.ts b/apps/sim/tools/google_drive/create_comment.ts new file mode 100644 index 00000000000..9487b5d6a57 --- /dev/null +++ b/apps/sim/tools/google_drive/create_comment.ts @@ -0,0 +1,112 @@ +import type { GoogleDriveComment, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_COMMENT_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveCreateCommentParams extends GoogleDriveToolParams { + fileId: string + content: string + anchor?: string +} + +interface GoogleDriveCreateCommentResponse extends ToolResponse { + output: { + comment: GoogleDriveComment + } +} + +export const createCommentTool: ToolConfig< + GoogleDriveCreateCommentParams, + GoogleDriveCreateCommentResponse +> = { + id: 'google_drive_create_comment', + name: 'Create Google Drive Comment', + description: 'Add a comment to a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to comment on', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The plain text content of the comment', + }, + anchor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'A region of the document the comment refers to (JSON anchor string)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/comments` + ) + url.searchParams.append('fields', ALL_COMMENT_FIELDS) + return url.toString() + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + content: params.content, + } + if (params.anchor) { + body.anchor = params.anchor + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as GoogleDriveComment + + return { + success: true, + output: { + comment: data, + }, + } + }, + + outputs: { + comment: { + type: 'json', + description: 'The created comment', + properties: { + id: { type: 'string', description: 'Comment ID' }, + content: { type: 'string', description: 'Plain text content of the comment' }, + htmlContent: { type: 'string', description: 'HTML-formatted content of the comment' }, + author: { type: 'json', description: 'User who authored the comment' }, + createdTime: { type: 'string', description: 'When the comment was created' }, + modifiedTime: { type: 'string', description: 'When the comment was last modified' }, + resolved: { type: 'boolean', description: 'Whether the comment has been resolved' }, + deleted: { type: 'boolean', description: 'Whether the comment has been deleted' }, + anchor: { type: 'string', description: 'Region of the document the comment refers to' }, + quotedFileContent: { type: 'json', description: 'The file content the comment quotes' }, + replies: { type: 'json', description: 'Threaded replies to the comment' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/delete_comment.ts b/apps/sim/tools/google_drive/delete_comment.ts new file mode 100644 index 00000000000..6ff41dc3322 --- /dev/null +++ b/apps/sim/tools/google_drive/delete_comment.ts @@ -0,0 +1,75 @@ +import type { GoogleDriveToolParams } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveDeleteCommentParams extends GoogleDriveToolParams { + fileId: string + commentId: string +} + +interface GoogleDriveDeleteCommentResponse extends ToolResponse { + output: { + deleted: boolean + fileId: string + commentId: string + } +} + +export const deleteCommentTool: ToolConfig< + GoogleDriveDeleteCommentParams, + GoogleDriveDeleteCommentResponse +> = { + id: 'google_drive_delete_comment', + name: 'Delete Google Drive Comment', + description: 'Delete a comment from a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file the comment belongs to', + }, + commentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the comment to delete', + }, + }, + + request: { + url: (params) => + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/comments/${params.commentId?.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (_response: Response, params) => ({ + success: true, + output: { + deleted: true, + fileId: params?.fileId ?? '', + commentId: params?.commentId ?? '', + }, + }), + + outputs: { + deleted: { type: 'boolean', description: 'Whether the comment was successfully deleted' }, + fileId: { type: 'string', description: 'The ID of the file' }, + commentId: { type: 'string', description: 'The ID of the deleted comment' }, + }, +} diff --git a/apps/sim/tools/google_drive/export.ts b/apps/sim/tools/google_drive/export.ts new file mode 100644 index 00000000000..24f1ebb98e8 --- /dev/null +++ b/apps/sim/tools/google_drive/export.ts @@ -0,0 +1,85 @@ +import type { GoogleDriveToolParams } from '@/tools/google_drive/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveExportParams extends GoogleDriveToolParams { + fileId: string + mimeType: string + fileName?: string +} + +interface GoogleDriveExportResponse extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: string + size: number + } + exportedMimeType: string + } +} + +export const exportTool: ToolConfig = { + id: 'google_drive_export', + name: 'Export Google Drive File', + description: + 'Export a Google Workspace file (Docs, Sheets, Slides, Drawings) to a chosen format such as PDF, DOCX, XLSX, or CSV', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Google Drive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the Google Workspace file to export', + }, + mimeType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The target MIME type to export to (e.g. application/pdf, text/csv)', + }, + fileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional filename override for the exported file', + }, + }, + + request: { + url: '/api/tools/google_drive/export', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + fileId: params.fileId, + mimeType: params.mimeType, + fileName: params.fileName, + }), + }, + + outputs: { + file: { + type: 'file', + description: 'Exported file stored in execution files', + }, + exportedMimeType: { + type: 'string', + description: 'The MIME type the file was exported to', + }, + }, +} diff --git a/apps/sim/tools/google_drive/get_revision.ts b/apps/sim/tools/google_drive/get_revision.ts new file mode 100644 index 00000000000..0c009566771 --- /dev/null +++ b/apps/sim/tools/google_drive/get_revision.ts @@ -0,0 +1,98 @@ +import type { GoogleDriveRevision, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_REVISION_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveGetRevisionParams extends GoogleDriveToolParams { + fileId: string + revisionId: string +} + +interface GoogleDriveGetRevisionResponse extends ToolResponse { + output: { + revision: GoogleDriveRevision + } +} + +export const getRevisionTool: ToolConfig< + GoogleDriveGetRevisionParams, + GoogleDriveGetRevisionResponse +> = { + id: 'google_drive_get_revision', + name: 'Get Google Drive Revision', + description: 'Get metadata for a specific revision of a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file the revision belongs to', + }, + revisionId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the revision to retrieve', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/revisions/${params.revisionId?.trim()}` + ) + url.searchParams.append('fields', ALL_REVISION_FIELDS) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = (await response.json()) as GoogleDriveRevision + + return { + success: true, + output: { + revision: data, + }, + } + }, + + outputs: { + revision: { + type: 'json', + description: 'The revision metadata', + properties: { + id: { type: 'string', description: 'Revision ID' }, + mimeType: { type: 'string', description: 'MIME type of the revision' }, + modifiedTime: { type: 'string', description: 'When this revision was created' }, + keepForever: { + type: 'boolean', + description: 'Whether this revision is preserved forever', + }, + published: { type: 'boolean', description: 'Whether this revision is published' }, + publishedLink: { type: 'string', description: 'Public link to the published revision' }, + lastModifyingUser: { type: 'json', description: 'User who created this revision' }, + originalFilename: { type: 'string', description: 'Original filename for binary revisions' }, + md5Checksum: { type: 'string', description: 'MD5 checksum for binary revisions' }, + size: { type: 'string', description: 'Size of the revision in bytes' }, + exportLinks: { type: 'json', description: 'Export format links for the revision' }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_drive/index.ts b/apps/sim/tools/google_drive/index.ts index 7c81c6c2fc7..d6e5167d4c7 100644 --- a/apps/sim/tools/google_drive/index.ts +++ b/apps/sim/tools/google_drive/index.ts @@ -1,12 +1,18 @@ import { copyTool } from '@/tools/google_drive/copy' +import { createCommentTool } from '@/tools/google_drive/create_comment' import { createFolderTool } from '@/tools/google_drive/create_folder' import { deleteTool } from '@/tools/google_drive/delete' +import { deleteCommentTool } from '@/tools/google_drive/delete_comment' import { downloadTool } from '@/tools/google_drive/download' +import { exportTool } from '@/tools/google_drive/export' import { getAboutTool } from '@/tools/google_drive/get_about' import { getContentTool } from '@/tools/google_drive/get_content' import { getFileTool } from '@/tools/google_drive/get_file' +import { getRevisionTool } from '@/tools/google_drive/get_revision' import { listTool } from '@/tools/google_drive/list' +import { listCommentsTool } from '@/tools/google_drive/list_comments' import { listPermissionsTool } from '@/tools/google_drive/list_permissions' +import { listRevisionsTool } from '@/tools/google_drive/list_revisions' import { moveTool } from '@/tools/google_drive/move' import { searchTool } from '@/tools/google_drive/search' import { shareTool } from '@/tools/google_drive/share' @@ -17,14 +23,20 @@ import { updateTool } from '@/tools/google_drive/update' import { uploadTool } from '@/tools/google_drive/upload' export const googleDriveCopyTool = copyTool +export const googleDriveCreateCommentTool = createCommentTool export const googleDriveCreateFolderTool = createFolderTool export const googleDriveDeleteTool = deleteTool +export const googleDriveDeleteCommentTool = deleteCommentTool export const googleDriveDownloadTool = downloadTool +export const googleDriveExportTool = exportTool export const googleDriveGetAboutTool = getAboutTool export const googleDriveGetContentTool = getContentTool export const googleDriveGetFileTool = getFileTool +export const googleDriveGetRevisionTool = getRevisionTool export const googleDriveListTool = listTool +export const googleDriveListCommentsTool = listCommentsTool export const googleDriveListPermissionsTool = listPermissionsTool +export const googleDriveListRevisionsTool = listRevisionsTool export const googleDriveMoveTool = moveTool export const googleDriveSearchTool = searchTool export const googleDriveShareTool = shareTool diff --git a/apps/sim/tools/google_drive/list_comments.ts b/apps/sim/tools/google_drive/list_comments.ts new file mode 100644 index 00000000000..28ac3aeec3f --- /dev/null +++ b/apps/sim/tools/google_drive/list_comments.ts @@ -0,0 +1,140 @@ +import type { GoogleDriveComment, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_COMMENT_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveListCommentsParams extends GoogleDriveToolParams { + fileId: string + includeDeleted?: boolean + pageSize?: number + pageToken?: string + startModifiedTime?: string +} + +interface GoogleDriveListCommentsResponse extends ToolResponse { + output: { + comments: GoogleDriveComment[] + nextPageToken?: string + } +} + +export const listCommentsTool: ToolConfig< + GoogleDriveListCommentsParams, + GoogleDriveListCommentsResponse +> = { + id: 'google_drive_list_comments', + name: 'List Google Drive Comments', + description: 'List comments on a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to list comments for', + }, + includeDeleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include deleted comments (their content is stripped)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of comments to return (1-100, default 20)', + }, + startModifiedTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only return comments modified after this RFC 3339 timestamp', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The page token to use for pagination', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/comments` + ) + url.searchParams.append('fields', `nextPageToken,comments(${ALL_COMMENT_FIELDS})`) + if (params.includeDeleted !== undefined) { + url.searchParams.append('includeDeleted', String(params.includeDeleted)) + } + if (params.pageSize) { + url.searchParams.append('pageSize', String(params.pageSize)) + } + if (params.startModifiedTime) { + url.searchParams.append('startModifiedTime', params.startModifiedTime) + } + if (params.pageToken) { + url.searchParams.append('pageToken', params.pageToken) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + comments: (data.comments ?? []) as GoogleDriveComment[], + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + comments: { + type: 'array', + description: 'List of comments on the file', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Comment ID' }, + content: { type: 'string', description: 'Plain text content of the comment' }, + htmlContent: { type: 'string', description: 'HTML-formatted content of the comment' }, + author: { type: 'json', description: 'User who authored the comment' }, + createdTime: { type: 'string', description: 'When the comment was created' }, + modifiedTime: { type: 'string', description: 'When the comment was last modified' }, + resolved: { type: 'boolean', description: 'Whether the comment has been resolved' }, + deleted: { type: 'boolean', description: 'Whether the comment has been deleted' }, + anchor: { type: 'string', description: 'Region of the document the comment refers to' }, + quotedFileContent: { + type: 'json', + description: 'The file content the comment quotes', + }, + replies: { type: 'json', description: 'Threaded replies to the comment' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of comments', + }, + }, +} diff --git a/apps/sim/tools/google_drive/list_revisions.ts b/apps/sim/tools/google_drive/list_revisions.ts new file mode 100644 index 00000000000..6bfbad61d37 --- /dev/null +++ b/apps/sim/tools/google_drive/list_revisions.ts @@ -0,0 +1,126 @@ +import type { GoogleDriveRevision, GoogleDriveToolParams } from '@/tools/google_drive/types' +import { ALL_REVISION_FIELDS } from '@/tools/google_drive/utils' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +interface GoogleDriveListRevisionsParams extends GoogleDriveToolParams { + fileId: string + pageSize?: number + pageToken?: string +} + +interface GoogleDriveListRevisionsResponse extends ToolResponse { + output: { + revisions: GoogleDriveRevision[] + nextPageToken?: string + } +} + +export const listRevisionsTool: ToolConfig< + GoogleDriveListRevisionsParams, + GoogleDriveListRevisionsResponse +> = { + id: 'google_drive_list_revisions', + name: 'List Google Drive Revisions', + description: 'List the revision history of a file in Google Drive', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-drive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file to list revisions for', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of revisions to return (1-1000, default 200)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The page token to use for pagination', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/revisions` + ) + url.searchParams.append('fields', `nextPageToken,revisions(${ALL_REVISION_FIELDS})`) + if (params.pageSize) { + url.searchParams.append('pageSize', String(params.pageSize)) + } + if (params.pageToken) { + url.searchParams.append('pageToken', params.pageToken) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + revisions: (data.revisions ?? []) as GoogleDriveRevision[], + nextPageToken: data.nextPageToken, + }, + } + }, + + outputs: { + revisions: { + type: 'array', + description: 'List of revisions for the file (most recent last)', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Revision ID' }, + mimeType: { type: 'string', description: 'MIME type of the revision' }, + modifiedTime: { type: 'string', description: 'When this revision was created' }, + keepForever: { + type: 'boolean', + description: 'Whether this revision is preserved forever', + }, + published: { type: 'boolean', description: 'Whether this revision is published' }, + publishedLink: { type: 'string', description: 'Public link to the published revision' }, + lastModifyingUser: { + type: 'json', + description: 'User who created this revision', + }, + originalFilename: { + type: 'string', + description: 'Original filename for binary revisions', + }, + md5Checksum: { type: 'string', description: 'MD5 checksum for binary revisions' }, + size: { type: 'string', description: 'Size of the revision in bytes' }, + exportLinks: { type: 'json', description: 'Export format links for the revision' }, + }, + }, + }, + nextPageToken: { + type: 'string', + description: 'Token for fetching the next page of revisions', + }, + }, +} diff --git a/apps/sim/tools/google_drive/types.ts b/apps/sim/tools/google_drive/types.ts index dcb41e3e536..c2e16522900 100644 --- a/apps/sim/tools/google_drive/types.ts +++ b/apps/sim/tools/google_drive/types.ts @@ -184,6 +184,38 @@ export interface GoogleDriveRevision { kind?: string } +/** A threaded reply attached to a comment. */ +export interface GoogleDriveCommentReply { + id?: string + kind?: string + createdTime?: string + modifiedTime?: string + author?: GoogleDriveUser + htmlContent?: string + content?: string + deleted?: boolean + action?: string // 'resolve' | 'reopen' +} + +/** A comment on a Google Drive file. */ +export interface GoogleDriveComment { + id?: string + kind?: string + createdTime?: string + modifiedTime?: string + author?: GoogleDriveUser + htmlContent?: string + content?: string + deleted?: boolean + resolved?: boolean + anchor?: string + quotedFileContent?: { + mimeType?: string + value?: string + } + replies?: GoogleDriveCommentReply[] +} + // Complete file metadata - all 50+ fields from Google Drive API v3 export interface GoogleDriveFile { // Basic Info diff --git a/apps/sim/tools/google_drive/utils.ts b/apps/sim/tools/google_drive/utils.ts index fd046cf1b83..340e84dc713 100644 --- a/apps/sim/tools/google_drive/utils.ts +++ b/apps/sim/tools/google_drive/utils.ts @@ -97,6 +97,41 @@ export const ALL_REVISION_FIELDS = [ 'kind', ].join(',') +/** All reply fields requested from the Google Drive API v3. */ +const ALL_REPLY_FIELDS = [ + 'id', + 'kind', + 'createdTime', + 'modifiedTime', + 'author', + 'htmlContent', + 'content', + 'deleted', + 'action', +].join(',') + +/** All comment fields requested from the Google Drive API v3. */ +export const ALL_COMMENT_FIELDS = [ + 'id', + 'kind', + 'createdTime', + 'modifiedTime', + 'author', + 'htmlContent', + 'content', + 'deleted', + 'resolved', + 'anchor', + 'quotedFileContent', + `replies(${ALL_REPLY_FIELDS})`, +].join(',') + +/** + * Maximum bytes accepted when exporting a Google Workspace file. + * Mirrors Google's own 10 MB export ceiling and keeps memory bounded. + */ +export const MAX_EXPORT_BYTES = 10 * 1024 * 1024 + export const GOOGLE_WORKSPACE_MIME_TYPES = [ 'application/vnd.google-apps.document', // Google Docs 'application/vnd.google-apps.spreadsheet', // Google Sheets diff --git a/apps/sim/tools/pinecone/delete_vectors.ts b/apps/sim/tools/pinecone/delete_vectors.ts new file mode 100644 index 00000000000..3824ef79359 --- /dev/null +++ b/apps/sim/tools/pinecone/delete_vectors.ts @@ -0,0 +1,107 @@ +import type { PineconeDeleteVectorsParams, PineconeResponse } from '@/tools/pinecone/types' +import { parseJsonParam } from '@/tools/pinecone/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteVectorsTool: ToolConfig = { + id: 'pinecone_delete_vectors', + name: 'Pinecone Delete Vectors', + description: 'Delete vectors from a Pinecone namespace by IDs, by metadata filter, or delete all', + version: '1.0', + + params: { + indexHost: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full Pinecone index host URL (e.g., "https://my-index-abc123.svc.pinecone.io")', + }, + namespace: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Namespace to delete vectors from (e.g., "documents", "embeddings")', + }, + ids: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: + 'Vector IDs to delete (1-1000 items). Mutually exclusive with deleteAll and filter', + }, + deleteAll: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Delete all vectors in the namespace. Mutually exclusive with ids and filter', + }, + filter: { + type: 'object', + required: false, + visibility: 'user-or-llm', + description: + 'Metadata filter selecting vectors to delete (e.g., {"category": {"$eq": "product"}}). Mutually exclusive with ids and deleteAll', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, + }, + + request: { + method: 'POST', + url: (params) => `${params.indexHost}/vectors/delete`, + headers: (params) => ({ + 'Api-Key': params.apiKey, + 'Content-Type': 'application/json', + 'X-Pinecone-API-Version': '2025-01', + }), + body: (params) => { + const body: Record = {} + if (params.namespace) { + body.namespace = params.namespace + } + if (params.ids != null && params.ids !== '') { + const ids = parseJsonParam(params.ids, 'ids') + if (Array.isArray(ids) && ids.length > 0) { + body.ids = ids + } + } + if (params.deleteAll === true || params.deleteAll === 'true') { + body.deleteAll = true + } + if (params.filter != null && params.filter !== '') { + body.filter = parseJsonParam(params.filter, 'filter') + } + + const selectorCount = [body.ids, body.deleteAll, body.filter].filter( + (selector) => selector !== undefined + ).length + if (selectorCount === 0) { + throw new Error('Provide exactly one of ids, deleteAll, or filter to delete vectors') + } + if (selectorCount > 1) { + throw new Error('ids, deleteAll, and filter are mutually exclusive — provide only one') + } + + return body + }, + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + statusText: response.status === 200 ? 'Deleted' : response.statusText, + }, + } + }, + + outputs: { + statusText: { + type: 'string', + description: 'Status of the delete operation', + }, + }, +} diff --git a/apps/sim/tools/pinecone/describe_index.ts b/apps/sim/tools/pinecone/describe_index.ts new file mode 100644 index 00000000000..6f757df5d74 --- /dev/null +++ b/apps/sim/tools/pinecone/describe_index.ts @@ -0,0 +1,79 @@ +import type { + PineconeDescribeIndexParams, + PineconeIndexModel, + PineconeResponse, +} from '@/tools/pinecone/types' +import type { ToolConfig } from '@/tools/types' + +export const describeIndexTool: ToolConfig = { + id: 'pinecone_describe_index', + name: 'Pinecone Describe Index', + description: 'Get the configuration and status of a Pinecone index by name', + version: '1.0', + + params: { + indexName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the index to describe', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, + }, + + request: { + method: 'GET', + url: (params) => + `https://api.pinecone.io/indexes/${encodeURIComponent(params.indexName.trim())}`, + headers: (params) => ({ + 'Api-Key': params.apiKey, + Accept: 'application/json', + 'X-Pinecone-API-Version': '2025-01', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + const index: PineconeIndexModel = { + name: data.name, + dimension: data.dimension ?? null, + metric: data.metric ?? null, + host: data.host ?? null, + vectorType: data.vectorType ?? data.vector_type ?? null, + deletionProtection: data.deletionProtection ?? data.deletion_protection ?? null, + tags: data.tags ?? null, + spec: data.spec ?? null, + status: data.status ?? null, + } + return { + success: true, + output: { index }, + } + }, + + outputs: { + index: { + type: 'object', + description: 'Index configuration and status', + properties: { + name: { type: 'string', description: 'Index name' }, + dimension: { type: 'number', description: 'Vector dimensionality' }, + metric: { type: 'string', description: 'Distance metric (cosine, euclidean, dotproduct)' }, + host: { type: 'string', description: 'Index host URL for data-plane operations' }, + vectorType: { type: 'string', description: 'Vector type (dense or sparse)' }, + deletionProtection: { + type: 'string', + description: 'Deletion protection (enabled or disabled)', + }, + tags: { type: 'object', description: 'Custom user tags on the index' }, + spec: { type: 'object', description: 'Index spec (serverless or pod configuration)' }, + status: { type: 'object', description: 'Index status with ready and state' }, + }, + }, + }, +} diff --git a/apps/sim/tools/pinecone/describe_index_stats.ts b/apps/sim/tools/pinecone/describe_index_stats.ts new file mode 100644 index 00000000000..a11441baa17 --- /dev/null +++ b/apps/sim/tools/pinecone/describe_index_stats.ts @@ -0,0 +1,92 @@ +import type { PineconeDescribeIndexStatsParams, PineconeResponse } from '@/tools/pinecone/types' +import { parseJsonParam } from '@/tools/pinecone/utils' +import type { ToolConfig } from '@/tools/types' + +export const describeIndexStatsTool: ToolConfig< + PineconeDescribeIndexStatsParams, + PineconeResponse +> = { + id: 'pinecone_describe_index_stats', + name: 'Pinecone Describe Index Stats', + description: 'Get statistics about a Pinecone index, including per-namespace vector counts', + version: '1.0', + + params: { + indexHost: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full Pinecone index host URL (e.g., "https://my-index-abc123.svc.pinecone.io")', + }, + filter: { + type: 'object', + required: false, + visibility: 'user-or-llm', + description: + 'Metadata filter to limit which vectors are counted (pod-based indexes only, e.g., {"category": {"$eq": "product"}})', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, + }, + + request: { + method: 'POST', + url: (params) => `${params.indexHost}/describe_index_stats`, + headers: (params) => ({ + 'Api-Key': params.apiKey, + 'Content-Type': 'application/json', + 'X-Pinecone-API-Version': '2025-01', + }), + body: (params) => { + const body: Record = {} + if (params.filter != null && params.filter !== '') { + body.filter = parseJsonParam(params.filter, 'filter') + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + const rawNamespaces = (data.namespaces ?? {}) as Record< + string, + { vectorCount?: number; vector_count?: number } + > + const namespaces: Record = {} + for (const [name, summary] of Object.entries(rawNamespaces)) { + namespaces[name] = { vectorCount: summary.vectorCount ?? summary.vector_count ?? null } + } + return { + success: true, + output: { + namespaces, + dimension: data.dimension ?? null, + indexFullness: data.indexFullness ?? data.index_fullness ?? null, + totalVectorCount: data.totalVectorCount ?? data.total_vector_count ?? null, + }, + } + }, + + outputs: { + namespaces: { + type: 'json', + description: 'Map of namespace name to its summary including vectorCount', + }, + dimension: { + type: 'number', + description: 'Dimensionality of the indexed vectors', + }, + indexFullness: { + type: 'number', + description: 'Fullness of the index (pod-based indexes only)', + }, + totalVectorCount: { + type: 'number', + description: 'Total number of vectors across all namespaces', + }, + }, +} diff --git a/apps/sim/tools/pinecone/index.ts b/apps/sim/tools/pinecone/index.ts index ae1dccdfcee..8b0c32d526e 100644 --- a/apps/sim/tools/pinecone/index.ts +++ b/apps/sim/tools/pinecone/index.ts @@ -1,11 +1,23 @@ +import { deleteVectorsTool } from '@/tools/pinecone/delete_vectors' +import { describeIndexTool } from '@/tools/pinecone/describe_index' +import { describeIndexStatsTool } from '@/tools/pinecone/describe_index_stats' import { fetchTool } from '@/tools/pinecone/fetch' import { generateEmbeddingsTool } from '@/tools/pinecone/generate_embeddings' +import { listIndexesTool } from '@/tools/pinecone/list_indexes' +import { listVectorIdsTool } from '@/tools/pinecone/list_vector_ids' import { searchTextTool } from '@/tools/pinecone/search_text' import { searchVectorTool } from '@/tools/pinecone/search_vector' +import { updateVectorTool } from '@/tools/pinecone/update_vector' import { upsertTextTool } from '@/tools/pinecone/upsert_text' +export const pineconeDeleteVectorsTool = deleteVectorsTool +export const pineconeDescribeIndexTool = describeIndexTool +export const pineconeDescribeIndexStatsTool = describeIndexStatsTool export const pineconeFetchTool = fetchTool export const pineconeGenerateEmbeddingsTool = generateEmbeddingsTool +export const pineconeListIndexesTool = listIndexesTool +export const pineconeListVectorIdsTool = listVectorIdsTool export const pineconeSearchTextTool = searchTextTool export const pineconeSearchVectorTool = searchVectorTool +export const pineconeUpdateVectorTool = updateVectorTool export const pineconeUpsertTextTool = upsertTextTool diff --git a/apps/sim/tools/pinecone/list_indexes.ts b/apps/sim/tools/pinecone/list_indexes.ts new file mode 100644 index 00000000000..e4190ff91d3 --- /dev/null +++ b/apps/sim/tools/pinecone/list_indexes.ts @@ -0,0 +1,84 @@ +import type { + PineconeIndexModel, + PineconeListIndexesParams, + PineconeResponse, +} from '@/tools/pinecone/types' +import type { ToolConfig } from '@/tools/types' + +/** Map a raw Pinecone index model to a clean output shape. */ +function mapIndex(index: any): PineconeIndexModel { + return { + name: index.name, + dimension: index.dimension ?? null, + metric: index.metric ?? null, + host: index.host ?? null, + vectorType: index.vectorType ?? index.vector_type ?? null, + deletionProtection: index.deletionProtection ?? index.deletion_protection ?? null, + tags: index.tags ?? null, + spec: index.spec ?? null, + status: index.status ?? null, + } +} + +export const listIndexesTool: ToolConfig = { + id: 'pinecone_list_indexes', + name: 'Pinecone List Indexes', + description: 'List all Pinecone indexes in the project', + version: '1.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, + }, + + request: { + method: 'GET', + url: () => 'https://api.pinecone.io/indexes', + headers: (params) => ({ + 'Api-Key': params.apiKey, + Accept: 'application/json', + 'X-Pinecone-API-Version': '2025-01', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + indexes: (data.indexes ?? []).map(mapIndex), + }, + } + }, + + outputs: { + indexes: { + type: 'array', + description: 'List of indexes with name, dimension, metric, host, spec, and status', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Index name' }, + dimension: { type: 'number', description: 'Vector dimensionality' }, + metric: { + type: 'string', + description: 'Distance metric (cosine, euclidean, dotproduct)', + }, + host: { type: 'string', description: 'Index host URL for data-plane operations' }, + vectorType: { type: 'string', description: 'Vector type (dense or sparse)' }, + deletionProtection: { + type: 'string', + description: 'Deletion protection (enabled or disabled)', + }, + tags: { type: 'object', description: 'Custom user tags on the index' }, + spec: { type: 'object', description: 'Index spec (serverless or pod configuration)' }, + status: { type: 'object', description: 'Index status with ready and state' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/pinecone/list_vector_ids.ts b/apps/sim/tools/pinecone/list_vector_ids.ts new file mode 100644 index 00000000000..3cea0f916a5 --- /dev/null +++ b/apps/sim/tools/pinecone/list_vector_ids.ts @@ -0,0 +1,112 @@ +import type { PineconeListVectorIdsParams, PineconeResponse } from '@/tools/pinecone/types' +import type { ToolConfig } from '@/tools/types' + +export const listVectorIdsTool: ToolConfig = { + id: 'pinecone_list_vector_ids', + name: 'Pinecone List Vector IDs', + description: 'List vector IDs in a Pinecone namespace by prefix (serverless indexes only)', + version: '1.0', + + params: { + indexHost: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full Pinecone index host URL (e.g., "https://my-index-abc123.svc.pinecone.io")', + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Namespace to list vector IDs from (e.g., "documents", "embeddings")', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter vector IDs by a common prefix (e.g., "doc1#")', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of IDs to return per page (default 100)', + }, + paginationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token from a previous response to fetch the next page', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, + }, + + request: { + method: 'GET', + url: (params) => { + const queryParams = new URLSearchParams() + queryParams.append('namespace', params.namespace) + if (params.prefix) { + queryParams.append('prefix', params.prefix) + } + if (params.limit != null && String(params.limit) !== '') { + queryParams.append('limit', String(params.limit)) + } + if (params.paginationToken) { + queryParams.append('paginationToken', params.paginationToken) + } + return `${params.indexHost}/vectors/list?${queryParams.toString()}` + }, + headers: (params) => ({ + 'Api-Key': params.apiKey, + Accept: 'application/json', + 'X-Pinecone-API-Version': '2025-01', + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + vectorIds: (data.vectors ?? []).map((vector: { id: string }) => vector.id), + pagination: data.pagination ?? null, + namespace: data.namespace ?? null, + usage: { + total_tokens: data.usage?.readUnits ?? 0, + }, + }, + } + }, + + outputs: { + vectorIds: { + type: 'array', + description: 'Vector IDs in the namespace', + items: { type: 'string', description: 'Vector ID' }, + }, + pagination: { + type: 'object', + description: 'Pagination info with a next token when more results exist', + properties: { + next: { type: 'string', description: 'Token to fetch the next page' }, + }, + }, + namespace: { + type: 'string', + description: 'Namespace the IDs were listed from', + }, + usage: { + type: 'object', + description: 'Usage statistics including read units', + properties: { + total_tokens: { type: 'number', description: 'Read units consumed' }, + }, + }, + }, +} diff --git a/apps/sim/tools/pinecone/types.ts b/apps/sim/tools/pinecone/types.ts index fe2cead8d4a..c13f2067084 100644 --- a/apps/sim/tools/pinecone/types.ts +++ b/apps/sim/tools/pinecone/types.ts @@ -15,6 +15,18 @@ interface PineconeMatchResponse { metadata?: Record } +export interface PineconeIndexModel { + name: string + dimension?: number | null + metric?: string | null + host?: string | null + vectorType?: string | null + deletionProtection?: string | null + tags?: Record | null + spec?: Record | null + status?: { ready?: boolean; state?: string } | null +} + export interface PineconeResponse extends ToolResponse { output: { matches?: PineconeMatchResponse[] @@ -25,9 +37,18 @@ export interface PineconeResponse extends ToolResponse { }> model?: string vector_type?: 'dense' | 'sparse' + namespace?: string | null usage?: { total_tokens: number } + indexes?: PineconeIndexModel[] + index?: PineconeIndexModel + namespaces?: Record + dimension?: number | null + indexFullness?: number | null + totalVectorCount?: number | null + vectorIds?: string[] + pagination?: { next?: string } | null } } @@ -159,3 +180,46 @@ export interface PineconeSearchVectorParams extends PineconeBaseParams { includeValues?: boolean includeMetadata?: boolean } + +export interface PineconeDeleteVectorsParams { + apiKey: string + indexHost: string + namespace?: string + ids?: string[] | string + deleteAll?: boolean | string + filter?: Record | string +} + +export interface PineconeUpdateVectorParams { + apiKey: string + indexHost: string + id: string + namespace?: string + values?: number[] | string + sparseValues?: { indices: number[]; values: number[] } | string + setMetadata?: Record | string +} + +export interface PineconeDescribeIndexStatsParams { + apiKey: string + indexHost: string + filter?: Record | string +} + +export interface PineconeListIndexesParams { + apiKey: string +} + +export interface PineconeDescribeIndexParams { + apiKey: string + indexName: string +} + +export interface PineconeListVectorIdsParams { + apiKey: string + indexHost: string + namespace: string + prefix?: string + limit?: number | string + paginationToken?: string +} diff --git a/apps/sim/tools/pinecone/update_vector.ts b/apps/sim/tools/pinecone/update_vector.ts new file mode 100644 index 00000000000..6463e175210 --- /dev/null +++ b/apps/sim/tools/pinecone/update_vector.ts @@ -0,0 +1,97 @@ +import type { PineconeResponse, PineconeUpdateVectorParams } from '@/tools/pinecone/types' +import { parseJsonParam } from '@/tools/pinecone/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateVectorTool: ToolConfig = { + id: 'pinecone_update_vector', + name: 'Pinecone Update Vector', + description: 'Update the values, sparse values, or metadata of a vector in a Pinecone namespace', + version: '1.0', + + params: { + indexHost: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full Pinecone index host URL (e.g., "https://my-index-abc123.svc.pinecone.io")', + }, + id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique ID of the vector to update', + }, + namespace: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Namespace containing the vector (e.g., "documents", "embeddings")', + }, + values: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'New dense vector values to overwrite the existing values', + }, + sparseValues: { + type: 'object', + required: false, + visibility: 'user-or-llm', + description: 'New sparse vector values with indices and values arrays', + }, + setMetadata: { + type: 'object', + required: false, + visibility: 'user-or-llm', + description: 'Metadata key-value pairs to add or overwrite on the vector', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Pinecone API key', + }, + }, + + request: { + method: 'POST', + url: (params) => `${params.indexHost}/vectors/update`, + headers: (params) => ({ + 'Api-Key': params.apiKey, + 'Content-Type': 'application/json', + 'X-Pinecone-API-Version': '2025-01', + }), + body: (params) => { + const body: Record = { id: params.id.trim() } + if (params.namespace) { + body.namespace = params.namespace + } + if (params.values != null && params.values !== '') { + body.values = parseJsonParam(params.values, 'values') + } + if (params.sparseValues != null && params.sparseValues !== '') { + body.sparseValues = parseJsonParam(params.sparseValues, 'sparseValues') + } + if (params.setMetadata != null && params.setMetadata !== '') { + body.setMetadata = parseJsonParam(params.setMetadata, 'setMetadata') + } + return body + }, + }, + + transformResponse: async (response) => { + return { + success: true, + output: { + statusText: response.status === 200 ? 'Updated' : response.statusText, + }, + } + }, + + outputs: { + statusText: { + type: 'string', + description: 'Status of the update operation', + }, + }, +} diff --git a/apps/sim/tools/pinecone/utils.ts b/apps/sim/tools/pinecone/utils.ts new file mode 100644 index 00000000000..b894306f2a6 --- /dev/null +++ b/apps/sim/tools/pinecone/utils.ts @@ -0,0 +1,15 @@ +/** + * Parse a Pinecone param that may arrive as a JSON string (from a block input or + * agent tool-call) or as an already-parsed value. Throws a descriptive error when + * a provided string is not valid JSON, instead of letting `JSON.parse` crash. + */ +export function parseJsonParam(value: unknown, fieldName: string): T { + if (typeof value !== 'string') { + return value as T + } + try { + return JSON.parse(value) as T + } catch { + throw new Error(`${fieldName} must be valid JSON`) + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3bbf132fc4d..a7f50264ed3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -792,7 +792,18 @@ import { elasticsearchSearchTool, elasticsearchUpdateDocumentTool, } from '@/tools/elasticsearch' -import { elevenLabsTtsTool } from '@/tools/elevenlabs' +import { + elevenLabsAudioIsolationTool, + elevenLabsEditVoiceSettingsTool, + elevenLabsGetUserTool, + elevenLabsGetVoiceSettingsTool, + elevenLabsGetVoiceTool, + elevenLabsListModelsTool, + elevenLabsListVoicesTool, + elevenLabsSoundEffectsTool, + elevenLabsSpeechToSpeechTool, + elevenLabsTtsTool, +} from '@/tools/elevenlabs' import { emailBisonAttachLeadsToCampaignTool, emailBisonAttachTagsToLeadsTool, @@ -901,7 +912,13 @@ import { } from '@/tools/findymail' import { firecrawlAgentTool, + firecrawlBatchScrapeStatusTool, + firecrawlBatchScrapeTool, + firecrawlCancelCrawlTool, + firecrawlCrawlStatusTool, firecrawlCrawlTool, + firecrawlCreditUsageTool, + firecrawlExtractStatusTool, firecrawlExtractTool, firecrawlMapTool, firecrawlParseTool, @@ -1260,13 +1277,19 @@ import { } from '@/tools/google_docs' import { googleDriveCopyTool, + googleDriveCreateCommentTool, googleDriveCreateFolderTool, + googleDriveDeleteCommentTool, googleDriveDeleteTool, googleDriveDownloadTool, + googleDriveExportTool, googleDriveGetAboutTool, googleDriveGetContentTool, googleDriveGetFileTool, + googleDriveGetRevisionTool, + googleDriveListCommentsTool, googleDriveListPermissionsTool, + googleDriveListRevisionsTool, googleDriveListTool, googleDriveMoveTool, googleDriveSearchTool, @@ -2427,10 +2450,16 @@ import { personaUpdateInquiryTool, } from '@/tools/persona' import { + pineconeDeleteVectorsTool, + pineconeDescribeIndexStatsTool, + pineconeDescribeIndexTool, pineconeFetchTool, pineconeGenerateEmbeddingsTool, + pineconeListIndexesTool, + pineconeListVectorIdsTool, pineconeSearchTextTool, pineconeSearchVectorTool, + pineconeUpdateVectorTool, pineconeUpsertTextTool, } from '@/tools/pinecone' import { @@ -2699,12 +2728,20 @@ import { } from '@/tools/redis' import { reductoParserTool, reductoParserV2Tool } from '@/tools/reducto' import { + resendCancelEmailTool, + resendCreateAudienceTool, + resendCreateBroadcastTool, resendCreateContactTool, + resendDeleteAudienceTool, resendDeleteContactTool, + resendGetAudienceTool, + resendGetBroadcastTool, resendGetContactTool, resendGetEmailTool, + resendListAudiencesTool, resendListContactsTool, resendListDomainsTool, + resendSendBroadcastTool, resendSendTool, resendUpdateContactTool, } from '@/tools/resend' @@ -2853,9 +2890,15 @@ import { } from '@/tools/rootly' import { s3CopyObjectTool, + s3CreateBucketTool, + s3DeleteBucketTool, + s3DeleteObjectsTool, s3DeleteObjectTool, s3GetObjectTool, + s3HeadObjectTool, + s3ListBucketsTool, s3ListObjectsTool, + s3PresignedUrlTool, s3PutObjectTool, } from '@/tools/s3' import { @@ -4386,10 +4429,16 @@ export const tools: Record = { firecrawl_scrape: firecrawlScrapeTool, firecrawl_search: firecrawlSearchTool, firecrawl_crawl: firecrawlCrawlTool, + firecrawl_crawl_status: firecrawlCrawlStatusTool, + firecrawl_cancel_crawl: firecrawlCancelCrawlTool, + firecrawl_batch_scrape: firecrawlBatchScrapeTool, + firecrawl_batch_scrape_status: firecrawlBatchScrapeStatusTool, firecrawl_map: firecrawlMapTool, firecrawl_extract: firecrawlExtractTool, + firecrawl_extract_status: firecrawlExtractStatusTool, firecrawl_agent: firecrawlAgentTool, firecrawl_parse: firecrawlParseTool, + firecrawl_credit_usage: firecrawlCreditUsageTool, fireflies_list_transcripts: firefliesListTranscriptsTool, fireflies_get_transcript: firefliesGetTranscriptTool, fireflies_get_user: firefliesGetUserTool, @@ -4569,6 +4618,14 @@ export const tools: Record = { resend_update_contact: resendUpdateContactTool, resend_delete_contact: resendDeleteContactTool, resend_list_domains: resendListDomainsTool, + resend_cancel_email: resendCancelEmailTool, + resend_create_audience: resendCreateAudienceTool, + resend_get_audience: resendGetAudienceTool, + resend_list_audiences: resendListAudiencesTool, + resend_delete_audience: resendDeleteAudienceTool, + resend_create_broadcast: resendCreateBroadcastTool, + resend_send_broadcast: resendSendBroadcastTool, + resend_get_broadcast: resendGetBroadcastTool, sendblue_send_message: sendblueSendMessageTool, sendblue_send_group_message: sendblueSendGroupMessageTool, sendblue_evaluate_service: sendblueEvaluateServiceTool, @@ -5297,10 +5354,16 @@ export const tools: Record = { x_get_usage: xGetUsageTool, x_hide_reply: xHideReplyTool, x_manage_mute: xManageMuteTool, + pinecone_delete_vectors: pineconeDeleteVectorsTool, + pinecone_describe_index: pineconeDescribeIndexTool, + pinecone_describe_index_stats: pineconeDescribeIndexStatsTool, pinecone_fetch: pineconeFetchTool, pinecone_generate_embeddings: pineconeGenerateEmbeddingsTool, + pinecone_list_indexes: pineconeListIndexesTool, + pinecone_list_vector_ids: pineconeListVectorIdsTool, pinecone_search_text: pineconeSearchTextTool, pinecone_search_vector: pineconeSearchVectorTool, + pinecone_update_vector: pineconeUpdateVectorTool, pinecone_upsert_text: pineconeUpsertTextTool, pipedrive_create_activity: pipedriveCreateActivityTool, pipedrive_create_deal: pipedriveCreateDealTool, @@ -5996,14 +6059,20 @@ export const tools: Record = { rootly_update_alert: rootlyUpdateAlertTool, rootly_update_incident: rootlyUpdateIncidentTool, google_drive_copy: googleDriveCopyTool, + google_drive_create_comment: googleDriveCreateCommentTool, google_drive_create_folder: googleDriveCreateFolderTool, google_drive_delete: googleDriveDeleteTool, + google_drive_delete_comment: googleDriveDeleteCommentTool, google_drive_download: googleDriveDownloadTool, + google_drive_export: googleDriveExportTool, google_drive_get_about: googleDriveGetAboutTool, google_drive_get_content: googleDriveGetContentTool, google_drive_get_file: googleDriveGetFileTool, + google_drive_get_revision: googleDriveGetRevisionTool, google_drive_list: googleDriveListTool, + google_drive_list_comments: googleDriveListCommentsTool, google_drive_list_permissions: googleDriveListPermissionsTool, + google_drive_list_revisions: googleDriveListRevisionsTool, google_drive_move: googleDriveMoveTool, google_drive_search: googleDriveSearchTool, google_drive_share: googleDriveShareTool, @@ -6707,6 +6776,15 @@ export const tools: Record = { knowledge_upsert_document: knowledgeUpsertDocumentTool, search_tool: searchTool, elevenlabs_tts: elevenLabsTtsTool, + elevenlabs_list_voices: elevenLabsListVoicesTool, + elevenlabs_get_voice: elevenLabsGetVoiceTool, + elevenlabs_get_voice_settings: elevenLabsGetVoiceSettingsTool, + elevenlabs_edit_voice_settings: elevenLabsEditVoiceSettingsTool, + elevenlabs_list_models: elevenLabsListModelsTool, + elevenlabs_get_user: elevenLabsGetUserTool, + elevenlabs_sound_effects: elevenLabsSoundEffectsTool, + elevenlabs_speech_to_speech: elevenLabsSpeechToSpeechTool, + elevenlabs_audio_isolation: elevenLabsAudioIsolationTool, fathom_list_meetings: fathomListMeetingsTool, fathom_get_summary: fathomGetSummaryTool, fathom_get_transcript: fathomGetTranscriptTool, @@ -6756,6 +6834,12 @@ export const tools: Record = { s3_list_objects: s3ListObjectsTool, s3_delete_object: s3DeleteObjectTool, s3_copy_object: s3CopyObjectTool, + s3_list_buckets: s3ListBucketsTool, + s3_head_object: s3HeadObjectTool, + s3_create_bucket: s3CreateBucketTool, + s3_delete_bucket: s3DeleteBucketTool, + s3_presigned_url: s3PresignedUrlTool, + s3_delete_objects: s3DeleteObjectsTool, secrets_manager_get_secret: secretsManagerGetSecretTool, secrets_manager_list_secrets: secretsManagerListSecretsTool, secrets_manager_create_secret: secretsManagerCreateSecretTool, diff --git a/apps/sim/tools/resend/cancel_email.ts b/apps/sim/tools/resend/cancel_email.ts new file mode 100644 index 00000000000..b6c6b556862 --- /dev/null +++ b/apps/sim/tools/resend/cancel_email.ts @@ -0,0 +1,63 @@ +import { createLogger } from '@sim/logger' +import type { CancelEmailParams, CancelEmailResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendCancelEmailTool') + +export const resendCancelEmailTool: ToolConfig = { + id: 'resend_cancel_email', + name: 'Cancel Email', + description: 'Cancel a scheduled email before it is sent', + version: '1.0.0', + + params: { + cancelEmailId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the scheduled email to cancel', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: CancelEmailParams) => + `https://api.resend.com/emails/${encodeURIComponent(params.cancelEmailId.trim())}/cancel`, + method: 'POST', + headers: (params: CancelEmailParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.id) { + logger.error('Resend Cancel Email API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to cancel email', + output: { + id: '', + }, + } + } + + return { + success: true, + output: { + id: data.id, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Canceled email ID' }, + }, +} diff --git a/apps/sim/tools/resend/create_audience.ts b/apps/sim/tools/resend/create_audience.ts new file mode 100644 index 00000000000..0ceba42b214 --- /dev/null +++ b/apps/sim/tools/resend/create_audience.ts @@ -0,0 +1,68 @@ +import { createLogger } from '@sim/logger' +import type { CreateAudienceParams, CreateAudienceResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendCreateAudienceTool') + +export const resendCreateAudienceTool: ToolConfig = { + id: 'resend_create_audience', + name: 'Create Audience', + description: 'Create a new audience in Resend', + version: '1.0.0', + + params: { + audienceName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the audience to create', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: 'https://api.resend.com/audiences', + method: 'POST', + headers: (params: CreateAudienceParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: CreateAudienceParams) => ({ + name: params.audienceName, + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.id) { + logger.error('Resend Create Audience API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to create audience', + output: { + id: '', + name: '', + }, + } + } + + return { + success: true, + output: { + id: data.id, + name: data.name ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Created audience ID' }, + name: { type: 'string', description: 'Audience name' }, + }, +} diff --git a/apps/sim/tools/resend/create_broadcast.ts b/apps/sim/tools/resend/create_broadcast.ts new file mode 100644 index 00000000000..6b20154f246 --- /dev/null +++ b/apps/sim/tools/resend/create_broadcast.ts @@ -0,0 +1,115 @@ +import { createLogger } from '@sim/logger' +import type { CreateBroadcastParams, CreateBroadcastResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendCreateBroadcastTool') + +export const resendCreateBroadcastTool: ToolConfig = { + id: 'resend_create_broadcast', + name: 'Create Broadcast', + description: 'Create a broadcast email for an audience in Resend', + version: '1.0.0', + + params: { + audienceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the audience to send the broadcast to', + }, + broadcastFrom: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Sender email address (e.g., "sender@example.com" or "Sender Name ")', + }, + broadcastSubject: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Broadcast email subject line', + }, + broadcastHtml: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'HTML content of the broadcast', + }, + broadcastText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Plain text content of the broadcast', + }, + broadcastReplyTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reply-to email address', + }, + broadcastName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Friendly internal name for the broadcast', + }, + broadcastPreviewText: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Preview text shown in the inbox before the email is opened', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: 'https://api.resend.com/broadcasts', + method: 'POST', + headers: (params: CreateBroadcastParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: CreateBroadcastParams) => ({ + audience_id: params.audienceId.trim(), + from: params.broadcastFrom, + subject: params.broadcastSubject, + ...(params.broadcastHtml && { html: params.broadcastHtml }), + ...(params.broadcastText && { text: params.broadcastText }), + ...(params.broadcastReplyTo && { reply_to: params.broadcastReplyTo }), + ...(params.broadcastName && { name: params.broadcastName }), + ...(params.broadcastPreviewText && { preview_text: params.broadcastPreviewText }), + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.id) { + logger.error('Resend Create Broadcast API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to create broadcast', + output: { + id: '', + }, + } + } + + return { + success: true, + output: { + id: data.id, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Created broadcast ID' }, + }, +} diff --git a/apps/sim/tools/resend/delete_audience.ts b/apps/sim/tools/resend/delete_audience.ts new file mode 100644 index 00000000000..db36529a649 --- /dev/null +++ b/apps/sim/tools/resend/delete_audience.ts @@ -0,0 +1,66 @@ +import { createLogger } from '@sim/logger' +import type { DeleteAudienceParams, DeleteAudienceResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendDeleteAudienceTool') + +export const resendDeleteAudienceTool: ToolConfig = { + id: 'resend_delete_audience', + name: 'Delete Audience', + description: 'Delete an audience from Resend by ID', + version: '1.0.0', + + params: { + audienceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the audience to delete', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: DeleteAudienceParams) => + `https://api.resend.com/audiences/${encodeURIComponent(params.audienceId.trim())}`, + method: 'DELETE', + headers: (params: DeleteAudienceParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.message && !data.deleted) { + logger.error('Resend Delete Audience API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to delete audience', + output: { + id: '', + deleted: false, + }, + } + } + + return { + success: true, + output: { + id: data.id ?? '', + deleted: data.deleted ?? true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deleted audience ID' }, + deleted: { type: 'boolean', description: 'Whether the audience was successfully deleted' }, + }, +} diff --git a/apps/sim/tools/resend/get_audience.ts b/apps/sim/tools/resend/get_audience.ts new file mode 100644 index 00000000000..d228cb899b4 --- /dev/null +++ b/apps/sim/tools/resend/get_audience.ts @@ -0,0 +1,69 @@ +import { createLogger } from '@sim/logger' +import type { GetAudienceParams, GetAudienceResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendGetAudienceTool') + +export const resendGetAudienceTool: ToolConfig = { + id: 'resend_get_audience', + name: 'Get Audience', + description: 'Retrieve details of an audience by ID', + version: '1.0.0', + + params: { + audienceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the audience to retrieve', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: GetAudienceParams) => + `https://api.resend.com/audiences/${encodeURIComponent(params.audienceId.trim())}`, + method: 'GET', + headers: (params: GetAudienceParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.id) { + logger.error('Resend Get Audience API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to retrieve audience', + output: { + id: '', + name: '', + createdAt: '', + }, + } + } + + return { + success: true, + output: { + id: data.id, + name: data.name ?? '', + createdAt: data.created_at ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Audience ID' }, + name: { type: 'string', description: 'Audience name' }, + createdAt: { type: 'string', description: 'Audience creation timestamp' }, + }, +} diff --git a/apps/sim/tools/resend/get_broadcast.ts b/apps/sim/tools/resend/get_broadcast.ts new file mode 100644 index 00000000000..322bdf11ccb --- /dev/null +++ b/apps/sim/tools/resend/get_broadcast.ts @@ -0,0 +1,100 @@ +import { createLogger } from '@sim/logger' +import type { GetBroadcastParams, GetBroadcastResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendGetBroadcastTool') + +export const resendGetBroadcastTool: ToolConfig = { + id: 'resend_get_broadcast', + name: 'Get Broadcast', + description: 'Retrieve details of a broadcast by ID', + version: '1.0.0', + + params: { + broadcastId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the broadcast to retrieve', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: GetBroadcastParams) => + `https://api.resend.com/broadcasts/${encodeURIComponent(params.broadcastId.trim())}`, + method: 'GET', + headers: (params: GetBroadcastParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.id) { + logger.error('Resend Get Broadcast API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to retrieve broadcast', + output: { + id: '', + name: '', + audienceId: null, + segmentId: null, + from: '', + subject: '', + replyTo: null, + previewText: null, + status: '', + createdAt: '', + scheduledAt: null, + sentAt: null, + }, + } + } + + return { + success: true, + output: { + id: data.id, + name: data.name ?? '', + audienceId: data.audience_id ?? null, + segmentId: data.segment_id ?? null, + from: data.from ?? '', + subject: data.subject ?? '', + replyTo: data.reply_to ?? null, + previewText: data.preview_text ?? null, + status: data.status ?? '', + createdAt: data.created_at ?? '', + scheduledAt: data.scheduled_at ?? null, + sentAt: data.sent_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Broadcast ID' }, + name: { type: 'string', description: 'Broadcast name' }, + audienceId: { type: 'string', description: 'Audience ID (legacy)', optional: true }, + segmentId: { + type: 'string', + description: 'Segment ID (the current recipient field)', + optional: true, + }, + from: { type: 'string', description: 'Sender email address' }, + subject: { type: 'string', description: 'Broadcast subject' }, + replyTo: { type: 'string', description: 'Reply-to email address', optional: true }, + previewText: { type: 'string', description: 'Inbox preview text', optional: true }, + status: { type: 'string', description: 'Broadcast status (e.g., draft, sent)' }, + createdAt: { type: 'string', description: 'Broadcast creation timestamp' }, + scheduledAt: { type: 'string', description: 'Scheduled send timestamp', optional: true }, + sentAt: { type: 'string', description: 'Timestamp the broadcast was sent', optional: true }, + }, +} diff --git a/apps/sim/tools/resend/index.ts b/apps/sim/tools/resend/index.ts index 4242b09be13..d465210044a 100644 --- a/apps/sim/tools/resend/index.ts +++ b/apps/sim/tools/resend/index.ts @@ -1,8 +1,17 @@ +export { resendCancelEmailTool } from '@/tools/resend/cancel_email' +export { resendCreateAudienceTool } from '@/tools/resend/create_audience' +export { resendCreateBroadcastTool } from '@/tools/resend/create_broadcast' export { resendCreateContactTool } from '@/tools/resend/create_contact' +export { resendDeleteAudienceTool } from '@/tools/resend/delete_audience' export { resendDeleteContactTool } from '@/tools/resend/delete_contact' +export { resendGetAudienceTool } from '@/tools/resend/get_audience' +export { resendGetBroadcastTool } from '@/tools/resend/get_broadcast' export { resendGetContactTool } from '@/tools/resend/get_contact' export { resendGetEmailTool } from '@/tools/resend/get_email' +export { resendListAudiencesTool } from '@/tools/resend/list_audiences' export { resendListContactsTool } from '@/tools/resend/list_contacts' export { resendListDomainsTool } from '@/tools/resend/list_domains' export { resendSendTool } from '@/tools/resend/send' +export { resendSendBroadcastTool } from '@/tools/resend/send_broadcast' export { resendUpdateContactTool } from '@/tools/resend/update_contact' +export * from './types' diff --git a/apps/sim/tools/resend/list_audiences.ts b/apps/sim/tools/resend/list_audiences.ts new file mode 100644 index 00000000000..c44b788837d --- /dev/null +++ b/apps/sim/tools/resend/list_audiences.ts @@ -0,0 +1,70 @@ +import { createLogger } from '@sim/logger' +import type { ListAudiencesParams, ListAudiencesResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendListAudiencesTool') + +export const resendListAudiencesTool: ToolConfig = { + id: 'resend_list_audiences', + name: 'List Audiences', + description: 'List all audiences in Resend', + version: '1.0.0', + + params: { + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: 'https://api.resend.com/audiences', + method: 'GET', + headers: (params: ListAudiencesParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (data.message) { + logger.error('Resend List Audiences API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to list audiences', + output: { + audiences: [], + hasMore: false, + }, + } + } + + return { + success: true, + output: { + audiences: data.data ?? [], + hasMore: data.has_more ?? false, + }, + } + }, + + outputs: { + audiences: { + type: 'array', + description: 'Array of audiences', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Audience ID' }, + name: { type: 'string', description: 'Audience name' }, + created_at: { type: 'string', description: 'Audience creation timestamp' }, + }, + }, + }, + hasMore: { type: 'boolean', description: 'Whether there are more audiences to retrieve' }, + }, +} diff --git a/apps/sim/tools/resend/send_broadcast.ts b/apps/sim/tools/resend/send_broadcast.ts new file mode 100644 index 00000000000..3971a7e9f70 --- /dev/null +++ b/apps/sim/tools/resend/send_broadcast.ts @@ -0,0 +1,73 @@ +import { createLogger } from '@sim/logger' +import type { SendBroadcastParams, SendBroadcastResult } from '@/tools/resend/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ResendSendBroadcastTool') + +export const resendSendBroadcastTool: ToolConfig = { + id: 'resend_send_broadcast', + name: 'Send Broadcast', + description: 'Send a broadcast immediately or schedule it for later', + version: '1.0.0', + + params: { + broadcastId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the broadcast to send', + }, + broadcastScheduledAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Schedule delivery in natural language (e.g., "in 1 min") or ISO 8601 format. Sends immediately if omitted', + }, + resendApiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Resend API key', + }, + }, + + request: { + url: (params: SendBroadcastParams) => + `https://api.resend.com/broadcasts/${encodeURIComponent(params.broadcastId.trim())}/send`, + method: 'POST', + headers: (params: SendBroadcastParams) => ({ + Authorization: `Bearer ${params.resendApiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: SendBroadcastParams) => ({ + ...(params.broadcastScheduledAt && { scheduled_at: params.broadcastScheduledAt }), + }), + }, + + transformResponse: async (response: Response): Promise => { + const data = await response.json() + + if (!data.id) { + logger.error('Resend Send Broadcast API error:', JSON.stringify(data, null, 2)) + return { + success: false, + error: data.message || 'Failed to send broadcast', + output: { + id: '', + }, + } + } + + return { + success: true, + output: { + id: data.id, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Broadcast ID' }, + }, +} diff --git a/apps/sim/tools/resend/types.ts b/apps/sim/tools/resend/types.ts index 52a013b8327..93bf6337cff 100644 --- a/apps/sim/tools/resend/types.ts +++ b/apps/sim/tools/resend/types.ts @@ -145,3 +145,126 @@ export interface ListDomainsResult extends ToolResponse { hasMore: boolean } } + +/** Cancel Email */ +export interface CancelEmailParams { + resendApiKey: string + cancelEmailId: string +} + +export interface CancelEmailResult extends ToolResponse { + output: { + id: string + } +} + +/** Create Audience */ +export interface CreateAudienceParams { + resendApiKey: string + audienceName: string +} + +export interface CreateAudienceResult extends ToolResponse { + output: { + id: string + name: string + } +} + +/** Get Audience */ +export interface GetAudienceParams { + resendApiKey: string + audienceId: string +} + +export interface GetAudienceResult extends ToolResponse { + output: { + id: string + name: string + createdAt: string + } +} + +/** List Audiences */ +export interface ListAudiencesParams { + resendApiKey: string +} + +export interface ListAudiencesResult extends ToolResponse { + output: { + audiences: Array<{ + id: string + name: string + created_at: string + }> + hasMore: boolean + } +} + +/** Delete Audience */ +export interface DeleteAudienceParams { + resendApiKey: string + audienceId: string +} + +export interface DeleteAudienceResult extends ToolResponse { + output: { + id: string + deleted: boolean + } +} + +/** Create Broadcast */ +export interface CreateBroadcastParams { + resendApiKey: string + audienceId: string + broadcastFrom: string + broadcastSubject: string + broadcastReplyTo?: string + broadcastHtml?: string + broadcastText?: string + broadcastName?: string + broadcastPreviewText?: string +} + +export interface CreateBroadcastResult extends ToolResponse { + output: { + id: string + } +} + +/** Send Broadcast */ +export interface SendBroadcastParams { + resendApiKey: string + broadcastId: string + broadcastScheduledAt?: string +} + +export interface SendBroadcastResult extends ToolResponse { + output: { + id: string + } +} + +/** Get Broadcast */ +export interface GetBroadcastParams { + resendApiKey: string + broadcastId: string +} + +export interface GetBroadcastResult extends ToolResponse { + output: { + id: string + name: string + audienceId: string | null + segmentId: string | null + from: string + subject: string + replyTo: string | string[] | null + previewText: string | null + status: string + createdAt: string + scheduledAt: string | null + sentAt: string | null + } +} diff --git a/apps/sim/tools/s3/create_bucket.ts b/apps/sim/tools/s3/create_bucket.ts new file mode 100644 index 00000000000..b946cd3432b --- /dev/null +++ b/apps/sim/tools/s3/create_bucket.ts @@ -0,0 +1,90 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3CreateBucketTool: ToolConfig = { + id: 's3_create_bucket', + name: 'S3 Create Bucket', + description: 'Create a new AWS S3 bucket in the specified region', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region to create the bucket in (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the new S3 bucket (must be globally unique)', + }, + acl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Canned ACL for the bucket (e.g., private, public-read)', + }, + }, + + request: { + url: '/api/tools/s3/create-bucket', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + acl: params.acl, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + metadata: { + error: data.error || 'Failed to create bucket', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + metadata: { + bucket: data.output.bucket, + location: data.output.location ?? null, + bucketArn: data.output.bucketArn ?? null, + }, + }, + } + }, + + outputs: { + metadata: { + type: 'object', + description: 'Created bucket metadata including name and location', + }, + }, +} diff --git a/apps/sim/tools/s3/delete_bucket.ts b/apps/sim/tools/s3/delete_bucket.ts new file mode 100644 index 00000000000..e56dd81789f --- /dev/null +++ b/apps/sim/tools/s3/delete_bucket.ts @@ -0,0 +1,87 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3DeleteBucketTool: ToolConfig = { + id: 's3_delete_bucket', + name: 'S3 Delete Bucket', + description: 'Delete an empty AWS S3 bucket', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region where the bucket is located (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the S3 bucket to delete (must be empty)', + }, + }, + + request: { + url: '/api/tools/s3/delete-bucket', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + deleted: false, + metadata: { + error: data.error || 'Failed to delete bucket', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + deleted: true, + metadata: { + bucket: data.output.bucket, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'boolean', + description: 'Whether the bucket was successfully deleted', + }, + metadata: { + type: 'object', + description: 'Deletion metadata including bucket name', + }, + }, +} diff --git a/apps/sim/tools/s3/delete_objects.ts b/apps/sim/tools/s3/delete_objects.ts new file mode 100644 index 00000000000..8b221341ae6 --- /dev/null +++ b/apps/sim/tools/s3/delete_objects.ts @@ -0,0 +1,124 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3DeleteObjectsTool: ToolConfig = { + id: 's3_delete_objects', + name: 'S3 Delete Objects', + description: 'Delete multiple objects from an AWS S3 bucket in a single batch request', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'S3 bucket name (e.g., my-bucket)', + }, + keys: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Array of object keys to delete (e.g., ["a.txt", "folder/b.txt"]). Max 1000.', + }, + quiet: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return only deletion errors, omitting successfully deleted keys', + }, + }, + + request: { + url: '/api/tools/s3/delete-objects', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + keys: params.keys, + quiet: params.quiet, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + deleted: [], + errors: [], + metadata: { + error: data.error || 'Failed to delete objects', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + deleted: data.output.deleted || [], + errors: data.output.errors || [], + metadata: { + deletedCount: (data.output.deleted || []).length, + errorCount: (data.output.errors || []).length, + }, + }, + } + }, + + outputs: { + deleted: { + type: 'array', + description: 'Objects that were successfully deleted', + items: { + type: 'object', + properties: { + key: { type: 'string', description: 'Deleted object key' }, + versionId: { type: 'string', description: 'Version ID of the deleted object' }, + deleteMarker: { type: 'boolean', description: 'Whether a delete marker was created' }, + }, + }, + }, + errors: { + type: 'array', + description: 'Objects that failed to delete', + items: { + type: 'object', + properties: { + key: { type: 'string', description: 'Object key that failed' }, + code: { type: 'string', description: 'Error code' }, + message: { type: 'string', description: 'Error message' }, + }, + }, + }, + metadata: { + type: 'object', + description: 'Batch deletion summary including counts', + }, + }, +} diff --git a/apps/sim/tools/s3/head_object.ts b/apps/sim/tools/s3/head_object.ts new file mode 100644 index 00000000000..a7c7ab1e7ec --- /dev/null +++ b/apps/sim/tools/s3/head_object.ts @@ -0,0 +1,109 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3HeadObjectTool: ToolConfig = { + id: 's3_head_object', + name: 'S3 Head Object', + description: 'Retrieve metadata for an S3 object without downloading its body', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'S3 bucket name (e.g., my-bucket)', + }, + objectKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object key/path to inspect (e.g., folder/file.txt)', + }, + versionId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Specific object version ID to inspect (for versioned buckets)', + }, + }, + + request: { + url: '/api/tools/s3/head-object', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + versionId: params.versionId, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + exists: false, + metadata: { + error: data.error || 'Failed to retrieve object metadata', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + exists: data.output.exists, + metadata: { + size: data.output.contentLength ?? null, + fileType: data.output.contentType ?? null, + etag: data.output.etag ?? null, + lastModified: data.output.lastModified ?? null, + versionId: data.output.versionId ?? null, + storageClass: data.output.storageClass ?? null, + serverSideEncryption: data.output.serverSideEncryption ?? null, + deleteMarker: data.output.deleteMarker ?? null, + userMetadata: data.output.metadata ?? {}, + }, + }, + } + }, + + outputs: { + exists: { + type: 'boolean', + description: 'Whether the object exists and was reachable', + }, + metadata: { + type: 'object', + description: 'Object metadata including size, content type, ETag, and last modified date', + }, + }, +} diff --git a/apps/sim/tools/s3/index.ts b/apps/sim/tools/s3/index.ts index 235c479bf4c..25c945723dc 100644 --- a/apps/sim/tools/s3/index.ts +++ b/apps/sim/tools/s3/index.ts @@ -1,7 +1,25 @@ import { s3CopyObjectTool } from '@/tools/s3/copy_object' +import { s3CreateBucketTool } from '@/tools/s3/create_bucket' +import { s3DeleteBucketTool } from '@/tools/s3/delete_bucket' import { s3DeleteObjectTool } from '@/tools/s3/delete_object' +import { s3DeleteObjectsTool } from '@/tools/s3/delete_objects' import { s3GetObjectTool } from '@/tools/s3/get_object' +import { s3HeadObjectTool } from '@/tools/s3/head_object' +import { s3ListBucketsTool } from '@/tools/s3/list_buckets' import { s3ListObjectsTool } from '@/tools/s3/list_objects' +import { s3PresignedUrlTool } from '@/tools/s3/presigned_url' import { s3PutObjectTool } from '@/tools/s3/put_object' -export { s3GetObjectTool, s3PutObjectTool, s3ListObjectsTool, s3DeleteObjectTool, s3CopyObjectTool } +export { + s3GetObjectTool, + s3PutObjectTool, + s3ListObjectsTool, + s3DeleteObjectTool, + s3CopyObjectTool, + s3ListBucketsTool, + s3HeadObjectTool, + s3CreateBucketTool, + s3DeleteBucketTool, + s3PresignedUrlTool, + s3DeleteObjectsTool, +} diff --git a/apps/sim/tools/s3/list_buckets.ts b/apps/sim/tools/s3/list_buckets.ts new file mode 100644 index 00000000000..8803c7f5068 --- /dev/null +++ b/apps/sim/tools/s3/list_buckets.ts @@ -0,0 +1,111 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3ListBucketsTool: ToolConfig = { + id: 's3_list_buckets', + name: 'S3 List Buckets', + description: 'List the S3 buckets owned by the authenticated AWS account', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region to address the request to (e.g., us-east-1)', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Limit the response to bucket names that begin with this prefix', + }, + maxBuckets: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of buckets to return (1-10000)', + }, + continuationToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination from a previous list buckets response', + }, + }, + + request: { + url: '/api/tools/s3/list-buckets', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + prefix: params.prefix, + maxBuckets: params.maxBuckets !== undefined ? Number(params.maxBuckets) : undefined, + continuationToken: params.continuationToken, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + buckets: [], + metadata: { + error: data.error || 'Failed to list buckets', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + buckets: data.output.buckets || [], + metadata: { + owner: data.output.owner ?? null, + continuationToken: data.output.continuationToken ?? null, + prefix: data.output.prefix ?? null, + }, + }, + } + }, + + outputs: { + buckets: { + type: 'array', + description: 'List of S3 buckets owned by the account', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Bucket name' }, + creationDate: { type: 'string', description: 'Bucket creation timestamp' }, + region: { type: 'string', description: 'AWS region where the bucket is located' }, + }, + }, + }, + metadata: { + type: 'object', + description: 'Listing metadata including owner and pagination info', + }, + }, +} diff --git a/apps/sim/tools/s3/presigned_url.ts b/apps/sim/tools/s3/presigned_url.ts new file mode 100644 index 00000000000..45c419e315b --- /dev/null +++ b/apps/sim/tools/s3/presigned_url.ts @@ -0,0 +1,117 @@ +import type { ToolConfig } from '@/tools/types' + +export const s3PresignedUrlTool: ToolConfig = { + id: 's3_presigned_url', + name: 'S3 Presigned URL', + description: 'Generate a time-limited presigned URL to download or upload an S3 object', + version: '1.0.0', + + params: { + accessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Access Key ID', + }, + secretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your AWS Secret Access Key', + }, + region: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region where the bucket is located (e.g., us-east-1)', + }, + bucketName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'S3 bucket name (e.g., my-bucket)', + }, + objectKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Object key/path for the presigned URL (e.g., folder/file.txt)', + }, + method: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Operation the URL grants: get (download) or put (upload)', + }, + expiresIn: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'URL validity in seconds (1-604800, default 3600)', + }, + contentType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Content-Type the upload must use (only applies to put URLs)', + }, + }, + + request: { + url: '/api/tools/s3/presigned-url', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + region: params.region, + bucketName: params.bucketName, + objectKey: params.objectKey, + method: params.method, + expiresIn: params.expiresIn !== undefined ? Number(params.expiresIn) : 3600, + contentType: params.contentType, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!data.success) { + return { + success: false, + output: { + url: '', + metadata: { + error: data.error || 'Failed to generate presigned URL', + }, + }, + error: data.error, + } + } + + return { + success: true, + output: { + url: data.output.url, + metadata: { + method: data.output.method, + expiresIn: data.output.expiresIn, + expiresAt: data.output.expiresAt, + }, + }, + } + }, + + outputs: { + url: { + type: 'string', + description: 'The generated presigned URL', + }, + metadata: { + type: 'object', + description: 'Presigned URL metadata including method and expiration', + }, + }, +} diff --git a/apps/sim/tools/s3/types.ts b/apps/sim/tools/s3/types.ts index 44612c44c0b..aa6c7fd0731 100644 --- a/apps/sim/tools/s3/types.ts +++ b/apps/sim/tools/s3/types.ts @@ -4,6 +4,7 @@ import type { ToolResponse } from '@/tools/types' export interface S3Response extends ToolResponse { output: { url?: string + uri?: string file?: UserFile objects?: Array<{ key: string @@ -11,23 +12,43 @@ export interface S3Response extends ToolResponse { lastModified: string etag: string }> - deleted?: boolean + buckets?: Array<{ + name: string + creationDate: string | null + region: string | null + }> + deleted?: + | boolean + | Array<{ key: string | null; versionId: string | null; deleteMarker: boolean | null }> + errors?: Array<{ key: string | null; code: string | null; message: string | null }> + exists?: boolean metadata: { - fileType?: string - size?: number + fileType?: string | null + size?: number | null name?: string - lastModified?: string - etag?: string - location?: string + lastModified?: string | null + etag?: string | null + location?: string | null key?: string bucket?: string isTruncated?: boolean - nextContinuationToken?: string + nextContinuationToken?: string | null keyCount?: number - prefix?: string - deleteMarker?: boolean - versionId?: string + prefix?: string | null + deleteMarker?: boolean | null + versionId?: string | null copySourceVersionId?: string + storageClass?: string | null + serverSideEncryption?: string | null + userMetadata?: Record + owner?: { displayName: string | null; id: string | null } | null + continuationToken?: string | null + bucketArn?: string | null + method?: string + expiresIn?: number + expiresAt?: string + deletedCount?: number + errorCount?: number error?: string } } diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 680dfa03bf1..5387f3cf638 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 872, - zodRoutes: 872, + totalRoutes: 873, + zodRoutes: 873, nonZodRoutes: 0, } as const