mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aedf5e70b0 | ||
|
|
503268ebcd | ||
|
|
9a4de1f0c6 | ||
|
|
43a3416347 | ||
|
|
7f39cd0f23 | ||
|
|
658942deb3 | ||
|
|
061bd6d5a8 | ||
|
|
0ec91f9010 | ||
|
|
db581dc727 | ||
|
|
87e0586d0a | ||
|
|
9a7c58c8a2 | ||
|
|
004cd3339d | ||
|
|
9bd3491eac | ||
|
|
fd3ca87c38 | ||
|
|
d264a6ade8 | ||
|
|
de93e167af |
@@ -71,19 +71,16 @@ Read records from an Airtable table
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token |
|
||||
| `baseId` | string | Yes | ID of the Airtable base |
|
||||
| `tableId` | string | Yes | ID of the table |
|
||||
| `maxRecords` | number | No | Maximum number of records to return |
|
||||
| `filterFormula` | string | No | Formula to filter records \(e.g., |
|
||||
| `filterFormula` | string | No | Formula to filter records \(e.g., "\(\{Field Name\} = \'Value\'\)"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | json | Retrieved record data |
|
||||
| `record` | json | Single record data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `records` | json | Array of retrieved Airtable records |
|
||||
|
||||
### `airtable_get_record`
|
||||
|
||||
@@ -93,7 +90,6 @@ Retrieve a single record from an Airtable table by its ID
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token |
|
||||
| `baseId` | string | Yes | ID of the Airtable base |
|
||||
| `tableId` | string | Yes | ID or name of the table |
|
||||
| `recordId` | string | Yes | ID of the record to retrieve |
|
||||
@@ -102,9 +98,8 @@ Retrieve a single record from an Airtable table by its ID
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | json | Retrieved record data |
|
||||
| `record` | json | Single record data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `record` | json | Retrieved Airtable record with id, createdTime, and fields |
|
||||
| `metadata` | json | Operation metadata including record count |
|
||||
|
||||
### `airtable_create_records`
|
||||
|
||||
@@ -114,17 +109,16 @@ Write new records to an Airtable table
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token |
|
||||
| `baseId` | string | Yes | ID of the Airtable base |
|
||||
| `tableId` | string | Yes | ID or name of the table |
|
||||
| `records` | json | Yes | Array of records to create, each with a `fields` object |
|
||||
| `fields` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | json | Retrieved record data |
|
||||
| `record` | json | Single record data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `records` | json | Array of created Airtable records |
|
||||
|
||||
### `airtable_update_record`
|
||||
|
||||
@@ -134,7 +128,6 @@ Update an existing record in an Airtable table by ID
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token |
|
||||
| `baseId` | string | Yes | ID of the Airtable base |
|
||||
| `tableId` | string | Yes | ID or name of the table |
|
||||
| `recordId` | string | Yes | ID of the record to update |
|
||||
@@ -144,9 +137,8 @@ Update an existing record in an Airtable table by ID
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | json | Retrieved record data |
|
||||
| `record` | json | Single record data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `record` | json | Updated Airtable record with id, createdTime, and fields |
|
||||
| `metadata` | json | Operation metadata including record count and updated field names |
|
||||
|
||||
### `airtable_update_multiple_records`
|
||||
|
||||
@@ -156,17 +148,17 @@ Update multiple existing records in an Airtable table
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token |
|
||||
| `baseId` | string | Yes | ID of the Airtable base |
|
||||
| `tableId` | string | Yes | ID or name of the table |
|
||||
| `records` | json | Yes | Array of records to update, each with an `id` and a `fields` object |
|
||||
| `fields` | string | No | No description |
|
||||
| `fields` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | json | Retrieved record data |
|
||||
| `record` | json | Single record data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `records` | json | Array of updated Airtable records |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,10 +71,7 @@ Search for academic papers on ArXiv by keywords, authors, titles, or other field
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `papers` | json | Found papers data |
|
||||
| `totalResults` | number | Total results count |
|
||||
| `paper` | json | Paper details |
|
||||
| `authorPapers` | json | Author papers list |
|
||||
| `papers` | json | Array of papers matching the search query |
|
||||
|
||||
### `arxiv_get_paper`
|
||||
|
||||
@@ -84,16 +81,13 @@ Get detailed information about a specific ArXiv paper by its ID.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `paperId` | string | Yes | ArXiv paper ID \(e.g., |
|
||||
| `paperId` | string | Yes | ArXiv paper ID \(e.g., "1706.03762"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `papers` | json | Found papers data |
|
||||
| `totalResults` | number | Total results count |
|
||||
| `paper` | json | Paper details |
|
||||
| `authorPapers` | json | Author papers list |
|
||||
| `paper` | json | Detailed information about the requested ArXiv paper |
|
||||
|
||||
### `arxiv_get_author_papers`
|
||||
|
||||
@@ -110,10 +104,7 @@ Search for papers by a specific author on ArXiv.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `papers` | json | Found papers data |
|
||||
| `totalResults` | number | Total results count |
|
||||
| `paper` | json | Paper details |
|
||||
| `authorPapers` | json | Author papers list |
|
||||
| `authorPapers` | json | Array of papers authored by the specified author |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ Runs a browser automation task using BrowserUse
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `task` | string | Yes | What should the browser agent do |
|
||||
| `variables` | json | No | Optional variables to use as secrets \(format: \{key: value\}\) |
|
||||
| `format` | string | No | No description |
|
||||
| `save_browser_data` | boolean | No | Whether to save browser data |
|
||||
| `model` | string | No | LLM model to use \(default: gpt-4o\) |
|
||||
| `apiKey` | string | Yes | API key for BrowserUse API |
|
||||
@@ -81,10 +82,9 @@ Runs a browser automation task using BrowserUse
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Task execution identifier |
|
||||
| `success` | boolean | Task completion status |
|
||||
| `output` | any | Task output data |
|
||||
| `steps` | json | Execution steps taken |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | json | Browser automation task results including task ID, success status, output data, and execution steps |
|
||||
| `error` | string | Error message if the operation failed |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -220,7 +220,8 @@ Populate Clay with data from a JSON file. Enables direct communication and notif
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | any | Response data |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | json | Clay populate operation results including response data from Clay webhook |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ Retrieve content from Confluence pages using the Confluence API.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Confluence |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `pageId` | string | Yes | Confluence page ID to retrieve |
|
||||
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
|
||||
@@ -66,11 +65,10 @@ Retrieve content from Confluence pages using the Confluence API.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp |
|
||||
| `pageId` | string | Page identifier |
|
||||
| `content` | string | Page content |
|
||||
| `ts` | string | Timestamp of retrieval |
|
||||
| `pageId` | string | Confluence page ID |
|
||||
| `content` | string | Page content with HTML tags stripped |
|
||||
| `title` | string | Page title |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `confluence_update`
|
||||
|
||||
@@ -80,7 +78,6 @@ Update a Confluence page using the Confluence API.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Confluence |
|
||||
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `pageId` | string | Yes | Confluence page ID to update |
|
||||
| `title` | string | No | New title for the page |
|
||||
@@ -92,11 +89,10 @@ Update a Confluence page using the Confluence API.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp |
|
||||
| `pageId` | string | Page identifier |
|
||||
| `content` | string | Page content |
|
||||
| `title` | string | Page title |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `ts` | string | Timestamp of update |
|
||||
| `pageId` | string | Confluence page ID |
|
||||
| `title` | string | Updated page title |
|
||||
| `success` | boolean | Update operation success status |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ Send a message to a Discord channel
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message content |
|
||||
| `data` | any | Response data |
|
||||
| `message` | string | Success or error message |
|
||||
| `data` | object | Discord message data |
|
||||
|
||||
### `discord_get_messages`
|
||||
|
||||
@@ -99,8 +99,8 @@ Retrieve messages from a Discord channel
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message content |
|
||||
| `data` | any | Response data |
|
||||
| `message` | string | Success or error message |
|
||||
| `messages` | array | Array of Discord messages with full metadata |
|
||||
|
||||
### `discord_get_server`
|
||||
|
||||
@@ -117,8 +117,8 @@ Retrieve information about a Discord server (guild)
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message content |
|
||||
| `data` | any | Response data |
|
||||
| `message` | string | Success or error message |
|
||||
| `data` | object | Discord server \(guild\) information |
|
||||
|
||||
### `discord_get_user`
|
||||
|
||||
@@ -135,8 +135,8 @@ Retrieve information about a Discord user
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Message content |
|
||||
| `data` | any | Response data |
|
||||
| `message` | string | Success or error message |
|
||||
| `data` | object | Discord user information |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,11 +68,7 @@ Search the web using Exa AI. Returns relevant search results with titles, URLs,
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `similarLinks` | json | Similar links found |
|
||||
| `answer` | string | Generated answer |
|
||||
| `citations` | json | Answer citations |
|
||||
| `research` | json | Research findings |
|
||||
| `results` | array | Search results with titles, URLs, and text snippets |
|
||||
|
||||
### `exa_get_contents`
|
||||
|
||||
@@ -91,11 +87,7 @@ Retrieve the contents of webpages using Exa AI. Returns the title, text content,
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `similarLinks` | json | Similar links found |
|
||||
| `answer` | string | Generated answer |
|
||||
| `citations` | json | Answer citations |
|
||||
| `research` | json | Research findings |
|
||||
| `results` | array | Retrieved content from URLs with title, text, and summaries |
|
||||
|
||||
### `exa_find_similar_links`
|
||||
|
||||
@@ -114,11 +106,7 @@ Find webpages similar to a given URL using Exa AI. Returns a list of similar lin
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `similarLinks` | json | Similar links found |
|
||||
| `answer` | string | Generated answer |
|
||||
| `citations` | json | Answer citations |
|
||||
| `research` | json | Research findings |
|
||||
| `similarLinks` | array | Similar links found with titles, URLs, and text snippets |
|
||||
|
||||
### `exa_answer`
|
||||
|
||||
@@ -136,11 +124,8 @@ Get an AI-generated answer to a question with citations from the web using Exa A
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `similarLinks` | json | Similar links found |
|
||||
| `answer` | string | Generated answer |
|
||||
| `citations` | json | Answer citations |
|
||||
| `research` | json | Research findings |
|
||||
| `answer` | string | AI-generated answer to the question |
|
||||
| `citations` | array | Sources and citations for the answer |
|
||||
|
||||
### `exa_research`
|
||||
|
||||
@@ -158,11 +143,7 @@ Perform comprehensive research using AI to generate detailed reports with citati
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `similarLinks` | json | Similar links found |
|
||||
| `answer` | string | Generated answer |
|
||||
| `citations` | json | Answer citations |
|
||||
| `research` | json | Research findings |
|
||||
| `research` | array | Comprehensive research findings with citations and summaries |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `files` | json | Parsed file data |
|
||||
| `combinedContent` | string | Combined file content |
|
||||
| `files` | json | Array of parsed file objects with content, metadata, and file properties |
|
||||
| `combinedContent` | string | All file contents merged into a single text string |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -81,14 +81,9 @@ Extract structured content from web pages with comprehensive metadata support. C
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `markdown` | string | Page content markdown |
|
||||
| `html` | any | Raw HTML content |
|
||||
| `metadata` | json | Page metadata |
|
||||
| `data` | json | Search results data |
|
||||
| `warning` | any | Warning messages |
|
||||
| `pages` | json | Crawled pages data |
|
||||
| `total` | number | Total pages found |
|
||||
| `creditsUsed` | number | Credits consumed |
|
||||
| `markdown` | string | Page content in markdown format |
|
||||
| `html` | string | Raw HTML content of the page |
|
||||
| `metadata` | object | Page metadata including SEO and Open Graph information |
|
||||
|
||||
### `firecrawl_search`
|
||||
|
||||
@@ -105,14 +100,7 @@ Search for information on the web using Firecrawl
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `markdown` | string | Page content markdown |
|
||||
| `html` | any | Raw HTML content |
|
||||
| `metadata` | json | Page metadata |
|
||||
| `data` | json | Search results data |
|
||||
| `warning` | any | Warning messages |
|
||||
| `pages` | json | Crawled pages data |
|
||||
| `total` | number | Total pages found |
|
||||
| `creditsUsed` | number | Credits consumed |
|
||||
| `data` | array | Search results data |
|
||||
|
||||
### `firecrawl_crawl`
|
||||
|
||||
@@ -131,14 +119,7 @@ Crawl entire websites and extract structured content from all accessible pages
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `markdown` | string | Page content markdown |
|
||||
| `html` | any | Raw HTML content |
|
||||
| `metadata` | json | Page metadata |
|
||||
| `data` | json | Search results data |
|
||||
| `warning` | any | Warning messages |
|
||||
| `pages` | json | Crawled pages data |
|
||||
| `total` | number | Total pages found |
|
||||
| `creditsUsed` | number | Credits consumed |
|
||||
| `pages` | array | Array of crawled pages with their content and metadata |
|
||||
|
||||
|
||||
|
||||
|
||||
32
apps/docs/content/docs/tools/generic_webhook.mdx
Normal file
32
apps/docs/content/docs/tools/generic_webhook.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Webhook
|
||||
description: Receive webhooks from any service
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="generic_webhook"
|
||||
color="#10B981"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon"
|
||||
|
||||
fill='currentColor'
|
||||
|
||||
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path d='M17.974 7A4.967 4.967 0 0 0 18 6.5a5.5 5.5 0 1 0-8.672 4.491L7.18 15.114A2.428 2.428 0 0 0 6.496 15 2.5 2.5 0 1 0 9 17.496a2.36 2.36 0 0 0-.93-1.925l2.576-4.943-.41-.241A4.5 4.5 0 1 1 17 6.5a4.8 4.8 0 0 1-.022.452zM6.503 18.999a1.5 1.5 0 1 1 1.496-1.503A1.518 1.518 0 0 1 6.503 19zM18.5 12a5.735 5.735 0 0 0-1.453.157l-2.744-3.941A2.414 2.414 0 0 0 15 6.5a2.544 2.544 0 1 0-1.518 2.284l3.17 4.557.36-.13A4.267 4.267 0 0 1 18.5 13a4.5 4.5 0 1 1-.008 9h-.006a4.684 4.684 0 0 1-3.12-1.355l-.703.71A5.653 5.653 0 0 0 18.49 23h.011a5.5 5.5 0 0 0 0-11zM11 6.5A1.5 1.5 0 1 1 12.5 8 1.509 1.509 0 0 1 11 6.5zM18.5 20a2.5 2.5 0 1 0-2.447-3h-5.05l-.003.497A4.546 4.546 0 0 1 6.5 22 4.526 4.526 0 0 1 2 17.5a4.596 4.596 0 0 1 3.148-4.37l-.296-.954A5.606 5.606 0 0 0 1 17.5 5.532 5.532 0 0 0 6.5 23a5.573 5.573 0 0 0 5.478-5h4.08a2.487 2.487 0 0 0 2.442 2zm0-4a1.5 1.5 0 1 1-1.5 1.5 1.509 1.509 0 0 1 1.5-1.5z' />
|
||||
<path fill='none' d='M0 0h24v24H0z' />
|
||||
</svg>`}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `triggers`
|
||||
- Type: `generic_webhook`
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: GitHub
|
||||
description: Interact with GitHub
|
||||
description: Interact with GitHub or trigger workflows from GitHub events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -35,7 +35,7 @@ In Sim, the GitHub integration enables your agents to interact directly with Git
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow.
|
||||
Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow. Trigger workflows from GitHub events like push, pull requests, and issues.
|
||||
|
||||
|
||||
|
||||
@@ -58,8 +58,8 @@ Fetch PR details including diff and files changed
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Response metadata |
|
||||
| `content` | string | Human-readable PR summary |
|
||||
| `metadata` | object | Detailed PR metadata including file changes |
|
||||
|
||||
### `github_comment`
|
||||
|
||||
@@ -85,8 +85,8 @@ Create comments on GitHub PRs
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Response metadata |
|
||||
| `content` | string | Human-readable comment confirmation |
|
||||
| `metadata` | object | Comment metadata |
|
||||
|
||||
### `github_repo_info`
|
||||
|
||||
@@ -104,8 +104,8 @@ Retrieve comprehensive GitHub repository metadata including stars, forks, issues
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Response metadata |
|
||||
| `content` | string | Human-readable repository summary |
|
||||
| `metadata` | object | Repository metadata |
|
||||
|
||||
### `github_latest_commit`
|
||||
|
||||
@@ -117,15 +117,15 @@ Retrieve the latest commit from a GitHub repository
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner \(user or organization\) |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `branch` | string | No | Branch name \(defaults to the repository |
|
||||
| `branch` | string | No | Branch name \(defaults to the repository's default branch\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Response metadata |
|
||||
| `content` | string | Human-readable commit summary |
|
||||
| `metadata` | object | Commit metadata |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Gmail
|
||||
description: Send Gmail
|
||||
description: Send Gmail or trigger workflows from Gmail events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -51,7 +51,7 @@ In Sim, the Gmail integration enables your agents to send, read, and search emai
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Gmail functionality to send email messages within your workflow. Automate email communications and process email content using OAuth authentication.
|
||||
Comprehensive Gmail integration with OAuth authentication. Send email messages, read email content, and trigger workflows from Gmail events like new emails and label changes.
|
||||
|
||||
|
||||
|
||||
@@ -65,17 +65,18 @@ Send emails using Gmail
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Gmail API |
|
||||
| `to` | string | Yes | Recipient email address |
|
||||
| `subject` | string | Yes | Email subject |
|
||||
| `body` | string | Yes | Email body content |
|
||||
| `cc` | string | No | CC recipients \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipients \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Email metadata |
|
||||
| `content` | string | Success message |
|
||||
| `metadata` | object | Email metadata |
|
||||
|
||||
### `gmail_draft`
|
||||
|
||||
@@ -85,17 +86,18 @@ Draft emails using Gmail
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Gmail API |
|
||||
| `to` | string | Yes | Recipient email address |
|
||||
| `subject` | string | Yes | Email subject |
|
||||
| `body` | string | Yes | Email body content |
|
||||
| `cc` | string | No | CC recipients \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipients \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Email metadata |
|
||||
| `content` | string | Success message |
|
||||
| `metadata` | object | Draft metadata |
|
||||
|
||||
### `gmail_read`
|
||||
|
||||
@@ -105,18 +107,19 @@ Read emails from Gmail
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Gmail API |
|
||||
| `messageId` | string | No | ID of the message to read |
|
||||
| `folder` | string | No | Folder/label to read emails from |
|
||||
| `unreadOnly` | boolean | No | Only retrieve unread messages |
|
||||
| `maxResults` | number | No | Maximum number of messages to retrieve \(default: 1, max: 10\) |
|
||||
| `includeAttachments` | boolean | No | Download and include email attachments |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Email metadata |
|
||||
| `content` | string | Text content of the email |
|
||||
| `metadata` | json | Metadata of the email |
|
||||
| `attachments` | file[] | Attachments of the email |
|
||||
|
||||
### `gmail_search`
|
||||
|
||||
@@ -126,7 +129,6 @@ Search emails in Gmail
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Gmail API |
|
||||
| `query` | string | Yes | Search query for emails |
|
||||
| `maxResults` | number | No | Maximum number of results to return |
|
||||
|
||||
@@ -134,8 +136,8 @@ Search emails in Gmail
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Response content |
|
||||
| `metadata` | json | Email metadata |
|
||||
| `content` | string | Search results summary |
|
||||
| `metadata` | object | Search metadata |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -104,7 +104,6 @@ Create a new event in Google Calendar
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Google Calendar API |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `summary` | string | Yes | Event title/summary |
|
||||
| `description` | string | No | Event description |
|
||||
@@ -119,8 +118,8 @@ Create a new event in Google Calendar
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Operation response content |
|
||||
| `metadata` | json | Event metadata |
|
||||
| `content` | string | Event creation confirmation message |
|
||||
| `metadata` | json | Created event metadata including ID, status, and details |
|
||||
|
||||
### `google_calendar_list`
|
||||
|
||||
@@ -130,7 +129,6 @@ List events from Google Calendar
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Google Calendar API |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `timeMin` | string | No | Lower bound for events \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
|
||||
| `timeMax` | string | No | Upper bound for events \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
|
||||
@@ -141,8 +139,8 @@ List events from Google Calendar
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Operation response content |
|
||||
| `metadata` | json | Event metadata |
|
||||
| `content` | string | Summary of found events count |
|
||||
| `metadata` | json | List of events with pagination tokens and event details |
|
||||
|
||||
### `google_calendar_get`
|
||||
|
||||
@@ -152,7 +150,6 @@ Get a specific event from Google Calendar
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Google Calendar API |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `eventId` | string | Yes | Event ID to retrieve |
|
||||
|
||||
@@ -160,8 +157,8 @@ Get a specific event from Google Calendar
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Operation response content |
|
||||
| `metadata` | json | Event metadata |
|
||||
| `content` | string | Event retrieval confirmation message |
|
||||
| `metadata` | json | Event details including ID, status, times, and attendees |
|
||||
|
||||
### `google_calendar_quick_add`
|
||||
|
||||
@@ -171,9 +168,8 @@ Create events from natural language text
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Google Calendar API |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `text` | string | Yes | Natural language text describing the event \(e.g., |
|
||||
| `text` | string | Yes | Natural language text describing the event \(e.g., "Meeting with John tomorrow at 3pm"\) |
|
||||
| `attendees` | array | No | Array of attendee email addresses \(comma-separated string also accepted\) |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
|
||||
@@ -181,8 +177,8 @@ Create events from natural language text
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Operation response content |
|
||||
| `metadata` | json | Event metadata |
|
||||
| `content` | string | Event creation confirmation message from natural language |
|
||||
| `metadata` | json | Created event metadata including parsed details |
|
||||
|
||||
### `google_calendar_invite`
|
||||
|
||||
@@ -192,7 +188,6 @@ Invite attendees to an existing Google Calendar event
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Google Calendar API |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `eventId` | string | Yes | Event ID to invite attendees to |
|
||||
| `attendees` | array | Yes | Array of attendee email addresses to invite |
|
||||
@@ -203,8 +198,8 @@ Invite attendees to an existing Google Calendar event
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Operation response content |
|
||||
| `metadata` | json | Event metadata |
|
||||
| `content` | string | Attendee invitation confirmation message with email delivery status |
|
||||
| `metadata` | json | Updated event metadata including attendee list and details |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,16 +95,14 @@ Read content from a Google Docs document
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Docs API |
|
||||
| `documentId` | string | Yes | The ID of the document to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Document content |
|
||||
| `metadata` | json | Document metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `content` | string | Extracted document text content |
|
||||
| `metadata` | json | Document metadata including ID, title, and URL |
|
||||
|
||||
### `google_docs_write`
|
||||
|
||||
@@ -114,7 +112,6 @@ Write or update content in a Google Docs document
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Docs API |
|
||||
| `documentId` | string | Yes | The ID of the document to write to |
|
||||
| `content` | string | Yes | The content to write to the document |
|
||||
|
||||
@@ -122,9 +119,8 @@ Write or update content in a Google Docs document
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Document content |
|
||||
| `metadata` | json | Document metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `updatedContent` | boolean | Indicates if document content was updated successfully |
|
||||
| `metadata` | json | Updated document metadata including ID, title, and URL |
|
||||
|
||||
### `google_docs_create`
|
||||
|
||||
@@ -134,7 +130,6 @@ Create a new Google Docs document
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Docs API |
|
||||
| `title` | string | Yes | The title of the document to create |
|
||||
| `content` | string | No | The content of the document to create |
|
||||
| `folderSelector` | string | No | Select the folder to create the document in |
|
||||
@@ -144,9 +139,7 @@ Create a new Google Docs document
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Document content |
|
||||
| `metadata` | json | Document metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `metadata` | json | Created document metadata including ID, title, and URL |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ Upload a file to Google Drive
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Drive API |
|
||||
| `fileName` | string | Yes | The name of the file to upload |
|
||||
| `content` | string | Yes | The content of the file to upload |
|
||||
| `mimeType` | string | No | The MIME type of the file to upload |
|
||||
@@ -98,8 +97,7 @@ Upload a file to Google Drive
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | File data |
|
||||
| `files` | json | Files list |
|
||||
| `file` | json | Uploaded file metadata including ID, name, and links |
|
||||
|
||||
### `google_drive_create_folder`
|
||||
|
||||
@@ -109,7 +107,6 @@ Create a new folder in Google Drive
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Drive API |
|
||||
| `fileName` | string | Yes | Name of the folder to create |
|
||||
| `folderSelector` | string | No | Select the parent folder to create the folder in |
|
||||
| `folderId` | string | No | ID of the parent folder \(internal use\) |
|
||||
@@ -118,8 +115,7 @@ Create a new folder in Google Drive
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | File data |
|
||||
| `files` | json | Files list |
|
||||
| `file` | json | Created folder metadata including ID, name, and parent information |
|
||||
|
||||
### `google_drive_list`
|
||||
|
||||
@@ -129,7 +125,6 @@ List files and folders in Google Drive
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Drive API |
|
||||
| `folderSelector` | string | No | Select the folder to list files from |
|
||||
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
|
||||
| `query` | string | No | A query to filter the files |
|
||||
@@ -140,8 +135,7 @@ List files and folders in Google Drive
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | File data |
|
||||
| `files` | json | Files list |
|
||||
| `files` | json | Array of file metadata objects from the specified folder |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -81,8 +81,7 @@ Search the web with the Custom Search API
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | json | Search result items |
|
||||
| `searchInformation` | json | Search metadata |
|
||||
| `items` | array | Array of search results from Google |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ Read data from a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Sheets API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from |
|
||||
| `range` | string | No | The range of cells to read from |
|
||||
|
||||
@@ -118,13 +117,8 @@ Read data from a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `tableRange` | string | Table range |
|
||||
| `data` | json | Sheet data including range and cell values |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
|
||||
### `google_sheets_write`
|
||||
|
||||
@@ -134,7 +128,6 @@ Write data to a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Sheets API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to |
|
||||
| `range` | string | No | The range of cells to write to |
|
||||
| `values` | array | Yes | The data to write to the spreadsheet |
|
||||
@@ -145,13 +138,11 @@ Write data to a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `tableRange` | string | Table range |
|
||||
| `updatedRange` | string | Range of cells that were updated |
|
||||
| `updatedRows` | number | Number of rows updated |
|
||||
| `updatedColumns` | number | Number of columns updated |
|
||||
| `updatedCells` | number | Number of cells updated |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
|
||||
### `google_sheets_update`
|
||||
|
||||
@@ -161,7 +152,6 @@ Update data in a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Sheets API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to update |
|
||||
| `range` | string | No | The range of cells to update |
|
||||
| `values` | array | Yes | The data to update in the spreadsheet |
|
||||
@@ -172,13 +162,11 @@ Update data in a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `tableRange` | string | Table range |
|
||||
| `updatedRange` | string | Range of cells that were updated |
|
||||
| `updatedRows` | number | Number of rows updated |
|
||||
| `updatedColumns` | number | Number of columns updated |
|
||||
| `updatedCells` | number | Number of cells updated |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
|
||||
### `google_sheets_append`
|
||||
|
||||
@@ -188,7 +176,6 @@ Append data to the end of a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Google Sheets API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to |
|
||||
| `range` | string | No | The range of cells to append after |
|
||||
| `values` | array | Yes | The data to append to the spreadsheet |
|
||||
@@ -200,13 +187,12 @@ Append data to the end of a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `tableRange` | string | Table range |
|
||||
| `tableRange` | string | Range of the table where data was appended |
|
||||
| `updatedRange` | string | Range of cells that were updated |
|
||||
| `updatedRows` | number | Number of rows updated |
|
||||
| `updatedColumns` | number | Number of columns updated |
|
||||
| `updatedCells` | number | Number of cells updated |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -92,9 +92,8 @@ Generate completions using Hugging Face Inference API
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Generated response |
|
||||
| `model` | string | Model used |
|
||||
| `usage` | json | Token usage stats |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Chat completion results |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search.
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `query` | string | No | Natural language search query for companies |
|
||||
| `domain` | string | No | Company domain names to filter by |
|
||||
| `headcount` | string | No | Company size filter \(e.g., |
|
||||
| `headcount` | string | No | Company size filter \(e.g., "1-10", "11-50"\) |
|
||||
| `company_type` | string | No | Type of organization |
|
||||
| `technology` | string | No | Technology used by companies |
|
||||
| `apiKey` | string | Yes | Hunter.io API Key |
|
||||
@@ -66,15 +66,7 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `emails` | json | Email addresses found |
|
||||
| `email` | string | Found email address |
|
||||
| `score` | number | Confidence score |
|
||||
| `result` | string | Verification result |
|
||||
| `status` | string | Status message |
|
||||
| `total` | number | Total results count |
|
||||
| `personal_emails` | number | Personal emails count |
|
||||
| `generic_emails` | number | Generic emails count |
|
||||
| `results` | array | Array of companies matching the search criteria, each containing domain, name, headcount, technologies, and email_count |
|
||||
|
||||
### `hunter_domain_search`
|
||||
|
||||
@@ -96,15 +88,26 @@ Returns all the email addresses found using one given domain name, with sources.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `emails` | json | Email addresses found |
|
||||
| `email` | string | Found email address |
|
||||
| `score` | number | Confidence score |
|
||||
| `result` | string | Verification result |
|
||||
| `status` | string | Status message |
|
||||
| `total` | number | Total results count |
|
||||
| `personal_emails` | number | Personal emails count |
|
||||
| `generic_emails` | number | Generic emails count |
|
||||
| `domain` | string | The searched domain name |
|
||||
| `disposable` | boolean | Whether the domain accepts disposable email addresses |
|
||||
| `webmail` | boolean | Whether the domain is a webmail provider |
|
||||
| `accept_all` | boolean | Whether the domain accepts all email addresses |
|
||||
| `pattern` | string | The email pattern used by the organization |
|
||||
| `organization` | string | The organization name |
|
||||
| `description` | string | Description of the organization |
|
||||
| `industry` | string | Industry of the organization |
|
||||
| `twitter` | string | Twitter profile of the organization |
|
||||
| `facebook` | string | Facebook profile of the organization |
|
||||
| `linkedin` | string | LinkedIn profile of the organization |
|
||||
| `instagram` | string | Instagram profile of the organization |
|
||||
| `youtube` | string | YouTube channel of the organization |
|
||||
| `technologies` | array | Array of technologies used by the organization |
|
||||
| `country` | string | Country where the organization is located |
|
||||
| `state` | string | State where the organization is located |
|
||||
| `city` | string | City where the organization is located |
|
||||
| `postal_code` | string | Postal code of the organization |
|
||||
| `street` | string | Street address of the organization |
|
||||
| `emails` | array | Array of email addresses found for the domain, each containing value, type, confidence, sources, and person details |
|
||||
|
||||
### `hunter_email_finder`
|
||||
|
||||
@@ -115,8 +118,8 @@ Finds the most likely email address for a person given their name and company do
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `domain` | string | Yes | Company domain name |
|
||||
| `first_name` | string | Yes | Person |
|
||||
| `last_name` | string | Yes | Person |
|
||||
| `first_name` | string | Yes | Person's first name |
|
||||
| `last_name` | string | Yes | Person's last name |
|
||||
| `company` | string | No | Company name |
|
||||
| `apiKey` | string | Yes | Hunter.io API Key |
|
||||
|
||||
@@ -124,15 +127,10 @@ Finds the most likely email address for a person given their name and company do
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `emails` | json | Email addresses found |
|
||||
| `email` | string | Found email address |
|
||||
| `score` | number | Confidence score |
|
||||
| `result` | string | Verification result |
|
||||
| `status` | string | Status message |
|
||||
| `total` | number | Total results count |
|
||||
| `personal_emails` | number | Personal emails count |
|
||||
| `generic_emails` | number | Generic emails count |
|
||||
| `email` | string | The found email address |
|
||||
| `score` | number | Confidence score for the found email address |
|
||||
| `sources` | array | Array of sources where the email was found, each containing domain, uri, extracted_on, last_seen_on, and still_on_page |
|
||||
| `verification` | object | Verification information containing date and status |
|
||||
|
||||
### `hunter_email_verifier`
|
||||
|
||||
@@ -149,15 +147,20 @@ Verifies the deliverability of an email address and provides detailed verificati
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `emails` | json | Email addresses found |
|
||||
| `email` | string | Found email address |
|
||||
| `score` | number | Confidence score |
|
||||
| `result` | string | Verification result |
|
||||
| `status` | string | Status message |
|
||||
| `total` | number | Total results count |
|
||||
| `personal_emails` | number | Personal emails count |
|
||||
| `generic_emails` | number | Generic emails count |
|
||||
| `result` | string | Deliverability result: deliverable, undeliverable, or risky |
|
||||
| `score` | number | Confidence score for the verification result |
|
||||
| `email` | string | The verified email address |
|
||||
| `regexp` | boolean | Whether the email follows a valid regex pattern |
|
||||
| `gibberish` | boolean | Whether the email appears to be gibberish |
|
||||
| `disposable` | boolean | Whether the email is from a disposable email provider |
|
||||
| `webmail` | boolean | Whether the email is from a webmail provider |
|
||||
| `mx_records` | boolean | Whether MX records exist for the domain |
|
||||
| `smtp_server` | boolean | Whether the SMTP server is reachable |
|
||||
| `smtp_check` | boolean | Whether the SMTP check was successful |
|
||||
| `accept_all` | boolean | Whether the domain accepts all email addresses |
|
||||
| `block` | boolean | Whether the email is blocked |
|
||||
| `status` | string | Verification status: valid, invalid, accept_all, webmail, disposable, or unknown |
|
||||
| `sources` | array | Array of sources where the email was found |
|
||||
|
||||
### `hunter_companies_find`
|
||||
|
||||
@@ -174,15 +177,8 @@ Enriches company data using domain name.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `emails` | json | Email addresses found |
|
||||
| `email` | string | Found email address |
|
||||
| `score` | number | Confidence score |
|
||||
| `result` | string | Verification result |
|
||||
| `status` | string | Status message |
|
||||
| `total` | number | Total results count |
|
||||
| `personal_emails` | number | Personal emails count |
|
||||
| `generic_emails` | number | Generic emails count |
|
||||
| `person` | object | Person information \(undefined for companies_find tool\) |
|
||||
| `company` | object | Company information including name, domain, industry, size, country, linkedin, and twitter |
|
||||
|
||||
### `hunter_email_count`
|
||||
|
||||
@@ -201,15 +197,11 @@ Returns the total number of email addresses found for a domain or company.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `emails` | json | Email addresses found |
|
||||
| `email` | string | Found email address |
|
||||
| `score` | number | Confidence score |
|
||||
| `result` | string | Verification result |
|
||||
| `status` | string | Status message |
|
||||
| `total` | number | Total results count |
|
||||
| `personal_emails` | number | Personal emails count |
|
||||
| `generic_emails` | number | Generic emails count |
|
||||
| `total` | number | Total number of email addresses found |
|
||||
| `personal_emails` | number | Number of personal email addresses found |
|
||||
| `generic_emails` | number | Number of generic email addresses found |
|
||||
| `department` | object | Breakdown of email addresses by department \(executive, it, finance, management, sales, legal, support, hr, marketing, communication\) |
|
||||
| `seniority` | object | Breakdown of email addresses by seniority level \(junior, senior, executive\) |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,9 +73,8 @@ Generate images using OpenAI
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Generation response |
|
||||
| `image` | string | Generated image URL |
|
||||
| `metadata` | json | Generation metadata |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Generated image data |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ Extract and process web content into clean, LLM-friendly text using Jina AI Read
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Extracted content |
|
||||
| `content` | string | The extracted content from the URL, processed into clean, LLM-friendly text |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ Retrieve detailed information about a specific Jira issue
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Jira |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `projectId` | string | No | Jira project ID to retrieve issues from. If not provided, all issues will be retrieved. |
|
||||
| `issueKey` | string | Yes | Jira issue key to retrieve \(e.g., PROJ-123\) |
|
||||
@@ -67,14 +66,8 @@ Retrieve detailed information about a specific Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `summary` | string | Issue summary |
|
||||
| `description` | string | Issue description |
|
||||
| `created` | string | Creation date |
|
||||
| `updated` | string | Update date |
|
||||
| `success` | boolean | Operation success |
|
||||
| `url` | string | Issue URL |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Jira issue details with issue key, summary, description, created and updated timestamps |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -84,7 +77,6 @@ Update a Jira issue
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Jira |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `projectId` | string | No | Jira project ID to update issues in. If not provided, all issues will be retrieved. |
|
||||
| `issueKey` | string | Yes | Jira issue key to update |
|
||||
@@ -99,14 +91,8 @@ Update a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `summary` | string | Issue summary |
|
||||
| `description` | string | Issue description |
|
||||
| `created` | string | Creation date |
|
||||
| `updated` | string | Update date |
|
||||
| `success` | boolean | Operation success |
|
||||
| `url` | string | Issue URL |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Updated Jira issue details with timestamp, issue key, summary, and success status |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -116,7 +102,6 @@ Write a Jira issue
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Jira |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `projectId` | string | Yes | Project ID for the issue |
|
||||
| `summary` | string | Yes | Summary for the issue |
|
||||
@@ -130,14 +115,8 @@ Write a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `summary` | string | Issue summary |
|
||||
| `description` | string | Issue description |
|
||||
| `created` | string | Creation date |
|
||||
| `updated` | string | Update date |
|
||||
| `success` | boolean | Operation success |
|
||||
| `url` | string | Issue URL |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created Jira issue details with timestamp, issue key, summary, success status, and URL |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -147,7 +126,6 @@ Retrieve multiple Jira issues in bulk
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Jira |
|
||||
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
|
||||
| `projectId` | string | Yes | Jira project ID |
|
||||
| `cloudId` | string | No | Jira cloud ID |
|
||||
@@ -156,14 +134,8 @@ Retrieve multiple Jira issues in bulk
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `summary` | string | Issue summary |
|
||||
| `description` | string | Issue description |
|
||||
| `created` | string | Creation date |
|
||||
| `updated` | string | Update date |
|
||||
| `success` | boolean | Operation success |
|
||||
| `url` | string | Issue URL |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | array | Array of Jira issues with summary, description, created and updated timestamps |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -72,9 +72,7 @@ Search for similar content in a knowledge base using vector similarity
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `query` | string | Query used |
|
||||
| `totalResults` | number | Total results count |
|
||||
| `results` | array | Array of search results from the knowledge base |
|
||||
|
||||
### `knowledge_upload_chunk`
|
||||
|
||||
@@ -92,9 +90,7 @@ Upload a new chunk to a document in a knowledge base
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `query` | string | Query used |
|
||||
| `totalResults` | number | Total results count |
|
||||
| `data` | object | Information about the uploaded chunk |
|
||||
|
||||
### `knowledge_create_document`
|
||||
|
||||
@@ -120,9 +116,7 @@ Create a new document in a knowledge base
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results |
|
||||
| `query` | string | Query used |
|
||||
| `totalResults` | number | Total results count |
|
||||
| `data` | object | Information about the created document |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -63,8 +63,7 @@ Fetch and filter issues from Linear
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | json | Issues list |
|
||||
| `issue` | json | Single issue data |
|
||||
| `issues` | array | Array of issues from the specified Linear team and project, each containing id, title, description, state, teamId, and projectId |
|
||||
|
||||
### `linear_create_issue`
|
||||
|
||||
@@ -83,8 +82,7 @@ Create a new issue in Linear
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | json | Issues list |
|
||||
| `issue` | json | Single issue data |
|
||||
| `issue` | object | The created issue containing id, title, description, state, teamId, and projectId |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,16 +58,16 @@ Search the web for information using Linkup
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | The search query |
|
||||
| `depth` | string | Yes | Search depth \(has to either be |
|
||||
| `outputType` | string | Yes | Type of output to return \(has to either be |
|
||||
| `depth` | string | Yes | Search depth \(has to either be "standard" or "deep"\) |
|
||||
| `outputType` | string | Yes | Type of output to return \(has to either be "sourcedAnswer" or "searchResults"\) |
|
||||
| `apiKey` | string | Yes | Enter your Linkup API key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `answer` | string | Generated answer |
|
||||
| `sources` | json | Source references |
|
||||
| `answer` | string | The sourced answer to the search query |
|
||||
| `sources` | array | Array of sources used to compile the answer, each containing name, url, and snippet |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -66,9 +66,8 @@ Add memories to Mem0 for persistent storage and retrieval
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ids` | any | Memory identifiers |
|
||||
| `memories` | any | Memory data |
|
||||
| `searchResults` | any | Search results |
|
||||
| `ids` | array | Array of memory IDs that were created |
|
||||
| `memories` | array | Array of memory objects that were created |
|
||||
|
||||
### `mem0_search_memories`
|
||||
|
||||
@@ -87,9 +86,8 @@ Search for memories in Mem0 using semantic search
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ids` | any | Memory identifiers |
|
||||
| `memories` | any | Memory data |
|
||||
| `searchResults` | any | Search results |
|
||||
| `searchResults` | array | Array of search results with memory data, each containing id, data, and score |
|
||||
| `ids` | array | Array of memory IDs found in the search results |
|
||||
|
||||
### `mem0_get_memories`
|
||||
|
||||
@@ -110,9 +108,8 @@ Retrieve memories from Mem0 by ID or filter criteria
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ids` | any | Memory identifiers |
|
||||
| `memories` | any | Memory data |
|
||||
| `searchResults` | any | Search results |
|
||||
| `memories` | array | Array of retrieved memory objects |
|
||||
| `ids` | array | Array of memory IDs that were retrieved |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,8 +57,9 @@ Add a new memory to the database or append to existing memory with the same ID.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `memories` | any | Memory data |
|
||||
| `id` | string | Memory identifier |
|
||||
| `success` | boolean | Whether the memory was added successfully |
|
||||
| `memories` | array | Array of memory objects including the new or updated memory |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_get`
|
||||
|
||||
@@ -74,8 +75,10 @@ Retrieve a specific memory by its ID
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `memories` | any | Memory data |
|
||||
| `id` | string | Memory identifier |
|
||||
| `success` | boolean | Whether the memory was retrieved successfully |
|
||||
| `memories` | array | Array of memory data for the requested ID |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_get_all`
|
||||
|
||||
@@ -90,8 +93,10 @@ Retrieve all memories from the database
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `memories` | any | Memory data |
|
||||
| `id` | string | Memory identifier |
|
||||
| `success` | boolean | Whether all memories were retrieved successfully |
|
||||
| `memories` | array | Array of all memory objects with keys, types, and data |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
### `memory_delete`
|
||||
|
||||
@@ -107,8 +112,9 @@ Delete a specific memory by its ID
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `memories` | any | Memory data |
|
||||
| `id` | string | Memory identifier |
|
||||
| `success` | boolean | Whether the memory was deleted successfully |
|
||||
| `message` | string | Success or error message |
|
||||
| `error` | string | Error message if operation failed |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"exa",
|
||||
"file",
|
||||
"firecrawl",
|
||||
"generic_webhook",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
|
||||
@@ -108,7 +108,6 @@ Read data from a Microsoft Excel spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Excel API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from |
|
||||
| `range` | string | No | The range of cells to read from |
|
||||
|
||||
@@ -116,14 +115,8 @@ Read data from a Microsoft Excel spreadsheet
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `index` | number | Row index |
|
||||
| `values` | json | Table values |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Excel spreadsheet data and metadata |
|
||||
|
||||
### `microsoft_excel_write`
|
||||
|
||||
@@ -133,7 +126,6 @@ Write data to a Microsoft Excel spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Excel API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to |
|
||||
| `range` | string | No | The range of cells to write to |
|
||||
| `values` | array | Yes | The data to write to the spreadsheet |
|
||||
@@ -144,14 +136,8 @@ Write data to a Microsoft Excel spreadsheet
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `index` | number | Row index |
|
||||
| `values` | json | Table values |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Write operation results and metadata |
|
||||
|
||||
### `microsoft_excel_table_add`
|
||||
|
||||
@@ -161,7 +147,6 @@ Add new rows to a Microsoft Excel table
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Excel API |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet containing the table |
|
||||
| `tableName` | string | Yes | The name of the table to add rows to |
|
||||
| `values` | array | Yes | The data to add to the table \(array of arrays or array of objects\) |
|
||||
@@ -170,14 +155,8 @@ Add new rows to a Microsoft Excel table
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Sheet data |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `updatedRange` | string | Updated range |
|
||||
| `updatedRows` | number | Updated rows count |
|
||||
| `updatedColumns` | number | Updated columns count |
|
||||
| `updatedCells` | number | Updated cells count |
|
||||
| `index` | number | Row index |
|
||||
| `values` | json | Table values |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Table add operation results and metadata |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ Read tasks from Microsoft Planner - get all user tasks or all tasks from a speci
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Planner API |
|
||||
| `planId` | string | No | The ID of the plan to get tasks from \(if not provided, gets all user tasks\) |
|
||||
| `taskId` | string | No | The ID of the task to get |
|
||||
|
||||
@@ -144,8 +143,9 @@ Read tasks from Microsoft Planner - get all user tasks or all tasks from a speci
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. |
|
||||
| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. |
|
||||
| `success` | boolean | Whether tasks were retrieved successfully |
|
||||
| `tasks` | array | Array of task objects with filtered properties |
|
||||
| `metadata` | object | Metadata including planId, userId, and planUrl |
|
||||
|
||||
### `microsoft_planner_create_task`
|
||||
|
||||
@@ -155,7 +155,6 @@ Create a new task in Microsoft Planner
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Planner API |
|
||||
| `planId` | string | Yes | The ID of the plan where the task will be created |
|
||||
| `title` | string | Yes | The title of the task |
|
||||
| `description` | string | No | The description of the task |
|
||||
@@ -167,8 +166,9 @@ Create a new task in Microsoft Planner
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `task` | json | The Microsoft Planner task object, including details such as id, title, description, status, due date, and assignees. |
|
||||
| `metadata` | json | Additional metadata about the operation, such as timestamps, request status, or other relevant information. |
|
||||
| `success` | boolean | Whether the task was created successfully |
|
||||
| `task` | object | The created task object with all properties |
|
||||
| `metadata` | object | Metadata including planId, taskId, and taskUrl |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -112,16 +112,19 @@ Read content from a Microsoft Teams chat
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
|
||||
| `chatId` | string | Yes | The ID of the chat to read from |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Message content |
|
||||
| `metadata` | json | Message metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `success` | boolean | Teams chat read operation success status |
|
||||
| `messageCount` | number | Number of messages retrieved from chat |
|
||||
| `chatId` | string | ID of the chat that was read from |
|
||||
| `messages` | array | Array of chat message objects |
|
||||
| `attachmentCount` | number | Total number of attachments found |
|
||||
| `attachmentTypes` | array | Types of attachments found |
|
||||
| `content` | string | Formatted content of chat messages |
|
||||
|
||||
### `microsoft_teams_write_chat`
|
||||
|
||||
@@ -131,7 +134,6 @@ Write or update content in a Microsoft Teams chat
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
|
||||
| `chatId` | string | Yes | The ID of the chat to write to |
|
||||
| `content` | string | Yes | The content to write to the message |
|
||||
|
||||
@@ -139,9 +141,12 @@ Write or update content in a Microsoft Teams chat
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Message content |
|
||||
| `metadata` | json | Message metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `success` | boolean | Teams chat message send success status |
|
||||
| `messageId` | string | Unique identifier for the sent message |
|
||||
| `chatId` | string | ID of the chat where message was sent |
|
||||
| `createdTime` | string | Timestamp when message was created |
|
||||
| `url` | string | Web URL to the message |
|
||||
| `updatedContent` | boolean | Whether content was successfully updated |
|
||||
|
||||
### `microsoft_teams_read_channel`
|
||||
|
||||
@@ -151,7 +156,6 @@ Read content from a Microsoft Teams channel
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
|
||||
| `teamId` | string | Yes | The ID of the team to read from |
|
||||
| `channelId` | string | Yes | The ID of the channel to read from |
|
||||
|
||||
@@ -159,9 +163,14 @@ Read content from a Microsoft Teams channel
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Message content |
|
||||
| `metadata` | json | Message metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `success` | boolean | Teams channel read operation success status |
|
||||
| `messageCount` | number | Number of messages retrieved from channel |
|
||||
| `teamId` | string | ID of the team that was read from |
|
||||
| `channelId` | string | ID of the channel that was read from |
|
||||
| `messages` | array | Array of channel message objects |
|
||||
| `attachmentCount` | number | Total number of attachments found |
|
||||
| `attachmentTypes` | array | Types of attachments found |
|
||||
| `content` | string | Formatted content of channel messages |
|
||||
|
||||
### `microsoft_teams_write_channel`
|
||||
|
||||
@@ -171,7 +180,6 @@ Write or send a message to a Microsoft Teams channel
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Microsoft Teams API |
|
||||
| `teamId` | string | Yes | The ID of the team to write to |
|
||||
| `channelId` | string | Yes | The ID of the channel to write to |
|
||||
| `content` | string | Yes | The content to write to the channel |
|
||||
@@ -180,9 +188,13 @@ Write or send a message to a Microsoft Teams channel
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Message content |
|
||||
| `metadata` | json | Message metadata |
|
||||
| `updatedContent` | boolean | Content update status |
|
||||
| `success` | boolean | Teams channel message send success status |
|
||||
| `messageId` | string | Unique identifier for the sent message |
|
||||
| `teamId` | string | ID of the team where message was sent |
|
||||
| `channelId` | string | ID of the channel where message was sent |
|
||||
| `createdTime` | string | Timestamp when message was created |
|
||||
| `url` | string | Web URL to the message |
|
||||
| `updatedContent` | boolean | Whether content was successfully updated |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,8 +106,9 @@ Parse PDF documents using Mistral OCR API
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Extracted content |
|
||||
| `metadata` | json | Processing metadata |
|
||||
| `success` | boolean | Whether the PDF was parsed successfully |
|
||||
| `content` | string | Extracted content in the requested format \(markdown, text, or JSON\) |
|
||||
| `metadata` | object | Processing metadata including jobId, fileType, pageCount, and usage info |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,15 +59,14 @@ Read content from a Notion page
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `pageId` | string | Yes | The ID of the Notion page to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Page content in markdown format with headers, paragraphs, lists, and todos |
|
||||
| `metadata` | object | Page metadata including title, URL, and timestamps |
|
||||
|
||||
### `notion_read_database`
|
||||
|
||||
@@ -77,15 +76,14 @@ Read database information and structure from Notion
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `databaseId` | string | Yes | The ID of the Notion database to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Database information including title, properties schema, and metadata |
|
||||
| `metadata` | object | Database metadata including title, ID, URL, timestamps, and properties schema |
|
||||
|
||||
### `notion_write`
|
||||
|
||||
@@ -95,7 +93,6 @@ Append content to a Notion page
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `pageId` | string | Yes | The ID of the Notion page to append content to |
|
||||
| `content` | string | Yes | The content to append to the page |
|
||||
|
||||
@@ -103,8 +100,7 @@ Append content to a Notion page
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Success message confirming content was appended to page |
|
||||
|
||||
### `notion_create_page`
|
||||
|
||||
@@ -114,7 +110,6 @@ Create a new page in Notion
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `parentId` | string | Yes | ID of the parent page |
|
||||
| `title` | string | No | Title of the new page |
|
||||
| `content` | string | No | Optional content to add to the page upon creation |
|
||||
@@ -123,8 +118,8 @@ Create a new page in Notion
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Success message confirming page creation |
|
||||
| `metadata` | object | Page metadata including title, page ID, URL, and timestamps |
|
||||
|
||||
### `notion_query_database`
|
||||
|
||||
@@ -134,7 +129,6 @@ Query and filter Notion database entries with advanced filtering
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `databaseId` | string | Yes | The ID of the database to query |
|
||||
| `filter` | string | No | Filter conditions as JSON \(optional\) |
|
||||
| `sorts` | string | No | Sort criteria as JSON array \(optional\) |
|
||||
@@ -144,8 +138,8 @@ Query and filter Notion database entries with advanced filtering
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Formatted list of database entries with their properties |
|
||||
| `metadata` | object | Query metadata including total results count, pagination info, and raw results array |
|
||||
|
||||
### `notion_search`
|
||||
|
||||
@@ -155,7 +149,6 @@ Search across all pages and databases in Notion workspace
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `query` | string | No | Search terms \(leave empty to get all pages\) |
|
||||
| `filterType` | string | No | Filter by object type: page, database, or leave empty for all |
|
||||
| `pageSize` | number | No | Number of results to return \(default: 100, max: 100\) |
|
||||
@@ -164,8 +157,8 @@ Search across all pages and databases in Notion workspace
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Formatted list of search results including pages and databases |
|
||||
| `metadata` | object | Search metadata including total results count, pagination info, and raw results array |
|
||||
|
||||
### `notion_create_database`
|
||||
|
||||
@@ -175,17 +168,16 @@ Create a new database in Notion with custom properties
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Notion OAuth access token |
|
||||
| `parentId` | string | Yes | ID of the parent page where the database will be created |
|
||||
| `title` | string | Yes | Title for the new database |
|
||||
| `properties` | string | No | Database properties as JSON object \(optional, will create a default |
|
||||
| `properties` | string | No | Database properties as JSON object \(optional, will create a default "Name" property if empty\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Page content |
|
||||
| `metadata` | any | Page metadata |
|
||||
| `content` | string | Success message with database details and properties list |
|
||||
| `metadata` | object | Database metadata including ID, title, URL, creation time, and properties schema |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ Upload a file to OneDrive
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the OneDrive API |
|
||||
| `fileName` | string | Yes | The name of the file to upload |
|
||||
| `content` | string | Yes | The content of the file to upload |
|
||||
| `folderSelector` | string | No | Select the folder to upload the file to |
|
||||
@@ -75,8 +74,8 @@ Upload a file to OneDrive
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The OneDrive file object, including details such as id, name, size, and more. |
|
||||
| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. |
|
||||
| `success` | boolean | Whether the file was uploaded successfully |
|
||||
| `file` | object | The uploaded file object with metadata including id, name, webViewLink, webContentLink, and timestamps |
|
||||
|
||||
### `onedrive_create_folder`
|
||||
|
||||
@@ -86,7 +85,6 @@ Create a new folder in OneDrive
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the OneDrive API |
|
||||
| `folderName` | string | Yes | Name of the folder to create |
|
||||
| `folderSelector` | string | No | Select the parent folder to create the folder in |
|
||||
| `folderId` | string | No | ID of the parent folder \(internal use\) |
|
||||
@@ -95,8 +93,8 @@ Create a new folder in OneDrive
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The OneDrive file object, including details such as id, name, size, and more. |
|
||||
| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. |
|
||||
| `success` | boolean | Whether the folder was created successfully |
|
||||
| `file` | object | The created folder object with metadata including id, name, webViewLink, and timestamps |
|
||||
|
||||
### `onedrive_list`
|
||||
|
||||
@@ -106,7 +104,6 @@ List files and folders in OneDrive
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the OneDrive API |
|
||||
| `folderSelector` | string | No | Select the folder to list files from |
|
||||
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
|
||||
| `query` | string | No | A query to filter the files |
|
||||
@@ -116,8 +113,9 @@ List files and folders in OneDrive
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The OneDrive file object, including details such as id, name, size, and more. |
|
||||
| `files` | json | An array of OneDrive file objects, each containing details such as id, name, size, and more. |
|
||||
| `success` | boolean | Whether files were listed successfully |
|
||||
| `files` | array | Array of file and folder objects with metadata |
|
||||
| `nextPageToken` | string | Token for retrieving the next page of results \(optional\) |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -66,9 +66,8 @@ Generate embeddings from text using OpenAI
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `embeddings` | json | Generated embeddings |
|
||||
| `model` | string | Model used |
|
||||
| `usage` | json | Token usage |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Embeddings generation results |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ Send emails using Outlook
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Outlook API |
|
||||
| `to` | string | Yes | Recipient email address |
|
||||
| `subject` | string | Yes | Email subject |
|
||||
| `body` | string | Yes | Email body content |
|
||||
@@ -167,8 +166,10 @@ Send emails using Outlook
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Response message |
|
||||
| `results` | json | Email results |
|
||||
| `success` | boolean | Email send success status |
|
||||
| `status` | string | Delivery status of the email |
|
||||
| `timestamp` | string | Timestamp when email was sent |
|
||||
| `message` | string | Success or error message |
|
||||
|
||||
### `outlook_draft`
|
||||
|
||||
@@ -178,17 +179,22 @@ Draft emails using Outlook
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Outlook API |
|
||||
| `to` | string | Yes | Recipient email address |
|
||||
| `subject` | string | Yes | Email subject |
|
||||
| `body` | string | Yes | Email body content |
|
||||
| `cc` | string | No | CC recipients \(comma-separated\) |
|
||||
| `bcc` | string | No | BCC recipients \(comma-separated\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Response message |
|
||||
| `results` | json | Email results |
|
||||
| `success` | boolean | Email draft creation success status |
|
||||
| `messageId` | string | Unique identifier for the drafted email |
|
||||
| `status` | string | Draft status of the email |
|
||||
| `subject` | string | Subject of the drafted email |
|
||||
| `timestamp` | string | Timestamp when draft was created |
|
||||
| `message` | string | Success or error message |
|
||||
|
||||
### `outlook_read`
|
||||
|
||||
@@ -198,7 +204,6 @@ Read emails from Outlook
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | OAuth access token for Outlook |
|
||||
| `folder` | string | No | Folder ID to read emails from \(default: Inbox\) |
|
||||
| `maxResults` | number | No | Maximum number of emails to retrieve \(default: 1, max: 10\) |
|
||||
|
||||
@@ -206,8 +211,10 @@ Read emails from Outlook
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Response message |
|
||||
| `results` | json | Email results |
|
||||
| `success` | boolean | Email read operation success status |
|
||||
| `messageCount` | number | Number of emails retrieved |
|
||||
| `messages` | array | Array of email message objects |
|
||||
| `message` | string | Success or status message |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,9 +62,8 @@ Generate completions using Perplexity AI chat models
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Generated response |
|
||||
| `model` | string | Model used |
|
||||
| `usage` | json | Token usage |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Chat completion results |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -67,12 +67,10 @@ Generate embeddings from text using Pinecone
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `model` | any | Model information |
|
||||
| `vector_type` | any | Vector type |
|
||||
| `usage` | any | Usage statistics |
|
||||
| `data` | array | Generated embeddings data with values and vector type |
|
||||
| `model` | string | Model used for generating embeddings |
|
||||
| `vector_type` | string | Type of vector generated \(dense/sparse\) |
|
||||
| `usage` | object | Usage statistics for embeddings generation |
|
||||
|
||||
### `pinecone_upsert_text`
|
||||
|
||||
@@ -91,12 +89,8 @@ Insert or update text records in a Pinecone index
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `model` | any | Model information |
|
||||
| `vector_type` | any | Vector type |
|
||||
| `usage` | any | Usage statistics |
|
||||
| `statusText` | string | Status of the upsert operation |
|
||||
| `upsertedCount` | number | Number of records successfully upserted |
|
||||
|
||||
### `pinecone_search_text`
|
||||
|
||||
@@ -119,12 +113,7 @@ Search for similar text in a Pinecone index
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `model` | any | Model information |
|
||||
| `vector_type` | any | Vector type |
|
||||
| `usage` | any | Usage statistics |
|
||||
| `matches` | array | Search results with ID, score, and metadata |
|
||||
|
||||
### `pinecone_search_vector`
|
||||
|
||||
@@ -147,12 +136,8 @@ Search for similar vectors in a Pinecone index
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `model` | any | Model information |
|
||||
| `vector_type` | any | Vector type |
|
||||
| `usage` | any | Usage statistics |
|
||||
| `matches` | array | Vector search results with ID, score, values, and metadata |
|
||||
| `namespace` | string | Namespace where the search was performed |
|
||||
|
||||
### `pinecone_fetch`
|
||||
|
||||
@@ -171,12 +156,7 @@ Fetch vectors by ID from a Pinecone index
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `model` | any | Model information |
|
||||
| `vector_type` | any | Vector type |
|
||||
| `usage` | any | Usage statistics |
|
||||
| `matches` | array | Fetched vectors with ID, values, metadata, and score |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -126,10 +126,8 @@ Insert or update points in a Qdrant collection
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `status` | any | Operation status |
|
||||
| `status` | string | Status of the upsert operation |
|
||||
| `data` | object | Result data from the upsert operation |
|
||||
|
||||
### `qdrant_search_vector`
|
||||
|
||||
@@ -152,10 +150,8 @@ Search for similar vectors in a Qdrant collection
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `status` | any | Operation status |
|
||||
| `data` | array | Vector search results with ID, score, payload, and optional vector data |
|
||||
| `status` | string | Status of the search operation |
|
||||
|
||||
### `qdrant_fetch_points`
|
||||
|
||||
@@ -176,10 +172,8 @@ Fetch points by ID from a Qdrant collection
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `matches` | any | Search matches |
|
||||
| `upsertedCount` | any | Upserted count |
|
||||
| `data` | any | Response data |
|
||||
| `status` | any | Operation status |
|
||||
| `data` | array | Fetched points with ID, payload, and optional vector data |
|
||||
| `status` | string | Status of the fetch operation |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -53,20 +53,17 @@ Fetch posts from a subreddit with different sorting options
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Reddit API |
|
||||
| `subreddit` | string | Yes | The name of the subreddit to fetch posts from \(without the r/ prefix\) |
|
||||
| `sort` | string | No | Sort method for posts: |
|
||||
| `sort` | string | No | Sort method for posts: "hot", "new", "top", or "rising" \(default: "hot"\) |
|
||||
| `limit` | number | No | Maximum number of posts to return \(default: 10, max: 100\) |
|
||||
| `time` | string | No | Time filter for |
|
||||
| `time` | string | No | Time filter for "top" sorted posts: "day", "week", "month", "year", or "all" \(default: "day"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subreddit` | string | Subreddit name |
|
||||
| `posts` | json | Posts data |
|
||||
| `post` | json | Single post data |
|
||||
| `comments` | json | Comments data |
|
||||
| `subreddit` | string | Name of the subreddit where posts were fetched from |
|
||||
| `posts` | array | Array of posts with title, author, URL, score, comments count, and metadata |
|
||||
|
||||
### `reddit_get_comments`
|
||||
|
||||
@@ -76,20 +73,16 @@ Fetch comments from a specific Reddit post
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | Access token for Reddit API |
|
||||
| `postId` | string | Yes | The ID of the Reddit post to fetch comments from |
|
||||
| `subreddit` | string | Yes | The subreddit where the post is located \(without the r/ prefix\) |
|
||||
| `sort` | string | No | Sort method for comments: |
|
||||
| `sort` | string | No | Sort method for comments: "confidence", "top", "new", "controversial", "old", "random", "qa" \(default: "confidence"\) |
|
||||
| `limit` | number | No | Maximum number of comments to return \(default: 50, max: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `subreddit` | string | Subreddit name |
|
||||
| `posts` | json | Posts data |
|
||||
| `post` | json | Single post data |
|
||||
| `comments` | json | Comments data |
|
||||
| `post` | object | Post information including ID, title, author, content, and metadata |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ Retrieve an object from an AWS S3 bucket
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `url` | string | Presigned URL |
|
||||
| `metadata` | json | Object metadata |
|
||||
| `url` | string | Pre-signed URL for downloading the S3 object |
|
||||
| `metadata` | object | File metadata including type, size, name, and last modified date |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ A powerful web search tool that provides access to Google search results through
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `searchResults` | json | Search results data |
|
||||
| `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images\) |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ Create a new page in a SharePoint site
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the SharePoint API |
|
||||
| `siteId` | string | No | The ID of the SharePoint site \(internal use\) |
|
||||
| `siteSelector` | string | No | Select the SharePoint site |
|
||||
| `pageName` | string | Yes | The name of the page to create |
|
||||
@@ -86,7 +85,7 @@ Create a new page in a SharePoint site
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. |
|
||||
| `page` | object | Created SharePoint page information |
|
||||
|
||||
### `sharepoint_read_page`
|
||||
|
||||
@@ -96,7 +95,6 @@ Read a specific page from a SharePoint site
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the SharePoint API |
|
||||
| `siteSelector` | string | No | Select the SharePoint site |
|
||||
| `siteId` | string | No | The ID of the SharePoint site \(internal use\) |
|
||||
| `pageId` | string | No | The ID of the page to read |
|
||||
@@ -107,7 +105,7 @@ Read a specific page from a SharePoint site
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. |
|
||||
| `page` | object | Information about the SharePoint page |
|
||||
|
||||
### `sharepoint_list_sites`
|
||||
|
||||
@@ -117,7 +115,6 @@ List details of all SharePoint sites
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the SharePoint API |
|
||||
| `siteSelector` | string | No | Select the SharePoint site |
|
||||
| `groupId` | string | No | The group ID for accessing a group team site |
|
||||
|
||||
@@ -125,7 +122,7 @@ List details of all SharePoint sites
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sites` | json | An array of SharePoint site objects, each containing details such as id, name, and more. |
|
||||
| `site` | object | Information about the current SharePoint site |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Slack
|
||||
description: Send messages to Slack
|
||||
description: Send messages to Slack or trigger workflows from Slack events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -64,7 +64,7 @@ This allows for powerful automation scenarios such as sending notifications, ale
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax.
|
||||
Comprehensive Slack integration with OAuth authentication. Send formatted messages using Slack's mrkdwn syntax or trigger workflows from Slack events like mentions and messages.
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
|
||||
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |
|
||||
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
||||
|
||||
@@ -89,10 +88,7 @@ Send messages to Slack channels or users through the Slack API. Supports Slack m
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Message timestamp |
|
||||
| `channel` | string | Channel identifier |
|
||||
| `canvas_id` | string | Canvas identifier |
|
||||
| `title` | string | Canvas title |
|
||||
| `messages` | json | Message data |
|
||||
| `channel` | string | Channel ID where message was sent |
|
||||
|
||||
### `slack_canvas`
|
||||
|
||||
@@ -104,7 +100,6 @@ Create and share Slack canvases in channels. Canvases are collaborative document
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
|
||||
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |
|
||||
| `title` | string | Yes | Title of the canvas |
|
||||
| `content` | string | Yes | Canvas content in markdown format |
|
||||
@@ -114,11 +109,9 @@ Create and share Slack canvases in channels. Canvases are collaborative document
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Message timestamp |
|
||||
| `channel` | string | Channel identifier |
|
||||
| `canvas_id` | string | Canvas identifier |
|
||||
| `title` | string | Canvas title |
|
||||
| `messages` | json | Message data |
|
||||
| `canvas_id` | string | ID of the created canvas |
|
||||
| `channel` | string | Channel where canvas was created |
|
||||
| `title` | string | Title of the canvas |
|
||||
|
||||
### `slack_message_reader`
|
||||
|
||||
@@ -130,7 +123,6 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `accessToken` | string | No | OAuth access token or bot token for Slack API |
|
||||
| `channel` | string | Yes | Slack channel to read messages from \(e.g., #general\) |
|
||||
| `limit` | number | No | Number of messages to retrieve \(default: 10, max: 100\) |
|
||||
| `oldest` | string | No | Start of time range \(timestamp\) |
|
||||
@@ -140,11 +132,7 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Message timestamp |
|
||||
| `channel` | string | Channel identifier |
|
||||
| `canvas_id` | string | Canvas identifier |
|
||||
| `title` | string | Canvas title |
|
||||
| `messages` | json | Message data |
|
||||
| `messages` | array | Array of message objects from the channel |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ Extract structured data from a webpage using Stagehand
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Extracted data |
|
||||
| `data` | object | Extracted structured data matching the provided schema |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -212,6 +212,7 @@ Run an autonomous web agent to complete tasks and extract structured data
|
||||
| `startUrl` | string | Yes | URL of the webpage to start the agent on |
|
||||
| `task` | string | Yes | The task to complete or goal to achieve on the website |
|
||||
| `variables` | json | No | Optional variables to substitute in the task \(format: \{key: value\}\). Reference in task using %key% |
|
||||
| `format` | string | No | No description |
|
||||
| `apiKey` | string | Yes | OpenAI API key for agent execution \(required by Stagehand\) |
|
||||
| `outputSchema` | json | No | Optional JSON schema defining the structure of data the agent should return |
|
||||
|
||||
@@ -219,8 +220,7 @@ Run an autonomous web agent to complete tasks and extract structured data
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `agentResult` | json | Agent execution result |
|
||||
| `structuredOutput` | any | Structured output data |
|
||||
| `agentResult` | object | Result from the Stagehand agent execution |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Query data from a Supabase table
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `filter` | string | No | PostgREST filter \(e.g., |
|
||||
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
|
||||
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
|
||||
| `limit` | number | No | Maximum number of rows to return |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
@@ -101,8 +101,8 @@ Query data from a Supabase table
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation message |
|
||||
| `results` | json | Query results |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Query operation results |
|
||||
|
||||
### `supabase_insert`
|
||||
|
||||
@@ -121,8 +121,8 @@ Insert data into a Supabase table
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation message |
|
||||
| `results` | json | Query results |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Insert operation results |
|
||||
|
||||
### `supabase_get_row`
|
||||
|
||||
@@ -134,15 +134,15 @@ Get a single row from a Supabase table based on filter criteria
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., |
|
||||
| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., "id=eq.123"\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation message |
|
||||
| `results` | json | Query results |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Get row operation results |
|
||||
|
||||
### `supabase_update`
|
||||
|
||||
@@ -154,7 +154,7 @@ Update rows in a Supabase table based on filter criteria
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to update |
|
||||
| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., |
|
||||
| `filter` | string | Yes | PostgREST filter to identify rows to update \(e.g., "id=eq.123"\) |
|
||||
| `data` | object | Yes | Data to update in the matching rows |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
@@ -162,8 +162,8 @@ Update rows in a Supabase table based on filter criteria
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation message |
|
||||
| `results` | json | Query results |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Update operation results |
|
||||
|
||||
### `supabase_delete`
|
||||
|
||||
@@ -175,15 +175,15 @@ Delete rows from a Supabase table based on filter criteria
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to delete from |
|
||||
| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., |
|
||||
| `filter` | string | Yes | PostgREST filter to identify rows to delete \(e.g., "id=eq.123"\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation message |
|
||||
| `results` | json | Query results |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Delete operation results |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,12 +80,8 @@ Perform AI-powered web searches using Tavily
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results data |
|
||||
| `answer` | any | Search answer |
|
||||
| `query` | string | Query used |
|
||||
| `content` | string | Extracted content |
|
||||
| `title` | string | Page title |
|
||||
| `url` | string | Source URL |
|
||||
| `query` | string | The search query that was executed |
|
||||
| `results` | array | results output from the tool |
|
||||
|
||||
### `tavily_extract`
|
||||
|
||||
@@ -103,12 +99,7 @@ Extract raw content from multiple web pages simultaneously using Tavily
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | json | Search results data |
|
||||
| `answer` | any | Search answer |
|
||||
| `query` | string | Query used |
|
||||
| `content` | string | Extracted content |
|
||||
| `title` | string | Page title |
|
||||
| `url` | string | Source URL |
|
||||
| `results` | array | The URL that was extracted |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Telegram
|
||||
description: Send a message through Telegram
|
||||
description: Send messages through Telegram or trigger workflows from Telegram events
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -67,7 +67,7 @@ In Sim, the Telegram integration enables your agents to leverage these powerful
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Send messages to any Telegram channel using your Bot API key. Integrate automated notifications and alerts into your workflow to keep your team informed.
|
||||
Send messages to any Telegram channel using your Bot API key or trigger workflows from Telegram bot messages. Integrate automated notifications and alerts into your workflow to keep your team informed.
|
||||
|
||||
|
||||
|
||||
@@ -89,8 +89,12 @@ Send messages to Telegram channels or users through the Telegram Bot API. Enable
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ok` | boolean | Success status |
|
||||
| `result` | json | Message result |
|
||||
| `success` | boolean | Telegram message send success status |
|
||||
| `messageId` | number | Unique Telegram message identifier |
|
||||
| `chatId` | string | Target chat ID where message was sent |
|
||||
| `text` | string | Text content of the sent message |
|
||||
| `timestamp` | number | Unix timestamp when message was sent |
|
||||
| `from` | object | Information about the bot that sent the message |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Processes a provided thought/instruction, making it available for subsequent ste
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `acknowledgedThought` | string | Acknowledged thought process |
|
||||
| `acknowledgedThought` | string | The thought that was processed and acknowledged |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ Convert text between languages while preserving meaning, nuance, and formatting.
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Translated text |
|
||||
| `model` | string | Model used |
|
||||
| `tokens` | any | Token usage |
|
||||
| `tokens` | json | Token usage |
|
||||
|
||||
### `anthropic_chat`
|
||||
|
||||
@@ -85,7 +85,7 @@ Convert text between languages while preserving meaning, nuance, and formatting.
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Translated text |
|
||||
| `model` | string | Model used |
|
||||
| `tokens` | any | Token usage |
|
||||
| `tokens` | json | Token usage |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,10 +58,11 @@ Send text messages to single or multiple recipients using the Twilio API.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Send success status |
|
||||
| `messageId` | any | Message identifier |
|
||||
| `status` | any | Delivery status |
|
||||
| `error` | any | Error information |
|
||||
| `success` | boolean | SMS send success status |
|
||||
| `messageId` | string | Unique Twilio message identifier \(SID\) |
|
||||
| `status` | string | Message delivery status from Twilio |
|
||||
| `fromNumber` | string | Phone number message was sent from |
|
||||
| `toNumber` | string | Phone number message was sent to |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -95,9 +95,9 @@ Download files uploaded in Typeform responses
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_items` | number | Total response count |
|
||||
| `page_count` | number | Total page count |
|
||||
| `items` | json | Response items |
|
||||
| `fileUrl` | string | Direct download URL for the uploaded file |
|
||||
| `contentType` | string | MIME type of the uploaded file |
|
||||
| `filename` | string | Original filename of the uploaded file |
|
||||
|
||||
### `typeform_insights`
|
||||
|
||||
@@ -114,9 +114,7 @@ Retrieve insights and analytics for Typeform forms
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_items` | number | Total response count |
|
||||
| `page_count` | number | Total page count |
|
||||
| `items` | json | Response items |
|
||||
| `fields` | array | Number of users who dropped off at this field |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,9 +70,10 @@ Process and analyze images using advanced vision models. Capable of understandin
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `content` | string | Analysis result |
|
||||
| `model` | any | Model used |
|
||||
| `tokens` | any | Token usage |
|
||||
| `content` | string | The analyzed content and description of the image |
|
||||
| `model` | string | The vision model that was used for analysis |
|
||||
| `tokens` | number | Total tokens used for the analysis |
|
||||
| `usage` | object | Detailed token usage breakdown |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -56,21 +56,14 @@ Read content from a Wealthbox note
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `noteId` | string | No | The ID of the note to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `note` | any | Note data |
|
||||
| `notes` | any | Notes list |
|
||||
| `contact` | any | Contact data |
|
||||
| `contacts` | any | Contacts list |
|
||||
| `task` | any | Task data |
|
||||
| `tasks` | any | Tasks list |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `success` | any | Success status |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Note data and metadata |
|
||||
|
||||
### `wealthbox_write_note`
|
||||
|
||||
@@ -80,7 +73,6 @@ Create or update a Wealthbox note
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `content` | string | Yes | The main body of the note |
|
||||
| `contactId` | string | No | ID of contact to link to this note |
|
||||
|
||||
@@ -88,14 +80,8 @@ Create or update a Wealthbox note
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `note` | any | Note data |
|
||||
| `notes` | any | Notes list |
|
||||
| `contact` | any | Contact data |
|
||||
| `contacts` | any | Contacts list |
|
||||
| `task` | any | Task data |
|
||||
| `tasks` | any | Tasks list |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `success` | any | Success status |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created or updated note data and metadata |
|
||||
|
||||
### `wealthbox_read_contact`
|
||||
|
||||
@@ -105,21 +91,14 @@ Read content from a Wealthbox contact
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `contactId` | string | No | The ID of the contact to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `note` | any | Note data |
|
||||
| `notes` | any | Notes list |
|
||||
| `contact` | any | Contact data |
|
||||
| `contacts` | any | Contacts list |
|
||||
| `task` | any | Task data |
|
||||
| `tasks` | any | Tasks list |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `success` | any | Success status |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Contact data and metadata |
|
||||
|
||||
### `wealthbox_write_contact`
|
||||
|
||||
@@ -129,7 +108,6 @@ Create a new Wealthbox contact
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `firstName` | string | Yes | The first name of the contact |
|
||||
| `lastName` | string | Yes | The last name of the contact |
|
||||
| `emailAddress` | string | No | The email address of the contact |
|
||||
@@ -139,14 +117,8 @@ Create a new Wealthbox contact
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `note` | any | Note data |
|
||||
| `notes` | any | Notes list |
|
||||
| `contact` | any | Contact data |
|
||||
| `contacts` | any | Contacts list |
|
||||
| `task` | any | Task data |
|
||||
| `tasks` | any | Tasks list |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `success` | any | Success status |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created or updated contact data and metadata |
|
||||
|
||||
### `wealthbox_read_task`
|
||||
|
||||
@@ -156,21 +128,14 @@ Read content from a Wealthbox task
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `taskId` | string | No | The ID of the task to read |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `note` | any | Note data |
|
||||
| `notes` | any | Notes list |
|
||||
| `contact` | any | Contact data |
|
||||
| `contacts` | any | Contacts list |
|
||||
| `task` | any | Task data |
|
||||
| `tasks` | any | Tasks list |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `success` | any | Success status |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Task data and metadata |
|
||||
|
||||
### `wealthbox_write_task`
|
||||
|
||||
@@ -180,9 +145,8 @@ Create or update a Wealthbox task
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | The access token for the Wealthbox API |
|
||||
| `title` | string | Yes | The name/title of the task |
|
||||
| `dueDate` | string | Yes | The due date and time of the task \(format: |
|
||||
| `dueDate` | string | Yes | The due date and time of the task \(format: "YYYY-MM-DD HH:MM AM/PM -HHMM", e.g., "2015-05-24 11:00 AM -0400"\) |
|
||||
| `contactId` | string | No | ID of contact to link to this task |
|
||||
| `description` | string | No | Description or notes about the task |
|
||||
|
||||
@@ -190,14 +154,8 @@ Create or update a Wealthbox task
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `note` | any | Note data |
|
||||
| `notes` | any | Notes list |
|
||||
| `contact` | any | Contact data |
|
||||
| `contacts` | any | Contacts list |
|
||||
| `task` | any | Task data |
|
||||
| `tasks` | any | Tasks list |
|
||||
| `metadata` | json | Operation metadata |
|
||||
| `success` | any | Success status |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created or updated task data and metadata |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -54,15 +54,16 @@ Send WhatsApp messages
|
||||
| `phoneNumber` | string | Yes | Recipient phone number with country code |
|
||||
| `message` | string | Yes | Message content to send |
|
||||
| `phoneNumberId` | string | Yes | WhatsApp Business Phone Number ID |
|
||||
| `accessToken` | string | Yes | WhatsApp Business API Access Token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Send success status |
|
||||
| `messageId` | any | Message identifier |
|
||||
| `error` | any | Error information |
|
||||
| `success` | boolean | WhatsApp message send success status |
|
||||
| `messageId` | string | Unique WhatsApp message identifier |
|
||||
| `phoneNumber` | string | Recipient phone number |
|
||||
| `status` | string | Message delivery status |
|
||||
| `timestamp` | string | Message send timestamp |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -74,11 +74,7 @@ Get a summary and metadata for a specific Wikipedia page.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `summary` | json | Page summary data |
|
||||
| `searchResults` | json | Search results data |
|
||||
| `totalHits` | number | Total search hits |
|
||||
| `content` | json | Page content data |
|
||||
| `randomPage` | json | Random page data |
|
||||
| `summary` | object | Wikipedia page summary and metadata |
|
||||
|
||||
### `wikipedia_search`
|
||||
|
||||
@@ -95,11 +91,7 @@ Search for Wikipedia pages by title or content.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `summary` | json | Page summary data |
|
||||
| `searchResults` | json | Search results data |
|
||||
| `totalHits` | number | Total search hits |
|
||||
| `content` | json | Page content data |
|
||||
| `randomPage` | json | Random page data |
|
||||
| `searchResults` | array | Array of matching Wikipedia pages |
|
||||
|
||||
### `wikipedia_content`
|
||||
|
||||
@@ -115,11 +107,7 @@ Get the full HTML content of a Wikipedia page.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `summary` | json | Page summary data |
|
||||
| `searchResults` | json | Search results data |
|
||||
| `totalHits` | number | Total search hits |
|
||||
| `content` | json | Page content data |
|
||||
| `randomPage` | json | Random page data |
|
||||
| `content` | object | Full HTML content and metadata of the Wikipedia page |
|
||||
|
||||
### `wikipedia_random`
|
||||
|
||||
@@ -134,11 +122,7 @@ Get a random Wikipedia page.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `summary` | json | Page summary data |
|
||||
| `searchResults` | json | Search results data |
|
||||
| `totalHits` | number | Total search hits |
|
||||
| `content` | json | Page content data |
|
||||
| `randomPage` | json | Random page data |
|
||||
| `randomPage` | object | Random Wikipedia page data |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ Post new tweets, reply to tweets, or create polls on X (Twitter)
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | X OAuth access token |
|
||||
| `text` | string | Yes | The text content of your tweet |
|
||||
| `replyTo` | string | No | ID of the tweet to reply to |
|
||||
| `mediaIds` | array | No | Array of media IDs to attach to the tweet |
|
||||
@@ -60,14 +59,7 @@ Post new tweets, reply to tweets, or create polls on X (Twitter)
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweet` | json | Tweet data |
|
||||
| `replies` | any | Tweet replies |
|
||||
| `context` | any | Tweet context |
|
||||
| `tweets` | json | Tweets data |
|
||||
| `includes` | any | Additional data |
|
||||
| `meta` | json | Response metadata |
|
||||
| `user` | json | User profile data |
|
||||
| `recentTweets` | any | Recent tweets data |
|
||||
| `tweet` | object | The newly created tweet data |
|
||||
|
||||
### `x_read`
|
||||
|
||||
@@ -77,7 +69,6 @@ Read tweet details, including replies and conversation context
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | X OAuth access token |
|
||||
| `tweetId` | string | Yes | ID of the tweet to read |
|
||||
| `includeReplies` | boolean | No | Whether to include replies to the tweet |
|
||||
|
||||
@@ -85,14 +76,7 @@ Read tweet details, including replies and conversation context
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweet` | json | Tweet data |
|
||||
| `replies` | any | Tweet replies |
|
||||
| `context` | any | Tweet context |
|
||||
| `tweets` | json | Tweets data |
|
||||
| `includes` | any | Additional data |
|
||||
| `meta` | json | Response metadata |
|
||||
| `user` | json | User profile data |
|
||||
| `recentTweets` | any | Recent tweets data |
|
||||
| `tweet` | object | The main tweet data |
|
||||
|
||||
### `x_search`
|
||||
|
||||
@@ -102,7 +86,6 @@ Search for tweets using keywords, hashtags, or advanced queries
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | X OAuth access token |
|
||||
| `query` | string | Yes | Search query \(supports X search operators\) |
|
||||
| `maxResults` | number | No | Maximum number of results to return \(default: 10, max: 100\) |
|
||||
| `startTime` | string | No | Start time for search \(ISO 8601 format\) |
|
||||
@@ -113,14 +96,7 @@ Search for tweets using keywords, hashtags, or advanced queries
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweet` | json | Tweet data |
|
||||
| `replies` | any | Tweet replies |
|
||||
| `context` | any | Tweet context |
|
||||
| `tweets` | json | Tweets data |
|
||||
| `includes` | any | Additional data |
|
||||
| `meta` | json | Response metadata |
|
||||
| `user` | json | User profile data |
|
||||
| `recentTweets` | any | Recent tweets data |
|
||||
| `tweets` | array | Array of tweets matching the search query |
|
||||
|
||||
### `x_user`
|
||||
|
||||
@@ -130,21 +106,13 @@ Get user profile information
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessToken` | string | Yes | X OAuth access token |
|
||||
| `username` | string | Yes | Username to look up \(without @ symbol\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tweet` | json | Tweet data |
|
||||
| `replies` | any | Tweet replies |
|
||||
| `context` | any | Tweet context |
|
||||
| `tweets` | json | Tweets data |
|
||||
| `includes` | any | Additional data |
|
||||
| `meta` | json | Response metadata |
|
||||
| `user` | json | User profile data |
|
||||
| `recentTweets` | any | Recent tweets data |
|
||||
| `user` | object | X user profile information |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,8 +62,7 @@ Search for videos on YouTube using the YouTube Data API.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | json | The items returned by the YouTube search |
|
||||
| `totalResults` | number | The total number of results returned by the YouTube search |
|
||||
| `items` | array | Array of YouTube videos matching the search query |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Choose your input method from the dropdown:
|
||||
<video autoPlay loop muted playsInline className="w-full -mb-2 rounded-lg" src="/chat-input.mp4"></video>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600">Chat with your workflow and access both input text and conversation ID for context-aware responses.</p>
|
||||
<p className="text-sm text-gray-600">Chat with your workflow and access input text, conversation ID, and uploaded files for context-aware responses.</p>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -60,13 +60,15 @@ Choose your input method from the dropdown:
|
||||
In Chat mode, access user input and conversation context through special variables:
|
||||
|
||||
```yaml
|
||||
# Reference the chat input and conversation ID in your workflow
|
||||
# Reference the chat input, conversation ID, and files in your workflow
|
||||
user_message: "<start.input>"
|
||||
conversation_id: "<start.conversationId>"
|
||||
uploaded_files: "<start.files>"
|
||||
```
|
||||
|
||||
- **`<start.input>`** - Contains the user's message text
|
||||
- **`<start.conversationId>`** - Unique identifier for the conversation thread
|
||||
- **`<start.files>`** - Array of files uploaded by the user (if any)
|
||||
|
||||
## API Execution
|
||||
|
||||
|
||||
@@ -94,10 +94,10 @@ describe('LoginPage', () => {
|
||||
const emailInput = screen.getByPlaceholderText(/enter your email/i)
|
||||
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } })
|
||||
|
||||
expect(emailInput).toHaveValue('test@example.com')
|
||||
expect(emailInput).toHaveValue('user@company.com')
|
||||
expect(passwordInput).toHaveValue('password123')
|
||||
})
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('LoginPage', () => {
|
||||
const submitButton = screen.getByRole('button', { name: /sign in/i })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } })
|
||||
fireEvent.click(submitButton)
|
||||
})
|
||||
@@ -140,14 +140,14 @@ describe('LoginPage', () => {
|
||||
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
|
||||
const submitButton = screen.getByRole('button', { name: /sign in/i })
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSignIn).toHaveBeenCalledWith(
|
||||
{
|
||||
email: 'test@example.com',
|
||||
email: 'user@company.com',
|
||||
password: 'password123',
|
||||
callbackURL: '/workspace',
|
||||
},
|
||||
@@ -181,7 +181,7 @@ describe('LoginPage', () => {
|
||||
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
|
||||
const submitButton = screen.getByRole('button', { name: /sign in/i })
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
@@ -242,13 +242,13 @@ describe('LoginPage', () => {
|
||||
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
|
||||
const submitButton = screen.getByRole('button', { name: /sign in/i })
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'password123' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSendOtp).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
email: 'user@company.com',
|
||||
type: 'email-verification',
|
||||
})
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/verify')
|
||||
|
||||
@@ -15,25 +15,27 @@ import {
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { quickValidateEmail } from '@/lib/email/validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
|
||||
const logger = createLogger('LoginForm')
|
||||
|
||||
const EMAIL_VALIDATIONS = {
|
||||
required: {
|
||||
test: (value: string) => Boolean(value && typeof value === 'string'),
|
||||
message: 'Email is required.',
|
||||
},
|
||||
notEmpty: {
|
||||
test: (value: string) => value.trim().length > 0,
|
||||
message: 'Email cannot be empty.',
|
||||
},
|
||||
basicFormat: {
|
||||
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: 'Please enter a valid email address.',
|
||||
},
|
||||
const validateEmailField = (emailValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!emailValue || !emailValue.trim()) {
|
||||
errors.push('Email is required.')
|
||||
return errors
|
||||
}
|
||||
|
||||
const validation = quickValidateEmail(emailValue.trim().toLowerCase())
|
||||
if (!validation.isValid) {
|
||||
errors.push(validation.reason || 'Please enter a valid email address.')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const PASSWORD_VALIDATIONS = {
|
||||
@@ -68,27 +70,6 @@ const validateCallbackUrl = (url: string): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email and return array of error messages
|
||||
const validateEmail = (emailValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!EMAIL_VALIDATIONS.required.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.required.message)
|
||||
return errors // Return early for required field
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.notEmpty.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.notEmpty.message)
|
||||
return errors // Return early for empty field
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.basicFormat.regex.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.basicFormat.message)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// Validate password and return array of error messages
|
||||
const validatePassword = (passwordValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
@@ -182,7 +163,7 @@ export default function LoginPage({
|
||||
setEmail(newEmail)
|
||||
|
||||
// Silently validate but don't show errors until submit
|
||||
const errors = validateEmail(newEmail)
|
||||
const errors = validateEmailField(newEmail)
|
||||
setEmailErrors(errors)
|
||||
setShowEmailValidationError(false)
|
||||
}
|
||||
@@ -205,7 +186,7 @@ export default function LoginPage({
|
||||
const email = formData.get('email') as string
|
||||
|
||||
// Validate email on submit
|
||||
const emailValidationErrors = validateEmail(email)
|
||||
const emailValidationErrors = validateEmailField(email)
|
||||
setEmailErrors(emailValidationErrors)
|
||||
setShowEmailValidationError(emailValidationErrors.length > 0)
|
||||
|
||||
|
||||
@@ -96,11 +96,11 @@ describe('SignupPage', () => {
|
||||
const passwordInput = screen.getByPlaceholderText(/enter your password/i)
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
|
||||
expect(nameInput).toHaveValue('John Doe')
|
||||
expect(emailInput).toHaveValue('test@example.com')
|
||||
expect(emailInput).toHaveValue('user@company.com')
|
||||
expect(passwordInput).toHaveValue('Password123!')
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('SignupPage', () => {
|
||||
const submitButton = screen.getByRole('button', { name: /create account/i })
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
@@ -144,14 +144,14 @@ describe('SignupPage', () => {
|
||||
|
||||
// Use valid input that passes all validation rules
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSignUp).toHaveBeenCalledWith(
|
||||
{
|
||||
email: 'test@example.com',
|
||||
email: 'user@company.com',
|
||||
password: 'Password123!',
|
||||
name: 'John Doe',
|
||||
},
|
||||
@@ -174,7 +174,7 @@ describe('SignupPage', () => {
|
||||
|
||||
// Use name with leading/trailing spaces which should fail validation
|
||||
fireEvent.change(nameInput, { target: { value: ' John Doe ' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
@@ -206,15 +206,13 @@ describe('SignupPage', () => {
|
||||
const submitButton = screen.getByRole('button', { name: /create account/i })
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSendOtp).toHaveBeenCalledWith({
|
||||
email: 'test@example.com',
|
||||
type: 'email-verification',
|
||||
})
|
||||
// With sendVerificationOnSignUp: true, OTP is sent automatically by Better Auth
|
||||
// No manual OTP sending in the component anymore
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/verify?fromSignup=true')
|
||||
})
|
||||
})
|
||||
@@ -267,7 +265,7 @@ describe('SignupPage', () => {
|
||||
const submitButton = screen.getByRole('button', { name: /create account/i })
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: longName } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
@@ -295,7 +293,7 @@ describe('SignupPage', () => {
|
||||
const submitButton = screen.getByRole('button', { name: /create account/i })
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: exactLengthName } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'ValidPass123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
@@ -308,7 +306,7 @@ describe('SignupPage', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockSignUp).toHaveBeenCalledWith(
|
||||
{
|
||||
email: 'test@example.com',
|
||||
email: 'user@company.com',
|
||||
password: 'ValidPass123!',
|
||||
name: exactLengthName,
|
||||
},
|
||||
@@ -343,7 +341,7 @@ describe('SignupPage', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
})
|
||||
@@ -385,12 +383,12 @@ describe('SignupPage', () => {
|
||||
const submitButton = screen.getByRole('button', { name: /create account/i })
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'test@example.com' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'user@company.com' } })
|
||||
fireEvent.change(passwordInput, { target: { value: 'Password123!' } })
|
||||
fireEvent.click(submitButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/invite/123')
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/verify?fromSignup=true')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { client } from '@/lib/auth-client'
|
||||
import { quickValidateEmail } from '@/lib/email/validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
|
||||
|
||||
const logger = createLogger('SignupForm')
|
||||
|
||||
const PASSWORD_VALIDATIONS = {
|
||||
minLength: { regex: /.{8,}/, message: 'Password must be at least 8 characters long.' },
|
||||
uppercase: {
|
||||
@@ -51,31 +55,20 @@ const NAME_VALIDATIONS = {
|
||||
},
|
||||
}
|
||||
|
||||
const EMAIL_VALIDATIONS = {
|
||||
required: {
|
||||
test: (value: string) => Boolean(value && typeof value === 'string'),
|
||||
message: 'Email is required.',
|
||||
},
|
||||
notEmpty: {
|
||||
test: (value: string) => value.trim().length > 0,
|
||||
message: 'Email cannot be empty.',
|
||||
},
|
||||
maxLength: {
|
||||
test: (value: string) => value.length <= 254,
|
||||
message: 'Email must be less than 254 characters.',
|
||||
},
|
||||
basicFormat: {
|
||||
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: 'Please enter a valid email address.',
|
||||
},
|
||||
noSpaces: {
|
||||
regex: /^[^\s]*$/,
|
||||
message: 'Email cannot contain spaces.',
|
||||
},
|
||||
validStart: {
|
||||
regex: /^[a-zA-Z0-9]/,
|
||||
message: 'Email must start with a letter or number.',
|
||||
},
|
||||
const validateEmailField = (emailValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!emailValue || !emailValue.trim()) {
|
||||
errors.push('Email is required.')
|
||||
return errors
|
||||
}
|
||||
|
||||
const validation = quickValidateEmail(emailValue.trim().toLowerCase())
|
||||
if (!validation.isValid) {
|
||||
errors.push(validation.reason || 'Please enter a valid email address.')
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
function SignupFormContent({
|
||||
@@ -188,39 +181,6 @@ function SignupFormContent({
|
||||
return errors
|
||||
}
|
||||
|
||||
// Validate email and return array of error messages
|
||||
const validateEmail = (emailValue: string): string[] => {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!EMAIL_VALIDATIONS.required.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.required.message)
|
||||
return errors // Return early for required field
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.notEmpty.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.notEmpty.message)
|
||||
return errors // Return early for empty field
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.maxLength.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.maxLength.message)
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.noSpaces.regex.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.noSpaces.message)
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.validStart.regex.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.validStart.message)
|
||||
}
|
||||
|
||||
if (!EMAIL_VALIDATIONS.basicFormat.regex.test(emailValue)) {
|
||||
errors.push(EMAIL_VALIDATIONS.basicFormat.message)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPassword = e.target.value
|
||||
setPassword(newPassword)
|
||||
@@ -246,7 +206,7 @@ function SignupFormContent({
|
||||
setEmail(newEmail)
|
||||
|
||||
// Silently validate but don't show errors until submit
|
||||
const errors = validateEmail(newEmail)
|
||||
const errors = validateEmailField(newEmail)
|
||||
setEmailErrors(errors)
|
||||
setShowEmailValidationError(false)
|
||||
|
||||
@@ -271,7 +231,7 @@ function SignupFormContent({
|
||||
setShowNameValidationError(nameValidationErrors.length > 0)
|
||||
|
||||
// Validate email on submit
|
||||
const emailValidationErrors = validateEmail(emailValue)
|
||||
const emailValidationErrors = validateEmailField(emailValue)
|
||||
setEmailErrors(emailValidationErrors)
|
||||
setShowEmailValidationError(emailValidationErrors.length > 0)
|
||||
|
||||
@@ -324,7 +284,7 @@ function SignupFormContent({
|
||||
},
|
||||
{
|
||||
onError: (ctx) => {
|
||||
console.error('Signup error:', ctx.error)
|
||||
logger.error('Signup error:', ctx.error)
|
||||
const errorMessage: string[] = ['Failed to create account']
|
||||
|
||||
if (ctx.error.code?.includes('USER_ALREADY_EXISTS')) {
|
||||
@@ -370,30 +330,37 @@ function SignupFormContent({
|
||||
return
|
||||
}
|
||||
|
||||
// Handle invitation flow redirect
|
||||
if (isInviteFlow && redirectUrl) {
|
||||
router.push(redirectUrl)
|
||||
return
|
||||
// For new signups, always require verification
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('verificationEmail', emailValue)
|
||||
localStorage.setItem('has_logged_in_before', 'true')
|
||||
|
||||
// Set cookie flag for middleware check
|
||||
document.cookie = 'requiresEmailVerification=true; path=/; max-age=900; SameSite=Lax' // 15 min expiry
|
||||
document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax'
|
||||
|
||||
// Store invitation flow state if applicable
|
||||
if (isInviteFlow && redirectUrl) {
|
||||
sessionStorage.setItem('inviteRedirectUrl', redirectUrl)
|
||||
sessionStorage.setItem('isInviteFlow', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
// Send verification OTP manually
|
||||
try {
|
||||
await client.emailOtp.sendVerificationOtp({
|
||||
email: emailValue,
|
||||
type: 'email-verification',
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to send verification OTP:', err)
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('verificationEmail', emailValue)
|
||||
localStorage.setItem('has_logged_in_before', 'true')
|
||||
document.cookie = 'has_logged_in_before=true; path=/; max-age=31536000; SameSite=Lax' // 1 year expiry
|
||||
} catch (otpError) {
|
||||
logger.error('Failed to send OTP:', otpError)
|
||||
// Continue anyway - user can use resend button
|
||||
}
|
||||
|
||||
// Always redirect to verification for new signups
|
||||
router.push('/verify?fromSignup=true')
|
||||
} catch (error) {
|
||||
console.error('Signup error:', error)
|
||||
logger.error('Signup error:', error)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +121,14 @@ export function useVerification({
|
||||
if (response && !response.error) {
|
||||
setIsVerified(true)
|
||||
|
||||
// Clear email from sessionStorage after successful verification
|
||||
// Clear verification requirements and session storage
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem('verificationEmail')
|
||||
|
||||
// Clear the verification requirement flag
|
||||
document.cookie =
|
||||
'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
|
||||
// Also clear invite-related items
|
||||
if (isInviteFlow) {
|
||||
sessionStorage.removeItem('inviteRedirectUrl')
|
||||
@@ -223,6 +227,11 @@ export function useVerification({
|
||||
// Auto-verify and redirect in development/docker environments
|
||||
if (isDevOrDocker || !hasResendKey) {
|
||||
setIsVerified(true)
|
||||
|
||||
// Clear verification requirement cookie (same as manual verification)
|
||||
document.cookie =
|
||||
'requiresEmailVerification=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
router.push('/workspace')
|
||||
}, 1000)
|
||||
|
||||
@@ -99,6 +99,7 @@ export const sampleWorkflowState = {
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: 95,
|
||||
},
|
||||
'agent-id': {
|
||||
@@ -127,6 +128,7 @@ export const sampleWorkflowState = {
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: 680,
|
||||
},
|
||||
},
|
||||
@@ -712,6 +714,7 @@ export function mockFileSystem(
|
||||
}
|
||||
return Promise.reject(new Error('File not found'))
|
||||
}),
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -761,14 +764,15 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
|
||||
getStorageProvider: vi.fn().mockReturnValue(provider),
|
||||
isUsingCloudStorage: vi.fn().mockReturnValue(isCloudEnabled),
|
||||
uploadFile: vi.fn().mockResolvedValue({
|
||||
path: '/api/files/serve/test-key',
|
||||
key: 'test-key',
|
||||
path: '/api/files/serve/test-key.txt',
|
||||
key: 'test-key.txt',
|
||||
name: 'test.txt',
|
||||
size: 100,
|
||||
type: 'text/plain',
|
||||
}),
|
||||
downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')),
|
||||
deleteFile: vi.fn().mockResolvedValue(undefined),
|
||||
getPresignedUrl: vi.fn().mockResolvedValue(presignedUrl),
|
||||
}))
|
||||
|
||||
if (provider === 's3') {
|
||||
@@ -1235,14 +1239,15 @@ export function setupFileApiMocks(
|
||||
getStorageProvider: vi.fn().mockReturnValue('local'),
|
||||
isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled),
|
||||
uploadFile: vi.fn().mockResolvedValue({
|
||||
path: '/api/files/serve/test-key',
|
||||
key: 'test-key',
|
||||
path: '/api/files/serve/test-key.txt',
|
||||
key: 'test-key.txt',
|
||||
name: 'test.txt',
|
||||
size: 100,
|
||||
type: 'text/plain',
|
||||
}),
|
||||
downloadFile: vi.fn().mockResolvedValue(Buffer.from('test content')),
|
||||
deleteFile: vi.fn().mockResolvedValue(undefined),
|
||||
getPresignedUrl: vi.fn().mockResolvedValue('https://example.com/presigned-url'),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1347,8 +1352,8 @@ export function mockUploadUtils(
|
||||
const {
|
||||
isCloudStorage = false,
|
||||
uploadResult = {
|
||||
path: '/api/files/serve/test-key',
|
||||
key: 'test-key',
|
||||
path: '/api/files/serve/test-key.txt',
|
||||
key: 'test-key.txt',
|
||||
name: 'test.txt',
|
||||
size: 100,
|
||||
type: 'text/plain',
|
||||
|
||||
99
apps/sim/app/api/files/download/route.ts
Normal file
99
apps/sim/app/api/files/download/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getPresignedUrl, getPresignedUrlWithConfig, isUsingCloudStorage } from '@/lib/uploads'
|
||||
import { BLOB_EXECUTION_FILES_CONFIG, S3_EXECUTION_FILES_CONFIG } from '@/lib/uploads/setup'
|
||||
import { createErrorResponse } from '@/app/api/files/utils'
|
||||
|
||||
const logger = createLogger('FileDownload')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { key, name, storageProvider, bucketName, isExecutionFile } = body
|
||||
|
||||
if (!key) {
|
||||
return createErrorResponse(new Error('File key is required'), 400)
|
||||
}
|
||||
|
||||
logger.info(`Generating download URL for file: ${name || key}`)
|
||||
|
||||
if (isUsingCloudStorage()) {
|
||||
// Generate a fresh 5-minute presigned URL for cloud storage
|
||||
try {
|
||||
let downloadUrl: string
|
||||
|
||||
// Use execution files storage if flagged as execution file
|
||||
if (isExecutionFile) {
|
||||
logger.info(`Using execution files storage for file: ${key}`)
|
||||
downloadUrl = await getPresignedUrlWithConfig(
|
||||
key,
|
||||
{
|
||||
bucket: S3_EXECUTION_FILES_CONFIG.bucket,
|
||||
region: S3_EXECUTION_FILES_CONFIG.region,
|
||||
},
|
||||
5 * 60 // 5 minutes
|
||||
)
|
||||
} else if (storageProvider && (storageProvider === 's3' || storageProvider === 'blob')) {
|
||||
// Use explicitly specified storage provider (legacy support)
|
||||
logger.info(`Using specified storage provider '${storageProvider}' for file: ${key}`)
|
||||
|
||||
if (storageProvider === 's3') {
|
||||
downloadUrl = await getPresignedUrlWithConfig(
|
||||
key,
|
||||
{
|
||||
bucket: bucketName || S3_EXECUTION_FILES_CONFIG.bucket,
|
||||
region: S3_EXECUTION_FILES_CONFIG.region,
|
||||
},
|
||||
5 * 60 // 5 minutes
|
||||
)
|
||||
} else {
|
||||
// blob
|
||||
downloadUrl = await getPresignedUrlWithConfig(
|
||||
key,
|
||||
{
|
||||
accountName: BLOB_EXECUTION_FILES_CONFIG.accountName,
|
||||
accountKey: BLOB_EXECUTION_FILES_CONFIG.accountKey,
|
||||
connectionString: BLOB_EXECUTION_FILES_CONFIG.connectionString,
|
||||
containerName: bucketName || BLOB_EXECUTION_FILES_CONFIG.containerName,
|
||||
},
|
||||
5 * 60 // 5 minutes
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Use default storage (regular uploads)
|
||||
logger.info(`Using default storage for file: ${key}`)
|
||||
downloadUrl = await getPresignedUrl(key, 5 * 60) // 5 minutes
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
downloadUrl,
|
||||
expiresIn: 300, // 5 minutes in seconds
|
||||
fileName: name || key.split('/').pop() || 'download',
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate presigned URL for ${key}:`, error)
|
||||
return createErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to generate download URL'),
|
||||
500
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// For local storage, return the direct path
|
||||
const downloadUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/files/serve/${key}`
|
||||
|
||||
return NextResponse.json({
|
||||
downloadUrl,
|
||||
expiresIn: null, // Local URLs don't expire
|
||||
fileName: name || key.split('/').pop() || 'download',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in file download endpoint:', error)
|
||||
return createErrorResponse(
|
||||
error instanceof Error ? error : new Error('Internal server error'),
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateExecutionFileDownloadUrl } from '@/lib/workflows/execution-file-storage'
|
||||
import { getExecutionFiles } from '@/lib/workflows/execution-files-server'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
|
||||
const logger = createLogger('ExecutionFileDownloadAPI')
|
||||
|
||||
/**
|
||||
* Generate a short-lived presigned URL for secure execution file download
|
||||
* GET /api/files/execution/[executionId]/[fileId]
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ executionId: string; fileId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { executionId, fileId } = await params
|
||||
|
||||
if (!executionId || !fileId) {
|
||||
return NextResponse.json({ error: 'Execution ID and File ID are required' }, { status: 400 })
|
||||
}
|
||||
|
||||
logger.info(`Generating download URL for file ${fileId} in execution ${executionId}`)
|
||||
|
||||
// Get files for this execution
|
||||
const executionFiles = await getExecutionFiles(executionId)
|
||||
|
||||
if (executionFiles.length === 0) {
|
||||
return NextResponse.json({ error: 'No files found for this execution' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Find the specific file
|
||||
const file = executionFiles.find((f) => f.id === fileId)
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: 'File not found in this execution' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if file is expired
|
||||
if (new Date(file.expiresAt) < new Date()) {
|
||||
return NextResponse.json({ error: 'File has expired' }, { status: 410 })
|
||||
}
|
||||
|
||||
// Since ExecutionFileMetadata is now just UserFile, no conversion needed
|
||||
const userFile: UserFile = file
|
||||
|
||||
// Generate a new short-lived presigned URL (5 minutes)
|
||||
const downloadUrl = await generateExecutionFileDownloadUrl(userFile)
|
||||
|
||||
logger.info(`Generated download URL for file ${file.name} (execution: ${executionId})`)
|
||||
|
||||
const response = NextResponse.json({
|
||||
downloadUrl,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type,
|
||||
expiresIn: 300, // 5 minutes
|
||||
})
|
||||
|
||||
// Ensure no caching of download URLs
|
||||
response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
response.headers.set('Pragma', 'no-cache')
|
||||
response.headers.set('Expires', '0')
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error generating execution file download URL:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { downloadFile, isUsingCloudStorage } from '@/lib/uploads'
|
||||
import { UPLOAD_DIR } from '@/lib/uploads/setup'
|
||||
import { UPLOAD_DIR_SERVER } from '@/lib/uploads/setup.server'
|
||||
import '@/lib/uploads/setup.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -70,7 +70,7 @@ export async function POST(request: NextRequest) {
|
||||
const requestData = await request.json()
|
||||
const { filePath, fileType } = requestData
|
||||
|
||||
if (!filePath) {
|
||||
if (!filePath || (typeof filePath === 'string' && filePath.trim() === '')) {
|
||||
return NextResponse.json({ success: false, error: 'No file path provided' }, { status: 400 })
|
||||
}
|
||||
|
||||
@@ -80,6 +80,16 @@ export async function POST(request: NextRequest) {
|
||||
if (Array.isArray(filePath)) {
|
||||
const results = []
|
||||
for (const path of filePath) {
|
||||
// Skip empty or invalid paths
|
||||
if (!path || (typeof path === 'string' && path.trim() === '')) {
|
||||
results.push({
|
||||
success: false,
|
||||
error: 'Empty file path in array',
|
||||
filePath: path || '',
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await parseFileSingle(path, fileType)
|
||||
// Add processing time to metadata
|
||||
if (result.metadata) {
|
||||
@@ -154,6 +164,15 @@ export async function POST(request: NextRequest) {
|
||||
async function parseFileSingle(filePath: string, fileType?: string): Promise<ParseResult> {
|
||||
logger.info('Parsing file:', filePath)
|
||||
|
||||
// Validate that filePath is not empty
|
||||
if (!filePath || filePath.trim() === '') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Empty file path provided',
|
||||
filePath: filePath || '',
|
||||
}
|
||||
}
|
||||
|
||||
// Validate path for security before any processing
|
||||
const pathValidation = validateFilePath(filePath)
|
||||
if (!pathValidation.isValid) {
|
||||
@@ -337,7 +356,7 @@ async function handleLocalFile(filePath: string, fileType?: string): Promise<Par
|
||||
try {
|
||||
// Extract filename from path
|
||||
const filename = filePath.split('/').pop() || filePath
|
||||
const fullPath = path.join(UPLOAD_DIR, filename)
|
||||
const fullPath = path.join(UPLOAD_DIR_SERVER, filename)
|
||||
|
||||
logger.info('Processing local file:', fullPath)
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getStorageProvider, isUsingCloudStorage } from '@/lib/uploads'
|
||||
import { getBlobServiceClient } from '@/lib/uploads/blob/blob-client'
|
||||
import { getS3Client, sanitizeFilenameForMetadata } from '@/lib/uploads/s3/s3-client'
|
||||
// Dynamic imports for storage clients to avoid client-side bundling
|
||||
import {
|
||||
BLOB_CHAT_CONFIG,
|
||||
BLOB_CONFIG,
|
||||
@@ -169,6 +168,7 @@ async function handleS3PresignedUrl(
|
||||
|
||||
const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}`
|
||||
|
||||
const { sanitizeFilenameForMetadata } = await import('@/lib/uploads/s3/s3-client')
|
||||
const sanitizedOriginalName = sanitizeFilenameForMetadata(fileName)
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
@@ -194,6 +194,7 @@ async function handleS3PresignedUrl(
|
||||
|
||||
let presignedUrl: string
|
||||
try {
|
||||
const { getS3Client } = await import('@/lib/uploads/s3/s3-client')
|
||||
presignedUrl = await getSignedUrl(getS3Client(), command, { expiresIn: 3600 })
|
||||
} catch (s3Error) {
|
||||
logger.error('Failed to generate S3 presigned URL:', s3Error)
|
||||
@@ -272,6 +273,7 @@ async function handleBlobPresignedUrl(
|
||||
|
||||
const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}`
|
||||
|
||||
const { getBlobServiceClient } = await import('@/lib/uploads/blob/blob-client')
|
||||
const blobServiceClient = getBlobServiceClient()
|
||||
const containerClient = blobServiceClient.getContainerClient(config.containerName)
|
||||
const blockBlobClient = containerClient.getBlockBlobClient(uniqueKey)
|
||||
|
||||
@@ -26,7 +26,9 @@ describe('File Upload API Route', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.doMock('@/lib/uploads/setup.server', () => ({}))
|
||||
vi.doMock('@/lib/uploads/setup.server', () => ({
|
||||
UPLOAD_DIR_SERVER: '/tmp/test-uploads',
|
||||
}))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -52,6 +54,12 @@ describe('File Upload API Route', () => {
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
// Log error details if test fails
|
||||
if (response.status !== 200) {
|
||||
console.error('Upload failed with status:', response.status)
|
||||
console.error('Error response:', data)
|
||||
}
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toHaveProperty('path')
|
||||
expect(data.path).toMatch(/\/api\/files\/serve\/.*\.txt$/)
|
||||
@@ -59,8 +67,9 @@ describe('File Upload API Route', () => {
|
||||
expect(data).toHaveProperty('size')
|
||||
expect(data).toHaveProperty('type', 'text/plain')
|
||||
|
||||
const fs = await import('fs/promises')
|
||||
expect(fs.writeFile).toHaveBeenCalled()
|
||||
// Verify the upload function was called (we're mocking at the uploadFile level)
|
||||
const { uploadFile } = await import('@/lib/uploads')
|
||||
expect(uploadFile).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should upload a file to S3 when in S3 mode', async () => {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { isUsingCloudStorage, uploadFile } from '@/lib/uploads'
|
||||
import { UPLOAD_DIR } from '@/lib/uploads/setup'
|
||||
import { getPresignedUrl, isUsingCloudStorage, uploadFile } from '@/lib/uploads'
|
||||
import '@/lib/uploads/setup.server'
|
||||
import {
|
||||
createErrorResponse,
|
||||
@@ -27,10 +23,21 @@ export async function POST(request: NextRequest) {
|
||||
throw new InvalidRequestError('No files provided')
|
||||
}
|
||||
|
||||
// Get optional scoping parameters for execution-scoped storage
|
||||
const workflowId = formData.get('workflowId') as string | null
|
||||
const executionId = formData.get('executionId') as string | null
|
||||
const workspaceId = formData.get('workspaceId') as string | null
|
||||
|
||||
// Log storage mode
|
||||
const usingCloudStorage = isUsingCloudStorage()
|
||||
logger.info(`Using storage mode: ${usingCloudStorage ? 'Cloud' : 'Local'} for file upload`)
|
||||
|
||||
if (workflowId && executionId) {
|
||||
logger.info(
|
||||
`Uploading files for execution-scoped storage: workflow=${workflowId}, execution=${executionId}`
|
||||
)
|
||||
}
|
||||
|
||||
const uploadResults = []
|
||||
|
||||
// Process each file
|
||||
@@ -39,33 +46,60 @@ export async function POST(request: NextRequest) {
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
if (usingCloudStorage) {
|
||||
// Upload to cloud storage (S3 or Azure Blob)
|
||||
try {
|
||||
logger.info(`Uploading file to cloud storage: ${originalName}`)
|
||||
const result = await uploadFile(buffer, originalName, file.type, file.size)
|
||||
logger.info(`Successfully uploaded to cloud storage: ${result.key}`)
|
||||
uploadResults.push(result)
|
||||
} catch (error) {
|
||||
logger.error('Error uploading to cloud storage:', error)
|
||||
throw error
|
||||
// For execution-scoped files, use the dedicated execution file storage
|
||||
if (workflowId && executionId) {
|
||||
// Use the dedicated execution file storage system
|
||||
const { uploadExecutionFile } = await import('@/lib/workflows/execution-file-storage')
|
||||
const userFile = await uploadExecutionFile(
|
||||
{
|
||||
workspaceId: workspaceId || '',
|
||||
workflowId,
|
||||
executionId,
|
||||
},
|
||||
buffer,
|
||||
originalName,
|
||||
file.type
|
||||
)
|
||||
|
||||
uploadResults.push(userFile)
|
||||
continue
|
||||
}
|
||||
|
||||
// Upload to cloud or local storage using the standard uploadFile function
|
||||
try {
|
||||
logger.info(`Uploading file: ${originalName}`)
|
||||
const result = await uploadFile(buffer, originalName, file.type, file.size)
|
||||
|
||||
// Generate a presigned URL for cloud storage with appropriate expiry
|
||||
// Regular files get 24 hours (execution files are handled above)
|
||||
let presignedUrl: string | undefined
|
||||
if (usingCloudStorage) {
|
||||
try {
|
||||
presignedUrl = await getPresignedUrl(result.key, 24 * 60 * 60) // 24 hours
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to generate presigned URL for ${originalName}:`, error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Upload to local file system in development
|
||||
const extension = originalName.split('.').pop() || ''
|
||||
const uniqueFilename = `${uuidv4()}.${extension}`
|
||||
const filePath = join(UPLOAD_DIR, uniqueFilename)
|
||||
|
||||
logger.info(`Uploading file to local storage: ${filePath}`)
|
||||
await writeFile(filePath, buffer)
|
||||
logger.info(`Successfully wrote file to: ${filePath}`)
|
||||
// Create the serve path
|
||||
const servePath = `/api/files/serve/${result.key}`
|
||||
|
||||
uploadResults.push({
|
||||
path: `/api/files/serve/${uniqueFilename}`,
|
||||
const uploadResult = {
|
||||
name: originalName,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
})
|
||||
key: result.key,
|
||||
path: servePath,
|
||||
url: presignedUrl || servePath,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
|
||||
}
|
||||
|
||||
logger.info(`Successfully uploaded: ${result.key}`)
|
||||
uploadResults.push(uploadResult)
|
||||
} catch (error) {
|
||||
logger.error(`Error uploading ${originalName}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ ${message}
|
||||
|
||||
// Send email using Resend
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: `Sim <noreply@${getEmailDomain()}>`,
|
||||
to: [`help@${getEmailDomain()}`],
|
||||
from: `Sim <noreply@${env.EMAIL_DOMAIN || getEmailDomain()}>`,
|
||||
to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`],
|
||||
subject: `[${type.toUpperCase()}] ${subject}`,
|
||||
replyTo: email,
|
||||
text: emailText,
|
||||
@@ -121,7 +121,7 @@ ${message}
|
||||
// Send confirmation email to the user
|
||||
await resend.emails
|
||||
.send({
|
||||
from: `Sim <noreply@${getEmailDomain()}>`,
|
||||
from: `Sim <noreply@${env.EMAIL_DOMAIN || getEmailDomain()}>`,
|
||||
to: [email],
|
||||
subject: `Your ${type} request has been received: ${subject}`,
|
||||
text: `
|
||||
@@ -137,7 +137,7 @@ ${images.length > 0 ? `You attached ${images.length} image(s).` : ''}
|
||||
Best regards,
|
||||
The Sim Team
|
||||
`,
|
||||
replyTo: `help@${getEmailDomain()}`,
|
||||
replyTo: `help@${env.EMAIL_DOMAIN || getEmailDomain()}`,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.warn(`[${requestId}] Failed to send confirmation email`, err)
|
||||
|
||||
@@ -376,7 +376,7 @@ describe('Knowledge Base Documents API Route', () => {
|
||||
],
|
||||
processingOptions: {
|
||||
chunkSize: 50, // Invalid: too small
|
||||
minCharactersPerChunk: 10, // Invalid: too small
|
||||
minCharactersPerChunk: 0, // Invalid: too small
|
||||
recipe: 'default',
|
||||
lang: 'en',
|
||||
chunkOverlap: 1000, // Invalid: too large
|
||||
|
||||
@@ -295,7 +295,7 @@ const BulkCreateDocumentsSchema = z.object({
|
||||
documents: z.array(CreateDocumentSchema),
|
||||
processingOptions: z.object({
|
||||
chunkSize: z.number().min(100).max(4000),
|
||||
minCharactersPerChunk: z.number().min(50).max(2000),
|
||||
minCharactersPerChunk: z.number().min(1).max(2000),
|
||||
recipe: z.string(),
|
||||
lang: z.string(),
|
||||
chunkOverlap: z.number().min(0).max(500),
|
||||
|
||||
@@ -168,7 +168,7 @@ describe('Knowledge Base API Route', () => {
|
||||
expect(data.data.embeddingDimension).toBe(1536)
|
||||
expect(data.data.chunkingConfig).toEqual({
|
||||
maxSize: 1024,
|
||||
minSize: 100,
|
||||
minSize: 1,
|
||||
overlap: 200,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,12 +18,12 @@ const CreateKnowledgeBaseSchema = z.object({
|
||||
chunkingConfig: z
|
||||
.object({
|
||||
maxSize: z.number().min(100).max(4000).default(1024),
|
||||
minSize: z.number().min(50).max(2000).default(100),
|
||||
minSize: z.number().min(1).max(2000).default(1),
|
||||
overlap: z.number().min(0).max(500).default(200),
|
||||
})
|
||||
.default({
|
||||
maxSize: 1024,
|
||||
minSize: 100,
|
||||
minSize: 1,
|
||||
overlap: 200,
|
||||
})
|
||||
.refine((data) => data.minSize < data.maxSize, {
|
||||
|
||||
@@ -531,7 +531,8 @@ export async function processDocumentAsync(
|
||||
docData.filename,
|
||||
docData.mimeType,
|
||||
processingOptions.chunkSize || 1000,
|
||||
processingOptions.chunkOverlap || 200
|
||||
processingOptions.chunkOverlap || 200,
|
||||
processingOptions.minCharactersPerChunk || 1
|
||||
)
|
||||
|
||||
const now = new Date()
|
||||
|
||||
@@ -5,7 +5,8 @@ import { verifyCronAuth } from '@/lib/auth/internal'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { snapshotService } from '@/lib/logs/execution/snapshot/service'
|
||||
import { getS3Client } from '@/lib/uploads/s3/s3-client'
|
||||
import { deleteFile, isUsingCloudStorage } from '@/lib/uploads'
|
||||
// Dynamic import for S3 client to avoid client-side bundling
|
||||
import { db } from '@/db'
|
||||
import { subscription, user, workflow, workflowExecutionLogs } from '@/db/schema'
|
||||
|
||||
@@ -69,6 +70,11 @@ export async function GET(request: NextRequest) {
|
||||
deleted: 0,
|
||||
deleteFailed: 0,
|
||||
},
|
||||
files: {
|
||||
total: 0,
|
||||
deleted: 0,
|
||||
deleteFailed: 0,
|
||||
},
|
||||
snapshots: {
|
||||
cleaned: 0,
|
||||
cleanupFailed: 0,
|
||||
@@ -106,6 +112,7 @@ export async function GET(request: NextRequest) {
|
||||
totalInputCost: workflowExecutionLogs.totalInputCost,
|
||||
totalOutputCost: workflowExecutionLogs.totalOutputCost,
|
||||
totalTokens: workflowExecutionLogs.totalTokens,
|
||||
files: workflowExecutionLogs.files,
|
||||
metadata: workflowExecutionLogs.metadata,
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
})
|
||||
@@ -132,6 +139,7 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
|
||||
try {
|
||||
const { getS3Client } = await import('@/lib/uploads/s3/s3-client')
|
||||
await getS3Client().send(
|
||||
new PutObjectCommand({
|
||||
Bucket: S3_CONFIG.bucket,
|
||||
@@ -150,6 +158,23 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
results.enhancedLogs.archived++
|
||||
|
||||
// Clean up associated files if using cloud storage
|
||||
if (isUsingCloudStorage() && log.files && Array.isArray(log.files)) {
|
||||
for (const file of log.files) {
|
||||
if (file && typeof file === 'object' && file.key) {
|
||||
results.files.total++
|
||||
try {
|
||||
await deleteFile(file.key)
|
||||
results.files.deleted++
|
||||
logger.info(`Deleted file: ${file.key}`)
|
||||
} catch (fileError) {
|
||||
results.files.deleteFailed++
|
||||
logger.error(`Failed to delete file ${file.key}:`, { fileError })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete enhanced log
|
||||
const deleteResult = await db
|
||||
@@ -198,7 +223,7 @@ export async function GET(request: NextRequest) {
|
||||
const reachedLimit = batchesProcessed >= MAX_BATCHES && hasMoreLogs
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Processed ${batchesProcessed} enhanced log batches (${results.enhancedLogs.total} logs) in ${timeElapsed.toFixed(2)}s${reachedLimit ? ' (batch limit reached)' : ''}`,
|
||||
message: `Processed ${batchesProcessed} enhanced log batches (${results.enhancedLogs.total} logs, ${results.files.total} files) in ${timeElapsed.toFixed(2)}s${reachedLimit ? ' (batch limit reached)' : ''}`,
|
||||
results,
|
||||
complete: !hasMoreLogs,
|
||||
batchLimitReached: reachedLimit,
|
||||
|
||||
@@ -95,6 +95,7 @@ export async function GET(request: NextRequest) {
|
||||
totalOutputCost: workflowExecutionLogs.totalOutputCost,
|
||||
totalTokens: workflowExecutionLogs.totalTokens,
|
||||
metadata: workflowExecutionLogs.metadata,
|
||||
files: workflowExecutionLogs.files,
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
@@ -334,6 +335,7 @@ export async function GET(request: NextRequest) {
|
||||
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
|
||||
trigger: log.trigger,
|
||||
createdAt: log.startedAt.toISOString(),
|
||||
files: log.files || undefined,
|
||||
workflow: params.includeWorkflow ? workflow : undefined,
|
||||
metadata: {
|
||||
totalDuration: log.totalDurationMs,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
validateSeatAvailability,
|
||||
} from '@/lib/billing/validation/seat-management'
|
||||
import { sendEmail } from '@/lib/email/mailer'
|
||||
import { validateAndNormalizeEmail } from '@/lib/email/utils'
|
||||
import { quickValidateEmail } from '@/lib/email/validation'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
|
||||
@@ -201,8 +201,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
// Validate and normalize emails
|
||||
const processedEmails = invitationEmails
|
||||
.map((email: string) => {
|
||||
const result = validateAndNormalizeEmail(email)
|
||||
return result.isValid ? result.normalized : null
|
||||
const normalized = email.trim().toLowerCase()
|
||||
const validation = quickValidateEmail(normalized)
|
||||
return validation.isValid ? normalized : null
|
||||
})
|
||||
.filter(Boolean) as string[]
|
||||
|
||||
@@ -401,7 +402,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
pendingEmails.includes(email)
|
||||
),
|
||||
invalidEmails: invitationEmails.filter(
|
||||
(email: string) => !validateAndNormalizeEmail(email)
|
||||
(email: string) => !quickValidateEmail(email.trim().toLowerCase()).isValid
|
||||
),
|
||||
workspaceInvitations: isBatch ? validWorkspaceInvitations.length : 0,
|
||||
seatInfo: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getEmailSubject, renderInvitationEmail } from '@/components/emails/rend
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
|
||||
import { sendEmail } from '@/lib/email/mailer'
|
||||
import { validateAndNormalizeEmail } from '@/lib/email/utils'
|
||||
import { quickValidateEmail } from '@/lib/email/validation'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { db } from '@/db'
|
||||
@@ -139,9 +139,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
|
||||
// Validate and normalize email
|
||||
const { isValid, normalized: normalizedEmail } = validateAndNormalizeEmail(email)
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Invalid email format' }, { status: 400 })
|
||||
const normalizedEmail = email.trim().toLowerCase()
|
||||
const validation = quickValidateEmail(normalizedEmail)
|
||||
if (!validation.isValid) {
|
||||
return NextResponse.json(
|
||||
{ error: validation.reason || 'Invalid email format' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify user has admin access
|
||||
|
||||
@@ -177,7 +177,7 @@ export async function POST(request: Request) {
|
||||
throw new Error('Invalid JSON in request body')
|
||||
}
|
||||
|
||||
const { toolId, params } = requestBody
|
||||
const { toolId, params, executionContext } = requestBody
|
||||
|
||||
if (!toolId) {
|
||||
logger.error(`[${requestId}] Missing toolId in request`)
|
||||
@@ -214,8 +214,21 @@ export async function POST(request: Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Check if tool has file outputs - if so, don't skip post-processing
|
||||
const hasFileOutputs =
|
||||
tool.outputs &&
|
||||
Object.values(tool.outputs).some(
|
||||
(output) => output.type === 'file' || output.type === 'file[]'
|
||||
)
|
||||
|
||||
// Execute tool
|
||||
const result = await executeTool(toolId, params, true, true)
|
||||
const result = await executeTool(
|
||||
toolId,
|
||||
params,
|
||||
true, // skipProxy (we're already in the proxy)
|
||||
!hasFileOutputs, // skipPostProcess (don't skip if tool has file outputs)
|
||||
executionContext // pass execution context for file processing
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
logger.warn(`[${requestId}] Tool execution failed for ${toolId}`, {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflow, workflowSchedule } from '@/db/schema'
|
||||
|
||||
@@ -36,6 +37,7 @@ export async function DELETE(
|
||||
workflow: {
|
||||
id: workflow.id,
|
||||
userId: workflow.userId,
|
||||
workspaceId: workflow.workspaceId,
|
||||
},
|
||||
})
|
||||
.from(workflowSchedule)
|
||||
@@ -48,7 +50,22 @@ export async function DELETE(
|
||||
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (schedules[0].workflow.userId !== session.user.id) {
|
||||
const workflowRecord = schedules[0].workflow
|
||||
|
||||
// Check authorization - either the user owns the workflow or has write/admin workspace permissions
|
||||
let isAuthorized = workflowRecord.userId === session.user.id
|
||||
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
|
||||
if (!isAuthorized && workflowRecord.workspaceId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workflowRecord.workspaceId
|
||||
)
|
||||
isAuthorized = userPermission === 'write' || userPermission === 'admin'
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
logger.warn(`[${requestId}] Unauthorized schedule deletion attempt for schedule: ${id}`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -42,7 +43,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
}
|
||||
|
||||
const [workflowRecord] = await db
|
||||
.select({ userId: workflow.userId })
|
||||
.select({ userId: workflow.userId, workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, schedule.workflowId))
|
||||
.limit(1)
|
||||
@@ -52,7 +53,20 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (workflowRecord.userId !== session.user.id) {
|
||||
// Check authorization - either the user owns the workflow or has workspace permissions
|
||||
let isAuthorized = workflowRecord.userId === session.user.id
|
||||
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
|
||||
if (!isAuthorized && workflowRecord.workspaceId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workflowRecord.workspaceId
|
||||
)
|
||||
isAuthorized = userPermission !== null
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
logger.warn(`[${requestId}] User not authorized to view this schedule: ${scheduleId}`)
|
||||
return NextResponse.json({ error: 'Not authorized to view this schedule' }, { status: 403 })
|
||||
}
|
||||
|
||||
@@ -209,244 +209,284 @@ export async function GET() {
|
||||
requestId
|
||||
)
|
||||
|
||||
// Load workflow data from normalized tables (no fallback to deprecated state column)
|
||||
logger.debug(
|
||||
`[${requestId}] Loading workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(schedule.workflowId)
|
||||
|
||||
if (!normalizedData) {
|
||||
logger.error(
|
||||
`[${requestId}] No normalized data found for scheduled workflow ${schedule.workflowId}`
|
||||
)
|
||||
throw new Error(
|
||||
`Workflow data not found in normalized tables for ${schedule.workflowId}`
|
||||
)
|
||||
}
|
||||
|
||||
// Use normalized data only
|
||||
const blocks = normalizedData.blocks
|
||||
const edges = normalizedData.edges
|
||||
const loops = normalizedData.loops
|
||||
const parallels = normalizedData.parallels
|
||||
logger.info(
|
||||
`[${requestId}] Loaded scheduled workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
|
||||
// Retrieve environment variables for this user (if any).
|
||||
const [userEnv] = await db
|
||||
.select()
|
||||
.from(environmentTable)
|
||||
.where(eq(environmentTable.userId, workflowRecord.userId))
|
||||
.limit(1)
|
||||
|
||||
if (!userEnv) {
|
||||
try {
|
||||
// Load workflow data from normalized tables (no fallback to deprecated state column)
|
||||
logger.debug(
|
||||
`[${requestId}] No environment record found for user ${workflowRecord.userId}. Proceeding with empty variables.`
|
||||
`[${requestId}] Loading workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
}
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(schedule.workflowId)
|
||||
|
||||
const variables = EnvVarsSchema.parse(userEnv?.variables ?? {})
|
||||
if (!normalizedData) {
|
||||
logger.error(
|
||||
`[${requestId}] No normalized data found for scheduled workflow ${schedule.workflowId}`
|
||||
)
|
||||
throw new Error(
|
||||
`Workflow data not found in normalized tables for ${schedule.workflowId}`
|
||||
)
|
||||
}
|
||||
|
||||
const currentBlockStates = await Object.entries(mergedStates).reduce(
|
||||
async (accPromise, [id, block]) => {
|
||||
const acc = await accPromise
|
||||
acc[id] = await Object.entries(block.subBlocks).reduce(
|
||||
async (subAccPromise, [key, subBlock]) => {
|
||||
const subAcc = await subAccPromise
|
||||
let value = subBlock.value
|
||||
// Use normalized data only
|
||||
const blocks = normalizedData.blocks
|
||||
const edges = normalizedData.edges
|
||||
const loops = normalizedData.loops
|
||||
const parallels = normalizedData.parallels
|
||||
logger.info(
|
||||
`[${requestId}] Loaded scheduled workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
|
||||
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
||||
const matches = value.match(/{{([^}]+)}}/g)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const varName = match.slice(2, -2)
|
||||
const encryptedValue = variables[varName]
|
||||
if (!encryptedValue) {
|
||||
throw new Error(`Environment variable "${varName}" was not found`)
|
||||
}
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(encryptedValue)
|
||||
value = (value as string).replace(match, decrypted)
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Error decrypting value for variable "${varName}"`,
|
||||
error
|
||||
)
|
||||
throw new Error(
|
||||
`Failed to decrypt environment variable "${varName}": ${error.message}`
|
||||
)
|
||||
// Retrieve environment variables for this user (if any).
|
||||
const [userEnv] = await db
|
||||
.select()
|
||||
.from(environmentTable)
|
||||
.where(eq(environmentTable.userId, workflowRecord.userId))
|
||||
.limit(1)
|
||||
|
||||
if (!userEnv) {
|
||||
logger.debug(
|
||||
`[${requestId}] No environment record found for user ${workflowRecord.userId}. Proceeding with empty variables.`
|
||||
)
|
||||
}
|
||||
|
||||
const variables = EnvVarsSchema.parse(userEnv?.variables ?? {})
|
||||
|
||||
const currentBlockStates = await Object.entries(mergedStates).reduce(
|
||||
async (accPromise, [id, block]) => {
|
||||
const acc = await accPromise
|
||||
acc[id] = await Object.entries(block.subBlocks).reduce(
|
||||
async (subAccPromise, [key, subBlock]) => {
|
||||
const subAcc = await subAccPromise
|
||||
let value = subBlock.value
|
||||
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
value.includes('{{') &&
|
||||
value.includes('}}')
|
||||
) {
|
||||
const matches = value.match(/{{([^}]+)}}/g)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const varName = match.slice(2, -2)
|
||||
const encryptedValue = variables[varName]
|
||||
if (!encryptedValue) {
|
||||
throw new Error(`Environment variable "${varName}" was not found`)
|
||||
}
|
||||
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(encryptedValue)
|
||||
value = (value as string).replace(match, decrypted)
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Error decrypting value for variable "${varName}"`,
|
||||
error
|
||||
)
|
||||
throw new Error(
|
||||
`Failed to decrypt environment variable "${varName}": ${error.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subAcc[key] = value
|
||||
return subAcc
|
||||
},
|
||||
Promise.resolve({} as Record<string, any>)
|
||||
)
|
||||
return acc
|
||||
},
|
||||
Promise.resolve({} as Record<string, Record<string, any>>)
|
||||
)
|
||||
subAcc[key] = value
|
||||
return subAcc
|
||||
},
|
||||
Promise.resolve({} as Record<string, any>)
|
||||
)
|
||||
return acc
|
||||
},
|
||||
Promise.resolve({} as Record<string, Record<string, any>>)
|
||||
)
|
||||
|
||||
const decryptedEnvVars: Record<string, string> = {}
|
||||
for (const [key, encryptedValue] of Object.entries(variables)) {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(encryptedValue)
|
||||
decryptedEnvVars[key] = decrypted
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to decrypt environment variable "${key}"`,
|
||||
error
|
||||
)
|
||||
throw new Error(`Failed to decrypt environment variable "${key}": ${error.message}`)
|
||||
const decryptedEnvVars: Record<string, string> = {}
|
||||
for (const [key, encryptedValue] of Object.entries(variables)) {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(encryptedValue)
|
||||
decryptedEnvVars[key] = decrypted
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to decrypt environment variable "${key}"`,
|
||||
error
|
||||
)
|
||||
throw new Error(
|
||||
`Failed to decrypt environment variable "${key}": ${error.message}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the block states to ensure response formats are properly parsed
|
||||
const processedBlockStates = Object.entries(currentBlockStates).reduce(
|
||||
(acc, [blockId, blockState]) => {
|
||||
// Check if this block has a responseFormat that needs to be parsed
|
||||
if (blockState.responseFormat && typeof blockState.responseFormat === 'string') {
|
||||
const responseFormatValue = blockState.responseFormat.trim()
|
||||
// Process the block states to ensure response formats are properly parsed
|
||||
const processedBlockStates = Object.entries(currentBlockStates).reduce(
|
||||
(acc, [blockId, blockState]) => {
|
||||
// Check if this block has a responseFormat that needs to be parsed
|
||||
if (blockState.responseFormat && typeof blockState.responseFormat === 'string') {
|
||||
const responseFormatValue = blockState.responseFormat.trim()
|
||||
|
||||
// Check for variable references like <start.input>
|
||||
if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) {
|
||||
logger.debug(
|
||||
`[${requestId}] Response format contains variable reference for block ${blockId}`
|
||||
)
|
||||
// Keep variable references as-is - they will be resolved during execution
|
||||
acc[blockId] = blockState
|
||||
} else if (responseFormatValue === '') {
|
||||
// Empty string - remove response format
|
||||
acc[blockId] = {
|
||||
...blockState,
|
||||
responseFormat: undefined,
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
|
||||
// Attempt to parse the responseFormat if it's a string
|
||||
const parsedResponseFormat = JSON.parse(responseFormatValue)
|
||||
|
||||
acc[blockId] = {
|
||||
...blockState,
|
||||
responseFormat: parsedResponseFormat,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`,
|
||||
error
|
||||
// Check for variable references like <start.input>
|
||||
if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) {
|
||||
logger.debug(
|
||||
`[${requestId}] Response format contains variable reference for block ${blockId}`
|
||||
)
|
||||
// Set to undefined instead of keeping malformed JSON - this allows execution to continue
|
||||
// Keep variable references as-is - they will be resolved during execution
|
||||
acc[blockId] = blockState
|
||||
} else if (responseFormatValue === '') {
|
||||
// Empty string - remove response format
|
||||
acc[blockId] = {
|
||||
...blockState,
|
||||
responseFormat: undefined,
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`)
|
||||
// Attempt to parse the responseFormat if it's a string
|
||||
const parsedResponseFormat = JSON.parse(responseFormatValue)
|
||||
|
||||
acc[blockId] = {
|
||||
...blockState,
|
||||
responseFormat: parsedResponseFormat,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`,
|
||||
error
|
||||
)
|
||||
// Set to undefined instead of keeping malformed JSON - this allows execution to continue
|
||||
acc[blockId] = {
|
||||
...blockState,
|
||||
responseFormat: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
acc[blockId] = blockState
|
||||
}
|
||||
} else {
|
||||
acc[blockId] = blockState
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Record<string, any>>
|
||||
)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Record<string, any>>
|
||||
)
|
||||
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
if (workflowRecord.variables) {
|
||||
try {
|
||||
if (typeof workflowRecord.variables === 'string') {
|
||||
workflowVariables = JSON.parse(workflowRecord.variables)
|
||||
} else {
|
||||
workflowVariables = workflowRecord.variables
|
||||
// Get workflow variables
|
||||
let workflowVariables = {}
|
||||
if (workflowRecord.variables) {
|
||||
try {
|
||||
if (typeof workflowRecord.variables === 'string') {
|
||||
workflowVariables = JSON.parse(workflowRecord.variables)
|
||||
} else {
|
||||
workflowVariables = workflowRecord.variables
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse workflow variables: ${schedule.workflowId}`, error)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse workflow variables: ${schedule.workflowId}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(
|
||||
mergedStates,
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
true // Enable validation during execution
|
||||
)
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(
|
||||
mergedStates,
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
true // Enable validation during execution
|
||||
)
|
||||
|
||||
const input = {
|
||||
workflowId: schedule.workflowId,
|
||||
_context: {
|
||||
const input = {
|
||||
workflowId: schedule.workflowId,
|
||||
},
|
||||
}
|
||||
|
||||
// Start logging with environment variables
|
||||
await loggingSession.safeStart({
|
||||
userId: workflowRecord.userId,
|
||||
workspaceId: workflowRecord.workspaceId || '',
|
||||
variables: variables || {},
|
||||
})
|
||||
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates,
|
||||
decryptedEnvVars,
|
||||
input,
|
||||
workflowVariables
|
||||
)
|
||||
|
||||
// Set up logging on the executor
|
||||
loggingSession.setupExecutor(executor)
|
||||
|
||||
const result = await executor.execute(
|
||||
schedule.workflowId,
|
||||
schedule.blockId || undefined
|
||||
)
|
||||
|
||||
const executionResult =
|
||||
'stream' in result && 'execution' in result ? result.execution : result
|
||||
|
||||
logger.info(`[${requestId}] Workflow execution completed: ${schedule.workflowId}`, {
|
||||
success: executionResult.success,
|
||||
executionTime: executionResult.metadata?.duration,
|
||||
})
|
||||
|
||||
if (executionResult.success) {
|
||||
await updateWorkflowRunCounts(schedule.workflowId)
|
||||
|
||||
try {
|
||||
await db
|
||||
.update(userStats)
|
||||
.set({
|
||||
totalScheduledExecutions: sql`total_scheduled_executions + 1`,
|
||||
lastActive: now,
|
||||
})
|
||||
.where(eq(userStats.userId, workflowRecord.userId))
|
||||
|
||||
logger.debug(`[${requestId}] Updated user stats for scheduled execution`)
|
||||
} catch (statsError) {
|
||||
logger.error(`[${requestId}] Error updating user stats:`, statsError)
|
||||
_context: {
|
||||
workflowId: schedule.workflowId,
|
||||
},
|
||||
}
|
||||
|
||||
// Start logging with environment variables
|
||||
await loggingSession.safeStart({
|
||||
userId: workflowRecord.userId,
|
||||
workspaceId: workflowRecord.workspaceId || '',
|
||||
variables: variables || {},
|
||||
})
|
||||
|
||||
const executor = new Executor({
|
||||
workflow: serializedWorkflow,
|
||||
currentBlockStates: processedBlockStates,
|
||||
envVarValues: decryptedEnvVars,
|
||||
workflowInput: input,
|
||||
workflowVariables,
|
||||
contextExtensions: {
|
||||
executionId,
|
||||
workspaceId: workflowRecord.workspaceId || '',
|
||||
},
|
||||
})
|
||||
|
||||
// Set up logging on the executor
|
||||
loggingSession.setupExecutor(executor)
|
||||
|
||||
const result = await executor.execute(
|
||||
schedule.workflowId,
|
||||
schedule.blockId || undefined
|
||||
)
|
||||
|
||||
const executionResult =
|
||||
'stream' in result && 'execution' in result ? result.execution : result
|
||||
|
||||
logger.info(`[${requestId}] Workflow execution completed: ${schedule.workflowId}`, {
|
||||
success: executionResult.success,
|
||||
executionTime: executionResult.metadata?.duration,
|
||||
})
|
||||
|
||||
if (executionResult.success) {
|
||||
await updateWorkflowRunCounts(schedule.workflowId)
|
||||
|
||||
try {
|
||||
await db
|
||||
.update(userStats)
|
||||
.set({
|
||||
totalScheduledExecutions: sql`total_scheduled_executions + 1`,
|
||||
lastActive: now,
|
||||
})
|
||||
.where(eq(userStats.userId, workflowRecord.userId))
|
||||
|
||||
logger.debug(`[${requestId}] Updated user stats for scheduled execution`)
|
||||
} catch (statsError) {
|
||||
logger.error(`[${requestId}] Error updating user stats:`, statsError)
|
||||
}
|
||||
}
|
||||
|
||||
const { traceSpans, totalDuration } = buildTraceSpans(executionResult)
|
||||
|
||||
// Complete logging
|
||||
await loggingSession.safeComplete({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: totalDuration || 0,
|
||||
finalOutput: executionResult.output || {},
|
||||
traceSpans: (traceSpans || []) as any,
|
||||
})
|
||||
|
||||
return { success: executionResult.success, blocks, executionResult }
|
||||
} catch (earlyError: any) {
|
||||
// Handle errors that occur before workflow execution (e.g., missing data, env vars, etc.)
|
||||
logger.error(
|
||||
`[${requestId}] Early failure in scheduled workflow ${schedule.workflowId}`,
|
||||
earlyError
|
||||
)
|
||||
|
||||
// Create a minimal log entry for early failures
|
||||
try {
|
||||
await loggingSession.safeStart({
|
||||
userId: workflowRecord.userId,
|
||||
workspaceId: workflowRecord.workspaceId || '',
|
||||
variables: {},
|
||||
})
|
||||
|
||||
await loggingSession.safeCompleteWithError({
|
||||
message: `Schedule execution failed before workflow started: ${earlyError.message}`,
|
||||
stackTrace: earlyError.stack,
|
||||
})
|
||||
} catch (loggingError) {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create log entry for early schedule failure`,
|
||||
loggingError
|
||||
)
|
||||
}
|
||||
|
||||
// Re-throw the error to be handled by the outer catch block
|
||||
throw earlyError
|
||||
}
|
||||
|
||||
const { traceSpans, totalDuration } = buildTraceSpans(executionResult)
|
||||
|
||||
// Complete logging
|
||||
await loggingSession.safeComplete({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: totalDuration || 0,
|
||||
finalOutput: executionResult.output || {},
|
||||
traceSpans: (traceSpans || []) as any,
|
||||
})
|
||||
|
||||
return { success: executionResult.success, blocks, executionResult }
|
||||
})()
|
||||
|
||||
if (executionSuccess.success) {
|
||||
@@ -535,7 +575,31 @@ export async function GET() {
|
||||
error
|
||||
)
|
||||
|
||||
// Error logging handled by logging session inside sync executor
|
||||
// Ensure we create a log entry for this failed execution
|
||||
try {
|
||||
const failureLoggingSession = new LoggingSession(
|
||||
schedule.workflowId,
|
||||
executionId,
|
||||
'schedule',
|
||||
requestId
|
||||
)
|
||||
|
||||
await failureLoggingSession.safeStart({
|
||||
userId: workflowRecord.userId,
|
||||
workspaceId: workflowRecord.workspaceId || '',
|
||||
variables: {},
|
||||
})
|
||||
|
||||
await failureLoggingSession.safeCompleteWithError({
|
||||
message: `Schedule execution failed: ${error.message}`,
|
||||
stackTrace: error.stack,
|
||||
})
|
||||
} catch (loggingError) {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create log entry for failed schedule execution`,
|
||||
loggingError
|
||||
)
|
||||
}
|
||||
|
||||
let nextRunAt: Date
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,11 @@ describe('Schedule Configuration API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock permissions
|
||||
vi.doMock('@/lib/permissions/utils', () => ({
|
||||
getUserEntityPermissions: vi.fn().mockResolvedValue('admin'), // User has admin permissions
|
||||
}))
|
||||
|
||||
// Extend sampleWorkflowState for scheduling
|
||||
const _workflowStateWithSchedule = {
|
||||
...sampleWorkflowState,
|
||||
@@ -46,33 +51,39 @@ describe('Schedule Configuration API Route', () => {
|
||||
}
|
||||
|
||||
// Create mock database with test schedules
|
||||
// Mock the database to return workflow data for authorization check
|
||||
vi.doMock('@/db', () => {
|
||||
let callCount = 0
|
||||
const mockDb = {
|
||||
select: vi.fn().mockImplementation(() => ({
|
||||
from: vi.fn().mockImplementation((table: string) => {
|
||||
if (table === 'workflow_schedule') {
|
||||
return {
|
||||
where: vi.fn().mockImplementation(() => ({
|
||||
limit: vi.fn().mockImplementation(() => [
|
||||
from: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockImplementation(() => ({
|
||||
limit: vi.fn().mockImplementation(() => {
|
||||
callCount++
|
||||
// First call: workflow lookup for authorization
|
||||
if (callCount === 1) {
|
||||
return [
|
||||
{
|
||||
id: 'schedule-id',
|
||||
workflowId: 'workflow-id',
|
||||
id: 'workflow-id',
|
||||
userId: 'user-id',
|
||||
nextRunAt: new Date(),
|
||||
lastRanAt: null,
|
||||
cronExpression: '0 9 * * *',
|
||||
triggerType: 'schedule',
|
||||
workspaceId: null, // User owns the workflow directly
|
||||
},
|
||||
]),
|
||||
})),
|
||||
}
|
||||
}
|
||||
return {
|
||||
where: vi.fn().mockImplementation(() => ({
|
||||
limit: vi.fn().mockImplementation(() => []),
|
||||
})),
|
||||
}
|
||||
}),
|
||||
]
|
||||
}
|
||||
// Second call: existing schedule lookup - return existing schedule for update test
|
||||
return [
|
||||
{
|
||||
id: 'existing-schedule-id',
|
||||
workflowId: 'workflow-id',
|
||||
blockId: 'starter-id',
|
||||
cronExpression: '0 9 * * *',
|
||||
nextRunAt: new Date(),
|
||||
status: 'active',
|
||||
},
|
||||
]
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockImplementation(() => ({
|
||||
@@ -176,94 +187,6 @@ describe('Schedule Configuration API Route', () => {
|
||||
// Instead, we just verify that the response has the expected properties
|
||||
})
|
||||
|
||||
/**
|
||||
* Test updating an existing schedule
|
||||
*/
|
||||
it('should update an existing schedule', async () => {
|
||||
// Setup the specific DB mock for this test
|
||||
vi.doMock('@/db', () => {
|
||||
const mockDb = {
|
||||
select: vi.fn().mockImplementation(() => ({
|
||||
from: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockImplementation(() => ({
|
||||
limit: vi.fn().mockImplementation(() => [
|
||||
{
|
||||
id: 'schedule-id',
|
||||
workflowId: 'workflow-id',
|
||||
nextRunAt: new Date(),
|
||||
cronExpression: '0 9 * * *',
|
||||
},
|
||||
]),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
insert: vi.fn().mockImplementation(() => ({
|
||||
values: vi.fn().mockImplementation(() => ({
|
||||
onConflictDoUpdate: vi.fn().mockResolvedValue({}),
|
||||
})),
|
||||
})),
|
||||
delete: vi.fn().mockImplementation(() => ({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}
|
||||
|
||||
return { db: mockDb }
|
||||
})
|
||||
|
||||
// Create a mock request with updated schedule
|
||||
const req = createMockRequest('POST', {
|
||||
workflowId: 'workflow-id',
|
||||
state: {
|
||||
blocks: {
|
||||
'starter-id': {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
startWorkflow: { value: 'schedule' },
|
||||
scheduleType: { value: 'daily' },
|
||||
scheduleTime: { value: '10:30' }, // Updated time
|
||||
dailyTime: { value: '10:30' },
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: [],
|
||||
loops: {},
|
||||
},
|
||||
})
|
||||
|
||||
// Override the schedule utils mock for this test
|
||||
vi.doMock('@/lib/schedules/utils', () => ({
|
||||
getScheduleTimeValues: vi.fn().mockReturnValue({
|
||||
scheduleTime: '10:30',
|
||||
dailyTime: [10, 30],
|
||||
}),
|
||||
getSubBlockValue: vi.fn().mockImplementation((block: any, id: string) => {
|
||||
const subBlocks = {
|
||||
startWorkflow: 'schedule',
|
||||
scheduleType: 'daily',
|
||||
scheduleTime: '10:30',
|
||||
dailyTime: '10:30',
|
||||
}
|
||||
return subBlocks[id as keyof typeof subBlocks] || ''
|
||||
}),
|
||||
generateCronExpression: vi.fn().mockReturnValue('0 10 * * *'),
|
||||
calculateNextRunTime: vi.fn().mockReturnValue(new Date()),
|
||||
BlockState: {},
|
||||
}))
|
||||
|
||||
// Import the route handler after mocks are set up
|
||||
const { POST } = await import('@/app/api/schedules/route')
|
||||
|
||||
// Call the handler
|
||||
const response = await POST(req)
|
||||
|
||||
// Verify response
|
||||
expect(response).toBeDefined()
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const responseData = await response.json()
|
||||
expect(responseData).toHaveProperty('message', 'Schedule updated')
|
||||
})
|
||||
|
||||
/**
|
||||
* Test removing a schedule
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
import {
|
||||
type BlockState,
|
||||
calculateNextRunTime,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
validateCronExpression,
|
||||
} from '@/lib/schedules/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflowSchedule } from '@/db/schema'
|
||||
import { workflow, workflowSchedule } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('ScheduledAPI')
|
||||
|
||||
@@ -87,6 +88,34 @@ export async function GET(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing workflowId parameter' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check if user has permission to view this workflow
|
||||
const [workflowRecord] = await db
|
||||
.select({ userId: workflow.userId, workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check authorization - either the user owns the workflow or has workspace permissions
|
||||
let isAuthorized = workflowRecord.userId === session.user.id
|
||||
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
|
||||
if (!isAuthorized && workflowRecord.workspaceId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workflowRecord.workspaceId
|
||||
)
|
||||
isAuthorized = userPermission !== null
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
return NextResponse.json({ error: 'Not authorized to view this workflow' }, { status: 403 })
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const lastLog = recentRequests.get(workflowId) || 0
|
||||
const shouldLog = now - lastLog > LOGGING_THROTTLE_MS
|
||||
@@ -152,6 +181,38 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
logger.info(`[${requestId}] Processing schedule update for workflow ${workflowId}`)
|
||||
|
||||
// Check if user has permission to modify this workflow
|
||||
const [workflowRecord] = await db
|
||||
.select({ userId: workflow.userId, workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
logger.warn(`[${requestId}] Workflow not found: ${workflowId}`)
|
||||
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check authorization - either the user owns the workflow or has write/admin workspace permissions
|
||||
let isAuthorized = workflowRecord.userId === session.user.id
|
||||
|
||||
// If not authorized by ownership and the workflow belongs to a workspace, check workspace permissions
|
||||
if (!isAuthorized && workflowRecord.workspaceId) {
|
||||
const userPermission = await getUserEntityPermissions(
|
||||
session.user.id,
|
||||
'workspace',
|
||||
workflowRecord.workspaceId
|
||||
)
|
||||
isAuthorized = userPermission === 'write' || userPermission === 'admin'
|
||||
}
|
||||
|
||||
if (!isAuthorized) {
|
||||
logger.warn(
|
||||
`[${requestId}] User not authorized to modify schedule for workflow: ${workflowId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Not authorized to modify this workflow' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Find the target block - prioritize the specific blockId if provided
|
||||
let targetBlock: BlockState | undefined
|
||||
if (blockId) {
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function POST(request: NextRequest) {
|
||||
const { workflowId, path, provider, providerConfig, blockId } = body
|
||||
|
||||
// Validate input
|
||||
if (!workflowId || !path) {
|
||||
if (!workflowId) {
|
||||
logger.warn(`[${requestId}] Missing required fields for webhook creation`, {
|
||||
hasWorkflowId: !!workflowId,
|
||||
hasPath: !!path,
|
||||
@@ -95,6 +95,26 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
||||
}
|
||||
|
||||
// For credential-based providers (those that use polling instead of webhooks),
|
||||
// generate a dummy path if none provided since they don't use actual webhook URLs
|
||||
// but still need database entries for the polling services to find them
|
||||
let finalPath = path
|
||||
if (!path || path.trim() === '') {
|
||||
// List of providers that use credential-based polling instead of webhooks
|
||||
const credentialBasedProviders = ['gmail', 'outlook']
|
||||
|
||||
if (credentialBasedProviders.includes(provider)) {
|
||||
finalPath = `${provider}-${crypto.randomUUID()}`
|
||||
logger.info(`[${requestId}] Generated dummy path for ${provider} trigger: ${finalPath}`)
|
||||
} else {
|
||||
logger.warn(`[${requestId}] Missing path for webhook creation`, {
|
||||
hasWorkflowId: !!workflowId,
|
||||
hasPath: !!path,
|
||||
})
|
||||
return NextResponse.json({ error: 'Missing required path' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the workflow exists and user has permission to modify it
|
||||
const workflowData = await db
|
||||
.select({
|
||||
@@ -144,29 +164,32 @@ export async function POST(request: NextRequest) {
|
||||
const existingWebhooks = await db
|
||||
.select({ id: webhook.id, workflowId: webhook.workflowId })
|
||||
.from(webhook)
|
||||
.where(eq(webhook.path, path))
|
||||
.where(eq(webhook.path, finalPath))
|
||||
.limit(1)
|
||||
|
||||
let savedWebhook: any = null // Variable to hold the result of save/update
|
||||
|
||||
// If a webhook with the same path exists but belongs to a different workflow, return an error
|
||||
if (existingWebhooks.length > 0 && existingWebhooks[0].workflowId !== workflowId) {
|
||||
logger.warn(`[${requestId}] Webhook path conflict: ${path}`)
|
||||
logger.warn(`[${requestId}] Webhook path conflict: ${finalPath}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Webhook path already exists.', code: 'PATH_EXISTS' },
|
||||
{ status: 409 }
|
||||
)
|
||||
}
|
||||
|
||||
// Use the original provider config - Gmail/Outlook configuration functions will inject userId automatically
|
||||
const finalProviderConfig = providerConfig
|
||||
|
||||
// If a webhook with the same path and workflowId exists, update it
|
||||
if (existingWebhooks.length > 0 && existingWebhooks[0].workflowId === workflowId) {
|
||||
logger.info(`[${requestId}] Updating existing webhook for path: ${path}`)
|
||||
logger.info(`[${requestId}] Updating existing webhook for path: ${finalPath}`)
|
||||
const updatedResult = await db
|
||||
.update(webhook)
|
||||
.set({
|
||||
blockId,
|
||||
provider,
|
||||
providerConfig,
|
||||
providerConfig: finalProviderConfig,
|
||||
isActive: true,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
@@ -183,9 +206,9 @@ export async function POST(request: NextRequest) {
|
||||
id: webhookId,
|
||||
workflowId,
|
||||
blockId,
|
||||
path,
|
||||
path: finalPath,
|
||||
provider,
|
||||
providerConfig,
|
||||
providerConfig: finalProviderConfig,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
||||
@@ -327,11 +327,14 @@ describe('Workflow Execution API Route', () => {
|
||||
expect(executeMock).toHaveBeenCalledWith('workflow-id')
|
||||
|
||||
expect(Executor).toHaveBeenCalledWith(
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
expect.anything(), // decryptedEnvVars
|
||||
requestBody, // processedInput (direct input, not wrapped)
|
||||
expect.anything() // workflowVariables
|
||||
expect.objectContaining({
|
||||
workflow: expect.any(Object), // serializedWorkflow
|
||||
currentBlockStates: expect.any(Object), // processedBlockStates
|
||||
envVarValues: expect.any(Object), // decryptedEnvVars
|
||||
workflowInput: requestBody, // processedInput (direct input, not wrapped)
|
||||
workflowVariables: expect.any(Object),
|
||||
contextExtensions: expect.any(Object), // Allow any context extensions object
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -363,11 +366,14 @@ describe('Workflow Execution API Route', () => {
|
||||
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalledWith(
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
expect.anything(), // decryptedEnvVars
|
||||
structuredInput, // processedInput (direct input, not wrapped)
|
||||
expect.anything() // workflowVariables
|
||||
expect.objectContaining({
|
||||
workflow: expect.any(Object), // serializedWorkflow
|
||||
currentBlockStates: expect.any(Object), // processedBlockStates
|
||||
envVarValues: expect.any(Object), // decryptedEnvVars
|
||||
workflowInput: structuredInput, // processedInput (direct input, not wrapped)
|
||||
workflowVariables: expect.any(Object),
|
||||
contextExtensions: expect.any(Object), // Allow any context extensions object
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -391,11 +397,14 @@ describe('Workflow Execution API Route', () => {
|
||||
|
||||
const Executor = (await import('@/executor')).Executor
|
||||
expect(Executor).toHaveBeenCalledWith(
|
||||
expect.anything(), // serializedWorkflow
|
||||
expect.anything(), // processedBlockStates
|
||||
expect.anything(), // decryptedEnvVars
|
||||
expect.objectContaining({}), // processedInput with empty input
|
||||
expect.anything() // workflowVariables
|
||||
expect.objectContaining({
|
||||
workflow: expect.any(Object), // serializedWorkflow
|
||||
currentBlockStates: expect.any(Object), // processedBlockStates
|
||||
envVarValues: expect.any(Object), // decryptedEnvVars
|
||||
workflowInput: expect.objectContaining({}), // processedInput with empty input
|
||||
workflowVariables: expect.any(Object),
|
||||
contextExtensions: expect.any(Object), // Allow any context extensions object
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -585,8 +594,13 @@ describe('Workflow Execution API Route', () => {
|
||||
expect(executorCalls.length).toBeGreaterThan(0)
|
||||
|
||||
const lastCall = executorCalls[executorCalls.length - 1]
|
||||
expect(lastCall.length).toBeGreaterThanOrEqual(5)
|
||||
expect(lastCall.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
expect(lastCall[4]).toEqual(workflowVariables)
|
||||
// Check that workflowVariables are passed in the options object
|
||||
expect(lastCall[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
workflowVariables: workflowVariables,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -278,13 +278,17 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any): P
|
||||
true // Enable validation during execution
|
||||
)
|
||||
|
||||
const executor = new Executor(
|
||||
serializedWorkflow,
|
||||
processedBlockStates,
|
||||
decryptedEnvVars,
|
||||
processedInput,
|
||||
workflowVariables
|
||||
)
|
||||
const executor = new Executor({
|
||||
workflow: serializedWorkflow,
|
||||
currentBlockStates: processedBlockStates,
|
||||
envVarValues: decryptedEnvVars,
|
||||
workflowInput: processedInput,
|
||||
workflowVariables,
|
||||
contextExtensions: {
|
||||
executionId,
|
||||
workspaceId: workflow.workspaceId,
|
||||
},
|
||||
})
|
||||
|
||||
// Set up logging on the executor
|
||||
loggingSession.setupExecutor(executor)
|
||||
|
||||
@@ -51,6 +51,7 @@ const BlockStateSchema = z.object({
|
||||
isWide: z.boolean().optional(),
|
||||
height: z.number().optional(),
|
||||
advancedMode: z.boolean().optional(),
|
||||
triggerMode: z.boolean().optional(),
|
||||
data: BlockDataSchema.optional(),
|
||||
})
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ export async function POST(req: NextRequest) {
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: 95,
|
||||
},
|
||||
},
|
||||
@@ -178,6 +179,7 @@ export async function POST(req: NextRequest) {
|
||||
horizontalHandles: true,
|
||||
isWide: false,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: '95',
|
||||
subBlocks: {
|
||||
startWorkflow: {
|
||||
|
||||
@@ -142,7 +142,7 @@ export function UploadModal({
|
||||
try {
|
||||
await uploadFiles(files, knowledgeBaseId, {
|
||||
chunkSize: chunkingConfig?.maxSize || 1024,
|
||||
minCharactersPerChunk: chunkingConfig?.minSize || 100,
|
||||
minCharactersPerChunk: chunkingConfig?.minSize || 1,
|
||||
chunkOverlap: chunkingConfig?.overlap || 200,
|
||||
recipe: 'default',
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ const FormSchema = z
|
||||
description: z.string().max(500, 'Description must be less than 500 characters').optional(),
|
||||
minChunkSize: z
|
||||
.number()
|
||||
.min(50, 'Min chunk size must be at least 50')
|
||||
.min(1, 'Min chunk size must be at least 1')
|
||||
.max(2000, 'Min chunk size must be less than 2000'),
|
||||
maxChunkSize: z
|
||||
.number()
|
||||
@@ -115,7 +115,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
minChunkSize: 100,
|
||||
minChunkSize: 1,
|
||||
maxChunkSize: 1024,
|
||||
overlapSize: 200,
|
||||
},
|
||||
@@ -299,7 +299,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
reset({
|
||||
name: '',
|
||||
description: '',
|
||||
minChunkSize: 100,
|
||||
minChunkSize: 1,
|
||||
maxChunkSize: 1024,
|
||||
overlapSize: 200,
|
||||
})
|
||||
@@ -423,7 +423,7 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
<Input
|
||||
id='minChunkSize'
|
||||
type='number'
|
||||
placeholder='100'
|
||||
placeholder='1'
|
||||
{...register('minChunkSize', { valueAsNumber: true })}
|
||||
className={errors.minChunkSize ? 'border-red-500' : ''}
|
||||
autoComplete='off'
|
||||
|
||||
@@ -278,7 +278,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
|
||||
})),
|
||||
processingOptions: {
|
||||
chunkSize: processingOptions.chunkSize || 1024,
|
||||
minCharactersPerChunk: processingOptions.minCharactersPerChunk || 100,
|
||||
minCharactersPerChunk: processingOptions.minCharactersPerChunk || 1,
|
||||
chunkOverlap: processingOptions.chunkOverlap || 200,
|
||||
recipe: processingOptions.recipe || 'default',
|
||||
lang: 'en',
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Download, Loader2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('FileDownload')
|
||||
|
||||
interface FileDownloadProps {
|
||||
file: {
|
||||
id?: string
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
key: string
|
||||
url: string
|
||||
uploadedAt: string
|
||||
expiresAt: string
|
||||
storageProvider?: 's3' | 'blob' | 'local'
|
||||
bucketName?: string
|
||||
}
|
||||
isExecutionFile?: boolean // Flag to indicate this is an execution file
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function FileDownload({ file, isExecutionFile = false, className }: FileDownloadProps) {
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (isDownloading) return
|
||||
|
||||
setIsDownloading(true)
|
||||
|
||||
try {
|
||||
logger.info(`Initiating download for file: ${file.name}`)
|
||||
|
||||
// Generate a fresh download URL
|
||||
const response = await fetch('/api/files/download', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
key: file.key,
|
||||
name: file.name,
|
||||
storageProvider: file.storageProvider,
|
||||
bucketName: file.bucketName,
|
||||
isExecutionFile, // Add flag to indicate execution file
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: response.statusText }))
|
||||
throw new Error(errorData.error || `Failed to generate download URL: ${response.status}`)
|
||||
}
|
||||
|
||||
const { downloadUrl, fileName } = await response.json()
|
||||
|
||||
// Open the download URL in a new tab
|
||||
window.open(downloadUrl, '_blank')
|
||||
|
||||
logger.info(`Download initiated for file: ${fileName}`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to download file ${file.name}:`, error)
|
||||
} finally {
|
||||
setIsDownloading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className={`h-7 px-2 text-xs ${className}`}
|
||||
onClick={handleDownload}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<Loader2 className='h-3 w-3 animate-spin' />
|
||||
) : (
|
||||
<Download className='h-3 w-3' />
|
||||
)}
|
||||
{isDownloading ? 'Downloading...' : 'Download'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
||||
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
|
||||
import { redactApiKeys } from '@/lib/utils'
|
||||
import { FrozenCanvasModal } from '@/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas-modal'
|
||||
import { FileDownload } from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/file-download'
|
||||
import LogMarkdownRenderer from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer'
|
||||
import { ToolCallsDisplay } from '@/app/workspace/[workspaceId]/logs/components/tool-calls/tool-calls-display'
|
||||
import { TraceSpansDisplay } from '@/app/workspace/[workspaceId]/logs/components/trace-spans/trace-spans-display'
|
||||
@@ -489,6 +490,36 @@ export function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Files */}
|
||||
{log.files && log.files.length > 0 && (
|
||||
<div>
|
||||
<h3 className='mb-1 font-medium text-muted-foreground text-xs'>
|
||||
Files ({log.files.length})
|
||||
</h3>
|
||||
<div className='space-y-2'>
|
||||
{log.files.map((file, index) => (
|
||||
<div
|
||||
key={file.id || index}
|
||||
className='flex items-center justify-between rounded-md border bg-muted/30 p-2'
|
||||
>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='truncate font-medium text-sm' title={file.name}>
|
||||
{file.name}
|
||||
</div>
|
||||
<div className='text-muted-foreground text-xs'>
|
||||
{file.size ? `${Math.round(file.size / 1024)}KB` : 'Unknown size'}
|
||||
{file.type && ` • ${file.type.split('/')[0]}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-2 flex items-center gap-1'>
|
||||
<FileDownload file={file} isExecutionFile={true} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Frozen Canvas Button - only show for workflow execution logs with execution ID */}
|
||||
{isWorkflowExecutionLog && log.executionId && (
|
||||
<div>
|
||||
|
||||
@@ -19,9 +19,18 @@ import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useChatStore } from '@/stores/panel/chat/store'
|
||||
import { useConsoleStore } from '@/stores/panel/console/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { ChatFileUpload } from './components/chat-file-upload'
|
||||
|
||||
const logger = createLogger('ChatPanel')
|
||||
|
||||
interface ChatFile {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
file: File
|
||||
}
|
||||
|
||||
interface ChatProps {
|
||||
panelWidth: number
|
||||
chatMessage: string
|
||||
@@ -51,6 +60,11 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
|
||||
const [promptHistory, setPromptHistory] = useState<string[]>([])
|
||||
const [historyIndex, setHistoryIndex] = useState(-1)
|
||||
|
||||
// File upload state
|
||||
const [chatFiles, setChatFiles] = useState<ChatFile[]>([])
|
||||
const [isUploadingFiles, setIsUploadingFiles] = useState(false)
|
||||
const [dragCounter, setDragCounter] = useState(0)
|
||||
const isDragOver = dragCounter > 0
|
||||
// Scroll state
|
||||
const [isNearBottom, setIsNearBottom] = useState(true)
|
||||
const [showScrollButton, setShowScrollButton] = useState(false)
|
||||
@@ -211,13 +225,22 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
|
||||
|
||||
// Handle send message
|
||||
const handleSendMessage = useCallback(async () => {
|
||||
if (!chatMessage.trim() || !activeWorkflowId || isExecuting) return
|
||||
if (
|
||||
(!chatMessage.trim() && chatFiles.length === 0) ||
|
||||
!activeWorkflowId ||
|
||||
isExecuting ||
|
||||
isUploadingFiles
|
||||
)
|
||||
return
|
||||
|
||||
// Store the message being sent for reference
|
||||
const sentMessage = chatMessage.trim()
|
||||
|
||||
// Add to prompt history if it's not already the most recent
|
||||
if (promptHistory.length === 0 || promptHistory[promptHistory.length - 1] !== sentMessage) {
|
||||
if (
|
||||
sentMessage &&
|
||||
(promptHistory.length === 0 || promptHistory[promptHistory.length - 1] !== sentMessage)
|
||||
) {
|
||||
setPromptHistory((prev) => [...prev, sentMessage])
|
||||
}
|
||||
|
||||
@@ -232,23 +255,46 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
|
||||
|
||||
// Get the conversationId for this workflow before adding the message
|
||||
const conversationId = getConversationId(activeWorkflowId)
|
||||
let result: any = null
|
||||
|
||||
// Add user message
|
||||
addMessage({
|
||||
content: sentMessage,
|
||||
workflowId: activeWorkflowId,
|
||||
type: 'user',
|
||||
})
|
||||
try {
|
||||
// Add user message
|
||||
addMessage({
|
||||
content:
|
||||
sentMessage || (chatFiles.length > 0 ? `Uploaded ${chatFiles.length} file(s)` : ''),
|
||||
workflowId: activeWorkflowId,
|
||||
type: 'user',
|
||||
})
|
||||
|
||||
// Clear input and refocus immediately
|
||||
setChatMessage('')
|
||||
focusInput(10)
|
||||
// Prepare workflow input
|
||||
const workflowInput: any = {
|
||||
input: sentMessage,
|
||||
conversationId: conversationId,
|
||||
}
|
||||
|
||||
// Execute the workflow to generate a response, passing the chat message and conversationId as input
|
||||
const result = await handleRunWorkflow({
|
||||
input: sentMessage,
|
||||
conversationId: conversationId,
|
||||
})
|
||||
// Add files if any (pass the File objects directly)
|
||||
if (chatFiles.length > 0) {
|
||||
workflowInput.files = chatFiles.map((chatFile) => ({
|
||||
name: chatFile.name,
|
||||
size: chatFile.size,
|
||||
type: chatFile.type,
|
||||
file: chatFile.file, // Pass the actual File object
|
||||
}))
|
||||
}
|
||||
|
||||
// Clear input and files, refocus immediately
|
||||
setChatMessage('')
|
||||
setChatFiles([])
|
||||
focusInput(10)
|
||||
|
||||
// Execute the workflow to generate a response
|
||||
result = await handleRunWorkflow(workflowInput)
|
||||
} catch (error) {
|
||||
logger.error('Error in handleSendMessage:', error)
|
||||
setIsUploadingFiles(false)
|
||||
// You might want to show an error message to the user here
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we got a streaming response
|
||||
if (result && 'stream' in result && result.stream instanceof ReadableStream) {
|
||||
@@ -541,7 +587,57 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
|
||||
</div>
|
||||
|
||||
{/* Input section - Fixed height */}
|
||||
<div className='-mt-[1px] relative flex-nonept-3 pb-4'>
|
||||
<div
|
||||
className='-mt-[1px] relative flex-none pt-3 pb-4'
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!(!activeWorkflowId || isExecuting || isUploadingFiles)) {
|
||||
setDragCounter((prev) => prev + 1)
|
||||
}
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!(!activeWorkflowId || isExecuting || isUploadingFiles)) {
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragCounter((prev) => Math.max(0, prev - 1))
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragCounter(0)
|
||||
if (!(!activeWorkflowId || isExecuting || isUploadingFiles)) {
|
||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
||||
if (droppedFiles.length > 0) {
|
||||
const newFiles = droppedFiles.slice(0, 5 - chatFiles.length).map((file) => ({
|
||||
id: crypto.randomUUID(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
file,
|
||||
}))
|
||||
setChatFiles([...chatFiles, ...newFiles])
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* File upload section */}
|
||||
<div className='mb-2'>
|
||||
<ChatFileUpload
|
||||
files={chatFiles}
|
||||
onFilesChange={setChatFiles}
|
||||
maxFiles={5}
|
||||
maxSize={10}
|
||||
disabled={!activeWorkflowId || isExecuting || isUploadingFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
@@ -551,14 +647,23 @@ export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
|
||||
setHistoryIndex(-1) // Reset history index when typing
|
||||
}}
|
||||
onKeyDown={handleKeyPress}
|
||||
placeholder='Type a message...'
|
||||
className='h-9 flex-1 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground shadow-xs focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-[#414141] dark:bg-[#202020]'
|
||||
disabled={!activeWorkflowId || isExecuting}
|
||||
placeholder={isDragOver ? 'Drop files here...' : 'Type a message...'}
|
||||
className={`h-9 flex-1 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] text-muted-foreground shadow-xs focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-[#414141] dark:bg-[#202020] ${
|
||||
isDragOver
|
||||
? 'border-[#802FFF] bg-purple-50/50 dark:border-[#802FFF] dark:bg-purple-950/20'
|
||||
: ''
|
||||
}`}
|
||||
disabled={!activeWorkflowId || isExecuting || isUploadingFiles}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSendMessage}
|
||||
size='icon'
|
||||
disabled={!chatMessage.trim() || !activeWorkflowId || isExecuting}
|
||||
disabled={
|
||||
(!chatMessage.trim() && chatFiles.length === 0) ||
|
||||
!activeWorkflowId ||
|
||||
isExecuting ||
|
||||
isUploadingFiles
|
||||
}
|
||||
className='h-9 w-9 rounded-lg bg-[#802FFF] text-white shadow-[0_0_0_0_#802FFF] transition-all duration-200 hover:bg-[#7028E6] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
|
||||
>
|
||||
<ArrowUp className='h-4 w-4' />
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { File, FileText, Image, Paperclip, X } from 'lucide-react'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('ChatFileUpload')
|
||||
|
||||
interface ChatFile {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
file: File
|
||||
}
|
||||
|
||||
interface ChatFileUploadProps {
|
||||
files: ChatFile[]
|
||||
onFilesChange: (files: ChatFile[]) => void
|
||||
maxFiles?: number
|
||||
maxSize?: number // in MB
|
||||
acceptedTypes?: string[]
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function ChatFileUpload({
|
||||
files,
|
||||
onFilesChange,
|
||||
maxFiles = 5,
|
||||
maxSize = 10,
|
||||
acceptedTypes = ['*'],
|
||||
disabled = false,
|
||||
}: ChatFileUploadProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleFileSelect = (selectedFiles: FileList | null) => {
|
||||
if (!selectedFiles || disabled) return
|
||||
|
||||
const newFiles: ChatFile[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
for (let i = 0; i < selectedFiles.length; i++) {
|
||||
const file = selectedFiles[i]
|
||||
|
||||
// Check file count limit
|
||||
if (files.length + newFiles.length >= maxFiles) {
|
||||
errors.push(`Maximum ${maxFiles} files allowed`)
|
||||
break
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (file.size > maxSize * 1024 * 1024) {
|
||||
errors.push(`${file.name} is too large (max ${maxSize}MB)`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check file type if specified
|
||||
if (acceptedTypes.length > 0 && !acceptedTypes.includes('*')) {
|
||||
const isAccepted = acceptedTypes.some((type) => {
|
||||
if (type.endsWith('/*')) {
|
||||
return file.type.startsWith(type.slice(0, -1))
|
||||
}
|
||||
return file.type === type
|
||||
})
|
||||
|
||||
if (!isAccepted) {
|
||||
errors.push(`${file.name} type not supported`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
const isDuplicate = files.some(
|
||||
(existingFile) => existingFile.name === file.name && existingFile.size === file.size
|
||||
)
|
||||
|
||||
if (isDuplicate) {
|
||||
errors.push(`${file.name} already added`)
|
||||
continue
|
||||
}
|
||||
|
||||
newFiles.push({
|
||||
id: crypto.randomUUID(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
file,
|
||||
})
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
logger.warn('File upload errors:', errors)
|
||||
// You could show these errors in a toast or alert
|
||||
}
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
onFilesChange([...files, ...newFiles])
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
onFilesChange(files.filter((f) => f.id !== fileId))
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!disabled) {
|
||||
setIsDragOver(true)
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!disabled) {
|
||||
setIsDragOver(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsDragOver(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsDragOver(false)
|
||||
if (!disabled) {
|
||||
handleFileSelect(e.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
if (type.startsWith('image/')) return <Image className='h-4 w-4' />
|
||||
if (type.includes('text') || type.includes('json')) return <FileText className='h-4 w-4' />
|
||||
return <File className='h-4 w-4' />
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
{/* File Upload Button */}
|
||||
<div className='flex items-center gap-2'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={disabled || files.length >= maxFiles}
|
||||
className='flex items-center gap-1 rounded-md px-2 py-1 text-gray-600 text-sm transition-colors hover:bg-gray-100 hover:text-gray-800 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
title={files.length >= maxFiles ? `Maximum ${maxFiles} files` : 'Attach files'}
|
||||
>
|
||||
<Paperclip className='h-4 w-4' />
|
||||
<span className='hidden sm:inline'>Attach</span>
|
||||
</button>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
multiple
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
className='hidden'
|
||||
accept={acceptedTypes.join(',')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{files.length > 0 && (
|
||||
<span className='text-gray-500 text-xs'>
|
||||
{files.length}/{maxFiles} files
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File List */}
|
||||
{files.length > 0 && (
|
||||
<div className='space-y-1'>
|
||||
{files.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className='flex items-center gap-2 rounded-md bg-gray-50 px-2 py-1 text-sm'
|
||||
>
|
||||
{getFileIcon(file.type)}
|
||||
<span className='flex-1 truncate' title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<span className='text-gray-500 text-xs'>{formatFileSize(file.size)}</span>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => handleRemoveFile(file.id)}
|
||||
className='p-0.5 text-gray-400 transition-colors hover:text-red-500'
|
||||
title='Remove file'
|
||||
>
|
||||
<X className='h-3 w-3' />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Drag and Drop Area (when dragging) */}
|
||||
{isDragOver && (
|
||||
<div
|
||||
className='fixed inset-0 z-50 flex items-center justify-center border-2 border-blue-500 border-dashed bg-blue-500/10'
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className='rounded-lg bg-white p-4 shadow-lg'>
|
||||
<p className='font-medium text-blue-600'>Drop files here to attach</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user