Compare commits

..

2 Commits

Author SHA1 Message Date
Siddharth Ganesan
691cff9790 Remove comments 2026-01-29 10:31:16 -08:00
Siddharth Ganesan
85130f47f4 Fix deactivation 2026-01-29 10:29:34 -08:00
18 changed files with 29 additions and 1661 deletions

View File

@@ -5113,21 +5113,3 @@ export function PulseIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function SimilarwebIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
role='img'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
height='24'
width='24'
>
<path
d='M22.099 5.781c-1.283 -2 -3.14 -3.67 -5.27 -4.52l-0.63 -0.213a7.433 7.433 0 0 0 -2.15 -0.331c-2.307 0.01 -4.175 1.92 -4.175 4.275a4.3 4.3 0 0 0 0.867 2.602l-0.26 -0.342c0.124 0.186 0.26 0.37 0.417 0.556 0.663 0.802 1.604 1.635 2.822 2.58 2.999 2.32 4.943 4.378 5.104 6.93 0.038 0.344 0.062 0.696 0.062 1.051 0 1.297 -0.283 2.67 -0.764 3.635h0.005s-0.207 0.377 -0.077 0.487c0.066 0.057 0.21 0.1 0.46 -0.053a12.104 12.104 0 0 0 3.4 -3.33 12.111 12.111 0 0 0 2.088 -6.635 12.098 12.098 0 0 0 -1.9 -6.692zm-9.096 8.718 -1.878 -1.55c-3.934 -2.87 -5.98 -5.966 -4.859 -9.783a8.73 8.73 0 0 1 0.37 -1.016v-0.004s0.278 -0.583 -0.327 -0.295a12.067 12.067 0 0 0 -6.292 9.975 12.11 12.11 0 0 0 2.053 7.421 9.394 9.394 0 0 0 2.154 2.168H4.22c4.148 3.053 7.706 1.446 7.706 1.446h0.003a4.847 4.847 0 0 0 2.962 -4.492 4.855 4.855 0 0 0 -1.889 -3.87z'
fill='currentColor'
/>
</svg>
)
}

View File

