v0.5.92: shortlinks, copilot scrolling stickiness, pagination

This commit is contained in:
Waleed
2026-02-17 15:13:21 -08:00
committed by GitHub
78 changed files with 952 additions and 729 deletions

View File

@@ -59,12 +59,6 @@ body {
--content-gap: 1.75rem;
}
/* Remove custom layout variable overrides to fallback to fumadocs defaults */
/* ============================================
Navbar Light Mode Styling
============================================ */
/* Light mode navbar and search styling */
:root:not(.dark) nav {
background-color: hsla(0, 0%, 96%, 0.85) !important;
@@ -88,10 +82,6 @@ body {
-webkit-backdrop-filter: blur(25px) saturate(180%) brightness(0.6) !important;
}
/* ============================================
Custom Sidebar Styling (Turborepo-inspired)
============================================ */
/* Floating sidebar appearance - remove background */
[data-sidebar-container],
#nd-sidebar {
@@ -468,10 +458,6 @@ aside[data-sidebar],
writing-mode: horizontal-tb !important;
}
/* ============================================
Code Block Styling (Improved)
============================================ */
/* Apply Geist Mono to code elements */
code,
pre,
@@ -532,10 +518,6 @@ pre code .line {
color: var(--color-fd-primary);
}
/* ============================================
TOC (Table of Contents) Styling
============================================ */
/* Remove the thin border-left on nested TOC items (keeps main indicator only) */
#nd-toc a[style*="padding-inline-start"] {
border-left: none !important;
@@ -554,10 +536,6 @@ main article,
padding-bottom: 4rem;
}
/* ============================================
Center and Constrain Main Content Width
============================================ */
/* Main content area - center and constrain like turborepo/raindrop */
/* Note: --sidebar-offset and --toc-offset are now applied at #nd-docs-layout level */
main[data-main] {

View File

@@ -234,7 +234,6 @@ List actions from incident.io. Optionally filter by incident ID.
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | incident.io API Key |
| `incident_id` | string | No | Filter actions by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) |
| `page_size` | number | No | Number of actions to return per page \(e.g., 10, 25, 50\) |
#### Output
@@ -309,7 +308,6 @@ List follow-ups from incident.io. Optionally filter by incident ID.
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | incident.io API Key |
| `incident_id` | string | No | Filter follow-ups by incident ID \(e.g., "01FCNDV6P870EA6S7TK1DSYDG0"\) |
| `page_size` | number | No | Number of follow-ups to return per page \(e.g., 10, 25, 50\) |
#### Output
@@ -396,6 +394,7 @@ List all users in your Incident.io workspace. Returns user details including id,
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Incident.io API Key |
| `page_size` | number | No | Number of results to return per page \(e.g., 10, 25, 50\). Default: 25 |
| `after` | string | No | Pagination cursor to fetch the next page of results |
#### Output
@@ -406,6 +405,10 @@ List all users in your Incident.io workspace. Returns user details including id,
| ↳ `name` | string | Full name of the user |
| ↳ `email` | string | Email address of the user |
| ↳ `role` | string | Role of the user in the workspace |
| `pagination_meta` | object | Pagination metadata |
| ↳ `after` | string | Cursor for next page |
| ↳ `page_size` | number | Number of items per page |
| ↳ `total_record_count` | number | Total number of records |
### `incidentio_users_show`
@@ -644,7 +647,6 @@ List all escalation policies in incident.io
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | incident.io API Key |
| `page_size` | number | No | Number of results per page \(e.g., 10, 25, 50\). Default: 25 |
#### Output

View File

@@ -49,6 +49,7 @@ Retrieve all deals from Pipedrive with optional filters
| `pipeline_id` | string | No | If supplied, only deals in the specified pipeline are returned \(e.g., "1"\) |
| `updated_since` | string | No | If set, only deals updated after this time are returned. Format: 2025-01-01T10:20:00Z |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
#### Output
@@ -74,6 +75,8 @@ Retrieve all deals from Pipedrive with optional filters
| `metadata` | object | Pagination metadata for the response |
| ↳ `total_items` | number | Total number of items |
| ↳ `has_more` | boolean | Whether more items are available |
| ↳ `next_cursor` | string | Cursor for fetching the next page \(v2 endpoints\) |
| ↳ `next_start` | number | Offset for fetching the next page \(v1 endpoints\) |
| `success` | boolean | Operation success status |
### `pipedrive_get_deal`
@@ -148,10 +151,9 @@ Retrieve files from Pipedrive with optional filters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `deal_id` | string | No | Filter files by deal ID \(e.g., "123"\) |
| `person_id` | string | No | Filter files by person ID \(e.g., "456"\) |
| `org_id` | string | No | Filter files by organization ID \(e.g., "789"\) |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `sort` | string | No | Sort files by field \(supported: "id", "update_time"\) |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 100\) |
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
| `downloadFiles` | boolean | No | Download file contents into file outputs |
#### Output
@@ -171,6 +173,8 @@ Retrieve files from Pipedrive with optional filters
| ↳ `url` | string | File download URL |
| `downloadedFiles` | file[] | Downloaded files from Pipedrive |
| `total_items` | number | Total number of files returned |
| `has_more` | boolean | Whether more files are available |
| `next_start` | number | Offset for fetching the next page |
| `success` | boolean | Operation success status |
### `pipedrive_get_mail_messages`
@@ -183,6 +187,7 @@ Retrieve mail threads from Pipedrive mailbox
| --------- | ---- | -------- | ----------- |
| `folder` | string | No | Filter by folder: inbox, drafts, sent, archive \(default: inbox\) |
| `limit` | string | No | Number of results to return \(e.g., "25", default: 50\) |
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
#### Output
@@ -190,6 +195,8 @@ Retrieve mail threads from Pipedrive mailbox
| --------- | ---- | ----------- |
| `messages` | array | Array of mail thread objects from Pipedrive mailbox |
| `total_items` | number | Total number of mail threads returned |
| `has_more` | boolean | Whether more messages are available |
| `next_start` | number | Offset for fetching the next page |
| `success` | boolean | Operation success status |
### `pipedrive_get_mail_thread`
@@ -221,7 +228,7 @@ Retrieve all pipelines from Pipedrive
| `sort_by` | string | No | Field to sort by: id, update_time, add_time \(default: id\) |
| `sort_direction` | string | No | Sorting direction: asc, desc \(default: asc\) |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
#### Output
@@ -237,6 +244,8 @@ Retrieve all pipelines from Pipedrive
| ↳ `add_time` | string | When the pipeline was created |
| ↳ `update_time` | string | When the pipeline was last updated |
| `total_items` | number | Total number of pipelines returned |
| `has_more` | boolean | Whether more pipelines are available |
| `next_start` | number | Offset for fetching the next page |
| `success` | boolean | Operation success status |
### `pipedrive_get_pipeline_deals`
@@ -249,8 +258,8 @@ Retrieve all deals in a specific pipeline
| --------- | ---- | -------- | ----------- |
| `pipeline_id` | string | Yes | The ID of the pipeline \(e.g., "1"\) |
| `stage_id` | string | No | Filter by specific stage within the pipeline \(e.g., "2"\) |
| `status` | string | No | Filter by deal status: open, won, lost |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
#### Output
@@ -271,6 +280,7 @@ Retrieve all projects or a specific project from Pipedrive
| `project_id` | string | No | Optional: ID of a specific project to retrieve \(e.g., "123"\) |
| `status` | string | No | Filter by project status: open, completed, deleted \(only for listing all\) |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500, only for listing all\) |
| `cursor` | string | No | For pagination, the marker representing the first item on the next page |
#### Output
@@ -279,6 +289,8 @@ Retrieve all projects or a specific project from Pipedrive
| `projects` | array | Array of project objects \(when listing all\) |
| `project` | object | Single project object \(when project_id is provided\) |
| `total_items` | number | Total number of projects returned |
| `has_more` | boolean | Whether more projects are available |
| `next_cursor` | string | Cursor for fetching the next page |
| `success` | boolean | Operation success status |
### `pipedrive_create_project`
@@ -309,12 +321,11 @@ Retrieve activities (tasks) from Pipedrive with optional filters
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `deal_id` | string | No | Filter activities by deal ID \(e.g., "123"\) |
| `person_id` | string | No | Filter activities by person ID \(e.g., "456"\) |
| `org_id` | string | No | Filter activities by organization ID \(e.g., "789"\) |
| `user_id` | string | No | Filter activities by user ID \(e.g., "123"\) |
| `type` | string | No | Filter by activity type \(call, meeting, task, deadline, email, lunch\) |
| `done` | string | No | Filter by completion status: 0 for not done, 1 for done |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
#### Output
@@ -335,6 +346,8 @@ Retrieve activities (tasks) from Pipedrive with optional filters
| ↳ `add_time` | string | When the activity was created |
| ↳ `update_time` | string | When the activity was last updated |
| `total_items` | number | Total number of activities returned |
| `has_more` | boolean | Whether more activities are available |
| `next_start` | number | Offset for fetching the next page |
| `success` | boolean | Operation success status |
### `pipedrive_create_activity`
@@ -399,6 +412,7 @@ Retrieve all leads or a specific lead from Pipedrive
| `person_id` | string | No | Filter by person ID \(e.g., "456"\) |
| `organization_id` | string | No | Filter by organization ID \(e.g., "789"\) |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `start` | string | No | Pagination start offset \(0-based index of the first item to return\) |
#### Output
@@ -433,6 +447,8 @@ Retrieve all leads or a specific lead from Pipedrive
| ↳ `add_time` | string | When the lead was created \(ISO 8601\) |
| ↳ `update_time` | string | When the lead was last updated \(ISO 8601\) |
| `total_items` | number | Total number of leads returned |
| `has_more` | boolean | Whether more leads are available |
| `next_start` | number | Offset for fetching the next page |
| `success` | boolean | Operation success status |
### `pipedrive_create_lead`

View File

@@ -57,6 +57,7 @@ Query data from a Supabase table
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
| `limit` | number | No | Maximum number of rows to return |
| `offset` | number | No | Number of rows to skip \(for pagination\) |
| `apiKey` | string | Yes | Your Supabase service role secret key |
#### Output
@@ -211,6 +212,7 @@ Perform full-text search on a Supabase table
| `searchType` | string | No | Search type: plain, phrase, or websearch \(default: websearch\) |
| `language` | string | No | Language for text search configuration \(default: english\) |
| `limit` | number | No | Maximum number of rows to return |
| `offset` | number | No | Number of rows to skip \(for pagination\) |
| `apiKey` | string | Yes | Your Supabase service role secret key |
#### Output

View File

@@ -43,6 +43,8 @@ Retrieve form responses from Typeform
| `formId` | string | Yes | Typeform form ID \(e.g., "abc123XYZ"\) |
| `apiKey` | string | Yes | Typeform Personal Access Token |
| `pageSize` | number | No | Number of responses to retrieve \(e.g., 10, 25, 50\) |
| `before` | string | No | Cursor token for fetching the next page of older responses |
| `after` | string | No | Cursor token for fetching the next page of newer responses |
| `since` | string | No | Retrieve responses submitted after this date \(e.g., "2024-01-01T00:00:00Z"\) |
| `until` | string | No | Retrieve responses submitted before this date \(e.g., "2024-12-31T23:59:59Z"\) |
| `completed` | string | No | Filter by completion status \(e.g., "true", "false", "all"\) |

View File

