mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
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:
207
apps/docs/content/docs/tools/google_calendar.mdx
Normal file
207
apps/docs/content/docs/tools/google_calendar.mdx
Normal 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`
|
||||
@@ -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.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"firecrawl",
|
||||
"github",
|
||||
"gmail",
|
||||
"google_calendar",
|
||||
"google_docs",
|
||||
"google_drive",
|
||||
"google_search",
|
||||
|
||||
130
apps/sim/app/api/tools/google_calendar/calendars/route.ts
Normal file
130
apps/sim/app/api/tools/google_calendar/calendars/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
319
apps/sim/blocks/blocks/google_calendar.ts
Normal file
319
apps/sim/blocks/blocks/google_calendar.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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.'
|
||||
|
||||
185
apps/sim/tools/google_calendar/create.ts
Normal file
185
apps/sim/tools/google_calendar/create.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
101
apps/sim/tools/google_calendar/get.ts
Normal file
101
apps/sim/tools/google_calendar/get.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
9
apps/sim/tools/google_calendar/index.ts
Normal file
9
apps/sim/tools/google_calendar/index.ts
Normal 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
|
||||
128
apps/sim/tools/google_calendar/list.ts
Normal file
128
apps/sim/tools/google_calendar/list.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
179
apps/sim/tools/google_calendar/quick_add.ts
Normal file
179
apps/sim/tools/google_calendar/quick_add.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
263
apps/sim/tools/google_calendar/types.ts
Normal file
263
apps/sim/tools/google_calendar/types.ts
Normal 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[]
|
||||
}
|
||||
217
apps/sim/tools/google_calendar/update.ts
Normal file
217
apps/sim/tools/google_calendar/update.ts
Normal 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'
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user