@@ -100,7 +100,6 @@ import {
ServiceNowIcon,
SftpIcon,
ShopifyIcon,
SimilarwebIcon,
SlackIcon,
SmtpIcon,
SQSIcon,
@@ -229,7 +228,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
sftp: SftpIcon,
sharepoint: MicrosoftSharepointIcon,
shopify: ShopifyIcon,
similarweb: SimilarwebIcon,
slack: SlackIcon,
smtp: SmtpIcon,
sqs: SQSIcon,

View File

@@ -96,7 +96,6 @@
"sftp",
"sharepoint",
"shopify",
"similarweb",
"slack",
"smtp",
"sqs",

View File

@@ -1,183 +0,0 @@
---
title: Similarweb
description: Website traffic and analytics data
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="similarweb"
color="#000922"
/>
{/* MANUAL-CONTENT-START:intro */}
[Similarweb](https://www.similarweb.com/) is a leading platform for web analytics that provides in-depth traffic and engagement data for millions of websites. Similarweb gives you insights into website visits, traffic sources, audience behavior, and competitive benchmarks.
With Similarweb in Sim, your agents can:
- **Analyze website traffic**: Retrieve key metrics such as monthly visits, average duration, bounce rates, and top countries.
- **Understand audience engagement**: Gain insights into how users interact with websites, including pages per visit and engagement duration.
- **Track rankings and performance**: Access global, country, and category ranks to benchmark sites against competitors.
- **Discover traffic sources**: Break down traffic by channels like direct, search, social, referrals, and more.
Use Sim's Similarweb integration to automate the monitoring of competitors, track your sites performance, or surface actionable market research—all integrated directly into your workflows and automations. Empower your agents to access and utilize reliable web analytics data easily and programmatically.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Access comprehensive website analytics including traffic estimates, engagement metrics, rankings, and traffic sources using the Similarweb API.
## Tools
### `similarweb_website_overview`
Get comprehensive website analytics including traffic, rankings, engagement, and traffic sources
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | SimilarWeb API key |
| `domain` | string | Yes | Website domain to analyze \(without www or protocol\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `siteName` | string | Website name |
| `description` | string | Website description |
| `globalRank` | number | Global traffic rank |
| `countryRank` | number | Country traffic rank |
| `categoryRank` | number | Category traffic rank |
| `category` | string | Website category |
| `monthlyVisits` | number | Estimated monthly visits |
| `engagementVisitDuration` | number | Average visit duration in seconds |
| `engagementPagesPerVisit` | number | Average pages per visit |
| `engagementBounceRate` | number | Bounce rate \(0-1\) |
| `topCountries` | array | Top countries by traffic share |
| ↳ `country` | string | Country code |
| ↳ `share` | number | Traffic share \(0-1\) |
| `trafficSources` | json | Traffic source breakdown |
| ↳ `direct` | number | Direct traffic share |
| ↳ `referrals` | number | Referral traffic share |
| ↳ `search` | number | Search traffic share |
| ↳ `social` | number | Social traffic share |
| ↳ `mail` | number | Email traffic share |
| ↳ `paidReferrals` | number | Paid referral traffic share |
### `similarweb_traffic_visits`
Get total website visits over time (desktop and mobile combined)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | SimilarWeb API key |
| `domain` | string | Yes | Website domain to analyze \(without www or protocol\) |
| `country` | string | Yes | 2-letter ISO country code or "world" for worldwide data |
| `granularity` | string | Yes | Data granularity: daily, weekly, or monthly |
| `startDate` | string | No | Start date in YYYY-MM format |
| `endDate` | string | No | End date in YYYY-MM format |
| `mainDomainOnly` | boolean | No | Exclude subdomains from results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domain` | string | Analyzed domain |
| `country` | string | Country filter applied |
| `granularity` | string | Data granularity |
| `lastUpdated` | string | Data last updated timestamp |
| `visits` | array | Visit data over time |
| ↳ `date` | string | Date \(YYYY-MM-DD\) |
| ↳ `visits` | number | Number of visits |
### `similarweb_bounce_rate`
Get website bounce rate over time (desktop and mobile combined)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | SimilarWeb API key |
| `domain` | string | Yes | Website domain to analyze \(without www or protocol\) |
| `country` | string | Yes | 2-letter ISO country code or "world" for worldwide data |
| `granularity` | string | Yes | Data granularity: daily, weekly, or monthly |
| `startDate` | string | No | Start date in YYYY-MM format |
| `endDate` | string | No | End date in YYYY-MM format |
| `mainDomainOnly` | boolean | No | Exclude subdomains from results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domain` | string | Analyzed domain |
| `country` | string | Country filter applied |
| `granularity` | string | Data granularity |
| `lastUpdated` | string | Data last updated timestamp |
| `bounceRate` | array | Bounce rate data over time |
| ↳ `date` | string | Date \(YYYY-MM-DD\) |
| ↳ `bounceRate` | number | Bounce rate \(0-1\) |
### `similarweb_pages_per_visit`
Get average pages per visit over time (desktop and mobile combined)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | SimilarWeb API key |
| `domain` | string | Yes | Website domain to analyze \(without www or protocol\) |
| `country` | string | Yes | 2-letter ISO country code or "world" for worldwide data |
| `granularity` | string | Yes | Data granularity: daily, weekly, or monthly |
| `startDate` | string | No | Start date in YYYY-MM format |
| `endDate` | string | No | End date in YYYY-MM format |
| `mainDomainOnly` | boolean | No | Exclude subdomains from results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domain` | string | Analyzed domain |
| `country` | string | Country filter applied |
| `granularity` | string | Data granularity |
| `lastUpdated` | string | Data last updated timestamp |
| `pagesPerVisit` | array | Pages per visit data over time |
| ↳ `date` | string | Date \(YYYY-MM-DD\) |
| ↳ `pagesPerVisit` | number | Average pages per visit |
### `similarweb_visit_duration`
Get average desktop visit duration over time (in seconds)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | SimilarWeb API key |
| `domain` | string | Yes | Website domain to analyze \(without www or protocol\) |
| `country` | string | Yes | 2-letter ISO country code or "world" for worldwide data |
| `granularity` | string | Yes | Data granularity: daily, weekly, or monthly |
| `startDate` | string | No | Start date in YYYY-MM format |
| `endDate` | string | No | End date in YYYY-MM format |
| `mainDomainOnly` | boolean | No | Exclude subdomains from results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `domain` | string | Analyzed domain |
| `country` | string | Country filter applied |
| `granularity` | string | Data granularity |
| `lastUpdated` | string | Data last updated timestamp |
| `averageVisitDuration` | array | Desktop visit duration data over time |
| ↳ `date` | string | Date \(YYYY-MM-DD\) |
| ↳ `durationSeconds` | number | Average visit duration in seconds |

View File

@@ -26,12 +26,38 @@ In Sim, the YouTube integration enables your agents to programmatically search a
## Usage Instructions
Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments.
Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video captions, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments.
## Tools
### `youtube_captions`
List available caption tracks (subtitles/transcripts) for a YouTube video. Returns information about each caption including language, type, and whether it is auto-generated.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `videoId` | string | Yes | YouTube video ID to get captions for |
| `apiKey` | string | Yes | YouTube API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | array | Array of available caption tracks for the video |
| ↳ `captionId` | string | Caption track ID |
| ↳ `language` | string | Language code of the caption \(e.g., |
| ↳ `name` | string | Name/label of the caption track |
| ↳ `trackKind` | string | Type of caption track: |
| ↳ `lastUpdated` | string | When the caption was last updated |
| ↳ `isCC` | boolean | Whether this is a closed caption track |
| ↳ `isAutoSynced` | boolean | Whether the caption timing was automatically synced |
| ↳ `audioTrackType` | string | Type of audio track this caption is for |
| `totalResults` | number | Total number of caption tracks available |
### `youtube_channel_info`
Get detailed information about a YouTube channel including statistics, branding, and content details.

View File

@@ -1,200 +0,0 @@
import { SimilarwebIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
export const SimilarwebBlock: BlockConfig = {
type: 'similarweb',
name: 'Similarweb',
description: 'Website traffic and analytics data',
longDescription:
'Access comprehensive website analytics including traffic estimates, engagement metrics, rankings, and traffic sources using the Similarweb API.',
docsLink: 'https://developers.similarweb.com/docs/similarweb-web-traffic-api',
category: 'tools',
bgColor: '#000922',
icon: SimilarwebIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Website Overview', id: 'similarweb_website_overview' },
{ label: 'Traffic Visits', id: 'similarweb_traffic_visits' },
{ label: 'Bounce Rate', id: 'similarweb_bounce_rate' },
{ label: 'Pages Per Visit', id: 'similarweb_pages_per_visit' },
{ label: 'Visit Duration (Desktop)', id: 'similarweb_visit_duration' },
],
value: () => 'similarweb_website_overview',
},
{
id: 'domain',
title: 'Domain',
type: 'short-input',
placeholder: 'example.com',
required: true,
},
{
id: 'country',
title: 'Country',
type: 'dropdown',
options: [
{ label: 'Worldwide', id: 'world' },
{ label: 'United States', id: 'us' },
{ label: 'United Kingdom', id: 'gb' },
{ label: 'Germany', id: 'de' },
{ label: 'France', id: 'fr' },
{ label: 'Spain', id: 'es' },
{ label: 'Italy', id: 'it' },
{ label: 'Canada', id: 'ca' },
{ label: 'Australia', id: 'au' },
{ label: 'Japan', id: 'jp' },
{ label: 'Brazil', id: 'br' },
{ label: 'India', id: 'in' },
{ label: 'Netherlands', id: 'nl' },
{ label: 'Poland', id: 'pl' },
{ label: 'Russia', id: 'ru' },
{ label: 'Mexico', id: 'mx' },
{ label: 'South Korea', id: 'kr' },
{ label: 'China', id: 'cn' },
],
value: () => 'world',
condition: {
field: 'operation',
value: 'similarweb_website_overview',
not: true,
},
},
{
id: 'granularity',
title: 'Granularity',
type: 'dropdown',
options: [
{ label: 'Monthly', id: 'monthly' },
{ label: 'Weekly', id: 'weekly' },
{ label: 'Daily', id: 'daily' },
],
value: () => 'monthly',
condition: {
field: 'operation',
value: 'similarweb_website_overview',
not: true,
},
},
{
id: 'startDate',
title: 'Start Date',
type: 'short-input',
placeholder: 'YYYY-MM (e.g., 2024-01)',
condition: {
field: 'operation',
value: 'similarweb_website_overview',
not: true,
},
wandConfig: {
enabled: true,
prompt: `Generate a date in YYYY-MM format based on the user's description.
Examples:
- "this month" -> Current month in YYYY-MM format
- "last month" -> Previous month in YYYY-MM format
- "3 months ago" -> Date 3 months ago in YYYY-MM format
- "beginning of year" -> January of current year (e.g., 2024-01)
Return ONLY the date string in YYYY-MM format - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the start date (e.g., "3 months ago", "last month")...',
generationType: 'timestamp',
},
},
{
id: 'endDate',
title: 'End Date',
type: 'short-input',
placeholder: 'YYYY-MM (e.g., 2024-12)',
condition: {
field: 'operation',
value: 'similarweb_website_overview',
not: true,
},
wandConfig: {
enabled: true,
prompt: `Generate a date in YYYY-MM format based on the user's description.
Examples:
- "this month" -> Current month in YYYY-MM format
- "last month" -> Previous month in YYYY-MM format
- "now" -> Current month in YYYY-MM format
Return ONLY the date string in YYYY-MM format - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the end date (e.g., "this month", "now")...',
generationType: 'timestamp',
},
},
{
id: 'mainDomainOnly',
title: 'Main Domain Only',
type: 'switch',
condition: {
field: 'operation',
value: 'similarweb_website_overview',
not: true,
},
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Similarweb API key',
password: true,
required: true,
},
],
tools: {
access: [
'similarweb_website_overview',
'similarweb_traffic_visits',
'similarweb_bounce_rate',
'similarweb_pages_per_visit',
'similarweb_visit_duration',
],
config: {
tool: (params) => params.operation,
},
},
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
domain: { type: 'string', description: 'Website domain to analyze' },
apiKey: { type: 'string', description: 'Similarweb API key' },
country: { type: 'string', description: '2-letter ISO country code or "world"' },
granularity: { type: 'string', description: 'Data granularity (daily, weekly, monthly)' },
startDate: { type: 'string', description: 'Start date in YYYY-MM format' },
endDate: { type: 'string', description: 'End date in YYYY-MM format' },
mainDomainOnly: { type: 'boolean', description: 'Exclude subdomains from results' },
},
outputs: {
// Website Overview outputs
siteName: { type: 'string', description: 'Website name' },
description: { type: 'string', description: 'Website description' },
globalRank: { type: 'number', description: 'Global traffic rank' },
countryRank: { type: 'number', description: 'Country traffic rank' },
categoryRank: { type: 'number', description: 'Category traffic rank' },
category: { type: 'string', description: 'Website category' },
monthlyVisits: { type: 'number', description: 'Estimated monthly visits' },
engagementVisitDuration: { type: 'number', description: 'Average visit duration (seconds)' },
engagementPagesPerVisit: { type: 'number', description: 'Average pages per visit' },
engagementBounceRate: { type: 'number', description: 'Bounce rate (0-1)' },
topCountries: { type: 'json', description: 'Top countries by traffic share' },
trafficSources: { type: 'json', description: 'Traffic source breakdown' },
// Time series outputs
domain: { type: 'string', description: 'Analyzed domain' },
country: { type: 'string', description: 'Country filter applied' },
granularity: { type: 'string', description: 'Data granularity' },
lastUpdated: { type: 'string', description: 'Data last updated timestamp' },
visits: { type: 'json', description: 'Visit data over time' },
bounceRate: { type: 'json', description: 'Bounce rate data over time' },
pagesPerVisit: { type: 'json', description: 'Pages per visit data over time' },
averageVisitDuration: { type: 'json', description: 'Desktop visit duration data over time' },
},
}

View File

@@ -111,7 +111,6 @@ import { ServiceNowBlock } from '@/blocks/blocks/servicenow'
import { SftpBlock } from '@/blocks/blocks/sftp'
import { SharepointBlock } from '@/blocks/blocks/sharepoint'
import { ShopifyBlock } from '@/blocks/blocks/shopify'
import { SimilarwebBlock } from '@/blocks/blocks/similarweb'
import { SlackBlock } from '@/blocks/blocks/slack'
import { SmtpBlock } from '@/blocks/blocks/smtp'
import { SpotifyBlock } from '@/blocks/blocks/spotify'
@@ -281,7 +280,6 @@ export const registry: Record<string, BlockConfig> = {
sftp: SftpBlock,
sharepoint: SharepointBlock,
shopify: ShopifyBlock,
similarweb: SimilarwebBlock,
slack: SlackBlock,
smtp: SmtpBlock,
spotify: SpotifyBlock,

View File

@@ -5113,21 +5113,3 @@ export function PulseIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function SimilarwebIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
role='img'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
height='24'
width='24'
>
<path
d='M22.099 5.781c-1.283 -2 -3.14 -3.67 -5.27 -4.52l-0.63 -0.213a7.433 7.433 0 0 0 -2.15 -0.331c-2.307 0.01 -4.175 1.92 -4.175 4.275a4.3 4.3 0 0 0 0.867 2.602l-0.26 -0.342c0.124 0.186 0.26 0.37 0.417 0.556 0.663 0.802 1.604 1.635 2.822 2.58 2.999 2.32 4.943 4.378 5.104 6.93 0.038 0.344 0.062 0.696 0.062 1.051 0 1.297 -0.283 2.67 -0.764 3.635h0.005s-0.207 0.377 -0.077 0.487c0.066 0.057 0.21 0.1 0.46 -0.053a12.104 12.104 0 0 0 3.4 -3.33 12.111 12.111 0 0 0 2.088 -6.635 12.098 12.098 0 0 0 -1.9 -6.692zm-9.096 8.718 -1.878 -1.55c-3.934 -2.87 -5.98 -5.966 -4.859 -9.783a8.73 8.73 0 0 1 0.37 -1.016v-0.004s0.278 -0.583 -0.327 -0.295a12.067 12.067 0 0 0 -6.292 9.975 12.11 12.11 0 0 0 2.053 7.421 9.394 9.394 0 0 0 2.154 2.168H4.22c4.148 3.053 7.706 1.446 7.706 1.446h0.003a4.847 4.847 0 0 0 2.962 -4.492 4.855 4.855 0 0 0 -1.889 -3.87z'
fill='currentColor'
/>
</svg>
)
}

View File

@@ -10,7 +10,6 @@ import {
type KnowledgeBaseArgs,
} from '@/lib/copilot/tools/shared/schemas'
import { useCopilotStore } from '@/stores/panel/copilot/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
/**
* Client tool for knowledge base operations
@@ -103,19 +102,7 @@ export class KnowledgeBaseClientTool extends BaseClientTool {
const logger = createLogger('KnowledgeBaseClientTool')
try {
this.setState(ClientToolCallState.executing)
// Get the workspace ID from the workflow registry hydration state
const { hydration } = useWorkflowRegistry.getState()
const workspaceId = hydration.workspaceId
// Build payload with workspace ID included in args
const payload: KnowledgeBaseArgs = {
...(args || { operation: 'list' }),
args: {
...(args?.args || {}),
workspaceId: workspaceId || undefined,
},
}
const payload: KnowledgeBaseArgs = { ...(args || { operation: 'list' }) }
const res = await fetch('/api/copilot/execute-copilot-server-tool', {
method: 'POST',

View File

@@ -2508,10 +2508,6 @@ async function validateWorkflowSelectorIds(
for (const subBlockConfig of blockConfig.subBlocks) {
if (!SELECTOR_TYPES.has(subBlockConfig.type)) continue
// Skip oauth-input - credentials are pre-validated before edit application
// This allows existing collaborator credentials to remain untouched
if (subBlockConfig.type === 'oauth-input') continue
const subBlockValue = blockData.subBlocks?.[subBlockConfig.id]?.value
if (!subBlockValue) continue
@@ -2577,295 +2573,6 @@ async function validateWorkflowSelectorIds(
return errors
}
/**
* Pre-validates credential and apiKey inputs in operations before they are applied.
* - Validates oauth-input (credential) IDs belong to the user
* - Filters out apiKey inputs for hosted models when isHosted is true
* - Also validates credentials and apiKeys in nestedNodes (blocks inside loop/parallel)
* Returns validation errors for any removed inputs.
*/
async function preValidateCredentialInputs(
operations: EditWorkflowOperation[],
context: { userId: string },
workflowState?: Record<string, unknown>
): Promise<{ filteredOperations: EditWorkflowOperation[]; errors: ValidationError[] }> {
const { isHosted } = await import('@/lib/core/config/feature-flags')
const { getHostedModels } = await import('@/providers/utils')
const logger = createLogger('PreValidateCredentials')
const errors: ValidationError[] = []
// Collect credential and apiKey inputs that need validation/filtering
const credentialInputs: Array<{
operationIndex: number
blockId: string
blockType: string
fieldName: string
value: string
nestedBlockId?: string
}> = []
const hostedApiKeyInputs: Array<{
operationIndex: number
blockId: string
blockType: string
model: string
nestedBlockId?: string
}> = []
const hostedModelsLower = isHosted ? new Set(getHostedModels().map((m) => m.toLowerCase())) : null
/**
* Collect credential inputs from a block's inputs based on its block config
*/
function collectCredentialInputs(
blockConfig: ReturnType<typeof getBlock>,
inputs: Record<string, unknown>,
opIndex: number,
blockId: string,
blockType: string,
nestedBlockId?: string
) {
if (!blockConfig) return
for (const subBlockConfig of blockConfig.subBlocks) {
if (subBlockConfig.type !== 'oauth-input') continue
const inputValue = inputs[subBlockConfig.id]
if (!inputValue || typeof inputValue !== 'string' || inputValue.trim() === '') continue
credentialInputs.push({
operationIndex: opIndex,
blockId,
blockType,
fieldName: subBlockConfig.id,
value: inputValue,
nestedBlockId,
})
}
}
/**
* Check if apiKey should be filtered for a block with the given model
*/
function collectHostedApiKeyInput(
inputs: Record<string, unknown>,
modelValue: string | undefined,
opIndex: number,
blockId: string,
blockType: string,
nestedBlockId?: string
) {
if (!hostedModelsLower || !inputs.apiKey) return
if (!modelValue || typeof modelValue !== 'string') return
if (hostedModelsLower.has(modelValue.toLowerCase())) {
hostedApiKeyInputs.push({
operationIndex: opIndex,
blockId,
blockType,
model: modelValue,
nestedBlockId,
})
}
}
operations.forEach((op, opIndex) => {
// Process main block inputs
if (op.params?.inputs && op.params?.type) {
const blockConfig = getBlock(op.params.type)
if (blockConfig) {
// Collect credentials from main block
collectCredentialInputs(
blockConfig,
op.params.inputs as Record<string, unknown>,
opIndex,
op.block_id,
op.params.type
)
// Check for apiKey inputs on hosted models
let modelValue = (op.params.inputs as Record<string, unknown>).model as string | undefined
// For edit operations, if model is not being changed, check existing block's model
if (
!modelValue &&
op.operation_type === 'edit' &&
(op.params.inputs as Record<string, unknown>).apiKey &&
workflowState
) {
const existingBlock = (workflowState.blocks as Record<string, unknown>)?.[op.block_id] as
| Record<string, unknown>
| undefined
const existingSubBlocks = existingBlock?.subBlocks as Record<string, unknown> | undefined
const existingModelSubBlock = existingSubBlocks?.model as
| Record<string, unknown>
| undefined
modelValue = existingModelSubBlock?.value as string | undefined
}
collectHostedApiKeyInput(
op.params.inputs as Record<string, unknown>,
modelValue,
opIndex,
op.block_id,
op.params.type
)
}
}
// Process nested nodes (blocks inside loop/parallel containers)
const nestedNodes = op.params?.nestedNodes as
| Record<string, Record<string, unknown>>
| undefined
if (nestedNodes) {
Object.entries(nestedNodes).forEach(([childId, childBlock]) => {
const childType = childBlock.type as string | undefined
const childInputs = childBlock.inputs as Record<string, unknown> | undefined
if (!childType || !childInputs) return
const childBlockConfig = getBlock(childType)
if (!childBlockConfig) return
// Collect credentials from nested block
collectCredentialInputs(
childBlockConfig,
childInputs,
opIndex,
op.block_id,
childType,
childId
)
// Check for apiKey inputs on hosted models in nested block
const modelValue = childInputs.model as string | undefined
collectHostedApiKeyInput(childInputs, modelValue, opIndex, op.block_id, childType, childId)
})
}
})
const hasCredentialsToValidate = credentialInputs.length > 0
const hasHostedApiKeysToFilter = hostedApiKeyInputs.length > 0
if (!hasCredentialsToValidate && !hasHostedApiKeysToFilter) {
return { filteredOperations: operations, errors }
}
// Deep clone operations so we can modify them
const filteredOperations = structuredClone(operations)
// Filter out apiKey inputs for hosted models and add validation errors
if (hasHostedApiKeysToFilter) {
logger.info('Filtering apiKey inputs for hosted models', { count: hostedApiKeyInputs.length })
for (const apiKeyInput of hostedApiKeyInputs) {
const op = filteredOperations[apiKeyInput.operationIndex]
// Handle nested block apiKey filtering
if (apiKeyInput.nestedBlockId) {
const nestedNodes = op.params?.nestedNodes as
| Record<string, Record<string, unknown>>
| undefined
const nestedBlock = nestedNodes?.[apiKeyInput.nestedBlockId]
const nestedInputs = nestedBlock?.inputs as Record<string, unknown> | undefined
if (nestedInputs?.apiKey) {
nestedInputs.apiKey = undefined
logger.debug('Filtered apiKey for hosted model in nested block', {
parentBlockId: apiKeyInput.blockId,
nestedBlockId: apiKeyInput.nestedBlockId,
model: apiKeyInput.model,
})
errors.push({
blockId: apiKeyInput.nestedBlockId,
blockType: apiKeyInput.blockType,
field: 'apiKey',
value: '[redacted]',
error: `Cannot set API key for hosted model "${apiKeyInput.model}" - API keys are managed by the platform when using hosted models`,
})
}
} else if (op.params?.inputs?.apiKey) {
// Handle main block apiKey filtering
op.params.inputs.apiKey = undefined
logger.debug('Filtered apiKey for hosted model', {
blockId: apiKeyInput.blockId,
model: apiKeyInput.model,
})
errors.push({
blockId: apiKeyInput.blockId,
blockType: apiKeyInput.blockType,
field: 'apiKey',
value: '[redacted]',
error: `Cannot set API key for hosted model "${apiKeyInput.model}" - API keys are managed by the platform when using hosted models`,
})
}
}
}
// Validate credential inputs
if (hasCredentialsToValidate) {
logger.info('Pre-validating credential inputs', {
credentialCount: credentialInputs.length,
userId: context.userId,
})
const allCredentialIds = credentialInputs.map((c) => c.value)
const validationResult = await validateSelectorIds('oauth-input', allCredentialIds, context)
const invalidSet = new Set(validationResult.invalid)
if (invalidSet.size > 0) {
for (const credInput of credentialInputs) {
if (!invalidSet.has(credInput.value)) continue
const op = filteredOperations[credInput.operationIndex]
// Handle nested block credential removal
if (credInput.nestedBlockId) {
const nestedNodes = op.params?.nestedNodes as
| Record<string, Record<string, unknown>>
| undefined
const nestedBlock = nestedNodes?.[credInput.nestedBlockId]
const nestedInputs = nestedBlock?.inputs as Record<string, unknown> | undefined
if (nestedInputs?.[credInput.fieldName]) {
delete nestedInputs[credInput.fieldName]
logger.info('Removed invalid credential from nested block', {
parentBlockId: credInput.blockId,
nestedBlockId: credInput.nestedBlockId,
field: credInput.fieldName,
invalidValue: credInput.value,
})
}
} else if (op.params?.inputs?.[credInput.fieldName]) {
// Handle main block credential removal
delete op.params.inputs[credInput.fieldName]
logger.info('Removed invalid credential from operation', {
blockId: credInput.blockId,
field: credInput.fieldName,
invalidValue: credInput.value,
})
}
const warningInfo = validationResult.warning ? `. ${validationResult.warning}` : ''
const errorBlockId = credInput.nestedBlockId ?? credInput.blockId
errors.push({
blockId: errorBlockId,
blockType: credInput.blockType,
field: credInput.fieldName,
value: credInput.value,
error: `Invalid credential ID "${credInput.value}" - credential does not exist or user doesn't have access${warningInfo}`,
})
}
logger.warn('Filtered out invalid credentials', {
invalidCount: invalidSet.size,
})
}
}
return { filteredOperations, errors }
}
async function getCurrentWorkflowStateFromDb(
workflowId: string
): Promise<{ workflowState: any; subBlockValues: Record<string, Record<string, any>> }> {
@@ -2950,29 +2657,12 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
// Get permission config for the user
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
// Pre-validate credential and apiKey inputs before applying operations
// This filters out invalid credentials and apiKeys for hosted models
let operationsToApply = operations
const credentialErrors: ValidationError[] = []
if (context?.userId) {
const { filteredOperations, errors: credErrors } = await preValidateCredentialInputs(
operations,
{ userId: context.userId },
workflowState
)
operationsToApply = filteredOperations
credentialErrors.push(...credErrors)
}
// Apply operations directly to the workflow state
const {
state: modifiedWorkflowState,
validationErrors,
skippedItems,
} = applyOperationsToWorkflowState(workflowState, operationsToApply, permissionConfig)
// Add credential validation errors
validationErrors.push(...credentialErrors)
} = applyOperationsToWorkflowState(workflowState, operations, permissionConfig)
// Get workspaceId for selector validation
let workspaceId: string | undefined