@@ -67,10 +67,9 @@ Retrieve a list of tickets from Zendesk with optional filtering
| `type` | string | No | Filter by type: "problem", "incident", "question", or "task" |
| `assigneeId` | string | No | Filter by assignee user ID as a numeric string \(e.g., "12345"\) |
| `organizationId` | string | No | Filter by organization ID as a numeric string \(e.g., "67890"\) |
| `sortBy` | string | No | Sort field: "created_at", "updated_at", "priority", or "status" |
| `sortOrder` | string | No | Sort order: "asc" or "desc" |
| `sort` | string | No | Sort field for ticket listing \(only applies without filters\): "updated_at", "id", or "status". Prefix with "-" for descending \(e.g., "-updated_at"\) |
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
#### Output
@@ -129,10 +128,10 @@ Retrieve a list of tickets from Zendesk with optional filtering
| ↳ `from_messaging_channel` | boolean | Whether the ticket originated from a messaging channel |
| ↳ `ticket_form_id` | number | Ticket form ID |
| ↳ `generated_timestamp` | number | Unix timestamp of the ticket generation |
| `paging` | object | Pagination information |
| `paging` | object | Cursor-based pagination information |
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
| ↳ `has_more` | boolean | Whether more results are available |
| ↳ `next_page` | string | URL for next page of results |
| ↳ `previous_page` | string | URL for previous page of results |
| ↳ `count` | number | Total count of items |
| `metadata` | object | Response metadata |
| ↳ `total_returned` | number | Number of items returned in this response |
| ↳ `has_more` | boolean | Whether more items are available |
@@ -515,7 +514,7 @@ Retrieve a list of users from Zendesk with optional filtering
| `role` | string | No | Filter by role: "end-user", "agent", or "admin" |
| `permissionSet` | string | No | Filter by permission set ID as a numeric string \(e.g., "12345"\) |
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
#### Output
@@ -563,10 +562,10 @@ Retrieve a list of users from Zendesk with optional filtering
| ↳ `shared` | boolean | Whether the user is shared from a different Zendesk |
| ↳ `shared_agent` | boolean | Whether the agent is shared from a different Zendesk |
| ↳ `remote_photo_url` | string | URL to a remote photo |
| `paging` | object | Pagination information |
| `paging` | object | Cursor-based pagination information |
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
| ↳ `has_more` | boolean | Whether more results are available |
| ↳ `next_page` | string | URL for next page of results |
| ↳ `previous_page` | string | URL for previous page of results |
| ↳ `count` | number | Total count of items |
| `metadata` | object | Response metadata |
| ↳ `total_returned` | number | Number of items returned in this response |
| ↳ `has_more` | boolean | Whether more items are available |
@@ -706,7 +705,7 @@ Search for users in Zendesk using a query string
| `query` | string | No | Search query string \(e.g., user name or email\) |
| `externalId` | string | No | External ID to search by \(your system identifier\) |
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
| `page` | string | No | Page number for pagination \(1-based\) |
#### Output
@@ -754,10 +753,10 @@ Search for users in Zendesk using a query string
| ↳ `shared` | boolean | Whether the user is shared from a different Zendesk |
| ↳ `shared_agent` | boolean | Whether the agent is shared from a different Zendesk |
| ↳ `remote_photo_url` | string | URL to a remote photo |
| `paging` | object | Pagination information |
| `paging` | object | Cursor-based pagination information |
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
| ↳ `has_more` | boolean | Whether more results are available |
| ↳ `next_page` | string | URL for next page of results |
| ↳ `previous_page` | string | URL for previous page of results |
| ↳ `count` | number | Total count of items |
| `metadata` | object | Response metadata |
| ↳ `total_returned` | number | Number of items returned in this response |
| ↳ `has_more` | boolean | Whether more items are available |
@@ -999,7 +998,7 @@ Retrieve a list of organizations from Zendesk
| `apiToken` | string | Yes | Zendesk API token |
| `subdomain` | string | Yes | Your Zendesk subdomain \(e.g., "mycompany" for mycompany.zendesk.com\) |
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
#### Output
@@ -1020,10 +1019,10 @@ Retrieve a list of organizations from Zendesk
| ↳ `created_at` | string | When the organization was created \(ISO 8601 format\) |
| ↳ `updated_at` | string | When the organization was last updated \(ISO 8601 format\) |
| ↳ `external_id` | string | External ID for linking to external records |
| `paging` | object | Pagination information |
| `paging` | object | Cursor-based pagination information |
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
| ↳ `has_more` | boolean | Whether more results are available |
| ↳ `next_page` | string | URL for next page of results |
| ↳ `previous_page` | string | URL for previous page of results |
| ↳ `count` | number | Total count of items |
| `metadata` | object | Response metadata |
| ↳ `total_returned` | number | Number of items returned in this response |
| ↳ `has_more` | boolean | Whether more items are available |
@@ -1075,7 +1074,7 @@ Autocomplete organizations in Zendesk by name prefix (for name matching/autocomp
| `subdomain` | string | Yes | Your Zendesk subdomain |
| `name` | string | Yes | Organization name prefix to search for \(e.g., "Acme"\) |
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
| `page` | string | No | Page number for pagination \(1-based\) |
#### Output
@@ -1096,10 +1095,10 @@ Autocomplete organizations in Zendesk by name prefix (for name matching/autocomp
| ↳ `created_at` | string | When the organization was created \(ISO 8601 format\) |
| ↳ `updated_at` | string | When the organization was last updated \(ISO 8601 format\) |
| ↳ `external_id` | string | External ID for linking to external records |
| `paging` | object | Pagination information |
| `paging` | object | Cursor-based pagination information |
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
| ↳ `has_more` | boolean | Whether more results are available |
| ↳ `next_page` | string | URL for next page of results |
| ↳ `previous_page` | string | URL for previous page of results |
| ↳ `count` | number | Total count of items |
| `metadata` | object | Response metadata |
| ↳ `total_returned` | number | Number of items returned in this response |
| ↳ `has_more` | boolean | Whether more items are available |
@@ -1249,19 +1248,18 @@ Unified search across tickets, users, and organizations in Zendesk
| `apiToken` | string | Yes | Zendesk API token |
| `subdomain` | string | Yes | Your Zendesk subdomain |
| `query` | string | Yes | Search query string using Zendesk search syntax \(e.g., "type:ticket status:open"\) |
| `sortBy` | string | No | Sort field: "relevance", "created_at", "updated_at", "priority", "status", or "ticket_type" |
| `sortOrder` | string | No | Sort order: "asc" or "desc" |
| `filterType` | string | Yes | Resource type to search for: "ticket", "user", "organization", or "group" |
| `perPage` | string | No | Results per page as a number string \(default: "100", max: "100"\) |
| `page` | string | No | Page number as a string \(e.g., "1", "2"\) |
| `pageAfter` | string | No | Cursor from a previous response to fetch the next page of results |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `paging` | object | Pagination information |
| `paging` | object | Cursor-based pagination information |
| ↳ `after_cursor` | string | Cursor for fetching the next page of results |
| ↳ `has_more` | boolean | Whether more results are available |
| ↳ `next_page` | string | URL for next page of results |
| ↳ `previous_page` | string | URL for previous page of results |
| ↳ `count` | number | Total count of items |
| `metadata` | object | Response metadata |
| ↳ `total_returned` | number | Number of items returned in this response |
| ↳ `has_more` | boolean | Whether more items are available |

View File

@@ -1,5 +1,3 @@
'use server'
import { env } from '@/lib/core/config/env'
import { isProd } from '@/lib/core/config/feature-flags'

View File

@@ -85,7 +85,7 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
transform: isAnimated ? 'translateY(0) scale(1)' : 'translateY(8px) scale(0.98)',
transition:
'opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1), transform 0.6s cubic-bezier(0.22, 1, 0.36, 1)',
willChange: 'transform, opacity',
willChange: isAnimated ? 'auto' : 'transform, opacity',
}}
>
<LandingBlock icon={data.icon} color={data.color} name={data.name} tags={data.tags} />

View File

@@ -67,7 +67,6 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
strokeLinejoin: 'round',
pointerEvents: 'none',
animation: `landing-edge-dash-${id} 1s linear infinite`,
willChange: 'stroke-dashoffset',
...style,
}}
/>

View File

