chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
import {
CALENDAR_API_BASE,
type CalendarAttendee,
type GoogleCalendarApiEventResponse,
type GoogleCalendarCreateParams,
type GoogleCalendarCreateResponse,
type GoogleCalendarEventRequestBody,
} from '@/tools/google_calendar/types'
import {
assertRecurringTimeZone,
buildEventDateTime,
buildGoogleMeetConferenceData,
normalizeAttendees,
normalizeRecurrence,
} from '@/tools/google_calendar/utils'
import type { ToolConfig } from '@/tools/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',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
summary: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Event title/summary',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Event description',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Event location',
},
startDateTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Start time. Use a datetime with timezone offset (2025-06-03T10:00:00-08:00) or a date (2025-06-03) for an all-day event',
},
endDateTime: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'End time. Use a datetime with timezone offset (2025-06-03T11:00:00-08:00) or a date (2025-06-04) for an all-day event',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'IANA time zone (e.g., America/Los_Angeles). Used as-is when provided. For recurring events a time zone is required to expand the recurrence correctly; for one-off events it is only needed when the datetime omits a UTC offset (a naive datetime defaults to America/Los_Angeles).',
},
attendees: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description: 'Array of attendee email addresses',
},
recurrence: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Recurrence rule(s) in RFC 5545 format (e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR). Separate multiple rules with newlines.',
},
addGoogleMeet: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Attach a Google Meet video conference link to the event',
},
sendUpdates: {
type: 'string',
required: false,
visibility: 'user-only',
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)
}
if (params.addGoogleMeet) {
queryParams.append('conferenceDataVersion', '1')
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
},
method: 'POST',
headers: (params: GoogleCalendarCreateParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarCreateParams): GoogleCalendarEventRequestBody => {
const recurrence = normalizeRecurrence(params.recurrence)
const isRecurring = recurrence.length > 0
if (isRecurring) {
assertRecurringTimeZone([params.startDateTime, params.endDateTime], params.timeZone)
}
const eventData: GoogleCalendarEventRequestBody = {
summary: params.summary,
start: buildEventDateTime(params.startDateTime, params.timeZone),
end: buildEventDateTime(params.endDateTime, params.timeZone),
}
if (params.description) {
eventData.description = params.description
}
if (params.location) {
eventData.location = params.location
}
const attendees = normalizeAttendees(params.attendees)
if (attendees.length > 0) {
eventData.attendees = attendees
}
if (isRecurring) {
eventData.recurrence = recurrence
}
if (params.addGoogleMeet) {
eventData.conferenceData = buildGoogleMeetConferenceData()
}
return eventData
},
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
content: `Event "${data.summary}" created successfully`,
metadata: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink,
status: data.status,
summary: data.summary,
description: data.description,
location: data.location,
recurrence: data.recurrence,
start: data.start,
end: data.end,
attendees: data.attendees,
creator: data.creator,
organizer: data.organizer,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Event creation confirmation message' },
metadata: {
type: 'json',
description: 'Created event metadata including ID, status, Meet link, and details',
},
},
}
interface GoogleCalendarCreateV2Response {
success: boolean
output: {
id: string
htmlLink: string
hangoutLink: string | null
status: string
summary: string | null
description: string | null
location: string | null
recurrence: string[] | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: CalendarAttendee[] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
}
export const createV2Tool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCreateV2Response> =
{
id: 'google_calendar_create_v2',
name: 'Google Calendar Create Event',
description: 'Create a new event in Google Calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: createTool.oauth,
params: createTool.params,
request: createTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink ?? null,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
recurrence: data.recurrence ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator ?? null,
organizer: data.organizer ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
hangoutLink: { type: 'string', description: 'Google Meet link', optional: true },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
recurrence: { type: 'json', description: 'Recurrence rules', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator', optional: true },
organizer: { type: 'json', description: 'Event organizer', optional: true },
},
}
@@ -0,0 +1,142 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiCalendarResponse,
type GoogleCalendarCreateCalendarParams,
type GoogleCalendarCreateCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export const createCalendarTool: ToolConfig<
GoogleCalendarCreateCalendarParams,
GoogleCalendarCreateCalendarResponse
> = {
id: 'google_calendar_create_calendar',
name: 'Google Calendar Create Calendar',
description: 'Create a new secondary calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
summary: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title of the new calendar',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description of the new calendar',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Geographic location of the calendar as free-form text',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Time zone of the calendar as an IANA name (e.g., America/Los_Angeles)',
},
},
request: {
url: () => `${CALENDAR_API_BASE}/calendars`,
method: 'POST',
headers: (params: GoogleCalendarCreateCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarCreateCalendarParams) => {
const body: Record<string, string> = { summary: params.summary }
if (params.description) body.description = params.description
if (params.location) body.location = params.location
if (params.timeZone) body.timeZone = params.timeZone
return body
},
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiCalendarResponse = await response.json()
return {
success: true,
output: {
content: `Calendar "${data.summary}" created successfully`,
metadata: {
id: data.id,
summary: data.summary,
description: data.description,
location: data.location,
timeZone: data.timeZone,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Calendar creation confirmation message' },
metadata: {
type: 'json',
description: 'Created calendar metadata (id, summary, description, location, timeZone)',
},
},
}
interface GoogleCalendarCreateCalendarV2Response {
success: boolean
output: {
id: string
summary: string
description: string | null
location: string | null
timeZone: string | null
}
}
export const createCalendarV2Tool: ToolConfig<
GoogleCalendarCreateCalendarParams,
GoogleCalendarCreateCalendarV2Response
> = {
id: 'google_calendar_create_calendar_v2',
name: 'Google Calendar Create Calendar',
description: 'Create a new secondary calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: createCalendarTool.oauth,
params: createCalendarTool.params,
request: createCalendarTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiCalendarResponse = await response.json()
return {
success: true,
output: {
id: data.id,
summary: data.summary,
description: data.description ?? null,
location: data.location ?? null,
timeZone: data.timeZone ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Calendar ID' },
summary: { type: 'string', description: 'Calendar title' },
description: { type: 'string', description: 'Calendar description', optional: true },
location: { type: 'string', description: 'Calendar location', optional: true },
timeZone: { type: 'string', description: 'Calendar time zone', optional: true },
},
}
+134
View File
@@ -0,0 +1,134 @@
import { CALENDAR_API_BASE, type GoogleCalendarDeleteParams } from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
interface GoogleCalendarDeleteResponse {
success: boolean
output: {
content: string
metadata: {
eventId: string
deleted: boolean
}
}
}
export const deleteTool: ToolConfig<GoogleCalendarDeleteParams, GoogleCalendarDeleteResponse> = {
id: 'google_calendar_delete',
name: 'Google Calendar Delete Event',
description: 'Delete an event from Google Calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
eventId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Calendar event ID to delete',
},
sendUpdates: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'How to send updates to attendees: all, externalOnly, or none',
},
},
request: {
url: (params: GoogleCalendarDeleteParams) => {
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: 'DELETE',
headers: (params: GoogleCalendarDeleteParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
content: `Event successfully deleted`,
metadata: {
eventId: params?.eventId || '',
deleted: true,
},
},
}
}
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to delete event')
},
outputs: {
content: { type: 'string', description: 'Event deletion confirmation message' },
metadata: {
type: 'json',
description: 'Deletion details including event ID',
},
},
}
interface GoogleCalendarDeleteV2Response {
success: boolean
output: {
eventId: string
deleted: boolean
}
}
export const deleteV2Tool: ToolConfig<GoogleCalendarDeleteParams, GoogleCalendarDeleteV2Response> =
{
id: 'google_calendar_delete_v2',
name: 'Google Calendar Delete Event',
description: 'Delete an event from Google Calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: deleteTool.oauth,
params: deleteTool.params,
request: deleteTool.request,
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
eventId: params?.eventId || '',
deleted: true,
},
}
}
const errorData = await response.json()
throw new Error(errorData.error?.message || 'Failed to delete event')
},
outputs: {
eventId: { type: 'string', description: 'Deleted event ID' },
deleted: { type: 'boolean', description: 'Whether deletion was successful' },
},
}
@@ -0,0 +1,126 @@
import { CALENDAR_API_BASE } from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export interface GoogleCalendarDeleteCalendarParams {
accessToken: string
calendarId: string
}
interface GoogleCalendarDeleteCalendarResponse {
success: boolean
output: {
content: string
metadata: {
calendarId: string
deleted: boolean
}
}
}
const buildDeleteCalendarUrl = (params: GoogleCalendarDeleteCalendarParams) =>
`${CALENDAR_API_BASE}/calendars/${encodeURIComponent(params.calendarId.trim())}`
export const deleteCalendarTool: ToolConfig<
GoogleCalendarDeleteCalendarParams,
GoogleCalendarDeleteCalendarResponse
> = {
id: 'google_calendar_delete_calendar',
name: 'Google Calendar Delete Calendar',
description: 'Permanently delete a secondary calendar (not the primary calendar)',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Secondary calendar ID to delete (e.g., calendar@group.calendar.google.com). The primary calendar cannot be deleted.',
},
},
request: {
url: buildDeleteCalendarUrl,
method: 'DELETE',
headers: (params: GoogleCalendarDeleteCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
content: 'Calendar successfully deleted',
metadata: {
calendarId: params?.calendarId || '',
deleted: true,
},
},
}
}
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to delete calendar')
},
outputs: {
content: { type: 'string', description: 'Calendar deletion confirmation message' },
metadata: {
type: 'json',
description: 'Deletion details including calendar ID',
},
},
}
interface GoogleCalendarDeleteCalendarV2Response {
success: boolean
output: {
calendarId: string
deleted: boolean
}
}
export const deleteCalendarV2Tool: ToolConfig<
GoogleCalendarDeleteCalendarParams,
GoogleCalendarDeleteCalendarV2Response
> = {
id: 'google_calendar_delete_calendar_v2',
name: 'Google Calendar Delete Calendar',
description: 'Permanently delete a secondary calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: deleteCalendarTool.oauth,
params: deleteCalendarTool.params,
request: deleteCalendarTool.request,
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
calendarId: params?.calendarId || '',
deleted: true,
},
}
}
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to delete calendar')
},
outputs: {
calendarId: { type: 'string', description: 'Deleted calendar ID' },
deleted: { type: 'boolean', description: 'Whether deletion was successful' },
},
}
+155
View File
@@ -0,0 +1,155 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiFreeBusyResponse,
type GoogleCalendarFreeBusyParams,
type GoogleCalendarFreeBusyResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export const freebusyTool: ToolConfig<
GoogleCalendarFreeBusyParams,
GoogleCalendarFreeBusyResponse
> = {
id: 'google_calendar_freebusy',
name: 'Google Calendar Free/Busy',
description: 'Query free/busy information for one or more Google Calendars',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarIds: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated calendar IDs to query (e.g., "primary,other@example.com")',
},
timeMin: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Start of the time range (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)',
},
timeMax: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'End of the time range (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'IANA time zone (e.g., "UTC", "America/New_York"). Defaults to UTC.',
},
},
request: {
url: () => `${CALENDAR_API_BASE}/freeBusy`,
method: 'POST',
headers: (params: GoogleCalendarFreeBusyParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarFreeBusyParams) => {
const ids = params.calendarIds
.split(',')
.map((id) => id.trim())
.filter(Boolean)
return {
timeMin: params.timeMin,
timeMax: params.timeMax,
timeZone: params.timeZone || 'UTC',
items: ids.map((id) => ({ id })),
}
},
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiFreeBusyResponse = await response.json()
const calendarIds = Object.keys(data.calendars || {})
const totalBusy = calendarIds.reduce((sum, id) => {
return sum + (data.calendars[id]?.busy?.length || 0)
}, 0)
return {
success: true,
output: {
content: `Found ${totalBusy} busy period${totalBusy !== 1 ? 's' : ''} across ${calendarIds.length} calendar${calendarIds.length !== 1 ? 's' : ''}`,
metadata: {
timeMin: data.timeMin,
timeMax: data.timeMax,
calendars: data.calendars,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Summary of free/busy results' },
metadata: {
type: 'json',
description: 'Free/busy data with time range and per-calendar busy periods',
},
},
}
interface GoogleCalendarFreeBusyV2Response {
success: boolean
output: {
timeMin: string
timeMax: string
calendars: Record<
string,
{
busy: Array<{ start: string; end: string }>
errors?: Array<{ domain: string; reason: string }>
}
>
}
}
export const freebusyV2Tool: ToolConfig<
GoogleCalendarFreeBusyParams,
GoogleCalendarFreeBusyV2Response
> = {
id: 'google_calendar_freebusy_v2',
name: 'Google Calendar Free/Busy',
description:
'Query free/busy information for one or more Google Calendars. Returns API-aligned fields only.',
version: '2.0.0',
oauth: freebusyTool.oauth,
params: freebusyTool.params,
request: freebusyTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiFreeBusyResponse = await response.json()
return {
success: true,
output: {
timeMin: data.timeMin,
timeMax: data.timeMax,
calendars: data.calendars,
},
}
},
outputs: {
timeMin: { type: 'string', description: 'Start of the queried time range' },
timeMax: { type: 'string', description: 'End of the queried time range' },
calendars: {
type: 'json',
description: 'Per-calendar free/busy data with busy periods and any errors',
},
},
}
+144
View File
@@ -0,0 +1,144 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
type GoogleCalendarGetParams,
type GoogleCalendarGetResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/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',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
eventId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Calendar 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) => {
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,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Event retrieval confirmation message' },
metadata: {
type: 'json',
description: 'Event details including ID, status, times, and attendees',
},
},
}
interface GoogleCalendarGetV2Response {
success: boolean
output: {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: GoogleCalendarApiEventResponse['attendees'] | null
creator: GoogleCalendarApiEventResponse['creator']
organizer: GoogleCalendarApiEventResponse['organizer']
}
}
export const getV2Tool: ToolConfig<GoogleCalendarGetParams, GoogleCalendarGetV2Response> = {
id: 'google_calendar_get_v2',
name: 'Google Calendar Get Event',
description: 'Get a specific event from Google Calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: getTool.oauth,
params: getTool.params,
request: getTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
id: data.id,
htmlLink: data.htmlLink,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator,
organizer: data.organizer,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
},
}
+59
View File
@@ -0,0 +1,59 @@
import { createTool, createV2Tool } from '@/tools/google_calendar/create'
import { createCalendarTool, createCalendarV2Tool } from '@/tools/google_calendar/create_calendar'
import { deleteTool, deleteV2Tool } from '@/tools/google_calendar/delete'
import { deleteCalendarTool, deleteCalendarV2Tool } from '@/tools/google_calendar/delete_calendar'
import { freebusyTool, freebusyV2Tool } from '@/tools/google_calendar/freebusy'
import { getTool, getV2Tool } from '@/tools/google_calendar/get'
import { instancesTool, instancesV2Tool } from '@/tools/google_calendar/instances'
import { inviteTool, inviteV2Tool } from '@/tools/google_calendar/invite'
import { listTool, listV2Tool } from '@/tools/google_calendar/list'
import { listAclTool, listAclV2Tool } from '@/tools/google_calendar/list_acl'
import { listCalendarsTool, listCalendarsV2Tool } from '@/tools/google_calendar/list_calendars'
import { moveTool, moveV2Tool } from '@/tools/google_calendar/move'
import { quickAddTool, quickAddV2Tool } from '@/tools/google_calendar/quick_add'
import { shareCalendarTool, shareCalendarV2Tool } from '@/tools/google_calendar/share_calendar'
import {
unshareCalendarTool,
unshareCalendarV2Tool,
} from '@/tools/google_calendar/unshare_calendar'
import { updateTool, updateV2Tool } from '@/tools/google_calendar/update'
import { updateAclTool, updateAclV2Tool } from '@/tools/google_calendar/update_acl'
import { updateCalendarTool, updateCalendarV2Tool } from '@/tools/google_calendar/update_calendar'
export const googleCalendarCreateTool = createTool
export const googleCalendarCreateCalendarTool = createCalendarTool
export const googleCalendarDeleteTool = deleteTool
export const googleCalendarDeleteCalendarTool = deleteCalendarTool
export const googleCalendarFreeBusyTool = freebusyTool
export const googleCalendarGetTool = getTool
export const googleCalendarInstancesTool = instancesTool
export const googleCalendarInviteTool = inviteTool
export const googleCalendarListTool = listTool
export const googleCalendarListAclTool = listAclTool
export const googleCalendarListCalendarsTool = listCalendarsTool
export const googleCalendarMoveTool = moveTool
export const googleCalendarQuickAddTool = quickAddTool
export const googleCalendarShareCalendarTool = shareCalendarTool
export const googleCalendarUnshareCalendarTool = unshareCalendarTool
export const googleCalendarUpdateTool = updateTool
export const googleCalendarUpdateAclTool = updateAclTool
export const googleCalendarUpdateCalendarTool = updateCalendarTool
export const googleCalendarCreateV2Tool = createV2Tool
export const googleCalendarCreateCalendarV2Tool = createCalendarV2Tool
export const googleCalendarDeleteV2Tool = deleteV2Tool
export const googleCalendarDeleteCalendarV2Tool = deleteCalendarV2Tool
export const googleCalendarFreeBusyV2Tool = freebusyV2Tool
export const googleCalendarGetV2Tool = getV2Tool
export const googleCalendarInstancesV2Tool = instancesV2Tool
export const googleCalendarInviteV2Tool = inviteV2Tool
export const googleCalendarListV2Tool = listV2Tool
export const googleCalendarListAclV2Tool = listAclV2Tool
export const googleCalendarListCalendarsV2Tool = listCalendarsV2Tool
export const googleCalendarMoveV2Tool = moveV2Tool
export const googleCalendarQuickAddV2Tool = quickAddV2Tool
export const googleCalendarShareCalendarV2Tool = shareCalendarV2Tool
export const googleCalendarUnshareCalendarV2Tool = unshareCalendarV2Tool
export const googleCalendarUpdateV2Tool = updateV2Tool
export const googleCalendarUpdateAclV2Tool = updateAclV2Tool
export const googleCalendarUpdateCalendarV2Tool = updateCalendarV2Tool
+288
View File
@@ -0,0 +1,288 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
interface GoogleCalendarInstancesParams {
accessToken: string
calendarId?: string
eventId: string
timeMin?: string
timeMax?: string
maxResults?: number
pageToken?: string
showDeleted?: boolean
}
interface GoogleCalendarInstancesResponse {
success: boolean
output: {
content: string
metadata: {
nextPageToken?: string
timeZone: string
instances: Array<{
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
}
recurringEventId: string
originalStartTime: {
dateTime?: string
date?: string
timeZone?: string
}
}>
}
}
}
interface InstanceApiResponse {
kind: string
etag: string
summary: string
description?: string
updated: string
timeZone: string
accessRole: string
nextPageToken?: string
items: Array<
GoogleCalendarApiEventResponse & {
recurringEventId: string
originalStartTime: {
dateTime?: string
date?: string
timeZone?: string
}
}
>
}
export const instancesTool: ToolConfig<
GoogleCalendarInstancesParams,
GoogleCalendarInstancesResponse
> = {
id: 'google_calendar_instances',
name: 'Google Calendar Get Instances',
description: 'Get instances of a recurring event from Google Calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
eventId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Recurring event ID to get instances of',
},
timeMin: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lower bound for instances (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)',
},
timeMax: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Upper bound for instances (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of instances to return (default 250, max 2500)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Token for retrieving subsequent pages of results',
},
showDeleted: {
type: 'boolean',
required: false,
visibility: 'hidden',
description: 'Include deleted instances',
},
},
request: {
url: (params: GoogleCalendarInstancesParams) => {
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)
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
if (params.showDeleted !== undefined)
queryParams.append('showDeleted', params.showDeleted.toString())
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}/instances${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params: GoogleCalendarInstancesParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data: InstanceApiResponse = await response.json()
const instances = data.items || []
const instancesCount = instances.length
return {
success: true,
output: {
content: `Found ${instancesCount} instance${instancesCount !== 1 ? 's' : ''} of the recurring event`,
metadata: {
nextPageToken: data.nextPageToken,
timeZone: data.timeZone,
instances: instances.map((instance) => ({
id: instance.id,
htmlLink: instance.htmlLink,
status: instance.status,
summary: instance.summary || 'No title',
description: instance.description,
location: instance.location,
start: instance.start,
end: instance.end,
attendees: instance.attendees,
creator: instance.creator,
organizer: instance.organizer,
recurringEventId: instance.recurringEventId,
originalStartTime: instance.originalStartTime,
})),
},
},
}
},
outputs: {
content: { type: 'string', description: 'Summary of found instances count' },
metadata: {
type: 'json',
description: 'List of recurring event instances with pagination tokens',
},
},
}
interface GoogleCalendarInstancesV2Instance {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: GoogleCalendarApiEventResponse['attendees'] | null
creator: GoogleCalendarApiEventResponse['creator']
organizer: GoogleCalendarApiEventResponse['organizer']
recurringEventId: string
originalStartTime: {
dateTime?: string
date?: string
timeZone?: string
}
}
interface GoogleCalendarInstancesV2Response {
success: boolean
output: {
nextPageToken: string | null
timeZone: string | null
instances: GoogleCalendarInstancesV2Instance[]
}
}
export const instancesV2Tool: ToolConfig<
GoogleCalendarInstancesParams,
GoogleCalendarInstancesV2Response
> = {
id: 'google_calendar_instances_v2',
name: 'Google Calendar Get Instances',
description:
'Get instances of a recurring event from Google Calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: instancesTool.oauth,
params: instancesTool.params,
request: instancesTool.request,
transformResponse: async (response: Response) => {
const data: InstanceApiResponse = await response.json()
const instances = data.items || []
return {
success: true,
output: {
nextPageToken: data.nextPageToken ?? null,
timeZone: data.timeZone ?? null,
instances: instances.map((instance) => ({
id: instance.id,
htmlLink: instance.htmlLink,
status: instance.status,
summary: instance.summary ?? null,
description: instance.description ?? null,
location: instance.location ?? null,
start: instance.start,
end: instance.end,
attendees: instance.attendees ?? null,
creator: instance.creator,
organizer: instance.organizer,
recurringEventId: instance.recurringEventId,
originalStartTime: instance.originalStartTime,
})),
},
}
},
outputs: {
nextPageToken: { type: 'string', description: 'Next page token', optional: true },
timeZone: { type: 'string', description: 'Calendar time zone', optional: true },
instances: { type: 'json', description: 'List of recurring event instances' },
},
}
+275
View File
@@ -0,0 +1,275 @@
import {
CALENDAR_API_BASE,
type CalendarAttendee,
type GoogleCalendarApiEventResponse,
type GoogleCalendarInviteParams,
type GoogleCalendarInviteResponse,
} from '@/tools/google_calendar/types'
import { normalizeAttendees } from '@/tools/google_calendar/utils'
import type { ToolConfig } from '@/tools/types'
interface InviteResult {
data: GoogleCalendarApiEventResponse
totalAttendees: number
newAttendeesAdded: number
shouldReplace: boolean
}
/**
* The Google Calendar update method replaces the entire event resource, so to invite
* attendees we read the existing event, merge the attendee list, then PUT it back.
*/
async function inviteAttendees(
response: Response,
params: GoogleCalendarInviteParams | undefined
): Promise<InviteResult> {
const existingEvent: GoogleCalendarApiEventResponse = await response.json()
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
throw new Error('Existing event is missing required fields (start, end, or summary)')
}
const newAttendeeList = normalizeAttendees(params?.attendees).map((attendee) => attendee.email)
const existingAttendees: CalendarAttendee[] = existingEvent.attendees ?? []
const shouldReplace =
params?.replaceExisting === true || String(params?.replaceExisting) === 'true'
const existingEmails = new Set(
existingAttendees.map((attendee) => attendee.email?.toLowerCase() ?? '')
)
const newAttendeesAdded = shouldReplace
? newAttendeeList.length
: newAttendeeList.filter((email) => !existingEmails.has(email.toLowerCase())).length
let finalAttendees: CalendarAttendee[]
if (shouldReplace) {
finalAttendees = newAttendeeList.map((email) => ({ email, responseStatus: 'needsAction' }))
} else {
finalAttendees = [...existingAttendees]
for (const email of newAttendeeList) {
if (!existingEmails.has(email.toLowerCase())) {
finalAttendees.push({ email, responseStatus: 'needsAction' })
}
}
}
const updatedEvent: Record<string, unknown> = { ...existingEvent, attendees: finalAttendees }
const readOnlyFields = [
'id',
'etag',
'kind',
'created',
'updated',
'htmlLink',
'iCalUID',
'creator',
'organizer',
]
for (const field of readOnlyFields) {
delete updatedEvent[field]
}
const calendarId = params?.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
queryParams.append('sendUpdates', params?.sendUpdates ?? 'all')
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId?.trim() ?? '')}?${queryParams.toString()}`
const putResponse = await fetch(putUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${params?.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedEvent),
})
if (!putResponse.ok) {
const errorData = await putResponse.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to invite attendees to calendar event')
}
const data: GoogleCalendarApiEventResponse = await putResponse.json()
return {
data,
totalAttendees: data.attendees?.length ?? 0,
newAttendeesAdded,
shouldReplace,
}
}
export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarInviteResponse> = {
id: 'google_calendar_invite',
name: 'Google Calendar Invite Attendees',
description: 'Invite attendees to an existing Google Calendar event',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
eventId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Calendar event ID to invite attendees to',
},
attendees: {
type: 'array',
required: true,
visibility: 'user-or-llm',
description: 'Array of attendee email addresses to invite',
},
sendUpdates: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'How to send updates to attendees: all, externalOnly, or none (defaults to all)',
},
replaceExisting: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to replace existing attendees or add to them (defaults to false)',
},
},
request: {
url: (params: GoogleCalendarInviteParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId.trim())}`
},
method: 'GET',
headers: (params: GoogleCalendarInviteParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
const { data, totalAttendees, newAttendeesAdded, shouldReplace } = await inviteAttendees(
response,
params
)
let baseMessage: string
if (shouldReplace) {
baseMessage = `Successfully updated event "${data.summary}" with ${totalAttendees} attendee${totalAttendees !== 1 ? 's' : ''}`
} else if (newAttendeesAdded > 0) {
baseMessage = `Successfully added ${newAttendeesAdded} new attendee${newAttendeesAdded !== 1 ? 's' : ''} to event "${data.summary}" (total: ${totalAttendees})`
} else {
baseMessage = `No new attendees added to event "${data.summary}" - all specified attendees were already invited (total: ${totalAttendees})`
}
const emailNote =
params?.sendUpdates !== 'none'
? ' Email invitations are being sent asynchronously - delivery may take a few minutes and depends on recipients Google Calendar settings.'
: ' No email notifications will be sent as requested.'
return {
success: true,
output: {
content: baseMessage + emailNote,
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,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Attendee invitation confirmation message with email delivery status',
},
metadata: {
type: 'json',
description: 'Updated event metadata including attendee list and details',
},
},
}
interface GoogleCalendarInviteV2Response {
success: boolean
output: {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: CalendarAttendee[] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
}
export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarInviteV2Response> =
{
id: 'google_calendar_invite_v2',
name: 'Google Calendar Invite Attendees',
description:
'Invite attendees to an existing Google Calendar event. Returns API-aligned fields only.',
version: '2.0.0',
oauth: inviteTool.oauth,
params: inviteTool.params,
request: inviteTool.request,
transformResponse: async (response: Response, params) => {
const { data } = await inviteAttendees(response, params)
return {
success: true,
output: {
id: data.id,
htmlLink: data.htmlLink,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator ?? null,
organizer: data.organizer ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator', optional: true },
organizer: { type: 'json', description: 'Event organizer', optional: true },
},
}
+206
View File
@@ -0,0 +1,206 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
type GoogleCalendarApiListResponse,
type GoogleCalendarListParams,
type GoogleCalendarListResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/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',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
timeMin: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Lower bound for events (RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z)',
},
timeMax: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Upper bound for events (RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z)',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Free-text search across event summary, description, location, attendees, and organizer',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of events to return (max 2500)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Token for retrieving the next page of results',
},
orderBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Order of events: startTime (chronological, the default) or updated (last-modified). startTime is always valid here because singleEvents is set.',
},
showDeleted: {
type: 'boolean',
required: false,
visibility: 'hidden',
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)
if (params.q) queryParams.append('q', params.q)
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
queryParams.append('singleEvents', 'true')
queryParams.append('orderBy', params.orderBy || 'startTime')
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) => {
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,
})),
},
},
}
},
outputs: {
content: { type: 'string', description: 'Summary of found events count' },
metadata: {
type: 'json',
description: 'List of events with pagination tokens and event details',
},
},
}
interface GoogleCalendarListV2Event {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: GoogleCalendarApiEventResponse['attendees'] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
interface GoogleCalendarListV2Response {
success: boolean
output: {
nextPageToken: string | null
timeZone: string | null
events: GoogleCalendarListV2Event[]
}
}
export const listV2Tool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListV2Response> = {
id: 'google_calendar_list_v2',
name: 'Google Calendar List Events',
description: 'List events from Google Calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: listTool.oauth,
params: listTool.params,
request: listTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiListResponse = await response.json()
const events = data.items || []
return {
success: true,
output: {
nextPageToken: data.nextPageToken ?? null,
timeZone: data.timeZone ?? null,
events: events.map((event: GoogleCalendarApiEventResponse) => ({
id: event.id,
htmlLink: event.htmlLink,
status: event.status,
summary: event.summary ?? null,
description: event.description ?? null,
location: event.location ?? null,
start: event.start,
end: event.end,
attendees: event.attendees ?? null,
creator: event.creator,
organizer: event.organizer,
})),
},
}
},
outputs: {
nextPageToken: { type: 'string', description: 'Next page token', optional: true },
timeZone: { type: 'string', description: 'Calendar time zone', optional: true },
events: { type: 'json', description: 'List of events' },
},
}
+152
View File
@@ -0,0 +1,152 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiAclListResponse,
type GoogleCalendarListAclParams,
type GoogleCalendarListAclResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export const listAclTool: ToolConfig<GoogleCalendarListAclParams, GoogleCalendarListAclResponse> = {
id: 'google_calendar_list_acl',
name: 'Google Calendar List Sharing',
description: 'List the access control rules (sharing) for a calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to inspect (e.g., primary or calendar@group.calendar.google.com)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of ACL rules to return',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Token for retrieving subsequent pages of results',
},
showDeleted: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Include deleted ACL rules (with role "none")',
},
},
request: {
url: (params: GoogleCalendarListAclParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
if (params.showDeleted !== undefined)
queryParams.append('showDeleted', params.showDeleted.toString())
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params: GoogleCalendarListAclParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclListResponse = await response.json()
const rules = data.items || []
const rulesCount = rules.length
return {
success: true,
output: {
content: `Found ${rulesCount} sharing rule${rulesCount !== 1 ? 's' : ''}`,
metadata: {
nextPageToken: data.nextPageToken,
rules: rules.map((rule) => ({
id: rule.id,
role: rule.role,
scope: rule.scope,
})),
},
},
}
},
outputs: {
content: { type: 'string', description: 'Summary of found sharing rules count' },
metadata: {
type: 'json',
description: 'List of ACL rules with pagination token',
},
},
}
interface GoogleCalendarListAclV2Response {
success: boolean
output: {
nextPageToken: string | null
rules: Array<{ id: string; role: string; scope: { type: string; value?: string } }>
}
}
export const listAclV2Tool: ToolConfig<
GoogleCalendarListAclParams,
GoogleCalendarListAclV2Response
> = {
id: 'google_calendar_list_acl_v2',
name: 'Google Calendar List Sharing',
description:
'List the access control rules (sharing) for a calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: listAclTool.oauth,
params: listAclTool.params,
request: listAclTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclListResponse = await response.json()
const rules = data.items || []
return {
success: true,
output: {
nextPageToken: data.nextPageToken ?? null,
rules: rules.map((rule) => ({
id: rule.id,
role: rule.role,
scope: rule.scope,
})),
},
}
},
outputs: {
nextPageToken: { type: 'string', description: 'Next page token', optional: true },
rules: {
type: 'array',
description: 'List of ACL rules',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'ACL rule ID' },
role: { type: 'string', description: 'Access role' },
scope: { type: 'json', description: 'Grantee scope (type and value)' },
},
},
},
},
}
@@ -0,0 +1,280 @@
import { CALENDAR_API_BASE } from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
interface GoogleCalendarListCalendarsParams {
accessToken: string
minAccessRole?: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
maxResults?: number
pageToken?: string
showDeleted?: boolean
showHidden?: boolean
}
interface CalendarListEntry {
kind: string
etag: string
id: string
summary: string
description?: string
location?: string
timeZone: string
summaryOverride?: string
colorId: string
backgroundColor: string
foregroundColor: string
hidden?: boolean
selected?: boolean
accessRole: string
defaultReminders: Array<{
method: string
minutes: number
}>
notificationSettings?: {
notifications: Array<{
type: string
method: string
}>
}
primary?: boolean
deleted?: boolean
conferenceProperties?: {
allowedConferenceSolutionTypes: string[]
}
}
interface CalendarListApiResponse {
kind: string
etag: string
nextPageToken?: string
nextSyncToken?: string
items: CalendarListEntry[]
}
interface GoogleCalendarListCalendarsResponse {
success: boolean
output: {
content: string
metadata: {
nextPageToken?: string
calendars: Array<{
id: string
summary: string
description?: string
location?: string
timeZone: string
accessRole: string
backgroundColor: string
foregroundColor: string
primary?: boolean
hidden?: boolean
selected?: boolean
}>
}
}
}
export const listCalendarsTool: ToolConfig<
GoogleCalendarListCalendarsParams,
GoogleCalendarListCalendarsResponse
> = {
id: 'google_calendar_list_calendars',
name: 'Google Calendar List Calendars',
description: "List all calendars in the user's calendar list",
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
minAccessRole: {
type: 'string',
required: false,
visibility: 'user-only',
description:
'Minimum access role for returned calendars: freeBusyReader, reader, writer, or owner',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of calendars to return (default 100, max 250)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Token for retrieving subsequent pages of results',
},
showDeleted: {
type: 'boolean',
required: false,
visibility: 'hidden',
description: 'Include deleted calendars',
},
showHidden: {
type: 'boolean',
required: false,
visibility: 'hidden',
description: 'Include hidden calendars',
},
},
request: {
url: (params: GoogleCalendarListCalendarsParams) => {
const queryParams = new URLSearchParams()
if (params.minAccessRole) queryParams.append('minAccessRole', params.minAccessRole)
if (params.maxResults) queryParams.append('maxResults', params.maxResults.toString())
if (params.pageToken) queryParams.append('pageToken', params.pageToken)
if (params.showDeleted !== undefined)
queryParams.append('showDeleted', params.showDeleted.toString())
if (params.showHidden !== undefined)
queryParams.append('showHidden', params.showHidden.toString())
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/users/me/calendarList${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params: GoogleCalendarListCalendarsParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data: CalendarListApiResponse = await response.json()
const calendars = data.items || []
const calendarsCount = calendars.length
return {
success: true,
output: {
content: `Found ${calendarsCount} calendar${calendarsCount !== 1 ? 's' : ''}`,
metadata: {
nextPageToken: data.nextPageToken,
calendars: calendars.map((calendar) => ({
id: calendar.id,
summary: calendar.summaryOverride || calendar.summary,
description: calendar.description,
location: calendar.location,
timeZone: calendar.timeZone,
accessRole: calendar.accessRole,
backgroundColor: calendar.backgroundColor,
foregroundColor: calendar.foregroundColor,
primary: calendar.primary,
hidden: calendar.hidden,
selected: calendar.selected,
})),
},
},
}
},
outputs: {
content: { type: 'string', description: 'Summary of found calendars count' },
metadata: {
type: 'json',
description: 'List of calendars with their details',
},
},
}
interface GoogleCalendarListCalendarsV2Response {
success: boolean
output: {
nextPageToken: string | null
calendars: Array<{
id: string
summary: string
description: string | null
location: string | null
timeZone: string
accessRole: string
backgroundColor: string
foregroundColor: string
primary: boolean | null
hidden: boolean | null
selected: boolean | null
}>
}
}
export const listCalendarsV2Tool: ToolConfig<
GoogleCalendarListCalendarsParams,
GoogleCalendarListCalendarsV2Response
> = {
id: 'google_calendar_list_calendars_v2',
name: 'Google Calendar List Calendars',
description: "List all calendars in the user's calendar list. Returns API-aligned fields only.",
version: '2.0.0',
oauth: listCalendarsTool.oauth,
params: listCalendarsTool.params,
request: listCalendarsTool.request,
transformResponse: async (response: Response) => {
const data: CalendarListApiResponse = await response.json()
const calendars = data.items || []
return {
success: true,
output: {
nextPageToken: data.nextPageToken ?? null,
calendars: calendars.map((calendar) => ({
id: calendar.id,
summary: calendar.summaryOverride || calendar.summary,
description: calendar.description ?? null,
location: calendar.location ?? null,
timeZone: calendar.timeZone,
accessRole: calendar.accessRole,
backgroundColor: calendar.backgroundColor,
foregroundColor: calendar.foregroundColor,
primary: calendar.primary ?? null,
hidden: calendar.hidden ?? null,
selected: calendar.selected ?? null,
})),
},
}
},
outputs: {
nextPageToken: { type: 'string', description: 'Next page token', optional: true },
calendars: {
type: 'array',
description: 'List of calendars',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Calendar ID' },
summary: { type: 'string', description: 'Calendar title' },
description: { type: 'string', description: 'Calendar description', optional: true },
location: { type: 'string', description: 'Calendar location', optional: true },
timeZone: { type: 'string', description: 'Calendar time zone' },
accessRole: { type: 'string', description: 'Access role for the calendar' },
backgroundColor: { type: 'string', description: 'Calendar background color' },
foregroundColor: { type: 'string', description: 'Calendar foreground color' },
primary: {
type: 'boolean',
description: 'Whether this is the primary calendar',
optional: true,
},
hidden: {
type: 'boolean',
description: 'Whether the calendar is hidden',
optional: true,
},
selected: {
type: 'boolean',
description: 'Whether the calendar is selected',
optional: true,
},
},
},
},
},
}
+209
View File
@@ -0,0 +1,209 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
interface GoogleCalendarMoveParams {
accessToken: string
calendarId?: string
eventId: string
destinationCalendarId: string
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
interface GoogleCalendarMoveResponse {
success: boolean
output: {
content: string
metadata: {
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
}
}
}
}
export const moveTool: ToolConfig<GoogleCalendarMoveParams, GoogleCalendarMoveResponse> = {
id: 'google_calendar_move',
name: 'Google Calendar Move Event',
description: 'Move an event to a different calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Source Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
eventId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Calendar event ID to move',
},
destinationCalendarId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Destination Google Calendar ID',
},
sendUpdates: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'How to send updates to attendees: all, externalOnly, or none',
},
},
request: {
url: (params: GoogleCalendarMoveParams) => {
const calendarId = params.calendarId || 'primary'
const queryParams = new URLSearchParams()
queryParams.append('destination', params.destinationCalendarId)
if (params.sendUpdates !== undefined) {
queryParams.append('sendUpdates', params.sendUpdates)
}
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}/move?${queryParams.toString()}`
},
method: 'POST',
headers: (params: GoogleCalendarMoveParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
content: `Event "${data.summary || 'Untitled'}" moved 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,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Event move confirmation message' },
metadata: {
type: 'json',
description: 'Moved event metadata including new details',
},
},
}
interface GoogleCalendarMoveV2Response {
success: boolean
output: {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: GoogleCalendarApiEventResponse['attendees'] | null
creator: GoogleCalendarApiEventResponse['creator']
organizer: GoogleCalendarApiEventResponse['organizer']
}
}
export const moveV2Tool: ToolConfig<GoogleCalendarMoveParams, GoogleCalendarMoveV2Response> = {
id: 'google_calendar_move_v2',
name: 'Google Calendar Move Event',
description: 'Move an event to a different calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: moveTool.oauth,
params: moveTool.params,
request: moveTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
id: data.id,
htmlLink: data.htmlLink,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator,
organizer: data.organizer,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
},
}
+294
View File
@@ -0,0 +1,294 @@
import { createLogger } from '@sim/logger'
import {
CALENDAR_API_BASE,
type GoogleCalendarApiEventResponse,
type GoogleCalendarQuickAddParams,
type GoogleCalendarQuickAddResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('GoogleCalendarQuickAddTool')
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',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Natural language text describing the event (e.g., "Meeting with John tomorrow at 3pm")',
},
attendees: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description: 'Array of attendee email addresses (comma-separated string also accepted)',
},
sendUpdates: {
type: 'string',
required: false,
visibility: 'user-only',
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: GoogleCalendarApiEventResponse = await response.json()
let finalEventData: GoogleCalendarApiEventResponse = 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) {
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
if (attendeeList.length > 0) {
try {
const calendarId = params.calendarId || 'primary'
const eventId = data.id
const updateData = {
attendees: attendeeList.map((email: string) => ({ email })),
}
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()}` : ''}`
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 {
logger.warn('Failed to add attendees to quick-added event', {
error: await updateResponse.text(),
})
}
} catch (error) {
logger.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,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Event creation confirmation message from natural language',
},
metadata: {
type: 'json',
description: 'Created event metadata including parsed details',
properties: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'URL to view the event in Google Calendar' },
status: { type: 'string', description: 'Event status (confirmed, tentative, cancelled)' },
summary: { type: 'string', description: 'Event title' },
description: { type: 'string', description: 'Event description' },
location: { type: 'string', description: 'Event location' },
start: { type: 'object', description: 'Event start time' },
end: { type: 'object', description: 'Event end time' },
attendees: { type: 'array', description: 'List of event attendees' },
creator: { type: 'object', description: 'Event creator info' },
organizer: { type: 'object', description: 'Event organizer info' },
},
},
},
}
interface GoogleCalendarQuickAddV2Response {
success: boolean
output: {
id: string
htmlLink: string
status: string
summary: string | null
description: string | null
location: string | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: GoogleCalendarApiEventResponse['attendees'] | null
creator: GoogleCalendarApiEventResponse['creator']
organizer: GoogleCalendarApiEventResponse['organizer']
}
}
export const quickAddV2Tool: ToolConfig<
GoogleCalendarQuickAddParams,
GoogleCalendarQuickAddV2Response
> = {
id: 'google_calendar_quick_add_v2',
name: 'Google Calendar Quick Add',
description: 'Create events from natural language text. Returns API-aligned fields only.',
version: '2.0.0',
oauth: quickAddTool.oauth,
params: quickAddTool.params,
request: quickAddTool.request,
transformResponse: async (response: Response, params) => {
const data: GoogleCalendarApiEventResponse = await response.json()
let finalEventData: GoogleCalendarApiEventResponse = 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) {
attendeeList = attendees
.split(',')
.map((email: string) => email.trim())
.filter((email: string) => email.length > 0)
}
if (attendeeList.length > 0) {
try {
const calendarId = params.calendarId || 'primary'
const eventId = data.id
const updateData = {
attendees: attendeeList.map((email: string) => ({ email })),
}
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()}` : ''}`
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 {
logger.warn('Failed to add attendees to quick-added event', {
error: await updateResponse.text(),
})
}
} catch (error) {
logger.warn('Error adding attendees to quick-added event', { error })
}
}
}
return {
success: true,
output: {
id: finalEventData.id,
htmlLink: finalEventData.htmlLink,
status: finalEventData.status,
summary: finalEventData.summary ?? null,
description: finalEventData.description ?? null,
location: finalEventData.location ?? null,
start: finalEventData.start,
end: finalEventData.end,
attendees: finalEventData.attendees ?? null,
creator: finalEventData.creator,
organizer: finalEventData.organizer,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator' },
organizer: { type: 'json', description: 'Event organizer' },
},
}
@@ -0,0 +1,160 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiAclRule,
type GoogleCalendarShareCalendarParams,
type GoogleCalendarShareCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
const buildAclBody = (params: GoogleCalendarShareCalendarParams) => {
const scope: { type: string; value?: string } = { type: params.scopeType }
if (params.scopeType !== 'default') {
const value = params.scopeValue?.trim()
if (!value) {
throw new Error(
`A grantee is required when scope type is "${params.scopeType}". Provide an email (user/group) or domain name in scopeValue.`
)
}
scope.value = value
}
return { role: params.role, scope }
}
const buildAclUrl = (params: GoogleCalendarShareCalendarParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
if (params.sendNotifications !== undefined) {
queryParams.append('sendNotifications', String(params.sendNotifications))
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl${queryString ? `?${queryString}` : ''}`
}
export const shareCalendarTool: ToolConfig<
GoogleCalendarShareCalendarParams,
GoogleCalendarShareCalendarResponse
> = {
id: 'google_calendar_share_calendar',
name: 'Google Calendar Share Calendar',
description: 'Grant a user, group, or domain access to a calendar by creating an ACL rule',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to share (e.g., primary or calendar@group.calendar.google.com)',
},
role: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Access role to grant: freeBusyReader, reader, writer, or owner',
},
scopeType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Type of grantee: user, group, domain, or default (public)',
},
scopeValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Email (user/group), domain name (domain), or empty for default. Required unless scope type is default.',
},
sendNotifications: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to send a notification email about the change. Defaults to true.',
},
},
request: {
url: buildAclUrl,
method: 'POST',
headers: (params: GoogleCalendarShareCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: buildAclBody,
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclRule = await response.json()
return {
success: true,
output: {
content: `Granted ${data.role} access to ${data.scope?.value || data.scope?.type}`,
metadata: {
id: data.id,
role: data.role,
scope: data.scope,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Sharing confirmation message' },
metadata: {
type: 'json',
description: 'Created ACL rule (id, role, scope)',
},
},
}
interface GoogleCalendarShareCalendarV2Response {
success: boolean
output: {
id: string
role: string
scope: { type: string; value?: string }
}
}
export const shareCalendarV2Tool: ToolConfig<
GoogleCalendarShareCalendarParams,
GoogleCalendarShareCalendarV2Response
> = {
id: 'google_calendar_share_calendar_v2',
name: 'Google Calendar Share Calendar',
description:
'Grant a user, group, or domain access to a calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: shareCalendarTool.oauth,
params: shareCalendarTool.params,
request: shareCalendarTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclRule = await response.json()
return {
success: true,
output: {
id: data.id,
role: data.role,
scope: data.scope,
},
}
},
outputs: {
id: { type: 'string', description: 'ACL rule ID' },
role: { type: 'string', description: 'Granted access role' },
scope: { type: 'json', description: 'Grantee scope (type and value)' },
},
}
+549
View File
@@ -0,0 +1,549 @@
import type { ToolResponse } from '@/tools/types'
export const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'
export interface CalendarAttendee {
id?: string
email: string
displayName?: string
organizer?: boolean
self?: boolean
resource?: boolean
optional?: boolean
responseStatus: string
comment?: string
additionalGuests?: number
}
interface BaseGoogleCalendarParams {
accessToken: string
calendarId?: string
}
export interface GoogleCalendarCreateParams extends BaseGoogleCalendarParams {
summary: string
description?: string
location?: string
startDateTime: string
endDateTime: string
timeZone?: string
attendees?: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
recurrence?: string | string[]
addGoogleMeet?: boolean
}
export interface GoogleCalendarListParams extends BaseGoogleCalendarParams {
timeMin?: string
timeMax?: string
q?: string
maxResults?: number
pageToken?: string
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'
recurrence?: string | string[]
addGoogleMeet?: boolean
}
export interface GoogleCalendarDeleteParams extends BaseGoogleCalendarParams {
eventId: string
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarQuickAddParams extends BaseGoogleCalendarParams {
text: string
attendees?: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
export interface GoogleCalendarInviteParams extends BaseGoogleCalendarParams {
eventId: string
attendees: string[]
sendUpdates?: 'all' | 'externalOnly' | 'none'
replaceExisting?: boolean
}
interface GoogleCalendarMoveParams extends BaseGoogleCalendarParams {
eventId: string
destinationCalendarId: string
sendUpdates?: 'all' | 'externalOnly' | 'none'
}
interface GoogleCalendarInstancesParams extends BaseGoogleCalendarParams {
eventId: string
timeMin?: string
timeMax?: string
maxResults?: number
pageToken?: string
showDeleted?: boolean
}
export interface GoogleCalendarFreeBusyParams {
accessToken: string
calendarIds: string
timeMin: string
timeMax: string
timeZone?: string
}
interface GoogleCalendarListCalendarsParams {
accessToken: string
minAccessRole?: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
maxResults?: number
pageToken?: string
showDeleted?: boolean
showHidden?: boolean
}
export interface GoogleCalendarCreateCalendarParams {
accessToken: string
summary: string
description?: string
location?: string
timeZone?: string
}
export type GoogleCalendarAclRole = 'freeBusyReader' | 'reader' | 'writer' | 'owner'
type GoogleCalendarAclScopeType = 'user' | 'group' | 'domain' | 'default'
export interface GoogleCalendarShareCalendarParams {
accessToken: string
calendarId?: string
role: GoogleCalendarAclRole
scopeType: GoogleCalendarAclScopeType
scopeValue?: string
sendNotifications?: boolean
}
export interface GoogleCalendarListAclParams {
accessToken: string
calendarId?: string
maxResults?: number
pageToken?: string
showDeleted?: boolean
}
export interface GoogleCalendarUnshareCalendarParams {
accessToken: string
calendarId?: string
ruleId: string
}
export type GoogleCalendarToolParams =
| GoogleCalendarCreateParams
| GoogleCalendarListParams
| GoogleCalendarGetParams
| GoogleCalendarUpdateParams
| GoogleCalendarDeleteParams
| GoogleCalendarQuickAddParams
| GoogleCalendarInviteParams
| GoogleCalendarMoveParams
| GoogleCalendarInstancesParams
| GoogleCalendarFreeBusyParams
| GoogleCalendarListCalendarsParams
| GoogleCalendarCreateCalendarParams
| GoogleCalendarShareCalendarParams
| GoogleCalendarListAclParams
| GoogleCalendarUnshareCalendarParams
interface EventMetadata {
id: string
htmlLink: string
hangoutLink?: string
status: string
summary: string
description?: string
location?: string
recurrence?: string[]
start: {
dateTime?: string
date?: string
timeZone?: string
}
end: {
dateTime?: string
date?: string
timeZone?: string
}
attendees?: CalendarAttendee[]
creator?: {
email: string
displayName?: string
}
organizer?: {
email: string
displayName?: string
}
}
interface ListMetadata {
nextPageToken?: string
nextSyncToken?: string
events: EventMetadata[]
timeZone: string
}
interface GoogleCalendarToolResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata | ListMetadata
}
}
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 GoogleCalendarInviteResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
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?: CalendarAttendee[]
creator?: {
email: string
displayName?: string
}
organizer?: {
email: string
displayName?: string
}
reminders?: {
useDefault: boolean
overrides?: Array<{
method: string
minutes: number
}>
}
}
interface GoogleCalendarEventDateTime {
dateTime?: string
date?: string
timeZone?: string
}
interface GoogleCalendarConferenceCreateRequest {
createRequest: {
requestId: string
conferenceSolutionKey: { type: string }
}
}
export interface GoogleCalendarEventRequestBody {
summary: string
description?: string
location?: string
start: GoogleCalendarEventDateTime
end: GoogleCalendarEventDateTime
attendees?: Array<{
email: string
}>
recurrence?: string[]
conferenceData?: GoogleCalendarConferenceCreateRequest
}
export interface GoogleCalendarApiEventResponse {
id: string
status: string
htmlLink: string
hangoutLink?: string
created?: string
updated?: string
summary: string
description?: string
location?: string
recurrence?: string[]
recurringEventId?: string
start: {
dateTime?: string
date?: string
timeZone?: string
}
end: {
dateTime?: string
date?: string
timeZone?: string
}
attendees?: CalendarAttendee[]
creator?: {
email: string
displayName?: string
}
organizer?: {
email: string
displayName?: string
}
conferenceData?: Record<string, unknown>
reminders?: {
useDefault: boolean
overrides?: Array<{
method: string
minutes: number
}>
}
}
export interface GoogleCalendarApiCalendarResponse {
kind: string
etag: string
id: string
summary: string
description?: string
location?: string
timeZone?: string
}
export interface GoogleCalendarApiAclRule {
kind: string
etag: string
id: string
role: string
scope: {
type: string
value?: string
}
}
export interface GoogleCalendarApiAclListResponse {
kind: string
etag: string
nextPageToken?: string
items: GoogleCalendarApiAclRule[]
}
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[]
}
interface GoogleCalendarDeleteResponse extends ToolResponse {
output: {
content: string
metadata: {
eventId: string
deleted: boolean
}
}
}
interface GoogleCalendarMoveResponse extends ToolResponse {
output: {
content: string
metadata: EventMetadata
}
}
interface GoogleCalendarInstancesResponse extends ToolResponse {
output: {
content: string
metadata: {
nextPageToken?: string
timeZone: string
instances: Array<
EventMetadata & {
recurringEventId: string
originalStartTime: {
dateTime?: string
date?: string
timeZone?: string
}
}
>
}
}
}
export interface GoogleCalendarFreeBusyResponse extends ToolResponse {
output: {
content: string
metadata: {
timeMin: string
timeMax: string
calendars: Record<
string,
{
busy: Array<{ start: string; end: string }>
errors?: Array<{ domain: string; reason: string }>
}
>
}
}
}
export interface GoogleCalendarApiFreeBusyResponse {
kind: string
timeMin: string
timeMax: string
calendars: Record<
string,
{
busy: Array<{ start: string; end: string }>
errors?: Array<{ domain: string; reason: string }>
}
>
}
interface GoogleCalendarListCalendarsResponse extends ToolResponse {
output: {
content: string
metadata: {
nextPageToken?: string
calendars: Array<{
id: string
summary: string
description?: string
location?: string
timeZone: string
accessRole: string
backgroundColor: string
foregroundColor: string
primary?: boolean
hidden?: boolean
selected?: boolean
}>
}
}
}
export interface GoogleCalendarCreateCalendarResponse extends ToolResponse {
output: {
content: string
metadata: {
id: string
summary: string
description?: string
location?: string
timeZone?: string
}
}
}
export interface GoogleCalendarShareCalendarResponse extends ToolResponse {
output: {
content: string
metadata: {
id: string
role: string
scope: { type: string; value?: string }
}
}
}
export interface GoogleCalendarListAclResponse extends ToolResponse {
output: {
content: string
metadata: {
nextPageToken?: string
rules: Array<{ id: string; role: string; scope: { type: string; value?: string } }>
}
}
}
export interface GoogleCalendarUnshareCalendarResponse extends ToolResponse {
output: {
content: string
metadata: {
ruleId: string
deleted: boolean
}
}
}
export type GoogleCalendarResponse =
| GoogleCalendarCreateResponse
| GoogleCalendarListResponse
| GoogleCalendarGetResponse
| GoogleCalendarQuickAddResponse
| GoogleCalendarInviteResponse
| GoogleCalendarUpdateResponse
| GoogleCalendarDeleteResponse
| GoogleCalendarMoveResponse
| GoogleCalendarInstancesResponse
| GoogleCalendarFreeBusyResponse
| GoogleCalendarListCalendarsResponse
| GoogleCalendarCreateCalendarResponse
| GoogleCalendarShareCalendarResponse
| GoogleCalendarListAclResponse
| GoogleCalendarUnshareCalendarResponse
@@ -0,0 +1,122 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarUnshareCalendarParams,
type GoogleCalendarUnshareCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
const buildUnshareUrl = (params: GoogleCalendarUnshareCalendarParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(params.ruleId.trim())}`
}
export const unshareCalendarTool: ToolConfig<
GoogleCalendarUnshareCalendarParams,
GoogleCalendarUnshareCalendarResponse
> = {
id: 'google_calendar_unshare_calendar',
name: 'Google Calendar Remove Sharing',
description: 'Revoke an access control rule (sharing) from a calendar',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to modify (e.g., primary or calendar@group.calendar.google.com)',
},
ruleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ACL rule ID to remove (e.g., user:person@example.com)',
},
},
request: {
url: buildUnshareUrl,
method: 'DELETE',
headers: (params: GoogleCalendarUnshareCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
content: 'Sharing rule successfully removed',
metadata: {
ruleId: params?.ruleId || '',
deleted: true,
},
},
}
}
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to remove sharing rule')
},
outputs: {
content: { type: 'string', description: 'Removal confirmation message' },
metadata: {
type: 'json',
description: 'Removal details including rule ID',
},
},
}
interface GoogleCalendarUnshareCalendarV2Response {
success: boolean
output: {
ruleId: string
deleted: boolean
}
}
export const unshareCalendarV2Tool: ToolConfig<
GoogleCalendarUnshareCalendarParams,
GoogleCalendarUnshareCalendarV2Response
> = {
id: 'google_calendar_unshare_calendar_v2',
name: 'Google Calendar Remove Sharing',
description:
'Revoke an access control rule (sharing) from a calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: unshareCalendarTool.oauth,
params: unshareCalendarTool.params,
request: unshareCalendarTool.request,
transformResponse: async (response: Response, params) => {
if (response.status === 204 || response.ok) {
return {
success: true,
output: {
ruleId: params?.ruleId || '',
deleted: true,
},
}
}
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.error?.message || 'Failed to remove sharing rule')
},
outputs: {
ruleId: { type: 'string', description: 'Removed ACL rule ID' },
deleted: { type: 'boolean', description: 'Whether removal was successful' },
},
}
+283
View File
@@ -0,0 +1,283 @@
import {
CALENDAR_API_BASE,
type CalendarAttendee,
type GoogleCalendarApiEventResponse,
type GoogleCalendarEventRequestBody,
type GoogleCalendarUpdateParams,
type GoogleCalendarUpdateResponse,
} from '@/tools/google_calendar/types'
import {
assertRecurringTimeZone,
buildEventDateTime,
buildGoogleMeetConferenceData,
normalizeAttendees,
normalizeRecurrence,
} from '@/tools/google_calendar/utils'
import type { ToolConfig } from '@/tools/types'
type EventPatchBody = Partial<GoogleCalendarEventRequestBody>
export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUpdateResponse> = {
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',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Google Calendar ID (e.g., primary or calendar@group.calendar.google.com)',
},
eventId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Google Calendar event ID to update',
},
summary: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New event title/summary',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New event description',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New event location',
},
startDateTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'New start time. Use a datetime with timezone offset (2025-06-03T10:00:00-08:00) or a date (2025-06-03) for an all-day event',
},
endDateTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'New end time. Use a datetime with timezone offset (2025-06-03T11:00:00-08:00) or a date (2025-06-04) for an all-day event',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'IANA time zone (e.g., America/Los_Angeles) applied to the start/end times provided in this update. Provide a new start and/or end time to change the time zone; a time zone on its own is not applied. Required for recurring events to expand the recurrence correctly.',
},
attendees: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description:
'Array of attendee email addresses. When one or more emails are provided, they replace the existing attendee list. Leaving this empty keeps the current attendees unchanged (it does not clear them).',
},
recurrence: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
"Recurrence rule(s) in RFC 5545 format (e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR). Separate multiple rules with newlines. When provided, replaces the event's recurrence; leaving it empty keeps the existing recurrence unchanged. Requires a timeZone for timed events.",
},
addGoogleMeet: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Attach a Google Meet video conference link to the event',
},
sendUpdates: {
type: 'string',
required: false,
visibility: 'user-only',
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)
}
if (params.addGoogleMeet) {
queryParams.append('conferenceDataVersion', '1')
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}${queryString ? `?${queryString}` : ''}`
},
method: 'PATCH',
headers: (params: GoogleCalendarUpdateParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarUpdateParams): EventPatchBody => {
const updateData: EventPatchBody = {}
const recurrence = normalizeRecurrence(params.recurrence)
const isRecurring = recurrence.length > 0
if (isRecurring) {
assertRecurringTimeZone([params.startDateTime, params.endDateTime], params.timeZone)
}
if (params.summary !== undefined) {
updateData.summary = params.summary
}
if (params.description !== undefined) {
updateData.description = params.description
}
if (params.location !== undefined) {
updateData.location = params.location
}
if (params.startDateTime !== undefined) {
updateData.start = buildEventDateTime(params.startDateTime, params.timeZone)
}
if (params.endDateTime !== undefined) {
updateData.end = buildEventDateTime(params.endDateTime, params.timeZone)
}
const attendees = normalizeAttendees(params.attendees)
if (attendees.length > 0) {
updateData.attendees = attendees
}
if (isRecurring) {
updateData.recurrence = recurrence
}
if (params.addGoogleMeet) {
updateData.conferenceData = buildGoogleMeetConferenceData()
}
return updateData
},
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
content: `Event "${data.summary}" updated successfully`,
metadata: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink,
status: data.status,
summary: data.summary,
description: data.description,
location: data.location,
recurrence: data.recurrence,
start: data.start,
end: data.end,
attendees: data.attendees,
creator: data.creator,
organizer: data.organizer,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Event update confirmation message' },
metadata: {
type: 'json',
description: 'Updated event metadata including ID, status, Meet link, and details',
},
},
}
interface GoogleCalendarUpdateV2Response {
success: boolean
output: {
id: string
htmlLink: string
hangoutLink: string | null
status: string
summary: string | null
description: string | null
location: string | null
recurrence: string[] | null
start: GoogleCalendarApiEventResponse['start']
end: GoogleCalendarApiEventResponse['end']
attendees: CalendarAttendee[] | null
creator: GoogleCalendarApiEventResponse['creator'] | null
organizer: GoogleCalendarApiEventResponse['organizer'] | null
}
}
export const updateV2Tool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUpdateV2Response> =
{
id: 'google_calendar_update_v2',
name: 'Google Calendar Update Event',
description: 'Update an existing event in Google Calendar. Returns API-aligned fields only.',
version: '2.0.0',
oauth: updateTool.oauth,
params: updateTool.params,
request: updateTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiEventResponse = await response.json()
return {
success: true,
output: {
id: data.id,
htmlLink: data.htmlLink,
hangoutLink: data.hangoutLink ?? null,
status: data.status,
summary: data.summary ?? null,
description: data.description ?? null,
location: data.location ?? null,
recurrence: data.recurrence ?? null,
start: data.start,
end: data.end,
attendees: data.attendees ?? null,
creator: data.creator ?? null,
organizer: data.organizer ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Event ID' },
htmlLink: { type: 'string', description: 'Event link' },
hangoutLink: { type: 'string', description: 'Google Meet link', optional: true },
status: { type: 'string', description: 'Event status' },
summary: { type: 'string', description: 'Event title', optional: true },
description: { type: 'string', description: 'Event description', optional: true },
location: { type: 'string', description: 'Event location', optional: true },
recurrence: { type: 'json', description: 'Recurrence rules', optional: true },
start: { type: 'json', description: 'Event start' },
end: { type: 'json', description: 'Event end' },
attendees: { type: 'json', description: 'Event attendees', optional: true },
creator: { type: 'json', description: 'Event creator', optional: true },
organizer: { type: 'json', description: 'Event organizer', optional: true },
},
}
@@ -0,0 +1,158 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarAclRole,
type GoogleCalendarApiAclRule,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export interface GoogleCalendarUpdateAclParams {
accessToken: string
calendarId?: string
ruleId: string
role: GoogleCalendarAclRole
sendNotifications?: boolean
}
export interface GoogleCalendarUpdateAclResponse {
success: boolean
output: {
content: string
metadata: {
id: string
role: string
scope: { type: string; value?: string }
}
}
}
const buildUpdateAclUrl = (params: GoogleCalendarUpdateAclParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
const queryParams = new URLSearchParams()
if (params.sendNotifications !== undefined) {
queryParams.append('sendNotifications', String(params.sendNotifications))
}
const queryString = queryParams.toString()
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(params.ruleId.trim())}${queryString ? `?${queryString}` : ''}`
}
export const updateAclTool: ToolConfig<
GoogleCalendarUpdateAclParams,
GoogleCalendarUpdateAclResponse
> = {
id: 'google_calendar_update_acl',
name: 'Google Calendar Update Sharing',
description: 'Change the access role granted by an existing calendar sharing (ACL) rule',
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to modify (e.g., primary or calendar@group.calendar.google.com)',
},
ruleId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ACL rule ID to update (e.g., user:person@example.com)',
},
role: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New access role to grant: freeBusyReader, reader, writer, or owner',
},
sendNotifications: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to send a notification email about the change. Defaults to true.',
},
},
request: {
url: buildUpdateAclUrl,
method: 'PATCH',
headers: (params: GoogleCalendarUpdateAclParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params: GoogleCalendarUpdateAclParams) => ({ role: params.role }),
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclRule = await response.json()
return {
success: true,
output: {
content: `Updated sharing rule for ${data.scope?.value || data.scope?.type} to ${data.role}`,
metadata: {
id: data.id,
role: data.role,
scope: data.scope,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Sharing update confirmation message' },
metadata: {
type: 'json',
description: 'Updated ACL rule (id, role, scope)',
},
},
}
interface GoogleCalendarUpdateAclV2Response {
success: boolean
output: {
id: string
role: string
scope: { type: string; value?: string }
}
}
export const updateAclV2Tool: ToolConfig<
GoogleCalendarUpdateAclParams,
GoogleCalendarUpdateAclV2Response
> = {
id: 'google_calendar_update_acl_v2',
name: 'Google Calendar Update Sharing',
description:
'Change the access role granted by an existing calendar sharing (ACL) rule. Returns API-aligned fields only.',
version: '2.0.0',
oauth: updateAclTool.oauth,
params: updateAclTool.params,
request: updateAclTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiAclRule = await response.json()
return {
success: true,
output: {
id: data.id,
role: data.role,
scope: data.scope,
},
}
},
outputs: {
id: { type: 'string', description: 'ACL rule ID' },
role: { type: 'string', description: 'Granted access role' },
scope: { type: 'json', description: 'Grantee scope (type and value)' },
},
}
@@ -0,0 +1,177 @@
import {
CALENDAR_API_BASE,
type GoogleCalendarApiCalendarResponse,
} from '@/tools/google_calendar/types'
import type { ToolConfig } from '@/tools/types'
export interface GoogleCalendarUpdateCalendarParams {
accessToken: string
calendarId?: string
summary?: string
description?: string
location?: string
timeZone?: string
}
export interface GoogleCalendarUpdateCalendarResponse {
success: boolean
output: {
content: string
metadata: {
id: string
summary: string
description?: string
location?: string
timeZone?: string
}
}
}
const buildUpdateCalendarUrl = (params: GoogleCalendarUpdateCalendarParams) => {
const calendarId = params.calendarId?.trim() || 'primary'
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}`
}
const buildUpdateCalendarBody = (params: GoogleCalendarUpdateCalendarParams) => {
const body: Record<string, string> = {}
if (params.summary !== undefined) body.summary = params.summary
if (params.description !== undefined) body.description = params.description
if (params.location !== undefined) body.location = params.location
if (params.timeZone !== undefined) body.timeZone = params.timeZone
return body
}
export const updateCalendarTool: ToolConfig<
GoogleCalendarUpdateCalendarParams,
GoogleCalendarUpdateCalendarResponse
> = {
id: 'google_calendar_update_calendar',
name: 'Google Calendar Update Calendar',
description: "Update a secondary calendar's metadata (title, description, location, time zone)",
version: '1.0.0',
oauth: {
required: true,
provider: 'google-calendar',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Access token for Google Calendar API',
},
calendarId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Calendar ID to update (e.g., primary or calendar@group.calendar.google.com)',
},
summary: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the calendar',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New description for the calendar',
},
location: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New geographic location of the calendar as free-form text',
},
timeZone: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New time zone of the calendar as an IANA name (e.g., America/Los_Angeles)',
},
},
request: {
url: buildUpdateCalendarUrl,
method: 'PATCH',
headers: (params: GoogleCalendarUpdateCalendarParams) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: buildUpdateCalendarBody,
},
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiCalendarResponse = await response.json()
return {
success: true,
output: {
content: `Calendar "${data.summary}" updated successfully`,
metadata: {
id: data.id,
summary: data.summary,
description: data.description,
location: data.location,
timeZone: data.timeZone,
},
},
}
},
outputs: {
content: { type: 'string', description: 'Calendar update confirmation message' },
metadata: {
type: 'json',
description: 'Updated calendar metadata (id, summary, description, location, timeZone)',
},
},
}
interface GoogleCalendarUpdateCalendarV2Response {
success: boolean
output: {
id: string
summary: string
description: string | null
location: string | null
timeZone: string | null
}
}
export const updateCalendarV2Tool: ToolConfig<
GoogleCalendarUpdateCalendarParams,
GoogleCalendarUpdateCalendarV2Response
> = {
id: 'google_calendar_update_calendar_v2',
name: 'Google Calendar Update Calendar',
description: "Update a secondary calendar's metadata. Returns API-aligned fields only.",
version: '2.0.0',
oauth: updateCalendarTool.oauth,
params: updateCalendarTool.params,
request: updateCalendarTool.request,
transformResponse: async (response: Response) => {
const data: GoogleCalendarApiCalendarResponse = await response.json()
return {
success: true,
output: {
id: data.id,
summary: data.summary,
description: data.description ?? null,
location: data.location ?? null,
timeZone: data.timeZone ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Calendar ID' },
summary: { type: 'string', description: 'Calendar title' },
description: { type: 'string', description: 'Calendar description', optional: true },
location: { type: 'string', description: 'Calendar location', optional: true },
timeZone: { type: 'string', description: 'Calendar time zone', optional: true },
},
}
+88
View File
@@ -0,0 +1,88 @@
import { generateId } from '@sim/utils/id'
import type { GoogleCalendarEventRequestBody } from '@/tools/google_calendar/types'
type EventDateTime = GoogleCalendarEventRequestBody['start']
const DEFAULT_TIME_ZONE = 'America/Los_Angeles'
const TZ_OFFSET_PATTERN = /([+-]\d{2}:?\d{2}|Z)$/
/**
* Build a Google Calendar event date/time object from a user-supplied value.
*
* A date-only value (e.g. `2025-06-03`) produces an all-day `{ date }` object.
* A datetime value produces `{ dateTime, timeZone? }`. An explicitly provided
* timezone always wins. Otherwise a default zone is attached only for "naive"
* datetimes that carry no UTC offset — when an offset is present it is authoritative
* and is never overridden with a guessed zone, which would misalign the time.
*
* For recurring events the Calendar API requires a named `timeZone` on start/end;
* callers should pass the user's timezone explicitly (an RFC3339 offset alone is
* insufficient to expand a recurrence across DST).
*/
export function buildEventDateTime(value: string, timeZone: string | undefined): EventDateTime {
const isDateOnly = !value.includes('T')
if (isDateOnly) {
return { date: value }
}
const hasOffset = TZ_OFFSET_PATTERN.test(value)
const result: EventDateTime = { dateTime: value }
if (timeZone) {
result.timeZone = timeZone
} else if (!hasOffset) {
result.timeZone = DEFAULT_TIME_ZONE
}
return result
}
/** Normalize a comma/newline-separated string or array of attendee emails into `[{ email }]`. */
export function normalizeAttendees(
attendees: string | string[] | undefined
): Array<{ email: string }> {
if (!attendees) return []
const list = Array.isArray(attendees)
? attendees
: attendees.split(',').map((email) => email.trim())
return list.filter((email) => email.length > 0).map((email) => ({ email }))
}
/**
* Recurring events require a named `timeZone` on their timed start/end — the Calendar API
* rejects them otherwise, and an RFC3339 offset is not a substitute (an IANA zone cannot be
* derived from a fixed offset). Throws a clear error so we fail fast with guidance instead of
* silently guessing a zone (which would misalign the recurrence) or sending an invalid request.
* All-day recurring events (date-only values) do not need a timezone and are allowed.
*/
export function assertRecurringTimeZone(
dateTimes: Array<string | undefined>,
timeZone: string | undefined
): void {
if (timeZone) return
const hasTimedValue = dateTimes.some((value) => value?.includes('T'))
if (hasTimedValue) {
throw new Error(
'Recurring events require a time zone. Provide the timeZone parameter (an IANA name, e.g. America/New_York).'
)
}
}
/** Normalize recurrence rules (single string, newline-separated string, or array) into an array. */
export function normalizeRecurrence(recurrence: string | string[] | undefined): string[] {
if (!recurrence) return []
const list = Array.isArray(recurrence) ? recurrence : recurrence.split('\n')
return list.map((rule) => rule.trim()).filter((rule) => rule.length > 0)
}
/** Build a `conferenceData.createRequest` payload that asks Google to attach a Meet link. */
export function buildGoogleMeetConferenceData(): GoogleCalendarEventRequestBody['conferenceData'] {
return {
createRequest: {
requestId: generateId(),
conferenceSolutionKey: { type: 'hangoutsMeet' },
},
}
}