View File

@@ -1342,13 +1342,6 @@ import {
shopifyUpdateOrderTool,
shopifyUpdateProductTool,
} from '@/tools/shopify'
import {
similarwebBounceRateTool,
similarwebPagesPerVisitTool,
similarwebTrafficVisitsTool,
similarwebVisitDurationTool,
similarwebWebsiteOverviewTool,
} from '@/tools/similarweb'
import {
slackAddReactionTool,
slackCanvasTool,
@@ -1943,11 +1936,6 @@ export const tools: Record<string, ToolConfig> = {
github_latest_commit: githubLatestCommitTool,
github_latest_commit_v2: githubLatestCommitV2Tool,
serper_search: serperSearchTool,
similarweb_website_overview: similarwebWebsiteOverviewTool,
similarweb_traffic_visits: similarwebTrafficVisitsTool,
similarweb_bounce_rate: similarwebBounceRateTool,
similarweb_pages_per_visit: similarwebPagesPerVisitTool,
similarweb_visit_duration: similarwebVisitDurationTool,
servicenow_create_record: servicenowCreateRecordTool,
servicenow_read_record: servicenowReadRecordTool,
servicenow_update_record: servicenowUpdateRecordTool,

View File

@@ -1,139 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { SimilarwebBounceRateParams, SimilarwebBounceRateResponse } from './types'
export const similarwebBounceRateTool: ToolConfig<
SimilarwebBounceRateParams,
SimilarwebBounceRateResponse
> = {
id: 'similarweb_bounce_rate',
name: 'SimilarWeb Bounce Rate',
description: 'Get website bounce rate over time (desktop and mobile combined)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'SimilarWeb API key',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Website domain to analyze (without www or protocol)',
},
country: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: '2-letter ISO country code or "world" for worldwide data',
},
granularity: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Data granularity: daily, weekly, or monthly',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Start date in YYYY-MM format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End date in YYYY-MM format',
},
mainDomainOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Exclude subdomains from results',
},
},
request: {
url: (params) => {
const domain = params.domain
?.trim()
.replace(/^(https?:\/\/)?(www\.)?/, '')
.replace(/\/$/, '')
const url = new URL(
`https://api.similarweb.com/v1/website/${domain}/total-traffic-and-engagement/bounce-rate`
)
url.searchParams.set('api_key', params.apiKey?.trim())
url.searchParams.set('country', params.country?.trim() ?? 'world')
url.searchParams.set('granularity', params.granularity ?? 'monthly')
url.searchParams.set('format', 'json')
if (params.startDate) url.searchParams.set('start_date', params.startDate)
if (params.endDate) url.searchParams.set('end_date', params.endDate)
if (params.mainDomainOnly !== undefined)
url.searchParams.set('main_domain_only', String(params.mainDomainOnly))
return url.toString()
},
method: 'GET',
headers: () => ({
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.message || 'Failed to get bounce rate')
}
const meta = data.meta ?? {}
const request = meta.request ?? {}
return {
success: true,
output: {
domain: request.domain ?? null,
country: request.country ?? null,
granularity: request.granularity ?? null,
lastUpdated: meta.last_updated ?? null,
bounceRate:
data.bounce_rate?.map((b: { date: string; bounce_rate: number }) => ({
date: b.date,
bounceRate: b.bounce_rate,
})) ?? [],
},
}
},
outputs: {
domain: {
type: 'string',
description: 'Analyzed domain',
},
country: {
type: 'string',
description: 'Country filter applied',
},
granularity: {
type: 'string',
description: 'Data granularity',
},
lastUpdated: {
type: 'string',
description: 'Data last updated timestamp',
optional: true,
},
bounceRate: {
type: 'array',
description: 'Bounce rate data over time',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
bounceRate: { type: 'number', description: 'Bounce rate (0-1)' },
},
},
},
},
}