@@ -754,3 +754,100 @@ input[type="search"]::-ms-clear {
text-decoration: none !important;
color: inherit !important;
}
/**
* Respect user's prefers-reduced-motion setting (WCAG 2.3.3)
* Disables animations and transitions for users who prefer reduced motion.
*/
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* WandPromptBar status indicator */
@keyframes smoke-pulse {
0%,
100% {
transform: scale(0.8);
opacity: 0.4;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.status-indicator {
position: relative;
width: 12px;
height: 12px;
border-radius: 50%;
overflow: hidden;
background-color: hsl(var(--muted-foreground) / 0.5);
transition: background-color 0.3s ease;
}
.status-indicator.streaming {
background-color: transparent;
}
.status-indicator.streaming::before {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(
circle,
hsl(var(--primary) / 0.9) 0%,
hsl(var(--primary) / 0.4) 60%,
transparent 80%
);
animation: smoke-pulse 1.8s ease-in-out infinite;
opacity: 0.9;
}
.dark .status-indicator.streaming::before {
background: #6b7280;
opacity: 0.9;
animation: smoke-pulse 1.8s ease-in-out infinite;
}
/* MessageContainer loading dot */
@keyframes growShrink {
0%,
100% {
transform: scale(0.9);
}
50% {
transform: scale(1.1);
}
}
.loading-dot {
animation: growShrink 1.5s infinite ease-in-out;
}
/* Subflow node z-index and drag-over styles */
.workflow-container .react-flow__node-subflowNode {
z-index: -1 !important;
}
.workflow-container .react-flow__node-subflowNode:has([data-subflow-selected="true"]) {
z-index: 10 !important;
}
.loop-node-drag-over,
.parallel-node-drag-over {
box-shadow: 0 0 0 1.75px var(--brand-secondary) !important;
border-radius: 8px !important;
}
.react-flow__node[data-parent-node-id] .react-flow__handle {
z-index: 30;
}

View File

@@ -22,15 +22,20 @@ interface PipedriveFile {
interface PipedriveApiResponse {
success: boolean
data?: PipedriveFile[]
additional_data?: {
pagination?: {
more_items_in_collection: boolean
next_start: number
}
}
error?: string
}
const PipedriveGetFilesSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
deal_id: z.string().optional().nullable(),
person_id: z.string().optional().nullable(),
org_id: z.string().optional().nullable(),
sort: z.enum(['id', 'update_time']).optional().nullable(),
limit: z.string().optional().nullable(),
start: z.string().optional().nullable(),
downloadFiles: z.boolean().optional().default(false),
})
@@ -54,20 +59,19 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const validatedData = PipedriveGetFilesSchema.parse(body)
const { accessToken, deal_id, person_id, org_id, limit, downloadFiles } = validatedData
const { accessToken, sort, limit, start, downloadFiles } = validatedData
const baseUrl = 'https://api.pipedrive.com/v1/files'
const queryParams = new URLSearchParams()
if (deal_id) queryParams.append('deal_id', deal_id)
if (person_id) queryParams.append('person_id', person_id)
if (org_id) queryParams.append('org_id', org_id)
if (sort) queryParams.append('sort', sort)
if (limit) queryParams.append('limit', limit)
if (start) queryParams.append('start', start)
const queryString = queryParams.toString()
const apiUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl
logger.info(`[${requestId}] Fetching files from Pipedrive`, { deal_id, person_id, org_id })
logger.info(`[${requestId}] Fetching files from Pipedrive`)
const urlValidation = await validateUrlWithDNS(apiUrl, 'apiUrl')
if (!urlValidation.isValid) {
@@ -93,6 +97,8 @@ export async function POST(request: NextRequest) {
}
const files = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
const nextStart = data.additional_data?.pagination?.next_start ?? null
const downloadedFiles: Array<{
name: string
mimeType: string
@@ -149,6 +155,8 @@ export async function POST(request: NextRequest) {
files,
downloadedFiles: downloadedFiles.length > 0 ? downloadedFiles : undefined,
total_items: files.length,
has_more: hasMore,
next_start: nextStart,
success: true,
},
})

View File

@@ -30,21 +30,6 @@ export const ChatMessageContainer = memo(function ChatMessageContainer({
}: ChatMessageContainerProps) {
return (
<div className='relative flex flex-1 flex-col overflow-hidden bg-white'>
<style jsx>{`
@keyframes growShrink {
0%,
100% {
transform: scale(0.9);
}
50% {
transform: scale(1.1);
}
}
.loading-dot {
animation: growShrink 1.5s infinite ease-in-out;
}
`}</style>
{/* Scrollable Messages Area */}
<div
ref={messagesContainerRef}

View File

@@ -71,7 +71,7 @@ export function VoiceInterface({
const [state, setState] = useState<'idle' | 'listening' | 'agent_speaking'>('idle')
const [isInitialized, setIsInitialized] = useState(false)
const [isMuted, setIsMuted] = useState(false)
const [audioLevels, setAudioLevels] = useState<number[]>(new Array(200).fill(0))
const [audioLevels, setAudioLevels] = useState<number[]>(() => new Array(200).fill(0))
const [permissionStatus, setPermissionStatus] = useState<'prompt' | 'granted' | 'denied'>(
'prompt'
)

View File

@@ -1,4 +1,4 @@
import { redirect } from 'next/navigation'
import { redirect, unstable_rethrow } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
@@ -14,24 +14,27 @@ interface FileViewerPageProps {
export default async function FileViewerPage({ params }: FileViewerPageProps) {
const { workspaceId, fileId } = await params
try {
const session = await getSession()
if (!session?.user?.id) {
redirect('/')
}
const session = await getSession()
if (!session?.user?.id) {
redirect('/')
}
const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
if (!hasPermission) {
redirect(`/workspace/${workspaceId}`)
}
const fileRecord = await getWorkspaceFile(workspaceId, fileId)
if (!fileRecord) {
redirect(`/workspace/${workspaceId}`)
}
return <FileViewer file={fileRecord} />
} catch (error) {
const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
if (!hasPermission) {
redirect(`/workspace/${workspaceId}`)
}
let fileRecord: Awaited<ReturnType<typeof getWorkspaceFile>>
try {
fileRecord = await getWorkspaceFile(workspaceId, fileId)
} catch (error) {
unstable_rethrow(error)
redirect(`/workspace/${workspaceId}`)
}
if (!fileRecord) {
redirect(`/workspace/${workspaceId}`)
}
return <FileViewer file={fileRecord} />
}

View File

@@ -131,10 +131,8 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
resumeActiveStream,
})
// Handle scroll management (80px stickiness for copilot)
const { scrollAreaRef, scrollToBottom } = useScrollManagement(messages, isSendingMessage, {
stickinessThreshold: 40,
})
// Handle scroll management
const { scrollAreaRef, scrollToBottom } = useScrollManagement(messages, isSendingMessage)
// Handle chat history grouping
const { groupedChats, handleHistoryDropdownOpen: handleHistoryDropdownOpenHook } = useChatHistory(

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { isEqual } from 'lodash'
import isEqual from 'lodash/isEqual'
import { useReactFlow } from 'reactflow'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import { Combobox, type ComboboxOption } from '@/components/emcn/components'

View File

@@ -1,5 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { isEqual } from 'lodash'
import isEqual from 'lodash/isEqual'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import { Badge } from '@/components/emcn'
import { Combobox, type ComboboxOption } from '@/components/emcn/components'

View File

@@ -7,7 +7,7 @@ import {
useRef,
useState,
} from 'react'
import { isEqual } from 'lodash'
import isEqual from 'lodash/isEqual'
import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react'
import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
import { Trash } from '@/components/emcn/icons/trash'

View File

@@ -1,5 +1,5 @@
import { type JSX, type MouseEvent, memo, useCallback, useRef, useState } from 'react'
import { isEqual } from 'lodash'
import isEqual from 'lodash/isEqual'
import { AlertTriangle, ArrowLeftRight, ArrowUp, Check, Clipboard } from 'lucide-react'
import { Button, Input, Label, Tooltip } from '@/components/emcn/components'
import { cn } from '@/lib/core/utils/cn'

View File

@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { isEqual } from 'lodash'
import isEqual from 'lodash/isEqual'
import {
BookOpen,
Check,

View File

@@ -10,40 +10,6 @@ import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { usePanelEditorStore } from '@/stores/panel'
/**
* Global styles for subflow nodes (loop and parallel containers).
* Includes animations for drag-over states and hover effects.
*
* @returns Style component with global CSS
*/
const SubflowNodeStyles: React.FC = () => {
return (
<style jsx global>{`
/* Z-index management for subflow nodes - default behind blocks */
.workflow-container .react-flow__node-subflowNode {
z-index: -1 !important;
}
/* Selected subflows appear above other subflows but below blocks (z-21) */
.workflow-container .react-flow__node-subflowNode:has([data-subflow-selected='true']) {
z-index: 10 !important;
}
/* Drag-over states */
.loop-node-drag-over,
.parallel-node-drag-over {
box-shadow: 0 0 0 1.75px var(--brand-secondary) !important;
border-radius: 8px !important;
}
/* Handle z-index for nested nodes */
.react-flow__node[data-parent-node-id] .react-flow__handle {
z-index: 30;
}
`}</style>
)
}
/**
* Data structure for subflow nodes (loop and parallel containers)
*/
@@ -151,133 +117,130 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
)
return (
<>
<SubflowNodeStyles />
<div className='group relative'>
<div className='group relative'>
<div
ref={blockRef}
onClick={() => setCurrentBlockId(id)}
className={cn(
'workflow-drag-handle relative cursor-grab select-none rounded-[8px] border border-[var(--border-1)] [&:active]:cursor-grabbing',
'transition-block-bg transition-ring',
'z-[20]'
)}
style={{
width: data.width || 500,
height: data.height || 300,
position: 'relative',
overflow: 'visible',
pointerEvents: isPreview ? 'none' : 'all',
}}
data-node-id={id}
data-type='subflowNode'
data-nesting-level={nestingLevel}
data-subflow-selected={isFocused || isSelected || isPreviewSelected}
>
{!isPreview && (
<ActionBar blockId={id} blockType={data.kind} disabled={!userPermissions.canEdit} />
)}
{/* Header Section */}
<div
ref={blockRef}
onClick={() => setCurrentBlockId(id)}
className={cn(
'workflow-drag-handle relative cursor-grab select-none rounded-[8px] border border-[var(--border-1)] [&:active]:cursor-grabbing',
'transition-block-bg transition-ring',
'z-[20]'
'flex items-center justify-between rounded-t-[8px] border-[var(--border)] border-b bg-[var(--surface-2)] py-[8px] pr-[12px] pl-[8px]'
)}
style={{
width: data.width || 500,
height: data.height || 300,
position: 'relative',
overflow: 'visible',
pointerEvents: isPreview ? 'none' : 'all',
}}
data-node-id={id}
data-type='subflowNode'
data-nesting-level={nestingLevel}
data-subflow-selected={isFocused || isSelected || isPreviewSelected}
>
{!isPreview && (
<ActionBar blockId={id} blockType={data.kind} disabled={!userPermissions.canEdit} />
)}
{/* Header Section */}
<div
className={cn(
'flex items-center justify-between rounded-t-[8px] border-[var(--border)] border-b bg-[var(--surface-2)] py-[8px] pr-[12px] pl-[8px]'
)}
>
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ backgroundColor: isEnabled ? blockIconBg : 'gray' }}
>
<BlockIcon className='h-[16px] w-[16px] text-white' />
</div>
<span
className={cn(
'truncate font-medium text-[16px]',
!isEnabled && 'text-[var(--text-muted)]'
)}
title={blockName}
>
{blockName}
</span>
</div>
<div className='flex items-center gap-1'>
{!isEnabled && <Badge variant='gray-secondary'>disabled</Badge>}
{isLocked && <Badge variant='gray-secondary'>locked</Badge>}
</div>
</div>
{!isPreview && (
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='absolute right-[8px] bottom-[8px] z-20 flex h-[32px] w-[32px] cursor-se-resize items-center justify-center text-muted-foreground'
style={{ pointerEvents: 'auto' }}
/>
)}
<div
className='h-[calc(100%-50px)] pt-[16px] pr-[80px] pb-[16px] pl-[16px]'
data-dragarea='true'
style={{
position: 'relative',
pointerEvents: isPreview ? 'none' : 'auto',
}}
>
{/* Subflow Start */}
<div
className='absolute top-[16px] left-[16px] flex items-center justify-center rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)] px-[12px] py-[6px]'
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
data-parent-id={id}
data-node-role={`${data.kind}-start`}
data-extent='parent'
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ backgroundColor: isEnabled ? blockIconBg : 'gray' }}
>
<span className='font-medium text-[14px] text-[var(--text-primary)]'>Start</span>
<Handle
type='source'
position={Position.Right}
id={startHandleId}
className={getHandleClasses('right')}
style={{
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'auto',
}}
data-parent-id={id}
/>
<BlockIcon className='h-[16px] w-[16px] text-white' />
</div>
<span
className={cn(
'truncate font-medium text-[16px]',
!isEnabled && 'text-[var(--text-muted)]'
)}
title={blockName}
>
{blockName}
</span>
</div>
<div className='flex items-center gap-1'>
{!isEnabled && <Badge variant='gray-secondary'>disabled</Badge>}
{isLocked && <Badge variant='gray-secondary'>locked</Badge>}
</div>
{/* Input handle on left middle */}
<Handle
type='target'
position={Position.Left}
className={getHandleClasses('left')}
style={{
...getHandleStyle(),
pointerEvents: 'auto',
}}
/>
{/* Output handle on right middle */}
<Handle
type='source'
position={Position.Right}
className={getHandleClasses('right')}
style={{
...getHandleStyle(),
pointerEvents: 'auto',
}}
id={endHandleId}
/>
{hasRing && (
<div
className={cn('pointer-events-none absolute inset-0 z-40 rounded-[8px]', ringStyles)}
/>
)}
</div>
{!isPreview && (
<div
className='absolute right-[8px] bottom-[8px] z-20 flex h-[32px] w-[32px] cursor-se-resize items-center justify-center text-muted-foreground'
style={{ pointerEvents: 'auto' }}
/>
)}
<div
className='h-[calc(100%-50px)] pt-[16px] pr-[80px] pb-[16px] pl-[16px]'
data-dragarea='true'
style={{
position: 'relative',
pointerEvents: isPreview ? 'none' : 'auto',
}}
>
{/* Subflow Start */}
<div
className='absolute top-[16px] left-[16px] flex items-center justify-center rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-2)] px-[12px] py-[6px]'
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
data-parent-id={id}
data-node-role={`${data.kind}-start`}
data-extent='parent'
>
<span className='font-medium text-[14px] text-[var(--text-primary)]'>Start</span>
<Handle
type='source'
position={Position.Right}
id={startHandleId}
className={getHandleClasses('right')}
style={{
top: '50%',
transform: 'translateY(-50%)',
pointerEvents: 'auto',
}}
data-parent-id={id}
/>
</div>
</div>
{/* Input handle on left middle */}
<Handle
type='target'
position={Position.Left}
className={getHandleClasses('left')}
style={{
...getHandleStyle(),
pointerEvents: 'auto',
}}
/>
{/* Output handle on right middle */}
<Handle
type='source'
position={Position.Right}
className={getHandleClasses('right')}
style={{
...getHandleStyle(),
pointerEvents: 'auto',
}}
id={endHandleId}
/>
{hasRing && (
<div
className={cn('pointer-events-none absolute inset-0 z-40 rounded-[8px]', ringStyles)}
/>
)}
</div>
</>
</div>
)
})

View File

@@ -134,57 +134,6 @@ export function WandPromptBar({
</Button>
)}
</div>
<style jsx global>{`
@keyframes smoke-pulse {
0%,
100% {
transform: scale(0.8);
opacity: 0.4;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.status-indicator {
position: relative;
width: 12px;
height: 12px;
border-radius: 50%;
overflow: hidden;
background-color: hsl(var(--muted-foreground) / 0.5);
transition: background-color 0.3s ease;
}
.status-indicator.streaming {
background-color: transparent;
}
.status-indicator.streaming::before {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: radial-gradient(
circle,
hsl(var(--primary) / 0.9) 0%,
hsl(var(--primary) / 0.4) 60%,
transparent 80%
);
animation: smoke-pulse 1.8s ease-in-out infinite;
opacity: 0.9;
}
.dark .status-indicator.streaming::before {
background: #6b7280;
opacity: 0.9;
animation: smoke-pulse 1.8s ease-in-out infinite;
}
`}</style>
</div>
)
}

View File

@@ -1,6 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { isEqual } from 'lodash'
import isEqual from 'lodash/isEqual'
import { useParams } from 'next/navigation'
import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow'
import { useStoreWithEqualityFn } from 'zustand/traditional'

View File

