Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 113 additions & 6 deletions workspace-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,12 +800,119 @@ async function main() {
'The ID of the calendar to delete the event from. Defaults to the primary calendar.',
),
},
},
calendarService.deleteEvent,
);

server.registerTool(
'chat.listSpaces',
},
calendarService.deleteEvent,
);

server.registerTool(
'calendar.createCalendar',
{
description: 'Creates a new calendar.',
inputSchema: {
summary: z.string().describe('Calendar name/title.'),
description: z
.string()
.optional()
.describe('Optional calendar description.'),
timeZone: z
.string()
.optional()
.describe('Optional IANA timezone (e.g., America/New_York).'),
},
},
calendarService.createCalendar,
);

server.registerTool(
'calendar.createRecurringEvent',
{
description: 'Creates a recurring event in a calendar.',
inputSchema: {
calendarId: z
.string()
.optional()
.describe(
'The ID of the calendar to create the recurring event in. Defaults to primary.',
),
summary: z.string().describe('The summary or title of the event.'),
description: z
.string()
.optional()
.describe('The description of the event.'),
start: z.object({
dateTime: z
.string()
.describe(
'The start time in strict ISO 8601 format with seconds and timezone.',
),
}),
end: z.object({
dateTime: z
.string()
.describe(
'The end time in strict ISO 8601 format with seconds and timezone.',
),
}),
attendees: z
.array(z.string())
.optional()
.describe('The email addresses of the attendees.'),
recurrence: z
.array(z.string())
.min(1)
.describe(
'Recurrence rules (e.g., ["RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=10"]).',
),
reminders: z
.object({
useDefault: z.boolean().optional(),
overrides: z
.array(
z.object({
method: z.enum(['email', 'popup']),
minutes: z.number().min(0),
}),
)
.optional(),
})
.optional()
.describe('Optional reminder configuration for the recurring event.'),
},
},
calendarService.createRecurringEvent,
);

server.registerTool(
'calendar.setEventReminders',
{
description:
'Sets custom reminders for an existing calendar event (or resets to default reminders).',
inputSchema: {
eventId: z.string().describe('The ID of the event to update reminders for.'),
calendarId: z
.string()
.optional()
.describe('The ID of the calendar containing the event.'),
useDefault: z
.boolean()
.optional()
.describe('Whether to use default calendar reminders (default: true).'),
overrides: z
.array(
z.object({
method: z.enum(['email', 'popup']),
minutes: z.number().min(0),
}),
)
.optional()
.describe('Custom reminder overrides.'),
},
},
calendarService.setEventReminders,
);

server.registerTool(
'chat.listSpaces',
{
description: 'Lists the spaces the user is a member of.',
inputSchema: {},
Expand Down
139 changes: 124 additions & 15 deletions workspace-server/src/services/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface UpdateEventInput {
start?: { dateTime: string };
end?: { dateTime: string };
attendees?: string[];
sendUpdates?: 'all' | 'externalOnly' | 'none';
addGoogleMeet?: boolean;
attachments?: EventAttachment[];
}
Expand All @@ -77,6 +78,34 @@ export interface FindFreeTimeInput {
duration: number;
}

export interface CreateRecurringEventInput {
calendarId?: string;
summary: string;
description?: string;
start: { dateTime: string };
end: { dateTime: string };
attendees?: string[];
recurrence: string[];
sendUpdates?: 'all' | 'externalOnly' | 'none';
reminders?: {
useDefault?: boolean;
overrides?: Array<{ method: 'email' | 'popup'; minutes: number }>;
};
}

export interface CreateCalendarInput {
summary: string;
description?: string;
timeZone?: string;
}

export interface SetEventRemindersInput {
eventId: string;
calendarId?: string;
useDefault?: boolean;
overrides?: Array<{ method: 'email' | 'popup'; minutes: number }>;
}

export class CalendarService {
private primaryCalendarId: string | null = null;

Expand Down Expand Up @@ -113,9 +142,17 @@ export class CalendarService {
}
}

/**
* Standardized error message extraction helper.
* Converts any error to a string message safely.
*/
private getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

private createValidationErrorResponse(error: unknown) {
const errorMessage =
error instanceof Error ? error.message : 'Validation failed';
error instanceof Error ? this.getErrorMessage(error) : 'Validation failed';
let helpMessage =
'Please use strict ISO 8601 format with seconds and timezone. Examples: 2024-01-15T10:30:00Z (UTC) or 2024-01-15T10:30:00-05:00 (EST)';

Expand Down Expand Up @@ -191,8 +228,7 @@ export class CalendarService {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.list: ${errorMessage}`);
return {
content: [
Expand Down Expand Up @@ -282,8 +318,7 @@ export class CalendarService {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.createEvent: ${errorMessage}`);
return {
content: [
Expand All @@ -296,6 +331,73 @@ export class CalendarService {
}
};

createCalendar = async (input: CreateCalendarInput) => {
const { summary, description, timeZone } = input;
logToFile(`Creating calendar with summary: ${summary}`);
try {
const calendar = await this.getCalendar();
const res = await calendar.calendars.insert({
requestBody: { summary, description, timeZone },
});
return { content: [{ type: 'text' as const, text: JSON.stringify(res.data) }] };
} catch (error) {
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.createCalendar: ${errorMessage}`);
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: errorMessage }) }] };
}
};

createRecurringEvent = async (input: CreateRecurringEventInput) => {
const { calendarId, summary, description, start, end, attendees, recurrence, sendUpdates, reminders } = input;
try {
iso8601DateTimeSchema.parse(start.dateTime);
iso8601DateTimeSchema.parse(end.dateTime);
if (attendees) emailArraySchema.parse(attendees);
if (!recurrence || recurrence.length === 0) throw new Error('recurrence must contain at least one RRULE string');
} catch (error) {
return this.createValidationErrorResponse(error);
}
const finalCalendarId = calendarId || (await this.getPrimaryCalendarId());
logToFile(`Creating recurring event in calendar: ${finalCalendarId}`);

// Determine sendUpdates value
let finalSendUpdates = sendUpdates;
if (finalSendUpdates === undefined) {
finalSendUpdates = attendees?.length ? 'all' : 'none';
}
if (finalSendUpdates) {
logToFile(`Sending updates: ${finalSendUpdates}`);
}

try {
const event: calendar_v3.Schema$Event = { summary, description, start, end, attendees: attendees?.map((email) => ({ email })), recurrence };
if (reminders) event.reminders = { useDefault: reminders.useDefault, overrides: reminders.overrides };
const calendar = await this.getCalendar();
const res = await calendar.events.insert({ calendarId: finalCalendarId, requestBody: event, sendUpdates: finalSendUpdates });
logToFile(`Successfully created recurring event: ${res.data.id}`);
return { content: [{ type: 'text' as const, text: JSON.stringify(res.data) }] };
} catch (error) {
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.createRecurringEvent: ${errorMessage}`);
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: errorMessage }) }] };
}
};

