feat(tools): added google calendar tools/block, and gcal selector subblock (#454)

* added google calendar picker, tools, & block

* added attendees list to quick add for gcal

* added docs for gcal

* cleanup

* consolidated utils, additional type safety
This commit is contained in:
Waleed Latif
2025-06-03 15:20:54 -07:00
committed by GitHub
parent fc7171b038
commit 10ab1b4041
21 changed files with 2196 additions and 25 deletions

View File

@@ -0,0 +1,207 @@
---
title: Google Calendar
description: Manage Google Calendar events
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_calendar"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
version='1.1'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
x='0px'
y='0px'
viewBox='0 0 200 200'
enableBackground='new 0 0 200 200'
xmlSpace='preserve'
>
<g>
<g transform='translate(3.75 3.75)'>
<path
fill='#FFFFFF'
d='M148.882,43.618l-47.368-5.263l-57.895,5.263L38.355,96.25l5.263,52.632l52.632,6.579l52.632-6.579
l5.263-53.947L148.882,43.618z'
/>
<path
fill='#1A73E8'
d='M65.211,125.276c-3.934-2.658-6.658-6.539-8.145-11.671l9.132-3.763c0.829,3.158,2.276,5.605,4.342,7.342
c2.053,1.737,4.553,2.592,7.474,2.592c2.987,0,5.553-0.908,7.697-2.724s3.224-4.132,3.224-6.934c0-2.868-1.132-5.211-3.395-7.026
s-5.105-2.724-8.5-2.724h-5.276v-9.039H76.5c2.921,0,5.382-0.789,7.382-2.368c2-1.579,3-3.737,3-6.487
c0-2.447-0.895-4.395-2.684-5.855s-4.053-2.197-6.803-2.197c-2.684,0-4.816,0.711-6.395,2.145s-2.724,3.197-3.447,5.276
l-9.039-3.763c1.197-3.395,3.395-6.395,6.618-8.987c3.224-2.592,7.342-3.895,12.342-3.895c3.697,0,7.026,0.711,9.974,2.145
c2.947,1.434,5.263,3.421,6.934,5.947c1.671,2.539,2.5,5.382,2.5,8.539c0,3.224-0.776,5.947-2.329,8.184
c-1.553,2.237-3.461,3.947-5.724,5.145v0.539c2.987,1.25,5.421,3.158,7.342,5.724c1.908,2.566,2.868,5.632,2.868,9.211
s-0.908,6.776-2.724,9.579c-1.816,2.803-4.329,5.013-7.513,6.618c-3.197,1.605-6.789,2.421-10.776,2.421
C73.408,129.263,69.145,127.934,65.211,125.276z'
/>
<path
fill='#1A73E8'
d='M121.25,79.961l-9.974,7.25l-5.013-7.605l17.987-12.974h6.895v61.197h-9.895L121.25,79.961z'
/>
<path
fill='#EA4335'
d='M148.882,196.25l47.368-47.368l-23.684-10.526l-23.684,10.526l-10.526,23.684L148.882,196.25z'
/>
<path
fill='#34A853'
d='M33.092,172.566l10.526,23.684h105.263v-47.368H43.618L33.092,172.566z'
/>
<path
fill='#4285F4'
d='M12.039-3.75C3.316-3.75-3.75,3.316-3.75,12.039v136.842l23.684,10.526l23.684-10.526V43.618h105.263
l10.526-23.684L148.882-3.75H12.039z'
/>
<path
fill='#188038'
d='M-3.75,148.882v31.579c0,8.724,7.066,15.789,15.789,15.789h31.579v-47.368H-3.75z'
/>
<path
fill='#FBBC04'
d='M148.882,43.618v105.263h47.368V43.618l-23.684-10.526L148.882,43.618z'
/>
<path
fill='#1967D2'
d='M196.25,43.618V12.039c0-8.724-7.066-15.789-15.789-15.789h-31.579v47.368H196.25z'
/>
</g>
</g>
</svg>`}
/>
{/* MANUAL-CONTENT-START:intro */}
[Google Calendar](https://calendar.google.com) is Google's powerful calendar and scheduling service that provides a comprehensive platform for managing events, meetings, and appointments. With seamless integration across Google's ecosystem and widespread adoption, Google Calendar offers robust features for both personal and professional scheduling needs.
With Google Calendar, you can:
- **Create and manage events**: Schedule meetings, appointments, and reminders with detailed information
- **Send calendar invites**: Automatically notify and coordinate with attendees through email invitations
- **Natural language event creation**: Quickly add events using conversational language like "Meeting with John tomorrow at 3pm"
- **View and search events**: Easily find and access your scheduled events across multiple calendars
- **Manage multiple calendars**: Organize different types of events across various calendars
In Sim Studio, the Google Calendar integration enables your agents to programmatically create, read, and manage calendar events. This allows for powerful automation scenarios such as scheduling meetings, sending calendar invites, checking availability, and managing event details. Your agents can create events with natural language input, send automated calendar invitations to attendees, retrieve event information, and list upcoming events. This integration bridges the gap between your AI workflows and calendar management, enabling seamless scheduling automation and coordination with one of the world's most widely used calendar platforms.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
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.
## Tools
### `google_calendar_create`
Create a new event in Google Calendar
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `summary` | string | Yes | Event title/summary |
| `description` | string | No | Event description |
| `location` | string | No | Event location |
| `startDateTime` | string | Yes | Start date and time \(RFC3339 format, e.g., 2025-06-03T10:00:00-08:00\) |
| `endDateTime` | string | Yes | End date and time \(RFC3339 format, e.g., 2025-06-03T11:00:00-08:00\) |
| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\) |
| `attendees` | array | No | Array of attendee email addresses |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
#### Output
| Parameter | Type |
| --------- | ---- |
| `content` | string |
### `google_calendar_list`
List events from Google Calendar
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `timeMin` | string | No | Lower bound for events \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
| `timeMax` | string | No | Upper bound for events \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
| `orderBy` | string | No | Order of events returned \(startTime or updated\) |
| `showDeleted` | boolean | No | Include deleted events |
#### Output
| Parameter | Type |
| --------- | ---- |
| `content` | string |
### `google_calendar_get`
Get a specific event from Google Calendar
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `eventId` | string | Yes | Event ID to retrieve |
#### Output
| Parameter | Type |
| --------- | ---- |
| `content` | string |
### `google_calendar_quick_add`
Create events from natural language text
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `accessToken` | string | Yes | Access token for Google Calendar API |
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
| `text` | string | Yes | Natural language text describing the event \(e.g., |
| `attendees` | array | No | Array of attendee email addresses \(comma-separated string also accepted\) |
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
#### Output
| Parameter | Type |
| --------- | ---- |
| `content` | string |
## Block Configuration
### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `operation` | string | Yes | Operation |
### Outputs
| Output | Type | Description |
| ------ | ---- | ----------- |
| `response` | object | Output from response |
| ↳ `content` | string | content of the response |
| ↳ `metadata` | json | metadata of the response |
## Notes
- Category: `tools`
- Type: `google_calendar`

View File

@@ -46,6 +46,7 @@ Key features of the Knowledge Base include:
In Sim Studio, the Knowledge Base block enables your agents to perform intelligent semantic searches across your custom knowledge bases. This creates opportunities for automated information retrieval, content recommendations, and knowledge discovery as part of your AI workflows. The integration allows agents to search and retrieve relevant information programmatically, facilitating automated knowledge management tasks and ensuring that important information is easily accessible. By leveraging the Knowledge Base block, you can build intelligent agents that enhance information discovery while automating routine knowledge management tasks, improving team efficiency and ensuring consistent access to organizational knowledge.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Perform semantic vector search across your knowledge base to find the most relevant content. Uses advanced AI embeddings to understand meaning and context, returning the most similar documents to your search query.

View File

@@ -13,6 +13,7 @@
"firecrawl",
"github",
"gmail",
"google_calendar",
"google_docs",
"google_drive",
"google_search",

View File

@@ -0,0 +1,130 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('GoogleCalendarAPI')
interface CalendarListItem {
id: string
summary: string
description?: string
primary?: boolean
accessRole: string
backgroundColor?: string
foregroundColor?: string
}
/**
* Get calendars from Google Calendar
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
logger.info(`[${requestId}] Google Calendar calendars request received`)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get the credential ID from the query params
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
if (!credentialId) {
logger.warn(`[${requestId}] Missing credentialId parameter`)
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
// Get the credential from the database
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
if (!credentials.length) {
logger.warn(`[${requestId}] Credential not found`, { credentialId })
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
}
const credential = credentials[0]
// Check if the credential belongs to the user
if (credential.userId !== session.user.id) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, {
credentialUserId: credential.userId,
requestUserId: session.user.id,
})
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(credentialId, session.user.id, requestId)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch calendars from Google Calendar API
logger.info(`[${requestId}] Fetching calendars from Google Calendar API`)
const calendarResponse = await fetch(
'https://www.googleapis.com/calendar/v3/users/me/calendarList',
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!calendarResponse.ok) {
const errorData = await calendarResponse
.text()
.then((text) => JSON.parse(text))
.catch(() => ({ error: { message: 'Unknown error' } }))
logger.error(`[${requestId}] Google Calendar API error`, {
status: calendarResponse.status,
error: errorData.error?.message || 'Failed to fetch calendars',
})
return NextResponse.json(
{ error: errorData.error?.message || 'Failed to fetch calendars' },
{ status: calendarResponse.status }
)
}
const data = await calendarResponse.json()
const calendars: CalendarListItem[] = data.items || []
// Sort calendars with primary first, then alphabetically
calendars.sort((a, b) => {
if (a.primary && !b.primary) return -1
if (!a.primary && b.primary) return 1
return a.summary.localeCompare(b.summary)
})
logger.info(`[${requestId}] Successfully fetched ${calendars.length} calendars`)
return NextResponse.json({
calendars: calendars.map((calendar) => ({
id: calendar.id,
summary: calendar.summary,
description: calendar.description,
primary: calendar.primary || false,
accessRole: calendar.accessRole,
backgroundColor: calendar.backgroundColor,
foregroundColor: calendar.foregroundColor,
})),
})
} catch (error) {
logger.error(`[${requestId}] Error fetching Google calendars`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,329 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { Check, ChevronDown, RefreshCw, X } from 'lucide-react'
import { GoogleCalendarIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('GoogleCalendarSelector')
export interface GoogleCalendarInfo {
id: string
summary: string
description?: string
primary?: boolean
accessRole: string
backgroundColor?: string
foregroundColor?: string
}
interface GoogleCalendarSelectorProps {
value: string
onChange: (value: string, calendarInfo?: GoogleCalendarInfo) => void
label?: string
disabled?: boolean
showPreview?: boolean
onCalendarInfoChange?: (info: GoogleCalendarInfo | null) => void
credentialId: string
}
export function GoogleCalendarSelector({
value,
onChange,
label = 'Select Google Calendar',
disabled = false,
showPreview = true,
onCalendarInfoChange,
credentialId,
}: GoogleCalendarSelectorProps) {
const [open, setOpen] = useState(false)
const [calendars, setCalendars] = useState<GoogleCalendarInfo[]>([])
const [selectedCalendarId, setSelectedCalendarId] = useState(value)
const [selectedCalendar, setSelectedCalendar] = useState<GoogleCalendarInfo | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [initialFetchDone, setInitialFetchDone] = useState(false)
const fetchCalendarsFromAPI = useCallback(async (): Promise<GoogleCalendarInfo[]> => {
if (!credentialId) {
throw new Error('Google Calendar account is required')
}
const queryParams = new URLSearchParams({
credentialId: credentialId,
})
const response = await fetch(`/api/tools/google_calendar/calendars?${queryParams.toString()}`)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to fetch Google Calendar calendars')
}
const data = await response.json()
return data.calendars || []
}, [credentialId])
const fetchCalendars = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const calendars = await fetchCalendarsFromAPI()
setCalendars(calendars)
const currentSelectedId = selectedCalendarId
if (currentSelectedId) {
const calendarInfo = calendars.find(
(calendar: GoogleCalendarInfo) => calendar.id === currentSelectedId
)
if (calendarInfo) {
setSelectedCalendar(calendarInfo)
onCalendarInfoChange?.(calendarInfo)
}
}
} catch (error) {
logger.error('Error fetching calendars:', error)
setError((error as Error).message)
setCalendars([])
} finally {
setIsLoading(false)
setInitialFetchDone(true)
}
}, [fetchCalendarsFromAPI, selectedCalendarId, onCalendarInfoChange])
const handleOpenChange = (isOpen: boolean) => {
setOpen(isOpen)
if (isOpen && credentialId && (!initialFetchDone || calendars.length === 0)) {
fetchCalendars()
}
}
const fetchSelectedCalendarInfo = useCallback(async () => {
if (!selectedCalendarId) return
setIsLoading(true)
setError(null)
try {
const calendars = await fetchCalendarsFromAPI()
if (calendars.length > 0) {
const calendarInfo = calendars.find(
(calendar: GoogleCalendarInfo) => calendar.id === selectedCalendarId
)
if (calendarInfo) {
setSelectedCalendar(calendarInfo)
onCalendarInfoChange?.(calendarInfo)
}
}
} catch (error) {
logger.error('Error fetching calendar info:', error)
setError((error as Error).message)
} finally {
setIsLoading(false)
}
}, [fetchCalendarsFromAPI, selectedCalendarId, onCalendarInfoChange])
// Fetch selected calendar info when component mounts or dependencies change
useEffect(() => {
if (value && credentialId && (!selectedCalendar || selectedCalendar.id !== value)) {
fetchSelectedCalendarInfo()
}
}, [value, credentialId, selectedCalendar, fetchSelectedCalendarInfo])
// Sync with external value
useEffect(() => {
if (value !== selectedCalendarId) {
setSelectedCalendarId(value)
// Find calendar info for the new value
if (value && calendars.length > 0) {
const calendarInfo = calendars.find((calendar) => calendar.id === value)
setSelectedCalendar(calendarInfo || null)
onCalendarInfoChange?.(calendarInfo || null)
} else if (value) {
// If we have a value but no calendar info, we might need to fetch it
if (!selectedCalendar || selectedCalendar.id !== value) {
fetchSelectedCalendarInfo()
}
} else {
setSelectedCalendar(null)
onCalendarInfoChange?.(null)
}
}
}, [
value,
calendars,
selectedCalendarId,
selectedCalendar,
fetchSelectedCalendarInfo,
onCalendarInfoChange,
])
// Handle calendar selection
const handleSelectCalendar = (calendar: GoogleCalendarInfo) => {
setSelectedCalendarId(calendar.id)
setSelectedCalendar(calendar)
onChange(calendar.id, calendar)
onCalendarInfoChange?.(calendar)
setOpen(false)
}
// Clear selection
const handleClearSelection = () => {
setSelectedCalendarId('')
setSelectedCalendar(null)
onChange('', undefined)
onCalendarInfoChange?.(null)
setError(null)
}
// Get calendar display name
const getCalendarDisplayName = (calendar: GoogleCalendarInfo) => {
if (calendar.primary) {
return `${calendar.summary} (Primary)`
}
return calendar.summary
}
return (
<div className='space-y-2'>
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className='w-full justify-between'
disabled={disabled || !credentialId}
>
{selectedCalendar ? (
<div className='flex items-center gap-2 overflow-hidden'>
<div
className='h-3 w-3 flex-shrink-0 rounded-full'
style={{
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
}}
/>
<span className='truncate font-normal'>
{getCalendarDisplayName(selectedCalendar)}
</span>
</div>
) : (
<div className='flex items-center gap-2'>
<GoogleCalendarIcon className='h-4 w-4' />
<span className='text-muted-foreground'>{label}</span>
</div>
)}
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className='w-[300px] p-0' align='start'>
<Command>
<CommandInput placeholder='Search calendars...' />
<CommandList>
<CommandEmpty>
{isLoading ? (
<div className='flex items-center justify-center p-4'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='ml-2'>Loading calendars...</span>
</div>
) : error ? (
<div className='p-4 text-center'>
<p className='text-destructive text-sm'>{error}</p>
</div>
) : calendars.length === 0 ? (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No calendars found</p>
<p className='text-muted-foreground text-xs'>
Please check your Google Calendar account access
</p>
</div>
) : (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No matching calendars</p>
</div>
)}
</CommandEmpty>
{calendars.length > 0 && (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
Calendars
</div>
{calendars.map((calendar) => (
<CommandItem
key={calendar.id}
value={`calendar-${calendar.id}-${calendar.summary}`}
onSelect={() => handleSelectCalendar(calendar)}
className='cursor-pointer'
>
<div className='flex items-center gap-2 overflow-hidden'>
<div
className='h-3 w-3 flex-shrink-0 rounded-full'
style={{
backgroundColor: calendar.backgroundColor || '#4285f4',
}}
/>
<span className='truncate font-normal'>
{getCalendarDisplayName(calendar)}
</span>
</div>
{calendar.id === selectedCalendarId && <Check className='ml-auto h-4 w-4' />}
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Calendar preview */}
{showPreview && selectedCalendar && (
<div className='relative mt-2 rounded-md border border-muted bg-muted/10 p-2'>
<div className='absolute top-2 right-2'>
<Button
variant='ghost'
size='icon'
className='h-5 w-5 hover:bg-muted'
onClick={handleClearSelection}
>
<X className='h-3 w-3' />
</Button>
</div>
<div className='flex items-center gap-3 pr-4'>
<div className='flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-muted/20'>
<div
className='h-3 w-3 rounded-full'
style={{
backgroundColor: selectedCalendar.backgroundColor || '#4285f4',
}}
/>
</div>
<div className='min-w-0 flex-1 overflow-hidden'>
<h4 className='truncate font-medium text-xs'>
{getCalendarDisplayName(selectedCalendar)}
</h4>
<div className='text-muted-foreground text-xs'>
Access: {selectedCalendar.accessRole}
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -10,6 +10,8 @@ import type { ConfluenceFileInfo } from './components/confluence-file-selector'
import { ConfluenceFileSelector } from './components/confluence-file-selector'
import type { DiscordChannelInfo } from './components/discord-channel-selector'
import { DiscordChannelSelector } from './components/discord-channel-selector'
import type { GoogleCalendarInfo } from './components/google-calendar-selector'
import { GoogleCalendarSelector } from './components/google-calendar-selector'
import type { FileInfo } from './components/google-drive-picker'
import { GoogleDrivePicker } from './components/google-drive-picker'
import type { JiraIssueInfo } from './components/jira-issue-selector'
@@ -44,6 +46,8 @@ export function FileSelectorInput({
const [channelInfo, setChannelInfo] = useState<DiscordChannelInfo | null>(null)
const [selectedMessageId, setSelectedMessageId] = useState<string>('')
const [messageInfo, setMessageInfo] = useState<TeamsMessageInfo | null>(null)
const [selectedCalendarId, setSelectedCalendarId] = useState<string>('')
const [calendarInfo, setCalendarInfo] = useState<GoogleCalendarInfo | null>(null)
// Get provider-specific values
const provider = subBlock.provider || 'google-drive'
@@ -52,6 +56,7 @@ export function FileSelectorInput({
const isDiscord = provider === 'discord'
const isMicrosoftTeams = provider === 'microsoft-teams'
const isMicrosoftExcel = provider === 'microsoft-excel'
const isGoogleCalendar = subBlock.provider || 'google-calendar'
// For Confluence and Jira, we need the domain and credentials
const domain = isConfluence || isJira ? (getValue(blockId, 'domain') as string) || '' : ''
// For Discord, we need the bot token and server ID
@@ -72,6 +77,8 @@ export function FileSelectorInput({
setSelectedChannelId(value)
} else if (isMicrosoftTeams) {
setSelectedMessageId(value)
} else if (isGoogleCalendar) {
setSelectedCalendarId(value)
} else {
setSelectedFileId(value)
}
@@ -85,12 +92,24 @@ export function FileSelectorInput({
setSelectedChannelId(value)
} else if (isMicrosoftTeams) {
setSelectedMessageId(value)
} else if (isGoogleCalendar) {
setSelectedCalendarId(value)
} else {
setSelectedFileId(value)
}
}
}
}, [blockId, subBlock.id, getValue, isJira, isDiscord, isMicrosoftTeams, isPreview, previewValue])
}, [
blockId,
subBlock.id,
getValue,
isJira,
isDiscord,
isMicrosoftTeams,
isGoogleCalendar,
isPreview,
previewValue,
])
// Handle file selection
const handleFileChange = (fileId: string, info?: any) => {
@@ -119,10 +138,47 @@ export function FileSelectorInput({
setValue(blockId, subBlock.id, channelId)
}
// Handle calendar selection
const handleCalendarChange = (calendarId: string, info?: GoogleCalendarInfo) => {
setSelectedCalendarId(calendarId)
setCalendarInfo(info || null)
setValue(blockId, subBlock.id, calendarId)
}
// For Google Drive
const clientId = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
const apiKey = env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
// Render Google Calendar selector
if (isGoogleCalendar) {
const credential = (getValue(blockId, 'credential') as string) || ''
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className='w-full'>
<GoogleCalendarSelector
value={selectedCalendarId}
onChange={handleCalendarChange}
label={subBlock.placeholder || 'Select Google Calendar'}
disabled={disabled || !credential}
showPreview={true}
onCalendarInfoChange={setCalendarInfo}
credentialId={credential}
/>
</div>
</TooltipTrigger>
{!credential && (
<TooltipContent side='top'>
<p>Please select Google Calendar credentials first</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)
}
// Render Discord channel selector
if (isDiscord) {
return (

View File

@@ -0,0 +1,319 @@
import { GoogleCalendarIcon } from '@/components/icons'
import type {
GoogleCalendarCreateResponse,
GoogleCalendarGetResponse,
GoogleCalendarListResponse,
GoogleCalendarQuickAddResponse,
GoogleCalendarUpdateResponse,
} from '@/tools/google_calendar/types'
import type { BlockConfig } from '../types'
type GoogleCalendarResponse =
| GoogleCalendarCreateResponse
| GoogleCalendarListResponse
| GoogleCalendarGetResponse
| GoogleCalendarQuickAddResponse
| GoogleCalendarUpdateResponse
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.',
docsLink: 'https://docs.simstudio.ai/tools/google-calendar',
category: 'tools',
bgColor: '#E0E0E0',
icon: GoogleCalendarIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
layout: 'full',
options: [
{ label: 'Create Event', id: 'create' },
{ label: 'List Events', id: 'list' },
{ label: 'Get Event', id: 'get' },
{ label: 'Quick Add (Natural Language)', id: 'quick_add' },
],
},
{
id: 'credential',
title: 'Google Calendar Account',
type: 'oauth-input',
layout: 'full',
provider: 'google-calendar',
serviceId: 'google-calendar',
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select Google Calendar account',
},
{
id: 'calendarId',
title: 'Calendar',
type: 'file-selector',
layout: 'full',
provider: 'google-calendar',
serviceId: 'google-calendar',
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select calendar',
},
// Create Event Fields
{
id: 'summary',
title: 'Event Title',
type: 'short-input',
layout: 'full',
placeholder: 'Meeting with team',
condition: { field: 'operation', value: 'create' },
},
{
id: 'description',
title: 'Description',
type: 'long-input',
layout: 'full',
placeholder: 'Event description',
condition: { field: 'operation', value: 'create' },
},
{
id: 'location',
title: 'Location',
type: 'short-input',
layout: 'full',
placeholder: 'Conference Room A',
condition: { field: 'operation', value: 'create' },
},
{
id: 'startDateTime',
title: 'Start Date & Time',
type: 'short-input',
layout: 'half',
placeholder: '2025-06-03T10:00:00-08:00',
condition: { field: 'operation', value: 'create' },
},
{
id: 'endDateTime',
title: 'End Date & Time',
type: 'short-input',
layout: 'half',
placeholder: '2025-06-03T11:00:00-08:00',
condition: { field: 'operation', value: 'create' },
},
{
id: 'attendees',
title: 'Attendees (comma-separated emails)',
type: 'short-input',
layout: 'full',
placeholder: 'john@example.com, jane@example.com',
condition: { field: 'operation', value: 'create' },
},
// List Events Fields
{
id: 'timeMin',
title: 'Start Time Filter',
type: 'short-input',
layout: 'half',
placeholder: '2025-06-03T00:00:00Z',
condition: { field: 'operation', value: 'list' },
},
{
id: 'timeMax',
title: 'End Time Filter',
type: 'short-input',
layout: 'half',
placeholder: '2025-06-04T00:00:00Z',
condition: { field: 'operation', value: 'list' },
},
// Get Event Fields
{
id: 'eventId',
title: 'Event ID',
type: 'short-input',
layout: 'full',
placeholder: 'Event ID to retrieve',
condition: { field: 'operation', value: 'get' },
},
// 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' },
},
{
id: 'attendees',
title: 'Attendees (comma-separated emails)',
type: 'short-input',
layout: 'full',
placeholder: 'john@example.com, jane@example.com',
condition: { field: 'operation', value: 'update' },
},
// Quick Add Fields
{
id: 'text',
title: 'Natural Language Event',
type: 'long-input',
layout: 'full',
placeholder: 'Meeting with John tomorrow at 3pm for 1 hour',
condition: { field: 'operation', value: 'quick_add' },
},
{
id: 'attendees',
title: 'Attendees (comma-separated emails)',
type: 'short-input',
layout: 'full',
placeholder: 'john@example.com, jane@example.com',
condition: { field: 'operation', value: 'quick_add' },
},
// Notification setting (for create, update, quick_add)
{
id: 'sendUpdates',
title: 'Send Email Notifications',
type: 'dropdown',
layout: 'full',
condition: {
field: 'operation',
value: ['create', 'update', 'quick_add'],
},
options: [
{ label: 'All', id: 'all' },
{ label: 'External Only', id: 'externalOnly' },
{ label: 'None', id: 'none' },
],
},
],
tools: {
access: [
'google_calendar_create',
'google_calendar_list',
'google_calendar_get',
'google_calendar_quick_add',
],
config: {
tool: (params) => {
switch (params.operation) {
case 'create':
return 'google_calendar_create'
case 'list':
return 'google_calendar_list'
case 'get':
return 'google_calendar_get'
case 'quick_add':
return 'google_calendar_quick_add'
default:
throw new Error(`Invalid Google Calendar operation: ${params.operation}`)
}
},
params: (params) => {
const { credential, operation, attendees, ...rest } = params
const processedParams = { ...rest }
// Convert comma-separated attendees string to array, only if it has content
if (attendees && typeof attendees === 'string' && attendees.trim().length > 0) {
const attendeeList = attendees
.split(',')
.map((email) => email.trim())
.filter((email) => email.length > 0)
// Only add attendees if we have valid entries
if (attendeeList.length > 0) {
processedParams.attendees = attendeeList
}
}
// Set default sendUpdates to 'all' if not specified for operations that support it
if (['create', 'update', 'quick_add'].includes(operation) && !processedParams.sendUpdates) {
processedParams.sendUpdates = 'all'
}
return {
accessToken: credential,
...processedParams,
}
},
},
},
inputs: {
operation: { type: 'string', required: true },
credential: { type: 'string', required: true },
calendarId: { type: 'string', required: false },
// Create operation inputs
summary: { type: 'string', required: false },
description: { type: 'string', required: false },
location: { type: 'string', required: false },
startDateTime: { type: 'string', required: false },
endDateTime: { type: 'string', required: false },
attendees: { type: 'string', required: false },
// List operation inputs
timeMin: { type: 'string', required: false },
timeMax: { type: 'string', required: false },
// Get/Update operation inputs
eventId: { type: 'string', required: false },
// Quick add inputs
text: { type: 'string', required: false },
// Common inputs
sendUpdates: { type: 'string', required: false },
},
outputs: {
response: {
type: {
content: 'string',
metadata: 'json',
},
},
},
}

View File

@@ -21,6 +21,7 @@ import { FunctionBlock } from './blocks/function'
import { GitHubBlock } from './blocks/github'
import { GmailBlock } from './blocks/gmail'
import { GoogleSearchBlock } from './blocks/google'
import { GoogleCalendarBlock } from './blocks/google_calendar'
import { GoogleDocsBlock } from './blocks/google_docs'
import { GoogleDriveBlock } from './blocks/google_drive'
import { GoogleSheetsBlock } from './blocks/google_sheets'
@@ -82,6 +83,7 @@ export const registry: Record<string, BlockConfig> = {
function: FunctionBlock,
github: GitHubBlock,
gmail: GmailBlock,
google_calendar: GoogleCalendarBlock,
google_docs: GoogleDocsBlock,
google_drive: GoogleDriveBlock,
google_search: GoogleSearchBlock,

View File

@@ -1566,25 +1566,65 @@ export function GoogleCalendarIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
aria-label='Google Calendar'
role='img'
viewBox='0 0 512 512'
xmlnsXlink='http://www.w3.org/1999/xlink'
x='0px'
y='0px'
viewBox='0 0 200 200'
enableBackground='new 0 0 200 200'
xmlSpace='preserve'
>
<rect width='512' height='512' rx='15%' fill='#ffffff' />
<path d='M100 340h74V174H340v-74H137Q100 100 100 135' fill='#4285f4' />
<path d='M338 100v76h74v-41q0-35-35-35' fill='#1967d2' />
<path d='M338 174h74V338h-74' fill='#fbbc04' />
<path d='M100 338v39q0 35 35 35h41v-74' fill='#188038' />
<path d='M174 338H338v74H174' fill='#34a853' />
<path d='M338 412v-74h74' fill='#ea4335' />
<path
d='M204 229a25 22 1 1 1 25 27h-9h9a25 22 1 1 1-25 27M270 231l27-19h4v-7V308'
stroke='#4285f4'
strokeWidth='15'
strokeLinejoin='bevel'
fill='none'
/>
<g>
<g transform='translate(3.75 3.75)'>
<path
fill='#FFFFFF'
d='M148.882,43.618l-47.368-5.263l-57.895,5.263L38.355,96.25l5.263,52.632l52.632,6.579l52.632-6.579
l5.263-53.947L148.882,43.618z'
/>
<path
fill='#1A73E8'
d='M65.211,125.276c-3.934-2.658-6.658-6.539-8.145-11.671l9.132-3.763c0.829,3.158,2.276,5.605,4.342,7.342
c2.053,1.737,4.553,2.592,7.474,2.592c2.987,0,5.553-0.908,7.697-2.724s3.224-4.132,3.224-6.934c0-2.868-1.132-5.211-3.395-7.026
s-5.105-2.724-8.5-2.724h-5.276v-9.039H76.5c2.921,0,5.382-0.789,7.382-2.368c2-1.579,3-3.737,3-6.487
c0-2.447-0.895-4.395-2.684-5.855s-4.053-2.197-6.803-2.197c-2.684,0-4.816,0.711-6.395,2.145s-2.724,3.197-3.447,5.276
l-9.039-3.763c1.197-3.395,3.395-6.395,6.618-8.987c3.224-2.592,7.342-3.895,12.342-3.895c3.697,0,7.026,0.711,9.974,2.145
c2.947,1.434,5.263,3.421,6.934,5.947c1.671,2.539,2.5,5.382,2.5,8.539c0,3.224-0.776,5.947-2.329,8.184
c-1.553,2.237-3.461,3.947-5.724,5.145v0.539c2.987,1.25,5.421,3.158,7.342,5.724c1.908,2.566,2.868,5.632,2.868,9.211
s-0.908,6.776-2.724,9.579c-1.816,2.803-4.329,5.013-7.513,6.618c-3.197,1.605-6.789,2.421-10.776,2.421
C73.408,129.263,69.145,127.934,65.211,125.276z'
/>
<path
fill='#1A73E8'
d='M121.25,79.961l-9.974,7.25l-5.013-7.605l17.987-12.974h6.895v61.197h-9.895L121.25,79.961z'
/>
<path
fill='#EA4335'
d='M148.882,196.25l47.368-47.368l-23.684-10.526l-23.684,10.526l-10.526,23.684L148.882,196.25z'
/>
<path
fill='#34A853'
d='M33.092,172.566l10.526,23.684h105.263v-47.368H43.618L33.092,172.566z'
/>
<path
fill='#4285F4'
d='M12.039-3.75C3.316-3.75-3.75,3.316-3.75,12.039v136.842l23.684,10.526l23.684-10.526V43.618h105.263
l10.526-23.684L148.882-3.75H12.039z'
/>
<path
fill='#188038'
d='M-3.75,148.882v31.579c0,8.724,7.066,15.789,15.789,15.789h31.579v-47.368H-3.75z'
/>
<path
fill='#FBBC04'
d='M148.882,43.618v105.263h47.368V43.618l-23.684-10.526L148.882,43.618z'
/>
<path
fill='#1967D2'
d='M196.25,43.618V12.039c0-8.724-7.066-15.789-15.789-15.789h-31.579v47.368H196.25z'
/>
</g>
</g>
</svg>
)
}

View File

@@ -26,7 +26,6 @@ import { env } from './env'
const logger = createLogger('OAuth')
// Define the base OAuth provider type
export type OAuthProvider =
| 'google'
| 'github'
@@ -48,6 +47,7 @@ export type OAuthService =
| 'google-drive'
| 'google-docs'
| 'google-sheets'
| 'google-calendar'
| 'github'
| 'x'
| 'supabase'
@@ -62,7 +62,6 @@ export type OAuthService =
| 'linear'
| 'slack'
// Define the interface for OAuth provider configuration
export interface OAuthProviderConfig {
id: OAuthProvider
name: string
@@ -71,7 +70,6 @@ export interface OAuthProviderConfig {
defaultService: string
}
// Define the interface for OAuth service configuration
export interface OAuthServiceConfig {
id: string
name: string
@@ -82,7 +80,6 @@ export interface OAuthServiceConfig {
scopes: string[]
}
// Define the available OAuth providers
export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
google: {
id: 'google',

View File

@@ -239,7 +239,6 @@ export const gmailReadTool: ToolConfig<GmailReadParams, GmailToolResponse> = {
},
transformError: (error) => {
// Handle Google API error format
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'

View File

@@ -75,7 +75,6 @@ export const gmailSearchTool: ToolConfig<GmailSearchParams, GmailToolResponse> =
},
transformError: (error) => {
// Handle Google API error format
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'

View File

@@ -82,7 +82,6 @@ export const gmailSendTool: ToolConfig<GmailSendParams, GmailToolResponse> = {
},
transformError: (error) => {
// Handle Google API error format
if (error.error?.message) {
if (error.error.message.includes('invalid authentication credentials')) {
return 'Invalid or expired access token. Please reauthenticate.'

View File

@@ -0,0 +1,185 @@
import type { ToolConfig } from '../types'
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
type GoogleCalendarCreateParams,
type GoogleCalendarCreateResponse,
type GoogleCalendarEventRequestBody,
} from './types'
export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCreateResponse> = {
id: 'google_calendar_create',
name: 'Google Calendar Create Event',
description: 'Create a new event in Google Calendar',
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)',
},
summary: {
type: 'string',
required: true,
description: 'Event title/summary',
},
description: {
type: 'string',
required: false,
description: 'Event description',
},
location: {
type: 'string',
required: false,
description: 'Event location',
},
startDateTime: {
type: 'string',
required: true,
description: 'Start date and time (RFC3339 format, e.g., 2025-06-03T10:00:00-08:00)',
},
endDateTime: {
type: 'string',
required: true,
description: 'End date and time (RFC3339 format, e.g., 2025-06-03T11:00:00-08:00)',
},
timeZone: {
type: 'string',
required: false,
description: 'Time zone (e.g., America/Los_Angeles)',
},
attendees: {
type: 'array',
required: false,
description: 'Array of attendee email addresses',
},
sendUpdates: {
type: 'string',
required: false,
description: 'How to send updates to attendees: all, externalOnly, or none',
},
},
request: {
url: (params: GoogleCalendarCreateParams) => {
const calendarId = params.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
const queryString = queryParams.toString()
const finalUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
return finalUrl
},
method: 'POST',
headers: (params: GoogleCalendarCreateParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarCreateParams): GoogleCalendarEventRequestBody => {
const eventData: GoogleCalendarEventRequestBody = {
summary: params.summary,
start: {
dateTime: params.startDateTime,
},
end: {
dateTime: params.endDateTime,
},
}
if (params.description) {
eventData.description = params.description
}
if (params.location) {
eventData.location = params.location
}
if (params.timeZone) {
eventData.start.timeZone = params.timeZone
eventData.end.timeZone = params.timeZone
}
// 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)
}
}
if (attendeeList.length > 0) {
eventData.attendees = attendeeList.map((email: string) => ({ email }))
}
return eventData
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to create calendar event')
}
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
content: `Event "${data.summary}" created successfully`,
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.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while creating the calendar event'
},
}

View File

@@ -0,0 +1,101 @@
import type { ToolConfig } from '../types'
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
type GoogleCalendarGetParams,
type GoogleCalendarGetResponse,
} from './types'
export const getTool: ToolConfig<GoogleCalendarGetParams, GoogleCalendarGetResponse> = {
id: 'google_calendar_get',
name: 'Google Calendar Get Event',
description: 'Get a specific event from Google Calendar',
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 retrieve',
},
},
request: {
url: (params: GoogleCalendarGetParams) => {
const calendarId = params.calendarId || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
},
method: 'GET',
headers: (params: GoogleCalendarGetParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to get calendar event')
}
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
content: `Retrieved event "${data.summary}"`,
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.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while retrieving the calendar event'
},
}

View File

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

View File

@@ -0,0 +1,128 @@
import type { ToolConfig } from '../types'
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
type GoogleCalendarApiListResponse,
type GoogleCalendarListParams,
type GoogleCalendarListResponse,
} from './types'
export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListResponse> = {
id: 'google_calendar_list',
name: 'Google Calendar List Events',
description: 'List events from Google Calendar',
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)',
},
timeMin: {
type: 'string',
required: false,
description: 'Lower bound for events (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)',
},
timeMax: {
type: 'string',
required: false,
description: 'Upper bound for events (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)',
},
orderBy: {
type: 'string',
required: false,
description: 'Order of events returned (startTime or updated)',
},
showDeleted: {
type: 'boolean',
required: false,
description: 'Include deleted events',
},
},
request: {
url: (params: GoogleCalendarListParams) => {
const calendarId = params.calendarId || 'primary'
const queryParams = new URLSearchParams()
if (params.timeMin) queryParams.append('timeMin', params.timeMin)
if (params.timeMax) queryParams.append('timeMax', params.timeMax)
queryParams.append('singleEvents', 'true')
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
if (params.showDeleted !== undefined)
queryParams.append('showDeleted', params.showDeleted.toString())
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params: GoogleCalendarListParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to list calendar events')
}
const data: GoogleCalendarApiListResponse = await response.json()
const events = data.items || []
const eventsCount = events.length
return {
success: true,
output: {
content: `Found ${eventsCount} event${eventsCount !== 1 ? 's' : ''}`,
metadata: {
nextPageToken: data.nextPageToken,
nextSyncToken: data.nextSyncToken,
timeZone: data.timeZone,
events: events.map((event: GoogleCalendarApiEventResponse) => ({
id: event.id,
htmlLink: event.htmlLink,
status: event.status,
summary: event.summary || 'No title',
description: event.description,
location: event.location,
start: event.start,
end: event.end,
attendees: event.attendees,
creator: event.creator,
organizer: event.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.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while listing calendar events'
},
}

View File

@@ -0,0 +1,179 @@
import type { ToolConfig } from '../types'
import {
CALENDAR_API_BASE,
type GoogleCalendarQuickAddParams,
type GoogleCalendarQuickAddResponse,
} from './types'
export const quickAddTool: ToolConfig<
GoogleCalendarQuickAddParams,
GoogleCalendarQuickAddResponse
> = {
id: 'google_calendar_quick_add',
name: 'Google Calendar Quick Add',
description: 'Create events from natural language text',
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)',
},
text: {
type: 'string',
required: true,
description:
'Natural language text describing the event (e.g., "Meeting with John tomorrow at 3pm")',
},
attendees: {
type: 'array',
required: false,
description: 'Array of attendee email addresses (comma-separated string also accepted)',
},
sendUpdates: {
type: 'string',
required: false,
description: 'How to send updates to attendees: all, externalOnly, or none',
},
},
request: {
url: (params: GoogleCalendarQuickAddParams) => {
const calendarId = params.calendarId || 'primary'
const queryParams = new URLSearchParams()
queryParams.append('text', params.text)
if (params.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/quickAdd?${queryParams.toString()}`
},
method: 'POST',
headers: (params: GoogleCalendarQuickAddParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to create calendar event from text')
}
// Handle attendees if provided
let finalEventData = data
if (params?.attendees) {
let attendeeList: string[] = []
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)
}
if (attendeeList.length > 0) {
try {
// Update the event with attendees
const calendarId = params.calendarId || 'primary'
const eventId = data.id
// Prepare update data
const updateData = {
attendees: attendeeList.map((email: string) => ({ email })),
}
// Build update URL with sendUpdates if specified
const updateQueryParams = new URLSearchParams()
if (params.sendUpdates !== undefined) {
updateQueryParams.append('sendUpdates', params.sendUpdates)
}
const updateUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${eventId}${updateQueryParams.toString() ? `?${updateQueryParams.toString()}` : ''}`
// Make the update request
const updateResponse = await fetch(updateUrl, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updateData),
})
if (updateResponse.ok) {
finalEventData = await updateResponse.json()
} else {
// If update fails, we still return the original event but log the error
console.warn(
'Failed to add attendees to quick-added event:',
await updateResponse.text()
)
}
} catch (error) {
// If attendee update fails, we still return the original event
console.warn('Error adding attendees to quick-added event:', error)
}
}
}
return {
success: true,
output: {
content: `Event "${finalEventData?.summary || 'Untitled'}" created successfully ${finalEventData?.attendees?.length ? ` with ${finalEventData.attendees.length} attendee(s)` : ''}`,
metadata: {
id: finalEventData.id,
htmlLink: finalEventData.htmlLink,
status: finalEventData.status,
summary: finalEventData.summary,
description: finalEventData.description,
location: finalEventData.location,
start: finalEventData.start,
end: finalEventData.end,
attendees: finalEventData.attendees,
creator: finalEventData.creator,
organizer: finalEventData.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('parse')) {
return 'Could not parse the natural language text. Please try a different format.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while creating the calendar event'
},
}

View File

@@ -0,0 +1,263 @@
import type { ToolResponse } from '../types'
export const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'
interface BaseGoogleCalendarParams {
accessToken: string
calendarId?: string // defaults to 'primary' if not provided
}
export interface GoogleCalendarCreateParams extends BaseGoogleCalendarParams {
summary: string
description?: string
location?: string
startDateTime: string
endDateTime: string
timeZone?: string
attendees?: string[] // Array of email addresses
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarListParams extends BaseGoogleCalendarParams {
timeMin?: string // RFC3339 timestamp
timeMax?: string // RFC3339 timestamp
maxResults?: number
singleEvents?: boolean
orderBy?: 'startTime' | 'updated'
showDeleted?: boolean
}
export interface GoogleCalendarGetParams extends BaseGoogleCalendarParams {
eventId: string
}
export interface GoogleCalendarUpdateParams extends BaseGoogleCalendarParams {
eventId: string
summary?: string
description?: string
location?: string
startDateTime?: string
endDateTime?: string
timeZone?: string
attendees?: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarDeleteParams extends BaseGoogleCalendarParams {
eventId: string
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarQuickAddParams extends BaseGoogleCalendarParams {
text: string // Natural language text like "Meeting with John tomorrow at 3pm"
attendees?: string[] // Array of email addresses (comma-separated string also accepted)
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export type GoogleCalendarToolParams =
| GoogleCalendarCreateParams
| GoogleCalendarListParams
| GoogleCalendarGetParams
| GoogleCalendarUpdateParams
| GoogleCalendarDeleteParams
| GoogleCalendarQuickAddParams
interface EventMetadata {
id: string
htmlLink: string
status: string
summary: string
description?: string
location?: string
start: {
dateTime?: string
date?: string
timeZone?: string
}
end: {
dateTime?: string
date?: string
timeZone?: string
}
attendees?: Array<{
email: string
displayName?: string
responseStatus: string
}>
creator?: {
email: string
displayName?: string
}
organizer?: {
email: string
displayName?: string
}
}
interface ListMetadata {
nextPageToken?: string
nextSyncToken?: string
events: EventMetadata[]
timeZone: string
}
export interface GoogleCalendarToolResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata | ListMetadata
}
}
// Specific response types for each operation
export interface GoogleCalendarCreateResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
export interface GoogleCalendarListResponse extends ToolResponse {
output: {
content: string
metadata: ListMetadata
}
}
export interface GoogleCalendarGetResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
export interface GoogleCalendarQuickAddResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
export interface GoogleCalendarUpdateResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
export interface GoogleCalendarEvent {
id: string
status: string
htmlLink: string
created: string
updated: string
summary: string
description?: string
location?: string
start: {
dateTime?: string
date?: string
timeZone?: string
}
end: {
dateTime?: string
date?: string
timeZone?: string
}
attendees?: Array<{
email: string
displayName?: string
responseStatus: string
optional?: boolean
}>
creator?: {
email: string
displayName?: string
}
organizer?: {
email: string
displayName?: string
}
reminders?: {
useDefault: boolean
overrides?: Array<{
method: string
minutes: number
}>
}
}
export interface GoogleCalendarEventRequestBody {
summary: string
description?: string
location?: string
start: {
dateTime: string
timeZone?: string
}
end: {
dateTime: string
timeZone?: string
}
attendees?: Array<{
email: string
}>
}
export interface GoogleCalendarApiEventResponse {
id: string
status: string
htmlLink: string
created?: string
updated?: string
summary: string
description?: string
location?: string
start: {
dateTime?: string
date?: string
timeZone?: string
}
end: {
dateTime?: string
date?: string
timeZone?: string
}
attendees?: Array<{
email: string
displayName?: string
responseStatus: string
optional?: boolean
}>
creator?: {
email: string
displayName?: string
}
organizer?: {
email: string
displayName?: string
}
reminders?: {
useDefault: boolean
overrides?: Array<{
method: string
minutes: number
}>
}
}
export interface GoogleCalendarApiListResponse {
kind: string
etag: string
summary: string
description?: string
updated: string
timeZone: string
accessRole: string
defaultReminders: Array<{
method: string
minutes: number
}>
nextPageToken?: string
nextSyncToken?: string
items: GoogleCalendarApiEventResponse[]
}

View File

@@ -0,0 +1,217 @@
import type { ToolConfig } from '../types'
import {
CALENDAR_API_BASE,
type GoogleCalendarToolResponse,
type GoogleCalendarUpdateParams,
} from './types'
export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarToolResponse> = {
id: 'google_calendar_update',
name: 'Google Calendar Update Event',
description: 'Update an existing event in Google Calendar',
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 update',
},
summary: {
type: 'string',
required: false,
description: 'Event title/summary',
},
description: {
type: 'string',
required: false,
description: 'Event description',
},
location: {
type: 'string',
required: false,
description: 'Event location',
},
startDateTime: {
type: 'string',
required: false,
description: 'Start date and time (RFC3339 format, e.g., 2025-06-03T10:00:00-08:00)',
},
endDateTime: {
type: 'string',
required: false,
description: 'End date and time (RFC3339 format, e.g., 2025-06-03T11:00:00-08:00)',
},
timeZone: {
type: 'string',
required: false,
description: 'Time zone (e.g., America/Los_Angeles)',
},
attendees: {
type: 'array',
required: false,
description: 'Array of attendee email addresses',
},
sendUpdates: {
type: 'string',
required: false,
description: 'How to send updates to attendees: all, externalOnly, or none',
},
},
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}` : ''}`
},
method: 'PUT',
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()
if (!response.ok) {
throw new Error(data.error?.message || 'Failed to update calendar event')
}
return {
success: true,
output: {
content: `Event "${data.summary}" updated successfully`,
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.'
}
return error.error.message
}
return error.message || 'An unexpected error occurred while updating the calendar event'
},
}

View File

@@ -27,6 +27,12 @@ import {
} from './github'
import { gmailReadTool, gmailSearchTool, gmailSendTool } from './gmail'
import { searchTool as googleSearchTool } from './google'
import {
googleCalendarCreateTool,
googleCalendarGetTool,
googleCalendarListTool,
googleCalendarQuickAddTool,
} from './google_calendar'
import { googleDocsCreateTool, googleDocsReadTool, googleDocsWriteTool } from './google_docs'
import {
googleDriveCreateFolderTool,
@@ -200,4 +206,8 @@ export const tools: Record<string, ToolConfig> = {
microsoft_excel_read: microsoftExcelReadTool,
microsoft_excel_write: microsoftExcelWriteTool,
microsoft_excel_table_add: microsoftExcelTableAddTool,
google_calendar_create: googleCalendarCreateTool,
google_calendar_get: googleCalendarGetTool,
google_calendar_list: googleCalendarListTool,
google_calendar_quick_add: googleCalendarQuickAddTool,
}