@@ -16,7 +16,7 @@ interface UseScrollManagementOptions {
/**
* Distance from bottom (in pixels) within which auto-scroll stays active
* @remarks Lower values = less sticky (user can scroll away easier)
* @defaultValue 100
* @defaultValue 30
*/
stickinessThreshold?: number
}
@@ -41,7 +41,7 @@ export function useScrollManagement(
const lastScrollTopRef = useRef(0)
const scrollBehavior = options?.behavior ?? 'smooth'
const stickinessThreshold = options?.stickinessThreshold ?? 100
const stickinessThreshold = options?.stickinessThreshold ?? 30
/** Scrolls the container to the bottom */
const scrollToBottom = useCallback(() => {

View File

@@ -514,6 +514,7 @@ export function HelpModal({ open, onOpenChange, workflowId, workspaceId }: HelpM
alt={`Preview ${index + 1}`}
fill
unoptimized
sizes='(max-width: 768px) 100vw, 50vw'
className='object-contain'
/>
<button

View File

@@ -165,12 +165,16 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
logger.info('Subscription restored successfully', result)
}
await queryClient.invalidateQueries({ queryKey: subscriptionKeys.all })
if (activeOrgId) {
await queryClient.invalidateQueries({ queryKey: organizationKeys.detail(activeOrgId) })
await queryClient.invalidateQueries({ queryKey: organizationKeys.billing(activeOrgId) })
await queryClient.invalidateQueries({ queryKey: organizationKeys.lists() })
}
await Promise.all([
queryClient.invalidateQueries({ queryKey: subscriptionKeys.all }),
...(activeOrgId
? [
queryClient.invalidateQueries({ queryKey: organizationKeys.detail(activeOrgId) }),
queryClient.invalidateQueries({ queryKey: organizationKeys.billing(activeOrgId) }),
queryClient.invalidateQueries({ queryKey: organizationKeys.lists() }),
]
: []),
])
setIsDialogOpen(false)
} catch (err) {

View File

@@ -37,7 +37,7 @@ export const UsageLimit = forwardRef<UsageLimitRef, UsageLimitProps>(
},
ref
) => {
const [inputValue, setInputValue] = useState(currentLimit.toString())
const [inputValue, setInputValue] = useState(() => currentLimit.toString())
const [hasError, setHasError] = useState(false)
const [errorType, setErrorType] = useState<'general' | 'belowUsage' | null>(null)
const [isEditing, setIsEditing] = useState(false)

View File

@@ -92,12 +92,9 @@ export const IncidentioBlock: BlockConfig<IncidentioResponse> = {
field: 'operation',
value: [
'incidentio_incidents_list',
'incidentio_actions_list',
'incidentio_follow_ups_list',
'incidentio_users_list',
'incidentio_workflows_list',
'incidentio_schedules_list',
'incidentio_escalations_list',
'incidentio_incident_updates_list',
'incidentio_schedule_entries_list',
],
@@ -113,6 +110,7 @@ export const IncidentioBlock: BlockConfig<IncidentioResponse> = {
field: 'operation',
value: [
'incidentio_incidents_list',
'incidentio_users_list',
'incidentio_workflows_list',
'incidentio_schedules_list',
'incidentio_incident_updates_list',

View File

@@ -216,31 +216,21 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
condition: { field: 'operation', value: ['update_deal'] },
},
{
id: 'deal_id',
title: 'Deal ID',
type: 'short-input',
placeholder: 'Filter by deal ID ',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Filter by person ID ',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'org_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Filter by organization ID ',
id: 'sort',
title: 'Sort By',
type: 'dropdown',
options: [
{ label: 'ID', id: 'id' },
{ label: 'Update Time', id: 'update_time' },
],
value: () => 'id',
condition: { field: 'operation', value: ['get_files'] },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Number of results (default 100, max 500)',
placeholder: 'Number of results (default 100, max 100)',
condition: { field: 'operation', value: ['get_files'] },
},
{
@@ -305,8 +295,28 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
id: 'cursor',
title: 'Cursor',
type: 'short-input',
placeholder: 'Pagination cursor (optional)',
condition: { field: 'operation', value: ['get_pipelines'] },
placeholder: 'Pagination cursor from previous response',
condition: {
field: 'operation',
value: ['get_all_deals', 'get_projects'],
},
},
{
id: 'start',
title: 'Start (Offset)',
type: 'short-input',
placeholder: 'Pagination offset (e.g., 0, 100, 200)',
condition: {
field: 'operation',
value: [
'get_activities',
'get_leads',
'get_files',
'get_pipeline_deals',
'get_mail_messages',
'get_pipelines',
],
},
},
{
id: 'pipeline_id',
@@ -323,19 +333,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
placeholder: 'Filter by stage ID ',
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'status',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Open', id: 'open' },
{ label: 'Won', id: 'won' },
{ label: 'Lost', id: 'lost' },
],
value: () => '',
condition: { field: 'operation', value: ['get_pipeline_deals'] },
},
{
id: 'limit',
title: 'Limit',
@@ -426,22 +423,29 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
id: 'deal_id',
title: 'Deal ID',
type: 'short-input',
placeholder: 'Filter by deal ID ',
condition: { field: 'operation', value: ['get_activities', 'create_activity'] },
placeholder: 'Associated deal ID ',
condition: { field: 'operation', value: ['create_activity'] },
},
{
id: 'person_id',
title: 'Person ID',
type: 'short-input',
placeholder: 'Filter by person ID ',
condition: { field: 'operation', value: ['get_activities', 'create_activity'] },
placeholder: 'Associated person ID ',
condition: { field: 'operation', value: ['create_activity'] },
},
{
id: 'org_id',
title: 'Organization ID',
type: 'short-input',
placeholder: 'Filter by organization ID ',
condition: { field: 'operation', value: ['get_activities', 'create_activity'] },
placeholder: 'Associated organization ID ',
condition: { field: 'operation', value: ['create_activity'] },
},
{
id: 'user_id',
title: 'User ID',
type: 'short-input',
placeholder: 'Filter by user ID',
condition: { field: 'operation', value: ['get_activities'] },
},
{
id: 'type',
@@ -781,7 +785,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
thread_id: { type: 'string', description: 'Mail thread ID' },
sort_by: { type: 'string', description: 'Field to sort by' },
sort_direction: { type: 'string', description: 'Sorting direction' },
cursor: { type: 'string', description: 'Pagination cursor' },
cursor: { type: 'string', description: 'Pagination cursor (v2 endpoints)' },
start: { type: 'string', description: 'Pagination start offset (v1 endpoints)' },
project_id: { type: 'string', description: 'Project ID' },
description: { type: 'string', description: 'Description' },
start_date: { type: 'string', description: 'Start date' },
@@ -793,12 +798,15 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
due_time: { type: 'string', description: 'Due time' },
duration: { type: 'string', description: 'Duration' },
done: { type: 'string', description: 'Completion status' },
user_id: { type: 'string', description: 'User ID' },
note: { type: 'string', description: 'Notes' },
lead_id: { type: 'string', description: 'Lead ID' },
archived: { type: 'string', description: 'Archived status' },
value_amount: { type: 'string', description: 'Value amount' },
value_currency: { type: 'string', description: 'Value currency' },
is_archived: { type: 'string', description: 'Archive status' },
organization_id: { type: 'string', description: 'Organization ID' },
owner_id: { type: 'string', description: 'Owner user ID' },
},
outputs: {
deals: { type: 'json', description: 'Array of deal objects' },

View File

@@ -445,6 +445,13 @@ Return ONLY the order by expression - no explanations, no extra text.`,
placeholder: '100',
condition: { field: 'operation', value: 'query' },
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: '0',
condition: { field: 'operation', value: 'query' },
},
// Vector search operation fields
{
id: 'functionName',
@@ -543,6 +550,13 @@ Return ONLY the order by expression - no explanations, no extra text.`,
placeholder: '100',
condition: { field: 'operation', value: 'text_search' },
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: '0',
condition: { field: 'operation', value: 'text_search' },
},
// Count operation fields
{
id: 'filter',

View File

@@ -66,6 +66,20 @@ export const TypeformBlock: BlockConfig<TypeformResponse> = {
placeholder: 'Number of responses per page (default: 25)',
condition: { field: 'operation', value: 'typeform_responses' },
},
{
id: 'before',
title: 'Before (Cursor)',
type: 'short-input',
placeholder: 'Cursor token from previous response for pagination',
condition: { field: 'operation', value: 'typeform_responses' },
},
{
id: 'after',
title: 'After (Cursor)',
type: 'short-input',
placeholder: 'Cursor token from previous response for newer results',
condition: { field: 'operation', value: 'typeform_responses' },
},
{
id: 'since',
title: 'Since',
@@ -380,6 +394,8 @@ Do not include any explanations, markdown formatting, or other text outside the
apiKey: { type: 'string', description: 'Personal access token' },
// Response operation params
pageSize: { type: 'number', description: 'Responses per page' },
before: { type: 'string', description: 'Cursor token for fetching the next page' },
after: { type: 'string', description: 'Cursor token for fetching newer results' },
since: { type: 'string', description: 'Start date filter' },
until: { type: 'string', description: 'End date filter' },
completed: { type: 'string', description: 'Completion status filter' },

View File

@@ -444,33 +444,36 @@ Return ONLY the search query - no explanations.`,
},
},
{
id: 'sortBy',
title: 'Sort By',
id: 'filterType',
title: 'Resource Type',
type: 'dropdown',
options: [
{ label: 'Relevance', id: 'relevance' },
{ label: 'Created At', id: 'created_at' },
{ label: 'Updated At', id: 'updated_at' },
{ label: 'Priority', id: 'priority' },
{ label: 'Status', id: 'status' },
{ label: 'Ticket Type', id: 'ticket_type' },
{ label: 'Ticket', id: 'ticket' },
{ label: 'User', id: 'user' },
{ label: 'Organization', id: 'organization' },
{ label: 'Group', id: 'group' },
],
required: true,
condition: {
field: 'operation',
value: ['search'],
},
},
{
id: 'sortOrder',
title: 'Sort Order',
id: 'sort',
title: 'Sort',
type: 'dropdown',
options: [
{ label: 'Ascending', id: 'asc' },
{ label: 'Descending', id: 'desc' },
{ label: 'Updated At (Asc)', id: 'updated_at' },
{ label: 'Updated At (Desc)', id: '-updated_at' },
{ label: 'ID (Asc)', id: 'id' },
{ label: 'ID (Desc)', id: '-id' },
{ label: 'Status (Asc)', id: 'status' },
{ label: 'Status (Desc)', id: '-status' },
],
condition: {
field: 'operation',
value: ['search'],
value: ['get_tickets'],
},
},
// Pagination fields
@@ -492,20 +495,25 @@ Return ONLY the search query - no explanations.`,
},
},
{
id: 'page',
title: 'Page',
id: 'pageAfter',
title: 'Page After (Cursor)',
type: 'short-input',
placeholder: 'Page number',
placeholder: 'Cursor from previous response (after_cursor)',
description: 'Cursor value from a previous response to fetch the next page of results',
condition: {
field: 'operation',
value: [
'get_tickets',
'get_users',
'get_organizations',
'search_users',
'autocomplete_organizations',
'search',
],
value: ['get_tickets', 'get_users', 'get_organizations', 'search'],
},
},
{
id: 'page',
title: 'Page Number',
type: 'short-input',
placeholder: 'Page number (default: 1)',
description: 'Page number for offset-based pagination',
condition: {
field: 'operation',
value: ['search_users', 'autocomplete_organizations'],
},
},
],
@@ -624,6 +632,7 @@ Return ONLY the search query - no explanations.`,
email: { type: 'string', description: 'Zendesk email address' },
apiToken: { type: 'string', description: 'Zendesk API token' },
subdomain: { type: 'string', description: 'Zendesk subdomain' },
sort: { type: 'string', description: 'Sort field for ticket listing' },
},
outputs: {
// Ticket operations - list
@@ -665,8 +674,11 @@ Return ONLY the search query - no explanations.`,
type: 'boolean',
description: 'Deletion confirmation (delete_ticket, delete_user, delete_organization)',
},
// Pagination (shared across list operations)
paging: { type: 'json', description: 'Pagination information for list operations' },
// Cursor-based pagination (shared across list operations)
paging: {
type: 'json',
description: 'Cursor-based pagination information (after_cursor, has_more)',
},
// Metadata (shared across all operations)
metadata: { type: 'json', description: 'Operation metadata including operation type' },
},

View File

@@ -9,6 +9,7 @@ import {
type ReactNode,
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
@@ -170,6 +171,7 @@ const Combobox = memo(
},
ref
) => {
const listboxId = useId()
const [open, setOpen] = useState(false)
const [highlightedIndex, setHighlightedIndex] = useState(-1)
const [searchQuery, setSearchQuery] = useState('')
@@ -513,6 +515,7 @@ const Combobox = memo(
role='combobox'
aria-expanded={open}
aria-haspopup='listbox'
aria-controls={listboxId}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={cn(
@@ -616,7 +619,7 @@ const Combobox = memo(
}
}}
>
<div ref={dropdownRef} role='listbox'>
<div ref={dropdownRef} role='listbox' id={listboxId}>
{isLoading ? (
<div className='flex items-center justify-center py-[14px]'>
<Loader2 className='h-[16px] w-[16px] animate-spin text-[var(--text-muted)]' />

View File

@@ -27,12 +27,14 @@ const Alert = React.forwardRef<
Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
({ className, children, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
>
{children}
</h5>
)
)
AlertTitle.displayName = 'AlertTitle'

View File

@@ -16,26 +16,32 @@ export const mdxComponents: MDXRemoteProps['components'] = {
unoptimized
/>
),
h2: (props: any) => (
h2: ({ children, className, ...props }: any) => (
<h2
{...props}
style={{ fontSize: '30px', marginTop: '3rem', marginBottom: '1.5rem' }}
className={clsx('font-medium text-black leading-tight', props.className)}
/>
className={clsx('font-medium text-black leading-tight', className)}
>
{children}
</h2>
),
h3: (props: any) => (
h3: ({ children, className, ...props }: any) => (
<h3
{...props}
style={{ fontSize: '24px', marginTop: '1.5rem', marginBottom: '0.75rem' }}
className={clsx('font-medium leading-tight', props.className)}
/>
className={clsx('font-medium leading-tight', className)}
>
{children}
</h3>
),
h4: (props: any) => (
h4: ({ children, className, ...props }: any) => (
<h4
{...props}
style={{ fontSize: '19px', marginTop: '1.5rem', marginBottom: '0.75rem' }}
className={clsx('font-medium leading-tight', props.className)}
/>
className={clsx('font-medium leading-tight', className)}
>
{children}
</h4>
),
p: (props: any) => (
<p

View File

@@ -107,5 +107,3 @@ if (typeof process !== 'undefined') {
logger.info(`S3 copilot bucket: ${env.S3_COPILOT_BUCKET_NAME}`)
}
}
export default ensureUploadsDirectory

View File

@@ -326,6 +326,18 @@ const nextConfig: NextConfig = {
return redirects
},
async rewrites() {
return [
...(isHosted
? [
{
source: '/r/:shortCode',
destination: 'https://go.trybeluga.ai/:shortCode',
},
]
: []),
]
},
}
export default nextConfig

View File

@@ -14,6 +14,7 @@ import {
supportsNativeStructuredOutputs,
} from '@/providers/models'
import type { ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -842,15 +843,11 @@ export async function executeAnthropicProviderRequest(
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
}
@@ -1299,14 +1296,10 @@ export async function executeAnthropicProviderRequest(
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
}

View File

@@ -30,6 +30,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -251,7 +252,7 @@ async function executeChatCompletionsRequest(
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls: (FunctionCallResponse & { success: boolean })[] = []
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...allMessages]
let iterationCount = 0
@@ -577,15 +578,11 @@ async function executeChatCompletionsRequest(
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore - Adding timing property to the error
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
}

View File

@@ -22,11 +22,13 @@ import {
} from '@/providers/bedrock/utils'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import type {
FunctionCallResponse,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -419,8 +421,8 @@ export const bedrockProvider: ProviderConfig = {
pricing: initialCost.pricing,
}
const toolCalls: any[] = []
const toolResults: any[] = []
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...messages]
let iterationCount = 0
let hasUsedForcedTool = false
@@ -561,7 +563,7 @@ export const bedrockProvider: ProviderConfig = {
let resultContent: any
if (result.success) {
toolResults.push(result.output)
toolResults.push(result.output!)
resultContent = result.output
} else {
resultContent = {
@@ -903,15 +905,11 @@ export const bedrockProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -11,6 +11,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -539,15 +540,11 @@ export const cerebrasProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore - Adding timing property to error for debugging
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -10,6 +10,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -538,15 +539,11 @@ export const deepseekProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -10,6 +10,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -496,15 +497,11 @@ export const groqProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -11,6 +11,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -551,15 +552,11 @@ export const mistralProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore - Adding timing property to error for debugging
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -12,6 +12,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import { calculateCost, prepareToolExecution } from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers'
import { executeTool } from '@/tools'
@@ -554,15 +555,11 @@ export const ollamaProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -3,6 +3,7 @@ import type OpenAI from 'openai'
import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -806,14 +807,10 @@ export async function executeResponsesProviderRequest(
duration: totalDuration,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore - Adding timing property to the error
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
}

View File

@@ -10,11 +10,14 @@ import {
supportsNativeStructuredOutputs,
} from '@/providers/openrouter/utils'
import type {
FunctionCallResponse,
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
generateSchemaInstructions,
@@ -90,7 +93,7 @@ export const openRouterProvider: ProviderConfig = {
stream: !!request.stream,
})
const allMessages = [] as any[]
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({ role: 'system', content: request.systemPrompt })
@@ -237,8 +240,8 @@ export const openRouterProvider: ProviderConfig = {
output: currentResponse.usage?.completion_tokens || 0,
total: currentResponse.usage?.total_tokens || 0,
}
const toolCalls = [] as any[]
const toolResults = [] as any[]
const toolCalls: FunctionCallResponse[] = []
const toolResults: Record<string, unknown>[] = []
const currentMessages = [...allMessages]
let iterationCount = 0
let modelTime = firstResponseTime
@@ -352,7 +355,7 @@ export const openRouterProvider: ProviderConfig = {
let resultContent: any
if (result.success) {
toolResults.push(result.output)
toolResults.push(result.output!)
resultContent = result.output
} else {
resultContent = {
@@ -593,14 +596,11 @@ export const openRouterProvider: ProviderConfig = {
}
logger.error('Error in OpenRouter request:', errorDetails)
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -59,6 +59,7 @@ export interface FunctionCallResponse {
result?: Record<string, any>
output?: Record<string, any>
input?: Record<string, any>
success?: boolean
}
export interface TimeSegment {
@@ -177,4 +178,21 @@ export interface ProviderRequest {
previousInteractionId?: string
}
/**
* Typed error class for provider failures that includes timing information.
*/
export class ProviderError extends Error {
timing: {
startTime: string
endTime: string
duration: number
}
constructor(message: string, timing: { startTime: string; endTime: string; duration: number }) {
super(message)
this.name = 'ProviderError'
this.timing = timing
}
}
export const providers: Record<string, ProviderConfig> = {}

View File

@@ -6,11 +6,13 @@ import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import type {
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -98,7 +100,7 @@ export const vllmProvider: ProviderConfig = {
baseURL: `${baseUrl}/v1`,
})
const allMessages = [] as any[]
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({
@@ -635,23 +637,11 @@ export const vllmProvider: ProviderConfig = {
duration: totalDuration,
})
const enhancedError = new Error(errorMessage)
// @ts-ignore
enhancedError.timing = {
throw new ProviderError(errorMessage, {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
if (errorType) {
// @ts-ignore
enhancedError.vllmErrorType = errorType
}
if (errorCode) {
// @ts-ignore
enhancedError.vllmErrorCode = errorCode
}
throw enhancedError
})
}
},
}

View File

@@ -5,11 +5,13 @@ import type { StreamingExecution } from '@/executor/types'
import { MAX_TOOL_ITERATIONS } from '@/providers'
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
import type {
Message,
ProviderConfig,
ProviderRequest,
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { ProviderError } from '@/providers/types'
import {
calculateCost,
prepareToolExecution,
@@ -52,7 +54,7 @@ export const xAIProvider: ProviderConfig = {
streaming: !!request.stream,
})
const allMessages: any[] = []
const allMessages: Message[] = []
if (request.systemPrompt) {
allMessages.push({
@@ -587,15 +589,11 @@ export const xAIProvider: ProviderConfig = {
hasResponseFormat: !!request.responseFormat,
})
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
// @ts-ignore - Adding timing property to error for debugging
enhancedError.timing = {
throw new ProviderError(error instanceof Error ? error.message : String(error), {
startTime: providerStartTimeISO,
endTime: providerEndTimeISO,
duration: totalDuration,
}
throw enhancedError
})
}
},
}

View File

@@ -26,12 +26,6 @@ export const actionsListTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Filter actions by incident ID (e.g., "01FCNDV6P870EA6S7TK1DSYDG0")',
},
page_size: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of actions to return per page (e.g., 10, 25, 50)',
},
},
request: {
@@ -42,10 +36,6 @@ export const actionsListTool: ToolConfig<
url.searchParams.append('incident_id', params.incident_id)
}
if (params.page_size) {
url.searchParams.append('page_size', params.page_size.toString())
}
return url.toString()
},
method: 'GET',

View File

@@ -20,22 +20,10 @@ export const escalationsListTool: ToolConfig<
visibility: 'user-only',
description: 'incident.io API Key',
},
page_size: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results per page (e.g., 10, 25, 50). Default: 25',
},
},
request: {
url: (params) => {
const url = new URL('https://api.incident.io/v2/escalations')
if (params.page_size) {
url.searchParams.append('page_size', params.page_size.toString())
}
return url.toString()
},
url: () => 'https://api.incident.io/v2/escalations',
method: 'GET',
headers: (params) => ({
'Content-Type': 'application/json',

View File

@@ -26,12 +26,6 @@ export const followUpsListTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Filter follow-ups by incident ID (e.g., "01FCNDV6P870EA6S7TK1DSYDG0")',
},
page_size: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of follow-ups to return per page (e.g., 10, 25, 50)',
},
},
request: {
@@ -42,10 +36,6 @@ export const followUpsListTool: ToolConfig<
url.searchParams.append('incident_id', params.incident_id)
}
if (params.page_size) {
url.searchParams.append('page_size', params.page_size.toString())
}
return url.toString()
},
method: 'GET',

View File

@@ -396,7 +396,6 @@ export interface IncidentioIncidentsUpdateResponse extends ToolResponse {
// Action types
export interface IncidentioActionsListParams extends IncidentioBaseParams {
incident_id?: string
page_size?: number
}
export interface IncidentioAction {
@@ -446,7 +445,6 @@ export interface IncidentioActionsShowResponse extends ToolResponse {
// Follow-up types
export interface IncidentioFollowUpsListParams extends IncidentioBaseParams {
incident_id?: string
page_size?: number
}
export interface IncidentioFollowUp {
@@ -664,6 +662,7 @@ export interface CustomFieldsDeleteResponse extends ToolResponse {
// Users list tool types
export interface IncidentioUsersListParams extends IncidentioBaseParams {
page_size?: number
after?: string
}
export interface IncidentioUser {
@@ -676,6 +675,11 @@ export interface IncidentioUser {
export interface IncidentioUsersListResponse extends ToolResponse {
output: {
users: IncidentioUser[]
pagination_meta?: {
after: string
page_size: number
total_record_count?: number
}
}
}
@@ -786,9 +790,7 @@ export type IncidentioResponse =
| IncidentioEscalationPathsDeleteResponse
// Escalations types
export interface IncidentioEscalationsListParams extends IncidentioBaseParams {
page_size?: number
}
export interface IncidentioEscalationsListParams extends IncidentioBaseParams {}
export interface IncidentioEscalation {
id: string

View File

@@ -1,6 +1,7 @@
import type {
IncidentioUsersListParams,
IncidentioUsersListResponse,
import {
INCIDENTIO_PAGINATION_OUTPUT_PROPERTIES,
type IncidentioUsersListParams,
type IncidentioUsersListResponse,
} from '@/tools/incidentio/types'
import type { ToolConfig } from '@/tools/types'
@@ -24,15 +25,27 @@ export const usersListTool: ToolConfig<IncidentioUsersListParams, IncidentioUser
visibility: 'user-or-llm',
description: 'Number of results to return per page (e.g., 10, 25, 50). Default: 25',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor to fetch the next page of results',
},
},
request: {
url: (params) => {
const baseUrl = 'https://api.incident.io/v2/users'
const url = new URL('https://api.incident.io/v2/users')
if (params.page_size) {
return `${baseUrl}?page_size=${params.page_size}`
url.searchParams.append('page_size', params.page_size.toString())
}
return baseUrl
if (params.after) {
url.searchParams.append('after', params.after)
}
return url.toString()
},
method: 'GET',
headers: (params) => ({
@@ -53,6 +66,13 @@ export const usersListTool: ToolConfig<IncidentioUsersListParams, IncidentioUser
email: user.email,
role: user.role,
})),
pagination_meta: data.pagination_meta
? {
after: data.pagination_meta.after,
page_size: data.pagination_meta.page_size,
total_record_count: data.pagination_meta.total_record_count,
}
: undefined,
},
}
},
@@ -71,5 +91,11 @@ export const usersListTool: ToolConfig<IncidentioUsersListParams, IncidentioUser
},
},
},
pagination_meta: {
type: 'object',
description: 'Pagination metadata',
optional: true,
properties: INCIDENTIO_PAGINATION_OUTPUT_PROPERTIES,
},
},
}

View File

@@ -24,23 +24,11 @@ export const pipedriveGetActivitiesTool: ToolConfig<
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
deal_id: {
user_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter activities by deal ID (e.g., "123")',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter activities by person ID (e.g., "456")',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter activities by organization ID (e.g., "789")',
description: 'Filter activities by user ID (e.g., "123")',
},
type: {
type: 'string',
@@ -60,6 +48,12 @@ export const pipedriveGetActivitiesTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "50", default: 100, max: 500)',
},
start: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination start offset (0-based index of the first item to return)',
},
},
request: {
@@ -67,12 +61,11 @@ export const pipedriveGetActivitiesTool: ToolConfig<
const baseUrl = 'https://api.pipedrive.com/v1/activities'
const queryParams = new URLSearchParams()
if (params.deal_id) queryParams.append('deal_id', params.deal_id)
if (params.person_id) queryParams.append('person_id', params.person_id)
if (params.org_id) queryParams.append('org_id', params.org_id)
if (params.user_id) queryParams.append('user_id', params.user_id)
if (params.type) queryParams.append('type', params.type)
if (params.done) queryParams.append('done', params.done)
if (params.limit) queryParams.append('limit', params.limit)
if (params.start) queryParams.append('start', params.start)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -99,12 +92,16 @@ export const pipedriveGetActivitiesTool: ToolConfig<
}
const activities = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
const nextStart = data.additional_data?.pagination?.next_start ?? null
return {
success: true,
output: {
activities,
total_items: activities.length,
has_more: hasMore,
next_start: nextStart,
success: true,
},
}
@@ -120,6 +117,16 @@ export const pipedriveGetActivitiesTool: ToolConfig<
},
},
total_items: { type: 'number', description: 'Total number of activities returned' },
has_more: {
type: 'boolean',
description: 'Whether more activities are available',
optional: true,
},
next_start: {
type: 'number',
description: 'Offset for fetching the next page',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -67,6 +67,12 @@ export const pipedriveGetAllDealsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "50", default: 100, max: 500)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'For pagination, the marker representing the first item on the next page',
},
},
request: {
@@ -81,6 +87,7 @@ export const pipedriveGetAllDealsTool: ToolConfig<
if (params.pipeline_id) queryParams.append('pipeline_id', params.pipeline_id)
if (params.updated_since) queryParams.append('updated_since', params.updated_since)
if (params.limit) queryParams.append('limit', params.limit)
if (params.cursor) queryParams.append('cursor', params.cursor)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -107,7 +114,8 @@ export const pipedriveGetAllDealsTool: ToolConfig<
}
const deals = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
const nextCursor = data.additional_data?.next_cursor ?? null
const hasMore = nextCursor !== null
return {
success: true,
@@ -116,6 +124,7 @@ export const pipedriveGetAllDealsTool: ToolConfig<
metadata: {
total_items: deals.length,
has_more: hasMore,
next_cursor: nextCursor,
},
success: true,
},