setEventReminders = async (input: SetEventRemindersInput) => {
const { eventId, calendarId, useDefault = true, overrides } = input;
const finalCalendarId = calendarId || (await this.getPrimaryCalendarId());
logToFile(`Setting reminders for event ${eventId} in ${finalCalendarId}`);
try {
const calendar = await this.getCalendar();
const res = await calendar.events.patch({ calendarId: finalCalendarId, eventId, requestBody: { reminders: { useDefault, overrides } } });
return { content: [{ type: 'text' as const, text: JSON.stringify(res.data) }] };
} catch (error) {
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.setEventReminders: ${errorMessage}`);
return { content: [{ type: 'text' as const, text: JSON.stringify({ error: errorMessage }) }] };
}
};

listEvents = async (input: ListEventsInput) => {
const {
calendarId,
Expand Down Expand Up @@ -351,8 +453,7 @@ export class CalendarService {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.listEvents: ${errorMessage}`);
return {
content: [
Expand Down Expand Up @@ -387,7 +488,7 @@ export class CalendarService {
} catch (error) {
const errorMessage =
(error as any).response?.data?.error?.message ||
(error instanceof Error ? error.message : String(error));
this.getErrorMessage(error);
logToFile(`Error during calendar.getEvent: ${errorMessage}`);
return {
content: [
Expand Down Expand Up @@ -426,7 +527,7 @@ export class CalendarService {
} catch (error) {
const errorMessage =
(error as any).response?.data?.error?.message ||
(error instanceof Error ? error.message : String(error));
this.getErrorMessage(error);
logToFile(`Error during calendar.deleteEvent: ${errorMessage}`);
return {
content: [
Expand All @@ -448,6 +549,7 @@ export class CalendarService {
start,
end,
attendees,
sendUpdates,
addGoogleMeet,
attachments,
} = input;
Expand All @@ -473,6 +575,15 @@ export class CalendarService {
if (attachments?.length)
logToFile(`Attachments: ${attachments.length} file(s)`);

// Determine sendUpdates value
let finalSendUpdates = sendUpdates;
if (finalSendUpdates === undefined) {
finalSendUpdates = attendees?.length ? 'all' : 'none';
}
if (finalSendUpdates) {
logToFile(`Sending updates: ${finalSendUpdates}`);
}

try {
const calendar = await this.getCalendar();

Expand All @@ -489,6 +600,7 @@ export class CalendarService {
calendarId: finalCalendarId,
eventId,
requestBody,
sendUpdates: finalSendUpdates,
};
this.applyMeetAndAttachments(
requestBody,
Expand All @@ -509,8 +621,7 @@ export class CalendarService {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.updateEvent: ${errorMessage}`);
return {
content: [
Expand Down Expand Up @@ -611,8 +722,7 @@ export class CalendarService {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.respondToEvent: ${errorMessage}`);
return {
content: [
Expand Down Expand Up @@ -803,8 +913,7 @@ export class CalendarService {
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorMessage = this.getErrorMessage(error);
logToFile(`Error during calendar.findFreeTime: ${errorMessage}`);
return {
content: [
Expand Down