feat(gcal-invite): added google calendar invite tool (#457)

* feat(gcal-invite): added google calendar invite

* fix: addressed comments
This commit is contained in:
Emir Karabeg
2025-06-03 20:10:19 -07:00
committed by GitHub
parent 77bbdc1089
commit c6907c2921
6 changed files with 476 additions and 164 deletions

View File

@@ -2,9 +2,9 @@ import { GoogleCalendarIcon } from '@/components/icons'
import type {
GoogleCalendarCreateResponse,
GoogleCalendarGetResponse,
GoogleCalendarInviteResponse,
GoogleCalendarListResponse,
GoogleCalendarQuickAddResponse,
GoogleCalendarUpdateResponse,
} from '@/tools/google_calendar/types'
import type { BlockConfig } from '../types'
@@ -13,14 +13,14 @@ type GoogleCalendarResponse =
| GoogleCalendarListResponse
| GoogleCalendarGetResponse
| GoogleCalendarQuickAddResponse
| GoogleCalendarUpdateResponse
| GoogleCalendarInviteResponse
export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
type: 'google_calendar',
name: 'Google Calendar',
description: 'Manage Google Calendar events',
longDescription:
'Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication.',
"Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication. Email invitations are sent asynchronously and delivery depends on recipients' Google Calendar settings.",
docsLink: 'https://docs.simstudio.ai/tools/google-calendar',
category: 'tools',
bgColor: '#E0E0E0',
@@ -36,6 +36,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
{ label: 'List Events', id: 'list' },
{ label: 'Get Event', id: 'get' },
{ label: 'Quick Add (Natural Language)', id: 'quick_add' },
{ label: 'Invite Attendees', id: 'invite' },
],
},
{
@@ -133,66 +134,29 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
title: 'Event ID',
type: 'short-input',
layout: 'full',
placeholder: 'Event ID to retrieve',
condition: { field: 'operation', value: 'get' },
placeholder: 'Event ID',
condition: { field: 'operation', value: ['get', 'invite'] },
},
// Update Event Fields
{
id: 'eventId',
title: 'Event ID',
type: 'short-input',
layout: 'full',
placeholder: 'Event ID to update',
condition: { field: 'operation', value: 'update' },
},
{
id: 'summary',
title: 'Event Title',
type: 'short-input',
layout: 'full',
placeholder: 'Updated meeting title',
condition: { field: 'operation', value: 'update' },
},
{
id: 'description',
title: 'Description',
type: 'long-input',
layout: 'full',
placeholder: 'Updated description',
condition: { field: 'operation', value: 'update' },
},
{
id: 'location',
title: 'Location',
type: 'short-input',
layout: 'full',
placeholder: 'Updated location',
condition: { field: 'operation', value: 'update' },
},
{
id: 'startDateTime',
title: 'Start Date & Time',
type: 'short-input',
layout: 'half',
placeholder: '2025-06-03T10:00:00-08:00',
condition: { field: 'operation', value: 'update' },
},
{
id: 'endDateTime',
title: 'End Date & Time',
type: 'short-input',
layout: 'half',
placeholder: '2025-06-03T11:00:00-08:00',
condition: { field: 'operation', value: 'update' },
},
// Invite Attendees Fields
{
id: 'attendees',
title: 'Attendees (comma-separated emails)',
type: 'short-input',
layout: 'full',
placeholder: 'john@example.com, jane@example.com',
condition: { field: 'operation', value: 'update' },
condition: { field: 'operation', value: 'invite' },
},
{
id: 'replaceExisting',
title: 'Replace Existing Attendees',
type: 'dropdown',
layout: 'full',
condition: { field: 'operation', value: 'invite' },
options: [
{ label: 'Add to existing attendees', id: 'false' },
{ label: 'Replace all attendees', id: 'true' },
],
},
// Quick Add Fields
@@ -213,7 +177,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
condition: { field: 'operation', value: 'quick_add' },
},
// Notification setting (for create, update, quick_add)
// Notification setting (for create, quick_add, invite)
{
id: 'sendUpdates',
title: 'Send Email Notifications',
@@ -221,12 +185,12 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
layout: 'full',
condition: {
field: 'operation',
value: ['create', 'update', 'quick_add'],
value: ['create', 'quick_add', 'invite'],
},
options: [
{ label: 'All', id: 'all' },
{ label: 'External Only', id: 'externalOnly' },
{ label: 'None', id: 'none' },
{ label: 'All attendees (recommended)', id: 'all' },
{ label: 'External attendees only', id: 'externalOnly' },
{ label: 'None (no emails sent)', id: 'none' },
],
},
],
@@ -236,6 +200,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
'google_calendar_list',
'google_calendar_get',
'google_calendar_quick_add',
'google_calendar_invite',
],
config: {
tool: (params) => {
@@ -248,12 +213,14 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
return 'google_calendar_get'
case 'quick_add':
return 'google_calendar_quick_add'
case 'invite':
return 'google_calendar_invite'
default:
throw new Error(`Invalid Google Calendar operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, operation, attendees, ...rest } = params
const { credential, operation, attendees, replaceExisting, ...rest } = params
const processedParams = { ...rest }
@@ -270,8 +237,13 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
}
}
// Convert replaceExisting string to boolean for invite operation
if (operation === 'invite' && replaceExisting !== undefined) {
processedParams.replaceExisting = replaceExisting === 'true'
}
// Set default sendUpdates to 'all' if not specified for operations that support it
if (['create', 'update', 'quick_add'].includes(operation) && !processedParams.sendUpdates) {
if (['create', 'quick_add', 'invite'].includes(operation) && !processedParams.sendUpdates) {
processedParams.sendUpdates = 'all'
}
@@ -299,12 +271,15 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
timeMin: { type: 'string', required: false },
timeMax: { type: 'string', required: false },
// Get/Update operation inputs
// Get/Invite operation inputs
eventId: { type: 'string', required: false },
// Quick add inputs
text: { type: 'string', required: false },
// Invite specific inputs
replaceExisting: { type: 'string', required: false },
// Common inputs
sendUpdates: { type: 'string', required: false },
},

View File

@@ -1,9 +1,11 @@
import { createTool } from './create'
import { getTool } from './get'
import { inviteTool } from './invite'
import { listTool } from './list'
import { quickAddTool } from './quick_add'
export const googleCalendarCreateTool = createTool
export const googleCalendarGetTool = getTool
export const googleCalendarInviteTool = inviteTool
export const googleCalendarListTool = listTool
export const googleCalendarQuickAddTool = quickAddTool

View File

@@ -0,0 +1,270 @@
import type { ToolConfig } from '../types'
import {
CALENDAR_API_BASE,
type GoogleCalendarInviteParams,
type GoogleCalendarInviteResponse,
} from './types'
export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarInviteResponse> = {
id: 'google_calendar_invite',
name: 'Google Calendar Invite Attendees',
description: 'Invite attendees to an existing Google Calendar event',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
additionalScopes: ['https://www.googleapis.com/auth/calendar'],
},
params: {
accessToken: {
type: 'string',
required: true,
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
description: 'Calendar ID (defaults to primary)',
},
eventId: {
type: 'string',
required: true,
description: 'Event ID to invite attendees to',
},
attendees: {
type: 'array',
required: true,
description: 'Array of attendee email addresses to invite',
},
sendUpdates: {
type: 'string',
required: false,
description: 'How to send updates to attendees: all, externalOnly, or none',
},
replaceExisting: {
type: 'boolean',
required: false,
description: 'Whether to replace existing attendees or add to them (defaults to false)',
},
},
request: {
url: (params: GoogleCalendarInviteParams) => {
const calendarId = params.calendarId || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
},
method: 'GET',
headers: (params: GoogleCalendarInviteParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const existingEvent = await response.json()
if (!response.ok) {
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
}
// Validate required fields exist
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
throw new Error('Existing event is missing required fields (start, end, or summary)')
}
// Process new attendees - handle both string and array formats
let newAttendeeList: string[] = []
if (params?.attendees) {
if (Array.isArray(params.attendees)) {
// Already an array from block processing
newAttendeeList = params.attendees.filter(
(email: string) => email && email.trim().length > 0
)
} else if (
typeof (params.attendees as any) === 'string' &&
(params.attendees as any).trim().length > 0
) {
// Fallback: process comma-separated string if block didn't convert it
newAttendeeList = (params.attendees as any)
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
}
// Calculate final attendees list
const existingAttendees = existingEvent.attendees || []
let finalAttendees: Array<any> = []
// Handle replaceExisting properly - check for both boolean true and string "true"
const shouldReplace =
params?.replaceExisting === true || (params?.replaceExisting as any) === 'true'
if (shouldReplace) {
// Replace all attendees with just the new ones
finalAttendees = newAttendeeList.map((email: string) => ({
email,
responseStatus: 'needsAction',
}))
} else {
// Add to existing attendees (preserve all existing ones)
// Start with ALL existing attendees - preserve them completely
finalAttendees = [...existingAttendees]
// Get set of existing emails for duplicate checking (case-insensitive)
const existingEmails = new Set(
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
)
// Add only new attendees that don't already exist
for (const newEmail of newAttendeeList) {
const emailLower = newEmail.toLowerCase()
if (!existingEmails.has(emailLower)) {
finalAttendees.push({
email: newEmail,
responseStatus: 'needsAction',
})
}
}
}
// Use the complete existing event object and only modify the attendees field
// This is crucial because the Google Calendar API update method "does not support patch semantics
// and always updates the entire event resource" according to the documentation
const updatedEvent = {
...existingEvent, // Start with the complete existing event to preserve all fields
attendees: finalAttendees, // Only modify the attendees field
}
// Remove read-only fields that shouldn't be included in updates
const readOnlyFields = [
'id',
'etag',
'kind',
'created',
'updated',
'htmlLink',
'iCalUID',
'sequence',
'creator',
'organizer',
]
readOnlyFields.forEach((field) => {
delete updatedEvent[field]
})
// Construct PUT URL with query parameters
const calendarId = params?.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params?.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
const queryString = queryParams.toString()
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
// Send PUT request to update the event
const putResponse = await fetch(putUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedEvent),
})
// Handle the PUT response
if (!putResponse.ok) {
const errorData = await putResponse.json()
throw new Error(errorData.error?.message || 'Failed to invite attendees to calendar event')
}
const data = await putResponse.json()
const totalAttendees = data.attendees?.length || 0
// Calculate how many new attendees were actually added
let newAttendeesAdded = 0
if (shouldReplace) {
newAttendeesAdded = newAttendeeList.length
} else {
// Count how many of the new emails weren't already in the existing list
const existingEmails = new Set(
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
)
newAttendeesAdded = newAttendeeList.filter(
(email) => !existingEmails.has(email.toLowerCase())
).length
}
// Improved messaging about email delivery
let baseMessage: string
if (shouldReplace) {
baseMessage = `Successfully updated event "${data.summary}" with ${totalAttendees} attendee${totalAttendees !== 1 ? 's' : ''}`
} else {
if (newAttendeesAdded > 0) {
baseMessage = `Successfully added ${newAttendeesAdded} new attendee${newAttendeesAdded !== 1 ? 's' : ''} to event "${data.summary}" (total: ${totalAttendees})`
} else {
baseMessage = `No new attendees added to event "${data.summary}" - all specified attendees were already invited (total: ${totalAttendees})`
}
}
const emailNote =
params?.sendUpdates !== 'none'
? ` Email invitations are being sent asynchronously - delivery may take a few minutes and depends on recipients' Google Calendar settings.`
: ` No email notifications will be sent as requested.`
const content = baseMessage + emailNote
return {
success: true,
output: {
content,
metadata: {
id: data.id,
htmlLink: data.htmlLink,
status: data.status,
summary: data.summary,
description: data.description,
location: data.location,
start: data.start,
end: data.end,
attendees: data.attendees,
creator: data.creator,
organizer: data.organizer,
},
},
}
},
transformError: (error) => {
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'
}
if (error.error.message.includes('quota')) {
return 'Google Calendar API quota exceeded. Please try again later.'
}
if (error.error.message.includes('Calendar not found')) {
return 'Calendar not found. Please check the calendar ID.'
}
if (
error.error.message.includes('Event not found') ||
error.error.message.includes('Not Found')
) {
return 'Event not found. Please check the event ID.'
}
if (error.error.message.includes('Failed to fetch existing event')) {
return `Unable to retrieve existing event details: ${error.error.message}`
}
return error.error.message
}
return (
error.message || 'An unexpected error occurred while inviting attendees to the calendar event'
)
},
}