View File

@@ -16,29 +16,23 @@ export const pipedriveGetFilesTool: ToolConfig<PipedriveGetFilesParams, Pipedriv
visibility: 'hidden',
description: 'The access token for the Pipedrive API',
},
deal_id: {
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter files by deal ID (e.g., "123")',
},
person_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter files by person ID (e.g., "456")',
},
org_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter files by organization ID (e.g., "789")',
description: 'Sort files by field (supported: "id", "update_time")',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "50", default: 100, max: 500)',
description: 'Number of results to return (e.g., "50", default: 100, max: 100)',
},
start: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination start offset (0-based index of the first item to return)',
},
downloadFiles: {
type: 'boolean',
@@ -56,10 +50,9 @@ export const pipedriveGetFilesTool: ToolConfig<PipedriveGetFilesParams, Pipedriv
}),
body: (params) => ({
accessToken: params.accessToken,
deal_id: params.deal_id,
person_id: params.person_id,
org_id: params.org_id,
sort: params.sort,
limit: params.limit,
start: params.start,
downloadFiles: params.downloadFiles,
}),
},
@@ -79,6 +72,16 @@ export const pipedriveGetFilesTool: ToolConfig<PipedriveGetFilesParams, Pipedriv
optional: true,
},
total_items: { type: 'number', description: 'Total number of files returned' },
has_more: {
type: 'boolean',
description: 'Whether more files are available',
optional: true,
},
next_start: {
type: 'number',
description: 'Offset for fetching the next page',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -60,6 +60,12 @@ export const pipedriveGetLeadsTool: ToolConfig<PipedriveGetLeadsParams, Pipedriv
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "50", default: 100, max: 500)',
},
start: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination start offset (0-based index of the first item to return)',
},
},
request: {
@@ -81,6 +87,7 @@ export const pipedriveGetLeadsTool: ToolConfig<PipedriveGetLeadsParams, Pipedriv
if (params.person_id) queryParams.append('person_id', params.person_id)
if (params.organization_id) queryParams.append('organization_id', params.organization_id)
if (params.limit) queryParams.append('limit', params.limit)
if (params.start) queryParams.append('start', params.start)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -119,12 +126,19 @@ export const pipedriveGetLeadsTool: ToolConfig<PipedriveGetLeadsParams, Pipedriv
// Otherwise, return list of leads
const leads = data.data || []
// Leads endpoint puts pagination fields directly on additional_data (no .pagination wrapper)
const hasMore = data.additional_data?.more_items_in_collection || false
const currentStart = data.additional_data?.start ?? 0
const currentLimit = data.additional_data?.limit ?? leads.length
const nextStart = hasMore ? currentStart + currentLimit : null
return {
success: true,
output: {
leads,
total_items: leads.length,
has_more: hasMore,
next_start: nextStart,
success: true,
},
}
@@ -151,6 +165,16 @@ export const pipedriveGetLeadsTool: ToolConfig<PipedriveGetLeadsParams, Pipedriv
description: 'Total number of leads returned',
optional: true,
},
has_more: {
type: 'boolean',
description: 'Whether more leads are available',
optional: true,
},
next_start: {
type: 'number',
description: 'Offset for fetching the next page',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -40,6 +40,12 @@ export const pipedriveGetMailMessagesTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "25", default: 50)',
},
start: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination start offset (0-based index of the first item to return)',
},
},
request: {
@@ -49,6 +55,7 @@ export const pipedriveGetMailMessagesTool: ToolConfig<
if (params.folder) queryParams.append('folder', params.folder)
if (params.limit) queryParams.append('limit', params.limit)
if (params.start) queryParams.append('start', params.start)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -75,12 +82,16 @@ export const pipedriveGetMailMessagesTool: ToolConfig<
}
const threads = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
const nextStart = data.additional_data?.pagination?.next_start ?? null
return {
success: true,
output: {
messages: threads,
total_items: threads.length,
has_more: hasMore,
next_start: nextStart,
success: true,
},
}
@@ -89,6 +100,16 @@ export const pipedriveGetMailMessagesTool: ToolConfig<
outputs: {
messages: { type: 'array', description: 'Array of mail thread objects from Pipedrive mailbox' },
total_items: { type: 'number', description: 'Total number of mail threads returned' },
has_more: {
type: 'boolean',
description: 'Whether more messages are available',
optional: true,
},
next_start: {
type: 'number',
description: 'Offset for fetching the next page',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -35,18 +35,18 @@ export const pipedriveGetPipelineDealsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Filter by specific stage within the pipeline (e.g., "2")',
},
status: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by deal status: open, won, lost',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "50", default: 100, max: 500)',
},
start: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination start offset (0-based index of the first item to return)',
},
},
request: {
@@ -55,8 +55,8 @@ export const pipedriveGetPipelineDealsTool: ToolConfig<
const queryParams = new URLSearchParams()
if (params.stage_id) queryParams.append('stage_id', params.stage_id)
if (params.status) queryParams.append('status', params.status)
if (params.limit) queryParams.append('limit', params.limit)
if (params.start) queryParams.append('start', params.start)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -83,6 +83,8 @@ export const pipedriveGetPipelineDealsTool: ToolConfig<
}
const deals = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
const nextStart = data.additional_data?.pagination?.next_start ?? null
return {
success: true,
@@ -91,6 +93,8 @@ export const pipedriveGetPipelineDealsTool: ToolConfig<
metadata: {
pipeline_id: params?.pipeline_id || '',
total_items: deals.length,
has_more: hasMore,
next_start: nextStart,
},
success: true,
},

