feat(integrations): extend ElevenLabs, Google Drive, Firecrawl, Pinecone, Resend, and S3 tool depth (#5270)

* feat(firecrawl): add crawl status/cancel, batch scrape + status, extract status, credit usage tools

* feat(resend): add audiences, broadcasts, and cancel-email tools

* feat(pinecone): add delete/update vectors, index, and stats tools

* feat(google-drive): add revisions, comments, and export tools

* feat(elevenlabs): add voices, settings, models, user, sound-effects, speech-to-speech, audio-isolation tools

* feat(s3): add bucket CRUD, head-object, presigned-url, and batch-delete tools

* chore(api-validation): bump route baseline to 873 for wave-3 internal tool routes (s3, elevenlabs, google_drive export)

* docs(integrations): regenerate docs + catalog for wave-3 tools

* fix(integrations): audit fixes for wave-3

- pinecone: read camelCase vectorType/deletionProtection (with snake_case fallback) so list_indexes/describe_index populate them; make describe_index_stats casing defensive
- google-drive: URL-encode fileId in the export route
- remove extraneous inline/section-divider comments across new blocks/tools; convert type docs to TSDoc

* fix(integrations): address review — elevenlabs settings bleed, batch-scrape job-id guard, s3 head-object existence

- elevenlabs: select stability/similarityBoost by operation so a stale edit-settings value can't bleed into a TTS call
- firecrawl: fail fast with a clear error when batch scrape returns no job id (avoids a misleading polling timeout)
- s3: head_object on a missing key now returns exists:false instead of a generic failure

* fix(s3): allow exists:false in head-object response contract (schema must match the missing-object output)

* fix(pinecone): guard JSON.parse of ids/filter/values/sparseValues/setMetadata

Malformed JSON-string input now throws a clear '<field> must be valid JSON' error via a shared parseJsonParam helper instead of crashing the request body builder.

* fix(pinecone): enforce mutual exclusivity of ids/deleteAll/filter in delete_vectors

Pinecone treats these delete selectors as mutually exclusive; the tool now requires exactly one and throws a clear error otherwise, instead of sending a conflicting body.

* fix(integrations): I/O fidelity vs API docs (wave-3 audit)

- pinecone: normalize describe_index_stats per-namespace vector_count -> vectorCount
- firecrawl: remove phantom 'sources' from extract_status, add real creditsUsed/tokensUsed; expose batch_scrape maxConcurrency/ignoreInvalidURLs
- google-drive: drop undocumented supportsAllDrives from the files.export URL
- elevenlabs: add next_page_token input to list_voices (fixes pagination dead-end)
- resend: surface segment_id on get_broadcast; declare replyTo + segment_id in block outputs
This commit is contained in:
Waleed
2026-06-29 15:59:14 -07:00
committed by GitHub
parent c7bb37d488
commit 48c1b453df
86 changed files with 8654 additions and 71 deletions
@@ -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 |
@@ -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 |
@@ -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)
@@ -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 |
@@ -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 &lt;sender@example.com&gt;"\) |
| `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
+142 -2
View File
@@ -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 |
@@ -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<string, string> = { 'xi-api-key': body.apiKey, Accept: 'audio/mpeg' }
const signal = AbortSignal.timeout(DEFAULT_EXECUTION_TIMEOUT_MS)
if (operation === 'sound_effects') {
const payload: Record<string, unknown> = { 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 }
)
}
})
@@ -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 }
)
}
})
@@ -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 }
)
}
})
@@ -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 }
)
}
})
@@ -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 }
)
}
})
@@ -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 }
)
}
})
@@ -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 }
)
}
})
@@ -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 }
)
}
})
+388 -16
View File
@@ -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<ElevenLabsBlockResponse> = {
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<ElevenLabsBlockResponse> = {
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<ElevenLabsBlockResponse> = {
{ 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<ElevenLabsBlockResponse> = {
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<ElevenLabsBlockResponse> = {
],
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' },
},
},
}
+119 -8
View File
@@ -23,12 +23,18 @@ export const FirecrawlBlock: BlockConfig<FirecrawlResponse> = {
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<FirecrawlResponse> = {
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<string, any> = { 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' },
+333 -3
View File
@@ -43,6 +43,12 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
{ 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,
+254 -2
View File
@@ -9,7 +9,7 @@ export const PineconeBlock: BlockConfig<PineconeResponse> = {
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<PineconeResponse> = {
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<PineconeResponse> = {
],
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<PineconeResponse> = {
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<string, any>) => {
@@ -281,16 +484,38 @@ export const PineconeBlock: BlockConfig<PineconeResponse> = {
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<string, any>) => {
const result: Record<string, any> = {}
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<PineconeResponse> = {
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}' },
},
}
+148
View File
@@ -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' },
},
+304 -7
View File
@@ -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<S3Response> = {
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<S3Response> = {
{ 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<S3Response> = {
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<S3Response> = {
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<S3Response> = {
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<S3Response> = {
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<S3Response> = {
'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<S3Response> = {
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<S3Response> = {
}
}
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<S3Response> = {
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<S3Response> = {
},
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
@@ -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<typeof awsS3CreateBucketContract>
export type AwsS3CreateBucketBody = ContractBody<typeof awsS3CreateBucketContract>
export type AwsS3CreateBucketResponse = ContractJsonResponse<typeof awsS3CreateBucketContract>
@@ -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<typeof awsS3DeleteBucketContract>
export type AwsS3DeleteBucketBody = ContractBody<typeof awsS3DeleteBucketContract>
export type AwsS3DeleteBucketResponse = ContractJsonResponse<typeof awsS3DeleteBucketContract>
@@ -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<typeof awsS3DeleteObjectsContract>
export type AwsS3DeleteObjectsBody = ContractBody<typeof awsS3DeleteObjectsContract>
export type AwsS3DeleteObjectsResponse = ContractJsonResponse<typeof awsS3DeleteObjectsContract>
@@ -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<typeof awsS3HeadObjectContract>
export type AwsS3HeadObjectBody = ContractBody<typeof awsS3HeadObjectContract>
export type AwsS3HeadObjectResponse = ContractJsonResponse<typeof awsS3HeadObjectContract>
@@ -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<typeof awsS3ListBucketsContract>
export type AwsS3ListBucketsBody = ContractBody<typeof awsS3ListBucketsContract>
export type AwsS3ListBucketsResponse = ContractJsonResponse<typeof awsS3ListBucketsContract>
@@ -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<typeof awsS3PresignedUrlContract>
export type AwsS3PresignedUrlBody = ContractBody<typeof awsS3PresignedUrlContract>
export type AwsS3PresignedUrlResponse = ContractJsonResponse<typeof awsS3PresignedUrlContract>
@@ -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<typeof gmailSendContract>
export type GmailUnarchiveBody = ContractBodyInput<typeof gmailUnarchiveContract>
export type GoogleDriveUploadBody = ContractBodyInput<typeof googleDriveUploadContract>
export type GoogleDriveDownloadBody = ContractBodyInput<typeof googleDriveDownloadContract>
export type GoogleDriveExportBody = ContractBodyInput<typeof googleDriveExportContract>
export type GoogleVaultDownloadExportFileBody = ContractBodyInput<
typeof googleVaultDownloadExportFileContract
>
@@ -222,6 +237,7 @@ export type GmailSendResponse = ContractJsonResponse<typeof gmailSendContract>
export type GmailUnarchiveResponse = ContractJsonResponse<typeof gmailUnarchiveContract>
export type GoogleDriveUploadResponse = ContractJsonResponse<typeof googleDriveUploadContract>
export type GoogleDriveDownloadResponse = ContractJsonResponse<typeof googleDriveDownloadContract>
export type GoogleDriveExportResponse = ContractJsonResponse<typeof googleDriveExportContract>
export type GoogleVaultDownloadExportFileResponse = ContractJsonResponse<
typeof googleVaultDownloadExportFileContract
>
@@ -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 },
})
+181 -12
View File
@@ -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",
@@ -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' },
},
}
@@ -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<string, unknown> = {}
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)' },
},
}
+72
View File
@@ -0,0 +1,72 @@
import type { ElevenLabsGetUserParams, ElevenLabsGetUserResponse } from '@/tools/elevenlabs/types'
import type { ToolConfig } from '@/tools/types'
export const elevenLabsGetUserTool: ToolConfig<ElevenLabsGetUserParams, ElevenLabsGetUserResponse> =
{
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',
},
},
},
},
}
@@ -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)' },
},
}
+73
View File
@@ -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' },
},
}
+11 -3
View File
@@ -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'
+87
View File
@@ -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' },
},
},
},
},
},
},
},
}
+110
View File
@@ -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 },
},
}
+102
View File
@@ -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' },
},
}
@@ -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' },
},
}
+163 -1
View File
@@ -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<string, string> | 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
@@ -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,
},
},
},
}
+274
View File
@@ -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<FirecrawlBatchScrapeParams, FirecrawlBatchScrapeResponse> =
{
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<string, any> = {
urls: normalizeUrls(params.urls),
}
const scrapeOptions: Record<string, any> = { ...(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' },
},
},
}
+56
View File
@@ -0,0 +1,56 @@
import type {
FirecrawlCancelCrawlParams,
FirecrawlCancelCrawlResponse,
} from '@/tools/firecrawl/types'
import type { ToolConfig } from '@/tools/types'
export const cancelCrawlTool: ToolConfig<FirecrawlCancelCrawlParams, FirecrawlCancelCrawlResponse> =
{
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")',
},
},
}
+85
View File
@@ -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<FirecrawlCrawlStatusParams, FirecrawlCrawlStatusResponse> =
{
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,
},
},
},
}
+68
View File
@@ -0,0 +1,68 @@
import type {
FirecrawlCreditUsageParams,
FirecrawlCreditUsageResponse,
} from '@/tools/firecrawl/types'
import type { ToolConfig } from '@/tools/types'
export const creditUsageTool: ToolConfig<FirecrawlCreditUsageParams, FirecrawlCreditUsageResponse> =
{
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,
},
},
}
@@ -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,
},
},
}
+12
View File
@@ -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
+117
View File
@@ -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<string, any> | 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
@@ -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<string, unknown> = {
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' },
},
},
},
}
@@ -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' },
},
}
+85
View File
@@ -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<GoogleDriveExportParams, GoogleDriveExportResponse> = {
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',
},
},
}
@@ -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' },
},
},
},
}
+12
View File
@@ -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
@@ -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',
},
},
}
@@ -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',
},
},
}
+32
View File
@@ -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
+35
View File
@@ -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
+107
View File
@@ -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<PineconeDeleteVectorsParams, PineconeResponse> = {
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<string, unknown> = {}
if (params.namespace) {
body.namespace = params.namespace
}
if (params.ids != null && params.ids !== '') {
const ids = parseJsonParam<unknown>(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',
},
},
}
+79
View File
@@ -0,0 +1,79 @@
import type {
PineconeDescribeIndexParams,
PineconeIndexModel,
PineconeResponse,
} from '@/tools/pinecone/types'
import type { ToolConfig } from '@/tools/types'
export const describeIndexTool: ToolConfig<PineconeDescribeIndexParams, PineconeResponse> = {
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' },
},
},
},
}
@@ -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<string, unknown> = {}
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<string, { vectorCount: number | null }> = {}
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',
},
},
}
+12
View File
@@ -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
+84
View File
@@ -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<PineconeListIndexesParams, PineconeResponse> = {
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' },
},
},
},
},
}
+112
View File
@@ -0,0 +1,112 @@
import type { PineconeListVectorIdsParams, PineconeResponse } from '@/tools/pinecone/types'
import type { ToolConfig } from '@/tools/types'
export const listVectorIdsTool: ToolConfig<PineconeListVectorIdsParams, PineconeResponse> = {
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' },
},
},
},
}
+64
View File
@@ -15,6 +15,18 @@ interface PineconeMatchResponse {
metadata?: Record<string, any>
}
export interface PineconeIndexModel {
name: string
dimension?: number | null
metric?: string | null
host?: string | null
vectorType?: string | null
deletionProtection?: string | null
tags?: Record<string, string> | null
spec?: Record<string, any> | 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<string, { vectorCount: number | null }>
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, any> | string
}
export interface PineconeUpdateVectorParams {
apiKey: string
indexHost: string
id: string
namespace?: string
values?: number[] | string
sparseValues?: { indices: number[]; values: number[] } | string
setMetadata?: Record<string, any> | string
}
export interface PineconeDescribeIndexStatsParams {
apiKey: string
indexHost: string
filter?: Record<string, any> | 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
}
+97
View File
@@ -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<PineconeUpdateVectorParams, PineconeResponse> = {
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<string, unknown> = { 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',
},
},
}
+15
View File
@@ -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<T>(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`)
}
}
+85 -1
View File
@@ -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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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<string, ToolConfig> = {
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,
+63
View File
@@ -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<CancelEmailParams, CancelEmailResult> = {
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<CancelEmailResult> => {
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' },
},
}
+68
View File
@@ -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<CreateAudienceParams, CreateAudienceResult> = {
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<CreateAudienceResult> => {
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' },
},
}
+115
View File
@@ -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<CreateBroadcastParams, CreateBroadcastResult> = {
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 <sender@example.com>")',
},
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<CreateBroadcastResult> => {
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' },
},
}
+66
View File
@@ -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<DeleteAudienceParams, DeleteAudienceResult> = {
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<DeleteAudienceResult> => {
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' },
},
}
+69
View File
@@ -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<GetAudienceParams, GetAudienceResult> = {
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<GetAudienceResult> => {
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' },
},
}
+100
View File
@@ -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<GetBroadcastParams, GetBroadcastResult> = {
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<GetBroadcastResult> => {
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 },
},
}
+9
View File
@@ -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'
+70
View File
@@ -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<ListAudiencesParams, ListAudiencesResult> = {
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<ListAudiencesResult> => {
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' },
},
}
+73
View File
@@ -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<SendBroadcastParams, SendBroadcastResult> = {
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<SendBroadcastResult> => {
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' },
},
}
+123
View File
@@ -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
}
}
+90
View File
@@ -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',
},
},
}
+87
View File
@@ -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',
},
},
}
+124
View File
@@ -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',
},
},
}
+109
View File
@@ -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',
},
},
}
+19 -1
View File
@@ -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,
}
+111
View File
@@ -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',
},
},
}
+117
View File
@@ -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',
},
},
}
+31 -10
View File
@@ -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<string, string>
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
}
}
+2 -2
View File
@@ -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