View File

@@ -2,6 +2,20 @@ import type { ToolResponse } from '../types'
export const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'
// Shared attendee interface that matches Google Calendar API specification
export interface CalendarAttendee {
id?: string
email: string
displayName?: string
organizer?: boolean
self?: boolean
resource?: boolean
optional?: boolean
responseStatus: string
comment?: string
additionalGuests?: number
}
interface BaseGoogleCalendarParams {
accessToken: string
calendarId?: string // defaults to 'primary' if not provided
@@ -54,6 +68,13 @@ export interface GoogleCalendarQuickAddParams extends BaseGoogleCalendarParams {
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarInviteParams extends BaseGoogleCalendarParams {
eventId: string
attendees: string[] // Array of email addresses to invite
sendUpdates?: 'all' | 'externalOnly' | 'none'
replaceExisting?: boolean // Whether to replace existing attendees or add to them
}
export type GoogleCalendarToolParams =
| GoogleCalendarCreateParams
| GoogleCalendarListParams
@@ -61,6 +82,7 @@ export type GoogleCalendarToolParams =
| GoogleCalendarUpdateParams
| GoogleCalendarDeleteParams
| GoogleCalendarQuickAddParams
| GoogleCalendarInviteParams
interface EventMetadata {
id: string
@@ -79,11 +101,7 @@ interface EventMetadata {
date?: string
timeZone?: string
}
attendees?: Array<{
email: string
displayName?: string
responseStatus: string
}>
attendees?: CalendarAttendee[]
creator?: {
email: string
displayName?: string
@@ -144,6 +162,13 @@ export interface GoogleCalendarUpdateResponse extends ToolResponse {
}
}
export interface GoogleCalendarInviteResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
export interface GoogleCalendarEvent {
id: string
status: string
@@ -163,12 +188,7 @@ export interface GoogleCalendarEvent {
date?: string
timeZone?: string
}
attendees?: Array<{
email: string
displayName?: string
responseStatus: string
optional?: boolean
}>
attendees?: CalendarAttendee[]
creator?: {
email: string
displayName?: string
@@ -222,12 +242,7 @@ export interface GoogleCalendarApiEventResponse {
date?: string
timeZone?: string
}
attendees?: Array<{
email: string
displayName?: string
responseStatus: string
optional?: boolean
}>
attendees?: CalendarAttendee[]
creator?: {
email: string
displayName?: string

View File

@@ -66,7 +66,7 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
attendees: {
type: 'array',
required: false,
description: 'Array of attendee email addresses',
description: 'Array of attendee email addresses (replaces all existing attendees)',
},
sendUpdates: {
type: 'string',
@@ -78,100 +78,145 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
request: {
url: (params: GoogleCalendarUpdateParams) => {
const calendarId = params.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}${queryString ? `?${queryString}` : ''}`
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
},
method: 'PUT',
method: 'GET',
headers: (params: GoogleCalendarUpdateParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarUpdateParams): Record<string, any> => {
const eventData: any = {}
// Only include fields that are provided and not empty
if (params.summary !== undefined && params.summary !== null && params.summary.trim() !== '') {
eventData.summary = params.summary
}
if (
params.description !== undefined &&
params.description !== null &&
params.description.trim() !== ''
) {
eventData.description = params.description
}
if (
params.location !== undefined &&
params.location !== null &&
params.location.trim() !== ''
) {
eventData.location = params.location
}
// Only update times if both start and end are provided (Google Calendar requires both)
const hasStartTime =
params.startDateTime !== undefined &&
params.startDateTime !== null &&
params.startDateTime.trim() !== ''
const hasEndTime =
params.endDateTime !== undefined &&
params.endDateTime !== null &&
params.endDateTime.trim() !== ''
if (hasStartTime && hasEndTime) {
eventData.start = {
dateTime: params.startDateTime,
}
eventData.end = {
dateTime: params.endDateTime,
}
if (params.timeZone) {
eventData.start.timeZone = params.timeZone
eventData.end.timeZone = params.timeZone
}
}
if (params.attendees !== undefined && params.attendees !== null) {
// Handle both string and array cases for attendees
let attendeeList: string[] = []
if (params.attendees) {
const attendees = params.attendees as string | string[]
if (Array.isArray(attendees)) {
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
// Convert comma-separated string to array
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
}
// Only update attendees if we have valid entries, otherwise preserve existing
if (attendeeList.length > 0) {
eventData.attendees = attendeeList.map((email: string) => ({ email }))
}
}
return eventData
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
transformResponse: async (response: Response, params) => {
const existingEvent = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to update calendar event')
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
}
// Start with the complete existing event to preserve all fields
const updatedEvent = { ...existingEvent }
// Apply updates only for fields that are provided and not empty
if (
params?.summary !== undefined &&
params?.summary !== null &&
params?.summary.trim() !== ''
) {
updatedEvent.summary = params.summary
}
if (
params?.description !== undefined &&
params?.description !== null &&
params?.description.trim() !== ''
) {
updatedEvent.description = params.description
}
if (
params?.location !== undefined &&
params?.location !== null &&
params?.location.trim() !== ''
) {
updatedEvent.location = params.location
}
// Only update times if both start and end are provided (Google Calendar requires both)
const hasStartTime =
params?.startDateTime !== undefined &&
params?.startDateTime !== null &&
params?.startDateTime.trim() !== ''
const hasEndTime =
params?.endDateTime !== undefined &&
params?.endDateTime !== null &&
params?.endDateTime.trim() !== ''
if (hasStartTime && hasEndTime) {
updatedEvent.start = {
dateTime: params.startDateTime,
}
updatedEvent.end = {
dateTime: params.endDateTime,
}
if (params?.timeZone) {
updatedEvent.start.timeZone = params.timeZone
updatedEvent.end.timeZone = params.timeZone
}
}
// Handle attendees update - this replaces all existing attendees
if (params?.attendees !== undefined && params?.attendees !== null) {
let attendeeList: string[] = []
if (params.attendees) {
const attendees = params.attendees as string | string[]
if (Array.isArray(attendees)) {
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
// Convert comma-separated string to array
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
}
// Replace all attendees with the new list
if (attendeeList.length > 0) {
updatedEvent.attendees = attendeeList.map((email: string) => ({
email,
responseStatus: 'needsAction',
}))
} else {
// If empty attendee list is provided, remove all attendees
updatedEvent.attendees = []
}
}
// Remove read-only fields that shouldn't be included in updates
const readOnlyFields = [
'id',
'etag',
'kind',
'created',
'updated',
'htmlLink',
'iCalUID',
'sequence',
'creator',
'organizer',
]
readOnlyFields.forEach((field) => {
delete updatedEvent[field]
})
// Construct PUT URL with query parameters
const calendarId = params?.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params?.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
const queryString = queryParams.toString()
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
// Send PUT request to update the event
const putResponse = await fetch(putUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedEvent),
})
if (!putResponse.ok) {
const errorData = await putResponse.json()
throw new Error(errorData.error?.message || 'Failed to update calendar event')
}
const data = await putResponse.json()
return {
success: true,
output: {
@@ -210,6 +255,9 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
) {
return 'Event not found. Please check the event ID.'
}
if (error.error.message.includes('Failed to fetch existing event')) {
return `Unable to retrieve existing event details: ${error.error.message}`
}
return error.error.message
}
return error.message || 'An unexpected error occurred while updating the calendar event'

View File

@@ -30,6 +30,7 @@ import { searchTool as googleSearchTool } from './google'
import {
googleCalendarCreateTool,
googleCalendarGetTool,
googleCalendarInviteTool,
googleCalendarListTool,
googleCalendarQuickAddTool,
} from './google_calendar'
@@ -211,4 +212,5 @@ export const tools: Record<string, ToolConfig> = {
google_calendar_get: googleCalendarGetTool,
google_calendar_list: googleCalendarListTool,
google_calendar_quick_add: googleCalendarQuickAddTool,
google_calendar_invite: googleCalendarInviteTool,
}