View File

@@ -1,6 +0,0 @@
export { similarwebBounceRateTool } from './bounce_rate'
export { similarwebPagesPerVisitTool } from './pages_per_visit'
export { similarwebTrafficVisitsTool } from './traffic_visits'
export * from './types'
export { similarwebVisitDurationTool } from './visit_duration'
export { similarwebWebsiteOverviewTool } from './website_overview'

View File

@@ -1,139 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { SimilarwebPagesPerVisitParams, SimilarwebPagesPerVisitResponse } from './types'
export const similarwebPagesPerVisitTool: ToolConfig<
SimilarwebPagesPerVisitParams,
SimilarwebPagesPerVisitResponse
> = {
id: 'similarweb_pages_per_visit',
name: 'SimilarWeb Pages Per Visit',
description: 'Get average pages per visit over time (desktop and mobile combined)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'SimilarWeb API key',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Website domain to analyze (without www or protocol)',
},
country: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: '2-letter ISO country code or "world" for worldwide data',
},
granularity: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Data granularity: daily, weekly, or monthly',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Start date in YYYY-MM format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End date in YYYY-MM format',
},
mainDomainOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Exclude subdomains from results',
},
},
request: {
url: (params) => {
const domain = params.domain
?.trim()
.replace(/^(https?:\/\/)?(www\.)?/, '')
.replace(/\/$/, '')
const url = new URL(
`https://api.similarweb.com/v1/website/${domain}/total-traffic-and-engagement/pages-per-visit`
)
url.searchParams.set('api_key', params.apiKey?.trim())
url.searchParams.set('country', params.country?.trim() ?? 'world')
url.searchParams.set('granularity', params.granularity ?? 'monthly')
url.searchParams.set('format', 'json')
if (params.startDate) url.searchParams.set('start_date', params.startDate)
if (params.endDate) url.searchParams.set('end_date', params.endDate)
if (params.mainDomainOnly !== undefined)
url.searchParams.set('main_domain_only', String(params.mainDomainOnly))
return url.toString()
},
method: 'GET',
headers: () => ({
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.message || 'Failed to get pages per visit')
}
const meta = data.meta ?? {}
const request = meta.request ?? {}
return {
success: true,
output: {
domain: request.domain ?? null,
country: request.country ?? null,
granularity: request.granularity ?? null,
lastUpdated: meta.last_updated ?? null,
pagesPerVisit:
data.pages_per_visit?.map((p: { date: string; pages_per_visit: number }) => ({
date: p.date,
pagesPerVisit: p.pages_per_visit,
})) ?? [],
},
}
},
outputs: {
domain: {
type: 'string',
description: 'Analyzed domain',
},
country: {
type: 'string',
description: 'Country filter applied',
},
granularity: {
type: 'string',
description: 'Data granularity',
},
lastUpdated: {
type: 'string',
description: 'Data last updated timestamp',
optional: true,
},
pagesPerVisit: {
type: 'array',
description: 'Pages per visit data over time',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
pagesPerVisit: { type: 'number', description: 'Average pages per visit' },
},
},
},
},
}

