mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(calendly): extend tools with booking, availability, no-shows, and routing forms (#6545)
* feat(calendly): extend tools with booking, availability, no-shows, and routing forms Adds 12 tools verified against the Calendly OpenAPI spec: get_user, get_event_invitee, create_event_invitee, list_event_type_available_times, list_user_busy_times, list_user_availability_schedules, create_scheduling_link, create/delete_invitee_no_show, list_organization_memberships, list_routing_forms, and list_routing_form_submissions. Also fixes issues found while validating the existing tools: - list_webhooks dropped the scope query param the API requires, so every call with scope unset returned 400 - list_event_types could only send active=true, making inactive event types unlistable - user and organization filters now accept a bare UUID or a full URI consistently across every operation - json array params (eventGuests, events) are normalized whether they arrive as an array or a JSON string * improvement(calendly): type the block/tool alignment test instead of using any * fix(calendly): normalize webhook organization and user identifiers
This commit is contained in:
@@ -58,6 +58,36 @@ Get information about the currently authenticated Calendly user
|
||||
| ↳ `updated_at` | string | ISO timestamp when user was last updated |
|
||||
| ↳ `current_organization` | string | URI of current organization |
|
||||
|
||||
### Calendly Get User
|
||||
|
||||
Get information about a specific Calendly user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `userUuid` | string | Yes | User UUID. Format: UUID \(e.g., "abc123-def456"\), full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\), or the constant "me" for the authenticated user |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | User information |
|
||||
| ↳ `uri` | string | Canonical reference to the user |
|
||||
| ↳ `name` | string | User full name |
|
||||
| ↳ `slug` | string | Unique identifier for the user in URLs |
|
||||
| ↳ `email` | string | User email address |
|
||||
| ↳ `scheduling_url` | string | URL to the user's scheduling page |
|
||||
| ↳ `timezone` | string | User timezone |
|
||||
| ↳ `time_notation` | string | Time notation preference \(12h or 24h\) |
|
||||
| ↳ `avatar_url` | string | URL to user avatar image |
|
||||
| ↳ `created_at` | string | ISO timestamp when user was created |
|
||||
| ↳ `updated_at` | string | ISO timestamp when user was last updated |
|
||||
| ↳ `current_organization` | string | URI of current organization |
|
||||
| ↳ `resource_type` | string | Resource type |
|
||||
| ↳ `locale` | string | User locale |
|
||||
|
||||
### Calendly List Event Types
|
||||
|
||||
Retrieve a list of all event types for a user or organization
|
||||
@@ -136,6 +166,29 @@ Get detailed information about a specific event type
|
||||
| ↳ `type` | string | Event type classification |
|
||||
| ↳ `updated_at` | string | ISO timestamp of last update |
|
||||
|
||||
### Calendly List Event Type Available Times
|
||||
|
||||
Retrieve bookable time slots for an event type within a date range
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventTypeUri` | string | Yes | Event type to check. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456"\) |
|
||||
| `startTime` | string | Yes | Start of the availability range. Cannot be in the past. Format: ISO 8601 \(e.g., "2024-01-01T00:00:00Z"\) |
|
||||
| `endTime` | string | Yes | End of the availability range. Must be in the future and no more than 31 days after the start. Format: ISO 8601 \(e.g., "2024-01-15T00:00:00Z"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of available time slots |
|
||||
| ↳ `status` | string | Availability status of the slot |
|
||||
| ↳ `invitees_remaining` | number | Number of invitees that can still book this slot |
|
||||
| ↳ `start_time` | string | ISO timestamp of the slot start |
|
||||
| ↳ `scheduling_url` | string | URL that books this exact slot |
|
||||
|
||||
### Calendly List Scheduled Events
|
||||
|
||||
Retrieve a list of scheduled events for a user or organization
|
||||
@@ -269,6 +322,108 @@ Retrieve a list of invitees for a scheduled event
|
||||
| ↳ `next_page_token` | string | Token for next page |
|
||||
| ↳ `previous_page_token` | string | Token for previous page |
|
||||
|
||||
### Calendly Get Event Invitee
|
||||
|
||||
Retrieve a single invitee of a scheduled event, including their intake answers
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventUuid` | string | Yes | Scheduled event UUID. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/scheduled_events/abc123-def456"\) |
|
||||
| `inviteeUuid` | string | Yes | Invitee UUID. Format: UUID \(e.g., "abc123-def456"\) or full invitee URI \(e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Invitee details |
|
||||
| ↳ `uri` | string | Canonical reference to the invitee |
|
||||
| ↳ `email` | string | Invitee email address |
|
||||
| ↳ `name` | string | Invitee full name |
|
||||
| ↳ `first_name` | string | Invitee first name |
|
||||
| ↳ `last_name` | string | Invitee last name |
|
||||
| ↳ `status` | string | Invitee status \(active or canceled\) |
|
||||
| ↳ `timezone` | string | Invitee timezone |
|
||||
| ↳ `event` | string | URI of the scheduled event |
|
||||
| ↳ `created_at` | string | ISO timestamp when invitee was created |
|
||||
| ↳ `updated_at` | string | ISO timestamp when invitee was updated |
|
||||
| ↳ `cancel_url` | string | URL to cancel the booking |
|
||||
| ↳ `reschedule_url` | string | URL to reschedule the booking |
|
||||
| ↳ `rescheduled` | boolean | Whether the invitee rescheduled |
|
||||
| ↳ `text_reminder_number` | string | Phone number used for SMS reminders |
|
||||
| ↳ `routing_form_submission` | string | URI of the routing form submission that produced this booking |
|
||||
| ↳ `questions_and_answers` | array | Responses to custom questions |
|
||||
| ↳ `question` | string | Question text |
|
||||
| ↳ `answer` | string | Invitee answer |
|
||||
| ↳ `position` | number | Question order |
|
||||
| ↳ `tracking` | object | UTM and Salesforce tracking parameters captured at booking |
|
||||
| ↳ `utm_campaign` | string | UTM campaign |
|
||||
| ↳ `utm_source` | string | UTM source |
|
||||
| ↳ `utm_medium` | string | UTM medium |
|
||||
| ↳ `utm_content` | string | UTM content |
|
||||
| ↳ `utm_term` | string | UTM term |
|
||||
| ↳ `salesforce_uuid` | string | Salesforce record identifier |
|
||||
| ↳ `cancellation` | object | Cancellation details when the invitee has canceled |
|
||||
| ↳ `canceled_by` | string | Name of person who canceled |
|
||||
| ↳ `reason` | string | Cancellation reason |
|
||||
| ↳ `canceler_type` | string | Type of canceler \(host or invitee\) |
|
||||
| ↳ `created_at` | string | ISO timestamp of the cancellation |
|
||||
| ↳ `no_show` | object | No-show record when the invitee has been marked as a no-show |
|
||||
| ↳ `uri` | string | Canonical reference to the no-show |
|
||||
| ↳ `created_at` | string | ISO timestamp when marked as no-show |
|
||||
| ↳ `payment` | object | Payment collected at booking |
|
||||
| ↳ `external_id` | string | Payment identifier at the provider |
|
||||
| ↳ `provider` | string | Payment provider |
|
||||
| ↳ `amount` | number | Amount charged |
|
||||
| ↳ `currency` | string | Currency code |
|
||||
| ↳ `terms` | string | Payment terms |
|
||||
| ↳ `successful` | boolean | Whether the payment succeeded |
|
||||
|
||||
### Calendly Book Meeting
|
||||
|
||||
Book a meeting by creating an invitee on an event type at a chosen time. Requires a paid Calendly plan
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventTypeUri` | string | Yes | Event type to book. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456"\) |
|
||||
| `startTime` | string | Yes | Start time of the booking in UTC. Must be an available slot. Format: ISO 8601 \(e.g., "2024-01-15T09:00:00Z"\) |
|
||||
| `inviteeEmail` | string | Yes | Email address of the invitee being booked |
|
||||
| `inviteeTimezone` | string | Yes | Invitee timezone. Format: IANA timezone \(e.g., "America/New_York"\) |
|
||||
| `inviteeName` | string | No | Full name of the invitee. Required when a first name is not provided |
|
||||
| `inviteeFirstName` | string | No | First name of the invitee. Required when a full name is not provided |
|
||||
| `inviteeLastName` | string | No | Last name of the invitee |
|
||||
| `textReminderNumber` | string | No | Phone number for SMS reminders. Format: E.164 phone number \(e.g., "+14155551234"\) |
|
||||
| `eventGuests` | json | No | Additional guests to copy on the invite. Format: array of email strings \(max 10, e.g., \["guest@example.com"\]\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | The invitee created for the booking |
|
||||
| ↳ `uri` | string | Canonical reference to the invitee |
|
||||
| ↳ `email` | string | Invitee email address |
|
||||
| ↳ `name` | string | Invitee full name |
|
||||
| ↳ `first_name` | string | Invitee first name |
|
||||
| ↳ `last_name` | string | Invitee last name |
|
||||
| ↳ `status` | string | Invitee status \(active or canceled\) |
|
||||
| ↳ `timezone` | string | Invitee timezone |
|
||||
| ↳ `event` | string | URI of the scheduled event that was booked |
|
||||
| ↳ `created_at` | string | ISO timestamp when the booking was created |
|
||||
| ↳ `updated_at` | string | ISO timestamp when the booking was updated |
|
||||
| ↳ `cancel_url` | string | URL to cancel the booking |
|
||||
| ↳ `reschedule_url` | string | URL to reschedule the booking |
|
||||
| ↳ `rescheduled` | boolean | Whether the invitee rescheduled |
|
||||
| ↳ `text_reminder_number` | string | Phone number used for SMS reminders |
|
||||
| ↳ `questions_and_answers` | array | Responses to custom questions |
|
||||
| ↳ `question` | string | Question text |
|
||||
| ↳ `answer` | string | Invitee answer |
|
||||
| ↳ `position` | number | Question order |
|
||||
|
||||
### Calendly Cancel Event
|
||||
|
||||
Cancel a scheduled event
|
||||
@@ -291,6 +446,248 @@ Cancel a scheduled event
|
||||
| ↳ `reason` | string | Cancellation reason |
|
||||
| ↳ `created_at` | string | ISO timestamp when event was canceled |
|
||||
|
||||
### Calendly Mark Invitee No-Show
|
||||
|
||||
Mark an invitee of a scheduled event as a no-show
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `inviteeUri` | string | Yes | Full invitee URI to mark as a no-show. Format: URI \(e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | The created no-show record |
|
||||
| ↳ `uri` | string | Canonical reference to the no-show |
|
||||
| ↳ `invitee` | string | URI of the invitee marked as a no-show |
|
||||
| ↳ `created_at` | string | ISO timestamp when the no-show was recorded |
|
||||
|
||||
### Calendly Unmark Invitee No-Show
|
||||
|
||||
Remove the no-show status from an invitee
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `noShowUuid` | string | Yes | No-show UUID to remove. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/invitee_no_shows/abc123-def456"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the no-show status was successfully removed |
|
||||
| `message` | string | Status message |
|
||||
|
||||
### Calendly Create Scheduling Link
|
||||
|
||||
Create a single-use scheduling link for an event type
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `eventTypeUri` | string | Yes | Event type the link books. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/event_types/abc123-def456"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | The created scheduling link |
|
||||
| ↳ `booking_url` | string | Single-use URL to share with an invitee |
|
||||
| ↳ `owner` | string | URI of the event type that owns the link |
|
||||
| ↳ `owner_type` | string | Resource type of the owner |
|
||||
|
||||
### Calendly List User Busy Times
|
||||
|
||||
Retrieve a user's internal and external busy times within a date range, based on their connected calendars
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `user` | string | Yes | User whose busy times are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\) |
|
||||
| `startTime` | string | Yes | Start of the requested range. Cannot be in the past. Format: ISO 8601 \(e.g., "2024-01-01T00:00:00Z"\) |
|
||||
| `endTime` | string | Yes | End of the requested range. Must be after the start and no more than 7 days later. Format: ISO 8601 \(e.g., "2024-01-07T00:00:00Z"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of busy time blocks |
|
||||
| ↳ `type` | string | Source of the busy block \(calendly, external, or reserved\) |
|
||||
| ↳ `start_time` | string | ISO timestamp when the block starts |
|
||||
| ↳ `end_time` | string | ISO timestamp when the block ends |
|
||||
| ↳ `buffered_start_time` | string | ISO timestamp when the block starts including buffer |
|
||||
| ↳ `buffered_end_time` | string | ISO timestamp when the block ends including buffer |
|
||||
| ↳ `event` | object | The Calendly event occupying this block |
|
||||
| ↳ `uri` | string | URI of the scheduled event |
|
||||
|
||||
### Calendly List User Availability Schedules
|
||||
|
||||
Retrieve a user's availability schedules, working hours, and date overrides
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `user` | string | Yes | User whose schedules are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of availability schedules |
|
||||
| ↳ `uri` | string | Canonical reference to the schedule |
|
||||
| ↳ `name` | string | Schedule name |
|
||||
| ↳ `default` | boolean | Whether this is the user's default schedule |
|
||||
| ↳ `user` | string | URI of the owning user |
|
||||
| ↳ `timezone` | string | Timezone the schedule is defined in |
|
||||
| ↳ `rules` | array | Weekly rules and date overrides that make up the schedule |
|
||||
| ↳ `type` | string | Rule type \(wday or date\) |
|
||||
| ↳ `wday` | string | Day of week the rule applies to, for wday rules |
|
||||
| ↳ `date` | string | Calendar date the rule overrides, for date rules |
|
||||
| ↳ `intervals` | array | Available intervals for the rule; empty means unavailable |
|
||||
| ↳ `from` | string | Interval start time \(HH:MM\) |
|
||||
| ↳ `to` | string | Interval end time \(HH:MM\) |
|
||||
|
||||
### Calendly List Organization Memberships
|
||||
|
||||
Retrieve the members of an organization, including each member profile and role
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `organization` | string | No | Return memberships that belong to this organization. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/organizations/abc123-def456"\) |
|
||||
| `user` | string | No | Return memberships that belong to this user. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/users/abc123-def456"\) |
|
||||
| `email` | string | No | Filter memberships by member email address |
|
||||
| `role` | string | No | Filter by role. Format: "owner", "admin", or "user" |
|
||||
| `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) |
|
||||
| `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of organization membership objects |
|
||||
| ↳ `uri` | string | Canonical reference to the membership |
|
||||
| ↳ `role` | string | Member role \(owner, admin, or user\) |
|
||||
| ↳ `organization` | string | URI of the organization |
|
||||
| ↳ `created_at` | string | ISO timestamp when the member joined |
|
||||
| ↳ `updated_at` | string | ISO timestamp when the membership changed |
|
||||
| ↳ `user` | object | The member |
|
||||
| ↳ `uri` | string | Canonical reference to the user |
|
||||
| ↳ `name` | string | User full name |
|
||||
| ↳ `slug` | string | Unique identifier for the user in URLs |
|
||||
| ↳ `email` | string | User email address |
|
||||
| ↳ `scheduling_url` | string | URL to the user's scheduling page |
|
||||
| ↳ `timezone` | string | User timezone |
|
||||
| ↳ `time_notation` | string | Time notation preference \(12h or 24h\) |
|
||||
| ↳ `avatar_url` | string | URL to user avatar image |
|
||||
| ↳ `locale` | string | User locale |
|
||||
| ↳ `created_at` | string | ISO timestamp when user was created |
|
||||
| ↳ `updated_at` | string | ISO timestamp when user was updated |
|
||||
| `pagination` | object | Pagination information |
|
||||
| ↳ `count` | number | Number of results in this page |
|
||||
| ↳ `next_page` | string | URL to next page \(if available\) |
|
||||
| ↳ `previous_page` | string | URL to previous page \(if available\) |
|
||||
| ↳ `next_page_token` | string | Token for next page |
|
||||
| ↳ `previous_page_token` | string | Token for previous page |
|
||||
|
||||
### Calendly List Routing Forms
|
||||
|
||||
Retrieve the routing forms of an organization, including their questions
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `organization` | string | Yes | Organization whose routing forms are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/organizations/abc123-def456"\) |
|
||||
| `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) |
|
||||
| `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token |
|
||||
| `sort` | string | No | Sort order for results. Format: "created_at:direction" \(e.g., "created_at:asc", "created_at:desc"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of routing form objects |
|
||||
| ↳ `uri` | string | Canonical reference to the routing form |
|
||||
| ↳ `organization` | string | URI of the owning organization |
|
||||
| ↳ `name` | string | Routing form name |
|
||||
| ↳ `status` | string | Routing form status \(published or draft\) |
|
||||
| ↳ `created_at` | string | ISO timestamp when the form was created |
|
||||
| ↳ `updated_at` | string | ISO timestamp when the form was updated |
|
||||
| ↳ `questions` | array | Questions asked by the routing form |
|
||||
| ↳ `uuid` | string | Question identifier |
|
||||
| ↳ `name` | string | Question text |
|
||||
| ↳ `type` | string | Question answer type |
|
||||
| ↳ `required` | boolean | Whether an answer is required |
|
||||
| ↳ `answer_choices` | array | Selectable answers for choice questions |
|
||||
| `pagination` | object | Pagination information |
|
||||
| ↳ `count` | number | Number of results in this page |
|
||||
| ↳ `next_page` | string | URL to next page \(if available\) |
|
||||
| ↳ `previous_page` | string | URL to previous page \(if available\) |
|
||||
| ↳ `next_page_token` | string | Token for next page |
|
||||
| ↳ `previous_page_token` | string | Token for previous page |
|
||||
|
||||
### Calendly List Routing Form Submissions
|
||||
|
||||
Retrieve the submissions of a routing form, including answers and routing result
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Calendly Personal Access Token |
|
||||
| `formUri` | string | Yes | Routing form whose submissions are returned. Format: UUID \(e.g., "abc123-def456"\) or full URI \(e.g., "https://api.calendly.com/routing_forms/abc123-def456"\) |
|
||||
| `count` | number | No | Number of results per page. Format: integer \(default: 20, max: 100\) |
|
||||
| `pageToken` | string | No | Page token for pagination. Format: opaque string from previous response next_page_token |
|
||||
| `sort` | string | No | Sort order for results. Format: "created_at:direction" \(e.g., "created_at:asc", "created_at:desc"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `collection` | array | Array of routing form submission objects |
|
||||
| ↳ `uri` | string | Canonical reference to the submission |
|
||||
| ↳ `routing_form` | string | URI of the routing form |
|
||||
| ↳ `submitter` | string | URI of the invitee who submitted, when the submission led to a booking |
|
||||
| ↳ `submitter_type` | string | Type of the submitter |
|
||||
| ↳ `created_at` | string | ISO timestamp when the form was submitted |
|
||||
| ↳ `updated_at` | string | ISO timestamp when the submission was updated |
|
||||
| ↳ `questions_and_answers` | array | Answers given on the routing form |
|
||||
| ↳ `question_uuid` | string | Question identifier |
|
||||
| ↳ `question` | string | Question text |
|
||||
| ↳ `answer` | string | Submitted answer |
|
||||
| ↳ `tracking` | object | UTM and Salesforce tracking parameters captured at submission |
|
||||
| ↳ `utm_campaign` | string | UTM campaign |
|
||||
| ↳ `utm_source` | string | UTM source |
|
||||
| ↳ `utm_medium` | string | UTM medium |
|
||||
| ↳ `utm_content` | string | UTM content |
|
||||
| ↳ `utm_term` | string | UTM term |
|
||||
| ↳ `salesforce_uuid` | string | Salesforce record identifier |
|
||||
| ↳ `result` | object | Where the submission routed to |
|
||||
| ↳ `type` | string | Routing result type |
|
||||
| ↳ `value` | string | Routing destination |
|
||||
| `pagination` | object | Pagination information |
|
||||
| ↳ `count` | number | Number of results in this page |
|
||||
| ↳ `next_page` | string | URL to next page \(if available\) |
|
||||
| ↳ `previous_page` | string | URL to previous page \(if available\) |
|
||||
| ↳ `next_page_token` | string | Token for next page |
|
||||
| ↳ `previous_page_token` | string | Token for previous page |
|
||||
|
||||
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Guards the block/tool contract: the operation dropdown, `tools.access`, the tool params, and the
|
||||
* declared inputs all describe the same set of operations, and drift in any one of them fails here.
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CalendlyBlock } from '@/blocks/blocks/calendly'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import * as calendlyTools from '@/tools/calendly'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const TRIGGER_FIELDS = new Set(['operation', 'selectedTriggerId'])
|
||||
|
||||
const toolsById = new Map<string, ToolConfig>(
|
||||
Object.values(calendlyTools).map((tool) => [tool.id, tool])
|
||||
)
|
||||
|
||||
const subBlocks: SubBlockConfig[] = CalendlyBlock.subBlocks
|
||||
const access: string[] = CalendlyBlock.tools.access ?? []
|
||||
const declaredInputs = Object.keys(CalendlyBlock.inputs ?? {})
|
||||
|
||||
const operationSubBlock = subBlocks.find((subBlock) => subBlock.id === 'operation')
|
||||
const operationOptions =
|
||||
typeof operationSubBlock?.options === 'function'
|
||||
? operationSubBlock.options()
|
||||
: (operationSubBlock?.options ?? [])
|
||||
const operations = operationOptions.map((option) => option.id)
|
||||
|
||||
/**
|
||||
* Trigger mode re-declares its own credential fields keyed on `selectedTriggerId`, so only the
|
||||
* operation-gated subblocks describe the tool surface.
|
||||
*/
|
||||
const operationSubBlocks = subBlocks.filter((subBlock) => {
|
||||
const condition = subBlock.condition
|
||||
if (typeof condition === 'function') return false
|
||||
return subBlock.id === 'operation' || !condition || condition.field === 'operation'
|
||||
})
|
||||
|
||||
function visibleFor(operation: string): string[] {
|
||||
return operationSubBlocks
|
||||
.filter((subBlock) => {
|
||||
const condition = subBlock.condition
|
||||
if (typeof condition === 'function' || !condition) return true
|
||||
return Array.isArray(condition.value)
|
||||
? condition.value.includes(operation)
|
||||
: condition.value === operation
|
||||
})
|
||||
.map((subBlock) => subBlock.id)
|
||||
}
|
||||
|
||||
describe('calendly block/tool alignment', () => {
|
||||
it('maps every operation to a registered tool and back', () => {
|
||||
expect(operations.filter((operation) => !access.includes(operation))).toEqual([])
|
||||
expect(access.filter((tool) => !operations.includes(tool))).toEqual([])
|
||||
expect(access.filter((tool) => !toolsById.has(tool))).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps operation-gated subBlock ids unique', () => {
|
||||
const ids = operationSubBlocks.map((subBlock) => subBlock.id)
|
||||
expect(ids.filter((id, index) => ids.indexOf(id) !== index)).toEqual([])
|
||||
})
|
||||
|
||||
it('exposes a subBlock for every required tool param', () => {
|
||||
const missing: string[] = []
|
||||
|
||||
for (const operation of operations) {
|
||||
const tool = toolsById.get(operation)
|
||||
if (!tool) continue
|
||||
const visible = visibleFor(operation)
|
||||
|
||||
for (const [name, param] of Object.entries(tool.params)) {
|
||||
if (param.visibility === 'hidden' || !param.required) continue
|
||||
if (!visible.includes(name)) missing.push(`${operation}.${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it('backs every visible subBlock with a param on its operation tool', () => {
|
||||
const stray: string[] = []
|
||||
|
||||
for (const operation of operations) {
|
||||
const tool = toolsById.get(operation)
|
||||
if (!tool) continue
|
||||
|
||||
for (const id of visibleFor(operation)) {
|
||||
if (TRIGGER_FIELDS.has(id)) continue
|
||||
if (!tool.params[id]) stray.push(`${operation}.${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
expect(stray).toEqual([])
|
||||
})
|
||||
|
||||
it('declares every operation-gated subBlock in block inputs', () => {
|
||||
const undeclared = operationSubBlocks
|
||||
.filter((subBlock) => !TRIGGER_FIELDS.has(subBlock.id) && subBlock.condition)
|
||||
.map((subBlock) => subBlock.id)
|
||||
.filter((id) => !declaredInputs.includes(id))
|
||||
|
||||
expect(undeclared).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { BlockConfig, BlockMeta } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
@@ -10,6 +11,20 @@ import { getTrigger } from '@/triggers'
|
||||
*/
|
||||
const SCOPE_FIELD = ['user', 'organization'] as const
|
||||
|
||||
/**
|
||||
* A `json` field arrives as text the user typed, but as a real array when an upstream block is
|
||||
* referenced, so only the text form is parsed.
|
||||
*/
|
||||
function parseJsonArray(value: unknown, field: string): unknown {
|
||||
if (typeof value !== 'string') return value
|
||||
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input for ${field}: ${getErrorMessage(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'calendly',
|
||||
name: 'Calendly',
|
||||
@@ -33,8 +48,14 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
sentences: {
|
||||
byOperation: {
|
||||
calendly_get_current_user: ['Read the connected account profile'],
|
||||
calendly_get_user: [{ text: 'Read user', field: 'userUuid', core: true }],
|
||||
calendly_list_event_types: ['List event types', { text: ', for', field: SCOPE_FIELD }],
|
||||
calendly_get_event_type: [{ text: 'Read event type', field: 'eventTypeUuid', core: true }],
|
||||
calendly_list_event_type_available_times: [
|
||||
{ text: 'List open slots for event type', field: 'eventTypeUri', core: true },
|
||||
{ text: ', from', field: 'startTime' },
|
||||
{ text: ', until', field: 'endTime' },
|
||||
],
|
||||
calendly_list_scheduled_events: [
|
||||
'List scheduled events',
|
||||
{ text: ', for', field: SCOPE_FIELD },
|
||||
@@ -49,10 +70,48 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
{ text: ', matching', field: 'email' },
|
||||
{ text: ', with status', field: 'status' },
|
||||
],
|
||||
calendly_get_event_invitee: [
|
||||
{ text: 'Read invitee', field: 'inviteeUuid', core: true },
|
||||
{ text: ', of event', field: 'eventUuid' },
|
||||
],
|
||||
calendly_create_event_invitee: [
|
||||
{ text: 'Book event type', field: 'eventTypeUri', core: true },
|
||||
{ text: ', for', field: 'inviteeEmail' },
|
||||
{ text: ', at', field: 'startTime' },
|
||||
],
|
||||
calendly_cancel_event: [
|
||||
{ text: 'Cancel event', field: 'eventUuid', core: true },
|
||||
{ text: ', with reason', field: 'reason' },
|
||||
],
|
||||
calendly_create_invitee_no_show: [
|
||||
{ text: 'Mark invitee as a no-show', field: 'inviteeUri', core: true },
|
||||
],
|
||||
calendly_delete_invitee_no_show: [
|
||||
{ text: 'Remove no-show', field: 'noShowUuid', core: true },
|
||||
],
|
||||
calendly_create_scheduling_link: [
|
||||
{ text: 'Create a single-use link for event type', field: 'eventTypeUri', core: true },
|
||||
],
|
||||
calendly_list_user_busy_times: [
|
||||
{ text: 'List busy times for', field: 'user', core: true },
|
||||
{ text: ', from', field: 'startTime' },
|
||||
{ text: ', until', field: 'endTime' },
|
||||
],
|
||||
calendly_list_user_availability_schedules: [
|
||||
{ text: 'List availability schedules for', field: 'user', core: true },
|
||||
],
|
||||
calendly_list_organization_memberships: [
|
||||
'List organization members',
|
||||
{ text: ', in', field: 'organization' },
|
||||
{ text: ', with role', field: 'role' },
|
||||
{ text: ', matching', field: 'email' },
|
||||
],
|
||||
calendly_list_routing_forms: [
|
||||
{ text: 'List routing forms of', field: 'organization', core: true },
|
||||
],
|
||||
calendly_list_routing_form_submissions: [
|
||||
{ text: 'List submissions of routing form', field: 'formUri', core: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -63,12 +122,30 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Get Current User', id: 'calendly_get_current_user' },
|
||||
{ label: 'Get User', id: 'calendly_get_user' },
|
||||
{ label: 'List Event Types', id: 'calendly_list_event_types' },
|
||||
{ label: 'Get Event Type', id: 'calendly_get_event_type' },
|
||||
{
|
||||
label: 'List Event Type Available Times',
|
||||
id: 'calendly_list_event_type_available_times',
|
||||
},
|
||||
{ label: 'List Scheduled Events', id: 'calendly_list_scheduled_events' },
|
||||
{ label: 'Get Scheduled Event', id: 'calendly_get_scheduled_event' },
|
||||
{ label: 'List Event Invitees', id: 'calendly_list_event_invitees' },
|
||||
{ label: 'Get Event Invitee', id: 'calendly_get_event_invitee' },
|
||||
{ label: 'Book Meeting', id: 'calendly_create_event_invitee' },
|
||||
{ label: 'Cancel Event', id: 'calendly_cancel_event' },
|
||||
{ label: 'Mark Invitee No-Show', id: 'calendly_create_invitee_no_show' },
|
||||
{ label: 'Unmark Invitee No-Show', id: 'calendly_delete_invitee_no_show' },
|
||||
{ label: 'Create Scheduling Link', id: 'calendly_create_scheduling_link' },
|
||||
{ label: 'List User Busy Times', id: 'calendly_list_user_busy_times' },
|
||||
{
|
||||
label: 'List User Availability Schedules',
|
||||
id: 'calendly_list_user_availability_schedules',
|
||||
},
|
||||
{ label: 'List Organization Memberships', id: 'calendly_list_organization_memberships' },
|
||||
{ label: 'List Routing Forms', id: 'calendly_list_routing_forms' },
|
||||
{ label: 'List Routing Form Submissions', id: 'calendly_list_routing_form_submissions' },
|
||||
],
|
||||
value: () => 'calendly_list_scheduled_events',
|
||||
},
|
||||
@@ -80,7 +157,14 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
// Get Event Type fields
|
||||
{
|
||||
id: 'userUuid',
|
||||
title: 'User UUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter user UUID or URI, or "me"',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_get_user' },
|
||||
},
|
||||
{
|
||||
id: 'eventTypeUuid',
|
||||
title: 'Event Type UUID',
|
||||
@@ -89,25 +173,55 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_get_event_type' },
|
||||
},
|
||||
// List Event Types fields
|
||||
{
|
||||
id: 'eventTypeUri',
|
||||
title: 'Event Type',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter event type UUID or URI',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'calendly_list_event_type_available_times',
|
||||
'calendly_create_event_invitee',
|
||||
'calendly_create_scheduling_link',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: 'User URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by user URI',
|
||||
placeholder: 'Enter user UUID or URI',
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_user_busy_times', 'calendly_list_user_availability_schedules'],
|
||||
},
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_event_types', 'calendly_list_scheduled_events'],
|
||||
value: [
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_user_busy_times',
|
||||
'calendly_list_user_availability_schedules',
|
||||
'calendly_list_organization_memberships',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by organization URI (optional)',
|
||||
placeholder: 'Enter organization UUID or URI',
|
||||
required: { field: 'operation', value: 'calendly_list_routing_forms' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_event_types', 'calendly_list_scheduled_events'],
|
||||
value: [
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_organization_memberships',
|
||||
'calendly_list_routing_forms',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -118,7 +232,6 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
'When enabled, shows only active event types. When disabled, shows all event types.',
|
||||
condition: { field: 'operation', value: 'calendly_list_event_types' },
|
||||
},
|
||||
// List Scheduled Events fields
|
||||
{
|
||||
id: 'invitee_email',
|
||||
title: 'Invitee Email',
|
||||
@@ -130,6 +243,7 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
id: 'min_start_time',
|
||||
title: 'Min Start Time',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
placeholder: 'ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
|
||||
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
|
||||
wandConfig: {
|
||||
@@ -151,6 +265,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
id: 'max_start_time',
|
||||
title: 'Max Start Time',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
placeholder: 'ISO 8601 format (e.g., 2024-12-31T23:59:59Z)',
|
||||
condition: { field: 'operation', value: 'calendly_list_scheduled_events' },
|
||||
wandConfig: {
|
||||
@@ -182,7 +297,60 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
value: ['calendly_list_scheduled_events', 'calendly_list_event_invitees'],
|
||||
},
|
||||
},
|
||||
// Get Scheduled Event / List Invitees / Cancel Event fields
|
||||
{
|
||||
id: 'startTime',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'calendly_list_event_type_available_times',
|
||||
'calendly_list_user_busy_times',
|
||||
'calendly_create_event_invitee',
|
||||
],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "today" -> Today's date at 00:00:00Z
|
||||
- "tomorrow morning" -> Tomorrow's date at 09:00:00Z
|
||||
- "start of next week" -> Monday of next week at 00:00:00Z
|
||||
|
||||
Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the start time (e.g., "today", "tomorrow morning")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'endTime',
|
||||
title: 'End Time',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601 format (e.g., 2024-01-07T00:00:00Z)',
|
||||
description:
|
||||
'Busy times allow a range of up to 7 days. Available times allow a range of up to 31 days.',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_event_type_available_times', 'calendly_list_user_busy_times'],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "end of today" -> Today's date at 23:59:59Z
|
||||
- "in 7 days" -> 7 days from now at 23:59:59Z
|
||||
- "end of this week" -> Sunday of the current week at 23:59:59Z
|
||||
|
||||
Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the end time (e.g., "in 7 days", "end of week")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'eventUuid',
|
||||
title: 'Event UUID',
|
||||
@@ -194,11 +362,19 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
value: [
|
||||
'calendly_get_scheduled_event',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_get_event_invitee',
|
||||
'calendly_cancel_event',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Cancel Event fields
|
||||
{
|
||||
id: 'inviteeUuid',
|
||||
title: 'Invitee UUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter invitee UUID or URI',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_get_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'reason',
|
||||
title: 'Cancellation Reason',
|
||||
@@ -206,19 +382,121 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
placeholder: 'Reason for cancellation (optional)',
|
||||
condition: { field: 'operation', value: 'calendly_cancel_event' },
|
||||
},
|
||||
// List Event Invitees fields
|
||||
{
|
||||
id: 'inviteeEmail',
|
||||
title: 'Invitee Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'Email address of the person being booked',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'inviteeName',
|
||||
title: 'Invitee Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Full name of the invitee',
|
||||
description: 'Provide a full name or a first name.',
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'inviteeTimezone',
|
||||
title: 'Invitee Timezone',
|
||||
type: 'short-input',
|
||||
placeholder: 'IANA timezone (e.g., America/New_York)',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'inviteeFirstName',
|
||||
title: 'Invitee First Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'First name of the invitee',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'inviteeLastName',
|
||||
title: 'Invitee Last Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Last name of the invitee',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'textReminderNumber',
|
||||
title: 'Text Reminder Number',
|
||||
type: 'short-input',
|
||||
placeholder: 'E.164 phone number (e.g., +14155551234)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
},
|
||||
{
|
||||
id: 'eventGuests',
|
||||
title: 'Event Guests',
|
||||
type: 'long-input',
|
||||
placeholder: '["guest@example.com"]',
|
||||
description: 'Additional guests to copy on the invite. Maximum of 10.',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'calendly_create_event_invitee' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a JSON array of guest email address strings, for example ["guest@example.com"]. Return ONLY the JSON array - no explanations, no extra text.',
|
||||
placeholder: 'Describe the guests to copy on the invite...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inviteeUri',
|
||||
title: 'Invitee URI',
|
||||
type: 'short-input',
|
||||
placeholder: 'https://api.calendly.com/scheduled_events/.../invitees/...',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_create_invitee_no_show' },
|
||||
},
|
||||
{
|
||||
id: 'noShowUuid',
|
||||
title: 'No-Show UUID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter no-show UUID or URI',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_delete_invitee_no_show' },
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
title: 'Role',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All', id: '' },
|
||||
{ label: 'Owner', id: 'owner' },
|
||||
{ label: 'Admin', id: 'admin' },
|
||||
{ label: 'User', id: 'user' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'calendly_list_organization_memberships' },
|
||||
},
|
||||
{
|
||||
id: 'formUri',
|
||||
title: 'Routing Form',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter routing form UUID or URI',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'calendly_list_routing_form_submissions' },
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
title: 'Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by invitee email',
|
||||
condition: { field: 'operation', value: 'calendly_list_event_invitees' },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['calendly_list_event_invitees', 'calendly_list_organization_memberships'],
|
||||
},
|
||||
},
|
||||
// Pagination fields
|
||||
{
|
||||
id: 'count',
|
||||
title: 'Results Per Page',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
placeholder: 'Number of results (default: 20, max: 100)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -226,6 +504,9 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_list_organization_memberships',
|
||||
'calendly_list_routing_forms',
|
||||
'calendly_list_routing_form_submissions',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -233,6 +514,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
id: 'pageToken',
|
||||
title: 'Page Token',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
placeholder: 'Token for pagination',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -240,6 +522,9 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_list_organization_memberships',
|
||||
'calendly_list_routing_forms',
|
||||
'calendly_list_routing_form_submissions',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -247,6 +532,7 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
id: 'sort',
|
||||
title: 'Sort Order',
|
||||
type: 'short-input',
|
||||
mode: 'advanced',
|
||||
placeholder: 'e.g., "name:asc", "start_time:desc"',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -254,10 +540,11 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
'calendly_list_event_types',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_list_routing_forms',
|
||||
'calendly_list_routing_form_submissions',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Trigger SubBlocks
|
||||
...getTrigger('calendly_invitee_created').subBlocks,
|
||||
...getTrigger('calendly_invitee_canceled').subBlocks,
|
||||
...getTrigger('calendly_routing_form_submitted').subBlocks,
|
||||
@@ -266,31 +553,40 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
tools: {
|
||||
access: [
|
||||
'calendly_get_current_user',
|
||||
'calendly_get_user',
|
||||
'calendly_list_event_types',
|
||||
'calendly_get_event_type',
|
||||
'calendly_list_event_type_available_times',
|
||||
'calendly_list_scheduled_events',
|
||||
'calendly_get_scheduled_event',
|
||||
'calendly_list_event_invitees',
|
||||
'calendly_get_event_invitee',
|
||||
'calendly_create_event_invitee',
|
||||
'calendly_cancel_event',
|
||||
'calendly_create_invitee_no_show',
|
||||
'calendly_delete_invitee_no_show',
|
||||
'calendly_create_scheduling_link',
|
||||
'calendly_list_user_busy_times',
|
||||
'calendly_list_user_availability_schedules',
|
||||
'calendly_list_organization_memberships',
|
||||
'calendly_list_routing_forms',
|
||||
'calendly_list_routing_form_submissions',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
return params.operation || 'calendly_list_scheduled_events'
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, events, ...rest } = params
|
||||
|
||||
let parsedEvents: any | undefined
|
||||
|
||||
try {
|
||||
if (events) parsedEvents = JSON.parse(events)
|
||||
} catch (error: any) {
|
||||
throw new Error(`Invalid JSON input for events: ${error.message}`)
|
||||
}
|
||||
const { operation, events, eventGuests, active, ...rest } = params
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(parsedEvents && { events: parsedEvents }),
|
||||
// The switch cannot express "inactive only", so off means unfiltered, not `active=false`.
|
||||
...(active === true && { active: true }),
|
||||
...(events !== undefined && { events: parseJsonArray(events, 'events') }),
|
||||
...(eventGuests !== undefined && {
|
||||
eventGuests: parseJsonArray(eventGuests, 'eventGuests'),
|
||||
}),
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -298,26 +594,36 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
apiKey: { type: 'string', description: 'Personal access token' },
|
||||
// Event Type params
|
||||
userUuid: { type: 'string', description: 'User UUID' },
|
||||
eventTypeUuid: { type: 'string', description: 'Event type UUID' },
|
||||
eventTypeUri: { type: 'string', description: 'Event type UUID or URI' },
|
||||
user: { type: 'string', description: 'User URI filter' },
|
||||
organization: { type: 'string', description: 'Organization URI' },
|
||||
active: { type: 'boolean', description: 'Filter by active status' },
|
||||
// Scheduled Event params
|
||||
startTime: { type: 'string', description: 'Start of the requested range (ISO 8601)' },
|
||||
endTime: { type: 'string', description: 'End of the requested range (ISO 8601)' },
|
||||
eventUuid: { type: 'string', description: 'Scheduled event UUID' },
|
||||
invitee_email: { type: 'string', description: 'Filter by invitee email' },
|
||||
min_start_time: { type: 'string', description: 'Minimum start time (ISO 8601)' },
|
||||
max_start_time: { type: 'string', description: 'Maximum start time (ISO 8601)' },
|
||||
status: { type: 'string', description: 'Status filter (active or canceled)' },
|
||||
// Cancel Event params
|
||||
reason: { type: 'string', description: 'Cancellation reason' },
|
||||
// Invitees params
|
||||
email: { type: 'string', description: 'Filter by email' },
|
||||
// Pagination params
|
||||
inviteeUuid: { type: 'string', description: 'Invitee UUID' },
|
||||
inviteeUri: { type: 'string', description: 'Invitee URI' },
|
||||
noShowUuid: { type: 'string', description: 'No-show UUID' },
|
||||
inviteeEmail: { type: 'string', description: 'Email address of the invitee being booked' },
|
||||
inviteeName: { type: 'string', description: 'Full name of the invitee' },
|
||||
inviteeFirstName: { type: 'string', description: 'First name of the invitee' },
|
||||
inviteeLastName: { type: 'string', description: 'Last name of the invitee' },
|
||||
inviteeTimezone: { type: 'string', description: 'Timezone of the invitee' },
|
||||
textReminderNumber: { type: 'string', description: 'Phone number for SMS reminders' },
|
||||
eventGuests: { type: 'json', description: 'Array of additional guest email addresses' },
|
||||
role: { type: 'string', description: 'Filter memberships by role' },
|
||||
formUri: { type: 'string', description: 'Routing form UUID or URI' },
|
||||
count: { type: 'number', description: 'Results per page' },
|
||||
pageToken: { type: 'string', description: 'Pagination token' },
|
||||
sort: { type: 'string', description: 'Sort order' },
|
||||
// Webhook params
|
||||
webhookUuid: { type: 'string', description: 'Webhook UUID' },
|
||||
url: { type: 'string', description: 'Webhook callback URL' },
|
||||
events: { type: 'json', description: 'Array of event types' },
|
||||
@@ -325,14 +631,10 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
signing_key: { type: 'string', description: 'Webhook signing key' },
|
||||
},
|
||||
outputs: {
|
||||
// Common outputs
|
||||
success: { type: 'boolean', description: 'Whether operation succeeded' },
|
||||
// User outputs
|
||||
resource: { type: 'json', description: 'Resource data (user, event type, event, etc.)' },
|
||||
// List outputs
|
||||
collection: { type: 'json', description: 'Array of items' },
|
||||
pagination: { type: 'json', description: 'Pagination information' },
|
||||
// Event details
|
||||
uri: { type: 'string', description: 'Resource URI' },
|
||||
name: { type: 'string', description: 'Resource name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
@@ -341,13 +643,11 @@ Return ONLY the timestamp string in ISO 8601 format - no explanations, no quotes
|
||||
end_time: { type: 'string', description: 'Event end time (ISO 8601)' },
|
||||
location: { type: 'json', description: 'Event location details' },
|
||||
scheduling_url: { type: 'string', description: 'Scheduling page URL' },
|
||||
// Webhook outputs
|
||||
booking_url: { type: 'string', description: 'Single-use scheduling link' },
|
||||
callback_url: { type: 'string', description: 'Webhook URL' },
|
||||
signing_key: { type: 'string', description: 'Webhook signing key' },
|
||||
// Delete outputs
|
||||
deleted: { type: 'boolean', description: 'Whether deletion succeeded' },
|
||||
message: { type: 'string', description: 'Status message' },
|
||||
// Trigger outputs
|
||||
event: { type: 'string', description: 'Webhook event type' },
|
||||
created_at: { type: 'string', description: 'Webhook event creation timestamp' },
|
||||
created_by: {
|
||||
@@ -442,6 +742,34 @@ export const CalendlyBlockMeta = {
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
name: 'book-a-meeting',
|
||||
description:
|
||||
'Find an open slot on a Calendly event type and book an invitee into it. Use to schedule a meeting end to end without sending the invitee a link.',
|
||||
content:
|
||||
'# Book a Meeting\n\nSchedule a Calendly meeting directly.\n\n## Steps\n1. Use List Event Types to find the event type to book, and take its URI.\n2. Use List Event Type Available Times with that event type and a start/end window (ISO 8601 UTC, at most 31 days apart) to get open slots.\n3. Pick a slot and use Book Meeting with the event type, the slot start time, and the invitee email, timezone, and name. Add event guests to copy others on the invite.\n\n## Output\nReturn the booked start time, the invitee, and the reschedule and cancel URLs. Booking requires a paid Calendly plan; if the call is rejected as forbidden, say so rather than retrying. If no slot matches the request, report the nearest available times instead of booking a different one.',
|
||||
},
|
||||
{
|
||||
name: 'check-availability',
|
||||
description:
|
||||
"Read a Calendly user's busy times and working hours for a date range. Use to find when someone is free before proposing times.",
|
||||
content:
|
||||
"# Check Availability\n\nInspect when a host is free.\n\n## Steps\n1. Get the user URI (Get Current User for yourself, or List Organization Memberships to find a teammate by email).\n2. Use List User Busy Times with that user and a start/end window (ISO 8601 UTC, at most 7 days apart) to get booked and externally blocked time.\n3. Use List User Availability Schedules to read the working hours and date overrides the busy times sit inside.\n\n## Output\nReturn the free windows implied by the working hours minus the busy blocks, in the schedule's timezone. Note which busy blocks are Calendly events versus external calendar holds. Both endpoints cap the range, so split a longer request into several calls rather than widening the window.",
|
||||
},
|
||||
{
|
||||
name: 'share-a-single-use-link',
|
||||
description:
|
||||
'Create a one-time Calendly booking link for an event type. Use to invite someone to schedule without exposing a reusable link.',
|
||||
content:
|
||||
'# Share a Single-Use Link\n\nHand out a booking link that works once.\n\n## Steps\n1. Use List Event Types to find the event type and take its URI.\n2. Use Create Scheduling Link with that event type.\n3. Send the returned booking URL to the invitee.\n\n## Output\nReturn the booking URL and the event type it books. Make clear the link expires after one booking, so generate a new one per invitee rather than reusing it.',
|
||||
},
|
||||
{
|
||||
name: 'mark-a-no-show',
|
||||
description:
|
||||
'Flag a Calendly invitee who did not attend, or clear that flag. Use when reconciling attendance after a meeting.',
|
||||
content:
|
||||
'# Mark a No-Show\n\nRecord attendance for a past meeting.\n\n## Steps\n1. Use List Event Invitees for the scheduled event and take the URI of the invitee who did not attend.\n2. Use Mark Invitee No-Show with that invitee URI.\n3. To reverse it, take the no-show URI from the invitee record and use Unmark Invitee No-Show.\n\n## Output\nReturn which invitees were marked and the no-show URI for each, so the flag can be reversed later. Only mark meetings that have already ended.',
|
||||
},
|
||||
{
|
||||
name: 'list-upcoming-meetings',
|
||||
description:
|
||||
|
||||
@@ -2842,6 +2842,10 @@
|
||||
"name": "Get Current User",
|
||||
"description": "Get information about the currently authenticated Calendly user"
|
||||
},
|
||||
{
|
||||
"name": "Get User",
|
||||
"description": "Get information about a specific Calendly user"
|
||||
},
|
||||
{
|
||||
"name": "List Event Types",
|
||||
"description": "Retrieve a list of all event types for a user or organization"
|
||||
@@ -2850,6 +2854,10 @@
|
||||
"name": "Get Event Type",
|
||||
"description": "Get detailed information about a specific event type"
|
||||
},
|
||||
{
|
||||
"name": "List Event Type Available Times",
|
||||
"description": "Retrieve bookable time slots for an event type within a date range"
|
||||
},
|
||||
{
|
||||
"name": "List Scheduled Events",
|
||||
"description": "Retrieve a list of scheduled events for a user or organization"
|
||||
@@ -2862,12 +2870,52 @@
|
||||
"name": "List Event Invitees",
|
||||
"description": "Retrieve a list of invitees for a scheduled event"
|
||||
},
|
||||
{
|
||||
"name": "Get Event Invitee",
|
||||
"description": "Retrieve a single invitee of a scheduled event, including their intake answers"
|
||||
},
|
||||
{
|
||||
"name": "Book Meeting",
|
||||
"description": "Book a meeting by creating an invitee on an event type at a chosen time. Requires a paid Calendly plan"
|
||||
},
|
||||
{
|
||||
"name": "Cancel Event",
|
||||
"description": "Cancel a scheduled event"
|
||||
},
|
||||
{
|
||||
"name": "Mark Invitee No-Show",
|
||||
"description": "Mark an invitee of a scheduled event as a no-show"
|
||||
},
|
||||
{
|
||||
"name": "Unmark Invitee No-Show",
|
||||
"description": "Remove the no-show status from an invitee"
|
||||
},
|
||||
{
|
||||
"name": "Create Scheduling Link",
|
||||
"description": "Create a single-use scheduling link for an event type"
|
||||
},
|
||||
{
|
||||
"name": "List User Busy Times",
|
||||
"description": "Retrieve a user's internal and external busy times within a date range, based on their connected calendars"
|
||||
},
|
||||
{
|
||||
"name": "List User Availability Schedules",
|
||||
"description": "Retrieve a user's availability schedules, working hours, and date overrides"
|
||||
},
|
||||
{
|
||||
"name": "List Organization Memberships",
|
||||
"description": "Retrieve the members of an organization, including each member profile and role"
|
||||
},
|
||||
{
|
||||
"name": "List Routing Forms",
|
||||
"description": "Retrieve the routing forms of an organization, including their questions"
|
||||
},
|
||||
{
|
||||
"name": "List Routing Form Submissions",
|
||||
"description": "Retrieve the submissions of a routing form, including answers and routing result"
|
||||
}
|
||||
],
|
||||
"operationCount": 7,
|
||||
"operationCount": 19,
|
||||
"triggers": [
|
||||
{
|
||||
"id": "calendly_invitee_created",
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createEventInviteeTool } from '@/tools/calendly/create_event_invitee'
|
||||
import { createInviteeNoShowTool } from '@/tools/calendly/create_invitee_no_show'
|
||||
import { createWebhookTool } from '@/tools/calendly/create_webhook'
|
||||
import { getEventInviteeTool } from '@/tools/calendly/get_event_invitee'
|
||||
import { listEventTypesTool } from '@/tools/calendly/list_event_types'
|
||||
import { listScheduledEventsTool } from '@/tools/calendly/list_scheduled_events'
|
||||
import { listWebhooksTool } from '@/tools/calendly/list_webhooks'
|
||||
import { toResourceUri, toStringArray, toUuid } from '@/tools/calendly/utils'
|
||||
|
||||
function buildUrl(tool: { request: { url: unknown } }, params: Record<string, unknown>): string {
|
||||
const url = tool.request.url
|
||||
return typeof url === 'function' ? url(params) : (url as string)
|
||||
}
|
||||
|
||||
function buildBody(tool: { request: { body?: unknown } }, params: Record<string, unknown>) {
|
||||
const body = tool.request.body as (p: Record<string, unknown>) => Record<string, unknown>
|
||||
return body(params)
|
||||
}
|
||||
|
||||
describe('calendly resource identifiers', () => {
|
||||
it('extracts the trailing uuid from a nested invitee uri', () => {
|
||||
expect(toUuid('https://api.calendly.com/scheduled_events/event-1/invitees/invitee-2')).toBe(
|
||||
'invitee-2'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves a bare uuid untouched and strips surrounding whitespace', () => {
|
||||
expect(toUuid(' event-1 ')).toBe('event-1')
|
||||
})
|
||||
|
||||
it('expands a bare uuid into a canonical uri and passes a full uri through', () => {
|
||||
expect(toResourceUri(' user-1 ', 'users')).toBe('https://api.calendly.com/users/user-1')
|
||||
expect(toResourceUri('https://api.calendly.com/users/user-1', 'users')).toBe(
|
||||
'https://api.calendly.com/users/user-1'
|
||||
)
|
||||
})
|
||||
|
||||
it('accepts json array params as both an array and a json string', () => {
|
||||
expect(toStringArray(['a@example.com'], 'eventGuests')).toEqual(['a@example.com'])
|
||||
expect(toStringArray('["a@example.com"]', 'eventGuests')).toEqual(['a@example.com'])
|
||||
expect(toStringArray(undefined, 'eventGuests')).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects a json array param that is not an array', () => {
|
||||
expect(() => toStringArray('{"a":1}', 'eventGuests')).toThrow(/array of strings/)
|
||||
expect(() => toStringArray('not json', 'eventGuests')).toThrow(/Invalid JSON input/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calendly list tools', () => {
|
||||
it('always sends the scope required by the webhook subscriptions endpoint', () => {
|
||||
const url = buildUrl(listWebhooksTool, {
|
||||
apiKey: 'k',
|
||||
organization: 'org-1',
|
||||
scope: 'organization',
|
||||
})
|
||||
|
||||
expect(url).toContain('scope=organization')
|
||||
expect(url).toContain(
|
||||
`organization=${encodeURIComponent('https://api.calendly.com/organizations/org-1')}`
|
||||
)
|
||||
})
|
||||
|
||||
it('sends active=false so inactive event types can be listed', () => {
|
||||
expect(buildUrl(listEventTypesTool, { apiKey: 'k', user: 'user-1', active: false })).toContain(
|
||||
'active=false'
|
||||
)
|
||||
expect(buildUrl(listEventTypesTool, { apiKey: 'k', user: 'user-1', active: true })).toContain(
|
||||
'active=true'
|
||||
)
|
||||
})
|
||||
|
||||
it('omits the active filter entirely when it is not set', () => {
|
||||
expect(buildUrl(listEventTypesTool, { apiKey: 'k', user: 'user-1' })).not.toContain('active=')
|
||||
})
|
||||
|
||||
it('normalizes bare uuids for the shared user and organization filters', () => {
|
||||
const url = buildUrl(listScheduledEventsTool, { apiKey: 'k', user: 'user-1' })
|
||||
expect(url).toContain(`user=${encodeURIComponent('https://api.calendly.com/users/user-1')}`)
|
||||
})
|
||||
|
||||
it('still requires a user or organization scope', () => {
|
||||
expect(() => buildUrl(listScheduledEventsTool, { apiKey: 'k' })).toThrow(
|
||||
/"user" or "organization"/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calendly booking', () => {
|
||||
const bookingParams = {
|
||||
apiKey: 'k',
|
||||
eventTypeUri: 'event-type-1',
|
||||
startTime: '2024-01-15T09:00:00Z',
|
||||
inviteeEmail: 'invitee@example.com',
|
||||
inviteeTimezone: 'America/New_York',
|
||||
inviteeName: 'Jane Doe',
|
||||
}
|
||||
|
||||
it('builds the documented booking body', () => {
|
||||
expect(buildBody(createEventInviteeTool, bookingParams)).toEqual({
|
||||
event_type: 'https://api.calendly.com/event_types/event-type-1',
|
||||
start_time: '2024-01-15T09:00:00Z',
|
||||
invitee: {
|
||||
email: 'invitee@example.com',
|
||||
timezone: 'America/New_York',
|
||||
name: 'Jane Doe',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts event guests supplied as a json string', () => {
|
||||
const body = buildBody(createEventInviteeTool, {
|
||||
...bookingParams,
|
||||
eventGuests: '["guest@example.com"]',
|
||||
})
|
||||
|
||||
expect(body.event_guests).toEqual(['guest@example.com'])
|
||||
})
|
||||
|
||||
it('rejects a booking with neither a full name nor a first name', () => {
|
||||
const { inviteeName, ...withoutName } = bookingParams
|
||||
expect(() => buildBody(createEventInviteeTool, withoutName)).toThrow(
|
||||
/"inviteeName" or "inviteeFirstName"/
|
||||
)
|
||||
})
|
||||
|
||||
it('trims the invitee uri when marking a no-show', () => {
|
||||
expect(
|
||||
buildBody(createInviteeNoShowTool, {
|
||||
apiKey: 'k',
|
||||
inviteeUri: ' https://api.calendly.com/scheduled_events/e-1/invitees/i-1 ',
|
||||
})
|
||||
).toEqual({ invitee: 'https://api.calendly.com/scheduled_events/e-1/invitees/i-1' })
|
||||
})
|
||||
|
||||
it('addresses a single invitee by its event and invitee uuids', () => {
|
||||
expect(
|
||||
buildUrl(getEventInviteeTool, {
|
||||
apiKey: 'k',
|
||||
eventUuid: 'https://api.calendly.com/scheduled_events/e-1',
|
||||
inviteeUuid: 'i-1',
|
||||
})
|
||||
).toBe('https://api.calendly.com/scheduled_events/e-1/invitees/i-1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('calendly webhook subscriptions', () => {
|
||||
it('normalizes the organization and user identifiers when creating a webhook', () => {
|
||||
expect(
|
||||
buildBody(createWebhookTool, {
|
||||
apiKey: 'k',
|
||||
url: 'https://example.com/hook',
|
||||
events: ['invitee.created'],
|
||||
organization: 'org-1',
|
||||
scope: 'user',
|
||||
user: 'user-1',
|
||||
})
|
||||
).toEqual({
|
||||
url: 'https://example.com/hook',
|
||||
events: ['invitee.created'],
|
||||
organization: 'https://api.calendly.com/organizations/org-1',
|
||||
scope: 'user',
|
||||
user: 'https://api.calendly.com/users/user-1',
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts webhook events supplied as a json string', () => {
|
||||
const body = buildBody(createWebhookTool, {
|
||||
apiKey: 'k',
|
||||
url: 'https://example.com/hook',
|
||||
events: '["invitee.created","invitee.canceled"]',
|
||||
organization: 'https://api.calendly.com/organizations/org-1',
|
||||
scope: 'organization',
|
||||
})
|
||||
|
||||
expect(body.events).toEqual(['invitee.created', 'invitee.canceled'])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CalendlyCancelEventParams, CalendlyCancelEventResponse } from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cancelEventTool: ToolConfig<CalendlyCancelEventParams, CalendlyCancelEventResponse> = {
|
||||
@@ -31,9 +32,7 @@ export const cancelEventTool: ToolConfig<CalendlyCancelEventParams, CalendlyCanc
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyCancelEventParams) => {
|
||||
const uuid = params.eventUuid.includes('/')
|
||||
? params.eventUuid.split('/').pop()
|
||||
: params.eventUuid
|
||||
const uuid = toUuid(params.eventUuid)
|
||||
return `https://api.calendly.com/scheduled_events/${uuid}/cancellation`
|
||||
},
|
||||
method: 'POST',
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import type {
|
||||
CalendlyCreateEventInviteeParams,
|
||||
CalendlyCreateEventInviteeResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri, toStringArray } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createEventInviteeTool: ToolConfig<
|
||||
CalendlyCreateEventInviteeParams,
|
||||
CalendlyCreateEventInviteeResponse
|
||||
> = {
|
||||
id: 'calendly_create_event_invitee',
|
||||
name: 'Calendly Book Meeting',
|
||||
description:
|
||||
'Book a meeting by creating an invitee on an event type at a chosen time. Requires a paid Calendly plan',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventTypeUri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Event type to book. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/event_types/abc123-def456")',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start time of the booking in UTC. Must be an available slot. Format: ISO 8601 (e.g., "2024-01-15T09:00:00Z")',
|
||||
},
|
||||
inviteeEmail: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address of the invitee being booked',
|
||||
},
|
||||
inviteeTimezone: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Invitee timezone. Format: IANA timezone (e.g., "America/New_York")',
|
||||
},
|
||||
inviteeName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Full name of the invitee. Required when a first name is not provided',
|
||||
},
|
||||
inviteeFirstName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'First name of the invitee. Required when a full name is not provided',
|
||||
},
|
||||
inviteeLastName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Last name of the invitee',
|
||||
},
|
||||
textReminderNumber: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Phone number for SMS reminders. Format: E.164 phone number (e.g., "+14155551234")',
|
||||
},
|
||||
eventGuests: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Additional guests to copy on the invite. Format: array of email strings (max 10, e.g., ["guest@example.com"])',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.calendly.com/invitees',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: CalendlyCreateEventInviteeParams) => {
|
||||
if (!params.inviteeName && !params.inviteeFirstName) {
|
||||
throw new Error(
|
||||
'Either "inviteeName" or "inviteeFirstName" is required to book a meeting. Please provide the invitee\'s name.'
|
||||
)
|
||||
}
|
||||
|
||||
const eventGuests = toStringArray(params.eventGuests, 'eventGuests')
|
||||
|
||||
return {
|
||||
event_type: toResourceUri(params.eventTypeUri, 'event_types'),
|
||||
start_time: params.startTime,
|
||||
invitee: {
|
||||
email: params.inviteeEmail,
|
||||
timezone: params.inviteeTimezone,
|
||||
...(params.inviteeName && { name: params.inviteeName }),
|
||||
...(params.inviteeFirstName && { first_name: params.inviteeFirstName }),
|
||||
...(params.inviteeLastName && { last_name: params.inviteeLastName }),
|
||||
...(params.textReminderNumber && { text_reminder_number: params.textReminderNumber }),
|
||||
},
|
||||
...(eventGuests.length > 0 && { event_guests: eventGuests }),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'The invitee created for the booking',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the invitee' },
|
||||
email: { type: 'string', description: 'Invitee email address' },
|
||||
name: { type: 'string', description: 'Invitee full name' },
|
||||
first_name: { type: 'string', description: 'Invitee first name' },
|
||||
last_name: { type: 'string', description: 'Invitee last name' },
|
||||
status: { type: 'string', description: 'Invitee status (active or canceled)' },
|
||||
timezone: { type: 'string', description: 'Invitee timezone' },
|
||||
event: { type: 'string', description: 'URI of the scheduled event that was booked' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when the booking was created' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when the booking was updated' },
|
||||
cancel_url: { type: 'string', description: 'URL to cancel the booking' },
|
||||
reschedule_url: { type: 'string', description: 'URL to reschedule the booking' },
|
||||
rescheduled: { type: 'boolean', description: 'Whether the invitee rescheduled' },
|
||||
text_reminder_number: {
|
||||
type: 'string',
|
||||
description: 'Phone number used for SMS reminders',
|
||||
},
|
||||
questions_and_answers: {
|
||||
type: 'array',
|
||||
description: 'Responses to custom questions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: { type: 'string', description: 'Question text' },
|
||||
answer: { type: 'string', description: 'Invitee answer' },
|
||||
position: { type: 'number', description: 'Question order' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type {
|
||||
CalendlyCreateInviteeNoShowParams,
|
||||
CalendlyCreateInviteeNoShowResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createInviteeNoShowTool: ToolConfig<
|
||||
CalendlyCreateInviteeNoShowParams,
|
||||
CalendlyCreateInviteeNoShowResponse
|
||||
> = {
|
||||
id: 'calendly_create_invitee_no_show',
|
||||
name: 'Calendly Mark Invitee No-Show',
|
||||
description: 'Mark an invitee of a scheduled event as a no-show',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
inviteeUri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Full invitee URI to mark as a no-show. Format: URI (e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.calendly.com/invitee_no_shows',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: CalendlyCreateInviteeNoShowParams) => ({
|
||||
invitee: params.inviteeUri.trim(),
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'The created no-show record',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the no-show' },
|
||||
invitee: { type: 'string', description: 'URI of the invitee marked as a no-show' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when the no-show was recorded' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type {
|
||||
CalendlyCreateSchedulingLinkParams,
|
||||
CalendlyCreateSchedulingLinkResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
/** Calendly only supports single-use links today, so the event count is fixed at one. */
|
||||
const MAX_EVENT_COUNT = 1
|
||||
|
||||
export const createSchedulingLinkTool: ToolConfig<
|
||||
CalendlyCreateSchedulingLinkParams,
|
||||
CalendlyCreateSchedulingLinkResponse
|
||||
> = {
|
||||
id: 'calendly_create_scheduling_link',
|
||||
name: 'Calendly Create Scheduling Link',
|
||||
description: 'Create a single-use scheduling link for an event type',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventTypeUri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Event type the link books. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/event_types/abc123-def456")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => 'https://api.calendly.com/scheduling_links',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: CalendlyCreateSchedulingLinkParams) => ({
|
||||
max_event_count: MAX_EVENT_COUNT,
|
||||
owner: toResourceUri(params.eventTypeUri, 'event_types'),
|
||||
owner_type: 'EventType',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'The created scheduling link',
|
||||
properties: {
|
||||
booking_url: { type: 'string', description: 'Single-use URL to share with an invitee' },
|
||||
owner: { type: 'string', description: 'URI of the event type that owns the link' },
|
||||
owner_type: { type: 'string', description: 'Resource type of the owner' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyCreateWebhookParams,
|
||||
CalendlyCreateWebhookResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri, toStringArray } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createWebhookTool: ToolConfig<
|
||||
@@ -71,13 +72,13 @@ export const createWebhookTool: ToolConfig<
|
||||
body: (params: CalendlyCreateWebhookParams) => {
|
||||
const body: any = {
|
||||
url: params.url,
|
||||
events: params.events,
|
||||
organization: params.organization,
|
||||
events: toStringArray(params.events, 'events'),
|
||||
organization: toResourceUri(params.organization, 'organizations'),
|
||||
scope: params.scope,
|
||||
}
|
||||
|
||||
if (params.user && params.scope === 'user') {
|
||||
body.user = params.user
|
||||
body.user = toResourceUri(params.user, 'users')
|
||||
}
|
||||
|
||||
if (params.signing_key) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
CalendlyDeleteInviteeNoShowParams,
|
||||
CalendlyDeleteInviteeNoShowResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteInviteeNoShowTool: ToolConfig<
|
||||
CalendlyDeleteInviteeNoShowParams,
|
||||
CalendlyDeleteInviteeNoShowResponse
|
||||
> = {
|
||||
id: 'calendly_delete_invitee_no_show',
|
||||
name: 'Calendly Unmark Invitee No-Show',
|
||||
description: 'Remove the no-show status from an invitee',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
noShowUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'No-show UUID to remove. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/invitee_no_shows/abc123-def456")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyDeleteInviteeNoShowParams) =>
|
||||
`https://api.calendly.com/invitee_no_shows/${toUuid(params.noShowUuid)}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: 'No-show status removed successfully',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
deleted: false,
|
||||
message: data.message || 'Failed to remove no-show status',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the no-show status was successfully removed',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Status message',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyDeleteWebhookParams,
|
||||
CalendlyDeleteWebhookResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteWebhookTool: ToolConfig<
|
||||
@@ -31,9 +32,7 @@ export const deleteWebhookTool: ToolConfig<
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyDeleteWebhookParams) => {
|
||||
const uuid = params.webhookUuid.includes('/')
|
||||
? params.webhookUuid.split('/').pop()
|
||||
: params.webhookUuid
|
||||
const uuid = toUuid(params.webhookUuid)
|
||||
return `https://api.calendly.com/webhook_subscriptions/${uuid}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import type {
|
||||
CalendlyGetEventInviteeParams,
|
||||
CalendlyGetEventInviteeResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getEventInviteeTool: ToolConfig<
|
||||
CalendlyGetEventInviteeParams,
|
||||
CalendlyGetEventInviteeResponse
|
||||
> = {
|
||||
id: 'calendly_get_event_invitee',
|
||||
name: 'Calendly Get Event Invitee',
|
||||
description: 'Retrieve a single invitee of a scheduled event, including their intake answers',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Scheduled event UUID. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/scheduled_events/abc123-def456")',
|
||||
},
|
||||
inviteeUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Invitee UUID. Format: UUID (e.g., "abc123-def456") or full invitee URI (e.g., "https://api.calendly.com/scheduled_events/abc123/invitees/def456")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyGetEventInviteeParams) =>
|
||||
`https://api.calendly.com/scheduled_events/${toUuid(params.eventUuid)}/invitees/${toUuid(params.inviteeUuid)}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'Invitee details',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the invitee' },
|
||||
email: { type: 'string', description: 'Invitee email address' },
|
||||
name: { type: 'string', description: 'Invitee full name' },
|
||||
first_name: { type: 'string', description: 'Invitee first name' },
|
||||
last_name: { type: 'string', description: 'Invitee last name' },
|
||||
status: { type: 'string', description: 'Invitee status (active or canceled)' },
|
||||
timezone: { type: 'string', description: 'Invitee timezone' },
|
||||
event: { type: 'string', description: 'URI of the scheduled event' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when invitee was created' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when invitee was updated' },
|
||||
cancel_url: { type: 'string', description: 'URL to cancel the booking' },
|
||||
reschedule_url: { type: 'string', description: 'URL to reschedule the booking' },
|
||||
rescheduled: { type: 'boolean', description: 'Whether the invitee rescheduled' },
|
||||
text_reminder_number: {
|
||||
type: 'string',
|
||||
description: 'Phone number used for SMS reminders',
|
||||
},
|
||||
routing_form_submission: {
|
||||
type: 'string',
|
||||
description: 'URI of the routing form submission that produced this booking',
|
||||
},
|
||||
questions_and_answers: {
|
||||
type: 'array',
|
||||
description: 'Responses to custom questions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question: { type: 'string', description: 'Question text' },
|
||||
answer: { type: 'string', description: 'Invitee answer' },
|
||||
position: { type: 'number', description: 'Question order' },
|
||||
},
|
||||
},
|
||||
},
|
||||
tracking: {
|
||||
type: 'object',
|
||||
description: 'UTM and Salesforce tracking parameters captured at booking',
|
||||
properties: {
|
||||
utm_campaign: { type: 'string', description: 'UTM campaign' },
|
||||
utm_source: { type: 'string', description: 'UTM source' },
|
||||
utm_medium: { type: 'string', description: 'UTM medium' },
|
||||
utm_content: { type: 'string', description: 'UTM content' },
|
||||
utm_term: { type: 'string', description: 'UTM term' },
|
||||
salesforce_uuid: { type: 'string', description: 'Salesforce record identifier' },
|
||||
},
|
||||
},
|
||||
cancellation: {
|
||||
type: 'object',
|
||||
description: 'Cancellation details when the invitee has canceled',
|
||||
optional: true,
|
||||
properties: {
|
||||
canceled_by: { type: 'string', description: 'Name of person who canceled' },
|
||||
reason: { type: 'string', description: 'Cancellation reason' },
|
||||
canceler_type: { type: 'string', description: 'Type of canceler (host or invitee)' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp of the cancellation' },
|
||||
},
|
||||
},
|
||||
no_show: {
|
||||
type: 'object',
|
||||
description: 'No-show record when the invitee has been marked as a no-show',
|
||||
optional: true,
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the no-show' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when marked as no-show' },
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
type: 'object',
|
||||
description: 'Payment collected at booking',
|
||||
optional: true,
|
||||
properties: {
|
||||
external_id: { type: 'string', description: 'Payment identifier at the provider' },
|
||||
provider: { type: 'string', description: 'Payment provider' },
|
||||
amount: { type: 'number', description: 'Amount charged' },
|
||||
currency: { type: 'string', description: 'Currency code' },
|
||||
terms: { type: 'string', description: 'Payment terms' },
|
||||
successful: { type: 'boolean', description: 'Whether the payment succeeded' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyGetEventTypeParams,
|
||||
CalendlyGetEventTypeResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getEventTypeTool: ToolConfig<
|
||||
@@ -31,9 +32,7 @@ export const getEventTypeTool: ToolConfig<
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyGetEventTypeParams) => {
|
||||
const uuid = params.eventTypeUuid.includes('/')
|
||||
? params.eventTypeUuid.split('/').pop()
|
||||
: params.eventTypeUuid
|
||||
const uuid = toUuid(params.eventTypeUuid)
|
||||
return `https://api.calendly.com/event_types/${uuid}`
|
||||
},
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyGetScheduledEventParams,
|
||||
CalendlyGetScheduledEventResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getScheduledEventTool: ToolConfig<
|
||||
@@ -31,9 +32,7 @@ export const getScheduledEventTool: ToolConfig<
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyGetScheduledEventParams) => {
|
||||
const uuid = params.eventUuid.includes('/')
|
||||
? params.eventUuid.split('/').pop()
|
||||
: params.eventUuid
|
||||
const uuid = toUuid(params.eventUuid)
|
||||
return `https://api.calendly.com/scheduled_events/${uuid}`
|
||||
},
|
||||
method: 'GET',
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { CalendlyGetUserParams, CalendlyGetUserResponse } from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getUserTool: ToolConfig<CalendlyGetUserParams, CalendlyGetUserResponse> = {
|
||||
id: 'calendly_get_user',
|
||||
name: 'Calendly Get User',
|
||||
description: 'Get information about a specific Calendly user',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
userUuid: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'User UUID. Format: UUID (e.g., "abc123-def456"), full URI (e.g., "https://api.calendly.com/users/abc123-def456"), or the constant "me" for the authenticated user',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyGetUserParams) =>
|
||||
`https://api.calendly.com/users/${toUuid(params.userUuid)}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resource: {
|
||||
type: 'object',
|
||||
description: 'User information',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the user' },
|
||||
name: { type: 'string', description: 'User full name' },
|
||||
slug: { type: 'string', description: 'Unique identifier for the user in URLs' },
|
||||
email: { type: 'string', description: 'User email address' },
|
||||
scheduling_url: { type: 'string', description: "URL to the user's scheduling page" },
|
||||
timezone: { type: 'string', description: 'User timezone' },
|
||||
time_notation: { type: 'string', description: 'Time notation preference (12h or 24h)' },
|
||||
avatar_url: { type: 'string', description: 'URL to user avatar image' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when user was created' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when user was last updated' },
|
||||
current_organization: { type: 'string', description: 'URI of current organization' },
|
||||
resource_type: { type: 'string', description: 'Resource type' },
|
||||
locale: { type: 'string', description: 'User locale' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,21 +1,45 @@
|
||||
import { cancelEventTool } from '@/tools/calendly/cancel_event'
|
||||
import { createEventInviteeTool } from '@/tools/calendly/create_event_invitee'
|
||||
import { createInviteeNoShowTool } from '@/tools/calendly/create_invitee_no_show'
|
||||
import { createSchedulingLinkTool } from '@/tools/calendly/create_scheduling_link'
|
||||
import { createWebhookTool } from '@/tools/calendly/create_webhook'
|
||||
import { deleteInviteeNoShowTool } from '@/tools/calendly/delete_invitee_no_show'
|
||||
import { deleteWebhookTool } from '@/tools/calendly/delete_webhook'
|
||||
import { getCurrentUserTool } from '@/tools/calendly/get_current_user'
|
||||
import { getEventInviteeTool } from '@/tools/calendly/get_event_invitee'
|
||||
import { getEventTypeTool } from '@/tools/calendly/get_event_type'
|
||||
import { getScheduledEventTool } from '@/tools/calendly/get_scheduled_event'
|
||||
import { getUserTool } from '@/tools/calendly/get_user'
|
||||
import { listEventInviteesTool } from '@/tools/calendly/list_event_invitees'
|
||||
import { listEventTypeAvailableTimesTool } from '@/tools/calendly/list_event_type_available_times'
|
||||
import { listEventTypesTool } from '@/tools/calendly/list_event_types'
|
||||
import { listOrganizationMembershipsTool } from '@/tools/calendly/list_organization_memberships'
|
||||
import { listRoutingFormSubmissionsTool } from '@/tools/calendly/list_routing_form_submissions'
|
||||
import { listRoutingFormsTool } from '@/tools/calendly/list_routing_forms'
|
||||
import { listScheduledEventsTool } from '@/tools/calendly/list_scheduled_events'
|
||||
import { listUserAvailabilitySchedulesTool } from '@/tools/calendly/list_user_availability_schedules'
|
||||
import { listUserBusyTimesTool } from '@/tools/calendly/list_user_busy_times'
|
||||
import { listWebhooksTool } from '@/tools/calendly/list_webhooks'
|
||||
|
||||
export const calendlyGetCurrentUserTool = getCurrentUserTool
|
||||
export const calendlyGetUserTool = getUserTool
|
||||
export const calendlyListEventTypesTool = listEventTypesTool
|
||||
export const calendlyGetEventTypeTool = getEventTypeTool
|
||||
export const calendlyListEventTypeAvailableTimesTool = listEventTypeAvailableTimesTool
|
||||
export const calendlyListScheduledEventsTool = listScheduledEventsTool
|
||||
export const calendlyGetScheduledEventTool = getScheduledEventTool
|
||||
export const calendlyListEventInviteesTool = listEventInviteesTool
|
||||
export const calendlyGetEventInviteeTool = getEventInviteeTool
|
||||
export const calendlyCreateEventInviteeTool = createEventInviteeTool
|
||||
export const calendlyCancelEventTool = cancelEventTool
|
||||
export const calendlyCreateInviteeNoShowTool = createInviteeNoShowTool
|
||||
export const calendlyDeleteInviteeNoShowTool = deleteInviteeNoShowTool
|
||||
export const calendlyCreateSchedulingLinkTool = createSchedulingLinkTool
|
||||
export const calendlyListUserBusyTimesTool = listUserBusyTimesTool
|
||||
export const calendlyListUserAvailabilitySchedulesTool = listUserAvailabilitySchedulesTool
|
||||
export const calendlyListOrganizationMembershipsTool = listOrganizationMembershipsTool
|
||||
export const calendlyListRoutingFormsTool = listRoutingFormsTool
|
||||
export const calendlyListRoutingFormSubmissionsTool = listRoutingFormSubmissionsTool
|
||||
export const calendlyListWebhooksTool = listWebhooksTool
|
||||
export const calendlyCreateWebhookTool = createWebhookTool
|
||||
export const calendlyDeleteWebhookTool = deleteWebhookTool
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyListEventInviteesParams,
|
||||
CalendlyListEventInviteesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toUuid } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEventInviteesTool: ToolConfig<
|
||||
@@ -63,9 +64,7 @@ export const listEventInviteesTool: ToolConfig<
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListEventInviteesParams) => {
|
||||
const uuid = params.eventUuid.includes('/')
|
||||
? params.eventUuid.split('/').pop()
|
||||
: params.eventUuid
|
||||
const uuid = toUuid(params.eventUuid)
|
||||
const url = `https://api.calendly.com/scheduled_events/${uuid}/invitees`
|
||||
const queryParams = []
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
CalendlyListEventTypeAvailableTimesParams,
|
||||
CalendlyListEventTypeAvailableTimesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEventTypeAvailableTimesTool: ToolConfig<
|
||||
CalendlyListEventTypeAvailableTimesParams,
|
||||
CalendlyListEventTypeAvailableTimesResponse
|
||||
> = {
|
||||
id: 'calendly_list_event_type_available_times',
|
||||
name: 'Calendly List Event Type Available Times',
|
||||
description: 'Retrieve bookable time slots for an event type within a date range',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
eventTypeUri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Event type to check. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/event_types/abc123-def456")',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the availability range. Cannot be in the past. Format: ISO 8601 (e.g., "2024-01-01T00:00:00Z")',
|
||||
},
|
||||
endTime: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'End of the availability range. Must be in the future and no more than 31 days after the start. Format: ISO 8601 (e.g., "2024-01-15T00:00:00Z")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListEventTypeAvailableTimesParams) => {
|
||||
const queryParams = [
|
||||
`event_type=${encodeURIComponent(toResourceUri(params.eventTypeUri, 'event_types'))}`,
|
||||
`start_time=${encodeURIComponent(params.startTime)}`,
|
||||
`end_time=${encodeURIComponent(params.endTime)}`,
|
||||
]
|
||||
|
||||
return `https://api.calendly.com/event_type_available_times?${queryParams.join('&')}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of available time slots',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: { type: 'string', description: 'Availability status of the slot' },
|
||||
invitees_remaining: {
|
||||
type: 'number',
|
||||
description: 'Number of invitees that can still book this slot',
|
||||
},
|
||||
start_time: { type: 'string', description: 'ISO timestamp of the slot start' },
|
||||
scheduling_url: { type: 'string', description: 'URL that books this exact slot' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyListEventTypesParams,
|
||||
CalendlyListEventTypesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listEventTypesTool: ToolConfig<
|
||||
@@ -69,11 +70,13 @@ export const listEventTypesTool: ToolConfig<
|
||||
const queryParams = []
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(params.user)}`)
|
||||
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
|
||||
}
|
||||
|
||||
if (params.organization) {
|
||||
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
|
||||
queryParams.push(
|
||||
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
|
||||
)
|
||||
}
|
||||
|
||||
if (params.count) {
|
||||
@@ -88,8 +91,8 @@ export const listEventTypesTool: ToolConfig<
|
||||
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
|
||||
}
|
||||
|
||||
if (params.active === true) {
|
||||
queryParams.push('active=true')
|
||||
if (params.active !== undefined) {
|
||||
queryParams.push(`active=${params.active}`)
|
||||
}
|
||||
|
||||
return queryParams.length > 0 ? `${url}?${queryParams.join('&')}` : url
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import type {
|
||||
CalendlyListOrganizationMembershipsParams,
|
||||
CalendlyListOrganizationMembershipsResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listOrganizationMembershipsTool: ToolConfig<
|
||||
CalendlyListOrganizationMembershipsParams,
|
||||
CalendlyListOrganizationMembershipsResponse
|
||||
> = {
|
||||
id: 'calendly_list_organization_memberships',
|
||||
name: 'Calendly List Organization Memberships',
|
||||
description: 'Retrieve the members of an organization, including each member profile and role',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Return memberships that belong to this organization. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/organizations/abc123-def456")',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Return memberships that belong to this user. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/users/abc123-def456")',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Filter memberships by member email address',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by role. Format: "owner", "admin", or "user"',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page. Format: integer (default: 20, max: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Page token for pagination. Format: opaque string from previous response next_page_token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListOrganizationMembershipsParams) => {
|
||||
const url = 'https://api.calendly.com/organization_memberships'
|
||||
const queryParams = []
|
||||
|
||||
if (!params.user && !params.organization) {
|
||||
throw new Error(
|
||||
'At least one of "user" or "organization" parameter is required. Please provide either a user URI or organization URI.'
|
||||
)
|
||||
}
|
||||
|
||||
if (params.organization) {
|
||||
queryParams.push(
|
||||
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
|
||||
)
|
||||
}
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
|
||||
}
|
||||
|
||||
if (params.email) {
|
||||
queryParams.push(`email=${encodeURIComponent(params.email)}`)
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
queryParams.push(`role=${encodeURIComponent(params.role)}`)
|
||||
}
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
return `${url}?${queryParams.join('&')}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of organization membership objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the membership' },
|
||||
role: { type: 'string', description: 'Member role (owner, admin, or user)' },
|
||||
organization: { type: 'string', description: 'URI of the organization' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when the member joined' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when the membership changed' },
|
||||
user: {
|
||||
type: 'object',
|
||||
description: 'The member',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the user' },
|
||||
name: { type: 'string', description: 'User full name' },
|
||||
slug: { type: 'string', description: 'Unique identifier for the user in URLs' },
|
||||
email: { type: 'string', description: 'User email address' },
|
||||
scheduling_url: { type: 'string', description: "URL to the user's scheduling page" },
|
||||
timezone: { type: 'string', description: 'User timezone' },
|
||||
time_notation: {
|
||||
type: 'string',
|
||||
description: 'Time notation preference (12h or 24h)',
|
||||
},
|
||||
avatar_url: { type: 'string', description: 'URL to user avatar image' },
|
||||
locale: { type: 'string', description: 'User locale' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when user was created' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when user was updated' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import type {
|
||||
CalendlyListRoutingFormSubmissionsParams,
|
||||
CalendlyListRoutingFormSubmissionsResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listRoutingFormSubmissionsTool: ToolConfig<
|
||||
CalendlyListRoutingFormSubmissionsParams,
|
||||
CalendlyListRoutingFormSubmissionsResponse
|
||||
> = {
|
||||
id: 'calendly_list_routing_form_submissions',
|
||||
name: 'Calendly List Routing Form Submissions',
|
||||
description: 'Retrieve the submissions of a routing form, including answers and routing result',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
formUri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Routing form whose submissions are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/routing_forms/abc123-def456")',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page. Format: integer (default: 20, max: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Page token for pagination. Format: opaque string from previous response next_page_token',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort order for results. Format: "created_at:direction" (e.g., "created_at:asc", "created_at:desc")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListRoutingFormSubmissionsParams) => {
|
||||
const queryParams = [
|
||||
`form=${encodeURIComponent(toResourceUri(params.formUri, 'routing_forms'))}`,
|
||||
]
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.sort) {
|
||||
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
|
||||
}
|
||||
|
||||
return `https://api.calendly.com/routing_form_submissions?${queryParams.join('&')}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of routing form submission objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the submission' },
|
||||
routing_form: { type: 'string', description: 'URI of the routing form' },
|
||||
submitter: {
|
||||
type: 'string',
|
||||
description: 'URI of the invitee who submitted, when the submission led to a booking',
|
||||
},
|
||||
submitter_type: { type: 'string', description: 'Type of the submitter' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when the form was submitted' },
|
||||
updated_at: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp when the submission was updated',
|
||||
},
|
||||
questions_and_answers: {
|
||||
type: 'array',
|
||||
description: 'Answers given on the routing form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
question_uuid: { type: 'string', description: 'Question identifier' },
|
||||
question: { type: 'string', description: 'Question text' },
|
||||
answer: { type: 'string', description: 'Submitted answer' },
|
||||
},
|
||||
},
|
||||
},
|
||||
tracking: {
|
||||
type: 'object',
|
||||
description: 'UTM and Salesforce tracking parameters captured at submission',
|
||||
properties: {
|
||||
utm_campaign: { type: 'string', description: 'UTM campaign' },
|
||||
utm_source: { type: 'string', description: 'UTM source' },
|
||||
utm_medium: { type: 'string', description: 'UTM medium' },
|
||||
utm_content: { type: 'string', description: 'UTM content' },
|
||||
utm_term: { type: 'string', description: 'UTM term' },
|
||||
salesforce_uuid: { type: 'string', description: 'Salesforce record identifier' },
|
||||
},
|
||||
},
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'Where the submission routed to',
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Routing result type' },
|
||||
value: { type: 'string', description: 'Routing destination' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type {
|
||||
CalendlyListRoutingFormsParams,
|
||||
CalendlyListRoutingFormsResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listRoutingFormsTool: ToolConfig<
|
||||
CalendlyListRoutingFormsParams,
|
||||
CalendlyListRoutingFormsResponse
|
||||
> = {
|
||||
id: 'calendly_list_routing_forms',
|
||||
name: 'Calendly List Routing Forms',
|
||||
description: 'Retrieve the routing forms of an organization, including their questions',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Organization whose routing forms are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/organizations/abc123-def456")',
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results per page. Format: integer (default: 20, max: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Page token for pagination. Format: opaque string from previous response next_page_token',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Sort order for results. Format: "created_at:direction" (e.g., "created_at:asc", "created_at:desc")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListRoutingFormsParams) => {
|
||||
const queryParams = [
|
||||
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`,
|
||||
]
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
}
|
||||
|
||||
if (params.pageToken) {
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.sort) {
|
||||
queryParams.push(`sort=${encodeURIComponent(params.sort)}`)
|
||||
}
|
||||
|
||||
return `https://api.calendly.com/routing_forms?${queryParams.join('&')}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of routing form objects',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the routing form' },
|
||||
organization: { type: 'string', description: 'URI of the owning organization' },
|
||||
name: { type: 'string', description: 'Routing form name' },
|
||||
status: { type: 'string', description: 'Routing form status (published or draft)' },
|
||||
created_at: { type: 'string', description: 'ISO timestamp when the form was created' },
|
||||
updated_at: { type: 'string', description: 'ISO timestamp when the form was updated' },
|
||||
questions: {
|
||||
type: 'array',
|
||||
description: 'Questions asked by the routing form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uuid: { type: 'string', description: 'Question identifier' },
|
||||
name: { type: 'string', description: 'Question text' },
|
||||
type: { type: 'string', description: 'Question answer type' },
|
||||
required: { type: 'boolean', description: 'Whether an answer is required' },
|
||||
answer_choices: {
|
||||
type: 'array',
|
||||
description: 'Selectable answers for choice questions',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Pagination information',
|
||||
properties: {
|
||||
count: { type: 'number', description: 'Number of results in this page' },
|
||||
next_page: { type: 'string', description: 'URL to next page (if available)' },
|
||||
previous_page: { type: 'string', description: 'URL to previous page (if available)' },
|
||||
next_page_token: { type: 'string', description: 'Token for next page' },
|
||||
previous_page_token: { type: 'string', description: 'Token for previous page' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyListScheduledEventsParams,
|
||||
CalendlyListScheduledEventsResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listScheduledEventsTool: ToolConfig<
|
||||
@@ -94,11 +95,13 @@ export const listScheduledEventsTool: ToolConfig<
|
||||
}
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(params.user)}`)
|
||||
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
|
||||
}
|
||||
|
||||
if (params.organization) {
|
||||
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
|
||||
queryParams.push(
|
||||
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
|
||||
)
|
||||
}
|
||||
|
||||
if (params.invitee_email) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type {
|
||||
CalendlyListUserAvailabilitySchedulesParams,
|
||||
CalendlyListUserAvailabilitySchedulesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listUserAvailabilitySchedulesTool: ToolConfig<
|
||||
CalendlyListUserAvailabilitySchedulesParams,
|
||||
CalendlyListUserAvailabilitySchedulesResponse
|
||||
> = {
|
||||
id: 'calendly_list_user_availability_schedules',
|
||||
name: 'Calendly List User Availability Schedules',
|
||||
description: "Retrieve a user's availability schedules, working hours, and date overrides",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'User whose schedules are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/users/abc123-def456")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListUserAvailabilitySchedulesParams) =>
|
||||
`https://api.calendly.com/user_availability_schedules?user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of availability schedules',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'Canonical reference to the schedule' },
|
||||
name: { type: 'string', description: 'Schedule name' },
|
||||
default: { type: 'boolean', description: "Whether this is the user's default schedule" },
|
||||
user: { type: 'string', description: 'URI of the owning user' },
|
||||
timezone: { type: 'string', description: 'Timezone the schedule is defined in' },
|
||||
rules: {
|
||||
type: 'array',
|
||||
description: 'Weekly rules and date overrides that make up the schedule',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string', description: 'Rule type (wday or date)' },
|
||||
wday: {
|
||||
type: 'string',
|
||||
description: 'Day of week the rule applies to, for wday rules',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Calendar date the rule overrides, for date rules',
|
||||
},
|
||||
intervals: {
|
||||
type: 'array',
|
||||
description: 'Available intervals for the rule; empty means unavailable',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from: { type: 'string', description: 'Interval start time (HH:MM)' },
|
||||
to: { type: 'string', description: 'Interval end time (HH:MM)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type {
|
||||
CalendlyListUserBusyTimesParams,
|
||||
CalendlyListUserBusyTimesResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listUserBusyTimesTool: ToolConfig<
|
||||
CalendlyListUserBusyTimesParams,
|
||||
CalendlyListUserBusyTimesResponse
|
||||
> = {
|
||||
id: 'calendly_list_user_busy_times',
|
||||
name: 'Calendly List User Busy Times',
|
||||
description:
|
||||
"Retrieve a user's internal and external busy times within a date range, based on their connected calendars",
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Calendly Personal Access Token',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'User whose busy times are returned. Format: UUID (e.g., "abc123-def456") or full URI (e.g., "https://api.calendly.com/users/abc123-def456")',
|
||||
},
|
||||
startTime: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start of the requested range. Cannot be in the past. Format: ISO 8601 (e.g., "2024-01-01T00:00:00Z")',
|
||||
},
|
||||
endTime: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'End of the requested range. Must be after the start and no more than 7 days later. Format: ISO 8601 (e.g., "2024-01-07T00:00:00Z")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: CalendlyListUserBusyTimesParams) => {
|
||||
const queryParams = [
|
||||
`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`,
|
||||
`start_time=${encodeURIComponent(params.startTime)}`,
|
||||
`end_time=${encodeURIComponent(params.endTime)}`,
|
||||
]
|
||||
|
||||
return `https://api.calendly.com/user_busy_times?${queryParams.join('&')}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
collection: {
|
||||
type: 'array',
|
||||
description: 'Array of busy time blocks',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Source of the busy block (calendly, external, or reserved)',
|
||||
},
|
||||
start_time: { type: 'string', description: 'ISO timestamp when the block starts' },
|
||||
end_time: { type: 'string', description: 'ISO timestamp when the block ends' },
|
||||
buffered_start_time: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp when the block starts including buffer',
|
||||
optional: true,
|
||||
},
|
||||
buffered_end_time: {
|
||||
type: 'string',
|
||||
description: 'ISO timestamp when the block ends including buffer',
|
||||
optional: true,
|
||||
},
|
||||
event: {
|
||||
type: 'object',
|
||||
description: 'The Calendly event occupying this block',
|
||||
optional: true,
|
||||
properties: {
|
||||
uri: { type: 'string', description: 'URI of the scheduled event' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
CalendlyListWebhooksParams,
|
||||
CalendlyListWebhooksResponse,
|
||||
} from '@/tools/calendly/types'
|
||||
import { toResourceUri } from '@/tools/calendly/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listWebhooksTool: ToolConfig<
|
||||
@@ -42,9 +43,9 @@ export const listWebhooksTool: ToolConfig<
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by scope. Format: "organization" or "user"',
|
||||
description: 'Scope of the webhooks to list. Format: "organization", "user", or "group"',
|
||||
},
|
||||
user: {
|
||||
type: 'string',
|
||||
@@ -60,7 +61,9 @@ export const listWebhooksTool: ToolConfig<
|
||||
const url = 'https://api.calendly.com/webhook_subscriptions'
|
||||
const queryParams = []
|
||||
|
||||
queryParams.push(`organization=${encodeURIComponent(params.organization)}`)
|
||||
queryParams.push(
|
||||
`organization=${encodeURIComponent(toResourceUri(params.organization, 'organizations'))}`
|
||||
)
|
||||
|
||||
if (params.count) {
|
||||
queryParams.push(`count=${Number(params.count)}`)
|
||||
@@ -70,12 +73,10 @@ export const listWebhooksTool: ToolConfig<
|
||||
queryParams.push(`page_token=${encodeURIComponent(params.pageToken)}`)
|
||||
}
|
||||
|
||||
if (params.scope) {
|
||||
queryParams.push(`scope=${encodeURIComponent(params.scope)}`)
|
||||
}
|
||||
queryParams.push(`scope=${encodeURIComponent(params.scope)}`)
|
||||
|
||||
if (params.user) {
|
||||
queryParams.push(`user=${encodeURIComponent(params.user)}`)
|
||||
queryParams.push(`user=${encodeURIComponent(toResourceUri(params.user, 'users'))}`)
|
||||
}
|
||||
|
||||
return `${url}?${queryParams.join('&')}`
|
||||
|
||||
@@ -1,25 +1,49 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/** Shared pagination envelope returned by every Calendly list endpoint. */
|
||||
export interface CalendlyPagination {
|
||||
count: number
|
||||
next_page: string | null
|
||||
previous_page: string | null
|
||||
next_page_token: string | null
|
||||
previous_page_token: string | null
|
||||
}
|
||||
|
||||
/** The User resource, returned by both `/users/me` and `/users/{uuid}`. */
|
||||
export interface CalendlyUserResource {
|
||||
uri: string
|
||||
name: string
|
||||
slug: string
|
||||
email: string
|
||||
scheduling_url: string
|
||||
timezone: string
|
||||
time_notation: string
|
||||
avatar_url: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
current_organization: string
|
||||
resource_type: string
|
||||
locale: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetCurrentUserParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetCurrentUserResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
uri: string
|
||||
name: string
|
||||
slug: string
|
||||
email: string
|
||||
scheduling_url: string
|
||||
timezone: string
|
||||
avatar_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
current_organization: string
|
||||
resource_type: string
|
||||
locale: string
|
||||
}
|
||||
resource: CalendlyUserResource
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyGetUserParams {
|
||||
apiKey: string
|
||||
userUuid: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetUserResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: CalendlyUserResource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,9 +335,9 @@ export interface CalendlyCancelEventResponse extends ToolResponse {
|
||||
export interface CalendlyListWebhooksParams {
|
||||
apiKey: string
|
||||
organization: string
|
||||
scope: string
|
||||
count?: number
|
||||
pageToken?: string
|
||||
scope?: string
|
||||
user?: string
|
||||
}
|
||||
|
||||
@@ -346,7 +370,7 @@ export interface CalendlyListWebhooksResponse extends ToolResponse {
|
||||
export interface CalendlyCreateWebhookParams {
|
||||
apiKey: string
|
||||
url: string
|
||||
events: string[]
|
||||
events: string[] | string
|
||||
organization: string
|
||||
user?: string
|
||||
scope: string
|
||||
@@ -383,3 +407,304 @@ export interface CalendlyDeleteWebhookResponse extends ToolResponse {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
/** The Invitee resource, returned by the single-invitee read and by the booking endpoint. */
|
||||
export interface CalendlyInviteeResource {
|
||||
uri: string
|
||||
email: string
|
||||
name: string
|
||||
first_name: string | null
|
||||
last_name: string | null
|
||||
status: string
|
||||
timezone: string
|
||||
event: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
cancel_url: string
|
||||
reschedule_url: string
|
||||
rescheduled: boolean
|
||||
old_invitee: string | null
|
||||
new_invitee: string | null
|
||||
text_reminder_number: string | null
|
||||
routing_form_submission: string | null
|
||||
scheduling_method: string | null
|
||||
invitee_scheduled_by: string | null
|
||||
questions_and_answers: Array<{
|
||||
question: string
|
||||
answer: string
|
||||
position: number
|
||||
}>
|
||||
tracking: {
|
||||
utm_campaign: string | null
|
||||
utm_source: string | null
|
||||
utm_medium: string | null
|
||||
utm_content: string | null
|
||||
utm_term: string | null
|
||||
salesforce_uuid: string | null
|
||||
}
|
||||
cancellation?: {
|
||||
canceled_by: string
|
||||
reason: string
|
||||
canceler_type: string
|
||||
created_at: string
|
||||
}
|
||||
payment?: {
|
||||
external_id: string
|
||||
provider: string
|
||||
amount: number
|
||||
currency: string
|
||||
terms: string
|
||||
successful: boolean
|
||||
} | null
|
||||
no_show?: {
|
||||
uri: string
|
||||
created_at: string
|
||||
} | null
|
||||
reconfirmation?: {
|
||||
created_at: string
|
||||
confirmed_at: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface CalendlyGetEventInviteeParams {
|
||||
apiKey: string
|
||||
eventUuid: string
|
||||
inviteeUuid: string
|
||||
}
|
||||
|
||||
export interface CalendlyGetEventInviteeResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: CalendlyInviteeResource
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyCreateEventInviteeParams {
|
||||
apiKey: string
|
||||
eventTypeUri: string
|
||||
startTime: string
|
||||
inviteeEmail: string
|
||||
inviteeTimezone: string
|
||||
inviteeName?: string
|
||||
inviteeFirstName?: string
|
||||
inviteeLastName?: string
|
||||
textReminderNumber?: string
|
||||
eventGuests?: string[] | string
|
||||
}
|
||||
|
||||
export interface CalendlyCreateEventInviteeResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: CalendlyInviteeResource
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListEventTypeAvailableTimesParams {
|
||||
apiKey: string
|
||||
eventTypeUri: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
|
||||
export interface CalendlyListEventTypeAvailableTimesResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
status: string
|
||||
invitees_remaining: number
|
||||
start_time: string
|
||||
scheduling_url: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListUserBusyTimesParams {
|
||||
apiKey: string
|
||||
user: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
|
||||
export interface CalendlyListUserBusyTimesResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
type: string
|
||||
start_time: string
|
||||
end_time: string
|
||||
buffered_start_time?: string
|
||||
buffered_end_time?: string
|
||||
event?: {
|
||||
uri: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListUserAvailabilitySchedulesParams {
|
||||
apiKey: string
|
||||
user: string
|
||||
}
|
||||
|
||||
export interface CalendlyListUserAvailabilitySchedulesResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
default: boolean
|
||||
name: string
|
||||
user: string
|
||||
timezone: string
|
||||
rules: Array<{
|
||||
type: string
|
||||
intervals: Array<{
|
||||
from: string
|
||||
to: string
|
||||
}>
|
||||
wday?: string
|
||||
date?: string
|
||||
}>
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyCreateSchedulingLinkParams {
|
||||
apiKey: string
|
||||
eventTypeUri: string
|
||||
}
|
||||
|
||||
export interface CalendlyCreateSchedulingLinkResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
booking_url: string
|
||||
owner: string
|
||||
owner_type: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyCreateInviteeNoShowParams {
|
||||
apiKey: string
|
||||
inviteeUri: string
|
||||
}
|
||||
|
||||
export interface CalendlyCreateInviteeNoShowResponse extends ToolResponse {
|
||||
output: {
|
||||
resource: {
|
||||
uri: string
|
||||
invitee: string
|
||||
created_at: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyDeleteInviteeNoShowParams {
|
||||
apiKey: string
|
||||
noShowUuid: string
|
||||
}
|
||||
|
||||
export interface CalendlyDeleteInviteeNoShowResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListOrganizationMembershipsParams {
|
||||
apiKey: string
|
||||
organization?: string
|
||||
user?: string
|
||||
email?: string
|
||||
role?: string
|
||||
count?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
export interface CalendlyListOrganizationMembershipsResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
role: string
|
||||
organization: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
user: {
|
||||
uri: string
|
||||
name: string
|
||||
slug: string
|
||||
email: string
|
||||
scheduling_url: string
|
||||
timezone: string
|
||||
time_notation: string
|
||||
avatar_url: string | null
|
||||
locale: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
}>
|
||||
pagination: CalendlyPagination
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListRoutingFormsParams {
|
||||
apiKey: string
|
||||
organization: string
|
||||
count?: number
|
||||
pageToken?: string
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export interface CalendlyListRoutingFormsResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
organization: string
|
||||
name: string
|
||||
status: string
|
||||
questions: Array<{
|
||||
uuid: string
|
||||
name: string
|
||||
type: string
|
||||
required: boolean
|
||||
answer_choices: string[] | null
|
||||
}>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}>
|
||||
pagination: CalendlyPagination
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalendlyListRoutingFormSubmissionsParams {
|
||||
apiKey: string
|
||||
formUri: string
|
||||
count?: number
|
||||
pageToken?: string
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export interface CalendlyListRoutingFormSubmissionsResponse extends ToolResponse {
|
||||
output: {
|
||||
collection: Array<{
|
||||
uri: string
|
||||
routing_form: string
|
||||
submitter: string | null
|
||||
submitter_type: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
questions_and_answers: Array<{
|
||||
question_uuid: string
|
||||
question: string
|
||||
answer: string
|
||||
}>
|
||||
tracking: {
|
||||
utm_campaign: string | null
|
||||
utm_source: string | null
|
||||
utm_medium: string | null
|
||||
utm_content: string | null
|
||||
utm_term: string | null
|
||||
salesforce_uuid: string | null
|
||||
}
|
||||
result: {
|
||||
type: string
|
||||
value: string
|
||||
} | null
|
||||
}>
|
||||
pagination: CalendlyPagination
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
|
||||
/**
|
||||
* Calendly addresses every resource both by bare UUID and by canonical URI, and users
|
||||
* paste whichever one they have. Path segments need the UUID, query filters and request
|
||||
* bodies need the URI, so each builder normalizes its input to the form the API expects.
|
||||
*/
|
||||
|
||||
const CALENDLY_API_BASE = 'https://api.calendly.com'
|
||||
|
||||
/** Extracts the trailing UUID from a Calendly URI, or returns an already-bare UUID unchanged. */
|
||||
export function toUuid(value: string): string {
|
||||
const trimmed = value.trim().replace(/\/+$/, '')
|
||||
return trimmed.includes('/') ? (trimmed.split('/').pop() as string) : trimmed
|
||||
}
|
||||
|
||||
/** Expands a bare UUID into a canonical resource URI, or returns an already-full URI unchanged. */
|
||||
export function toResourceUri(value: string, collection: string): string {
|
||||
const trimmed = value.trim().replace(/\/+$/, '')
|
||||
return trimmed.startsWith('http') ? trimmed : `${CALENDLY_API_BASE}/${collection}/${trimmed}`
|
||||
}
|
||||
|
||||
/**
|
||||
* `json` params reach a tool as a parsed array from the block, but as a raw JSON string when the
|
||||
* tool is called directly from the registry, so array bodies normalize both shapes before sending.
|
||||
*/
|
||||
export function toStringArray(value: string[] | string | undefined, field: string): string[] {
|
||||
if (value === undefined) return []
|
||||
if (Array.isArray(value)) return value
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(value)
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input for ${field}: ${getErrorMessage(error)}`)
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error(`Expected ${field} to be an array of strings.`)
|
||||
}
|
||||
|
||||
return parsed as string[]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -426,14 +426,26 @@ import {
|
||||
} from '@/tools/calcom'
|
||||
import {
|
||||
calendlyCancelEventTool,
|
||||
calendlyCreateEventInviteeTool,
|
||||
calendlyCreateInviteeNoShowTool,
|
||||
calendlyCreateSchedulingLinkTool,
|
||||
calendlyCreateWebhookTool,
|
||||
calendlyDeleteInviteeNoShowTool,
|
||||
calendlyDeleteWebhookTool,
|
||||
calendlyGetCurrentUserTool,
|
||||
calendlyGetEventInviteeTool,
|
||||
calendlyGetEventTypeTool,
|
||||
calendlyGetScheduledEventTool,
|
||||
calendlyGetUserTool,
|
||||
calendlyListEventInviteesTool,
|
||||
calendlyListEventTypeAvailableTimesTool,
|
||||
calendlyListEventTypesTool,
|
||||
calendlyListOrganizationMembershipsTool,
|
||||
calendlyListRoutingFormSubmissionsTool,
|
||||
calendlyListRoutingFormsTool,
|
||||
calendlyListScheduledEventsTool,
|
||||
calendlyListUserAvailabilitySchedulesTool,
|
||||
calendlyListUserBusyTimesTool,
|
||||
calendlyListWebhooksTool,
|
||||
} from '@/tools/calendly'
|
||||
import { clayPopulateTool } from '@/tools/clay'
|
||||
@@ -6074,12 +6086,24 @@ export const tools: Record<string, ToolConfig> = {
|
||||
tailscale_get_acl: tailscaleGetAclTool,
|
||||
tailscale_set_acl: tailscaleSetAclTool,
|
||||
calendly_get_current_user: calendlyGetCurrentUserTool,
|
||||
calendly_get_user: calendlyGetUserTool,
|
||||
calendly_list_event_types: calendlyListEventTypesTool,
|
||||
calendly_get_event_type: calendlyGetEventTypeTool,
|
||||
calendly_list_event_type_available_times: calendlyListEventTypeAvailableTimesTool,
|
||||
calendly_list_scheduled_events: calendlyListScheduledEventsTool,
|
||||
calendly_get_scheduled_event: calendlyGetScheduledEventTool,
|
||||
calendly_list_event_invitees: calendlyListEventInviteesTool,
|
||||
calendly_get_event_invitee: calendlyGetEventInviteeTool,
|
||||
calendly_create_event_invitee: calendlyCreateEventInviteeTool,
|
||||
calendly_cancel_event: calendlyCancelEventTool,
|
||||
calendly_create_invitee_no_show: calendlyCreateInviteeNoShowTool,
|
||||
calendly_delete_invitee_no_show: calendlyDeleteInviteeNoShowTool,
|
||||
calendly_create_scheduling_link: calendlyCreateSchedulingLinkTool,
|
||||
calendly_list_user_busy_times: calendlyListUserBusyTimesTool,
|
||||
calendly_list_user_availability_schedules: calendlyListUserAvailabilitySchedulesTool,
|
||||
calendly_list_organization_memberships: calendlyListOrganizationMembershipsTool,
|
||||
calendly_list_routing_forms: calendlyListRoutingFormsTool,
|
||||
calendly_list_routing_form_submissions: calendlyListRoutingFormSubmissionsTool,
|
||||
calendly_list_webhooks: calendlyListWebhooksTool,
|
||||
calendly_create_webhook: calendlyCreateWebhookTool,
|
||||
calendly_delete_webhook: calendlyDeleteWebhookTool,
|
||||
|
||||
Reference in New Issue
Block a user