mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(google-calendar): wire freebusy, align tools with API v3, add calendar + sharing tools (#5084)
* feat(google-calendar): wire freebusy, align tools with API v3, add calendar + sharing tools * fix(google-calendar): address review — trust offset timezones, make list_acl showDeleted usable, harden unshare error parse, clarify update attendees * fix(google-calendar): wire list q/pageToken into block, harden invite PUT error parse * fix(google-calendar): make list orderBy user-selectable, clarify update timeZone applies to start/end * fix(google-calendar): require timeZone for recurring timed events, clarify recurrence replace semantics * fix(google-calendar): validate grantee before building share ACL body Throw a clear error when scopeType is user/group/domain but scopeValue is missing or blank, instead of POSTing a scope-type-only body that the Calendar ACL API rejects with an opaque error.
This commit is contained in:
@@ -48,10 +48,12 @@ Create a new event in Google Calendar. Returns API-aligned fields only.
|
||||
| `summary` | string | Yes | Event title/summary |
|
||||
| `description` | string | No | Event description |
|
||||
| `location` | string | No | Event location |
|
||||
| `startDateTime` | string | Yes | Start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter |
|
||||
| `endDateTime` | string | Yes | End date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter |
|
||||
| `startDateTime` | string | Yes | 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` | string | Yes | 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` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. Defaults to America/Los_Angeles if not provided. |
|
||||
| `attendees` | array | No | Array of attendee email addresses |
|
||||
| `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. |
|
||||
| `addGoogleMeet` | boolean | No | Attach a Google Meet video conference link to the event |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
|
||||
#### Output
|
||||
@@ -60,10 +62,12 @@ Create a new event in Google Calendar. Returns API-aligned fields only.
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Event ID |
|
||||
| `htmlLink` | string | Event link |
|
||||
| `hangoutLink` | string | Google Meet link |
|
||||
| `status` | string | Event status |
|
||||
| `summary` | string | Event title |
|
||||
| `description` | string | Event description |
|
||||
| `location` | string | Event location |
|
||||
| `recurrence` | json | Recurrence rules |
|
||||
| `start` | json | Event start |
|
||||
| `end` | json | Event end |
|
||||
| `attendees` | json | Event attendees |
|
||||
@@ -81,7 +85,10 @@ List events from Google Calendar. Returns API-aligned fields only.
|
||||
| `calendarId` | string | No | Google Calendar ID \(e.g., primary or calendar@group.calendar.google.com\) |
|
||||
| `timeMin` | string | No | Lower bound for events \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
|
||||
| `timeMax` | string | No | Upper bound for events \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
|
||||
| `orderBy` | string | No | Order of events returned \(startTime or updated\) |
|
||||
| `q` | string | No | Free-text search across event summary, description, location, attendees, and organizer |
|
||||
| `maxResults` | number | No | Maximum number of events to return \(max 2500\) |
|
||||
| `pageToken` | string | No | Token for retrieving the next page of results |
|
||||
| `orderBy` | string | No | Order of events returned \(startTime or updated\). Defaults to startTime. |
|
||||
| `showDeleted` | boolean | No | Include deleted events |
|
||||
|
||||
#### Output
|
||||
@@ -132,10 +139,12 @@ Update an existing event in Google Calendar. Returns API-aligned fields only.
|
||||
| `summary` | string | No | New event title/summary |
|
||||
| `description` | string | No | New event description |
|
||||
| `location` | string | No | New event location |
|
||||
| `startDateTime` | string | No | New start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter |
|
||||
| `endDateTime` | string | No | New end date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter |
|
||||
| `startDateTime` | string | No | 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` | string | No | 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` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. |
|
||||
| `attendees` | array | No | Array of attendee email addresses \(replaces existing attendees\) |
|
||||
| `attendees` | array | No | Array of attendee email addresses \(replaces the existing attendee list\) |
|
||||
| `recurrence` | string | No | Recurrence rule\(s\) in RFC 5545 format \(e.g., RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\). Separate multiple rules with newlines. |
|
||||
| `addGoogleMeet` | boolean | No | Attach a Google Meet video conference link to the event |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
|
||||
#### Output
|
||||
@@ -144,10 +153,12 @@ Update an existing event in Google Calendar. Returns API-aligned fields only.
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Event ID |
|
||||
| `htmlLink` | string | Event link |
|
||||
| `hangoutLink` | string | Google Meet link |
|
||||
| `status` | string | Event status |
|
||||
| `summary` | string | Event title |
|
||||
| `description` | string | Event description |
|
||||
| `location` | string | Event location |
|
||||
| `recurrence` | json | Recurrence rules |
|
||||
| `start` | json | Event start |
|
||||
| `end` | json | Event end |
|
||||
| `attendees` | json | Event attendees |
|
||||
@@ -298,7 +309,7 @@ Invite attendees to an existing Google Calendar event. Returns API-aligned field
|
||||
| `calendarId` | string | No | Google Calendar ID \(e.g., primary or calendar@group.calendar.google.com\) |
|
||||
| `eventId` | string | Yes | Google Calendar event ID to invite attendees to |
|
||||
| `attendees` | array | Yes | Array of attendee email addresses to invite |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none \(defaults to all\) |
|
||||
| `replaceExisting` | boolean | No | Whether to replace existing attendees or add to them \(defaults to false\) |
|
||||
|
||||
#### Output
|
||||
@@ -317,6 +328,113 @@ Invite attendees to an existing Google Calendar event. Returns API-aligned field
|
||||
| `creator` | json | Event creator |
|
||||
| `organizer` | json | Event organizer |
|
||||
|
||||
### `google_calendar_freebusy`
|
||||
|
||||
Query free/busy information for one or more Google Calendars. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarIds` | string | Yes | Comma-separated calendar IDs to query \(e.g., "primary,other@example.com"\) |
|
||||
| `timeMin` | string | Yes | Start of the time range \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
|
||||
| `timeMax` | string | Yes | End of the time range \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
|
||||
| `timeZone` | string | No | IANA time zone \(e.g., "UTC", "America/New_York"\). Defaults to UTC. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `timeMin` | string | Start of the queried time range |
|
||||
| `timeMax` | string | End of the queried time range |
|
||||
| `calendars` | json | Per-calendar free/busy data with busy periods and any errors |
|
||||
|
||||
### `google_calendar_create_calendar`
|
||||
|
||||
Create a new secondary calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `summary` | string | Yes | Title of the new calendar |
|
||||
| `description` | string | No | Description of the new calendar |
|
||||
| `location` | string | No | Geographic location of the calendar as free-form text |
|
||||
| `timeZone` | string | No | Time zone of the calendar as an IANA name \(e.g., America/Los_Angeles\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Calendar ID |
|
||||
| `summary` | string | Calendar title |
|
||||
| `description` | string | Calendar description |
|
||||
| `location` | string | Calendar location |
|
||||
| `timeZone` | string | Calendar time zone |
|
||||
|
||||
### `google_calendar_share_calendar`
|
||||
|
||||
Grant a user, group, or domain access to a calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Calendar ID to share \(e.g., primary or calendar@group.calendar.google.com\) |
|
||||
| `role` | string | Yes | Access role to grant: freeBusyReader, reader, writer, or owner |
|
||||
| `scopeType` | string | Yes | Type of grantee: user, group, domain, or default \(public\) |
|
||||
| `scopeValue` | string | No | Email \(user/group\), domain name \(domain\), or empty for default. Required unless scope type is default. |
|
||||
| `sendNotifications` | boolean | No | Whether to send a notification email about the change. Defaults to true. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | ACL rule ID |
|
||||
| `role` | string | Granted access role |
|
||||
| `scope` | json | Grantee scope \(type and value\) |
|
||||
|
||||
### `google_calendar_list_acl`
|
||||
|
||||
List the access control rules (sharing) for a calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Calendar ID to inspect \(e.g., primary or calendar@group.calendar.google.com\) |
|
||||
| `maxResults` | number | No | Maximum number of ACL rules to return |
|
||||
| `pageToken` | string | No | Token for retrieving subsequent pages of results |
|
||||
| `showDeleted` | boolean | No | Include deleted ACL rules \(with role "none"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `nextPageToken` | string | Next page token |
|
||||
| `rules` | array | List of ACL rules |
|
||||
| ↳ `id` | string | ACL rule ID |
|
||||
| ↳ `role` | string | Access role |
|
||||
| ↳ `scope` | json | Grantee scope \(type and value\) |
|
||||
|
||||
### `google_calendar_unshare_calendar`
|
||||
|
||||
Revoke an access control rule (sharing) from a calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Calendar ID to modify \(e.g., primary or calendar@group.calendar.google.com\) |
|
||||
| `ruleId` | string | Yes | ACL rule ID to remove \(e.g., user:person@example.com\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ruleId` | string | Removed ACL rule ID |
|
||||
| `deleted` | boolean | Whether removal was successful |
|
||||
|
||||
|
||||
|
||||
## Triggers
|
||||
|
||||
@@ -35,6 +35,11 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
{ label: 'List Calendars', id: 'list_calendars' },
|
||||
{ label: 'Quick Add (Natural Language)', id: 'quick_add' },
|
||||
{ label: 'Invite Attendees', id: 'invite' },
|
||||
{ label: 'Check Free/Busy', id: 'freebusy' },
|
||||
{ label: 'Create Calendar', id: 'create_calendar' },
|
||||
{ label: 'Share Calendar', id: 'share_calendar' },
|
||||
{ label: 'List Sharing', id: 'list_acl' },
|
||||
{ label: 'Remove Sharing', id: 'unshare_calendar' },
|
||||
],
|
||||
value: () => 'create',
|
||||
},
|
||||
@@ -59,7 +64,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
required: true,
|
||||
},
|
||||
...SERVICE_ACCOUNT_SUBBLOCKS,
|
||||
// Calendar selector (basic mode) - not needed for list_calendars
|
||||
{
|
||||
id: 'calendarId',
|
||||
title: 'Calendar',
|
||||
@@ -71,9 +75,12 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
placeholder: 'Select calendar',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: 'list_calendars', not: true },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_calendars', 'create_calendar', 'freebusy'],
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
// Manual calendar ID input (advanced mode) - not needed for list_calendars
|
||||
{
|
||||
id: 'manualCalendarId',
|
||||
title: 'Calendar ID',
|
||||
@@ -82,10 +89,13 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'list_calendars', not: true },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_calendars', 'create_calendar', 'freebusy'],
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
|
||||
// Create Event Fields
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'Event Title',
|
||||
@@ -178,7 +188,47 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
},
|
||||
|
||||
// List Events Fields
|
||||
{
|
||||
id: 'recurrence',
|
||||
title: 'Recurrence Rule',
|
||||
type: 'long-input',
|
||||
placeholder: 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an RFC 5545 recurrence rule (RRULE) for a Google Calendar event based on the user's description.
|
||||
Examples:
|
||||
- "every weekday" -> RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
|
||||
- "every Monday" -> RRULE:FREQ=WEEKLY;BYDAY=MO
|
||||
- "monthly on the 1st" -> RRULE:FREQ=MONTHLY;BYMONTHDAY=1
|
||||
- "daily for 10 occurrences" -> RRULE:FREQ=DAILY;COUNT=10
|
||||
|
||||
Return ONLY the RRULE string - no explanations, no extra text.`,
|
||||
placeholder: 'Describe the recurrence (e.g., "every weekday", "monthly on the 1st")...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'addGoogleMeet',
|
||||
title: 'Add Google Meet',
|
||||
type: 'dropdown',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
mode: 'advanced',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
value: () => 'false',
|
||||
},
|
||||
{
|
||||
id: 'timeZone',
|
||||
title: 'Time Zone',
|
||||
type: 'short-input',
|
||||
placeholder: 'America/Los_Angeles',
|
||||
condition: { field: 'operation', value: ['create', 'update'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'timeMin',
|
||||
title: 'Start Time Filter',
|
||||
@@ -221,8 +271,37 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'q',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'standup',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
},
|
||||
{
|
||||
id: 'orderBy',
|
||||
title: 'Order By',
|
||||
type: 'dropdown',
|
||||
condition: { field: 'operation', value: 'list' },
|
||||
mode: 'advanced',
|
||||
options: [
|
||||
{ label: 'Start time', id: 'startTime' },
|
||||
{ label: 'Last updated', id: 'updated' },
|
||||
],
|
||||
value: () => 'startTime',
|
||||
},
|
||||
{
|
||||
id: 'pageToken',
|
||||
title: 'Page Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Token from a previous response (nextPageToken)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list', 'instances', 'list_calendars', 'list_acl'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Get Event Fields
|
||||
{
|
||||
id: 'eventId',
|
||||
title: 'Event ID',
|
||||
@@ -235,7 +314,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Update Event Fields
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'New Event Title',
|
||||
@@ -325,7 +403,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
|
||||
// Move Event Fields - Destination calendar selector (basic mode)
|
||||
{
|
||||
id: 'destinationCalendar',
|
||||
title: 'Destination Calendar',
|
||||
@@ -340,7 +417,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Move Event Fields - Manual destination calendar ID (advanced mode)
|
||||
{
|
||||
id: 'manualDestinationCalendarId',
|
||||
title: 'Destination Calendar ID',
|
||||
@@ -353,7 +429,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Instances Fields
|
||||
{
|
||||
id: 'timeMin',
|
||||
title: 'Start Time Filter',
|
||||
@@ -401,10 +476,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: '250',
|
||||
condition: { field: 'operation', value: ['instances', 'list_calendars'] },
|
||||
condition: { field: 'operation', value: ['list', 'instances', 'list_calendars', 'list_acl'] },
|
||||
},
|
||||
|
||||
// List Calendars Fields
|
||||
{
|
||||
id: 'minAccessRole',
|
||||
title: 'Minimum Access Role',
|
||||
@@ -419,13 +493,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
},
|
||||
|
||||
// Invite Attendees Fields
|
||||
{
|
||||
id: 'attendees',
|
||||
title: 'Attendees (comma-separated emails)',
|
||||
type: 'short-input',
|
||||
placeholder: 'john@example.com, jane@example.com',
|
||||
condition: { field: 'operation', value: 'invite' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'replaceExisting',
|
||||
@@ -438,7 +512,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
},
|
||||
|
||||
// Quick Add Fields
|
||||
{
|
||||
id: 'text',
|
||||
title: 'Natural Language Event',
|
||||
@@ -470,10 +543,149 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
type: 'short-input',
|
||||
placeholder: 'john@example.com, jane@example.com',
|
||||
condition: { field: 'operation', value: 'quick_add' },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'calendarIds',
|
||||
title: 'Calendars (comma-separated IDs)',
|
||||
type: 'short-input',
|
||||
placeholder: 'primary, teammate@example.com',
|
||||
condition: { field: 'operation', value: 'freebusy' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'timeMin',
|
||||
title: 'Start Time',
|
||||
type: 'short-input',
|
||||
placeholder: '2025-06-03T00:00:00Z',
|
||||
condition: { field: 'operation', value: 'freebusy' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "today" -> Calculate today's date at 00:00:00Z
|
||||
- "next Monday" -> Calculate next Monday at 00:00:00Z
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the start of the range (e.g., "today", "next Monday")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'timeMax',
|
||||
title: 'End Time',
|
||||
type: 'short-input',
|
||||
placeholder: '2025-06-04T00:00:00Z',
|
||||
condition: { field: 'operation', value: 'freebusy' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "end of today" -> Calculate today's date at 23:59:59Z
|
||||
- "next Friday" -> Calculate next Friday at 00:00:00Z
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the end of the range (e.g., "end of today", "next Friday")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'Calendar Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Team Calendar',
|
||||
condition: { field: 'operation', value: 'create_calendar' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
title: 'Calendar Description',
|
||||
type: 'long-input',
|
||||
placeholder: 'Shared team events and milestones',
|
||||
condition: { field: 'operation', value: 'create_calendar' },
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
title: 'Calendar Location',
|
||||
type: 'short-input',
|
||||
placeholder: 'San Francisco, CA',
|
||||
condition: { field: 'operation', value: 'create_calendar' },
|
||||
},
|
||||
{
|
||||
id: 'timeZone',
|
||||
title: 'Time Zone',
|
||||
type: 'short-input',
|
||||
placeholder: 'America/Los_Angeles',
|
||||
condition: { field: 'operation', value: ['create_calendar', 'freebusy'] },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'role',
|
||||
title: 'Access Role',
|
||||
type: 'dropdown',
|
||||
condition: { field: 'operation', value: 'share_calendar' },
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'See free/busy only', id: 'freeBusyReader' },
|
||||
{ label: 'See all event details (Reader)', id: 'reader' },
|
||||
{ label: 'Make changes (Writer)', id: 'writer' },
|
||||
{ label: 'Make changes & manage sharing (Owner)', id: 'owner' },
|
||||
],
|
||||
value: () => 'reader',
|
||||
},
|
||||
{
|
||||
id: 'scopeType',
|
||||
title: 'Grantee Type',
|
||||
type: 'dropdown',
|
||||
condition: { field: 'operation', value: 'share_calendar' },
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'User', id: 'user' },
|
||||
{ label: 'Group', id: 'group' },
|
||||
{ label: 'Domain', id: 'domain' },
|
||||
{ label: 'Public (anyone)', id: 'default' },
|
||||
],
|
||||
value: () => 'user',
|
||||
},
|
||||
{
|
||||
id: 'scopeValue',
|
||||
title: 'Grantee (email or domain)',
|
||||
type: 'short-input',
|
||||
placeholder: 'person@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'share_calendar',
|
||||
and: { field: 'scopeType', value: 'default', not: true },
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'sendNotifications',
|
||||
title: 'Send Notification Email',
|
||||
type: 'dropdown',
|
||||
condition: { field: 'operation', value: 'share_calendar' },
|
||||
mode: 'advanced',
|
||||
options: [
|
||||
{ label: 'Yes', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
value: () => 'true',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ruleId',
|
||||
title: 'ACL Rule ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'user:person@example.com',
|
||||
condition: { field: 'operation', value: 'unshare_calendar' },
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Notification setting (for create, update, delete, move, quick_add, invite)
|
||||
{
|
||||
id: 'sendUpdates',
|
||||
title: 'Send Email Notifications',
|
||||
@@ -502,6 +714,11 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
'google_calendar_list_calendars',
|
||||
'google_calendar_quick_add',
|
||||
'google_calendar_invite',
|
||||
'google_calendar_freebusy',
|
||||
'google_calendar_create_calendar',
|
||||
'google_calendar_share_calendar',
|
||||
'google_calendar_list_acl',
|
||||
'google_calendar_unshare_calendar',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -526,6 +743,16 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
return 'google_calendar_quick_add'
|
||||
case 'invite':
|
||||
return 'google_calendar_invite'
|
||||
case 'freebusy':
|
||||
return 'google_calendar_freebusy'
|
||||
case 'create_calendar':
|
||||
return 'google_calendar_create_calendar'
|
||||
case 'share_calendar':
|
||||
return 'google_calendar_share_calendar'
|
||||
case 'list_acl':
|
||||
return 'google_calendar_list_acl'
|
||||
case 'unshare_calendar':
|
||||
return 'google_calendar_unshare_calendar'
|
||||
default:
|
||||
throw new Error(`Invalid Google Calendar operation: ${params.operation}`)
|
||||
}
|
||||
@@ -541,10 +768,8 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
// Use canonical 'calendarId' param directly
|
||||
const effectiveCalendarId = calendarId ? String(calendarId).trim() : ''
|
||||
|
||||
// Use canonical 'destinationCalendarId' param directly
|
||||
const effectiveDestinationCalendarId = destinationCalendarId
|
||||
? String(destinationCalendarId).trim()
|
||||
: ''
|
||||
@@ -554,30 +779,36 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
calendarId: effectiveCalendarId || 'primary',
|
||||
}
|
||||
|
||||
// Add destination calendar ID for move operation
|
||||
if (operation === 'move' && effectiveDestinationCalendarId) {
|
||||
processedParams.destinationCalendarId = effectiveDestinationCalendarId
|
||||
}
|
||||
|
||||
// Convert comma-separated attendees string to array, only if it has content
|
||||
if (attendees && typeof attendees === 'string' && attendees.trim().length > 0) {
|
||||
const attendeeList = attendees
|
||||
.split(',')
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => email.length > 0)
|
||||
|
||||
// Only add attendees if we have valid entries
|
||||
if (attendeeList.length > 0) {
|
||||
processedParams.attendees = attendeeList
|
||||
}
|
||||
}
|
||||
|
||||
// Convert replaceExisting string to boolean for invite operation
|
||||
if (operation === 'invite' && replaceExisting !== undefined) {
|
||||
processedParams.replaceExisting = replaceExisting === 'true'
|
||||
}
|
||||
|
||||
// Set default sendUpdates to 'all' if not specified for operations that support it
|
||||
if (processedParams.addGoogleMeet !== undefined) {
|
||||
processedParams.addGoogleMeet =
|
||||
processedParams.addGoogleMeet === 'true' || processedParams.addGoogleMeet === true
|
||||
}
|
||||
|
||||
if (operation === 'share_calendar' && processedParams.sendNotifications !== undefined) {
|
||||
processedParams.sendNotifications =
|
||||
processedParams.sendNotifications === 'true' ||
|
||||
processedParams.sendNotifications === true
|
||||
}
|
||||
|
||||
if (
|
||||
['create', 'update', 'delete', 'move', 'quick_add', 'invite'].includes(operation) &&
|
||||
!processedParams.sendUpdates
|
||||
@@ -585,12 +816,10 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
processedParams.sendUpdates = 'all'
|
||||
}
|
||||
|
||||
// Convert maxResults to number if provided
|
||||
if (processedParams.maxResults && typeof processedParams.maxResults === 'string') {
|
||||
processedParams.maxResults = Number.parseInt(processedParams.maxResults, 10)
|
||||
}
|
||||
|
||||
// Remove empty minAccessRole
|
||||
if (processedParams.minAccessRole === '') {
|
||||
processedParams.minAccessRole = undefined
|
||||
}
|
||||
@@ -607,38 +836,44 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
oauthCredential: { type: 'string', description: 'Google Calendar access token' },
|
||||
calendarId: { type: 'string', description: 'Calendar identifier (canonical param)' },
|
||||
|
||||
// Create/Update operation inputs
|
||||
summary: { type: 'string', description: 'Event title' },
|
||||
description: { type: 'string', description: 'Event description' },
|
||||
location: { type: 'string', description: 'Event location' },
|
||||
startDateTime: { type: 'string', description: 'Event start time' },
|
||||
endDateTime: { type: 'string', description: 'Event end time' },
|
||||
attendees: { type: 'string', description: 'Attendee email list' },
|
||||
recurrence: { type: 'string', description: 'Recurrence rule (RRULE)' },
|
||||
addGoogleMeet: { type: 'boolean', description: 'Attach a Google Meet link' },
|
||||
timeZone: { type: 'string', description: 'Time zone (IANA name)' },
|
||||
|
||||
// List/Instances operation inputs
|
||||
timeMin: { type: 'string', description: 'Start time filter' },
|
||||
timeMax: { type: 'string', description: 'End time filter' },
|
||||
q: { type: 'string', description: 'Free-text search query' },
|
||||
orderBy: { type: 'string', description: 'Event ordering (startTime or updated)' },
|
||||
maxResults: { type: 'string', description: 'Maximum number of results' },
|
||||
pageToken: { type: 'string', description: 'Pagination token from a previous response' },
|
||||
|
||||
calendarIds: { type: 'string', description: 'Comma-separated calendar IDs' },
|
||||
|
||||
role: { type: 'string', description: 'Access role to grant' },
|
||||
scopeType: { type: 'string', description: 'Grantee type (user, group, domain, default)' },
|
||||
scopeValue: { type: 'string', description: 'Grantee email or domain' },
|
||||
sendNotifications: { type: 'boolean', description: 'Send sharing notification email' },
|
||||
ruleId: { type: 'string', description: 'ACL rule ID to remove' },
|
||||
|
||||
// Get/Update/Delete/Move/Instances/Invite operation inputs
|
||||
eventId: { type: 'string', description: 'Event identifier' },
|
||||
|
||||
// Move operation inputs
|
||||
destinationCalendarId: {
|
||||
type: 'string',
|
||||
description: 'Destination calendar ID (canonical param)',
|
||||
},
|
||||
|
||||
// List Calendars operation inputs
|
||||
minAccessRole: { type: 'string', description: 'Minimum access role filter' },
|
||||
|
||||
// Quick add inputs
|
||||
text: { type: 'string', description: 'Natural language event' },
|
||||
|
||||
// Invite specific inputs
|
||||
replaceExisting: { type: 'string', description: 'Replace existing attendees' },
|
||||
|
||||
// Common inputs
|
||||
sendUpdates: { type: 'string', description: 'Send email notifications' },
|
||||
},
|
||||
outputs: {
|
||||
@@ -670,6 +905,11 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
|
||||
'google_calendar_list_calendars_v2',
|
||||
'google_calendar_quick_add_v2',
|
||||
'google_calendar_invite_v2',
|
||||
'google_calendar_freebusy_v2',
|
||||
'google_calendar_create_calendar_v2',
|
||||
'google_calendar_share_calendar_v2',
|
||||
'google_calendar_list_acl_v2',
|
||||
'google_calendar_unshare_calendar_v2',
|
||||
],
|
||||
config: {
|
||||
...GoogleCalendarBlock.tools?.config,
|
||||
@@ -682,28 +922,30 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
// Event outputs (create, get, update, move, quick_add, invite)
|
||||
id: { type: 'string', description: 'Event ID' },
|
||||
id: { type: 'string', description: 'Event or calendar ID' },
|
||||
htmlLink: { type: 'string', description: 'Event link' },
|
||||
hangoutLink: { type: 'string', description: 'Google Meet link' },
|
||||
status: { type: 'string', description: 'Event status' },
|
||||
summary: { type: 'string', description: 'Event title' },
|
||||
description: { type: 'string', description: 'Event description' },
|
||||
location: { type: 'string', description: 'Event location' },
|
||||
summary: { type: 'string', description: 'Event or calendar title' },
|
||||
description: { type: 'string', description: 'Event or calendar description' },
|
||||
location: { type: 'string', description: 'Event or calendar location' },
|
||||
recurrence: { type: 'json', description: 'Recurrence rules (RRULE)' },
|
||||
start: { type: 'json', description: 'Event start' },
|
||||
end: { type: 'json', description: 'Event end' },
|
||||
attendees: { type: 'json', description: 'Event attendees' },
|
||||
creator: { type: 'json', description: 'Event creator' },
|
||||
organizer: { type: 'json', description: 'Event organizer' },
|
||||
// List events outputs
|
||||
events: { type: 'json', description: 'List of events (list operation)' },
|
||||
// Delete outputs
|
||||
eventId: { type: 'string', description: 'Deleted event ID' },
|
||||
deleted: { type: 'boolean', description: 'Whether deletion was successful' },
|
||||
// Instances outputs
|
||||
deleted: { type: 'boolean', description: 'Whether deletion/removal was successful' },
|
||||
instances: { type: 'json', description: 'List of recurring event instances' },
|
||||
// List calendars outputs
|
||||
calendars: { type: 'json', description: 'List of calendars' },
|
||||
// Common outputs
|
||||
timeMin: { type: 'string', description: 'Start of the queried free/busy range' },
|
||||
timeMax: { type: 'string', description: 'End of the queried free/busy range' },
|
||||
role: { type: 'string', description: 'Granted access role (share operation)' },
|
||||
scope: { type: 'json', description: 'Grantee scope (share operation)' },
|
||||
rules: { type: 'json', description: 'List of ACL sharing rules (list sharing operation)' },
|
||||
ruleId: { type: 'string', description: 'Removed ACL rule ID (remove sharing operation)' },
|
||||
nextPageToken: { type: 'string', description: 'Next page token' },
|
||||
timeZone: { type: 'string', description: 'Calendar time zone' },
|
||||
},
|
||||
|
||||
@@ -5901,9 +5901,29 @@
|
||||
{
|
||||
"name": "Invite Attendees",
|
||||
"description": "Invite attendees to an existing Google Calendar event"
|
||||
},
|
||||
{
|
||||
"name": "Check Free/Busy",
|
||||
"description": "Query free/busy information for one or more Google Calendars"
|
||||
},
|
||||
{
|
||||
"name": "Create Calendar",
|
||||
"description": "Create a new secondary calendar"
|
||||
},
|
||||
{
|
||||
"name": "Share Calendar",
|
||||
"description": "Grant a user, group, or domain access to a calendar by creating an ACL rule"
|
||||
},
|
||||
{
|
||||
"name": "List Sharing",
|
||||
"description": "List the access control rules (sharing) for a calendar"
|
||||
},
|
||||
{
|
||||
"name": "Remove Sharing",
|
||||
"description": "Revoke an access control rule (sharing) from a calendar"
|
||||
}
|
||||
],
|
||||
"operationCount": 10,
|
||||
"operationCount": 15,
|
||||
"triggers": [
|
||||
{
|
||||
"id": "google_calendar_poller",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
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> = {
|
||||
@@ -54,22 +62,21 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Start date and time. MUST include timezone offset (e.g., 2025-06-03T10:00:00-08:00) OR provide timeZone parameter',
|
||||
'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 date and time. MUST include timezone offset (e.g., 2025-06-03T11:00:00-08:00) OR provide timeZone parameter',
|
||||
'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:
|
||||
'Time zone (e.g., America/Los_Angeles). Required if datetime does not include offset. Defaults to America/Los_Angeles if not provided.',
|
||||
default: 'America/Los_Angeles',
|
||||
'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',
|
||||
@@ -77,6 +84,19 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
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,
|
||||
@@ -93,11 +113,12 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
if (params.sendUpdates !== undefined) {
|
||||
queryParams.append('sendUpdates', params.sendUpdates)
|
||||
}
|
||||
if (params.addGoogleMeet) {
|
||||
queryParams.append('conferenceDataVersion', '1')
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const finalUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
return finalUrl
|
||||
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events${queryString ? `?${queryString}` : ''}`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: GoogleCalendarCreateParams) => ({
|
||||
@@ -105,21 +126,17 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: GoogleCalendarCreateParams): GoogleCalendarEventRequestBody => {
|
||||
// Default timezone if not provided and datetime doesn't include offset
|
||||
const timeZone = params.timeZone || 'America/Los_Angeles'
|
||||
const needsTimezone =
|
||||
!params.startDateTime.includes('+') && !params.startDateTime.includes('-', 10)
|
||||
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: {
|
||||
dateTime: params.startDateTime,
|
||||
...(needsTimezone ? { timeZone } : {}),
|
||||
},
|
||||
end: {
|
||||
dateTime: params.endDateTime,
|
||||
...(needsTimezone ? { timeZone } : {}),
|
||||
},
|
||||
start: buildEventDateTime(params.startDateTime, params.timeZone),
|
||||
end: buildEventDateTime(params.endDateTime, params.timeZone),
|
||||
}
|
||||
|
||||
if (params.description) {
|
||||
@@ -130,29 +147,17 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
eventData.location = params.location
|
||||
}
|
||||
|
||||
// Always set timezone if explicitly provided
|
||||
if (params.timeZone) {
|
||||
eventData.start.timeZone = params.timeZone
|
||||
eventData.end.timeZone = params.timeZone
|
||||
const attendees = normalizeAttendees(params.attendees)
|
||||
if (attendees.length > 0) {
|
||||
eventData.attendees = attendees
|
||||
}
|
||||
|
||||
// Handle both string and array cases for attendees
|
||||
let attendeeList: string[] = []
|
||||
if (params.attendees) {
|
||||
const attendees = params.attendees as string | string[]
|
||||
if (Array.isArray(attendees)) {
|
||||
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
|
||||
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
|
||||
// Convert comma-separated string to array
|
||||
attendeeList = attendees
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
}
|
||||
if (isRecurring) {
|
||||
eventData.recurrence = recurrence
|
||||
}
|
||||
|
||||
if (attendeeList.length > 0) {
|
||||
eventData.attendees = attendeeList.map((email: string) => ({ email }))
|
||||
if (params.addGoogleMeet) {
|
||||
eventData.conferenceData = buildGoogleMeetConferenceData()
|
||||
}
|
||||
|
||||
return eventData
|
||||
@@ -169,10 +174,12 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
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,
|
||||
@@ -187,7 +194,7 @@ export const createTool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendarCr
|
||||
content: { type: 'string', description: 'Event creation confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Created event metadata including ID, status, and details',
|
||||
description: 'Created event metadata including ID, status, Meet link, and details',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -197,15 +204,17 @@ interface GoogleCalendarCreateV2Response {
|
||||
output: {
|
||||
id: string
|
||||
htmlLink: string
|
||||
hangoutLink: string | null
|
||||
status: string
|
||||
summary: string | null
|
||||
description: string | null
|
||||
location: string | null
|
||||
start: any
|
||||
end: any
|
||||
attendees: any | null
|
||||
creator: any
|
||||
organizer: any
|
||||
recurrence: string[] | null
|
||||
start: GoogleCalendarApiEventResponse['start']
|
||||
end: GoogleCalendarApiEventResponse['end']
|
||||
attendees: CalendarAttendee[] | null
|
||||
creator: GoogleCalendarApiEventResponse['creator'] | null
|
||||
organizer: GoogleCalendarApiEventResponse['organizer'] | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,29 +235,33 @@ export const createV2Tool: ToolConfig<GoogleCalendarCreateParams, GoogleCalendar
|
||||
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,
|
||||
organizer: data.organizer,
|
||||
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' },
|
||||
organizer: { type: 'json', description: 'Event organizer' },
|
||||
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 },
|
||||
},
|
||||
}
|
||||
@@ -70,7 +70,6 @@ export const deleteTool: ToolConfig<GoogleCalendarDeleteParams, GoogleCalendarDe
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
// DELETE returns 204 No Content on success
|
||||
if (response.status === 204 || response.ok) {
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -1,35 +1,50 @@
|
||||
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 { 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'
|
||||
|
||||
export const googleCalendarCreateTool = createTool
|
||||
export const googleCalendarCreateCalendarTool = createCalendarTool
|
||||
export const googleCalendarDeleteTool = deleteTool
|
||||
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 googleCalendarCreateV2Tool = createV2Tool
|
||||
export const googleCalendarCreateCalendarV2Tool = createCalendarV2Tool
|
||||
export const googleCalendarDeleteV2Tool = deleteV2Tool
|
||||
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
|
||||
|
||||
@@ -1,10 +1,102 @@
|
||||
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',
|
||||
@@ -45,7 +137,7 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'How to send updates to attendees: all, externalOnly, or none',
|
||||
description: 'How to send updates to attendees: all, externalOnly, or none (defaults to all)',
|
||||
},
|
||||
replaceExisting: {
|
||||
type: 'boolean',
|
||||
@@ -57,8 +149,8 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
|
||||
|
||||
request: {
|
||||
url: (params: GoogleCalendarInviteParams) => {
|
||||
const calendarId = params.calendarId || 'primary'
|
||||
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
|
||||
const calendarId = params.calendarId?.trim() || 'primary'
|
||||
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId.trim())}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: GoogleCalendarInviteParams) => ({
|
||||
@@ -68,163 +160,29 @@ export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarIn
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const existingEvent = await response.json()
|
||||
const { data, totalAttendees, newAttendeesAdded, shouldReplace } = await inviteAttendees(
|
||||
response,
|
||||
params
|
||||
)
|
||||
|
||||
// Validate required fields exist
|
||||
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
|
||||
throw new Error('Existing event is missing required fields (start, end, or summary)')
|
||||
}
|
||||
|
||||
// Process new attendees - handle both string and array formats
|
||||
let newAttendeeList: string[] = []
|
||||
|
||||
if (params?.attendees) {
|
||||
if (Array.isArray(params.attendees)) {
|
||||
// Already an array from block processing
|
||||
newAttendeeList = params.attendees.filter(
|
||||
(email: string) => email && email.trim().length > 0
|
||||
)
|
||||
} else if (
|
||||
typeof (params.attendees as any) === 'string' &&
|
||||
(params.attendees as any).trim().length > 0
|
||||
) {
|
||||
// Fallback: process comma-separated string if block didn't convert it
|
||||
newAttendeeList = (params.attendees as any)
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final attendees list
|
||||
const existingAttendees = existingEvent.attendees || []
|
||||
let finalAttendees: Array<any> = []
|
||||
|
||||
// Handle replaceExisting properly - check for both boolean true and string "true"
|
||||
const shouldReplace =
|
||||
params?.replaceExisting === true || (params?.replaceExisting as any) === 'true'
|
||||
|
||||
if (shouldReplace) {
|
||||
// Replace all attendees with just the new ones
|
||||
finalAttendees = newAttendeeList.map((email: string) => ({
|
||||
email,
|
||||
responseStatus: 'needsAction',
|
||||
}))
|
||||
} else {
|
||||
// Add to existing attendees (preserve all existing ones)
|
||||
|
||||
// Start with ALL existing attendees - preserve them completely
|
||||
finalAttendees = [...existingAttendees]
|
||||
|
||||
// Get set of existing emails for duplicate checking (case-insensitive)
|
||||
const existingEmails = new Set(
|
||||
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
|
||||
)
|
||||
|
||||
// Add only new attendees that don't already exist
|
||||
for (const newEmail of newAttendeeList) {
|
||||
const emailLower = newEmail.toLowerCase()
|
||||
if (!existingEmails.has(emailLower)) {
|
||||
finalAttendees.push({
|
||||
email: newEmail,
|
||||
responseStatus: 'needsAction',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the complete existing event object and only modify the attendees field
|
||||
// This is crucial because the Google Calendar API update method "does not support patch semantics
|
||||
// and always updates the entire event resource" according to the documentation
|
||||
const updatedEvent = {
|
||||
...existingEvent, // Start with the complete existing event to preserve all fields
|
||||
attendees: finalAttendees, // Only modify the attendees field
|
||||
}
|
||||
|
||||
// Remove read-only fields that shouldn't be included in updates
|
||||
const readOnlyFields = [
|
||||
'id',
|
||||
'etag',
|
||||
'kind',
|
||||
'created',
|
||||
'updated',
|
||||
'htmlLink',
|
||||
'iCalUID',
|
||||
'sequence',
|
||||
'creator',
|
||||
'organizer',
|
||||
]
|
||||
readOnlyFields.forEach((field) => {
|
||||
delete updatedEvent[field]
|
||||
})
|
||||
|
||||
// Construct PUT URL with query parameters
|
||||
const calendarId = params?.calendarId || 'primary'
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.sendUpdates !== undefined) {
|
||||
queryParams.append('sendUpdates', params.sendUpdates)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
// Send PUT request to update the event
|
||||
const putResponse = await fetch(putUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedEvent),
|
||||
})
|
||||
|
||||
// Handle the PUT response
|
||||
if (!putResponse.ok) {
|
||||
const errorData = await putResponse.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to invite attendees to calendar event')
|
||||
}
|
||||
|
||||
const data = await putResponse.json()
|
||||
const totalAttendees = data.attendees?.length || 0
|
||||
|
||||
// Calculate how many new attendees were actually added
|
||||
let newAttendeesAdded = 0
|
||||
|
||||
if (shouldReplace) {
|
||||
newAttendeesAdded = newAttendeeList.length
|
||||
} else {
|
||||
// Count how many of the new emails weren't already in the existing list
|
||||
const existingEmails = new Set(
|
||||
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
|
||||
)
|
||||
newAttendeesAdded = newAttendeeList.filter(
|
||||
(email) => !existingEmails.has(email.toLowerCase())
|
||||
).length
|
||||
}
|
||||
|
||||
// Improved messaging about email delivery
|
||||
let baseMessage: string
|
||||
if (shouldReplace) {
|
||||
baseMessage = `Successfully updated event "${data.summary}" with ${totalAttendees} attendee${totalAttendees !== 1 ? 's' : ''}`
|
||||
} else if (newAttendeesAdded > 0) {
|
||||
baseMessage = `Successfully added ${newAttendeesAdded} new attendee${newAttendeesAdded !== 1 ? 's' : ''} to event "${data.summary}" (total: ${totalAttendees})`
|
||||
} else {
|
||||
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})`
|
||||
}
|
||||
baseMessage = `No new attendees added to event "${data.summary}" - all specified attendees were already invited (total: ${totalAttendees})`
|
||||
}
|
||||
|
||||
const emailNote =
|
||||
params?.sendUpdates !== 'none'
|
||||
? ` Email invitations are being sent asynchronously - delivery may take a few minutes and depends on recipients' Google Calendar settings.`
|
||||
: ` No email notifications will be sent as requested.`
|
||||
|
||||
const content = baseMessage + emailNote
|
||||
? ' 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,
|
||||
content: baseMessage + emailNote,
|
||||
metadata: {
|
||||
id: data.id,
|
||||
htmlLink: data.htmlLink,
|
||||
@@ -258,16 +216,16 @@ interface GoogleCalendarInviteV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
id: string
|
||||
htmlLink?: string
|
||||
status?: string
|
||||
summary?: string
|
||||
description?: string
|
||||
location?: string
|
||||
start?: any
|
||||
end?: any
|
||||
attendees?: any
|
||||
creator?: any
|
||||
organizer?: any
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,104 +240,7 @@ export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendar
|
||||
params: inviteTool.params,
|
||||
request: inviteTool.request,
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const existingEvent = await response.json()
|
||||
|
||||
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
|
||||
throw new Error('Existing event is missing required fields (start, end, or summary)')
|
||||
}
|
||||
|
||||
let newAttendeeList: string[] = []
|
||||
|
||||
if (params?.attendees) {
|
||||
if (Array.isArray(params.attendees)) {
|
||||
newAttendeeList = params.attendees.filter(
|
||||
(email: string) => email && email.trim().length > 0
|
||||
)
|
||||
} else if (
|
||||
typeof (params.attendees as any) === 'string' &&
|
||||
(params.attendees as any).trim().length > 0
|
||||
) {
|
||||
newAttendeeList = (params.attendees as any)
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
const existingAttendees = existingEvent.attendees || []
|
||||
let finalAttendees: Array<any> = []
|
||||
|
||||
const shouldReplace =
|
||||
params?.replaceExisting === true || (params?.replaceExisting as any) === 'true'
|
||||
|
||||
if (shouldReplace) {
|
||||
finalAttendees = newAttendeeList.map((email: string) => ({
|
||||
email,
|
||||
responseStatus: 'needsAction',
|
||||
}))
|
||||
} else {
|
||||
finalAttendees = [...existingAttendees]
|
||||
|
||||
const existingEmails = new Set(
|
||||
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
|
||||
)
|
||||
|
||||
for (const newEmail of newAttendeeList) {
|
||||
const emailLower = newEmail.toLowerCase()
|
||||
if (!existingEmails.has(emailLower)) {
|
||||
finalAttendees.push({
|
||||
email: newEmail,
|
||||
responseStatus: 'needsAction',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedEvent = {
|
||||
...existingEvent,
|
||||
attendees: finalAttendees,
|
||||
}
|
||||
|
||||
const readOnlyFields = [
|
||||
'id',
|
||||
'etag',
|
||||
'kind',
|
||||
'created',
|
||||
'updated',
|
||||
'htmlLink',
|
||||
'iCalUID',
|
||||
'sequence',
|
||||
'creator',
|
||||
'organizer',
|
||||
]
|
||||
readOnlyFields.forEach((field) => {
|
||||
delete updatedEvent[field]
|
||||
})
|
||||
|
||||
const calendarId = params?.calendarId || 'primary'
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.sendUpdates !== undefined) {
|
||||
queryParams.append('sendUpdates', params.sendUpdates)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
const putResponse = await fetch(putUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedEvent),
|
||||
})
|
||||
|
||||
if (!putResponse.ok) {
|
||||
const errorData = await putResponse.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to invite attendees to calendar event')
|
||||
}
|
||||
|
||||
const data = await putResponse.json()
|
||||
const { data } = await inviteAttendees(response, params)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -393,8 +254,8 @@ export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendar
|
||||
start: data.start,
|
||||
end: data.end,
|
||||
attendees: data.attendees ?? null,
|
||||
creator: data.creator,
|
||||
organizer: data.organizer,
|
||||
creator: data.creator ?? null,
|
||||
organizer: data.organizer ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -408,7 +269,7 @@ export const inviteV2Tool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendar
|
||||
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' },
|
||||
creator: { type: 'json', description: 'Event creator', optional: true },
|
||||
organizer: { type: 'json', description: 'Event organizer', optional: true },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,11 +43,31 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
|
||||
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: 'hidden',
|
||||
description: 'Order of events returned (startTime or updated)',
|
||||
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',
|
||||
@@ -64,8 +84,11 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
|
||||
|
||||
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')
|
||||
if (params.orderBy) queryParams.append('orderBy', params.orderBy)
|
||||
queryParams.append('orderBy', params.orderBy || 'startTime')
|
||||
if (params.showDeleted !== undefined)
|
||||
queryParams.append('showDeleted', params.showDeleted.toString())
|
||||
|
||||
@@ -119,12 +142,26 @@ export const listTool: ToolConfig<GoogleCalendarListParams, GoogleCalendarListRe
|
||||
},
|
||||
}
|
||||
|
||||
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: Array<Record<string, any>>
|
||||
events: GoogleCalendarListV2Event[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export const moveTool: ToolConfig<GoogleCalendarMoveParams, GoogleCalendarMoveRe
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: `Event "${data.summary}" moved successfully`,
|
||||
content: `Event "${data.summary || 'Untitled'}" moved successfully`,
|
||||
metadata: {
|
||||
id: data.id,
|
||||
htmlLink: data.htmlLink,
|
||||
|
||||
@@ -79,7 +79,6 @@ export const quickAddTool: ToolConfig<
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const data = await response.json()
|
||||
|
||||
// Handle attendees if provided
|
||||
let finalEventData = data
|
||||
if (params?.attendees) {
|
||||
let attendeeList: string[] = []
|
||||
@@ -88,7 +87,6 @@ export const quickAddTool: ToolConfig<
|
||||
if (Array.isArray(attendees)) {
|
||||
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
|
||||
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
|
||||
// Convert comma-separated string to array
|
||||
attendeeList = attendees
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
@@ -97,16 +95,13 @@ export const quickAddTool: ToolConfig<
|
||||
|
||||
if (attendeeList.length > 0) {
|
||||
try {
|
||||
// Update the event with attendees
|
||||
const calendarId = params.calendarId || 'primary'
|
||||
const eventId = data.id
|
||||
|
||||
// Prepare update data
|
||||
const updateData = {
|
||||
attendees: attendeeList.map((email: string) => ({ email })),
|
||||
}
|
||||
|
||||
// Build update URL with sendUpdates if specified
|
||||
const updateQueryParams = new URLSearchParams()
|
||||
if (params.sendUpdates !== undefined) {
|
||||
updateQueryParams.append('sendUpdates', params.sendUpdates)
|
||||
@@ -114,7 +109,6 @@ export const quickAddTool: ToolConfig<
|
||||
|
||||
const updateUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${eventId}${updateQueryParams.toString() ? `?${updateQueryParams.toString()}` : ''}`
|
||||
|
||||
// Make the update request
|
||||
const updateResponse = await fetch(updateUrl, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
@@ -127,13 +121,11 @@ export const quickAddTool: ToolConfig<
|
||||
if (updateResponse.ok) {
|
||||
finalEventData = await updateResponse.json()
|
||||
} else {
|
||||
// If update fails, we still return the original event but log the error
|
||||
logger.warn('Failed to add attendees to quick-added event', {
|
||||
error: await updateResponse.text(),
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// If attendee update fails, we still return the original event
|
||||
logger.warn('Error adding attendees to quick-added event', { error })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)' },
|
||||
},
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'
|
||||
|
||||
// Shared attendee interface that matches Google Calendar API specification
|
||||
interface CalendarAttendee {
|
||||
export interface CalendarAttendee {
|
||||
id?: string
|
||||
email: string
|
||||
displayName?: string
|
||||
@@ -18,7 +17,7 @@ interface CalendarAttendee {
|
||||
|
||||
interface BaseGoogleCalendarParams {
|
||||
accessToken: string
|
||||
calendarId?: string // defaults to 'primary' if not provided
|
||||
calendarId?: string
|
||||
}
|
||||
|
||||
export interface GoogleCalendarCreateParams extends BaseGoogleCalendarParams {
|
||||
@@ -28,14 +27,18 @@ export interface GoogleCalendarCreateParams extends BaseGoogleCalendarParams {
|
||||
startDateTime: string
|
||||
endDateTime: string
|
||||
timeZone?: string
|
||||
attendees?: string[] // Array of email addresses
|
||||
attendees?: string[]
|
||||
sendUpdates?: 'all' | 'externalOnly' | 'none'
|
||||
recurrence?: string | string[]
|
||||
addGoogleMeet?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleCalendarListParams extends BaseGoogleCalendarParams {
|
||||
timeMin?: string // RFC3339 timestamp
|
||||
timeMax?: string // RFC3339 timestamp
|
||||
timeMin?: string
|
||||
timeMax?: string
|
||||
q?: string
|
||||
maxResults?: number
|
||||
pageToken?: string
|
||||
singleEvents?: boolean
|
||||
orderBy?: 'startTime' | 'updated'
|
||||
showDeleted?: boolean
|
||||
@@ -55,6 +58,8 @@ export interface GoogleCalendarUpdateParams extends BaseGoogleCalendarParams {
|
||||
timeZone?: string
|
||||
attendees?: string[]
|
||||
sendUpdates?: 'all' | 'externalOnly' | 'none'
|
||||
recurrence?: string | string[]
|
||||
addGoogleMeet?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleCalendarDeleteParams extends BaseGoogleCalendarParams {
|
||||
@@ -63,16 +68,16 @@ export interface GoogleCalendarDeleteParams extends BaseGoogleCalendarParams {
|
||||
}
|
||||
|
||||
export interface GoogleCalendarQuickAddParams extends BaseGoogleCalendarParams {
|
||||
text: string // Natural language text like "Meeting with John tomorrow at 3pm"
|
||||
attendees?: string[] // Array of email addresses (comma-separated string also accepted)
|
||||
text: string
|
||||
attendees?: string[]
|
||||
sendUpdates?: 'all' | 'externalOnly' | 'none'
|
||||
}
|
||||
|
||||
export interface GoogleCalendarInviteParams extends BaseGoogleCalendarParams {
|
||||
eventId: string
|
||||
attendees: string[] // Array of email addresses to invite
|
||||
attendees: string[]
|
||||
sendUpdates?: 'all' | 'externalOnly' | 'none'
|
||||
replaceExisting?: boolean // Whether to replace existing attendees or add to them
|
||||
replaceExisting?: boolean
|
||||
}
|
||||
|
||||
interface GoogleCalendarMoveParams extends BaseGoogleCalendarParams {
|
||||
@@ -92,10 +97,10 @@ interface GoogleCalendarInstancesParams extends BaseGoogleCalendarParams {
|
||||
|
||||
export interface GoogleCalendarFreeBusyParams {
|
||||
accessToken: string
|
||||
calendarIds: string // Comma-separated calendar IDs (e.g., "primary,other@example.com")
|
||||
timeMin: string // RFC3339 timestamp (e.g., 2025-06-03T00:00:00Z)
|
||||
timeMax: string // RFC3339 timestamp (e.g., 2025-06-04T00:00:00Z)
|
||||
timeZone?: string // IANA time zone (e.g., "UTC", "America/New_York")
|
||||
calendarIds: string
|
||||
timeMin: string
|
||||
timeMax: string
|
||||
timeZone?: string
|
||||
}
|
||||
|
||||
interface GoogleCalendarListCalendarsParams {
|
||||
@@ -107,6 +112,40 @@ interface GoogleCalendarListCalendarsParams {
|
||||
showHidden?: boolean
|
||||
}
|
||||
|
||||
export interface GoogleCalendarCreateCalendarParams {
|
||||
accessToken: string
|
||||
summary: string
|
||||
description?: string
|
||||
location?: string
|
||||
timeZone?: string
|
||||
}
|
||||
|
||||
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
|
||||
@@ -119,14 +158,20 @@ export type GoogleCalendarToolParams =
|
||||
| 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
|
||||
@@ -162,7 +207,6 @@ interface GoogleCalendarToolResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// Specific response types for each operation
|
||||
export interface GoogleCalendarCreateResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
@@ -242,32 +286,44 @@ interface GoogleCalendarEvent {
|
||||
}
|
||||
}
|
||||
|
||||
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: {
|
||||
dateTime: string
|
||||
timeZone?: string
|
||||
}
|
||||
end: {
|
||||
dateTime: string
|
||||
timeZone?: 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
|
||||
@@ -287,6 +343,7 @@ export interface GoogleCalendarApiEventResponse {
|
||||
email: string
|
||||
displayName?: string
|
||||
}
|
||||
conferenceData?: Record<string, unknown>
|
||||
reminders?: {
|
||||
useDefault: boolean
|
||||
overrides?: Array<{
|
||||
@@ -296,6 +353,34 @@ export interface GoogleCalendarApiEventResponse {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -402,6 +487,50 @@ interface GoogleCalendarListCalendarsResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -414,3 +543,7 @@ export type GoogleCalendarResponse =
|
||||
| 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' },
|
||||
},
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
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',
|
||||
@@ -59,27 +70,41 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'New start date and time. MUST include timezone offset (e.g., 2025-06-03T10:00:00-08:00) OR provide timeZone parameter',
|
||||
'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 date and time. MUST include timezone offset (e.g., 2025-06-03T11:00:00-08:00) OR provide timeZone parameter',
|
||||
'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:
|
||||
'Time zone (e.g., America/Los_Angeles). Required if datetime does not include offset.',
|
||||
'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 (replaces existing attendees)',
|
||||
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',
|
||||
@@ -97,6 +122,9 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
|
||||
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}` : ''}`
|
||||
@@ -106,8 +134,14 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: GoogleCalendarUpdateParams) => {
|
||||
const updateData: Record<string, unknown> = {}
|
||||
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
|
||||
@@ -122,38 +156,24 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
|
||||
}
|
||||
|
||||
if (params.startDateTime !== undefined) {
|
||||
const needsTimezone =
|
||||
!params.startDateTime.includes('+') && !params.startDateTime.includes('-', 10)
|
||||
updateData.start = {
|
||||
dateTime: params.startDateTime,
|
||||
...(needsTimezone && params.timeZone ? { timeZone: params.timeZone } : {}),
|
||||
}
|
||||
updateData.start = buildEventDateTime(params.startDateTime, params.timeZone)
|
||||
}
|
||||
|
||||
if (params.endDateTime !== undefined) {
|
||||
const needsTimezone =
|
||||
!params.endDateTime.includes('+') && !params.endDateTime.includes('-', 10)
|
||||
updateData.end = {
|
||||
dateTime: params.endDateTime,
|
||||
...(needsTimezone && params.timeZone ? { timeZone: params.timeZone } : {}),
|
||||
}
|
||||
updateData.end = buildEventDateTime(params.endDateTime, params.timeZone)
|
||||
}
|
||||
|
||||
// Handle attendees - convert to array format
|
||||
if (params.attendees !== undefined) {
|
||||
let attendeeList: string[] = []
|
||||
const attendees = params.attendees as string | string[]
|
||||
const attendees = normalizeAttendees(params.attendees)
|
||||
if (attendees.length > 0) {
|
||||
updateData.attendees = attendees
|
||||
}
|
||||
|
||||
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 (isRecurring) {
|
||||
updateData.recurrence = recurrence
|
||||
}
|
||||
|
||||
updateData.attendees = attendeeList.map((email: string) => ({ email }))
|
||||
if (params.addGoogleMeet) {
|
||||
updateData.conferenceData = buildGoogleMeetConferenceData()
|
||||
}
|
||||
|
||||
return updateData
|
||||
@@ -170,10 +190,12 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
|
||||
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,
|
||||
@@ -188,7 +210,7 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarUp
|
||||
content: { type: 'string', description: 'Event update confirmation message' },
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Updated event metadata including ID, status, and details',
|
||||
description: 'Updated event metadata including ID, status, Meet link, and details',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -198,15 +220,17 @@ interface GoogleCalendarUpdateV2Response {
|
||||
output: {
|
||||
id: string
|
||||
htmlLink: string
|
||||
hangoutLink: string | null
|
||||
status: string
|
||||
summary: string | null
|
||||
description: string | null
|
||||
location: string | null
|
||||
start: any
|
||||
end: any
|
||||
attendees: any | null
|
||||
creator: any
|
||||
organizer: any
|
||||
recurrence: string[] | null
|
||||
start: GoogleCalendarApiEventResponse['start']
|
||||
end: GoogleCalendarApiEventResponse['end']
|
||||
attendees: CalendarAttendee[] | null
|
||||
creator: GoogleCalendarApiEventResponse['creator'] | null
|
||||
organizer: GoogleCalendarApiEventResponse['organizer'] | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,29 +251,33 @@ export const updateV2Tool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendar
|
||||
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,
|
||||
organizer: data.organizer,
|
||||
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' },
|
||||
organizer: { type: 'json', description: 'Event organizer' },
|
||||
creator: { type: 'json', description: 'Event creator', optional: true },
|
||||
organizer: { type: 'json', description: 'Event organizer', optional: true },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1169,6 +1169,8 @@ import {
|
||||
} from '@/tools/google_bigquery'
|
||||
import { googleBooksVolumeDetailsTool, googleBooksVolumeSearchTool } from '@/tools/google_books'
|
||||
import {
|
||||
googleCalendarCreateCalendarTool,
|
||||
googleCalendarCreateCalendarV2Tool,
|
||||
googleCalendarCreateTool,
|
||||
googleCalendarCreateV2Tool,
|
||||
googleCalendarDeleteTool,
|
||||
@@ -1181,6 +1183,8 @@ import {
|
||||
googleCalendarInstancesV2Tool,
|
||||
googleCalendarInviteTool,
|
||||
googleCalendarInviteV2Tool,
|
||||
googleCalendarListAclTool,
|
||||
googleCalendarListAclV2Tool,
|
||||
googleCalendarListCalendarsTool,
|
||||
googleCalendarListCalendarsV2Tool,
|
||||
googleCalendarListTool,
|
||||
@@ -1189,6 +1193,10 @@ import {
|
||||
googleCalendarMoveV2Tool,
|
||||
googleCalendarQuickAddTool,
|
||||
googleCalendarQuickAddV2Tool,
|
||||
googleCalendarShareCalendarTool,
|
||||
googleCalendarShareCalendarV2Tool,
|
||||
googleCalendarUnshareCalendarTool,
|
||||
googleCalendarUnshareCalendarV2Tool,
|
||||
googleCalendarUpdateTool,
|
||||
googleCalendarUpdateV2Tool,
|
||||
} from '@/tools/google_calendar'
|
||||
@@ -6344,8 +6352,12 @@ export const tools: Record<string, ToolConfig> = {
|
||||
microsoft_planner_update_task_details: microsoftPlannerUpdateTaskDetailsTool,
|
||||
google_calendar_create: googleCalendarCreateTool,
|
||||
google_calendar_create_v2: googleCalendarCreateV2Tool,
|
||||
google_calendar_create_calendar: googleCalendarCreateCalendarTool,
|
||||
google_calendar_create_calendar_v2: googleCalendarCreateCalendarV2Tool,
|
||||
google_calendar_delete: googleCalendarDeleteTool,
|
||||
google_calendar_delete_v2: googleCalendarDeleteV2Tool,
|
||||
google_calendar_freebusy: googleCalendarFreeBusyTool,
|
||||
google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool,
|
||||
google_calendar_get: googleCalendarGetTool,
|
||||
google_calendar_get_v2: googleCalendarGetV2Tool,
|
||||
google_calendar_instances: googleCalendarInstancesTool,
|
||||
@@ -6354,12 +6366,18 @@ export const tools: Record<string, ToolConfig> = {
|
||||
google_calendar_invite_v2: googleCalendarInviteV2Tool,
|
||||
google_calendar_list: googleCalendarListTool,
|
||||
google_calendar_list_v2: googleCalendarListV2Tool,
|
||||
google_calendar_list_acl: googleCalendarListAclTool,
|
||||
google_calendar_list_acl_v2: googleCalendarListAclV2Tool,
|
||||
google_calendar_list_calendars: googleCalendarListCalendarsTool,
|
||||
google_calendar_list_calendars_v2: googleCalendarListCalendarsV2Tool,
|
||||
google_calendar_move: googleCalendarMoveTool,
|
||||
google_calendar_move_v2: googleCalendarMoveV2Tool,
|
||||
google_calendar_quick_add: googleCalendarQuickAddTool,
|
||||
google_calendar_quick_add_v2: googleCalendarQuickAddV2Tool,
|
||||
google_calendar_share_calendar: googleCalendarShareCalendarTool,
|
||||
google_calendar_share_calendar_v2: googleCalendarShareCalendarV2Tool,
|
||||
google_calendar_unshare_calendar: googleCalendarUnshareCalendarTool,
|
||||
google_calendar_unshare_calendar_v2: googleCalendarUnshareCalendarV2Tool,
|
||||
google_calendar_update: googleCalendarUpdateTool,
|
||||
google_calendar_update_v2: googleCalendarUpdateV2Tool,
|
||||
google_contacts_create: googleContactsCreateTool,
|
||||
@@ -6368,8 +6386,6 @@ export const tools: Record<string, ToolConfig> = {
|
||||
google_contacts_list: googleContactsListTool,
|
||||
google_contacts_search: googleContactsSearchTool,
|
||||
google_contacts_update: googleContactsUpdateTool,
|
||||
google_calendar_freebusy: googleCalendarFreeBusyTool,
|
||||
google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool,
|
||||
google_forms_get_responses: googleFormsGetResponsesTool,
|
||||
google_forms_get_form: googleFormsGetFormTool,
|
||||
google_forms_create_form: googleFormsCreateFormTool,
|
||||
|
||||
Reference in New Issue
Block a user