View File

@@ -42,11 +42,11 @@ export const pipedriveGetPipelinesTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Number of results to return (e.g., "50", default: 100, max: 500)',
},
cursor: {
start: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'For pagination, the marker representing the first item on the next page',
description: 'Pagination start offset (0-based index of the first item to return)',
},
},
@@ -58,7 +58,7 @@ export const pipedriveGetPipelinesTool: ToolConfig<
if (params.sort_by) queryParams.append('sort_by', params.sort_by)
if (params.sort_direction) queryParams.append('sort_direction', params.sort_direction)
if (params.limit) queryParams.append('limit', params.limit)
if (params.cursor) queryParams.append('cursor', params.cursor)
if (params.start) queryParams.append('start', params.start)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -85,12 +85,16 @@ export const pipedriveGetPipelinesTool: ToolConfig<
}
const pipelines = data.data || []
const hasMore = data.additional_data?.pagination?.more_items_in_collection || false
const nextStart = data.additional_data?.pagination?.next_start ?? null
return {
success: true,
output: {
pipelines,
total_items: pipelines.length,
has_more: hasMore,
next_start: nextStart,
success: true,
},
}
@@ -106,6 +110,16 @@ export const pipedriveGetPipelinesTool: ToolConfig<
},
},
total_items: { type: 'number', description: 'Total number of pipelines returned' },
has_more: {
type: 'boolean',
description: 'Whether more pipelines are available',
optional: true,
},
next_start: {
type: 'number',
description: 'Offset for fetching the next page',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -42,6 +42,12 @@ export const pipedriveGetProjectsTool: ToolConfig<
description:
'Number of results to return (e.g., "50", default: 100, max: 500, only for listing all)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'For pagination, the marker representing the first item on the next page',
},
},
request: {
@@ -57,6 +63,7 @@ export const pipedriveGetProjectsTool: ToolConfig<
if (params.status) queryParams.append('status', params.status)
if (params.limit) queryParams.append('limit', params.limit)
if (params.cursor) queryParams.append('cursor', params.cursor)
const queryString = queryParams.toString()
return queryString ? `${baseUrl}?${queryString}` : baseUrl
@@ -95,12 +102,16 @@ export const pipedriveGetProjectsTool: ToolConfig<
// Otherwise, return list of projects
const projects = data.data || []
const nextCursor = data.additional_data?.next_cursor ?? null
const hasMore = nextCursor !== null
return {
success: true,
output: {
projects,
total_items: projects.length,
has_more: hasMore,
next_cursor: nextCursor,
success: true,
},
}
@@ -122,6 +133,16 @@ export const pipedriveGetProjectsTool: ToolConfig<
description: 'Total number of projects returned',
optional: true,
},
has_more: {
type: 'boolean',
description: 'Whether more projects are available',
optional: true,
},
next_cursor: {
type: 'string',
description: 'Cursor for fetching the next page',
optional: true,
},
success: { type: 'boolean', description: 'Operation success status' },
},
}

View File

