From 3788660366664cb070e13bc4873d0f6897a266ac Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 28 Feb 2026 13:58:21 -0800 Subject: [PATCH 1/3] fix(monitoring): set MemoryTelemetry logger to INFO level for production visibility (#3386) Production defaults to ERROR-only logging. Without this override, memory snapshots would be silently suppressed. --- apps/sim/lib/monitoring/memory-telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/monitoring/memory-telemetry.ts b/apps/sim/lib/monitoring/memory-telemetry.ts index a730e0b976..d9383c6411 100644 --- a/apps/sim/lib/monitoring/memory-telemetry.ts +++ b/apps/sim/lib/monitoring/memory-telemetry.ts @@ -12,7 +12,7 @@ import { getActiveSSEConnectionsByRoute, } from '@/lib/monitoring/sse-connections' -const logger = createLogger('MemoryTelemetry') +const logger = createLogger('MemoryTelemetry', { logLevel: 'INFO' }) const MB = 1024 * 1024 From ee20e119deac25a08860375b4bf17af3add9963c Mon Sep 17 00:00:00 2001 From: Waleed Date: Sat, 28 Feb 2026 18:56:34 -0800 Subject: [PATCH 2/3] feat(integrations): add amplitude, google pagespeed insights, and pagerduty integrations (#3385) * feat(integrations): add amplitude and google pagespeed insights integrations * verified and regen docs * fix icons * fix(integrations): add pagerduty to tool and block registries Re-add registry entries that were reverted after initial commit. Co-Authored-By: Claude Opus 4.6 * more updates * ack comemnts --------- Co-authored-by: Claude Opus 4.6 --- apps/docs/components/icons.tsx | 69 +- apps/docs/components/ui/icon-mapping.ts | 6 + apps/docs/content/docs/en/tools/amplitude.mdx | 313 ++++++++ .../docs/en/tools/google_pagespeed.mdx | 84 ++ apps/docs/content/docs/en/tools/meta.json | 3 + apps/docs/content/docs/en/tools/pagerduty.mdx | 217 +++++ apps/sim/blocks/blocks/amplitude.ts | 745 ++++++++++++++++++ apps/sim/blocks/blocks/google_pagespeed.ts | 86 ++ apps/sim/blocks/blocks/pagerduty.ts | 482 +++++++++++ apps/sim/blocks/registry.ts | 6 + apps/sim/components/icons.tsx | 69 +- .../sim/tools/amplitude/event_segmentation.ts | 134 ++++ apps/sim/tools/amplitude/get_active_users.ts | 105 +++ apps/sim/tools/amplitude/get_revenue.ts | 102 +++ apps/sim/tools/amplitude/group_identify.ts | 99 +++ apps/sim/tools/amplitude/identify_user.ts | 97 +++ apps/sim/tools/amplitude/index.ts | 23 + apps/sim/tools/amplitude/list_events.ts | 79 ++ .../tools/amplitude/realtime_active_users.ts | 74 ++ apps/sim/tools/amplitude/send_event.ts | 214 +++++ apps/sim/tools/amplitude/types.ts | 241 ++++++ apps/sim/tools/amplitude/user_activity.ts | 144 ++++ apps/sim/tools/amplitude/user_profile.ts | 120 +++ apps/sim/tools/amplitude/user_search.ts | 89 +++ apps/sim/tools/google_pagespeed/analyze.ts | 223 ++++++ apps/sim/tools/google_pagespeed/index.ts | 5 + apps/sim/tools/google_pagespeed/types.ts | 37 + apps/sim/tools/pagerduty/add_note.ts | 78 ++ apps/sim/tools/pagerduty/create_incident.ts | 149 ++++ apps/sim/tools/pagerduty/index.ts | 13 + apps/sim/tools/pagerduty/list_incidents.ts | 161 ++++ apps/sim/tools/pagerduty/list_oncalls.ts | 145 ++++ apps/sim/tools/pagerduty/list_services.ts | 108 +++ apps/sim/tools/pagerduty/types.ts | 169 ++++ apps/sim/tools/pagerduty/update_incident.ts | 117 +++ apps/sim/tools/registry.ts | 40 + 36 files changed, 4826 insertions(+), 20 deletions(-) create mode 100644 apps/docs/content/docs/en/tools/amplitude.mdx create mode 100644 apps/docs/content/docs/en/tools/google_pagespeed.mdx create mode 100644 apps/docs/content/docs/en/tools/pagerduty.mdx create mode 100644 apps/sim/blocks/blocks/amplitude.ts create mode 100644 apps/sim/blocks/blocks/google_pagespeed.ts create mode 100644 apps/sim/blocks/blocks/pagerduty.ts create mode 100644 apps/sim/tools/amplitude/event_segmentation.ts create mode 100644 apps/sim/tools/amplitude/get_active_users.ts create mode 100644 apps/sim/tools/amplitude/get_revenue.ts create mode 100644 apps/sim/tools/amplitude/group_identify.ts create mode 100644 apps/sim/tools/amplitude/identify_user.ts create mode 100644 apps/sim/tools/amplitude/index.ts create mode 100644 apps/sim/tools/amplitude/list_events.ts create mode 100644 apps/sim/tools/amplitude/realtime_active_users.ts create mode 100644 apps/sim/tools/amplitude/send_event.ts create mode 100644 apps/sim/tools/amplitude/types.ts create mode 100644 apps/sim/tools/amplitude/user_activity.ts create mode 100644 apps/sim/tools/amplitude/user_profile.ts create mode 100644 apps/sim/tools/amplitude/user_search.ts create mode 100644 apps/sim/tools/google_pagespeed/analyze.ts create mode 100644 apps/sim/tools/google_pagespeed/index.ts create mode 100644 apps/sim/tools/google_pagespeed/types.ts create mode 100644 apps/sim/tools/pagerduty/add_note.ts create mode 100644 apps/sim/tools/pagerduty/create_incident.ts create mode 100644 apps/sim/tools/pagerduty/index.ts create mode 100644 apps/sim/tools/pagerduty/list_incidents.ts create mode 100644 apps/sim/tools/pagerduty/list_oncalls.ts create mode 100644 apps/sim/tools/pagerduty/list_services.ts create mode 100644 apps/sim/tools/pagerduty/types.ts create mode 100644 apps/sim/tools/pagerduty/update_incident.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 51cd709bb7..c4666fba17 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1209,6 +1209,17 @@ export function AlgoliaIcon(props: SVGProps) { ) } +export function AmplitudeIcon(props: SVGProps) { + return ( + + + + ) +} + export function GoogleBooksIcon(props: SVGProps) { return ( @@ -1938,13 +1949,11 @@ export function ElevenLabsIcon(props: SVGProps) { export function LinkupIcon(props: SVGProps) { return ( - - - - + + ) } @@ -2453,6 +2462,17 @@ export function OutlookIcon(props: SVGProps) { ) } +export function PagerDutyIcon(props: SVGProps) { + return ( + + + + ) +} + export function MicrosoftExcelIcon(props: SVGProps) { const id = useId() const gradientId = `excel_gradient_${id}` @@ -3996,10 +4016,10 @@ export function IntercomIcon(props: SVGProps) { export function LoopsIcon(props: SVGProps) { return ( - + ) @@ -5578,6 +5598,35 @@ export function GoogleMapsIcon(props: SVGProps) { ) } +export function GooglePagespeedIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function GoogleTranslateIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index a69b0d90f5..6d15e539dc 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -9,6 +9,7 @@ import { AirtableIcon, AirweaveIcon, AlgoliaIcon, + AmplitudeIcon, ApifyIcon, ApolloIcon, ArxivIcon, @@ -56,6 +57,7 @@ import { GoogleGroupsIcon, GoogleIcon, GoogleMapsIcon, + GooglePagespeedIcon, GoogleSheetsIcon, GoogleSlidesIcon, GoogleTasksIcon, @@ -102,6 +104,7 @@ import { OpenAIIcon, OutlookIcon, PackageSearchIcon, + PagerDutyIcon, ParallelIcon, PerplexityIcon, PineconeIcon, @@ -167,6 +170,7 @@ export const blockTypeToIconMap: Record = { airtable: AirtableIcon, airweave: AirweaveIcon, algolia: AlgoliaIcon, + amplitude: AmplitudeIcon, apify: ApifyIcon, apollo: ApolloIcon, arxiv: ArxivIcon, @@ -211,6 +215,7 @@ export const blockTypeToIconMap: Record = { google_forms: GoogleFormsIcon, google_groups: GoogleGroupsIcon, google_maps: GoogleMapsIcon, + google_pagespeed: GooglePagespeedIcon, google_search: GoogleIcon, google_sheets_v2: GoogleSheetsIcon, google_slides_v2: GoogleSlidesIcon, @@ -258,6 +263,7 @@ export const blockTypeToIconMap: Record = { onepassword: OnePasswordIcon, openai: OpenAIIcon, outlook: OutlookIcon, + pagerduty: PagerDutyIcon, parallel_ai: ParallelIcon, perplexity: PerplexityIcon, pinecone: PineconeIcon, diff --git a/apps/docs/content/docs/en/tools/amplitude.mdx b/apps/docs/content/docs/en/tools/amplitude.mdx new file mode 100644 index 0000000000..177b5c5455 --- /dev/null +++ b/apps/docs/content/docs/en/tools/amplitude.mdx @@ -0,0 +1,313 @@ +--- +title: Amplitude +description: Track events and query analytics from Amplitude +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Amplitude](https://amplitude.com/) is a leading digital analytics platform that helps teams understand user behavior, measure product performance, and make data-driven decisions at scale. + +The Amplitude integration in Sim connects with the Amplitude HTTP and Dashboard REST APIs using API key and secret key authentication, allowing your agents to track events, manage user properties, and query analytics data programmatically. This API-based approach ensures secure access to Amplitude's full suite of analytics capabilities. + +With the Amplitude integration, your agents can: + +- **Track events**: Send custom events to Amplitude with rich properties, revenue data, and user context directly from your workflows +- **Identify users**: Set and update user properties using operations like $set, $setOnce, $add, $append, and $unset to maintain detailed user profiles +- **Search for users**: Look up users by User ID, Device ID, or Amplitude ID to retrieve profile information and metadata +- **Query event analytics**: Run event segmentation queries with grouping, custom metrics (uniques, totals, averages, DAU percentages), and flexible date ranges +- **Monitor user activity**: Retrieve event streams for specific users to understand individual user journeys and behavior patterns +- **Analyze active users**: Get active or new user counts over time with daily, weekly, or monthly granularity +- **Track revenue**: Access revenue LTV metrics including ARPU, ARPPU, total revenue, and paying user counts + +In Sim, the Amplitude integration enables powerful analytics automation scenarios. Your agents can track product events in real time based on workflow triggers, enrich user profiles as new data becomes available, query segmentation data to inform downstream decisions, or build monitoring workflows that alert on changes in key metrics. By connecting Sim with Amplitude, you can build intelligent agents that bridge the gap between analytics insights and automated action, enabling data-driven workflows that respond to user behavior patterns and product performance trends. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Amplitude into your workflow to track events, identify users and groups, search for users, query analytics, and retrieve revenue data. + + + +## Tools + +### `amplitude_send_event` + +Track an event in Amplitude using the HTTP V2 API. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `userId` | string | No | User ID \(required if no device_id\) | +| `deviceId` | string | No | Device ID \(required if no user_id\) | +| `eventType` | string | Yes | Name of the event \(e.g., "page_view", "purchase"\) | +| `eventProperties` | string | No | JSON object of custom event properties | +| `userProperties` | string | No | JSON object of user properties to set \(supports $set, $setOnce, $add, $append, $unset\) | +| `time` | string | No | Event timestamp in milliseconds since epoch | +| `sessionId` | string | No | Session start time in milliseconds since epoch | +| `insertId` | string | No | Unique ID for deduplication \(within 7-day window\) | +| `appVersion` | string | No | Application version string | +| `platform` | string | No | Platform \(e.g., "Web", "iOS", "Android"\) | +| `country` | string | No | Two-letter country code | +| `language` | string | No | Language code \(e.g., "en"\) | +| `ip` | string | No | IP address for geo-location | +| `price` | string | No | Price of the item purchased | +| `quantity` | string | No | Quantity of items purchased | +| `revenue` | string | No | Revenue amount | +| `productId` | string | No | Product identifier | +| `revenueType` | string | No | Revenue type \(e.g., "purchase", "refund"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `code` | number | Response code \(200 for success\) | +| `eventsIngested` | number | Number of events ingested | +| `payloadSizeBytes` | number | Size of the payload in bytes | +| `serverUploadTime` | number | Server upload timestamp | + +### `amplitude_identify_user` + +Set user properties in Amplitude using the Identify API. Supports $set, $setOnce, $add, $append, $unset operations. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `userId` | string | No | User ID \(required if no device_id\) | +| `deviceId` | string | No | Device ID \(required if no user_id\) | +| `userProperties` | string | Yes | JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `code` | number | HTTP response status code | +| `message` | string | Response message | + +### `amplitude_group_identify` + +Set group-level properties in Amplitude. Supports $set, $setOnce, $add, $append, $unset operations. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `groupType` | string | Yes | Group classification \(e.g., "company", "org_id"\) | +| `groupValue` | string | Yes | Specific group identifier \(e.g., "Acme Corp"\) | +| `groupProperties` | string | Yes | JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `code` | number | HTTP response status code | +| `message` | string | Response message | + +### `amplitude_user_search` + +Search for a user by User ID, Device ID, or Amplitude ID using the Dashboard REST API. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | +| `user` | string | Yes | User ID, Device ID, or Amplitude ID to search for | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matches` | array | List of matching users | +| ↳ `amplitudeId` | number | Amplitude internal user ID | +| ↳ `userId` | string | External user ID | +| `type` | string | Match type \(e.g., match_user_or_device_id\) | + +### `amplitude_user_activity` + +Get the event stream for a specific user by their Amplitude ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | +| `amplitudeId` | string | Yes | Amplitude internal user ID | +| `offset` | string | No | Offset for pagination \(default 0\) | +| `limit` | string | No | Maximum number of events to return \(default 1000, max 1000\) | +| `direction` | string | No | Sort direction: "latest" or "earliest" \(default: latest\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | array | List of user events | +| ↳ `eventType` | string | Type of event | +| ↳ `eventTime` | string | Event timestamp | +| ↳ `eventProperties` | json | Custom event properties | +| ↳ `userProperties` | json | User properties at event time | +| ↳ `sessionId` | number | Session ID | +| ↳ `platform` | string | Platform | +| ↳ `country` | string | Country | +| ↳ `city` | string | City | +| `userData` | json | User metadata | +| ↳ `userId` | string | External user ID | +| ↳ `canonicalAmplitudeId` | number | Canonical Amplitude ID | +| ↳ `numEvents` | number | Total event count | +| ↳ `numSessions` | number | Total session count | +| ↳ `platform` | string | Primary platform | +| ↳ `country` | string | Country | + +### `amplitude_user_profile` + +Get a user profile including properties, cohort memberships, and computed properties. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `secretKey` | string | Yes | Amplitude Secret Key | +| `userId` | string | No | External user ID \(required if no device_id\) | +| `deviceId` | string | No | Device ID \(required if no user_id\) | +| `getAmpProps` | string | No | Include Amplitude user properties \(true/false, default: false\) | +| `getCohortIds` | string | No | Include cohort IDs the user belongs to \(true/false, default: false\) | +| `getComputations` | string | No | Include computed user properties \(true/false, default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `userId` | string | External user ID | +| `deviceId` | string | Device ID | +| `ampProps` | json | Amplitude user properties \(library, first_used, last_used, custom properties\) | +| `cohortIds` | array | List of cohort IDs the user belongs to | +| `computations` | json | Computed user properties | + +### `amplitude_event_segmentation` + +Query event analytics data with segmentation. Get event counts, uniques, averages, and more. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | +| `eventType` | string | Yes | Event type name to analyze | +| `start` | string | Yes | Start date in YYYYMMDD format | +| `end` | string | Yes | End date in YYYYMMDD format | +| `metric` | string | No | Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula \(default: uniques\) | +| `interval` | string | No | Time interval: 1 \(daily\), 7 \(weekly\), or 30 \(monthly\) | +| `groupBy` | string | No | Property name to group by \(prefix custom user properties with "gp:"\) | +| `limit` | string | No | Maximum number of group-by values \(max 1000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `series` | json | Time-series data arrays indexed by series | +| `seriesLabels` | array | Labels for each data series | +| `seriesCollapsed` | json | Collapsed aggregate totals per series | +| `xValues` | array | Date values for the x-axis | + +### `amplitude_get_active_users` + +Get active or new user counts over a date range from the Dashboard REST API. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | +| `start` | string | Yes | Start date in YYYYMMDD format | +| `end` | string | Yes | End date in YYYYMMDD format | +| `metric` | string | No | Metric type: "active" or "new" \(default: active\) | +| `interval` | string | No | Time interval: 1 \(daily\), 7 \(weekly\), or 30 \(monthly\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `series` | json | Array of data series with user counts per time interval | +| `seriesMeta` | array | Metadata labels for each data series \(e.g., segment names\) | +| `xValues` | array | Date values for the x-axis | + +### `amplitude_realtime_active_users` + +Get real-time active user counts at 5-minute granularity for the last 2 days. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `series` | json | Array of data series with active user counts at 5-minute intervals | +| `seriesLabels` | array | Labels for each series \(e.g., "Today", "Yesterday"\) | +| `xValues` | array | Time values for the x-axis \(e.g., "15:00", "15:05"\) | + +### `amplitude_list_events` + +List all event types in the Amplitude project with their weekly totals and unique counts. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | array | List of event types in the project | +| ↳ `value` | string | Event type name | +| ↳ `displayName` | string | Event display name | +| ↳ `totals` | number | Weekly total count | +| ↳ `hidden` | boolean | Whether the event is hidden | +| ↳ `deleted` | boolean | Whether the event is deleted | + +### `amplitude_get_revenue` + +Get revenue LTV data including ARPU, ARPPU, total revenue, and paying user counts. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Amplitude API Key | +| `secretKey` | string | Yes | Amplitude Secret Key | +| `start` | string | Yes | Start date in YYYYMMDD format | +| `end` | string | Yes | End date in YYYYMMDD format | +| `metric` | string | No | Metric: 0 \(ARPU\), 1 \(ARPPU\), 2 \(Total Revenue\), 3 \(Paying Users\) | +| `interval` | string | No | Time interval: 1 \(daily\), 7 \(weekly\), or 30 \(monthly\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `series` | json | Array of revenue data series | +| `seriesLabels` | array | Labels for each data series | +| `xValues` | array | Date values for the x-axis | + + diff --git a/apps/docs/content/docs/en/tools/google_pagespeed.mdx b/apps/docs/content/docs/en/tools/google_pagespeed.mdx new file mode 100644 index 0000000000..65b62e0e75 --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_pagespeed.mdx @@ -0,0 +1,84 @@ +--- +title: Google PageSpeed +description: Analyze webpage performance with Google PageSpeed Insights +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Google PageSpeed Insights](https://pagespeed.web.dev/) is a web performance analysis tool powered by Lighthouse that evaluates the quality of web pages across multiple dimensions including performance, accessibility, SEO, and best practices. + +With the Google PageSpeed integration in Sim, you can: + +- **Analyze webpage performance**: Get detailed performance scores and metrics for any public URL, including First Contentful Paint, Largest Contentful Paint, and Speed Index +- **Evaluate accessibility**: Check how well a webpage meets accessibility standards and identify areas for improvement +- **Audit SEO**: Assess a page's search engine optimization and discover opportunities to improve rankings +- **Review best practices**: Verify that a webpage follows modern web development best practices +- **Compare strategies**: Run analyses using either desktop or mobile strategies to understand performance across device types +- **Localize results**: Retrieve analysis results in different locales for internationalized reporting + +In Sim, the Google PageSpeed integration enables your agents to programmatically audit web pages as part of automated workflows. This is useful for monitoring site performance over time, triggering alerts when scores drop below thresholds, generating performance reports, and ensuring that deployed changes meet quality standards before release. + +### Getting Your API Key + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) +2. Create or select a project +3. Enable the **PageSpeed Insights API** from the API Library +4. Navigate to **Credentials** and create an API key +5. Use the API key in the Sim block configuration +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Analyze web pages for performance, accessibility, SEO, and best practices using Google PageSpeed Insights API powered by Lighthouse. + + + +## Tools + +### `google_pagespeed_analyze` + +Analyze a webpage for performance, accessibility, SEO, and best practices using Google PageSpeed Insights. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google PageSpeed Insights API Key | +| `url` | string | Yes | The URL of the webpage to analyze | +| `category` | string | No | Lighthouse categories to analyze \(comma-separated\): performance, accessibility, best-practices, seo | +| `strategy` | string | No | Analysis strategy: desktop or mobile | +| `locale` | string | No | Locale for results \(e.g., en, fr, de\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `finalUrl` | string | The final URL after redirects | +| `performanceScore` | number | Performance category score \(0-1\) | +| `accessibilityScore` | number | Accessibility category score \(0-1\) | +| `bestPracticesScore` | number | Best Practices category score \(0-1\) | +| `seoScore` | number | SEO category score \(0-1\) | +| `firstContentfulPaint` | string | Time to First Contentful Paint \(display value\) | +| `firstContentfulPaintMs` | number | Time to First Contentful Paint in milliseconds | +| `largestContentfulPaint` | string | Time to Largest Contentful Paint \(display value\) | +| `largestContentfulPaintMs` | number | Time to Largest Contentful Paint in milliseconds | +| `totalBlockingTime` | string | Total Blocking Time \(display value\) | +| `totalBlockingTimeMs` | number | Total Blocking Time in milliseconds | +| `cumulativeLayoutShift` | string | Cumulative Layout Shift \(display value\) | +| `cumulativeLayoutShiftValue` | number | Cumulative Layout Shift numeric value | +| `speedIndex` | string | Speed Index \(display value\) | +| `speedIndexMs` | number | Speed Index in milliseconds | +| `interactive` | string | Time to Interactive \(display value\) | +| `interactiveMs` | number | Time to Interactive in milliseconds | +| `overallCategory` | string | Overall loading experience category \(FAST, AVERAGE, SLOW, or NONE\) | +| `analysisTimestamp` | string | UTC timestamp of the analysis | +| `lighthouseVersion` | string | Version of Lighthouse used for the analysis | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 3a1917fc19..612cba972c 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -6,6 +6,7 @@ "airtable", "airweave", "algolia", + "amplitude", "apify", "apollo", "arxiv", @@ -50,6 +51,7 @@ "google_forms", "google_groups", "google_maps", + "google_pagespeed", "google_search", "google_sheets", "google_slides", @@ -97,6 +99,7 @@ "onepassword", "openai", "outlook", + "pagerduty", "parallel_ai", "perplexity", "pinecone", diff --git a/apps/docs/content/docs/en/tools/pagerduty.mdx b/apps/docs/content/docs/en/tools/pagerduty.mdx new file mode 100644 index 0000000000..5876c1cd7d --- /dev/null +++ b/apps/docs/content/docs/en/tools/pagerduty.mdx @@ -0,0 +1,217 @@ +--- +title: PagerDuty +description: Manage incidents and on-call schedules with PagerDuty +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[PagerDuty](https://www.pagerduty.com/) is a leading incident management platform that helps engineering and operations teams detect, triage, and resolve infrastructure and application issues in real time. PagerDuty integrates with monitoring tools, orchestrates on-call schedules, and ensures the right people are alerted when incidents occur. + +The PagerDuty integration in Sim connects with the PagerDuty REST API v2 using API key authentication, enabling your agents to manage the full incident lifecycle and query on-call information programmatically. + +With the PagerDuty integration, your agents can: + +- **List and filter incidents**: Retrieve incidents filtered by status (triggered, acknowledged, resolved), service, date range, and sort order to monitor your operational health +- **Create incidents**: Trigger new incidents on specific services with custom titles, descriptions, urgency levels, and assignees directly from your workflows +- **Update incidents**: Acknowledge or resolve incidents, change urgency, and add resolution notes to keep your incident management in sync with automated processes +- **Add notes to incidents**: Attach contextual information, investigation findings, or automated diagnostics as notes on existing incidents +- **List services**: Query your PagerDuty service catalog to discover service IDs and metadata for use in other operations +- **Check on-call schedules**: Retrieve current on-call entries filtered by escalation policy or schedule to determine who is responsible at any given time + +In Sim, the PagerDuty integration enables powerful incident automation scenarios. Your agents can automatically create incidents based on monitoring alerts, enrich incidents with diagnostic data from other tools, resolve incidents when automated remediation succeeds, or build escalation workflows that check on-call schedules and route notifications accordingly. By connecting Sim with PagerDuty, you can build intelligent agents that bridge the gap between detection and response, reducing mean time to resolution and ensuring consistent incident handling across your organization. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate PagerDuty into your workflow to list, create, and update incidents, add notes, list services, and check on-call schedules. + + + +## Tools + +### `pagerduty_list_incidents` + +List incidents from PagerDuty with optional filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `statuses` | string | No | Comma-separated statuses to filter \(triggered, acknowledged, resolved\) | +| `serviceIds` | string | No | Comma-separated service IDs to filter | +| `since` | string | No | Start date filter \(ISO 8601 format\) | +| `until` | string | No | End date filter \(ISO 8601 format\) | +| `sortBy` | string | No | Sort field \(e.g., created_at:desc\) | +| `limit` | string | No | Maximum number of results \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `incidents` | array | Array of incidents | +| ↳ `id` | string | Incident ID | +| ↳ `incidentNumber` | number | Incident number | +| ↳ `title` | string | Incident title | +| ↳ `status` | string | Incident status | +| ↳ `urgency` | string | Incident urgency | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last updated timestamp | +| ↳ `serviceName` | string | Service name | +| ↳ `serviceId` | string | Service ID | +| ↳ `assigneeName` | string | Assignee name | +| ↳ `assigneeId` | string | Assignee ID | +| ↳ `escalationPolicyName` | string | Escalation policy name | +| ↳ `htmlUrl` | string | PagerDuty web URL | +| `total` | number | Total number of matching incidents | +| `more` | boolean | Whether more results are available | + +### `pagerduty_create_incident` + +Create a new incident in PagerDuty. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `fromEmail` | string | Yes | Email address of a valid PagerDuty user | +| `title` | string | Yes | Incident title/summary | +| `serviceId` | string | Yes | ID of the PagerDuty service | +| `urgency` | string | No | Urgency level \(high or low\) | +| `body` | string | No | Detailed description of the incident | +| `escalationPolicyId` | string | No | Escalation policy ID to assign | +| `assigneeId` | string | No | User ID to assign the incident to | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Created incident ID | +| `incidentNumber` | number | Incident number | +| `title` | string | Incident title | +| `status` | string | Incident status | +| `urgency` | string | Incident urgency | +| `createdAt` | string | Creation timestamp | +| `serviceName` | string | Service name | +| `serviceId` | string | Service ID | +| `htmlUrl` | string | PagerDuty web URL | + +### `pagerduty_update_incident` + +Update an incident in PagerDuty (acknowledge, resolve, change urgency, etc.). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `fromEmail` | string | Yes | Email address of a valid PagerDuty user | +| `incidentId` | string | Yes | ID of the incident to update | +| `status` | string | No | New status \(acknowledged or resolved\) | +| `title` | string | No | New incident title | +| `urgency` | string | No | New urgency \(high or low\) | +| `escalationLevel` | string | No | Escalation level to escalate to | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Incident ID | +| `incidentNumber` | number | Incident number | +| `title` | string | Incident title | +| `status` | string | Updated status | +| `urgency` | string | Updated urgency | +| `updatedAt` | string | Last updated timestamp | +| `htmlUrl` | string | PagerDuty web URL | + +### `pagerduty_add_note` + +Add a note to an existing PagerDuty incident. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `fromEmail` | string | Yes | Email address of a valid PagerDuty user | +| `incidentId` | string | Yes | ID of the incident to add the note to | +| `content` | string | Yes | Note content text | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Note ID | +| `content` | string | Note content | +| `createdAt` | string | Creation timestamp | +| `userName` | string | Name of the user who created the note | + +### `pagerduty_list_services` + +List services from PagerDuty with optional name filter. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `query` | string | No | Filter services by name | +| `limit` | string | No | Maximum number of results \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `services` | array | Array of services | +| ↳ `id` | string | Service ID | +| ↳ `name` | string | Service name | +| ↳ `description` | string | Service description | +| ↳ `status` | string | Service status | +| ↳ `escalationPolicyName` | string | Escalation policy name | +| ↳ `escalationPolicyId` | string | Escalation policy ID | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `htmlUrl` | string | PagerDuty web URL | +| `total` | number | Total number of matching services | +| `more` | boolean | Whether more results are available | + +### `pagerduty_list_oncalls` + +List current on-call entries from PagerDuty. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `escalationPolicyIds` | string | No | Comma-separated escalation policy IDs to filter | +| `scheduleIds` | string | No | Comma-separated schedule IDs to filter | +| `since` | string | No | Start time filter \(ISO 8601 format\) | +| `until` | string | No | End time filter \(ISO 8601 format\) | +| `limit` | string | No | Maximum number of results \(max 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `oncalls` | array | Array of on-call entries | +| ↳ `userName` | string | On-call user name | +| ↳ `userId` | string | On-call user ID | +| ↳ `escalationLevel` | number | Escalation level | +| ↳ `escalationPolicyName` | string | Escalation policy name | +| ↳ `escalationPolicyId` | string | Escalation policy ID | +| ↳ `scheduleName` | string | Schedule name | +| ↳ `scheduleId` | string | Schedule ID | +| ↳ `start` | string | On-call start time | +| ↳ `end` | string | On-call end time | +| `total` | number | Total number of matching on-call entries | +| `more` | boolean | Whether more results are available | + + diff --git a/apps/sim/blocks/blocks/amplitude.ts b/apps/sim/blocks/blocks/amplitude.ts new file mode 100644 index 0000000000..e9cbf61834 --- /dev/null +++ b/apps/sim/blocks/blocks/amplitude.ts @@ -0,0 +1,745 @@ +import { AmplitudeIcon } from '@/components/icons' +import { AuthMode, type BlockConfig } from '@/blocks/types' + +export const AmplitudeBlock: BlockConfig = { + type: 'amplitude', + name: 'Amplitude', + description: 'Track events and query analytics from Amplitude', + longDescription: + 'Integrate Amplitude into your workflow to track events, identify users and groups, search for users, query analytics, and retrieve revenue data.', + docsLink: 'https://docs.sim.ai/tools/amplitude', + category: 'tools', + bgColor: '#1B1F3B', + icon: AmplitudeIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Send Event', id: 'send_event' }, + { label: 'Identify User', id: 'identify_user' }, + { label: 'Group Identify', id: 'group_identify' }, + { label: 'User Search', id: 'user_search' }, + { label: 'User Activity', id: 'user_activity' }, + { label: 'User Profile', id: 'user_profile' }, + { label: 'Event Segmentation', id: 'event_segmentation' }, + { label: 'Get Active Users', id: 'get_active_users' }, + { label: 'Real-time Active Users', id: 'realtime_active_users' }, + { label: 'List Events', id: 'list_events' }, + { label: 'Get Revenue', id: 'get_revenue' }, + ], + value: () => 'send_event', + }, + + // API Key (required for all operations) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Amplitude API Key', + password: true, + condition: { + field: 'operation', + value: 'user_profile', + not: true, + }, + }, + + // API Key for user_profile (not required - uses only secretKey) + // User Profile uses Api-Key header with secret key only + + // Secret Key (required for Dashboard REST API operations + User Profile) + { + id: 'secretKey', + title: 'Secret Key', + type: 'short-input', + required: { + field: 'operation', + value: [ + 'user_search', + 'user_activity', + 'user_profile', + 'event_segmentation', + 'get_active_users', + 'realtime_active_users', + 'list_events', + 'get_revenue', + ], + }, + placeholder: 'Enter your Amplitude Secret Key', + password: true, + condition: { + field: 'operation', + value: [ + 'user_search', + 'user_activity', + 'user_profile', + 'event_segmentation', + 'get_active_users', + 'realtime_active_users', + 'list_events', + 'get_revenue', + ], + }, + }, + + // --- Send Event fields --- + { + id: 'eventType', + title: 'Event Type', + type: 'short-input', + required: { field: 'operation', value: 'send_event' }, + placeholder: 'e.g., page_view, purchase, signup', + condition: { field: 'operation', value: 'send_event' }, + }, + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'User identifier', + condition: { field: 'operation', value: ['send_event', 'identify_user'] }, + }, + { + id: 'profileUserId', + title: 'User ID', + type: 'short-input', + placeholder: 'External user ID (required if no Device ID)', + condition: { field: 'operation', value: 'user_profile' }, + }, + { + id: 'deviceId', + title: 'Device ID', + type: 'short-input', + placeholder: 'Device identifier', + condition: { field: 'operation', value: ['send_event', 'identify_user'] }, + mode: 'advanced', + }, + { + id: 'profileDeviceId', + title: 'Device ID', + type: 'short-input', + placeholder: 'Device ID (required if no User ID)', + condition: { field: 'operation', value: 'user_profile' }, + mode: 'advanced', + }, + { + id: 'eventProperties', + title: 'Event Properties', + type: 'long-input', + placeholder: '{"button": "signup", "page": "/home"}', + condition: { field: 'operation', value: 'send_event' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of event properties for an Amplitude event. Return ONLY the JSON object - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'sendEventUserProperties', + title: 'User Properties', + type: 'long-input', + placeholder: '{"$set": {"plan": "premium"}}', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of user properties for Amplitude. Use $set, $setOnce, $add, $append, or $unset operations. Return ONLY the JSON object - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'platform', + title: 'Platform', + type: 'short-input', + placeholder: 'e.g., Web, iOS, Android', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'appVersion', + title: 'App Version', + type: 'short-input', + placeholder: 'e.g., 1.0.0', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'insertId', + title: 'Insert ID', + type: 'short-input', + placeholder: 'Unique ID for deduplication', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'price', + title: 'Price', + type: 'short-input', + placeholder: '9.99', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'quantity', + title: 'Quantity', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'revenue', + title: 'Revenue', + type: 'short-input', + placeholder: '9.99', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'productId', + title: 'Product ID', + type: 'short-input', + placeholder: 'Product identifier', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'revenueType', + title: 'Revenue Type', + type: 'short-input', + placeholder: 'e.g., purchase, refund', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'country', + title: 'Country', + type: 'short-input', + placeholder: 'Two-letter country code (e.g., US)', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'language', + title: 'Language', + type: 'short-input', + placeholder: 'Language code (e.g., en)', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'ip', + title: 'IP Address', + type: 'short-input', + placeholder: 'IP for geo-location (use "$remote" for request IP)', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + { + id: 'time', + title: 'Timestamp', + type: 'short-input', + placeholder: 'Milliseconds since epoch', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a timestamp in milliseconds since epoch for the current time. Return ONLY the number - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'sessionId', + title: 'Session ID', + type: 'short-input', + placeholder: 'Session start time in milliseconds (-1 for no session)', + condition: { field: 'operation', value: 'send_event' }, + mode: 'advanced', + }, + + // --- Identify User fields --- + { + id: 'identifyUserProperties', + title: 'User Properties', + type: 'long-input', + required: { field: 'operation', value: 'identify_user' }, + placeholder: '{"$set": {"plan": "premium", "company": "Acme"}}', + condition: { field: 'operation', value: 'identify_user' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of user properties for Amplitude Identify API. Use $set, $setOnce, $add, $append, or $unset operations. Return ONLY the JSON object - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + + // --- Group Identify fields --- + { + id: 'groupType', + title: 'Group Type', + type: 'short-input', + required: { field: 'operation', value: 'group_identify' }, + placeholder: 'e.g., company, org_id', + condition: { field: 'operation', value: 'group_identify' }, + }, + { + id: 'groupValue', + title: 'Group Value', + type: 'short-input', + required: { field: 'operation', value: 'group_identify' }, + placeholder: 'e.g., Acme Corp', + condition: { field: 'operation', value: 'group_identify' }, + }, + { + id: 'groupProperties', + title: 'Group Properties', + type: 'long-input', + required: { field: 'operation', value: 'group_identify' }, + placeholder: '{"$set": {"industry": "tech", "employee_count": 500}}', + condition: { field: 'operation', value: 'group_identify' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON object of group properties for Amplitude Group Identify API. Use $set, $setOnce, $add, $append, or $unset operations. Return ONLY the JSON object - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + + // --- User Search fields --- + { + id: 'searchUser', + title: 'User', + type: 'short-input', + required: { field: 'operation', value: 'user_search' }, + placeholder: 'User ID, Device ID, or Amplitude ID', + condition: { field: 'operation', value: 'user_search' }, + }, + + // --- User Activity fields --- + { + id: 'amplitudeId', + title: 'Amplitude ID', + type: 'short-input', + required: { field: 'operation', value: 'user_activity' }, + placeholder: 'Amplitude internal user ID', + condition: { field: 'operation', value: 'user_activity' }, + }, + { + id: 'activityOffset', + title: 'Offset', + type: 'short-input', + placeholder: '0', + condition: { field: 'operation', value: 'user_activity' }, + mode: 'advanced', + }, + { + id: 'activityLimit', + title: 'Limit', + type: 'short-input', + placeholder: '1000', + condition: { field: 'operation', value: 'user_activity' }, + mode: 'advanced', + }, + { + id: 'activityDirection', + title: 'Direction', + type: 'dropdown', + options: [ + { label: 'Latest First', id: 'latest' }, + { label: 'Earliest First', id: 'earliest' }, + ], + value: () => 'latest', + condition: { field: 'operation', value: 'user_activity' }, + mode: 'advanced', + }, + + // --- User Profile fields --- + { + id: 'getAmpProps', + title: 'Include User Properties', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'user_profile' }, + mode: 'advanced', + }, + { + id: 'getCohortIds', + title: 'Include Cohort IDs', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'user_profile' }, + mode: 'advanced', + }, + { + id: 'getComputations', + title: 'Include Computed Properties', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'user_profile' }, + mode: 'advanced', + }, + + // --- Event Segmentation fields --- + { + id: 'segmentationEventType', + title: 'Event Type', + type: 'short-input', + required: { field: 'operation', value: 'event_segmentation' }, + placeholder: 'Event type to analyze', + condition: { field: 'operation', value: 'event_segmentation' }, + }, + { + id: 'segmentationStart', + title: 'Start Date', + type: 'short-input', + required: { field: 'operation', value: 'event_segmentation' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'event_segmentation' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'segmentationEnd', + title: 'End Date', + type: 'short-input', + required: { field: 'operation', value: 'event_segmentation' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'event_segmentation' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'segmentationMetric', + title: 'Metric', + type: 'dropdown', + options: [ + { label: 'Uniques', id: 'uniques' }, + { label: 'Totals', id: 'totals' }, + { label: '% DAU', id: 'pct_dau' }, + { label: 'Average', id: 'average' }, + { label: 'Histogram', id: 'histogram' }, + { label: 'Sums', id: 'sums' }, + { label: 'Value Average', id: 'value_avg' }, + { label: 'Formula', id: 'formula' }, + ], + value: () => 'uniques', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + }, + { + id: 'segmentationInterval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: 'Daily', id: '1' }, + { label: 'Weekly', id: '7' }, + { label: 'Monthly', id: '30' }, + ], + value: () => '1', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + }, + { + id: 'segmentationGroupBy', + title: 'Group By', + type: 'short-input', + placeholder: 'Property name (prefix custom with "gp:")', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + }, + { + id: 'segmentationLimit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max group-by values (max 1000)', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + }, + + // --- Get Active Users fields --- + { + id: 'activeUsersStart', + title: 'Start Date', + type: 'short-input', + required: { field: 'operation', value: 'get_active_users' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'get_active_users' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'activeUsersEnd', + title: 'End Date', + type: 'short-input', + required: { field: 'operation', value: 'get_active_users' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'get_active_users' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'activeUsersMetric', + title: 'Metric', + type: 'dropdown', + options: [ + { label: 'Active Users', id: 'active' }, + { label: 'New Users', id: 'new' }, + ], + value: () => 'active', + condition: { field: 'operation', value: 'get_active_users' }, + mode: 'advanced', + }, + { + id: 'activeUsersInterval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: 'Daily', id: '1' }, + { label: 'Weekly', id: '7' }, + { label: 'Monthly', id: '30' }, + ], + value: () => '1', + condition: { field: 'operation', value: 'get_active_users' }, + mode: 'advanced', + }, + + // --- Get Revenue fields --- + { + id: 'revenueStart', + title: 'Start Date', + type: 'short-input', + required: { field: 'operation', value: 'get_revenue' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'get_revenue' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'revenueEnd', + title: 'End Date', + type: 'short-input', + required: { field: 'operation', value: 'get_revenue' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'get_revenue' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'revenueMetric', + title: 'Metric', + type: 'dropdown', + options: [ + { label: 'ARPU', id: '0' }, + { label: 'ARPPU', id: '1' }, + { label: 'Total Revenue', id: '2' }, + { label: 'Paying Users', id: '3' }, + ], + value: () => '2', + condition: { field: 'operation', value: 'get_revenue' }, + mode: 'advanced', + }, + { + id: 'revenueInterval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: 'Daily', id: '1' }, + { label: 'Weekly', id: '7' }, + { label: 'Monthly', id: '30' }, + ], + value: () => '1', + condition: { field: 'operation', value: 'get_revenue' }, + mode: 'advanced', + }, + ], + + tools: { + access: [ + 'amplitude_send_event', + 'amplitude_identify_user', + 'amplitude_group_identify', + 'amplitude_user_search', + 'amplitude_user_activity', + 'amplitude_user_profile', + 'amplitude_event_segmentation', + 'amplitude_get_active_users', + 'amplitude_realtime_active_users', + 'amplitude_list_events', + 'amplitude_get_revenue', + ], + config: { + tool: (params) => `amplitude_${params.operation}`, + params: (params) => { + const result: Record = {} + + switch (params.operation) { + case 'send_event': + if (params.sendEventUserProperties) + result.userProperties = params.sendEventUserProperties + break + + case 'identify_user': + if (params.identifyUserProperties) result.userProperties = params.identifyUserProperties + break + + case 'user_search': + if (params.searchUser) result.user = params.searchUser + break + + case 'user_activity': + if (params.activityOffset) result.offset = params.activityOffset + if (params.activityLimit) result.limit = params.activityLimit + if (params.activityDirection) result.direction = params.activityDirection + break + + case 'user_profile': + if (params.profileUserId) result.userId = params.profileUserId + if (params.profileDeviceId) result.deviceId = params.profileDeviceId + break + + case 'event_segmentation': + if (params.segmentationEventType) result.eventType = params.segmentationEventType + if (params.segmentationStart) result.start = params.segmentationStart + if (params.segmentationEnd) result.end = params.segmentationEnd + if (params.segmentationMetric) result.metric = params.segmentationMetric + if (params.segmentationInterval) result.interval = params.segmentationInterval + if (params.segmentationGroupBy) result.groupBy = params.segmentationGroupBy + if (params.segmentationLimit) result.limit = params.segmentationLimit + break + + case 'get_active_users': + if (params.activeUsersStart) result.start = params.activeUsersStart + if (params.activeUsersEnd) result.end = params.activeUsersEnd + if (params.activeUsersMetric) result.metric = params.activeUsersMetric + if (params.activeUsersInterval) result.interval = params.activeUsersInterval + break + + case 'get_revenue': + if (params.revenueStart) result.start = params.revenueStart + if (params.revenueEnd) result.end = params.revenueEnd + if (params.revenueMetric) result.metric = params.revenueMetric + if (params.revenueInterval) result.interval = params.revenueInterval + break + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'Amplitude API Key' }, + secretKey: { type: 'string', description: 'Amplitude Secret Key' }, + eventType: { type: 'string', description: 'Event type name' }, + userId: { type: 'string', description: 'User ID' }, + deviceId: { type: 'string', description: 'Device ID' }, + eventProperties: { type: 'string', description: 'Event properties JSON' }, + sendEventUserProperties: { type: 'string', description: 'User properties for send event' }, + identifyUserProperties: { type: 'string', description: 'User properties for identify' }, + groupType: { type: 'string', description: 'Group type classification' }, + groupValue: { type: 'string', description: 'Group identifier value' }, + groupProperties: { type: 'string', description: 'Group properties JSON' }, + searchUser: { type: 'string', description: 'User to search for' }, + amplitudeId: { type: 'string', description: 'Amplitude internal user ID' }, + profileUserId: { type: 'string', description: 'User ID for profile lookup' }, + profileDeviceId: { type: 'string', description: 'Device ID for profile lookup' }, + segmentationEventType: { type: 'string', description: 'Event type to analyze' }, + segmentationStart: { type: 'string', description: 'Segmentation start date' }, + segmentationEnd: { type: 'string', description: 'Segmentation end date' }, + activeUsersStart: { type: 'string', description: 'Active users start date' }, + activeUsersEnd: { type: 'string', description: 'Active users end date' }, + revenueStart: { type: 'string', description: 'Revenue start date' }, + revenueEnd: { type: 'string', description: 'Revenue end date' }, + }, + + outputs: { + code: { + type: 'number', + description: 'Response status code', + }, + message: { + type: 'string', + description: 'Response message (identify_user, group_identify)', + }, + eventsIngested: { + type: 'number', + description: 'Number of events ingested (send_event)', + }, + matches: { + type: 'json', + description: 'User search matches (amplitudeId, userId)', + }, + events: { + type: 'json', + description: 'Event list (list_events, user_activity)', + }, + userData: { + type: 'json', + description: 'User metadata (user_activity)', + }, + series: { + type: 'json', + description: 'Time-series data (segmentation, active_users, revenue, realtime)', + }, + seriesLabels: { + type: 'json', + description: 'Labels for each data series (segmentation, realtime, revenue)', + }, + seriesMeta: { + type: 'json', + description: 'Metadata labels for data series (active_users)', + }, + seriesCollapsed: { + type: 'json', + description: 'Collapsed aggregate totals per series (segmentation)', + }, + xValues: { + type: 'json', + description: 'X-axis date/time values for chart data', + }, + }, +} diff --git a/apps/sim/blocks/blocks/google_pagespeed.ts b/apps/sim/blocks/blocks/google_pagespeed.ts new file mode 100644 index 0000000000..955b895cbd --- /dev/null +++ b/apps/sim/blocks/blocks/google_pagespeed.ts @@ -0,0 +1,86 @@ +import { GooglePagespeedIcon } from '@/components/icons' +import { AuthMode, type BlockConfig } from '@/blocks/types' +import type { GooglePagespeedAnalyzeResponse } from '@/tools/google_pagespeed/types' + +export const GooglePagespeedBlock: BlockConfig = { + type: 'google_pagespeed', + name: 'Google PageSpeed', + description: 'Analyze webpage performance with Google PageSpeed Insights', + longDescription: + 'Analyze web pages for performance, accessibility, SEO, and best practices using Google PageSpeed Insights API powered by Lighthouse.', + docsLink: 'https://docs.sim.ai/tools/google_pagespeed', + category: 'tools', + bgColor: '#E0E0E0', + icon: GooglePagespeedIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'url', + title: 'URL', + type: 'short-input', + required: true, + placeholder: 'https://example.com', + }, + { + id: 'strategy', + title: 'Strategy', + type: 'dropdown', + options: [ + { label: 'Desktop', id: 'desktop' }, + { label: 'Mobile', id: 'mobile' }, + ], + value: () => 'desktop', + }, + { + id: 'category', + title: 'Categories', + type: 'short-input', + placeholder: 'performance, accessibility, best-practices, seo', + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of Google PageSpeed Insights categories to analyze. Valid values are: performance, accessibility, best-practices, seo. Return ONLY the comma-separated list - no explanations, no extra text.', + }, + }, + { + id: 'locale', + title: 'Locale', + type: 'short-input', + placeholder: 'en', + mode: 'advanced', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Google PageSpeed API key', + password: true, + }, + ], + + tools: { + access: ['google_pagespeed_analyze'], + config: { + tool: () => 'google_pagespeed_analyze', + }, + }, + + inputs: { + url: { type: 'string', description: 'URL to analyze' }, + strategy: { type: 'string', description: 'Analysis strategy (desktop or mobile)' }, + category: { type: 'string', description: 'Comma-separated categories to analyze' }, + locale: { type: 'string', description: 'Locale for results' }, + apiKey: { type: 'string', description: 'Google PageSpeed API key' }, + }, + + outputs: { + response: { + type: 'json', + description: + 'PageSpeed analysis results including category scores (performanceScore, accessibilityScore, bestPracticesScore, seoScore), Core Web Vitals display values and numeric values (firstContentfulPaint, largestContentfulPaint, totalBlockingTime, cumulativeLayoutShift, speedIndex, interactive), and metadata (finalUrl, overallCategory, analysisTimestamp, lighthouseVersion)', + }, + }, +} diff --git a/apps/sim/blocks/blocks/pagerduty.ts b/apps/sim/blocks/blocks/pagerduty.ts new file mode 100644 index 0000000000..34e1336bb9 --- /dev/null +++ b/apps/sim/blocks/blocks/pagerduty.ts @@ -0,0 +1,482 @@ +import { PagerDutyIcon } from '@/components/icons' +import { AuthMode, type BlockConfig } from '@/blocks/types' + +export const PagerDutyBlock: BlockConfig = { + type: 'pagerduty', + name: 'PagerDuty', + description: 'Manage incidents and on-call schedules with PagerDuty', + longDescription: + 'Integrate PagerDuty into your workflow to list, create, and update incidents, add notes, list services, and check on-call schedules.', + docsLink: 'https://docs.sim.ai/tools/pagerduty', + category: 'tools', + bgColor: '#06AC38', + icon: PagerDutyIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Incidents', id: 'list_incidents' }, + { label: 'Create Incident', id: 'create_incident' }, + { label: 'Update Incident', id: 'update_incident' }, + { label: 'Add Note', id: 'add_note' }, + { label: 'List Services', id: 'list_services' }, + { label: 'List On-Calls', id: 'list_oncalls' }, + ], + value: () => 'list_incidents', + }, + + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your PagerDuty REST API Key', + password: true, + }, + + { + id: 'fromEmail', + title: 'From Email', + type: 'short-input', + required: { + field: 'operation', + value: ['create_incident', 'update_incident', 'add_note'], + }, + placeholder: 'Valid PagerDuty user email (required for write operations)', + condition: { + field: 'operation', + value: ['create_incident', 'update_incident', 'add_note'], + }, + }, + + // --- List Incidents fields --- + { + id: 'statuses', + title: 'Statuses', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Triggered', id: 'triggered' }, + { label: 'Acknowledged', id: 'acknowledged' }, + { label: 'Resolved', id: 'resolved' }, + ], + value: () => '', + condition: { field: 'operation', value: 'list_incidents' }, + }, + { + id: 'listServiceIds', + title: 'Service IDs', + type: 'short-input', + placeholder: 'Comma-separated service IDs to filter', + condition: { field: 'operation', value: 'list_incidents' }, + mode: 'advanced', + }, + { + id: 'listSince', + title: 'Since', + type: 'short-input', + placeholder: 'Start date (ISO 8601, e.g., 2024-01-01T00:00:00Z)', + condition: { field: 'operation', value: 'list_incidents' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'listUntil', + title: 'Until', + type: 'short-input', + placeholder: 'End date (ISO 8601, e.g., 2024-12-31T23:59:59Z)', + condition: { field: 'operation', value: 'list_incidents' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'listSortBy', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Created At (newest)', id: 'created_at:desc' }, + { label: 'Created At (oldest)', id: 'created_at:asc' }, + ], + value: () => 'created_at:desc', + condition: { field: 'operation', value: 'list_incidents' }, + mode: 'advanced', + }, + { + id: 'listLimit', + title: 'Limit', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'list_incidents' }, + mode: 'advanced', + }, + + // --- Create Incident fields --- + { + id: 'title', + title: 'Title', + type: 'short-input', + required: { field: 'operation', value: 'create_incident' }, + placeholder: 'Incident title/summary', + condition: { field: 'operation', value: 'create_incident' }, + }, + { + id: 'createServiceId', + title: 'Service ID', + type: 'short-input', + required: { field: 'operation', value: 'create_incident' }, + placeholder: 'PagerDuty service ID', + condition: { field: 'operation', value: 'create_incident' }, + }, + { + id: 'createUrgency', + title: 'Urgency', + type: 'dropdown', + options: [ + { label: 'High', id: 'high' }, + { label: 'Low', id: 'low' }, + ], + value: () => 'high', + condition: { field: 'operation', value: 'create_incident' }, + }, + { + id: 'body', + title: 'Description', + type: 'long-input', + placeholder: 'Detailed description of the incident', + condition: { field: 'operation', value: 'create_incident' }, + }, + { + id: 'escalationPolicyId', + title: 'Escalation Policy ID', + type: 'short-input', + placeholder: 'Escalation policy ID (optional)', + condition: { field: 'operation', value: 'create_incident' }, + mode: 'advanced', + }, + { + id: 'assigneeId', + title: 'Assignee User ID', + type: 'short-input', + placeholder: 'User ID to assign (optional)', + condition: { field: 'operation', value: 'create_incident' }, + mode: 'advanced', + }, + + // --- Update Incident fields --- + { + id: 'updateIncidentId', + title: 'Incident ID', + type: 'short-input', + required: { field: 'operation', value: 'update_incident' }, + placeholder: 'ID of the incident to update', + condition: { field: 'operation', value: 'update_incident' }, + }, + { + id: 'updateStatus', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'No Change', id: '' }, + { label: 'Acknowledged', id: 'acknowledged' }, + { label: 'Resolved', id: 'resolved' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_incident' }, + }, + { + id: 'updateTitle', + title: 'New Title', + type: 'short-input', + placeholder: 'New incident title (optional)', + condition: { field: 'operation', value: 'update_incident' }, + mode: 'advanced', + }, + { + id: 'updateUrgency', + title: 'Urgency', + type: 'dropdown', + options: [ + { label: 'No Change', id: '' }, + { label: 'High', id: 'high' }, + { label: 'Low', id: 'low' }, + ], + value: () => '', + condition: { field: 'operation', value: 'update_incident' }, + mode: 'advanced', + }, + { + id: 'updateEscalationLevel', + title: 'Escalation Level', + type: 'short-input', + placeholder: 'Escalation level number (e.g., 2)', + condition: { field: 'operation', value: 'update_incident' }, + mode: 'advanced', + }, + // --- Add Note fields --- + { + id: 'noteIncidentId', + title: 'Incident ID', + type: 'short-input', + required: { field: 'operation', value: 'add_note' }, + placeholder: 'ID of the incident', + condition: { field: 'operation', value: 'add_note' }, + }, + { + id: 'noteContent', + title: 'Note Content', + type: 'long-input', + required: { field: 'operation', value: 'add_note' }, + placeholder: 'Note text to add to the incident', + condition: { field: 'operation', value: 'add_note' }, + }, + + // --- List Services fields --- + { + id: 'serviceQuery', + title: 'Search Query', + type: 'short-input', + placeholder: 'Filter services by name', + condition: { field: 'operation', value: 'list_services' }, + }, + { + id: 'serviceLimit', + title: 'Limit', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'list_services' }, + mode: 'advanced', + }, + + // --- List On-Calls fields --- + { + id: 'oncallEscalationPolicyIds', + title: 'Escalation Policy IDs', + type: 'short-input', + placeholder: 'Comma-separated escalation policy IDs', + condition: { field: 'operation', value: 'list_oncalls' }, + }, + { + id: 'oncallScheduleIds', + title: 'Schedule IDs', + type: 'short-input', + placeholder: 'Comma-separated schedule IDs', + condition: { field: 'operation', value: 'list_oncalls' }, + mode: 'advanced', + }, + { + id: 'oncallLimit', + title: 'Limit', + type: 'short-input', + placeholder: '25', + condition: { field: 'operation', value: 'list_oncalls' }, + mode: 'advanced', + }, + { + id: 'oncallSince', + title: 'Since', + type: 'short-input', + placeholder: 'Start time (ISO 8601)', + condition: { field: 'operation', value: 'list_oncalls' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'oncallUntil', + title: 'Until', + type: 'short-input', + placeholder: 'End time (ISO 8601)', + condition: { field: 'operation', value: 'list_oncalls' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate an ISO 8601 timestamp. Return ONLY the timestamp string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + ], + + tools: { + access: [ + 'pagerduty_list_incidents', + 'pagerduty_create_incident', + 'pagerduty_update_incident', + 'pagerduty_add_note', + 'pagerduty_list_services', + 'pagerduty_list_oncalls', + ], + config: { + tool: (params) => `pagerduty_${params.operation}`, + params: (params) => { + const result: Record = {} + + switch (params.operation) { + case 'list_incidents': + if (params.statuses) result.statuses = params.statuses + if (params.listServiceIds) result.serviceIds = params.listServiceIds + if (params.listSince) result.since = params.listSince + if (params.listUntil) result.until = params.listUntil + if (params.listSortBy) result.sortBy = params.listSortBy + if (params.listLimit) result.limit = params.listLimit + break + + case 'create_incident': + if (params.createServiceId) result.serviceId = params.createServiceId + if (params.createUrgency) result.urgency = params.createUrgency + break + + case 'update_incident': + if (params.updateIncidentId) result.incidentId = params.updateIncidentId + if (params.updateStatus) result.status = params.updateStatus + if (params.updateTitle) result.title = params.updateTitle + if (params.updateUrgency) result.urgency = params.updateUrgency + if (params.updateEscalationLevel) result.escalationLevel = params.updateEscalationLevel + break + + case 'add_note': + if (params.noteIncidentId) result.incidentId = params.noteIncidentId + if (params.noteContent) result.content = params.noteContent + break + + case 'list_services': + if (params.serviceQuery) result.query = params.serviceQuery + if (params.serviceLimit) result.limit = params.serviceLimit + break + + case 'list_oncalls': + if (params.oncallEscalationPolicyIds) + result.escalationPolicyIds = params.oncallEscalationPolicyIds + if (params.oncallScheduleIds) result.scheduleIds = params.oncallScheduleIds + if (params.oncallSince) result.since = params.oncallSince + if (params.oncallUntil) result.until = params.oncallUntil + if (params.oncallLimit) result.limit = params.oncallLimit + break + } + + return result + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'PagerDuty REST API Key' }, + fromEmail: { type: 'string', description: 'Valid PagerDuty user email' }, + statuses: { type: 'string', description: 'Status filter for incidents' }, + listServiceIds: { type: 'string', description: 'Service IDs filter' }, + listSince: { type: 'string', description: 'Start date filter' }, + listUntil: { type: 'string', description: 'End date filter' }, + title: { type: 'string', description: 'Incident title' }, + createServiceId: { type: 'string', description: 'Service ID for new incident' }, + createUrgency: { type: 'string', description: 'Urgency level' }, + body: { type: 'string', description: 'Incident description' }, + updateIncidentId: { type: 'string', description: 'Incident ID to update' }, + updateStatus: { type: 'string', description: 'New status' }, + noteIncidentId: { type: 'string', description: 'Incident ID for note' }, + noteContent: { type: 'string', description: 'Note content' }, + escalationPolicyId: { type: 'string', description: 'Escalation policy ID' }, + assigneeId: { type: 'string', description: 'Assignee user ID' }, + updateTitle: { type: 'string', description: 'New incident title' }, + updateUrgency: { type: 'string', description: 'New urgency level' }, + updateEscalationLevel: { type: 'string', description: 'Escalation level number' }, + listSortBy: { type: 'string', description: 'Sort field' }, + listLimit: { type: 'string', description: 'Max results for incidents' }, + serviceQuery: { type: 'string', description: 'Service name filter' }, + serviceLimit: { type: 'string', description: 'Max results for services' }, + oncallEscalationPolicyIds: { type: 'string', description: 'Escalation policy IDs filter' }, + oncallScheduleIds: { type: 'string', description: 'Schedule IDs filter' }, + oncallSince: { type: 'string', description: 'On-call start time filter' }, + oncallUntil: { type: 'string', description: 'On-call end time filter' }, + oncallLimit: { type: 'string', description: 'Max results for on-calls' }, + }, + + outputs: { + incidents: { + type: 'json', + description: 'Array of incidents (list_incidents)', + }, + total: { + type: 'number', + description: 'Total count of results', + }, + more: { + type: 'boolean', + description: 'Whether more results are available', + }, + id: { + type: 'string', + description: 'Created/updated resource ID', + }, + incidentNumber: { + type: 'number', + description: 'Incident number', + }, + title: { + type: 'string', + description: 'Incident title', + }, + status: { + type: 'string', + description: 'Incident status', + }, + urgency: { + type: 'string', + description: 'Incident urgency', + }, + createdAt: { + type: 'string', + description: 'Creation timestamp', + }, + updatedAt: { + type: 'string', + description: 'Last updated timestamp', + }, + serviceName: { + type: 'string', + description: 'Service name', + }, + serviceId: { + type: 'string', + description: 'Service ID', + }, + htmlUrl: { + type: 'string', + description: 'PagerDuty web URL', + }, + content: { + type: 'string', + description: 'Note content (add_note)', + }, + userName: { + type: 'string', + description: 'User name (add_note)', + }, + services: { + type: 'json', + description: 'Array of services (list_services)', + }, + oncalls: { + type: 'json', + description: 'Array of on-call entries (list_oncalls)', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index b65005b316..0e3e2c695e 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -4,6 +4,7 @@ import { AhrefsBlock } from '@/blocks/blocks/ahrefs' import { AirtableBlock } from '@/blocks/blocks/airtable' import { AirweaveBlock } from '@/blocks/blocks/airweave' import { AlgoliaBlock } from '@/blocks/blocks/algolia' +import { AmplitudeBlock } from '@/blocks/blocks/amplitude' import { ApiBlock } from '@/blocks/blocks/api' import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger' import { ApifyBlock } from '@/blocks/blocks/apify' @@ -56,6 +57,7 @@ import { GoogleDriveBlock } from '@/blocks/blocks/google_drive' import { GoogleFormsBlock } from '@/blocks/blocks/google_forms' import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups' import { GoogleMapsBlock } from '@/blocks/blocks/google_maps' +import { GooglePagespeedBlock } from '@/blocks/blocks/google_pagespeed' import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets' import { GoogleSlidesBlock, GoogleSlidesV2Block } from '@/blocks/blocks/google_slides' import { GoogleTasksBlock } from '@/blocks/blocks/google_tasks' @@ -112,6 +114,7 @@ import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { OnePasswordBlock } from '@/blocks/blocks/onepassword' import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' +import { PagerDutyBlock } from '@/blocks/blocks/pagerduty' import { ParallelBlock } from '@/blocks/blocks/parallel' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' @@ -193,6 +196,7 @@ export const registry: Record = { airtable: AirtableBlock, airweave: AirweaveBlock, algolia: AlgoliaBlock, + amplitude: AmplitudeBlock, api: ApiBlock, api_trigger: ApiTriggerBlock, apify: ApifyBlock, @@ -250,6 +254,7 @@ export const registry: Record = { google_forms: GoogleFormsBlock, google_groups: GoogleGroupsBlock, google_maps: GoogleMapsBlock, + google_pagespeed: GooglePagespeedBlock, google_tasks: GoogleTasksBlock, google_translate: GoogleTranslateBlock, gong: GongBlock, @@ -313,6 +318,7 @@ export const registry: Record = { onedrive: OneDriveBlock, openai: OpenAIBlock, outlook: OutlookBlock, + pagerduty: PagerDutyBlock, parallel_ai: ParallelBlock, perplexity: PerplexityBlock, pinecone: PineconeBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 51cd709bb7..c4666fba17 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -1209,6 +1209,17 @@ export function AlgoliaIcon(props: SVGProps) { ) } +export function AmplitudeIcon(props: SVGProps) { + return ( + + + + ) +} + export function GoogleBooksIcon(props: SVGProps) { return ( @@ -1938,13 +1949,11 @@ export function ElevenLabsIcon(props: SVGProps) { export function LinkupIcon(props: SVGProps) { return ( - - - - + + ) } @@ -2453,6 +2462,17 @@ export function OutlookIcon(props: SVGProps) { ) } +export function PagerDutyIcon(props: SVGProps) { + return ( + + + + ) +} + export function MicrosoftExcelIcon(props: SVGProps) { const id = useId() const gradientId = `excel_gradient_${id}` @@ -3996,10 +4016,10 @@ export function IntercomIcon(props: SVGProps) { export function LoopsIcon(props: SVGProps) { return ( - + ) @@ -5578,6 +5598,35 @@ export function GoogleMapsIcon(props: SVGProps) { ) } +export function GooglePagespeedIcon(props: SVGProps) { + return ( + + + + + + + + + + ) +} + export function GoogleTranslateIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/amplitude/event_segmentation.ts b/apps/sim/tools/amplitude/event_segmentation.ts new file mode 100644 index 0000000000..d5cbab3357 --- /dev/null +++ b/apps/sim/tools/amplitude/event_segmentation.ts @@ -0,0 +1,134 @@ +import type { + AmplitudeEventSegmentationParams, + AmplitudeEventSegmentationResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const eventSegmentationTool: ToolConfig< + AmplitudeEventSegmentationParams, + AmplitudeEventSegmentationResponse +> = { + id: 'amplitude_event_segmentation', + name: 'Amplitude Event Segmentation', + description: + 'Query event analytics data with segmentation. Get event counts, uniques, averages, and more.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + eventType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Event type name to analyze', + }, + start: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYYMMDD format', + }, + end: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYYMMDD format', + }, + metric: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula (default: uniques)', + }, + interval: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Time interval: 1 (daily), 7 (weekly), or 30 (monthly)', + }, + groupBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Property name to group by (prefix custom user properties with "gp:")', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of group-by values (max 1000)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://amplitude.com/api/2/events/segmentation') + const eventObj = JSON.stringify({ event_type: params.eventType }) + url.searchParams.set('e', eventObj) + url.searchParams.set('start', params.start) + url.searchParams.set('end', params.end) + if (params.metric) url.searchParams.set('m', params.metric) + if (params.interval) url.searchParams.set('i', params.interval) + if (params.groupBy) url.searchParams.set('g', params.groupBy) + if (params.limit) url.searchParams.set('limit', params.limit) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude Event Segmentation API error: ${response.status}`) + } + + const result = data.data ?? {} + + return { + success: true, + output: { + series: result.series ?? [], + seriesLabels: result.seriesLabels ?? [], + seriesCollapsed: result.seriesCollapsed ?? [], + xValues: result.xValues ?? [], + }, + } + }, + + outputs: { + series: { + type: 'json', + description: 'Time-series data arrays indexed by series', + }, + seriesLabels: { + type: 'array', + description: 'Labels for each data series', + items: { type: 'string' }, + }, + seriesCollapsed: { + type: 'json', + description: 'Collapsed aggregate totals per series', + }, + xValues: { + type: 'array', + description: 'Date values for the x-axis', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/get_active_users.ts b/apps/sim/tools/amplitude/get_active_users.ts new file mode 100644 index 0000000000..6670e5ab15 --- /dev/null +++ b/apps/sim/tools/amplitude/get_active_users.ts @@ -0,0 +1,105 @@ +import type { + AmplitudeGetActiveUsersParams, + AmplitudeGetActiveUsersResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const getActiveUsersTool: ToolConfig< + AmplitudeGetActiveUsersParams, + AmplitudeGetActiveUsersResponse +> = { + id: 'amplitude_get_active_users', + name: 'Amplitude Get Active Users', + description: 'Get active or new user counts over a date range from the Dashboard REST API.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + start: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYYMMDD format', + }, + end: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYYMMDD format', + }, + metric: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Metric type: "active" or "new" (default: active)', + }, + interval: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Time interval: 1 (daily), 7 (weekly), or 30 (monthly)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://amplitude.com/api/2/users') + url.searchParams.set('start', params.start) + url.searchParams.set('end', params.end) + if (params.metric) url.searchParams.set('m', params.metric) + if (params.interval) url.searchParams.set('i', params.interval) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude Active Users API error: ${response.status}`) + } + + const result = data.data ?? {} + + return { + success: true, + output: { + series: result.series ?? [], + seriesMeta: result.seriesMeta ?? [], + xValues: result.xValues ?? [], + }, + } + }, + + outputs: { + series: { + type: 'json', + description: 'Array of data series with user counts per time interval', + }, + seriesMeta: { + type: 'array', + description: 'Metadata labels for each data series (e.g., segment names)', + items: { type: 'string' }, + }, + xValues: { + type: 'array', + description: 'Date values for the x-axis', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/get_revenue.ts b/apps/sim/tools/amplitude/get_revenue.ts new file mode 100644 index 0000000000..264eb1e0f9 --- /dev/null +++ b/apps/sim/tools/amplitude/get_revenue.ts @@ -0,0 +1,102 @@ +import type { + AmplitudeGetRevenueParams, + AmplitudeGetRevenueResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const getRevenueTool: ToolConfig = { + id: 'amplitude_get_revenue', + name: 'Amplitude Get Revenue', + description: 'Get revenue LTV data including ARPU, ARPPU, total revenue, and paying user counts.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + start: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Start date in YYYYMMDD format', + }, + end: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'End date in YYYYMMDD format', + }, + metric: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Metric: 0 (ARPU), 1 (ARPPU), 2 (Total Revenue), 3 (Paying Users)', + }, + interval: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Time interval: 1 (daily), 7 (weekly), or 30 (monthly)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://amplitude.com/api/2/revenue/ltv') + url.searchParams.set('start', params.start) + url.searchParams.set('end', params.end) + if (params.metric) url.searchParams.set('m', params.metric) + if (params.interval) url.searchParams.set('i', params.interval) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude Revenue API error: ${response.status}`) + } + + const result = data.data ?? {} + + return { + success: true, + output: { + series: result.series ?? [], + seriesLabels: result.seriesLabels ?? [], + xValues: result.xValues ?? [], + }, + } + }, + + outputs: { + series: { + type: 'json', + description: 'Array of revenue data series', + }, + seriesLabels: { + type: 'array', + description: 'Labels for each data series', + items: { type: 'string' }, + }, + xValues: { + type: 'array', + description: 'Date values for the x-axis', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/group_identify.ts b/apps/sim/tools/amplitude/group_identify.ts new file mode 100644 index 0000000000..b0bd548c49 --- /dev/null +++ b/apps/sim/tools/amplitude/group_identify.ts @@ -0,0 +1,99 @@ +import type { + AmplitudeGroupIdentifyParams, + AmplitudeGroupIdentifyResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const groupIdentifyTool: ToolConfig< + AmplitudeGroupIdentifyParams, + AmplitudeGroupIdentifyResponse +> = { + id: 'amplitude_group_identify', + name: 'Amplitude Group Identify', + description: + 'Set group-level properties in Amplitude. Supports $set, $setOnce, $add, $append, $unset operations.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + groupType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Group classification (e.g., "company", "org_id")', + }, + groupValue: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Specific group identifier (e.g., "Acme Corp")', + }, + groupProperties: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset.', + }, + }, + + request: { + url: 'https://api2.amplitude.com/groupidentify', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + let groupProperties: Record = {} + try { + groupProperties = JSON.parse(params.groupProperties) + } catch { + groupProperties = {} + } + + return { + api_key: params.apiKey, + identification: [ + { + group_type: params.groupType, + group_value: params.groupValue, + group_properties: groupProperties, + }, + ], + } + }, + }, + + transformResponse: async (response: Response) => { + const text = await response.text() + + if (!response.ok) { + throw new Error(`Amplitude Group Identify API error: ${text}`) + } + + return { + success: true, + output: { + code: response.status, + message: text || null, + }, + } + }, + + outputs: { + code: { + type: 'number', + description: 'HTTP response status code', + }, + message: { + type: 'string', + description: 'Response message', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/amplitude/identify_user.ts b/apps/sim/tools/amplitude/identify_user.ts new file mode 100644 index 0000000000..a0cb031680 --- /dev/null +++ b/apps/sim/tools/amplitude/identify_user.ts @@ -0,0 +1,97 @@ +import type { + AmplitudeIdentifyUserParams, + AmplitudeIdentifyUserResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const identifyUserTool: ToolConfig< + AmplitudeIdentifyUserParams, + AmplitudeIdentifyUserResponse +> = { + id: 'amplitude_identify_user', + name: 'Amplitude Identify User', + description: + 'Set user properties in Amplitude using the Identify API. Supports $set, $setOnce, $add, $append, $unset operations.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID (required if no device_id)', + }, + deviceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Device ID (required if no user_id)', + }, + userProperties: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset.', + }, + }, + + request: { + url: 'https://api2.amplitude.com/identify', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const identification: Record = {} + + if (params.userId) identification.user_id = params.userId + if (params.deviceId) identification.device_id = params.deviceId + + try { + identification.user_properties = JSON.parse(params.userProperties) + } catch { + identification.user_properties = {} + } + + return { + api_key: params.apiKey, + identification: [identification], + } + }, + }, + + transformResponse: async (response: Response) => { + const text = await response.text() + + if (!response.ok) { + throw new Error(`Amplitude Identify API error: ${text}`) + } + + return { + success: true, + output: { + code: response.status, + message: text || null, + }, + } + }, + + outputs: { + code: { + type: 'number', + description: 'HTTP response status code', + }, + message: { + type: 'string', + description: 'Response message', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/amplitude/index.ts b/apps/sim/tools/amplitude/index.ts new file mode 100644 index 0000000000..b0f1aab792 --- /dev/null +++ b/apps/sim/tools/amplitude/index.ts @@ -0,0 +1,23 @@ +import { eventSegmentationTool } from '@/tools/amplitude/event_segmentation' +import { getActiveUsersTool } from '@/tools/amplitude/get_active_users' +import { getRevenueTool } from '@/tools/amplitude/get_revenue' +import { groupIdentifyTool } from '@/tools/amplitude/group_identify' +import { identifyUserTool } from '@/tools/amplitude/identify_user' +import { listEventsTool } from '@/tools/amplitude/list_events' +import { realtimeActiveUsersTool } from '@/tools/amplitude/realtime_active_users' +import { sendEventTool } from '@/tools/amplitude/send_event' +import { userActivityTool } from '@/tools/amplitude/user_activity' +import { userProfileTool } from '@/tools/amplitude/user_profile' +import { userSearchTool } from '@/tools/amplitude/user_search' + +export const amplitudeSendEventTool = sendEventTool +export const amplitudeIdentifyUserTool = identifyUserTool +export const amplitudeGroupIdentifyTool = groupIdentifyTool +export const amplitudeUserSearchTool = userSearchTool +export const amplitudeUserActivityTool = userActivityTool +export const amplitudeUserProfileTool = userProfileTool +export const amplitudeEventSegmentationTool = eventSegmentationTool +export const amplitudeGetActiveUsersTool = getActiveUsersTool +export const amplitudeRealtimeActiveUsersTool = realtimeActiveUsersTool +export const amplitudeListEventsTool = listEventsTool +export const amplitudeGetRevenueTool = getRevenueTool diff --git a/apps/sim/tools/amplitude/list_events.ts b/apps/sim/tools/amplitude/list_events.ts new file mode 100644 index 0000000000..fa89d3ddf1 --- /dev/null +++ b/apps/sim/tools/amplitude/list_events.ts @@ -0,0 +1,79 @@ +import type { + AmplitudeListEventsParams, + AmplitudeListEventsResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const listEventsTool: ToolConfig = { + id: 'amplitude_list_events', + name: 'Amplitude List Events', + description: + 'List all event types in the Amplitude project with their weekly totals and unique counts.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + }, + + request: { + url: 'https://amplitude.com/api/2/events/list', + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude List Events API error: ${response.status}`) + } + + const events = (data.data ?? []).map( + (e: Record) => + ({ + value: (e.value as string) ?? '', + displayName: (e.display as string) ?? null, + totals: (e.totals as number) ?? 0, + hidden: (e.hidden as boolean) ?? false, + deleted: (e.deleted as boolean) ?? false, + }) as const + ) + + return { + success: true, + output: { + events, + }, + } + }, + + outputs: { + events: { + type: 'array', + description: 'List of event types in the project', + items: { + type: 'object', + properties: { + value: { type: 'string', description: 'Event type name' }, + displayName: { type: 'string', description: 'Event display name' }, + totals: { type: 'number', description: 'Weekly total count' }, + hidden: { type: 'boolean', description: 'Whether the event is hidden' }, + deleted: { type: 'boolean', description: 'Whether the event is deleted' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/realtime_active_users.ts b/apps/sim/tools/amplitude/realtime_active_users.ts new file mode 100644 index 0000000000..0462d699dc --- /dev/null +++ b/apps/sim/tools/amplitude/realtime_active_users.ts @@ -0,0 +1,74 @@ +import type { + AmplitudeRealtimeActiveUsersParams, + AmplitudeRealtimeActiveUsersResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const realtimeActiveUsersTool: ToolConfig< + AmplitudeRealtimeActiveUsersParams, + AmplitudeRealtimeActiveUsersResponse +> = { + id: 'amplitude_realtime_active_users', + name: 'Amplitude Real-time Active Users', + description: 'Get real-time active user counts at 5-minute granularity for the last 2 days.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + }, + + request: { + url: 'https://amplitude.com/api/2/realtime', + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude Real-time API error: ${response.status}`) + } + + const result = data.data ?? {} + + return { + success: true, + output: { + series: result.series ?? [], + seriesLabels: result.seriesLabels ?? [], + xValues: result.xValues ?? [], + }, + } + }, + + outputs: { + series: { + type: 'json', + description: 'Array of data series with active user counts at 5-minute intervals', + }, + seriesLabels: { + type: 'array', + description: 'Labels for each series (e.g., "Today", "Yesterday")', + items: { type: 'string' }, + }, + xValues: { + type: 'array', + description: 'Time values for the x-axis (e.g., "15:00", "15:05")', + items: { type: 'string' }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/send_event.ts b/apps/sim/tools/amplitude/send_event.ts new file mode 100644 index 0000000000..a24c0a45f3 --- /dev/null +++ b/apps/sim/tools/amplitude/send_event.ts @@ -0,0 +1,214 @@ +import type { AmplitudeSendEventParams, AmplitudeSendEventResponse } from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const sendEventTool: ToolConfig = { + id: 'amplitude_send_event', + name: 'Amplitude Send Event', + description: 'Track an event in Amplitude using the HTTP V2 API.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID (required if no device_id)', + }, + deviceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Device ID (required if no user_id)', + }, + eventType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the event (e.g., "page_view", "purchase")', + }, + eventProperties: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object of custom event properties', + }, + userProperties: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'JSON object of user properties to set (supports $set, $setOnce, $add, $append, $unset)', + }, + time: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Event timestamp in milliseconds since epoch', + }, + sessionId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Session start time in milliseconds since epoch', + }, + insertId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Unique ID for deduplication (within 7-day window)', + }, + appVersion: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Application version string', + }, + platform: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Platform (e.g., "Web", "iOS", "Android")', + }, + country: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Two-letter country code', + }, + language: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Language code (e.g., "en")', + }, + ip: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'IP address for geo-location', + }, + price: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Price of the item purchased', + }, + quantity: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Quantity of items purchased', + }, + revenue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Revenue amount', + }, + productId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Product identifier', + }, + revenueType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Revenue type (e.g., "purchase", "refund")', + }, + }, + + request: { + url: 'https://api2.amplitude.com/2/httpapi', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => { + const event: Record = { + event_type: params.eventType, + } + + if (params.userId) event.user_id = params.userId + if (params.deviceId) event.device_id = params.deviceId + if (params.time) event.time = Number(params.time) + if (params.sessionId) event.session_id = Number(params.sessionId) + if (params.insertId) event.insert_id = params.insertId + if (params.appVersion) event.app_version = params.appVersion + if (params.platform) event.platform = params.platform + if (params.country) event.country = params.country + if (params.language) event.language = params.language + if (params.ip) event.ip = params.ip + if (params.price) event.price = Number(params.price) + if (params.quantity) event.quantity = Number(params.quantity) + if (params.revenue) event.revenue = Number(params.revenue) + if (params.productId) event.product_id = params.productId + if (params.revenueType) event.revenue_type = params.revenueType + + if (params.eventProperties) { + try { + event.event_properties = JSON.parse(params.eventProperties) + } catch { + event.event_properties = {} + } + } + + if (params.userProperties) { + try { + event.user_properties = JSON.parse(params.userProperties) + } catch { + event.user_properties = {} + } + } + + return { + api_key: params.apiKey, + events: [event], + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (data.code !== 200) { + throw new Error(data.error || `Amplitude API error: code ${data.code}`) + } + + return { + success: true, + output: { + code: data.code ?? 200, + eventsIngested: data.events_ingested ?? 0, + payloadSizeBytes: data.payload_size_bytes ?? 0, + serverUploadTime: data.server_upload_time ?? 0, + }, + } + }, + + outputs: { + code: { + type: 'number', + description: 'Response code (200 for success)', + }, + eventsIngested: { + type: 'number', + description: 'Number of events ingested', + }, + payloadSizeBytes: { + type: 'number', + description: 'Size of the payload in bytes', + }, + serverUploadTime: { + type: 'number', + description: 'Server upload timestamp', + }, + }, +} diff --git a/apps/sim/tools/amplitude/types.ts b/apps/sim/tools/amplitude/types.ts new file mode 100644 index 0000000000..737de7c8e4 --- /dev/null +++ b/apps/sim/tools/amplitude/types.ts @@ -0,0 +1,241 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Base params shared by endpoints using API key in body. + */ +export interface AmplitudeApiKeyParams { + apiKey: string +} + +/** + * Base params shared by endpoints using Basic Auth (api_key:secret_key). + */ +export interface AmplitudeBasicAuthParams { + apiKey: string + secretKey: string +} + +/** + * Send Event params (HTTP V2 API). + */ +export interface AmplitudeSendEventParams extends AmplitudeApiKeyParams { + userId?: string + deviceId?: string + eventType: string + eventProperties?: string + userProperties?: string + time?: string + sessionId?: string + insertId?: string + appVersion?: string + platform?: string + country?: string + language?: string + ip?: string + price?: string + quantity?: string + revenue?: string + productId?: string + revenueType?: string +} + +export interface AmplitudeSendEventResponse extends ToolResponse { + output: { + code: number + eventsIngested: number + payloadSizeBytes: number + serverUploadTime: number + } +} + +/** + * Identify User params (Identify API). + */ +export interface AmplitudeIdentifyUserParams extends AmplitudeApiKeyParams { + userId?: string + deviceId?: string + userProperties: string +} + +export interface AmplitudeIdentifyUserResponse extends ToolResponse { + output: { + code: number + message: string | null + } +} + +/** + * Group Identify params (Group Identify API). + */ +export interface AmplitudeGroupIdentifyParams extends AmplitudeApiKeyParams { + groupType: string + groupValue: string + groupProperties: string +} + +export interface AmplitudeGroupIdentifyResponse extends ToolResponse { + output: { + code: number + message: string | null + } +} + +/** + * User Search params (Dashboard REST API). + */ +export interface AmplitudeUserSearchParams extends AmplitudeBasicAuthParams { + user: string +} + +export interface AmplitudeUserSearchResponse extends ToolResponse { + output: { + matches: Array<{ + amplitudeId: number + userId: string | null + }> + type: string | null + } +} + +/** + * User Activity params (Dashboard REST API). + */ +export interface AmplitudeUserActivityParams extends AmplitudeBasicAuthParams { + amplitudeId: string + offset?: string + limit?: string + direction?: string +} + +export interface AmplitudeUserActivityResponse extends ToolResponse { + output: { + events: Array<{ + eventType: string + eventTime: string + eventProperties: Record + userProperties: Record + sessionId: number | null + platform: string | null + country: string | null + city: string | null + }> + userData: { + userId: string | null + canonicalAmplitudeId: number | null + numEvents: number | null + numSessions: number | null + platform: string | null + country: string | null + } | null + } +} + +/** + * User Profile params (User Profile API). + */ +export interface AmplitudeUserProfileParams { + secretKey: string + userId?: string + deviceId?: string + getAmpProps?: string + getCohortIds?: string + getComputations?: string +} + +export interface AmplitudeUserProfileResponse extends ToolResponse { + output: { + userId: string | null + deviceId: string | null + ampProps: Record | null + cohortIds: string[] | null + computations: Record | null + } +} + +/** + * Event Segmentation params (Dashboard REST API). + */ +export interface AmplitudeEventSegmentationParams extends AmplitudeBasicAuthParams { + eventType: string + start: string + end: string + metric?: string + interval?: string + groupBy?: string + limit?: string +} + +export interface AmplitudeEventSegmentationResponse extends ToolResponse { + output: { + series: unknown[] + seriesLabels: string[] + seriesCollapsed: unknown[] + xValues: string[] + } +} + +/** + * Get Active Users params (Dashboard REST API). + */ +export interface AmplitudeGetActiveUsersParams extends AmplitudeBasicAuthParams { + start: string + end: string + metric?: string + interval?: string +} + +export interface AmplitudeGetActiveUsersResponse extends ToolResponse { + output: { + series: number[][] + seriesMeta: string[] + xValues: string[] + } +} + +/** + * Real-time Active Users params (Dashboard REST API). + */ +export interface AmplitudeRealtimeActiveUsersParams extends AmplitudeBasicAuthParams {} + +export interface AmplitudeRealtimeActiveUsersResponse extends ToolResponse { + output: { + series: number[][] + seriesLabels: string[] + xValues: string[] + } +} + +/** + * List Events params (Dashboard REST API). + */ +export interface AmplitudeListEventsParams extends AmplitudeBasicAuthParams {} + +export interface AmplitudeListEventsResponse extends ToolResponse { + output: { + events: Array<{ + value: string + displayName: string | null + totals: number + hidden: boolean + deleted: boolean + }> + } +} + +/** + * Get Revenue params (Dashboard REST API). + */ +export interface AmplitudeGetRevenueParams extends AmplitudeBasicAuthParams { + start: string + end: string + metric?: string + interval?: string +} + +export interface AmplitudeGetRevenueResponse extends ToolResponse { + output: { + series: unknown[] + seriesLabels: string[] + xValues: string[] + } +} diff --git a/apps/sim/tools/amplitude/user_activity.ts b/apps/sim/tools/amplitude/user_activity.ts new file mode 100644 index 0000000000..1dfa504e55 --- /dev/null +++ b/apps/sim/tools/amplitude/user_activity.ts @@ -0,0 +1,144 @@ +import type { + AmplitudeUserActivityParams, + AmplitudeUserActivityResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const userActivityTool: ToolConfig< + AmplitudeUserActivityParams, + AmplitudeUserActivityResponse +> = { + id: 'amplitude_user_activity', + name: 'Amplitude User Activity', + description: 'Get the event stream for a specific user by their Amplitude ID.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + amplitudeId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Amplitude internal user ID', + }, + offset: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Offset for pagination (default 0)', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of events to return (default 1000, max 1000)', + }, + direction: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort direction: "latest" or "earliest" (default: latest)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://amplitude.com/api/2/useractivity') + url.searchParams.set('user', params.amplitudeId.trim()) + if (params.offset) url.searchParams.set('offset', params.offset) + if (params.limit) url.searchParams.set('limit', params.limit) + if (params.direction) url.searchParams.set('direction', params.direction) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude User Activity API error: ${response.status}`) + } + + const events = (data.events ?? []).map( + (e: Record) => + ({ + eventType: (e.event_type as string) ?? '', + eventTime: (e.event_time as string) ?? '', + eventProperties: (e.event_properties as Record) ?? {}, + userProperties: (e.user_properties as Record) ?? {}, + sessionId: (e.session_id as number) ?? null, + platform: (e.platform as string) ?? null, + country: (e.country as string) ?? null, + city: (e.city as string) ?? null, + }) as const + ) + + const ud = data.userData as Record | undefined + const userData = ud + ? { + userId: (ud.user_id as string) ?? null, + canonicalAmplitudeId: (ud.canonical_amplitude_id as number) ?? null, + numEvents: (ud.num_events as number) ?? null, + numSessions: (ud.num_sessions as number) ?? null, + platform: (ud.platform as string) ?? null, + country: (ud.country as string) ?? null, + } + : null + + return { + success: true, + output: { + events, + userData, + }, + } + }, + + outputs: { + events: { + type: 'array', + description: 'List of user events', + items: { + type: 'object', + properties: { + eventType: { type: 'string', description: 'Type of event' }, + eventTime: { type: 'string', description: 'Event timestamp' }, + eventProperties: { type: 'json', description: 'Custom event properties' }, + userProperties: { type: 'json', description: 'User properties at event time' }, + sessionId: { type: 'number', description: 'Session ID' }, + platform: { type: 'string', description: 'Platform' }, + country: { type: 'string', description: 'Country' }, + city: { type: 'string', description: 'City' }, + }, + }, + }, + userData: { + type: 'json', + description: 'User metadata', + optional: true, + properties: { + userId: { type: 'string', description: 'External user ID' }, + canonicalAmplitudeId: { type: 'number', description: 'Canonical Amplitude ID' }, + numEvents: { type: 'number', description: 'Total event count' }, + numSessions: { type: 'number', description: 'Total session count' }, + platform: { type: 'string', description: 'Primary platform' }, + country: { type: 'string', description: 'Country' }, + }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/user_profile.ts b/apps/sim/tools/amplitude/user_profile.ts new file mode 100644 index 0000000000..05395d7e4c --- /dev/null +++ b/apps/sim/tools/amplitude/user_profile.ts @@ -0,0 +1,120 @@ +import type { + AmplitudeUserProfileParams, + AmplitudeUserProfileResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const userProfileTool: ToolConfig = + { + id: 'amplitude_user_profile', + name: 'Amplitude User Profile', + description: + 'Get a user profile including properties, cohort memberships, and computed properties.', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + userId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'External user ID (required if no device_id)', + }, + deviceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Device ID (required if no user_id)', + }, + getAmpProps: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Include Amplitude user properties (true/false, default: false)', + }, + getCohortIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Include cohort IDs the user belongs to (true/false, default: false)', + }, + getComputations: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Include computed user properties (true/false, default: false)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://profile-api.amplitude.com/v1/userprofile') + if (params.userId) url.searchParams.set('user_id', params.userId.trim()) + if (params.deviceId) url.searchParams.set('device_id', params.deviceId.trim()) + if (params.getAmpProps) url.searchParams.set('get_amp_props', params.getAmpProps) + if (params.getCohortIds) url.searchParams.set('get_cohort_ids', params.getCohortIds) + if (params.getComputations) url.searchParams.set('get_computations', params.getComputations) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Api-Key ${params.secretKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude User Profile API error: ${response.status}`) + } + + const userData = data.userData ?? {} + + return { + success: true, + output: { + userId: (userData.user_id as string) ?? null, + deviceId: (userData.device_id as string) ?? null, + ampProps: (userData.amp_props as Record) ?? null, + cohortIds: (userData.cohort_ids as string[]) ?? null, + computations: (userData.computations as Record) ?? null, + }, + } + }, + + outputs: { + userId: { + type: 'string', + description: 'External user ID', + optional: true, + }, + deviceId: { + type: 'string', + description: 'Device ID', + optional: true, + }, + ampProps: { + type: 'json', + description: + 'Amplitude user properties (library, first_used, last_used, custom properties)', + optional: true, + }, + cohortIds: { + type: 'array', + description: 'List of cohort IDs the user belongs to', + optional: true, + items: { type: 'string' }, + }, + computations: { + type: 'json', + description: 'Computed user properties', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/amplitude/user_search.ts b/apps/sim/tools/amplitude/user_search.ts new file mode 100644 index 0000000000..91d8d2b9af --- /dev/null +++ b/apps/sim/tools/amplitude/user_search.ts @@ -0,0 +1,89 @@ +import type { + AmplitudeUserSearchParams, + AmplitudeUserSearchResponse, +} from '@/tools/amplitude/types' +import type { ToolConfig } from '@/tools/types' + +export const userSearchTool: ToolConfig = { + id: 'amplitude_user_search', + name: 'Amplitude User Search', + description: + 'Search for a user by User ID, Device ID, or Amplitude ID using the Dashboard REST API.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude API Key', + }, + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Amplitude Secret Key', + }, + user: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'User ID, Device ID, or Amplitude ID to search for', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://amplitude.com/api/2/usersearch') + url.searchParams.set('user', params.user.trim()) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || `Amplitude User Search API error: ${response.status}`) + } + + const matches = (data.matches ?? []).map( + (m: Record) => + ({ + amplitudeId: (m.amplitude_id as number) ?? 0, + userId: (m.user_id as string) ?? null, + }) as const + ) + + return { + success: true, + output: { + matches, + type: (data.type as string) ?? null, + }, + } + }, + + outputs: { + matches: { + type: 'array', + description: 'List of matching users', + items: { + type: 'object', + properties: { + amplitudeId: { type: 'number', description: 'Amplitude internal user ID' }, + userId: { type: 'string', description: 'External user ID' }, + }, + }, + }, + type: { + type: 'string', + description: 'Match type (e.g., match_user_or_device_id)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_pagespeed/analyze.ts b/apps/sim/tools/google_pagespeed/analyze.ts new file mode 100644 index 0000000000..a5fc0cfa5e --- /dev/null +++ b/apps/sim/tools/google_pagespeed/analyze.ts @@ -0,0 +1,223 @@ +import type { + GooglePagespeedAnalyzeParams, + GooglePagespeedAnalyzeResponse, +} from '@/tools/google_pagespeed/types' +import type { ToolConfig } from '@/tools/types' + +export const analyzeTool: ToolConfig = + { + id: 'google_pagespeed_analyze', + name: 'Google PageSpeed Analyze', + description: + 'Analyze a webpage for performance, accessibility, SEO, and best practices using Google PageSpeed Insights.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google PageSpeed Insights API Key', + }, + url: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The URL of the webpage to analyze', + }, + category: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Lighthouse categories to analyze (comma-separated): performance, accessibility, best-practices, seo', + }, + strategy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Analysis strategy: desktop or mobile', + }, + locale: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Locale for results (e.g., en, fr, de)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://www.googleapis.com/pagespeedonline/v5/runPagespeed') + url.searchParams.append('url', params.url.trim()) + url.searchParams.append('key', params.apiKey) + + if (params.category) { + const categories = params.category.split(',').map((c) => c.trim()) + for (const cat of categories) { + url.searchParams.append('category', cat) + } + } else { + url.searchParams.append('category', 'performance') + url.searchParams.append('category', 'accessibility') + url.searchParams.append('category', 'best-practices') + url.searchParams.append('category', 'seo') + } + + if (params.strategy) { + url.searchParams.append('strategy', params.strategy) + } + if (params.locale) { + url.searchParams.append('locale', params.locale) + } + + 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 ?? 'Failed to analyze page') + } + + const lighthouse = data.lighthouseResult ?? {} + const categories = lighthouse.categories ?? {} + const audits = lighthouse.audits ?? {} + const loadingExperience = data.loadingExperience ?? {} + + return { + success: true, + output: { + finalUrl: data.id ?? null, + performanceScore: categories.performance?.score ?? null, + accessibilityScore: categories.accessibility?.score ?? null, + bestPracticesScore: categories['best-practices']?.score ?? null, + seoScore: categories.seo?.score ?? null, + firstContentfulPaint: audits['first-contentful-paint']?.displayValue ?? null, + firstContentfulPaintMs: audits['first-contentful-paint']?.numericValue ?? null, + largestContentfulPaint: audits['largest-contentful-paint']?.displayValue ?? null, + largestContentfulPaintMs: audits['largest-contentful-paint']?.numericValue ?? null, + totalBlockingTime: audits['total-blocking-time']?.displayValue ?? null, + totalBlockingTimeMs: audits['total-blocking-time']?.numericValue ?? null, + cumulativeLayoutShift: audits['cumulative-layout-shift']?.displayValue ?? null, + cumulativeLayoutShiftValue: audits['cumulative-layout-shift']?.numericValue ?? null, + speedIndex: audits['speed-index']?.displayValue ?? null, + speedIndexMs: audits['speed-index']?.numericValue ?? null, + interactive: audits.interactive?.displayValue ?? null, + interactiveMs: audits.interactive?.numericValue ?? null, + overallCategory: loadingExperience.overall_category ?? null, + analysisTimestamp: data.analysisUTCTimestamp ?? null, + lighthouseVersion: lighthouse.lighthouseVersion ?? null, + }, + } + }, + + outputs: { + finalUrl: { + type: 'string', + description: 'The final URL after redirects', + optional: true, + }, + performanceScore: { + type: 'number', + description: 'Performance category score (0-1)', + optional: true, + }, + accessibilityScore: { + type: 'number', + description: 'Accessibility category score (0-1)', + optional: true, + }, + bestPracticesScore: { + type: 'number', + description: 'Best Practices category score (0-1)', + optional: true, + }, + seoScore: { + type: 'number', + description: 'SEO category score (0-1)', + optional: true, + }, + firstContentfulPaint: { + type: 'string', + description: 'Time to First Contentful Paint (display value)', + optional: true, + }, + firstContentfulPaintMs: { + type: 'number', + description: 'Time to First Contentful Paint in milliseconds', + optional: true, + }, + largestContentfulPaint: { + type: 'string', + description: 'Time to Largest Contentful Paint (display value)', + optional: true, + }, + largestContentfulPaintMs: { + type: 'number', + description: 'Time to Largest Contentful Paint in milliseconds', + optional: true, + }, + totalBlockingTime: { + type: 'string', + description: 'Total Blocking Time (display value)', + optional: true, + }, + totalBlockingTimeMs: { + type: 'number', + description: 'Total Blocking Time in milliseconds', + optional: true, + }, + cumulativeLayoutShift: { + type: 'string', + description: 'Cumulative Layout Shift (display value)', + optional: true, + }, + cumulativeLayoutShiftValue: { + type: 'number', + description: 'Cumulative Layout Shift numeric value', + optional: true, + }, + speedIndex: { + type: 'string', + description: 'Speed Index (display value)', + optional: true, + }, + speedIndexMs: { + type: 'number', + description: 'Speed Index in milliseconds', + optional: true, + }, + interactive: { + type: 'string', + description: 'Time to Interactive (display value)', + optional: true, + }, + interactiveMs: { + type: 'number', + description: 'Time to Interactive in milliseconds', + optional: true, + }, + overallCategory: { + type: 'string', + description: 'Overall loading experience category (FAST, AVERAGE, SLOW, or NONE)', + optional: true, + }, + analysisTimestamp: { + type: 'string', + description: 'UTC timestamp of the analysis', + optional: true, + }, + lighthouseVersion: { + type: 'string', + description: 'Version of Lighthouse used for the analysis', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/google_pagespeed/index.ts b/apps/sim/tools/google_pagespeed/index.ts new file mode 100644 index 0000000000..61d688f0be --- /dev/null +++ b/apps/sim/tools/google_pagespeed/index.ts @@ -0,0 +1,5 @@ +import { analyzeTool } from '@/tools/google_pagespeed/analyze' + +export const googlePagespeedAnalyzeTool = analyzeTool + +export * from '@/tools/google_pagespeed/types' diff --git a/apps/sim/tools/google_pagespeed/types.ts b/apps/sim/tools/google_pagespeed/types.ts new file mode 100644 index 0000000000..77f49aeee6 --- /dev/null +++ b/apps/sim/tools/google_pagespeed/types.ts @@ -0,0 +1,37 @@ +import type { ToolResponse } from '@/tools/types' + +export interface GooglePagespeedBaseParams { + apiKey: string +} + +export interface GooglePagespeedAnalyzeParams extends GooglePagespeedBaseParams { + url: string + category?: string + strategy?: string + locale?: string +} + +export interface GooglePagespeedAnalyzeResponse extends ToolResponse { + output: { + finalUrl: string | null + performanceScore: number | null + accessibilityScore: number | null + bestPracticesScore: number | null + seoScore: number | null + firstContentfulPaint: string | null + firstContentfulPaintMs: number | null + largestContentfulPaint: string | null + largestContentfulPaintMs: number | null + totalBlockingTime: string | null + totalBlockingTimeMs: number | null + cumulativeLayoutShift: string | null + cumulativeLayoutShiftValue: number | null + speedIndex: string | null + speedIndexMs: number | null + interactive: string | null + interactiveMs: number | null + overallCategory: string | null + analysisTimestamp: string | null + lighthouseVersion: string | null + } +} diff --git a/apps/sim/tools/pagerduty/add_note.ts b/apps/sim/tools/pagerduty/add_note.ts new file mode 100644 index 0000000000..5c900c1561 --- /dev/null +++ b/apps/sim/tools/pagerduty/add_note.ts @@ -0,0 +1,78 @@ +import type { PagerDutyAddNoteParams, PagerDutyAddNoteResponse } from '@/tools/pagerduty/types' +import type { ToolConfig } from '@/tools/types' + +export const addNoteTool: ToolConfig = { + id: 'pagerduty_add_note', + name: 'PagerDuty Add Note', + description: 'Add a note to an existing PagerDuty incident.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PagerDuty REST API Key', + }, + fromEmail: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address of a valid PagerDuty user', + }, + incidentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the incident to add the note to', + }, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Note content text', + }, + }, + + request: { + url: (params) => `https://api.pagerduty.com/incidents/${params.incidentId.trim()}/notes`, + method: 'POST', + headers: (params) => ({ + Authorization: `Token token=${params.apiKey}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + From: params.fromEmail, + }), + body: (params) => ({ + note: { + content: params.content, + }, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || `PagerDuty API error: ${response.status}`) + } + + const note = data.note ?? {} + return { + success: true, + output: { + id: note.id ?? null, + content: note.content ?? null, + createdAt: note.created_at ?? null, + userName: note.user?.summary ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Note ID' }, + content: { type: 'string', description: 'Note content' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + userName: { type: 'string', description: 'Name of the user who created the note' }, + }, +} diff --git a/apps/sim/tools/pagerduty/create_incident.ts b/apps/sim/tools/pagerduty/create_incident.ts new file mode 100644 index 0000000000..6a4c98854f --- /dev/null +++ b/apps/sim/tools/pagerduty/create_incident.ts @@ -0,0 +1,149 @@ +import type { + PagerDutyCreateIncidentParams, + PagerDutyCreateIncidentResponse, +} from '@/tools/pagerduty/types' +import type { ToolConfig } from '@/tools/types' + +export const createIncidentTool: ToolConfig< + PagerDutyCreateIncidentParams, + PagerDutyCreateIncidentResponse +> = { + id: 'pagerduty_create_incident', + name: 'PagerDuty Create Incident', + description: 'Create a new incident in PagerDuty.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PagerDuty REST API Key', + }, + fromEmail: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address of a valid PagerDuty user', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Incident title/summary', + }, + serviceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the PagerDuty service', + }, + urgency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Urgency level (high or low)', + }, + body: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Detailed description of the incident', + }, + escalationPolicyId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Escalation policy ID to assign', + }, + assigneeId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID to assign the incident to', + }, + }, + + request: { + url: 'https://api.pagerduty.com/incidents', + method: 'POST', + headers: (params) => ({ + Authorization: `Token token=${params.apiKey}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + From: params.fromEmail, + }), + body: (params) => { + const incident: Record = { + type: 'incident', + title: params.title, + service: { + id: params.serviceId, + type: 'service_reference', + }, + } + + if (params.urgency) incident.urgency = params.urgency + if (params.body) { + incident.body = { + type: 'incident_body', + details: params.body, + } + } + if (params.escalationPolicyId) { + incident.escalation_policy = { + id: params.escalationPolicyId, + type: 'escalation_policy_reference', + } + } + if (params.assigneeId) { + incident.assignments = [ + { + assignee: { + id: params.assigneeId, + type: 'user_reference', + }, + }, + ] + } + + return { incident } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || `PagerDuty API error: ${response.status}`) + } + + const inc = data.incident ?? {} + return { + success: true, + output: { + id: inc.id ?? null, + incidentNumber: inc.incident_number ?? null, + title: inc.title ?? null, + status: inc.status ?? null, + urgency: inc.urgency ?? null, + createdAt: inc.created_at ?? null, + serviceName: inc.service?.summary ?? null, + serviceId: inc.service?.id ?? null, + htmlUrl: inc.html_url ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Created incident ID' }, + incidentNumber: { type: 'number', description: 'Incident number' }, + title: { type: 'string', description: 'Incident title' }, + status: { type: 'string', description: 'Incident status' }, + urgency: { type: 'string', description: 'Incident urgency' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + serviceName: { type: 'string', description: 'Service name' }, + serviceId: { type: 'string', description: 'Service ID' }, + htmlUrl: { type: 'string', description: 'PagerDuty web URL' }, + }, +} diff --git a/apps/sim/tools/pagerduty/index.ts b/apps/sim/tools/pagerduty/index.ts new file mode 100644 index 0000000000..e6ee2bc34b --- /dev/null +++ b/apps/sim/tools/pagerduty/index.ts @@ -0,0 +1,13 @@ +import { addNoteTool } from '@/tools/pagerduty/add_note' +import { createIncidentTool } from '@/tools/pagerduty/create_incident' +import { listIncidentsTool } from '@/tools/pagerduty/list_incidents' +import { listOncallsTool } from '@/tools/pagerduty/list_oncalls' +import { listServicesTool } from '@/tools/pagerduty/list_services' +import { updateIncidentTool } from '@/tools/pagerduty/update_incident' + +export const pagerdutyListIncidentsTool = listIncidentsTool +export const pagerdutyCreateIncidentTool = createIncidentTool +export const pagerdutyUpdateIncidentTool = updateIncidentTool +export const pagerdutyAddNoteTool = addNoteTool +export const pagerdutyListServicesTool = listServicesTool +export const pagerdutyListOncallsTool = listOncallsTool diff --git a/apps/sim/tools/pagerduty/list_incidents.ts b/apps/sim/tools/pagerduty/list_incidents.ts new file mode 100644 index 0000000000..a2ed353076 --- /dev/null +++ b/apps/sim/tools/pagerduty/list_incidents.ts @@ -0,0 +1,161 @@ +import type { + PagerDutyListIncidentsParams, + PagerDutyListIncidentsResponse, +} from '@/tools/pagerduty/types' +import type { ToolConfig } from '@/tools/types' + +export const listIncidentsTool: ToolConfig< + PagerDutyListIncidentsParams, + PagerDutyListIncidentsResponse +> = { + id: 'pagerduty_list_incidents', + name: 'PagerDuty List Incidents', + description: 'List incidents from PagerDuty with optional filters.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PagerDuty REST API Key', + }, + statuses: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated statuses to filter (triggered, acknowledged, resolved)', + }, + serviceIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated service IDs to filter', + }, + since: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Start date filter (ISO 8601 format)', + }, + until: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'End date filter (ISO 8601 format)', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort field (e.g., created_at:desc)', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (max 100)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.statuses) { + for (const s of params.statuses.split(',')) { + query.append('statuses[]', s.trim()) + } + } + if (params.serviceIds) { + for (const id of params.serviceIds.split(',')) { + query.append('service_ids[]', id.trim()) + } + } + if (params.since) query.set('since', params.since) + if (params.until) query.set('until', params.until) + if (params.sortBy) query.set('sort_by', params.sortBy) + if (params.limit) query.set('limit', params.limit) + query.append('include[]', 'services') + const qs = query.toString() + return `https://api.pagerduty.com/incidents${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Token token=${params.apiKey}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || `PagerDuty API error: ${response.status}`) + } + + return { + success: true, + output: { + incidents: (data.incidents ?? []).map( + ( + inc: Record & { + service?: Record + assignments?: Array & { assignee?: Record }> + escalation_policy?: Record + } + ) => ({ + id: inc.id ?? null, + incidentNumber: inc.incident_number ?? null, + title: inc.title ?? null, + status: inc.status ?? null, + urgency: inc.urgency ?? null, + createdAt: inc.created_at ?? null, + updatedAt: inc.updated_at ?? null, + serviceName: inc.service?.summary ?? null, + serviceId: inc.service?.id ?? null, + assigneeName: inc.assignments?.[0]?.assignee?.summary ?? null, + assigneeId: inc.assignments?.[0]?.assignee?.id ?? null, + escalationPolicyName: inc.escalation_policy?.summary ?? null, + htmlUrl: inc.html_url ?? null, + }) + ), + total: data.total ?? 0, + more: data.more ?? false, + }, + } + }, + + outputs: { + incidents: { + type: 'array', + description: 'Array of incidents', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Incident ID' }, + incidentNumber: { type: 'number', description: 'Incident number' }, + title: { type: 'string', description: 'Incident title' }, + status: { type: 'string', description: 'Incident status' }, + urgency: { type: 'string', description: 'Incident urgency' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + updatedAt: { type: 'string', description: 'Last updated timestamp' }, + serviceName: { type: 'string', description: 'Service name' }, + serviceId: { type: 'string', description: 'Service ID' }, + assigneeName: { type: 'string', description: 'Assignee name' }, + assigneeId: { type: 'string', description: 'Assignee ID' }, + escalationPolicyName: { type: 'string', description: 'Escalation policy name' }, + htmlUrl: { type: 'string', description: 'PagerDuty web URL' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of matching incidents', + }, + more: { + type: 'boolean', + description: 'Whether more results are available', + }, + }, +} diff --git a/apps/sim/tools/pagerduty/list_oncalls.ts b/apps/sim/tools/pagerduty/list_oncalls.ts new file mode 100644 index 0000000000..92f436b9c3 --- /dev/null +++ b/apps/sim/tools/pagerduty/list_oncalls.ts @@ -0,0 +1,145 @@ +import type { + PagerDutyListOncallsParams, + PagerDutyListOncallsResponse, +} from '@/tools/pagerduty/types' +import type { ToolConfig } from '@/tools/types' + +export const listOncallsTool: ToolConfig = + { + id: 'pagerduty_list_oncalls', + name: 'PagerDuty List On-Calls', + description: 'List current on-call entries from PagerDuty.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PagerDuty REST API Key', + }, + escalationPolicyIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated escalation policy IDs to filter', + }, + scheduleIds: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated schedule IDs to filter', + }, + since: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Start time filter (ISO 8601 format)', + }, + until: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'End time filter (ISO 8601 format)', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (max 100)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.escalationPolicyIds) { + for (const id of params.escalationPolicyIds.split(',')) { + query.append('escalation_policy_ids[]', id.trim()) + } + } + if (params.scheduleIds) { + for (const id of params.scheduleIds.split(',')) { + query.append('schedule_ids[]', id.trim()) + } + } + if (params.since) query.set('since', params.since) + if (params.until) query.set('until', params.until) + if (params.limit) query.set('limit', params.limit) + const qs = query.toString() + return `https://api.pagerduty.com/oncalls${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Token token=${params.apiKey}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || `PagerDuty API error: ${response.status}`) + } + + const oncalls = (data.oncalls ?? []).map( + ( + oc: Record & { + user?: Record + escalation_policy?: Record + schedule?: Record + } + ) => ({ + userName: oc.user?.summary ?? null, + userId: oc.user?.id ?? null, + escalationLevel: oc.escalation_level ?? 0, + escalationPolicyName: oc.escalation_policy?.summary ?? null, + escalationPolicyId: oc.escalation_policy?.id ?? null, + scheduleName: oc.schedule?.summary ?? null, + scheduleId: oc.schedule?.id ?? null, + start: oc.start ?? null, + end: oc.end ?? null, + }) + ) + + return { + success: true, + output: { + oncalls, + total: data.total ?? oncalls.length, + more: data.more ?? false, + }, + } + }, + + outputs: { + oncalls: { + type: 'array', + description: 'Array of on-call entries', + items: { + type: 'object', + properties: { + userName: { type: 'string', description: 'On-call user name' }, + userId: { type: 'string', description: 'On-call user ID' }, + escalationLevel: { type: 'number', description: 'Escalation level' }, + escalationPolicyName: { type: 'string', description: 'Escalation policy name' }, + escalationPolicyId: { type: 'string', description: 'Escalation policy ID' }, + scheduleName: { type: 'string', description: 'Schedule name' }, + scheduleId: { type: 'string', description: 'Schedule ID' }, + start: { type: 'string', description: 'On-call start time' }, + end: { type: 'string', description: 'On-call end time' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of matching on-call entries', + }, + more: { + type: 'boolean', + description: 'Whether more results are available', + }, + }, + } diff --git a/apps/sim/tools/pagerduty/list_services.ts b/apps/sim/tools/pagerduty/list_services.ts new file mode 100644 index 0000000000..af281ffc83 --- /dev/null +++ b/apps/sim/tools/pagerduty/list_services.ts @@ -0,0 +1,108 @@ +import type { + PagerDutyListServicesParams, + PagerDutyListServicesResponse, +} from '@/tools/pagerduty/types' +import type { ToolConfig } from '@/tools/types' + +export const listServicesTool: ToolConfig< + PagerDutyListServicesParams, + PagerDutyListServicesResponse +> = { + id: 'pagerduty_list_services', + name: 'PagerDuty List Services', + description: 'List services from PagerDuty with optional name filter.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PagerDuty REST API Key', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter services by name', + }, + limit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (max 100)', + }, + }, + + request: { + url: (params) => { + const query = new URLSearchParams() + if (params.query) query.set('query', params.query) + if (params.limit) query.set('limit', params.limit) + const qs = query.toString() + return `https://api.pagerduty.com/services${qs ? `?${qs}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Token token=${params.apiKey}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || `PagerDuty API error: ${response.status}`) + } + + return { + success: true, + output: { + services: (data.services ?? []).map( + (svc: Record & { escalation_policy?: Record }) => ({ + id: svc.id ?? null, + name: svc.name ?? null, + description: svc.description ?? null, + status: svc.status ?? null, + escalationPolicyName: svc.escalation_policy?.summary ?? null, + escalationPolicyId: svc.escalation_policy?.id ?? null, + createdAt: svc.created_at ?? null, + htmlUrl: svc.html_url ?? null, + }) + ), + total: data.total ?? 0, + more: data.more ?? false, + }, + } + }, + + outputs: { + services: { + type: 'array', + description: 'Array of services', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Service ID' }, + name: { type: 'string', description: 'Service name' }, + description: { type: 'string', description: 'Service description' }, + status: { type: 'string', description: 'Service status' }, + escalationPolicyName: { type: 'string', description: 'Escalation policy name' }, + escalationPolicyId: { type: 'string', description: 'Escalation policy ID' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + htmlUrl: { type: 'string', description: 'PagerDuty web URL' }, + }, + }, + }, + total: { + type: 'number', + description: 'Total number of matching services', + }, + more: { + type: 'boolean', + description: 'Whether more results are available', + }, + }, +} diff --git a/apps/sim/tools/pagerduty/types.ts b/apps/sim/tools/pagerduty/types.ts new file mode 100644 index 0000000000..ab800bf188 --- /dev/null +++ b/apps/sim/tools/pagerduty/types.ts @@ -0,0 +1,169 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * Base params shared by all PagerDuty endpoints. + */ +export interface PagerDutyBaseParams { + apiKey: string +} + +/** + * Params that require a From header for write operations. + */ +export interface PagerDutyWriteParams extends PagerDutyBaseParams { + fromEmail: string +} + +/** + * List Incidents params. + */ +export interface PagerDutyListIncidentsParams extends PagerDutyBaseParams { + statuses?: string + serviceIds?: string + since?: string + until?: string + sortBy?: string + limit?: string +} + +export interface PagerDutyListIncidentsResponse extends ToolResponse { + output: { + incidents: Array<{ + id: string + incidentNumber: number + title: string + status: string + urgency: string + createdAt: string + updatedAt: string | null + serviceName: string | null + serviceId: string | null + assigneeName: string | null + assigneeId: string | null + escalationPolicyName: string | null + htmlUrl: string | null + }> + total: number + more: boolean + } +} + +/** + * Create Incident params. + */ +export interface PagerDutyCreateIncidentParams extends PagerDutyWriteParams { + title: string + serviceId: string + urgency?: string + body?: string + escalationPolicyId?: string + assigneeId?: string +} + +export interface PagerDutyCreateIncidentResponse extends ToolResponse { + output: { + id: string + incidentNumber: number + title: string + status: string + urgency: string + createdAt: string + serviceName: string | null + serviceId: string | null + htmlUrl: string | null + } +} + +/** + * Update Incident params. + */ +export interface PagerDutyUpdateIncidentParams extends PagerDutyWriteParams { + incidentId: string + status?: string + title?: string + urgency?: string + escalationLevel?: string +} + +export interface PagerDutyUpdateIncidentResponse extends ToolResponse { + output: { + id: string + incidentNumber: number + title: string + status: string + urgency: string + updatedAt: string | null + htmlUrl: string | null + } +} + +/** + * Add Note to Incident params. + */ +export interface PagerDutyAddNoteParams extends PagerDutyWriteParams { + incidentId: string + content: string +} + +export interface PagerDutyAddNoteResponse extends ToolResponse { + output: { + id: string + content: string + createdAt: string + userName: string | null + } +} + +/** + * List Services params. + */ +export interface PagerDutyListServicesParams extends PagerDutyBaseParams { + query?: string + limit?: string +} + +export interface PagerDutyListServicesResponse extends ToolResponse { + output: { + services: Array<{ + id: string + name: string + description: string | null + status: string + escalationPolicyName: string | null + escalationPolicyId: string | null + createdAt: string + htmlUrl: string | null + }> + total: number + more: boolean + } +} + +/** + * List On-Calls params. + */ +export interface PagerDutyListOncallsParams extends PagerDutyBaseParams { + escalationPolicyIds?: string + scheduleIds?: string + since?: string + until?: string + limit?: string +} + +export interface PagerDutyListOncallsResponse extends ToolResponse { + output: { + oncalls: Array<{ + userName: string | null + userId: string | null + escalationLevel: number + escalationPolicyName: string | null + escalationPolicyId: string | null + scheduleName: string | null + scheduleId: string | null + start: string | null + end: string | null + }> + total: number + more: boolean + } +} diff --git a/apps/sim/tools/pagerduty/update_incident.ts b/apps/sim/tools/pagerduty/update_incident.ts new file mode 100644 index 0000000000..156b5a1ad5 --- /dev/null +++ b/apps/sim/tools/pagerduty/update_incident.ts @@ -0,0 +1,117 @@ +import type { + PagerDutyUpdateIncidentParams, + PagerDutyUpdateIncidentResponse, +} from '@/tools/pagerduty/types' +import type { ToolConfig } from '@/tools/types' + +export const updateIncidentTool: ToolConfig< + PagerDutyUpdateIncidentParams, + PagerDutyUpdateIncidentResponse +> = { + id: 'pagerduty_update_incident', + name: 'PagerDuty Update Incident', + description: 'Update an incident in PagerDuty (acknowledge, resolve, change urgency, etc.).', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PagerDuty REST API Key', + }, + fromEmail: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address of a valid PagerDuty user', + }, + incidentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the incident to update', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New status (acknowledged or resolved)', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New incident title', + }, + urgency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New urgency (high or low)', + }, + escalationLevel: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Escalation level to escalate to', + }, + }, + + request: { + url: (params) => `https://api.pagerduty.com/incidents/${params.incidentId.trim()}`, + method: 'PUT', + headers: (params) => ({ + Authorization: `Token token=${params.apiKey}`, + Accept: 'application/vnd.pagerduty+json;version=2', + 'Content-Type': 'application/json', + From: params.fromEmail, + }), + body: (params) => { + const incident: Record = { + id: params.incidentId, + type: 'incident', + } + + if (params.status) incident.status = params.status + if (params.title) incident.title = params.title + if (params.urgency) incident.urgency = params.urgency + if (params.escalationLevel) { + incident.escalation_level = Number(params.escalationLevel) + } + return { incident } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || `PagerDuty API error: ${response.status}`) + } + + const inc = data.incident ?? {} + return { + success: true, + output: { + id: inc.id ?? null, + incidentNumber: inc.incident_number ?? null, + title: inc.title ?? null, + status: inc.status ?? null, + urgency: inc.urgency ?? null, + updatedAt: inc.updated_at ?? null, + htmlUrl: inc.html_url ?? null, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Incident ID' }, + incidentNumber: { type: 'number', description: 'Incident number' }, + title: { type: 'string', description: 'Incident title' }, + status: { type: 'string', description: 'Updated status' }, + urgency: { type: 'string', description: 'Updated urgency' }, + updatedAt: { type: 'string', description: 'Last updated timestamp' }, + htmlUrl: { type: 'string', description: 'PagerDuty web URL' }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 00b9d1e47c..e44e35ed1b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -42,6 +42,19 @@ import { algoliaSearchTool, algoliaUpdateSettingsTool, } from '@/tools/algolia' +import { + amplitudeEventSegmentationTool, + amplitudeGetActiveUsersTool, + amplitudeGetRevenueTool, + amplitudeGroupIdentifyTool, + amplitudeIdentifyUserTool, + amplitudeListEventsTool, + amplitudeRealtimeActiveUsersTool, + amplitudeSendEventTool, + amplitudeUserActivityTool, + amplitudeUserProfileTool, + amplitudeUserSearchTool, +} from '@/tools/amplitude' import { apifyRunActorAsyncTool, apifyRunActorSyncTool } from '@/tools/apify' import { apolloAccountBulkCreateTool, @@ -786,6 +799,7 @@ import { googleMapsTimezoneTool, googleMapsValidateAddressTool, } from '@/tools/google_maps' +import { googlePagespeedAnalyzeTool } from '@/tools/google_pagespeed' import { googleSheetsAppendTool, googleSheetsAppendV2Tool, @@ -1444,6 +1458,14 @@ import { outlookReadTool, outlookSendTool, } from '@/tools/outlook' +import { + pagerdutyAddNoteTool, + pagerdutyCreateIncidentTool, + pagerdutyListIncidentsTool, + pagerdutyListOncallsTool, + pagerdutyListServicesTool, + pagerdutyUpdateIncidentTool, +} from '@/tools/pagerduty' import { parallelDeepResearchTool, parallelExtractTool, parallelSearchTool } from '@/tools/parallel' import { perplexityChatTool, perplexitySearchTool } from '@/tools/perplexity' import { @@ -2248,6 +2270,17 @@ export const tools: Record = { a2a_send_message: a2aSendMessageTool, a2a_set_push_notification: a2aSetPushNotificationTool, airweave_search: airweaveSearchTool, + amplitude_send_event: amplitudeSendEventTool, + amplitude_identify_user: amplitudeIdentifyUserTool, + amplitude_group_identify: amplitudeGroupIdentifyTool, + amplitude_user_search: amplitudeUserSearchTool, + amplitude_user_activity: amplitudeUserActivityTool, + amplitude_user_profile: amplitudeUserProfileTool, + amplitude_event_segmentation: amplitudeEventSegmentationTool, + amplitude_get_active_users: amplitudeGetActiveUsersTool, + amplitude_realtime_active_users: amplitudeRealtimeActiveUsersTool, + amplitude_list_events: amplitudeListEventsTool, + amplitude_get_revenue: amplitudeGetRevenueTool, arxiv_get_author_papers: arxivGetAuthorPapersTool, arxiv_get_paper: arxivGetPaperTool, arxiv_search: arxivSearchTool, @@ -3163,6 +3196,7 @@ export const tools: Record = { google_maps_speed_limits: googleMapsSpeedLimitsTool, google_maps_timezone: googleMapsTimezoneTool, google_maps_validate_address: googleMapsValidateAddressTool, + google_pagespeed_analyze: googlePagespeedAnalyzeTool, google_tasks_create: googleTasksCreateTool, google_tasks_delete: googleTasksDeleteTool, google_tasks_get: googleTasksGetTool, @@ -3637,6 +3671,12 @@ export const tools: Record = { outlook_mark_unread: outlookMarkUnreadTool, outlook_delete: outlookDeleteTool, outlook_copy: outlookCopyTool, + pagerduty_list_incidents: pagerdutyListIncidentsTool, + pagerduty_create_incident: pagerdutyCreateIncidentTool, + pagerduty_update_incident: pagerdutyUpdateIncidentTool, + pagerduty_add_note: pagerdutyAddNoteTool, + pagerduty_list_services: pagerdutyListServicesTool, + pagerduty_list_oncalls: pagerdutyListOncallsTool, linear_read_issues: linearReadIssuesTool, linear_create_issue: linearCreateIssueTool, linear_get_issue: linearGetIssueTool, From 79bb4e5ad8b8d3d6bce2c7501d24d258c6fef978 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sun, 1 Mar 2026 22:53:18 -0800 Subject: [PATCH 3/3] feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages (#3388) * feat(docs): add API reference with OpenAPI spec and auto-generated endpoint pages * multiline curl * random improvements * cleanup * update docs copy * fix build * cast * fix builg --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti Co-authored-by: Vikhyath Mondreti --- apps/docs/app/[lang]/[[...slug]]/page.tsx | 169 +- apps/docs/app/[lang]/layout.tsx | 6 +- apps/docs/app/global.css | 712 ++++++- apps/docs/app/layout.config.tsx | 21 - .../docs-layout/sidebar-components.tsx | 35 +- .../components/docs-layout/toc-footer.tsx | 20 +- apps/docs/components/navbar/navbar.tsx | 29 +- apps/docs/components/structured-data.tsx | 19 +- apps/docs/components/ui/response-section.tsx | 169 ++ .../docs/de/api-reference/authentication.mdx | 94 + .../docs/de/api-reference/getting-started.mdx | 210 ++ .../content/docs/de/api-reference/meta.json | 16 + .../content/docs/de/api-reference/python.mdx | 766 +++++++ .../docs/de/api-reference/typescript.mdx | 1052 +++++++++ apps/docs/content/docs/de/meta.json | 24 + .../(generated)/workflows/meta.json | 3 + .../docs/en/api-reference/authentication.mdx | 94 + .../docs/en/api-reference/getting-started.mdx | 210 ++ .../content/docs/en/api-reference/meta.json | 16 + .../content/docs/en/api-reference/python.mdx | 761 +++++++ .../docs/en/api-reference/typescript.mdx | 1035 +++++++++ apps/docs/content/docs/en/execution/api.mdx | 2 +- apps/docs/content/docs/en/meta.json | 1 - .../en/permissions/roles-and-permissions.mdx | 2 +- .../docs/es/api-reference/authentication.mdx | 94 + .../docs/es/api-reference/getting-started.mdx | 210 ++ .../content/docs/es/api-reference/meta.json | 16 + .../content/docs/es/api-reference/python.mdx | 766 +++++++ .../docs/es/api-reference/typescript.mdx | 1052 +++++++++ apps/docs/content/docs/es/meta.json | 24 + .../docs/fr/api-reference/authentication.mdx | 94 + .../docs/fr/api-reference/getting-started.mdx | 210 ++ .../content/docs/fr/api-reference/meta.json | 16 + .../content/docs/fr/api-reference/python.mdx | 766 +++++++ .../docs/fr/api-reference/typescript.mdx | 1052 +++++++++ apps/docs/content/docs/fr/meta.json | 24 + .../docs/ja/api-reference/authentication.mdx | 94 + .../docs/ja/api-reference/getting-started.mdx | 210 ++ .../content/docs/ja/api-reference/meta.json | 16 + .../content/docs/ja/api-reference/python.mdx | 766 +++++++ .../docs/ja/api-reference/typescript.mdx | 1052 +++++++++ apps/docs/content/docs/ja/meta.json | 24 + .../docs/zh/api-reference/authentication.mdx | 94 + .../docs/zh/api-reference/getting-started.mdx | 210 ++ .../content/docs/zh/api-reference/meta.json | 16 + .../content/docs/zh/api-reference/python.mdx | 766 +++++++ .../docs/zh/api-reference/typescript.mdx | 1052 +++++++++ apps/docs/content/docs/zh/meta.json | 24 + apps/docs/lib/llms.ts | 3 +- apps/docs/lib/openapi.ts | 132 ++ apps/docs/lib/source.ts | 92 +- apps/docs/openapi.json | 1893 +++++++++++++++++ apps/docs/package.json | 8 +- bun.lock | 164 +- 54 files changed, 16235 insertions(+), 171 deletions(-) delete mode 100644 apps/docs/app/layout.config.tsx create mode 100644 apps/docs/components/ui/response-section.tsx create mode 100644 apps/docs/content/docs/de/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/de/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/de/api-reference/meta.json create mode 100644 apps/docs/content/docs/de/api-reference/python.mdx create mode 100644 apps/docs/content/docs/de/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/de/meta.json create mode 100644 apps/docs/content/docs/en/api-reference/(generated)/workflows/meta.json create mode 100644 apps/docs/content/docs/en/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/en/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/en/api-reference/meta.json create mode 100644 apps/docs/content/docs/en/api-reference/python.mdx create mode 100644 apps/docs/content/docs/en/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/es/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/es/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/es/api-reference/meta.json create mode 100644 apps/docs/content/docs/es/api-reference/python.mdx create mode 100644 apps/docs/content/docs/es/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/es/meta.json create mode 100644 apps/docs/content/docs/fr/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/fr/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/fr/api-reference/meta.json create mode 100644 apps/docs/content/docs/fr/api-reference/python.mdx create mode 100644 apps/docs/content/docs/fr/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/fr/meta.json create mode 100644 apps/docs/content/docs/ja/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/ja/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/ja/api-reference/meta.json create mode 100644 apps/docs/content/docs/ja/api-reference/python.mdx create mode 100644 apps/docs/content/docs/ja/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/ja/meta.json create mode 100644 apps/docs/content/docs/zh/api-reference/authentication.mdx create mode 100644 apps/docs/content/docs/zh/api-reference/getting-started.mdx create mode 100644 apps/docs/content/docs/zh/api-reference/meta.json create mode 100644 apps/docs/content/docs/zh/api-reference/python.mdx create mode 100644 apps/docs/content/docs/zh/api-reference/typescript.mdx create mode 100644 apps/docs/content/docs/zh/meta.json create mode 100644 apps/docs/lib/openapi.ts create mode 100644 apps/docs/openapi.json diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx index 0a4afc9818..461baf2f54 100644 --- a/apps/docs/app/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -1,5 +1,8 @@ import type React from 'react' +import type { Root } from 'fumadocs-core/page-tree' import { findNeighbour } from 'fumadocs-core/page-tree' +import type { ApiPageProps } from 'fumadocs-openapi/ui' +import { createAPIPage } from 'fumadocs-openapi/ui' import { Pre } from 'fumadocs-ui/components/codeblock' import defaultMdxComponents from 'fumadocs-ui/mdx' import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page' @@ -12,28 +15,75 @@ import { LLMCopyButton } from '@/components/page-actions' import { StructuredData } from '@/components/structured-data' import { CodeBlock } from '@/components/ui/code-block' import { Heading } from '@/components/ui/heading' +import { ResponseSection } from '@/components/ui/response-section' +import { i18n } from '@/lib/i18n' +import { getApiSpecContent, openapi } from '@/lib/openapi' import { type PageData, source } from '@/lib/source' +const SUPPORTED_LANGUAGES: Set = new Set(i18n.languages) +const BASE_URL = 'https://docs.sim.ai' + +function resolveLangAndSlug(params: { slug?: string[]; lang: string }) { + const isValidLang = SUPPORTED_LANGUAGES.has(params.lang) + const lang = isValidLang ? params.lang : 'en' + const slug = isValidLang ? params.slug : [params.lang, ...(params.slug ?? [])] + return { lang, slug } +} + +const APIPage = createAPIPage(openapi, { + playground: { enabled: false }, + content: { + renderOperationLayout: async (slots) => { + return ( +
+
+ {slots.header} + {slots.apiPlayground} + {slots.authSchemes &&
{slots.authSchemes}
} + {slots.paremeters} + {slots.body &&
{slots.body}
} + {slots.responses} + {slots.callbacks} +
+
+ {slots.apiExample} +
+
+ ) + }, + }, +}) + export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) { const params = await props.params - const page = source.getPage(params.slug, params.lang) + const { lang, slug } = resolveLangAndSlug(params) + const page = source.getPage(slug, lang) if (!page) notFound() - const data = page.data as PageData - const MDX = data.body - const baseUrl = 'https://docs.sim.ai' - const markdownContent = await data.getText('processed') + const data = page.data as unknown as PageData & { + _openapi?: { method?: string } + getAPIPageProps?: () => ApiPageProps + } + const isOpenAPI = '_openapi' in data && data._openapi != null + const isApiReference = slug?.some((s) => s === 'api-reference') ?? false - const pageTreeRecord = source.pageTree as Record - const pageTree = - pageTreeRecord[params.lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0] - const neighbours = pageTree ? findNeighbour(pageTree, page.url) : null + const pageTreeRecord = source.pageTree as Record + const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0] + const rawNeighbours = pageTree ? findNeighbour(pageTree, page.url) : null + const neighbours = isApiReference + ? { + previous: rawNeighbours?.previous?.url.includes('/api-reference/') + ? rawNeighbours.previous + : undefined, + next: rawNeighbours?.next?.url.includes('/api-reference/') ? rawNeighbours.next : undefined, + } + : rawNeighbours const generateBreadcrumbs = () => { const breadcrumbs: Array<{ name: string; url: string }> = [ { name: 'Home', - url: baseUrl, + url: BASE_URL, }, ] @@ -41,7 +91,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l let currentPath = '' urlParts.forEach((part, index) => { - if (index === 0 && ['en', 'es', 'fr', 'de', 'ja', 'zh'].includes(part)) { + if (index === 0 && SUPPORTED_LANGUAGES.has(part)) { currentPath = `/${part}` return } @@ -56,12 +106,12 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l if (index === urlParts.length - 1) { breadcrumbs.push({ name: data.title, - url: `${baseUrl}${page.url}`, + url: `${BASE_URL}${page.url}`, }) } else { breadcrumbs.push({ name: name, - url: `${baseUrl}${currentPath}`, + url: `${BASE_URL}${currentPath}`, }) } }) @@ -73,7 +123,6 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l const CustomFooter = () => (
- {/* Navigation links */}
{neighbours?.previous ? ( - {/* Divider line */}
- {/* Social icons */}
) + if (isOpenAPI && data.getAPIPageProps) { + const apiProps = data.getAPIPageProps() + const apiPageContent = getApiSpecContent( + data.title, + data.description, + apiProps.operations ?? [] + ) + + return ( + <> + + , + }} + > +
+
+
+ +
+ +
+ {data.title} + {data.description} +
+ + + +
+ + ) + } + + const MDX = data.body + const markdownContent = await data.getText('processed') + return ( <> }) { const params = await props.params - const page = source.getPage(params.slug, params.lang) + const { lang, slug } = resolveLangAndSlug(params) + const page = source.getPage(slug, lang) if (!page) notFound() - const data = page.data as PageData - const baseUrl = 'https://docs.sim.ai' - const fullUrl = `${baseUrl}${page.url}` + const data = page.data as unknown as PageData + const fullUrl = `${BASE_URL}${page.url}` - const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}` + const ogImageUrl = `${BASE_URL}/api/og?title=${encodeURIComponent(data.title)}` return { title: data.title, @@ -286,10 +389,10 @@ export async function generateMetadata(props: { url: fullUrl, siteName: 'Sim Documentation', type: 'article', - locale: params.lang === 'en' ? 'en_US' : `${params.lang}_${params.lang.toUpperCase()}`, + locale: lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`, alternateLocale: ['en', 'es', 'fr', 'de', 'ja', 'zh'] - .filter((lang) => lang !== params.lang) - .map((lang) => (lang === 'en' ? 'en_US' : `${lang}_${lang.toUpperCase()}`)), + .filter((l) => l !== lang) + .map((l) => (l === 'en' ? 'en_US' : `${l}_${l.toUpperCase()}`)), images: [ { url: ogImageUrl, @@ -323,13 +426,13 @@ export async function generateMetadata(props: { alternates: { canonical: fullUrl, languages: { - 'x-default': `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`, - en: `${baseUrl}${page.url.replace(`/${params.lang}`, '')}`, - es: `${baseUrl}/es${page.url.replace(`/${params.lang}`, '')}`, - fr: `${baseUrl}/fr${page.url.replace(`/${params.lang}`, '')}`, - de: `${baseUrl}/de${page.url.replace(`/${params.lang}`, '')}`, - ja: `${baseUrl}/ja${page.url.replace(`/${params.lang}`, '')}`, - zh: `${baseUrl}/zh${page.url.replace(`/${params.lang}`, '')}`, + 'x-default': `${BASE_URL}${page.url.replace(`/${lang}`, '')}`, + en: `${BASE_URL}${page.url.replace(`/${lang}`, '')}`, + es: `${BASE_URL}/es${page.url.replace(`/${lang}`, '')}`, + fr: `${BASE_URL}/fr${page.url.replace(`/${lang}`, '')}`, + de: `${BASE_URL}/de${page.url.replace(`/${lang}`, '')}`, + ja: `${BASE_URL}/ja${page.url.replace(`/${lang}`, '')}`, + zh: `${BASE_URL}/zh${page.url.replace(`/${lang}`, '')}`, }, }, } diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index d76a11f103..250e249c7b 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -55,8 +55,11 @@ type LayoutProps = { params: Promise<{ lang: string }> } +const SUPPORTED_LANGUAGES: Set = new Set(i18n.languages) + export default async function Layout({ children, params }: LayoutProps) { - const { lang } = await params + const { lang: rawLang } = await params + const lang = SUPPORTED_LANGUAGES.has(rawLang) ? rawLang : 'en' const structuredData = { '@context': 'https://schema.org', @@ -107,6 +110,7 @@ export default async function Layout({ children, params }: LayoutProps) { title: , }} sidebar={{ + tabs: false, defaultOpenLevel: 0, collapsible: false, footer: null, diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 70ec578bf9..120feee256 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "fumadocs-ui/css/neutral.css"; @import "fumadocs-ui/css/preset.css"; +@import "fumadocs-openapi/css/preset.css"; /* Prevent overscroll bounce effect on the page */ html, @@ -8,18 +9,12 @@ body { overscroll-behavior: none; } -@theme { - --color-fd-primary: #33c482; /* Green from Sim logo */ - --font-geist-sans: var(--font-geist-sans); - --font-geist-mono: var(--font-geist-mono); +/* Reserve scrollbar space to prevent layout jitter between pages */ +html { + scrollbar-gutter: stable; } -/* Ensure primary color is set in both light and dark modes */ -:root { - --color-fd-primary: #33c482; -} - -.dark { +@theme { --color-fd-primary: #33c482; } @@ -34,12 +29,6 @@ body { "Liberation Mono", "Courier New", monospace; } -/* Target any potential border classes */ -* { - --fd-border-sidebar: transparent !important; -} - -/* Override any CSS custom properties for borders */ :root { --fd-border: transparent !important; --fd-border-sidebar: transparent !important; @@ -86,7 +75,6 @@ body { [data-sidebar-container], #nd-sidebar { background: transparent !important; - background-color: transparent !important; border: none !important; --color-fd-muted: transparent !important; --color-fd-card: transparent !important; @@ -96,9 +84,7 @@ body { aside[data-sidebar], aside#nd-sidebar { background: transparent !important; - background-color: transparent !important; border: none !important; - border-right: none !important; } /* Fumadocs v16: Add sidebar placeholder styling for grid area */ @@ -157,7 +143,6 @@ aside#nd-sidebar { #nd-sidebar > div { padding: 0.5rem 12px 12px; background: transparent !important; - background-color: transparent !important; } /* Override sidebar item styling to match Raindrop */ @@ -434,10 +419,6 @@ aside[data-sidebar], #nd-sidebar, #nd-sidebar * { border: none !important; - border-right: none !important; - border-left: none !important; - border-top: none !important; - border-bottom: none !important; } /* Override fumadocs background colors for sidebar */ @@ -447,7 +428,6 @@ aside[data-sidebar], --color-fd-muted: transparent !important; --color-fd-secondary: transparent !important; background: transparent !important; - background-color: transparent !important; } /* Force normal text flow in sidebar */ @@ -564,16 +544,682 @@ main[data-main] { padding-top: 1.5rem !important; } -/* Override Fumadocs default content padding */ -article[data-content], -div[data-content] { - padding-top: 1.5rem !important; -} - -/* Remove any unwanted borders/outlines from video elements */ +/* Remove any unwanted outlines from video elements */ video { outline: none !important; - border-style: solid !important; +} + +/* API Reference Pages — Mintlify-style overrides */ + +/* OpenAPI pages: span main + TOC grid columns for wide two-column layout. + The grid has columns: spacer | sidebar | main | toc | spacer. + By spanning columns 3-4, the article fills both main and toc areas, + while the grid structure stays identical to non-OpenAPI pages (no jitter). */ +#nd-page:has(.api-page-header) { + grid-column: 3 / span 2 !important; + max-width: 1400px !important; +} + +/* Hide the empty TOC aside on OpenAPI pages so it doesn't overlay content */ +#nd-docs-layout:has(#nd-page .api-page-header) #nd-toc { + display: none; +} + +/* Hide the default "Response Body" heading rendered by fumadocs-openapi */ +.response-section-wrapper > .response-section-content > h2, +.response-section-wrapper > .response-section-content > h3 { + display: none !important; +} + +/* Hide default accordion triggers (status code rows) — we show our own dropdown */ +.response-section-wrapper [data-orientation="vertical"] > [data-state] > h3 { + display: none !important; +} + +/* Ensure API reference pages use the same font as the rest of the docs */ +#nd-page:has(.api-page-header), +#nd-page:has(.api-page-header) h2, +#nd-page:has(.api-page-header) h3, +#nd-page:has(.api-page-header) h4, +#nd-page:has(.api-page-header) p, +#nd-page:has(.api-page-header) span, +#nd-page:has(.api-page-header) div, +#nd-page:has(.api-page-header) label, +#nd-page:has(.api-page-header) button { + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +/* Method badge pills in page content — colored background pills */ +#nd-page span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(220 252 231 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(34 197 94 / 0.15); +} + +#nd-page span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(219 234 254 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(59 130 246 / 0.15); +} + +#nd-page span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(255 237 213 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(249 115 22 / 0.15); +} + +#nd-page span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(254 226 226 / 0.6); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.75rem; +} +html.dark #nd-page span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(239 68 68 / 0.15); +} + +/* Sidebar links with method badges — flex for vertical centering */ +#nd-sidebar a:has(span.font-mono.font-medium) { + display: flex !important; + align-items: center !important; + gap: 6px; +} + +/* Sidebar method badges — ensure proper inline flex display */ +#nd-sidebar a span.font-mono.font-medium { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2.25rem; + font-size: 10px !important; + line-height: 1 !important; + padding: 2.5px 4px; + border-radius: 3px; + flex-shrink: 0; +} + +/* Sidebar GET badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(220 252 231 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-green"] { + background-color: rgb(34 197 94 / 0.15); +} + +/* Sidebar POST badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(219 234 254 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-blue"] { + background-color: rgb(59 130 246 / 0.15); +} + +/* Sidebar PUT badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(255 237 213 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-orange"] { + background-color: rgb(249 115 22 / 0.15); +} + +/* Sidebar DELETE badges */ +#nd-sidebar a span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(254 226 226 / 0.6); +} +html.dark #nd-sidebar a span.font-mono.font-medium[class*="text-red"] { + background-color: rgb(239 68 68 / 0.15); +} + +/* Code block containers — match regular docs styling */ +#nd-page:has(.api-page-header) figure.shiki { + border-radius: 0.75rem !important; + background-color: var(--color-fd-card) !important; +} + +/* Hide "Filter Properties" search bar everywhere — main page and popovers */ +input[placeholder="Filter Properties"] { + display: none !important; +} +div:has(> input[placeholder="Filter Properties"]) { + display: none !important; +} +/* Remove top border on first visible property after hidden Filter Properties */ +div:has(> input[placeholder="Filter Properties"]) + .text-sm.border-t { + border-top: none !important; +} + +/* Hide "TypeScript Definitions" copy panel on API pages */ +#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3.mb-4 { + display: none !important; +} +#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3:has(> div > p.font-medium) { + display: none !important; +} + +/* Hide info tags (Format, Default, etc.) everywhere — main page and popovers */ +div.flex.flex-row.gap-2.flex-wrap.not-prose:has(> div.bg-fd-secondary) { + display: none !important; +} +div.flex.flex-row.items-start.bg-fd-secondary.border.rounded-lg.text-xs { + display: none !important; +} + +/* Method+path bar — cleaner, lighter styling like Gumloop. + Override bg-fd-card CSS variable directly for reliability. */ +#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose { + --color-fd-card: rgb(249 250 251) !important; + background-color: rgb(249 250 251) !important; + border-color: rgb(229 231 235) !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose { + --color-fd-card: rgb(24 24 27) !important; + background-color: rgb(24 24 27) !important; + border-color: rgb(63 63 70) !important; +} +/* Method badge inside path bar — cleaner sans-serif, softer colors */ +#nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium { + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important; + font-weight: 600 !important; + font-size: 0.6875rem !important; + letter-spacing: 0.025em; + text-transform: uppercase; +} +/* POST — softer blue */ +#nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-blue"] { + color: rgb(37 99 235) !important; + background-color: rgb(219 234 254 / 0.7) !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-blue"] { + color: rgb(96 165 250) !important; + background-color: rgb(59 130 246 / 0.15) !important; +} +/* GET — softer green */ +#nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-green"] { + color: rgb(22 163 74) !important; + background-color: rgb(220 252 231 / 0.7) !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + span.font-mono.font-medium[class*="text-green"] { + color: rgb(74 222 128) !important; + background-color: rgb(34 197 94 / 0.15) !important; +} + +/* Path text inside method+path bar — monospace, bright like Gumloop */ +#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose code { + color: rgb(55 65 81) !important; + background: none !important; + border: none !important; + padding: 0 !important; + font-size: 0.8125rem !important; +} +html.dark + #nd-page:has(.api-page-header) + div.flex.flex-row.items-center.rounded-xl.border.not-prose + code { + color: rgb(229 231 235) !important; +} + +/* Inline code in API pages — neutral color instead of red. + Exclude code inside the method+path bar (handled above). */ +#nd-page:has(.api-page-header) .prose :not(pre) > code { + color: rgb(79 70 229) !important; +} +html.dark #nd-page:has(.api-page-header) .prose :not(pre) > code { + color: rgb(165 180 252) !important; +} + +/* Response Section — custom dropdown-based rendering (Mintlify style) */ + +/* Hide divider lines between accordion items */ +.response-section-wrapper [data-orientation="vertical"].divide-y > * { + border-top-width: 0 !important; + border-bottom-width: 0 !important; +} +.response-section-wrapper [data-orientation="vertical"].divide-y { + border-top: none !important; +} + +/* Remove content type labels inside accordion items (we show one in the header) */ +.response-section-wrapper [data-orientation="vertical"] p.not-prose:has(code.text-xs) { + display: none !important; +} + +/* Hide the top-level response description (e.g. "Execution was successfully cancelled.") + but NOT field descriptions inside Schema which also use prose-no-margin. + The response description is a direct child of AccordionContent (role=region) with mb-2. */ +.response-section-wrapper [data-orientation="vertical"] [role="region"] > .prose-no-margin.mb-2, +.response-section-wrapper + [data-orientation="vertical"] + [role="region"] + > div + > .prose-no-margin.mb-2 { + display: none !important; +} + +/* Remove left padding on accordion content so it aligns with Path Parameters */ +.response-section-wrapper [data-orientation="vertical"] [role="region"] { + padding-inline-start: 0 !important; +} + +/* Response section header */ +.response-section-header { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 1.75rem; + margin-bottom: 0.5rem; +} + +.response-section-title { + font-size: 1.5rem; + font-weight: 600; + margin: 0; + color: var(--color-fd-foreground); + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, sans-serif; +} + +.response-section-meta { + display: flex; + align-items: center; + gap: 0.75rem; + margin-left: auto; +} + +/* Status code dropdown */ +.response-section-dropdown-wrapper { + position: relative; +} + +.response-section-dropdown-trigger { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.125rem 0.25rem; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-fd-muted-foreground); + background: none; + border: none; + cursor: pointer; + border-radius: 0.25rem; + transition: color 0.15s; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +.response-section-dropdown-trigger:hover { + color: var(--color-fd-foreground); +} + +.response-section-chevron { + width: 0.75rem; + height: 0.75rem; + transition: transform 0.15s; +} +.response-section-chevron-open { + transform: rotate(180deg); +} + +.response-section-dropdown-menu { + position: absolute; + top: calc(100% + 0.25rem); + left: 0; + z-index: 50; + min-width: 5rem; + background-color: white; + border: 1px solid rgb(229 231 235); + border-radius: 0.5rem; + box-shadow: + 0 4px 6px -1px rgb(0 0 0 / 0.1), + 0 2px 4px -2px rgb(0 0 0 / 0.1); + padding: 0.25rem; + overflow: hidden; +} +html.dark .response-section-dropdown-menu { + background-color: rgb(24 24 27); + border-color: rgb(63 63 70); + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3); +} + +.response-section-dropdown-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.375rem 0.5rem; + font-size: 0.875rem; + color: var(--color-fd-muted-foreground); + background: none; + border: none; + cursor: pointer; + border-radius: 0.25rem; + transition: + background-color 0.1s, + color 0.1s; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +.response-section-dropdown-item:hover { + background-color: rgb(243 244 246); + color: var(--color-fd-foreground); +} +html.dark .response-section-dropdown-item:hover { + background-color: rgb(39 39 42); +} +.response-section-dropdown-item-selected { + color: var(--color-fd-foreground); +} + +.response-section-check { + width: 0.875rem; + height: 0.875rem; +} + +.response-section-content-type { + font-size: 0.875rem; + color: var(--color-fd-muted-foreground); + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} + +/* Response schema container — remove border to match Path Parameters style */ +.response-section-wrapper [data-orientation="vertical"] .border.px-3.py-2.rounded-lg { + border: none !important; + padding: 0 !important; + border-radius: 0 !important; + background-color: transparent; +} + +/* Property row — reorder: name (1) → type badge (2) → required badge (3) */ +#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +/* Name span — order 1 */ +#nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span.font-medium.font-mono.text-fd-primary { + order: 1; +} + +/* Type badge — order 2, grey pill like Mintlify */ +#nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground { + order: 2; + background-color: rgb(240 240 243); + color: rgb(100 100 110); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* Hide the "*" inside the name span — we'll add "required" as a ::after on the flex row */ +#nd-page:has(.api-page-header) span.font-medium.font-mono.text-fd-primary > span.text-red-400 { + display: none; +} + +/* Required badge — order 3, light red pill */ +#nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after { + content: "required"; + order: 3; + display: inline-flex; + align-items: center; + background-color: rgb(254 235 235); + color: rgb(220 38 38); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after { + background-color: rgb(127 29 29 / 0.2); + color: rgb(252 165 165); +} + +/* Optional "?" indicator — hide it */ +#nd-page:has(.api-page-header) + span.font-medium.font-mono.text-fd-primary + > span.text-fd-muted-foreground { + display: none; +} + +/* Hide the auth scheme type label (e.g. "apiKey") next to Authorization heading */ +#nd-page:has(.api-page-header) .flex.items-start.justify-between.gap-2 > div.not-prose { + display: none !important; +} + +/* Auth property — replace "" with "string" badge, add "header" and "required" badges. + Auth properties use my-4 (vs py-4 for regular properties). */ + +/* Auth property flex row — name: order 1, type: order 2, ::before "header": order 3, ::after "required": order 4 */ +#nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.font-medium.font-mono.text-fd-primary { + order: 1; +} +#nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground { + order: 2; + font-size: 0; + padding: 0 !important; + background: none !important; + line-height: 0; +} +#nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground::after { + content: "string"; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + background-color: rgb(240 240 243); + color: rgb(100 100 110); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + display: inline-flex; + align-items: center; +} +html.dark + #nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose + > span.text-sm.font-mono.text-fd-muted-foreground::after { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* "header" badge via ::before on the auth flex row */ +#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::before { + content: "header"; + order: 3; + display: inline-flex; + align-items: center; + background-color: rgb(240 240 243); + color: rgb(100 100 110); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose::before { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* "required" badge via ::after on the auth flex row — light red pill */ +#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::after { + content: "required"; + order: 4; + display: inline-flex; + align-items: center; + background-color: rgb(254 235 235); + color: rgb(220 38 38); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark + #nd-page:has(.api-page-header) + div.my-4 + > .flex.flex-wrap.items-center.gap-3.not-prose::after { + background-color: rgb(127 29 29 / 0.2); + color: rgb(252 165 165); +} + +/* Hide "In: header" text below auth property — redundant with the header badge */ +#nd-page:has(.api-page-header) div.my-4 .prose-no-margin p:has(> code) { + display: none !important; +} + +/* Section dividers — bottom border after Authorization and Body sections. */ +.api-section-divider { + padding-bottom: 0.5rem; + border-bottom: 1px solid rgb(229 231 235 / 0.6); +} +html.dark .api-section-divider { + border-bottom-color: rgb(255 255 255 / 0.07); +} + +/* Property rows — breathing room like Mintlify. + Regular properties use border-t py-4; auth properties use border-t my-4. */ +#nd-page:has(.api-page-header) .text-sm.border-t.py-4 { + padding-top: 1.25rem !important; + padding-bottom: 1.25rem !important; +} +#nd-page:has(.api-page-header) .text-sm.border-t.my-4 { + margin-top: 1.25rem !important; + margin-bottom: 1.25rem !important; + padding-top: 1.25rem; +} + +/* Divider lines between fields — very subtle like Mintlify */ +#nd-page:has(.api-page-header) .text-sm.border-t { + border-color: rgb(229 231 235 / 0.6); +} +html.dark #nd-page:has(.api-page-header) .text-sm.border-t { + border-color: rgb(255 255 255 / 0.07); +} + +/* Body/Callback section "application/json" label — remove inline code styling */ +#nd-page:has(.api-page-header) .flex.gap-2.items-center.justify-between p.not-prose code.text-xs, +#nd-page:has(.api-page-header) .flex.justify-between.gap-2.items-end p.not-prose code.text-xs { + background: none !important; + border: none !important; + padding: 0 !important; + color: var(--color-fd-muted-foreground) !important; + font-size: 0.875rem !important; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important; +} + +/* Object/array type triggers in property rows — order 2 + badge chip styling */ +#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button, +#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > span:has(> button) { + order: 2; + background-color: rgb(240 240 243); + color: rgb(100 100 110); + padding: 0.125rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.6875rem; + line-height: 1.25rem; + font-weight: 500; + font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; +} +html.dark #nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button, +html.dark + #nd-page:has(.api-page-header) + .flex.flex-wrap.items-center.gap-3.not-prose + > span:has(> button) { + background-color: rgb(39 39 42); + color: rgb(212 212 216); +} + +/* Section headings (Authorization, Path Parameters, etc.) — consistent top spacing */ +#nd-page:has(.api-page-header) .min-w-0.flex-1 h2 { + margin-top: 1.75rem !important; + margin-bottom: 0.25rem !important; +} + +/* Code examples in right column — wrap long lines instead of horizontal scroll */ +#nd-page:has(.api-page-header) pre { + white-space: pre-wrap !important; + word-break: break-all !important; +} +#nd-page:has(.api-page-header) pre code { + width: 100% !important; + word-break: break-all !important; + overflow-wrap: break-word !important; +} + +/* API page header — constrain title/copy-page to left content column, not full width. + Only applies on OpenAPI pages (which have the two-column layout). */ +@media (min-width: 1280px) { + .api-page-header { + max-width: calc(100% - 400px - 1.5rem); + } +} + +/* Footer navigation — constrain to left content column on OpenAPI pages only. + Target pages that contain the two-column layout via :has() selector. */ +#nd-page:has(.api-page-header) > div:last-child { + max-width: calc(100% - 400px - 1.5rem); +} +@media (max-width: 1024px) { + #nd-page:has(.api-page-header) > div:last-child { + max-width: 100%; + } } /* Tailwind v4 content sources */ diff --git a/apps/docs/app/layout.config.tsx b/apps/docs/app/layout.config.tsx deleted file mode 100644 index 1998c90b8c..0000000000 --- a/apps/docs/app/layout.config.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared' - -/** - * Shared layout configurations - * - * you can customise layouts individually from: - * Home Layout: app/(home)/layout.tsx - * Docs Layout: app/docs/layout.tsx - */ -export const baseOptions: BaseLayoutProps = { - nav: { - title: ( - <> - - - - My App - - ), - }, -} diff --git a/apps/docs/components/docs-layout/sidebar-components.tsx b/apps/docs/components/docs-layout/sidebar-components.tsx index e6fbe18cd1..7bd6039f8d 100644 --- a/apps/docs/components/docs-layout/sidebar-components.tsx +++ b/apps/docs/components/docs-layout/sidebar-components.tsx @@ -52,15 +52,26 @@ export function SidebarItem({ item }: { item: Item }) { ) } +function isApiReferenceFolder(node: Folder): boolean { + if (node.index?.url.includes('/api-reference/')) return true + for (const child of node.children) { + if (child.type === 'page' && child.url.includes('/api-reference/')) return true + if (child.type === 'folder' && isApiReferenceFolder(child)) return true + } + return false +} + export function SidebarFolder({ item, children }: { item: Folder; children: ReactNode }) { const pathname = usePathname() const hasActiveChild = checkHasActiveChild(item, pathname) + const isApiRef = isApiReferenceFolder(item) + const isOnApiRefPage = stripLangPrefix(pathname).startsWith('/api-reference') const hasChildren = item.children.length > 0 - const [open, setOpen] = useState(hasActiveChild) + const [open, setOpen] = useState(hasActiveChild || (isApiRef && isOnApiRefPage)) useEffect(() => { - setOpen(hasActiveChild) - }, [hasActiveChild]) + setOpen(hasActiveChild || (isApiRef && isOnApiRefPage)) + }, [hasActiveChild, isApiRef, isOnApiRefPage]) const active = item.index ? isActive(item.index.url, pathname, false) : false @@ -157,16 +168,18 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac {hasChildren && (
- {/* Mobile: simple indent */} -
{children}
- {/* Desktop: styled with border */} -
    - {children} -
+
+ {/* Mobile: simple indent */} +
{children}
+ {/* Desktop: styled with border */} +
    + {children} +
+
)}
diff --git a/apps/docs/components/docs-layout/toc-footer.tsx b/apps/docs/components/docs-layout/toc-footer.tsx index eaf29088f2..3e59619e5f 100644 --- a/apps/docs/components/docs-layout/toc-footer.tsx +++ b/apps/docs/components/docs-layout/toc-footer.tsx @@ -1,12 +1,9 @@ 'use client' -import { useState } from 'react' import { ArrowRight, ChevronRight } from 'lucide-react' import Link from 'next/link' export function TOCFooter() { - const [isHovered, setIsHovered] = useState(false) - return (
@@ -21,18 +18,19 @@ export function TOCFooter() { href='https://sim.ai/signup' target='_blank' rel='noopener noreferrer' - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[10px] border border-[#2AAD6C] bg-gradient-to-b from-[#3ED990] to-[#2AAD6C] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#5EE8A8] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50' aria-label='Get started with Sim - Sign up for free' > Get started - - {isHovered ? ( -
diff --git a/apps/docs/components/navbar/navbar.tsx b/apps/docs/components/navbar/navbar.tsx index db82c69062..231a0b334e 100644 --- a/apps/docs/components/navbar/navbar.tsx +++ b/apps/docs/components/navbar/navbar.tsx @@ -1,12 +1,17 @@ 'use client' import Link from 'next/link' +import { usePathname } from 'next/navigation' import { LanguageDropdown } from '@/components/ui/language-dropdown' import { SearchTrigger } from '@/components/ui/search-trigger' import { SimLogoFull } from '@/components/ui/sim-logo' import { ThemeToggle } from '@/components/ui/theme-toggle' +import { cn } from '@/lib/utils' export function Navbar() { + const pathname = usePathname() + const isApiReference = pathname.includes('/api-reference') + return (
{/* Right cluster aligns with TOC edge */} -
+
+ + Documentation + + + API + Platform diff --git a/apps/docs/components/structured-data.tsx b/apps/docs/components/structured-data.tsx index c3aebd10d0..5875f3d732 100644 --- a/apps/docs/components/structured-data.tsx +++ b/apps/docs/components/structured-data.tsx @@ -25,8 +25,8 @@ export function StructuredData({ headline: title, description: description, url: url, - datePublished: dateModified || new Date().toISOString(), - dateModified: dateModified || new Date().toISOString(), + ...(dateModified && { datePublished: dateModified }), + ...(dateModified && { dateModified }), author: { '@type': 'Organization', name: 'Sim Team', @@ -91,12 +91,6 @@ export function StructuredData({ inLanguage: ['en', 'es', 'fr', 'de', 'ja', 'zh'], } - const faqStructuredData = title.toLowerCase().includes('faq') && { - '@context': 'https://schema.org', - '@type': 'FAQPage', - mainEntity: [], - } - const softwareStructuredData = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', @@ -151,15 +145,6 @@ export function StructuredData({ }} /> )} - {faqStructuredData && ( -