View File

@@ -1,139 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { SimilarwebTrafficVisitsParams, SimilarwebTrafficVisitsResponse } from './types'
export const similarwebTrafficVisitsTool: ToolConfig<
SimilarwebTrafficVisitsParams,
SimilarwebTrafficVisitsResponse
> = {
id: 'similarweb_traffic_visits',
name: 'SimilarWeb Traffic Visits',
description: 'Get total website visits over time (desktop and mobile combined)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'SimilarWeb API key',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Website domain to analyze (without www or protocol)',
},
country: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: '2-letter ISO country code or "world" for worldwide data',
},
granularity: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Data granularity: daily, weekly, or monthly',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Start date in YYYY-MM format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End date in YYYY-MM format',
},
mainDomainOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Exclude subdomains from results',
},
},
request: {
url: (params) => {
const domain = params.domain
?.trim()
.replace(/^(https?:\/\/)?(www\.)?/, '')
.replace(/\/$/, '')
const url = new URL(
`https://api.similarweb.com/v1/website/${domain}/total-traffic-and-engagement/visits`
)
url.searchParams.set('api_key', params.apiKey?.trim())
url.searchParams.set('country', params.country?.trim() ?? 'world')
url.searchParams.set('granularity', params.granularity ?? 'monthly')
url.searchParams.set('format', 'json')
if (params.startDate) url.searchParams.set('start_date', params.startDate)
if (params.endDate) url.searchParams.set('end_date', params.endDate)
if (params.mainDomainOnly !== undefined)
url.searchParams.set('main_domain_only', String(params.mainDomainOnly))
return url.toString()
},
method: 'GET',
headers: () => ({
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.message || 'Failed to get traffic visits')
}
const meta = data.meta ?? {}
const request = meta.request ?? {}
return {
success: true,
output: {
domain: request.domain ?? null,
country: request.country ?? null,
granularity: request.granularity ?? null,
lastUpdated: meta.last_updated ?? null,
visits:
data.visits?.map((v: { date: string; visits: number }) => ({
date: v.date,
visits: v.visits,
})) ?? [],
},
}
},
outputs: {
domain: {
type: 'string',
description: 'Analyzed domain',
},
country: {
type: 'string',
description: 'Country filter applied',
},
granularity: {
type: 'string',
description: 'Data granularity',
},
lastUpdated: {
type: 'string',
description: 'Data last updated timestamp',
optional: true,
},
visits: {
type: 'array',
description: 'Visit data over time',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
visits: { type: 'number', description: 'Number of visits' },
},
},
},
},
}

View File

@@ -1,139 +0,0 @@
import type { ToolResponse } from '@/tools/types'
/**
* Common parameters for all SimilarWeb API endpoints
*/
export interface SimilarwebBaseParams {
apiKey: string
domain: string
}
/**
* Parameters for time-series endpoints (visits, bounce rate, etc.)
*/
export interface SimilarwebTimeSeriesParams extends SimilarwebBaseParams {
country: string
granularity: 'daily' | 'weekly' | 'monthly'
startDate?: string
endDate?: string
mainDomainOnly?: boolean
}
/**
* Website Overview (API Lite) parameters
*/
export interface SimilarwebWebsiteOverviewParams extends SimilarwebBaseParams {}
/**
* Website Overview response
*/
export interface SimilarwebWebsiteOverviewResponse extends ToolResponse {
output: {
siteName: string
description: string | null
globalRank: number | null
countryRank: number | null
categoryRank: number | null
category: string | null
monthlyVisits: number | null
engagementVisitDuration: number | null
engagementPagesPerVisit: number | null
engagementBounceRate: number | null
topCountries: Array<{
country: string
share: number
}>
trafficSources: {
direct: number | null
referrals: number | null
search: number | null
social: number | null
mail: number | null
paidReferrals: number | null
}
}
}
/**
* Traffic Visits parameters
*/
export interface SimilarwebTrafficVisitsParams extends SimilarwebTimeSeriesParams {}
/**
* Traffic Visits response
*/
export interface SimilarwebTrafficVisitsResponse extends ToolResponse {
output: {
domain: string
country: string
granularity: string
lastUpdated: string | null
visits: Array<{
date: string
visits: number
}>
}
}
/**
* Bounce Rate parameters
*/
export interface SimilarwebBounceRateParams extends SimilarwebTimeSeriesParams {}
/**
* Bounce Rate response
*/
export interface SimilarwebBounceRateResponse extends ToolResponse {
output: {
domain: string
country: string
granularity: string
lastUpdated: string | null
bounceRate: Array<{
date: string
bounceRate: number
}>
}
}
/**
* Pages Per Visit parameters
*/
export interface SimilarwebPagesPerVisitParams extends SimilarwebTimeSeriesParams {}
/**
* Pages Per Visit response
*/
export interface SimilarwebPagesPerVisitResponse extends ToolResponse {
output: {
domain: string
country: string
granularity: string
lastUpdated: string | null
pagesPerVisit: Array<{
date: string
pagesPerVisit: number
}>
}
}
/**
* Average Visit Duration parameters
*/
export interface SimilarwebVisitDurationParams extends SimilarwebTimeSeriesParams {}
/**
* Average Visit Duration response
*/
export interface SimilarwebVisitDurationResponse extends ToolResponse {
output: {
domain: string
country: string
granularity: string
lastUpdated: string | null
averageVisitDuration: Array<{
date: string
durationSeconds: number
}>
}
}

View File

@@ -1,141 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { SimilarwebVisitDurationParams, SimilarwebVisitDurationResponse } from './types'
export const similarwebVisitDurationTool: ToolConfig<
SimilarwebVisitDurationParams,
SimilarwebVisitDurationResponse
> = {
id: 'similarweb_visit_duration',
name: 'SimilarWeb Visit Duration',
description: 'Get average desktop visit duration over time (in seconds)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'SimilarWeb API key',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Website domain to analyze (without www or protocol)',
},
country: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: '2-letter ISO country code or "world" for worldwide data',
},
granularity: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Data granularity: daily, weekly, or monthly',
},
startDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Start date in YYYY-MM format',
},
endDate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End date in YYYY-MM format',
},
mainDomainOnly: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Exclude subdomains from results',
},
},
request: {
url: (params) => {
const domain = params.domain
?.trim()
.replace(/^(https?:\/\/)?(www\.)?/, '')
.replace(/\/$/, '')
const url = new URL(
`https://api.similarweb.com/v1/website/${domain}/traffic-and-engagement/average-visit-duration`
)
url.searchParams.set('api_key', params.apiKey?.trim())
url.searchParams.set('country', params.country?.trim() ?? 'world')
url.searchParams.set('granularity', params.granularity ?? 'monthly')
url.searchParams.set('format', 'json')
if (params.startDate) url.searchParams.set('start_date', params.startDate)
if (params.endDate) url.searchParams.set('end_date', params.endDate)
if (params.mainDomainOnly !== undefined)
url.searchParams.set('main_domain_only', String(params.mainDomainOnly))
return url.toString()
},
method: 'GET',
headers: () => ({
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.message || 'Failed to get visit duration')
}
const meta = data.meta ?? {}
const request = meta.request ?? {}
return {
success: true,
output: {
domain: request.domain ?? null,
country: request.country ?? null,
granularity: request.granularity ?? null,
lastUpdated: meta.last_updated ?? null,
averageVisitDuration:
data.average_visit_duration?.map(
(d: { date: string; average_visit_duration: number }) => ({
date: d.date,
durationSeconds: d.average_visit_duration,
})
) ?? [],
},
}
},
outputs: {
domain: {
type: 'string',
description: 'Analyzed domain',
},
country: {
type: 'string',
description: 'Country filter applied',
},
granularity: {
type: 'string',
description: 'Data granularity',
},
lastUpdated: {
type: 'string',
description: 'Data last updated timestamp',
optional: true,
},
averageVisitDuration: {
type: 'array',
description: 'Desktop visit duration data over time',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
durationSeconds: { type: 'number', description: 'Average visit duration in seconds' },
},
},
},
},
}

View File

@@ -1,196 +0,0 @@
import type { ToolConfig } from '@/tools/types'
import type { SimilarwebWebsiteOverviewParams, SimilarwebWebsiteOverviewResponse } from './types'
export const similarwebWebsiteOverviewTool: ToolConfig<
SimilarwebWebsiteOverviewParams,
SimilarwebWebsiteOverviewResponse
> = {
id: 'similarweb_website_overview',
name: 'SimilarWeb Website Overview',
description:
'Get comprehensive website analytics including traffic, rankings, engagement, and traffic sources',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'SimilarWeb API key',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Website domain to analyze (without www or protocol)',
},
},
request: {
url: (params) => {
const domain = params.domain
?.trim()
.replace(/^(https?:\/\/)?(www\.)?/, '')
.replace(/\/$/, '')
const url = new URL(`https://api.similarweb.com/v1/website/${domain}/general-data/all`)
url.searchParams.set('api_key', params.apiKey?.trim())
url.searchParams.set('format', 'json')
return url.toString()
},
method: 'GET',
headers: () => ({
Accept: 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || data.message || 'Failed to get website overview')
}
const topCountriesRaw = data.TopCountryShares ?? data.top_country_shares ?? []
const topCountries = topCountriesRaw.map(
(c: {
Country?: number
CountryCode?: string
Value?: number
country?: number
country_code?: string
value?: number
}) => ({
country: c.CountryCode ?? c.country_code ?? String(c.Country ?? c.country ?? ''),
share: c.Value ?? c.value ?? 0,
})
)
const sources = data.TrafficSources ?? data.traffic_sources ?? {}
const engagements = data.Engagements ?? data.engagements ?? data.engagments ?? {}
const getGlobalRank = () => {
if (data.GlobalRank?.Rank !== undefined) return data.GlobalRank.Rank
if (data.global_rank?.rank !== undefined) return data.global_rank.rank
if (typeof data.GlobalRank === 'number') return data.GlobalRank
if (typeof data.global_rank === 'number') return data.global_rank
return null
}
const getCountryRank = () => {
if (data.CountryRank?.Rank !== undefined) return data.CountryRank.Rank
if (data.country_rank?.rank !== undefined) return data.country_rank.rank
if (typeof data.CountryRank === 'number') return data.CountryRank
if (typeof data.country_rank === 'number') return data.country_rank
return null
}
const getCategoryRank = () => {
if (data.CategoryRank?.Rank !== undefined) return data.CategoryRank.Rank
if (data.category_rank?.rank !== undefined) return data.category_rank.rank
if (typeof data.CategoryRank === 'number') return data.CategoryRank
if (typeof data.category_rank === 'number') return data.category_rank
return null
}
return {
success: true,
output: {
siteName: data.SiteName ?? data.site_name ?? null,
description: data.Description ?? data.description ?? null,
globalRank: getGlobalRank(),
countryRank: getCountryRank(),
categoryRank: getCategoryRank(),
category: data.Category ?? data.category ?? null,
monthlyVisits: engagements.Visits ?? engagements.visits ?? null,
engagementVisitDuration: engagements.TimeOnSite ?? engagements.time_on_site ?? null,
engagementPagesPerVisit: engagements.PagePerVisit ?? engagements.page_per_visit ?? null,
engagementBounceRate: engagements.BounceRate ?? engagements.bounce_rate ?? null,
topCountries,
trafficSources: {
direct: sources.Direct ?? sources.direct ?? null,
referrals: sources.Referrals ?? sources.referrals ?? null,
search: sources.Search ?? sources.search ?? null,
social: sources.Social ?? sources.social ?? null,
mail: sources.Mail ?? sources.mail ?? null,
paidReferrals: sources['Paid Referrals'] ?? sources.paid_referrals ?? null,
},
},
}
},
outputs: {
siteName: {
type: 'string',
description: 'Website name',
},
description: {
type: 'string',
description: 'Website description',
optional: true,
},
globalRank: {
type: 'number',
description: 'Global traffic rank',
optional: true,
},
countryRank: {
type: 'number',
description: 'Country traffic rank',
optional: true,
},
categoryRank: {
type: 'number',
description: 'Category traffic rank',
optional: true,
},
category: {
type: 'string',
description: 'Website category',
optional: true,
},
monthlyVisits: {
type: 'number',
description: 'Estimated monthly visits',
optional: true,
},
engagementVisitDuration: {
type: 'number',
description: 'Average visit duration in seconds',
optional: true,
},
engagementPagesPerVisit: {
type: 'number',
description: 'Average pages per visit',
optional: true,
},
engagementBounceRate: {
type: 'number',
description: 'Bounce rate (0-1)',
optional: true,
},
topCountries: {
type: 'array',
description: 'Top countries by traffic share',
items: {
type: 'object',
properties: {
country: { type: 'string', description: 'Country code' },
share: { type: 'number', description: 'Traffic share (0-1)' },
},
},
},
trafficSources: {
type: 'json',
description: 'Traffic source breakdown',
properties: {
direct: { type: 'number', description: 'Direct traffic share' },
referrals: { type: 'number', description: 'Referral traffic share' },
search: { type: 'number', description: 'Search traffic share' },
social: { type: 'number', description: 'Social traffic share' },
mail: { type: 'number', description: 'Email traffic share' },
paidReferrals: { type: 'number', description: 'Paid referral traffic share' },
},
},
},
}