@@ -239,6 +239,16 @@ export const PIPEDRIVE_MAIL_MESSAGE_OUTPUT: OutputProperty = {
export const PIPEDRIVE_METADATA_OUTPUT_PROPERTIES = {
total_items: { type: 'number', description: 'Total number of items' },
has_more: { type: 'boolean', description: 'Whether more items are available', optional: true },
next_cursor: {
type: 'string',
description: 'Cursor for fetching the next page (v2 endpoints)',
optional: true,
},
next_start: {
type: 'number',
description: 'Offset for fetching the next page (v1 endpoints)',
optional: true,
},
} as const satisfies Record<string, OutputProperty>
// Common Pipedrive types
@@ -355,6 +365,7 @@ export interface PipedriveGetAllDealsParams {
pipeline_id?: string
updated_since?: string
limit?: string
cursor?: string
}
export interface PipedriveGetAllDealsOutput {
@@ -362,6 +373,7 @@ export interface PipedriveGetAllDealsOutput {
metadata: {
total_items: number
has_more: boolean
next_cursor?: string
}
success: boolean
}
@@ -431,10 +443,9 @@ export interface PipedriveUpdateDealResponse extends ToolResponse {
// GET Files
export interface PipedriveGetFilesParams {
accessToken: string
deal_id?: string
person_id?: string
org_id?: string
sort?: string
limit?: string
start?: string
downloadFiles?: boolean
}
@@ -442,6 +453,8 @@ export interface PipedriveGetFilesOutput {
files: PipedriveFile[]
downloadedFiles?: ToolFileData[]
total_items: number
has_more?: boolean
next_start?: number
success: boolean
}
@@ -453,11 +466,14 @@ export interface PipedriveGetMailMessagesParams {
accessToken: string
folder?: string
limit?: string
start?: string
}
export interface PipedriveGetMailMessagesOutput {
messages: PipedriveMailMessage[]
total_items: number
has_more?: boolean
next_start?: number
success: boolean
}
@@ -490,12 +506,14 @@ export interface PipedriveGetPipelinesParams {
sort_by?: string
sort_direction?: string
limit?: string
cursor?: string
start?: string
}
export interface PipedriveGetPipelinesOutput {
pipelines: PipedrivePipeline[]
total_items: number
has_more?: boolean
next_start?: number
success: boolean
}
@@ -508,8 +526,8 @@ export interface PipedriveGetPipelineDealsParams {
accessToken: string
pipeline_id: string
stage_id?: string
status?: string
limit?: string
start?: string
}
export interface PipedriveGetPipelineDealsOutput {
@@ -517,6 +535,8 @@ export interface PipedriveGetPipelineDealsOutput {
metadata: {
pipeline_id: string
total_items: number
has_more?: boolean
next_start?: number
}
success: boolean
}
@@ -531,12 +551,15 @@ export interface PipedriveGetProjectsParams {
project_id?: string
status?: string
limit?: string
cursor?: string
}
export interface PipedriveGetProjectsOutput {
projects?: PipedriveProject[]
project?: PipedriveProject
total_items?: number
has_more?: boolean
next_cursor?: string
success: boolean
}
@@ -565,17 +588,18 @@ export interface PipedriveCreateProjectResponse extends ToolResponse {
// GET All Activities
export interface PipedriveGetActivitiesParams {
accessToken: string
deal_id?: string
person_id?: string
org_id?: string
user_id?: string
type?: string
done?: string
limit?: string
start?: string
}
export interface PipedriveGetActivitiesOutput {
activities: PipedriveActivity[]
total_items: number
has_more?: boolean
next_start?: number
success: boolean
}
@@ -636,12 +660,15 @@ export interface PipedriveGetLeadsParams {
person_id?: string
organization_id?: string
limit?: string
start?: string
}
export interface PipedriveGetLeadsOutput {
leads?: PipedriveLead[]
lead?: PipedriveLead
total_items?: number
has_more?: boolean
next_start?: number
success: boolean
}

View File

@@ -51,6 +51,12 @@ export const queryTool: ToolConfig<SupabaseQueryParams, SupabaseQueryResponse> =
visibility: 'user-or-llm',
description: 'Maximum number of rows to return',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of rows to skip (for pagination)',
},
apiKey: {
type: 'string',
required: true,
@@ -91,10 +97,15 @@ export const queryTool: ToolConfig<SupabaseQueryParams, SupabaseQueryResponse> =
}
// Add limit if provided
if (params.limit) {
if (params.limit !== undefined && params.limit !== null) {
url += `&limit=${Number(params.limit)}`
}
// Add offset if provided
if (params.offset !== undefined && params.offset !== null) {
url += `&offset=${Number(params.offset)}`
}
return url
},
method: 'GET',

View File

@@ -57,6 +57,12 @@ export const textSearchTool: ToolConfig<SupabaseTextSearchParams, SupabaseTextSe
visibility: 'user-or-llm',
description: 'Maximum number of rows to return',
},
offset: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of rows to skip (for pagination)',
},
apiKey: {
type: 'string',
required: true,
@@ -74,8 +80,9 @@ export const textSearchTool: ToolConfig<SupabaseTextSearchParams, SupabaseTextSe
let url = `https://${params.projectId}.supabase.co/rest/v1/${params.table}?select=*`
// Map search types to PostgREST operators
// plfts = plainto_tsquery (natural language), phfts = phraseto_tsquery, wfts = websearch_to_tsquery
const operatorMap: Record<string, string> = {
plain: 'fts',
plain: 'plfts',
phrase: 'phfts',
websearch: 'wfts',
}
@@ -86,10 +93,15 @@ export const textSearchTool: ToolConfig<SupabaseTextSearchParams, SupabaseTextSe
url += `&${params.column}=${operator}(${language}).${encodeURIComponent(params.query)}`
// Add limit if provided
if (params.limit) {
if (params.limit !== undefined && params.limit !== null) {
url += `&limit=${Number(params.limit)}`
}
// Add offset if provided
if (params.offset !== undefined && params.offset !== null) {
url += `&offset=${Number(params.offset)}`
}
return url
},
method: 'GET',

View File

@@ -315,6 +315,7 @@ export interface SupabaseQueryParams {
filter?: string
orderBy?: string
limit?: number
offset?: number
}
export interface SupabaseInsertParams {
@@ -413,6 +414,7 @@ export interface SupabaseTextSearchParams {
searchType?: string
language?: string
limit?: number
offset?: number
}
export interface SupabaseTextSearchResponse extends SupabaseBaseResponse {}

View File

@@ -26,6 +26,18 @@ export const responsesTool: ToolConfig<TypeformResponsesParams, TypeformResponse
visibility: 'user-or-llm',
description: 'Number of responses to retrieve (e.g., 10, 25, 50)',
},
before: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Cursor token for fetching the next page of older responses',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Cursor token for fetching the next page of newer responses',
},
since: {
type: 'string',
required: false,
@@ -56,6 +68,14 @@ export const responsesTool: ToolConfig<TypeformResponsesParams, TypeformResponse
queryParams.push(`page_size=${Number(params.pageSize)}`)
}
if (params.before) {
queryParams.push(`before=${encodeURIComponent(params.before)}`)
}
if (params.after) {
queryParams.push(`after=${encodeURIComponent(params.after)}`)
}
if (params.since) {
queryParams.push(`since=${encodeURIComponent(params.since)}`)
}

View File

@@ -62,6 +62,8 @@ export interface TypeformResponsesParams {
formId: string
apiKey: string
pageSize?: number
before?: string
after?: string
since?: string
until?: string
completed?: string

View File

@@ -21,9 +21,9 @@ export interface ZendeskAutocompleteOrganizationsResponse {
output: {
organizations: any[]
paging?: {
after_cursor: string | null
has_more: boolean
next_page?: string | null
previous_page?: string | null
count: number
}
metadata: {
total_returned: number
@@ -78,7 +78,7 @@ export const zendeskAutocompleteOrganizationsTool: ToolConfig<
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number as a string (e.g., "1", "2")',
description: 'Page number for pagination (1-based)',
},
},
@@ -86,8 +86,8 @@ export const zendeskAutocompleteOrganizationsTool: ToolConfig<
url: (params) => {
const queryParams = new URLSearchParams()
queryParams.append('name', params.name)
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
if (params.page) queryParams.append('page', params.page)
const query = queryParams.toString()
const url = buildZendeskUrl(params.subdomain, '/organizations/autocomplete')
@@ -112,19 +112,22 @@ export const zendeskAutocompleteOrganizationsTool: ToolConfig<
const data = await response.json()
const organizations = data.organizations || []
const hasMore = data.next_page !== null && data.next_page !== undefined
return {
success: true,
output: {
organizations,
// /organizations/autocomplete uses offset pagination (page/per_page), not cursor pagination.
// after_cursor is always null; use next_page URL or page param for subsequent pages.
paging: {
after_cursor: null,
has_more: hasMore,
next_page: data.next_page ?? null,
previous_page: data.previous_page ?? null,
count: data.count || organizations.length,
},
metadata: {
total_returned: organizations.length,
has_more: !!data.next_page,
has_more: hasMore,
},
success: true,
},

View File

@@ -1,6 +1,8 @@
import type { ToolConfig } from '@/tools/types'
import {
appendCursorPaginationParams,
buildZendeskUrl,
extractCursorPagingInfo,
handleZendeskError,
METADATA_OUTPUT,
ORGANIZATIONS_ARRAY_OUTPUT,
@@ -12,7 +14,7 @@ export interface ZendeskGetOrganizationsParams {
apiToken: string
subdomain: string
perPage?: string
page?: string
pageAfter?: string
}
export interface ZendeskGetOrganizationsResponse {
@@ -20,9 +22,8 @@ export interface ZendeskGetOrganizationsResponse {
output: {
organizations: any[]
paging?: {
next_page?: string | null
previous_page?: string | null
count: number
after_cursor: string | null
has_more: boolean
}
metadata: {
total_returned: number
@@ -66,19 +67,18 @@ export const zendeskGetOrganizationsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Results per page as a number string (default: "100", max: "100")',
},
page: {
pageAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number as a string (e.g., "1", "2")',
description: 'Cursor from a previous response to fetch the next page of results',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams()
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
appendCursorPaginationParams(queryParams, params)
const query = queryParams.toString()
const url = buildZendeskUrl(params.subdomain, '/organizations')
@@ -103,19 +103,16 @@ export const zendeskGetOrganizationsTool: ToolConfig<
const data = await response.json()
const organizations = data.organizations || []
const paging = extractCursorPagingInfo(data)
return {
success: true,
output: {
organizations,
paging: {
next_page: data.next_page ?? null,
previous_page: data.previous_page ?? null,
count: data.count || organizations.length,
},
paging,
metadata: {
total_returned: organizations.length,
has_more: !!data.next_page,
has_more: paging.has_more,
},
success: true,
},

View File

@@ -1,6 +1,8 @@
import type { ToolConfig } from '@/tools/types'
import {
appendCursorPaginationParams,
buildZendeskUrl,
extractCursorPagingInfo,
handleZendeskError,
METADATA_OUTPUT,
PAGING_OUTPUT,
@@ -16,10 +18,9 @@ export interface ZendeskGetTicketsParams {
type?: string
assigneeId?: string
organizationId?: string
sortBy?: string
sortOrder?: string
sort?: string
perPage?: string
page?: string
pageAfter?: string
}
export interface ZendeskGetTicketsResponse {
@@ -27,9 +28,8 @@ export interface ZendeskGetTicketsResponse {
output: {
tickets: any[]
paging?: {
next_page?: string | null
previous_page?: string | null
count: number
after_cursor: string | null
has_more: boolean
}
metadata: {
total_returned: number
@@ -95,17 +95,12 @@ export const zendeskGetTicketsTool: ToolConfig<ZendeskGetTicketsParams, ZendeskG
visibility: 'user-or-llm',
description: 'Filter by organization ID as a numeric string (e.g., "67890")',
},
sortBy: {
sort: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort field: "created_at", "updated_at", "priority", or "status"',
},
sortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: "asc" or "desc"',
description:
'Sort field for ticket listing (only applies without filters): "updated_at", "id", or "status". Prefix with "-" for descending (e.g., "-updated_at")',
},
perPage: {
type: 'string',
@@ -113,11 +108,11 @@ export const zendeskGetTicketsTool: ToolConfig<ZendeskGetTicketsParams, ZendeskG
visibility: 'user-or-llm',
description: 'Results per page as a number string (default: "100", max: "100")',
},
page: {
pageAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number as a string (e.g., "1", "2")',
description: 'Cursor from a previous response to fetch the next page of results',
},
},
@@ -142,20 +137,16 @@ export const zendeskGetTicketsTool: ToolConfig<ZendeskGetTicketsParams, ZendeskG
const queryParams = new URLSearchParams()
queryParams.append('query', searchTerms.join(' '))
if (params.sortBy) queryParams.append('sort_by', params.sortBy)
if (params.sortOrder) queryParams.append('sort_order', params.sortOrder)
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
queryParams.append('filter[type]', 'ticket')
appendCursorPaginationParams(queryParams, params)
return `${buildZendeskUrl(params.subdomain, '/search')}?${queryParams.toString()}`
return `${buildZendeskUrl(params.subdomain, '/search/export')}?${queryParams.toString()}`
}
// No filters - use the simple /tickets endpoint
// No filters - use the simple /tickets endpoint with cursor-based pagination
const queryParams = new URLSearchParams()
if (params.sortBy) queryParams.append('sort_by', params.sortBy)
if (params.sortOrder) queryParams.append('sort_order', params.sortOrder)
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
if (params.sort) queryParams.append('sort', params.sort)
appendCursorPaginationParams(queryParams, params)
const query = queryParams.toString()
const url = buildZendeskUrl(params.subdomain, '/tickets')
@@ -182,19 +173,16 @@ export const zendeskGetTicketsTool: ToolConfig<ZendeskGetTicketsParams, ZendeskG
const data = await response.json()
// Handle both /tickets response (data.tickets) and /search response (data.results)
const tickets = data.tickets || data.results || []
const paging = extractCursorPagingInfo(data)
return {
success: true,
output: {
tickets,
paging: {
next_page: data.next_page ?? null,
previous_page: data.previous_page ?? null,
count: data.count || tickets.length,
},
paging,
metadata: {
total_returned: tickets.length,
has_more: !!data.next_page,
has_more: paging.has_more,
},
success: true,
},

View File

@@ -1,6 +1,8 @@
import type { ToolConfig } from '@/tools/types'
import {
appendCursorPaginationParams,
buildZendeskUrl,
extractCursorPagingInfo,
handleZendeskError,
METADATA_OUTPUT,
PAGING_OUTPUT,
@@ -14,7 +16,7 @@ export interface ZendeskGetUsersParams {
role?: string
permissionSet?: string
perPage?: string
page?: string
pageAfter?: string
}
export interface ZendeskGetUsersResponse {
@@ -22,9 +24,8 @@ export interface ZendeskGetUsersResponse {
output: {
users: any[]
paging?: {
next_page?: string | null
previous_page?: string | null
count: number
after_cursor: string | null
has_more: boolean
}
metadata: {
total_returned: number
@@ -77,11 +78,11 @@ export const zendeskGetUsersTool: ToolConfig<ZendeskGetUsersParams, ZendeskGetUs
visibility: 'user-or-llm',
description: 'Results per page as a number string (default: "100", max: "100")',
},
page: {
pageAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number as a string (e.g., "1", "2")',
description: 'Cursor from a previous response to fetch the next page of results',
},
},
@@ -90,8 +91,7 @@ export const zendeskGetUsersTool: ToolConfig<ZendeskGetUsersParams, ZendeskGetUs
const queryParams = new URLSearchParams()
if (params.role) queryParams.append('role', params.role)
if (params.permissionSet) queryParams.append('permission_set', params.permissionSet)
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
appendCursorPaginationParams(queryParams, params)
const query = queryParams.toString()
const url = buildZendeskUrl(params.subdomain, '/users')
@@ -116,19 +116,16 @@ export const zendeskGetUsersTool: ToolConfig<ZendeskGetUsersParams, ZendeskGetUs
const data = await response.json()
const users = data.users || []
const paging = extractCursorPagingInfo(data)
return {
success: true,
output: {
users,
paging: {
next_page: data.next_page ?? null,
previous_page: data.previous_page ?? null,
count: data.count || users.length,
},
paging,
metadata: {
total_returned: users.length,
has_more: !!data.next_page,
has_more: paging.has_more,
},
success: true,
},

View File

@@ -1,6 +1,8 @@
import type { ToolConfig } from '@/tools/types'
import {
appendCursorPaginationParams,
buildZendeskUrl,
extractCursorPagingInfo,
handleZendeskError,
METADATA_OUTPUT,
PAGING_OUTPUT,
@@ -11,10 +13,9 @@ export interface ZendeskSearchParams {
apiToken: string
subdomain: string
query: string
sortBy?: string
sortOrder?: string
filterType: string
perPage?: string
page?: string
pageAfter?: string
}
export interface ZendeskSearchResponse {
@@ -22,9 +23,8 @@ export interface ZendeskSearchResponse {
output: {
results: any[]
paging?: {
next_page?: string | null
previous_page?: string | null
count: number
after_cursor: string | null
has_more: boolean
}
metadata: {
total_returned: number
@@ -66,18 +66,11 @@ export const zendeskSearchTool: ToolConfig<ZendeskSearchParams, ZendeskSearchRes
description:
'Search query string using Zendesk search syntax (e.g., "type:ticket status:open")',
},
sortBy: {
filterType: {
type: 'string',
required: false,
required: true,
visibility: 'user-or-llm',
description:
'Sort field: "relevance", "created_at", "updated_at", "priority", "status", or "ticket_type"',
},
sortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: "asc" or "desc"',
description: 'Resource type to search for: "ticket", "user", "organization", or "group"',
},
perPage: {
type: 'string',
@@ -85,11 +78,11 @@ export const zendeskSearchTool: ToolConfig<ZendeskSearchParams, ZendeskSearchRes
visibility: 'user-or-llm',
description: 'Results per page as a number string (default: "100", max: "100")',
},
page: {
pageAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number as a string (e.g., "1", "2")',
description: 'Cursor from a previous response to fetch the next page of results',
},
},
@@ -97,13 +90,11 @@ export const zendeskSearchTool: ToolConfig<ZendeskSearchParams, ZendeskSearchRes
url: (params) => {
const queryParams = new URLSearchParams()
queryParams.append('query', params.query)
if (params.sortBy) queryParams.append('sort_by', params.sortBy)
if (params.sortOrder) queryParams.append('sort_order', params.sortOrder)
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
queryParams.append('filter[type]', params.filterType)
appendCursorPaginationParams(queryParams, params)
const query = queryParams.toString()
const url = buildZendeskUrl(params.subdomain, '/search')
const url = buildZendeskUrl(params.subdomain, '/search/export')
return `${url}?${query}`
},
method: 'GET',
@@ -125,19 +116,16 @@ export const zendeskSearchTool: ToolConfig<ZendeskSearchParams, ZendeskSearchRes
const data = await response.json()
const results = data.results || []
const paging = extractCursorPagingInfo(data)
return {
success: true,
output: {
results,
paging: {
next_page: data.next_page ?? null,
previous_page: data.previous_page ?? null,
count: data.count || results.length,
},
paging,
metadata: {
total_returned: results.length,
has_more: !!data.next_page,
has_more: paging.has_more,
},
success: true,
},

View File

@@ -22,9 +22,9 @@ export interface ZendeskSearchUsersResponse {
output: {
users: any[]
paging?: {
after_cursor: string | null
has_more: boolean
next_page?: string | null
previous_page?: string | null
count: number
}
metadata: {
total_returned: number
@@ -84,7 +84,7 @@ export const zendeskSearchUsersTool: ToolConfig<
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page number as a string (e.g., "1", "2")',
description: 'Page number for pagination (1-based)',
},
},
@@ -93,8 +93,8 @@ export const zendeskSearchUsersTool: ToolConfig<
const queryParams = new URLSearchParams()
if (params.query) queryParams.append('query', params.query)
if (params.externalId) queryParams.append('external_id', params.externalId)
if (params.page) queryParams.append('page', params.page)
if (params.perPage) queryParams.append('per_page', params.perPage)
if (params.page) queryParams.append('page', params.page)
const query = queryParams.toString()
const url = buildZendeskUrl(params.subdomain, '/users/search')
@@ -119,19 +119,22 @@ export const zendeskSearchUsersTool: ToolConfig<
const data = await response.json()
const users = data.users || []
const hasMore = data.next_page !== null && data.next_page !== undefined
return {
success: true,
output: {
users,
// /users/search uses offset pagination (page/per_page), not cursor pagination.
// after_cursor is always null; use next_page URL or page param for subsequent pages.
paging: {
after_cursor: null,
has_more: hasMore,
next_page: data.next_page ?? null,
previous_page: data.previous_page ?? null,
count: data.count || users.length,
},
metadata: {
total_returned: users.length,
has_more: !!data.next_page,
has_more: hasMore,
},
success: true,
},

View File

@@ -11,14 +11,14 @@ export interface ZendeskBaseParams {
}
export interface ZendeskPaginationParams {
page?: string
perPage?: string
pageAfter?: string
}
export interface ZendeskPagingInfo {
after_cursor: string | null
has_more: boolean
next_page?: string | null
previous_page?: string | null
count: number
}
export interface ZendeskListMetadata {
@@ -50,6 +50,32 @@ export function handleZendeskError(data: any, status: number, operation: string)
throw new Error(`Zendesk ${operation} failed: ${errorMessage}`)
}
/**
* Appends cursor-based pagination query params.
* Zendesk uses bracket notation: `page[size]` and `page[after]`.
*/
export function appendCursorPaginationParams(
queryParams: URLSearchParams,
params: ZendeskPaginationParams
): void {
if (params.perPage) queryParams.append('page[size]', params.perPage)
if (params.pageAfter) queryParams.append('page[after]', params.pageAfter)
}
/**
* Extracts cursor-based pagination info from Zendesk API response.
* Zendesk cursor-based responses include `meta.after_cursor`, `meta.has_more`, and `links.next`.
*/
export function extractCursorPagingInfo(data: Record<string, unknown>): ZendeskPagingInfo {
const meta = (data.meta as Record<string, unknown>) || {}
const links = (data.links as Record<string, unknown>) || {}
return {
after_cursor: (meta.after_cursor as string) ?? null,
has_more: Boolean(meta.has_more),
next_page: (links.next as string) ?? null,
}
}
/**
* Output definition for the "via" object in ticket responses.
* Contains information about how the ticket was created.
@@ -377,13 +403,13 @@ export const ORGANIZATION_OUTPUT_PROPERTIES = {
* Pagination output properties for list endpoints
*/
export const PAGING_OUTPUT_PROPERTIES = {
next_page: { type: 'string', description: 'URL for next page of results', optional: true },
previous_page: {
after_cursor: {
type: 'string',
description: 'URL for previous page of results',
description: 'Cursor for fetching the next page of results',
optional: true,
},
count: { type: 'number', description: 'Total count of items' },
has_more: { type: 'boolean', description: 'Whether more results are available' },
next_page: { type: 'string', description: 'URL for next page of results', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
@@ -391,7 +417,7 @@ export const PAGING_OUTPUT_PROPERTIES = {
*/
export const PAGING_OUTPUT: OutputProperty = {
type: 'object',
description: 'Pagination information',
description: 'Cursor-based pagination information',
properties: PAGING_OUTPUT_PROPERTIES,
}

View File

@@ -13,7 +13,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.3",
"turbo": "2.8.9",
},
},
"apps/docs": {
@@ -3437,19 +3437,19 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="],
"turbo": ["turbo@2.8.9", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.9", "turbo-darwin-arm64": "2.8.9", "turbo-linux-64": "2.8.9", "turbo-linux-arm64": "2.8.9", "turbo-windows-64": "2.8.9", "turbo-windows-arm64": "2.8.9" }, "bin": { "turbo": "bin/turbo" } }, "sha512-G+Mq8VVQAlpz/0HTsxiNNk/xywaHGl+dk1oiBREgOEVCCDjXInDlONWUn5srRnC9s5tdHTFD1bx1N19eR4hI+g=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-KnCw1ZI9KTnEAhdI9avZrnZ/z4wsM++flMA1w8s8PKOqi5daGpFV36qoPafg4S8TmYMe52JPWEoFr0L+lQ5JIw=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xF7uCeC0UY0Hrv/tqax0BMbFlVP1J/aRyeGQPZT4NjvIPj8gSPDgFhfkfz06DhUwDg5NgMo04uiSkAWE8WB/QQ=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CbD5Y2NKJKBXTOZ7z7Cc7vGlFPZkYjApA7ri9lH4iFwKV1X7MoZswh9gyRLetXYWImVX1BqIvP8KftulJg/wIA=="],
"turbo-linux-64": ["turbo-linux-64@2.8.3", "", { "os": "linux", "cpu": "x64" }, "sha512-vxMDXwaOjweW/4etY7BxrXCSkvtwh0PbwVafyfT1Ww659SedUxd5rM3V2ZCmbwG8NiCfY7d6VtxyHx3Wh1GoZA=="],
"turbo-linux-64": ["turbo-linux-64@2.8.9", "", { "os": "linux", "cpu": "x64" }, "sha512-OXC9HdCtsHvyH+5KUoH8ds+p5WU13vdif0OPbsFzZca4cUXMwKA3HWwUuCgQetk0iAE4cscXpi/t8A263n3VTg=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-mQX7uYBZFkuPLLlKaNe9IjR1JIef4YvY8f21xFocvttXvdPebnq3PK1Zjzl9A1zun2BEuWNUwQIL8lgvN9Pm3Q=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-yI5n8jNXiFA6+CxnXG0gO7h5ZF1+19K8uO3/kXPQmyl37AdiA7ehKJQOvf9OPAnmkGDHcF2HSCPltabERNRmug=="],
"turbo-windows-64": ["turbo-windows-64@2.8.3", "", { "os": "win32", "cpu": "x64" }, "sha512-YLGEfppGxZj3VWcNOVa08h6ISsVKiG85aCAWosOKNUjb6yErWEuydv6/qImRJUI+tDLvDvW7BxopAkujRnWCrw=="],
"turbo-windows-64": ["turbo-windows-64@2.8.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/OztzeGftJAg258M/9vK2ZCkUKUzqrWXJIikiD2pm8TlqHcIYUmepDbyZSDfOiUjMy6NzrLFahpNLnY7b5vNgg=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-xZ2VTwVTjIqpFZKN4UBxDHCPM3oJ2J5cpRzCBSmRpJ/Pn33wpiYjs+9FB2E03svKaD04/lSSLlEUej0UYsugfg=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],

View File

@@ -42,7 +42,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.3"
"turbo": "2.8.9"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [