@sentry/junior-google 0.238.1 → 0.239.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -441,6 +441,11 @@ var GOOGLE_OPERATIONS = {
441
441
  method: "POST",
442
442
  path: /^\/calendar\/v3\/calendars\/primary\/events$/
443
443
  },
444
+ "google.calendar.event.delete": {
445
+ access: "write",
446
+ method: "DELETE",
447
+ path: /^\/calendar\/v3\/calendars\/primary\/events\/[A-Za-z0-9_]+$/
448
+ },
444
449
  "google.calendar.event.get": {
445
450
  access: "read",
446
451
  method: "GET",
@@ -726,11 +731,9 @@ function createGoogleSetupPage() {
726
731
  };
727
732
  }
728
733
 
729
- // src/tools/create-event.ts
730
- import { createHash as createHash2, randomUUID } from "crypto";
734
+ // src/tools/cancel-event.ts
731
735
  import {
732
736
  definePluginTool,
733
- PluginToolInputError as PluginToolInputError2,
734
737
  pluginToolOutputSchema
735
738
  } from "@sentry/junior-plugin-api";
736
739
  import { z as z3 } from "zod";
@@ -808,79 +811,285 @@ function formatInterval(startMs, endMs, timeZone) {
808
811
  timeZone,
809
812
  weekday: "short"
810
813
  }).format(startMs);
811
- const time = new Intl.DateTimeFormat("en-US", {
814
+ const time = (timeZoneName) => new Intl.DateTimeFormat("en-US", {
812
815
  hour: "2-digit",
813
816
  hourCycle: "h23",
814
817
  minute: "2-digit",
815
818
  timeZone,
816
- timeZoneName: "short"
819
+ timeZoneName
817
820
  });
818
- return `${day}, ${time.format(startMs)} \u2013 ${time.format(endMs)}`;
821
+ return `${day}, ${time().format(startMs)} \u2013 ${time("short").format(endMs)}`;
819
822
  }
820
823
  var MAX_EVENT_MS = 8 * 60 * 60 * 1e3;
824
+ var googleAttendeeSchema = z2.object({
825
+ displayName: z2.string().optional(),
826
+ email: z2.string(),
827
+ optional: z2.boolean().optional(),
828
+ resource: z2.boolean().optional(),
829
+ responseStatus: z2.string().optional()
830
+ }).loose();
831
+ var attendeeOutputSchema = z2.object({
832
+ email: z2.string(),
833
+ optional: z2.boolean().optional(),
834
+ response: z2.enum(["accepted", "declined", "tentative", "needsAction"]).optional()
835
+ });
836
+ var RESPONSES = /* @__PURE__ */ new Set(["accepted", "declined", "tentative", "needsAction"]);
837
+ function attendeeResults(attendees) {
838
+ return (attendees ?? []).filter((attendee) => attendee.resource !== true).map((attendee) => ({
839
+ email: attendee.email.toLowerCase(),
840
+ ...attendee.optional ? { optional: true } : void 0,
841
+ ...attendee.responseStatus && RESPONSES.has(attendee.responseStatus) ? {
842
+ response: attendee.responseStatus
843
+ } : void 0
844
+ }));
845
+ }
821
846
  var ownEventSchema = z2.object({
822
- attendees: z2.array(
823
- z2.object({ email: z2.string(), responseStatus: z2.string().optional() }).loose()
824
- ).optional(),
847
+ attendees: z2.array(googleAttendeeSchema).optional(),
848
+ description: z2.string().optional(),
825
849
  end: z2.object({ dateTime: z2.string() }),
826
850
  hangoutLink: z2.string().optional(),
827
851
  htmlLink: z2.string().optional(),
828
852
  id: z2.string(),
853
+ location: z2.string().optional(),
829
854
  organizer: z2.object({ self: z2.boolean().optional() }).optional(),
855
+ recurrence: z2.array(z2.string()).optional(),
856
+ recurringEventId: z2.string().optional(),
830
857
  start: z2.object({ dateTime: z2.string() }),
831
- summary: z2.string().optional()
858
+ summary: z2.string().optional(),
859
+ updated: z2.string().optional()
832
860
  });
833
861
  function ownEventResult(event, timeZone) {
862
+ const startMs = Date.parse(event.start.dateTime);
863
+ const when = formatInterval(
864
+ startMs,
865
+ Date.parse(event.end.dateTime),
866
+ timeZone
867
+ );
868
+ const annotation = {
869
+ kind: "object",
870
+ key: event.id,
871
+ label: new Intl.DateTimeFormat("en-US", {
872
+ day: "numeric",
873
+ month: "short",
874
+ timeZone
875
+ }).format(startMs),
876
+ objectType: "calendar_event",
877
+ // People can edit the event in Calendar. Shorten long values so a
878
+ // completed change never fails on the annotation schema.
879
+ title: (event.summary?.trim() || "Untitled event").slice(0, 512),
880
+ url: event.htmlLink ?? null,
881
+ description: event.description?.trim().slice(0, 4e3) || void 0,
882
+ sourceUpdatedAt: event.updated,
883
+ facts: {
884
+ type: "calendar_event",
885
+ when,
886
+ attendees: (event.attendees ?? []).filter((attendee) => attendee.resource !== true).slice(0, 5).map(
887
+ (attendee) => (attendee.displayName?.trim() || attendee.email).slice(0, 160)
888
+ )
889
+ }
890
+ };
834
891
  return {
835
- attendees: (event.attendees ?? []).map((attendee) => attendee.email),
892
+ objectAnnotations: [annotation],
893
+ attendees: attendeeResults(event.attendees),
836
894
  end: event.end.dateTime,
837
895
  eventId: event.id,
838
- label: formatInterval(
839
- Date.parse(event.start.dateTime),
840
- Date.parse(event.end.dateTime),
841
- timeZone
842
- ),
896
+ label: when,
843
897
  start: event.start.dateTime,
844
898
  ...event.summary ? { title: event.summary } : void 0,
899
+ ...event.location ? { location: event.location } : void 0,
900
+ ...event.recurrence ? { repeats: true } : void 0,
901
+ ...event.recurringEventId ? { seriesEventId: event.recurringEventId } : void 0,
845
902
  ...event.htmlLink ? { url: event.htmlLink } : void 0,
846
903
  ...event.hangoutLink ? { videoCallUrl: event.hangoutLink } : void 0
847
904
  };
848
905
  }
849
906
  var ownEventOutputFields = {
850
- attendees: z2.array(z2.string()),
907
+ attendees: z2.array(attendeeOutputSchema),
851
908
  end: z2.string(),
852
909
  eventId: z2.string(),
853
910
  label: z2.string(),
911
+ location: z2.string().optional(),
912
+ repeats: z2.boolean().optional().describe("True when this event id is a whole repeating series."),
913
+ seriesEventId: z2.string().optional().describe(
914
+ "Set when this is one occurrence of a repeating event. Use it as eventId to change or cancel the whole series."
915
+ ),
854
916
  start: z2.string(),
855
917
  title: z2.string().optional(),
856
918
  url: z2.string().optional(),
857
919
  videoCallUrl: z2.string().optional()
858
920
  };
921
+ var ownEventIdSchema = z2.string().trim().min(5).max(2048).describe(
922
+ "Event id from a Calendar tool, or a Google Calendar event link. An occurrence of a repeating event has its own id; its seriesEventId is the whole series."
923
+ );
924
+ var EVENT_ID_PATTERN = /^[A-Za-z0-9_]{5,1024}$/;
925
+ function parseEventId(value) {
926
+ let eventId = value;
927
+ if (/^https?:\/\//.test(value)) {
928
+ const eid = URL.parse(value)?.searchParams.get("eid");
929
+ eventId = eid ? Buffer.from(eid, "base64").toString("utf8").split(" ")[0] ?? "" : "";
930
+ }
931
+ if (!EVENT_ID_PATTERN.test(eventId)) {
932
+ throw new PluginToolInputError(
933
+ "Use an event id from a Calendar tool or a Google Calendar event link."
934
+ );
935
+ }
936
+ return eventId;
937
+ }
938
+ var cancelledEventSchema = z2.object({ status: z2.literal("cancelled") });
939
+ async function readOwnEventForRequester(ctx, eventIdOrLink, action) {
940
+ const eventId = parseEventId(eventIdOrLink);
941
+ const requester = await requesterEmail(ctx);
942
+ if (!requester) {
943
+ throw new PluginToolInputError(
944
+ `Junior does not know the requester's email, so it cannot check that they may ${action} this event.`
945
+ );
946
+ }
947
+ const path = `/calendar/v3/calendars/primary/events/${eventId}`;
948
+ const response = await googleApiRequest(ctx, {
949
+ operation: "google.calendar.event.get",
950
+ path
951
+ });
952
+ if (response.status === 410 || response.status === 200 && cancelledEventSchema.safeParse(response.body).success) {
953
+ return { eventId, path, status: "cancelled" };
954
+ }
955
+ if (response.status === 404) {
956
+ throw new PluginToolInputError(
957
+ `Junior's calendar has no event ${eventId}. Junior can only ${action} events it organizes; ask the organizer instead.`
958
+ );
959
+ }
960
+ if (response.status !== 200) {
961
+ throw googleApiError("google.calendar.event.get", response);
962
+ }
963
+ const event = ownEventSchema.parse(response.body);
964
+ if (event.organizer?.self !== true) {
965
+ throw new PluginToolInputError(
966
+ `Junior does not organize this event, so it cannot ${action} it. Ask the organizer instead.`
967
+ );
968
+ }
969
+ if (!(event.attendees ?? []).some(
970
+ (attendee) => attendee.email.toLowerCase() === requester
971
+ )) {
972
+ throw new PluginToolInputError(
973
+ `Only people invited to this event can ask Junior to ${action} it.`
974
+ );
975
+ }
976
+ return { event, eventId, path, status: "active" };
977
+ }
859
978
 
860
- // src/tools/create-event.ts
979
+ // src/tools/cancel-event.ts
861
980
  var inputSchema = z3.object({
862
- title: z3.string().trim().min(1).max(200),
863
- description: z3.string().max(4e3).optional().describe(
864
- "Event description. Do not include private details the attendees should not see."
981
+ eventId: ownEventIdSchema,
982
+ timeZone: timeZoneSchema
983
+ }).strict();
984
+ var outputSchema = pluginToolOutputSchema.extend({
985
+ target: z3.literal("cancelCalendarEvent"),
986
+ cancelled: z3.boolean().describe(
987
+ "False when the event was already cancelled before this call. Event details are then missing."
865
988
  ),
866
- start: z3.iso.datetime({ offset: true }).describe("Event start, RFC 3339 with offset."),
867
- end: z3.iso.datetime({ offset: true }).describe("Event end, RFC 3339 with offset."),
989
+ ...ownEventOutputFields
990
+ }).partial({ attendees: true, end: true, label: true, start: true });
991
+ function createCancelCalendarEventTool(ctx) {
992
+ return definePluginTool({
993
+ annotations: {
994
+ destructiveHint: true,
995
+ idempotentHint: true,
996
+ openWorldHint: true,
997
+ readOnlyHint: false
998
+ },
999
+ describeProposal(input) {
1000
+ return `Cancel Google Calendar event ${input.eventId} organized by Junior. Google emails every attendee a cancellation.`;
1001
+ },
1002
+ description: "Cancel a Google Calendar event organized by Junior's own Google account. Google removes it from every attendee's calendar and emails them a cancellation. Only people invited to the event can ask to cancel it. Junior cannot cancel events other people organize. For a repeating event, an occurrence id cancels only that occurrence; its seriesEventId cancels every occurrence. Confirm which event, and for a repeating event whether one occurrence or the series, unless the requester already made it clear.",
1003
+ inputSchema,
1004
+ outputSchema,
1005
+ async execute(input) {
1006
+ const read = await readOwnEventForRequester(ctx, input.eventId, "cancel");
1007
+ if (read.status === "cancelled") {
1008
+ return {
1009
+ target: "cancelCalendarEvent",
1010
+ cancelled: false,
1011
+ eventId: read.eventId
1012
+ };
1013
+ }
1014
+ const { event, path } = read;
1015
+ const result = {
1016
+ target: "cancelCalendarEvent",
1017
+ ...ownEventResult(event, input.timeZone)
1018
+ };
1019
+ const response = await googleApiRequest(ctx, {
1020
+ operation: "google.calendar.event.delete",
1021
+ path,
1022
+ query: { sendUpdates: "all" }
1023
+ });
1024
+ if (response.status !== 204 && response.status !== 410) {
1025
+ throw googleApiError("google.calendar.event.delete", response);
1026
+ }
1027
+ return {
1028
+ ...result,
1029
+ // Keep the card in the Conversation, and show that the event is gone.
1030
+ objectAnnotations: result.objectAnnotations.map((annotation) => ({
1031
+ ...annotation,
1032
+ status: "cancelled"
1033
+ })),
1034
+ cancelled: response.status === 204
1035
+ };
1036
+ }
1037
+ });
1038
+ }
1039
+
1040
+ // src/tools/create-event.ts
1041
+ import { createHash as createHash2, randomUUID } from "crypto";
1042
+ import {
1043
+ definePluginTool as definePluginTool2,
1044
+ PluginToolInputError as PluginToolInputError2,
1045
+ pluginToolOutputSchema as pluginToolOutputSchema2
1046
+ } from "@sentry/junior-plugin-api";
1047
+ import { z as z4 } from "zod";
1048
+ var repeatSchema = z4.object({
1049
+ frequency: z4.enum(["weekdays", "weekly", "monthly"]).describe(
1050
+ "`weekdays` repeats Monday to Friday. `weekly` repeats on the start's weekday. `monthly` repeats on the start's day of the month."
1051
+ ),
1052
+ interval: z4.number().int().min(1).max(12).default(1).describe(
1053
+ "Repeat every N weeks or months. 2 with `weekly` means every other week. Ignored for `weekdays`."
1054
+ ),
1055
+ count: z4.number().int().min(2).max(200).optional().describe("Number of occurrences. Omit when the series has no end.")
1056
+ }).strict();
1057
+ function recurrenceRule(repeat) {
1058
+ const parts = repeat.frequency === "weekdays" ? ["FREQ=WEEKLY", "BYDAY=MO,TU,WE,TH,FR"] : [
1059
+ `FREQ=${repeat.frequency === "weekly" ? "WEEKLY" : "MONTHLY"}`,
1060
+ ...repeat.interval > 1 ? [`INTERVAL=${repeat.interval}`] : []
1061
+ ];
1062
+ if (repeat.count) parts.push(`COUNT=${repeat.count}`);
1063
+ return `RRULE:${parts.join(";")}`;
1064
+ }
1065
+ var inputSchema2 = z4.object({
1066
+ title: z4.string().trim().min(1).max(200).describe(
1067
+ 'Specific title that says who and what, such as "Alice / Bob: Q4 hiring plan". Avoid generic titles such as "Meeting" or "Sync".'
1068
+ ),
1069
+ description: z4.string().max(4e3).optional().describe(
1070
+ "Purpose, agenda, and links attendees need to prepare. Do not include private details the attendees should not see."
1071
+ ),
1072
+ location: z4.string().trim().max(500).optional().describe("Room name or address for an in-person meeting."),
1073
+ start: z4.iso.datetime({ offset: true }).describe("Event start, RFC 3339 with offset."),
1074
+ end: z4.iso.datetime({ offset: true }).describe("Event end, RFC 3339 with offset."),
868
1075
  timeZone: timeZoneSchema,
869
1076
  attendees: emailListSchema(50).describe(
870
- "Email addresses to invite. Junior adds the requester automatically when it knows their email."
1077
+ "Email addresses of required attendees. Junior adds the requester automatically when it knows their email."
871
1078
  ),
872
- addVideoCall: z3.boolean().default(true).describe("Add a Google Meet link.")
1079
+ optionalAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as optional attendees."),
1080
+ addVideoCall: z4.boolean().default(true).describe("Add a Google Meet link."),
1081
+ repeat: repeatSchema.optional().describe("Make a repeating series, such as a weekly 1:1.")
873
1082
  }).strict();
874
- var outputSchema = pluginToolOutputSchema.extend({
875
- target: z3.literal("createCalendarEvent"),
876
- created: z3.boolean(),
1083
+ var outputSchema2 = pluginToolOutputSchema2.extend({
1084
+ target: z4.literal("createCalendarEvent"),
1085
+ created: z4.boolean(),
877
1086
  ...ownEventOutputFields
878
1087
  });
879
1088
  function calendarEventId(toolCallId) {
880
1089
  return createHash2("sha256").update(`junior-google:${toolCallId ?? randomUUID()}`).digest("hex").slice(0, 40);
881
1090
  }
882
1091
  function createCreateCalendarEventTool(ctx) {
883
- return definePluginTool({
1092
+ return definePluginTool2({
884
1093
  annotations: {
885
1094
  destructiveHint: false,
886
1095
  idempotentHint: true,
@@ -888,11 +1097,13 @@ function createCreateCalendarEventTool(ctx) {
888
1097
  readOnlyHint: false
889
1098
  },
890
1099
  describeProposal(input) {
891
- return `Create Google Calendar event "${input.title}" from ${input.start} to ${input.end} and email invites to ${input.attendees.join(", ") || "the requester"}.`;
1100
+ const optional = input.optionalAttendees.length ? ` (optional: ${input.optionalAttendees.join(", ")})` : "";
1101
+ const repeat = input.repeat ? `, repeating ${recurrenceRule(input.repeat)}` : "";
1102
+ return `Create Google Calendar event "${input.title}" from ${input.start} to ${input.end}${repeat} and email invites to ${input.attendees.join(", ") || "the requester"}${optional}.`;
892
1103
  },
893
- description: "Create a Google Calendar event organized by Junior's own Google account and email invites to the attendees. Use after the requester confirms the time. Only people in the company's Google Workspace domains can be invited.",
894
- inputSchema,
895
- outputSchema,
1104
+ description: "Create a Google Calendar event organized by Junior's own Google account and email invites to the attendees. Junior can later change or cancel it. Use after the requester confirms the time, or when they already gave an exact time. Check the time with findMeetingTimes first unless the requester says to book it anyway. Only people in the company's Google Workspace domains can be invited.",
1105
+ inputSchema: inputSchema2,
1106
+ outputSchema: outputSchema2,
896
1107
  async execute(input, options) {
897
1108
  const startMs = Date.parse(input.start);
898
1109
  const endMs = Date.parse(input.end);
@@ -903,14 +1114,24 @@ function createCreateCalendarEventTool(ctx) {
903
1114
  throw new PluginToolInputError2("Events can be at most 8 hours long.");
904
1115
  }
905
1116
  const attendees = await withRequester(ctx, input.attendees);
906
- if (attendees.length === 0) {
1117
+ const required = new Set(attendees);
1118
+ const optionalAttendees = [...new Set(input.optionalAttendees)].filter(
1119
+ (email) => !required.has(email)
1120
+ );
1121
+ if (attendees.length + optionalAttendees.length === 0) {
907
1122
  throw new PluginToolInputError2("Invite at least one attendee.");
908
1123
  }
909
- requireAllowedEmails(attendees, ctx.allowedDomains);
1124
+ requireAllowedEmails(
1125
+ [...attendees, ...optionalAttendees],
1126
+ ctx.allowedDomains
1127
+ );
910
1128
  const eventId = calendarEventId(options.toolCallId);
911
1129
  const response = await googleApiRequest(ctx, {
912
1130
  body: {
913
- attendees: attendees.map((email) => ({ email })),
1131
+ attendees: [
1132
+ ...attendees.map((email) => ({ email })),
1133
+ ...optionalAttendees.map((email) => ({ email, optional: true }))
1134
+ ],
914
1135
  ...input.addVideoCall ? {
915
1136
  conferenceData: {
916
1137
  createRequest: {
@@ -923,6 +1144,8 @@ function createCreateCalendarEventTool(ctx) {
923
1144
  end: { dateTime: input.end, timeZone: input.timeZone },
924
1145
  guestsCanModify: false,
925
1146
  id: eventId,
1147
+ ...input.location ? { location: input.location } : void 0,
1148
+ ...input.repeat ? { recurrence: [recurrenceRule(input.repeat)] } : void 0,
926
1149
  start: { dateTime: input.start, timeZone: input.timeZone },
927
1150
  summary: input.title
928
1151
  },
@@ -958,48 +1181,73 @@ function createCreateCalendarEventTool(ctx) {
958
1181
 
959
1182
  // src/tools/find-meeting-times.ts
960
1183
  import {
961
- definePluginTool as definePluginTool2,
1184
+ definePluginTool as definePluginTool3,
962
1185
  PluginToolInputError as PluginToolInputError3,
963
- pluginToolOutputSchema as pluginToolOutputSchema2
1186
+ pluginToolOutputSchema as pluginToolOutputSchema3
964
1187
  } from "@sentry/junior-plugin-api";
965
- import { z as z4 } from "zod";
966
- var MAX_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
967
- var clockTimeSchema = z4.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use 24-hour HH:MM");
968
- var inputSchema2 = z4.object({
1188
+ import { z as z5 } from "zod";
1189
+ var DAY_MS = 24 * 60 * 60 * 1e3;
1190
+ var MAX_WINDOW_MS = 14 * DAY_MS;
1191
+ var DEFAULT_WINDOW_MS = 7 * DAY_MS;
1192
+ var clockTimeSchema = z5.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use 24-hour HH:MM");
1193
+ var inputSchema3 = z5.object({
969
1194
  attendees: emailListSchema(20).min(1).describe(
970
- "Email addresses of the people who must attend. Junior adds the requester automatically when it knows their email."
1195
+ "Work emails of the people who must attend. Resolve names with userLookup. Junior adds the requester automatically when it knows their email."
971
1196
  ),
972
- durationMinutes: z4.number().int().min(15).max(480),
973
- timeMin: z4.iso.datetime({ offset: true }).describe("Start of the search window, RFC 3339 with offset."),
974
- timeMax: z4.iso.datetime({ offset: true }).describe(
975
- "End of the search window, RFC 3339 with offset. At most 14 days after timeMin."
1197
+ optionalAttendees: emailListSchema(20).default([]).describe(
1198
+ 'Work emails of people the requester calls optional or "if free". Their conflicts do not block a slot; slots where they are free come first.'
976
1199
  ),
977
- timeZone: timeZoneSchema,
978
- workdayStart: clockTimeSchema.default("09:00").describe("Earliest local start time in timeZone, 24-hour HH:MM."),
979
- workdayEnd: clockTimeSchema.default("17:00").describe("Latest local end time in timeZone, 24-hour HH:MM."),
980
- maxResults: z4.number().int().min(1).max(20).default(5)
1200
+ durationMinutes: z5.number().int().min(15).max(480).default(30),
1201
+ timeMin: z5.iso.datetime({ offset: true }).optional().describe(
1202
+ "Start of the search window, RFC 3339 with offset. Defaults to now. For an exact requested time, search only that slot."
1203
+ ),
1204
+ timeMax: z5.iso.datetime({ offset: true }).optional().describe(
1205
+ "End of the search window, RFC 3339 with offset. Defaults to 7 days after timeMin. At most 14 days after timeMin."
1206
+ ),
1207
+ timeZone: timeZoneSchema.describe(
1208
+ "The requester's IANA time zone, such as America/Los_Angeles. Slot labels use it. It is also the working-hours zone for anyone whose calendar time zone Junior cannot read."
1209
+ ),
1210
+ workdayStart: clockTimeSchema.default("09:00").describe(
1211
+ "Earliest local start, 24-hour HH:MM, in each attendee's own calendar time zone."
1212
+ ),
1213
+ workdayEnd: clockTimeSchema.default("17:00").describe(
1214
+ "Latest local end, 24-hour HH:MM, in each attendee's own calendar time zone."
1215
+ ),
1216
+ maxResults: z5.number().int().min(1).max(20).default(3)
981
1217
  }).strict();
982
- var slotSchema = z4.object({
983
- end: z4.string(),
984
- label: z4.string(),
985
- start: z4.string()
1218
+ var slotSchema = z5.object({
1219
+ end: z5.string(),
1220
+ label: z5.string().describe("The slot in the requester's time zone."),
1221
+ otherTimeZoneLabels: z5.array(z5.string()).optional().describe("The slot in the other attendees' time zones."),
1222
+ optionalConflicts: z5.array(z5.string()).optional().describe(
1223
+ "Optional attendees who are busy or outside their working hours at this time."
1224
+ ),
1225
+ start: z5.string()
986
1226
  });
987
- var outputSchema2 = pluginToolOutputSchema2.extend({
988
- target: z4.literal("findMeetingTimes"),
989
- checked: z4.array(z4.string()),
990
- slots: z4.array(slotSchema),
991
- timeZone: z4.string(),
992
- unavailable: z4.array(z4.object({ email: z4.string(), reason: z4.string() }))
1227
+ var outputSchema3 = pluginToolOutputSchema3.extend({
1228
+ target: z5.literal("findMeetingTimes"),
1229
+ checked: z5.array(
1230
+ z5.object({
1231
+ email: z5.string(),
1232
+ timeZone: z5.string().optional().describe(
1233
+ "Time zone of the person's calendar. Missing when Junior cannot read it; their hours then use the requester's time zone."
1234
+ )
1235
+ })
1236
+ ),
1237
+ slots: z5.array(slotSchema),
1238
+ timeZone: z5.string(),
1239
+ unavailable: z5.array(z5.object({ email: z5.string(), reason: z5.string() }))
993
1240
  });
994
- var freeBusyResponseSchema = z4.object({
995
- calendars: z4.record(
996
- z4.string(),
997
- z4.object({
998
- busy: z4.array(z4.object({ end: z4.string(), start: z4.string() })).default([]),
999
- errors: z4.array(z4.object({ reason: z4.string() })).optional()
1241
+ var freeBusyResponseSchema = z5.object({
1242
+ calendars: z5.record(
1243
+ z5.string(),
1244
+ z5.object({
1245
+ busy: z5.array(z5.object({ end: z5.string(), start: z5.string() })).default([]),
1246
+ errors: z5.array(z5.object({ reason: z5.string() })).optional()
1000
1247
  })
1001
1248
  )
1002
1249
  });
1250
+ var calendarTimeZoneSchema = z5.object({ timeZone: z5.string().optional() });
1003
1251
  function localParts(ms, format) {
1004
1252
  const parts = Object.fromEntries(
1005
1253
  format.formatToParts(ms).map((part) => [part.type, part.value])
@@ -1010,58 +1258,150 @@ function localParts(ms, format) {
1010
1258
  weekday: parts.weekday ?? ""
1011
1259
  };
1012
1260
  }
1013
- function clockMinutes(value) {
1014
- const [hours, minutes] = value.split(":").map(Number);
1015
- return hours * 60 + minutes;
1016
- }
1017
- function findFreeSlots(input) {
1018
- const durationMs = input.durationMinutes * 6e4;
1019
- const stepMs = (input.durationMinutes < 30 ? 15 : 30) * 6e4;
1020
- const format = new Intl.DateTimeFormat("en-US", {
1261
+ function localFormat(timeZone) {
1262
+ return new Intl.DateTimeFormat("en-US", {
1021
1263
  day: "2-digit",
1022
1264
  hour: "2-digit",
1023
1265
  hourCycle: "h23",
1024
1266
  minute: "2-digit",
1025
1267
  month: "2-digit",
1026
- timeZone: input.timeZone,
1268
+ timeZone,
1027
1269
  weekday: "short",
1028
1270
  year: "numeric"
1029
1271
  });
1272
+ }
1273
+ function clockMinutes(value) {
1274
+ const [hours, minutes] = value.split(":").map(Number);
1275
+ return hours * 60 + minutes;
1276
+ }
1277
+ function overlaps(busy, startMs, endMs) {
1278
+ return busy.endMs > startMs && busy.startMs < endMs;
1279
+ }
1280
+ function farthestOption(options, chosen) {
1281
+ if (chosen.length === 0) return options[0];
1282
+ let best;
1283
+ let bestGap = -1;
1284
+ for (const option of options) {
1285
+ const gap = Math.min(
1286
+ ...chosen.map((slot) => Math.abs(slot.startMs - option.startMs))
1287
+ );
1288
+ if (gap > bestGap) {
1289
+ best = option;
1290
+ bestGap = gap;
1291
+ }
1292
+ }
1293
+ return best;
1294
+ }
1295
+ function spreadAcrossDays(candidates, max) {
1296
+ const days = /* @__PURE__ */ new Map();
1297
+ for (const candidate of candidates) {
1298
+ days.set(candidate.date, [...days.get(candidate.date) ?? [], candidate]);
1299
+ }
1300
+ const chosen = /* @__PURE__ */ new Map();
1301
+ const result = [];
1302
+ let progress = true;
1303
+ while (result.length < max && progress) {
1304
+ progress = false;
1305
+ for (const [date, options] of days) {
1306
+ if (result.length >= max) break;
1307
+ const picked = chosen.get(date) ?? [];
1308
+ const next = farthestOption(options, picked);
1309
+ if (!next) continue;
1310
+ options.splice(options.indexOf(next), 1);
1311
+ picked.push(next);
1312
+ chosen.set(date, picked);
1313
+ result.push(next);
1314
+ progress = true;
1315
+ }
1316
+ }
1317
+ return result;
1318
+ }
1319
+ function findFreeSlots(input) {
1320
+ const durationMs = input.durationMinutes * 6e4;
1321
+ const stepMs = (input.durationMinutes < 30 ? 15 : 30) * 6e4;
1030
1322
  const dayStart = clockMinutes(input.workdayStart);
1031
1323
  const dayEnd = clockMinutes(input.workdayEnd);
1032
- const slots = [];
1324
+ const formats = /* @__PURE__ */ new Map();
1325
+ const partsIn = (ms, timeZone) => {
1326
+ let format = formats.get(timeZone);
1327
+ if (!format) {
1328
+ format = localFormat(timeZone);
1329
+ formats.set(timeZone, format);
1330
+ }
1331
+ return localParts(ms, format);
1332
+ };
1333
+ const available = (attendee, startMs2, endMs) => {
1334
+ const start = partsIn(startMs2, attendee.timeZone);
1335
+ const end = partsIn(endMs, attendee.timeZone);
1336
+ return start.weekday !== "Sat" && start.weekday !== "Sun" && start.date === end.date && start.minutes >= dayStart && end.minutes <= dayEnd && attendee.busy.every((busy) => !overlaps(busy, startMs2, endMs));
1337
+ };
1338
+ const required = input.attendees.filter((attendee) => attendee.required);
1339
+ const optional = input.attendees.filter((attendee) => !attendee.required);
1340
+ const candidates = [];
1033
1341
  let startMs = Math.ceil(input.timeMinMs / stepMs) * stepMs;
1034
- while (startMs + durationMs <= input.timeMaxMs && slots.length < input.maxResults) {
1342
+ while (startMs + durationMs <= input.timeMaxMs) {
1035
1343
  const endMs = startMs + durationMs;
1036
- const start = localParts(startMs, format);
1037
- const end = localParts(endMs, format);
1038
- const inWorkday = start.weekday !== "Sat" && start.weekday !== "Sun" && start.date === end.date && start.minutes >= dayStart && end.minutes <= dayEnd;
1039
- const free = input.busy.every(
1040
- (busy) => busy.endMs <= startMs || busy.startMs >= endMs
1041
- );
1042
- if (inWorkday && free) {
1043
- slots.push({ startMs, endMs });
1344
+ if (required.every((attendee) => available(attendee, startMs, endMs))) {
1345
+ candidates.push({
1346
+ date: partsIn(startMs, input.timeZone).date,
1347
+ endMs,
1348
+ optionalConflicts: optional.filter((attendee) => !available(attendee, startMs, endMs)).map((attendee) => attendee.email),
1349
+ startMs
1350
+ });
1044
1351
  startMs = endMs;
1045
1352
  } else {
1046
1353
  startMs += stepMs;
1047
1354
  }
1048
1355
  }
1049
- return slots;
1356
+ const picked = spreadAcrossDays(
1357
+ candidates.filter((slot) => slot.optionalConflicts.length === 0),
1358
+ input.maxResults
1359
+ );
1360
+ picked.push(
1361
+ ...spreadAcrossDays(
1362
+ candidates.filter((slot) => slot.optionalConflicts.length > 0),
1363
+ input.maxResults - picked.length
1364
+ )
1365
+ );
1366
+ return picked.sort((a, b) => a.startMs - b.startMs).map(({ endMs, optionalConflicts, startMs: startMs2 }) => ({
1367
+ endMs,
1368
+ optionalConflicts,
1369
+ startMs: startMs2
1370
+ }));
1371
+ }
1372
+ async function calendarTimeZone(ctx, email) {
1373
+ const response = await googleApiRequest(ctx, {
1374
+ operation: "google.calendar.events.list",
1375
+ path: `/calendar/v3/calendars/${encodeURIComponent(email)}/events`,
1376
+ // No `timeZone` parameter: Google then answers with the calendar's own.
1377
+ query: { fields: "timeZone", maxResults: "1" }
1378
+ });
1379
+ if (response.status === 403 || response.status === 404) {
1380
+ return void 0;
1381
+ }
1382
+ if (response.status !== 200) {
1383
+ throw googleApiError("google.calendar.events.list", response);
1384
+ }
1385
+ const { timeZone } = calendarTimeZoneSchema.parse(response.body);
1386
+ return timeZone && isTimeZone(timeZone) ? timeZone : void 0;
1050
1387
  }
1051
1388
  function createFindMeetingTimesTool(ctx) {
1052
- return definePluginTool2({
1389
+ return definePluginTool3({
1053
1390
  annotations: {
1054
1391
  destructiveHint: false,
1055
1392
  idempotentHint: true,
1056
1393
  openWorldHint: true,
1057
1394
  readOnlyHint: true
1058
1395
  },
1059
- description: "Find meeting times when every attendee is free, using Google Calendar free/busy data from Junior's own Google account. Returns open slots only, never event details. Only people in the company's Google Workspace domains can be checked. People listed in `unavailable` were not checked; tell the user instead of treating them as free.",
1060
- inputSchema: inputSchema2,
1061
- outputSchema: outputSchema2,
1396
+ description: "Find meeting times when every required attendee is free and inside working hours in their own calendar time zone, using Google Calendar free/busy data from Junior's own Google account. Returns open slots only, never event details. Slots are spread across days and times; offer them as a short list. Only people in the company's Google Workspace domains can be checked. People in `unavailable` were not checked: say so instead of treating them as free. If no slot is free, widen the window or shorten the meeting once, then offer the closest option.",
1397
+ inputSchema: inputSchema3,
1398
+ outputSchema: outputSchema3,
1062
1399
  async execute(input) {
1063
- const timeMinMs = Math.max(Date.parse(input.timeMin), Date.now());
1064
- const timeMaxMs = Date.parse(input.timeMax);
1400
+ const timeMinMs = Math.max(
1401
+ input.timeMin ? Date.parse(input.timeMin) : 0,
1402
+ Date.now()
1403
+ );
1404
+ const timeMaxMs = input.timeMax ? Date.parse(input.timeMax) : timeMinMs + DEFAULT_WINDOW_MS;
1065
1405
  if (timeMaxMs <= timeMinMs) {
1066
1406
  throw new PluginToolInputError3(
1067
1407
  "timeMax must be after timeMin and now."
@@ -1076,40 +1416,52 @@ function createFindMeetingTimesTool(ctx) {
1076
1416
  );
1077
1417
  }
1078
1418
  const attendees = await withRequester(ctx, input.attendees);
1079
- requireAllowedEmails(attendees, ctx.allowedDomains);
1080
- const response = await googleApiRequest(ctx, {
1081
- body: {
1082
- items: attendees.map((id) => ({ id })),
1083
- timeMax: new Date(timeMaxMs).toISOString(),
1084
- timeMin: new Date(timeMinMs).toISOString()
1085
- },
1086
- operation: "google.calendar.freebusy.query",
1087
- path: "/calendar/v3/freeBusy"
1088
- });
1419
+ const required = new Set(attendees);
1420
+ const optionalAttendees = [...new Set(input.optionalAttendees)].filter(
1421
+ (email) => !required.has(email)
1422
+ );
1423
+ const everyone = [...attendees, ...optionalAttendees];
1424
+ requireAllowedEmails(everyone, ctx.allowedDomains);
1425
+ const [response, ...timeZones] = await Promise.all([
1426
+ googleApiRequest(ctx, {
1427
+ body: {
1428
+ items: everyone.map((id) => ({ id })),
1429
+ timeMax: new Date(timeMaxMs).toISOString(),
1430
+ timeMin: new Date(timeMinMs).toISOString()
1431
+ },
1432
+ operation: "google.calendar.freebusy.query",
1433
+ path: "/calendar/v3/freeBusy"
1434
+ }),
1435
+ ...everyone.map((email) => calendarTimeZone(ctx, email))
1436
+ ]);
1089
1437
  if (response.status !== 200) {
1090
1438
  throw googleApiError("google.calendar.freebusy.query", response);
1091
1439
  }
1092
1440
  const { calendars } = freeBusyResponseSchema.parse(response.body);
1093
1441
  const checked = [];
1094
1442
  const unavailable2 = [];
1095
- const busy = [];
1096
- for (const email of attendees) {
1443
+ const slotAttendees = [];
1444
+ everyone.forEach((email, index) => {
1097
1445
  const calendar = calendars[email];
1098
1446
  const error = calendar?.errors?.[0]?.reason;
1099
1447
  if (!calendar || error) {
1100
1448
  unavailable2.push({ email, reason: error ?? "notFound" });
1101
- continue;
1449
+ return;
1102
1450
  }
1103
- checked.push(email);
1104
- for (const interval of calendar.busy) {
1105
- busy.push({
1451
+ const timeZone = timeZones[index];
1452
+ checked.push({ email, ...timeZone ? { timeZone } : void 0 });
1453
+ slotAttendees.push({
1454
+ busy: calendar.busy.map((interval) => ({
1106
1455
  endMs: Date.parse(interval.end),
1107
1456
  startMs: Date.parse(interval.start)
1108
- });
1109
- }
1110
- }
1111
- const slots = checked.length === 0 ? [] : findFreeSlots({
1112
- busy,
1457
+ })),
1458
+ email,
1459
+ required: required.has(email),
1460
+ timeZone: timeZone ?? input.timeZone
1461
+ });
1462
+ });
1463
+ const slots = !slotAttendees.some((attendee) => attendee.required) ? [] : findFreeSlots({
1464
+ attendees: slotAttendees,
1113
1465
  durationMinutes: input.durationMinutes,
1114
1466
  maxResults: input.maxResults,
1115
1467
  timeMaxMs,
@@ -1118,12 +1470,21 @@ function createFindMeetingTimesTool(ctx) {
1118
1470
  workdayEnd: input.workdayEnd,
1119
1471
  workdayStart: input.workdayStart
1120
1472
  });
1473
+ const otherTimeZones = [
1474
+ ...new Set(slotAttendees.map((attendee) => attendee.timeZone))
1475
+ ].filter((timeZone) => timeZone !== input.timeZone);
1121
1476
  return {
1122
1477
  target: "findMeetingTimes",
1123
1478
  checked,
1124
1479
  slots: slots.map((slot) => ({
1125
1480
  end: new Date(slot.endMs).toISOString(),
1126
1481
  label: formatInterval(slot.startMs, slot.endMs, input.timeZone),
1482
+ ...otherTimeZones.length ? {
1483
+ otherTimeZoneLabels: otherTimeZones.map(
1484
+ (timeZone) => formatInterval(slot.startMs, slot.endMs, timeZone)
1485
+ )
1486
+ } : void 0,
1487
+ ...slot.optionalConflicts.length ? { optionalConflicts: slot.optionalConflicts } : void 0,
1127
1488
  start: new Date(slot.startMs).toISOString()
1128
1489
  })),
1129
1490
  timeZone: input.timeZone,
@@ -1135,61 +1496,70 @@ function createFindMeetingTimesTool(ctx) {
1135
1496
 
1136
1497
  // src/tools/list-events.ts
1137
1498
  import {
1138
- definePluginTool as definePluginTool3,
1499
+ definePluginTool as definePluginTool4,
1139
1500
  PluginToolInputError as PluginToolInputError4,
1140
- pluginToolOutputSchema as pluginToolOutputSchema3
1501
+ pluginToolOutputSchema as pluginToolOutputSchema4
1141
1502
  } from "@sentry/junior-plugin-api";
1142
- import { z as z5 } from "zod";
1503
+ import { z as z6 } from "zod";
1143
1504
  var MAX_WINDOW_MS2 = 31 * 24 * 60 * 60 * 1e3;
1144
- var inputSchema3 = z5.object({
1145
- calendar: z5.string().trim().toLowerCase().pipe(z5.email()).describe("Email address of the person whose calendar to read."),
1146
- timeMin: z5.iso.datetime({ offset: true }).describe("Start of the window, RFC 3339 with offset."),
1147
- timeMax: z5.iso.datetime({ offset: true }).describe(
1505
+ var inputSchema4 = z6.object({
1506
+ calendar: z6.string().trim().toLowerCase().pipe(z6.email()).optional().describe(
1507
+ "Email address of the person whose calendar to read. Omit to read Junior's own calendar, which holds every event Junior organizes."
1508
+ ),
1509
+ timeMin: z6.iso.datetime({ offset: true }).describe("Start of the window, RFC 3339 with offset."),
1510
+ timeMax: z6.iso.datetime({ offset: true }).describe(
1148
1511
  "End of the window, RFC 3339 with offset. At most 31 days after timeMin."
1149
1512
  ),
1150
1513
  timeZone: timeZoneSchema,
1151
- maxResults: z5.number().int().min(1).max(100).default(50)
1514
+ maxResults: z6.number().int().min(1).max(100).default(50)
1152
1515
  }).strict();
1153
- var eventSchema = z5.object({
1154
- attendees: z5.array(z5.string()).optional(),
1155
- end: z5.string(),
1156
- eventId: z5.string(),
1157
- label: z5.string(),
1158
- location: z5.string().optional(),
1159
- organizer: z5.string().optional(),
1160
- start: z5.string(),
1161
- title: z5.string().optional(),
1162
- url: z5.string().optional(),
1163
- videoCallUrl: z5.string().optional()
1516
+ var eventSchema = z6.object({
1517
+ attendees: z6.array(attendeeOutputSchema).optional(),
1518
+ end: z6.string(),
1519
+ eventId: z6.string(),
1520
+ label: z6.string(),
1521
+ location: z6.string().optional(),
1522
+ organizer: z6.string().optional(),
1523
+ organizedByJunior: z6.boolean().describe(
1524
+ "True when Junior organizes the event, so Junior can change or cancel it with this eventId."
1525
+ ),
1526
+ seriesEventId: z6.string().optional().describe(
1527
+ "Set when this is one occurrence of a repeating event. It is the id of the whole series."
1528
+ ),
1529
+ start: z6.string(),
1530
+ title: z6.string().optional(),
1531
+ url: z6.string().optional(),
1532
+ videoCallUrl: z6.string().optional()
1164
1533
  });
1165
- var outputSchema3 = pluginToolOutputSchema3.extend({
1166
- target: z5.literal("listCalendarEvents"),
1167
- calendar: z5.string(),
1168
- events: z5.array(eventSchema),
1169
- timeZone: z5.string(),
1170
- truncated: z5.boolean(),
1171
- visible: z5.boolean()
1534
+ var outputSchema4 = pluginToolOutputSchema4.extend({
1535
+ target: z6.literal("listCalendarEvents"),
1536
+ calendar: z6.string(),
1537
+ events: z6.array(eventSchema),
1538
+ timeZone: z6.string(),
1539
+ truncated: z6.boolean(),
1540
+ visible: z6.boolean()
1172
1541
  });
1173
- var eventTimeSchema = z5.object({
1174
- date: z5.string().optional(),
1175
- dateTime: z5.string().optional()
1542
+ var eventTimeSchema = z6.object({
1543
+ date: z6.string().optional(),
1544
+ dateTime: z6.string().optional()
1176
1545
  });
1177
- var listResponseSchema = z5.object({
1178
- items: z5.array(
1179
- z5.object({
1180
- attendees: z5.array(z5.object({ email: z5.string() })).optional(),
1546
+ var listResponseSchema = z6.object({
1547
+ items: z6.array(
1548
+ z6.object({
1549
+ attendees: z6.array(googleAttendeeSchema).optional(),
1181
1550
  end: eventTimeSchema,
1182
- hangoutLink: z5.string().optional(),
1183
- htmlLink: z5.string().optional(),
1184
- id: z5.string(),
1185
- location: z5.string().optional(),
1186
- organizer: z5.object({ email: z5.string().optional() }).optional(),
1551
+ hangoutLink: z6.string().optional(),
1552
+ htmlLink: z6.string().optional(),
1553
+ id: z6.string(),
1554
+ location: z6.string().optional(),
1555
+ organizer: z6.object({ email: z6.string().optional() }).optional(),
1556
+ recurringEventId: z6.string().optional(),
1187
1557
  start: eventTimeSchema,
1188
- status: z5.string().optional(),
1189
- summary: z5.string().optional()
1558
+ status: z6.string().optional(),
1559
+ summary: z6.string().optional()
1190
1560
  })
1191
1561
  ).default([]),
1192
- nextPageToken: z5.string().optional()
1562
+ nextPageToken: z6.string().optional()
1193
1563
  });
1194
1564
  function lastAllDayDate(end) {
1195
1565
  if (!end.date) return void 0;
@@ -1210,16 +1580,16 @@ function eventLabel(start, end, timeZone) {
1210
1580
  return last && last !== first ? `${first} to ${last}, all day` : `${first}, all day`;
1211
1581
  }
1212
1582
  function createListCalendarEventsTool(ctx) {
1213
- return definePluginTool3({
1583
+ return definePluginTool4({
1214
1584
  annotations: {
1215
1585
  destructiveHint: false,
1216
1586
  idempotentHint: true,
1217
1587
  openWorldHint: true,
1218
1588
  readOnlyHint: true
1219
1589
  },
1220
- description: "List events on a colleague's Google Calendar, as seen by Junior's own Google account. Shows titles, times, locations, organizer, and attendees when the calendar's sharing settings allow it. Events without a title are busy blocks the owner did not share. Only calendars in the company's Google Workspace domains can be read. Calendar details can be private: share only what the requester needs, and avoid repeating them in public channels.",
1221
- inputSchema: inputSchema3,
1222
- outputSchema: outputSchema3,
1590
+ description: "List events on a colleague's Google Calendar or on Junior's own calendar, as seen by Junior's own Google account. Shows titles, times, locations, organizer, and attendee responses when the calendar's sharing settings allow it. Use it to find an event to change or cancel, check who accepted, or see what someone has coming up. Events without a title are busy blocks the owner did not share. Only calendars in the company's Google Workspace domains can be read. Calendar details can be private: share only what the requester needs, and avoid repeating them in public channels.",
1591
+ inputSchema: inputSchema4,
1592
+ outputSchema: outputSchema4,
1223
1593
  async execute(input) {
1224
1594
  const timeMinMs = Date.parse(input.timeMin);
1225
1595
  const timeMaxMs = Date.parse(input.timeMax);
@@ -1229,10 +1599,11 @@ function createListCalendarEventsTool(ctx) {
1229
1599
  if (timeMaxMs - timeMinMs > MAX_WINDOW_MS2) {
1230
1600
  throw new PluginToolInputError4("Read at most 31 days at a time.");
1231
1601
  }
1232
- requireAllowedEmails([input.calendar], ctx.allowedDomains);
1602
+ const calendar = input.calendar ?? ctx.accountEmail;
1603
+ requireAllowedEmails([calendar], ctx.allowedDomains);
1233
1604
  const response = await googleApiRequest(ctx, {
1234
1605
  operation: "google.calendar.events.list",
1235
- path: `/calendar/v3/calendars/${encodeURIComponent(input.calendar)}/events`,
1606
+ path: `/calendar/v3/calendars/${calendar === ctx.accountEmail ? "primary" : encodeURIComponent(calendar)}/events`,
1236
1607
  query: {
1237
1608
  maxResults: String(input.maxResults),
1238
1609
  orderBy: "startTime",
@@ -1244,7 +1615,7 @@ function createListCalendarEventsTool(ctx) {
1244
1615
  });
1245
1616
  const result = {
1246
1617
  target: "listCalendarEvents",
1247
- calendar: input.calendar,
1618
+ calendar,
1248
1619
  timeZone: input.timeZone
1249
1620
  };
1250
1621
  if (response.status === 404) {
@@ -1264,7 +1635,9 @@ function createListCalendarEventsTool(ctx) {
1264
1635
  ...event.summary ? { title: event.summary } : void 0,
1265
1636
  ...event.location ? { location: event.location } : void 0,
1266
1637
  ...event.organizer?.email ? { organizer: event.organizer.email } : void 0,
1267
- ...event.attendees ? { attendees: event.attendees.map((person) => person.email) } : void 0,
1638
+ organizedByJunior: event.organizer?.email?.toLowerCase() === ctx.accountEmail,
1639
+ ...event.recurringEventId ? { seriesEventId: event.recurringEventId } : void 0,
1640
+ ...event.attendees ? { attendees: attendeeResults(event.attendees) } : void 0,
1268
1641
  ...event.htmlLink ? { url: event.htmlLink } : void 0,
1269
1642
  ...event.hangoutLink ? { videoCallUrl: event.hangoutLink } : void 0
1270
1643
  })),
@@ -1277,39 +1650,42 @@ function createListCalendarEventsTool(ctx) {
1277
1650
 
1278
1651
  // src/tools/update-event.ts
1279
1652
  import {
1280
- definePluginTool as definePluginTool4,
1653
+ definePluginTool as definePluginTool5,
1281
1654
  PluginToolInputError as PluginToolInputError5,
1282
- pluginToolOutputSchema as pluginToolOutputSchema4
1655
+ pluginToolOutputSchema as pluginToolOutputSchema5
1283
1656
  } from "@sentry/junior-plugin-api";
1284
- import { z as z6 } from "zod";
1285
- var inputSchema4 = z6.object({
1286
- eventId: z6.string().regex(/^[A-Za-z0-9_]{5,1024}$/, "Use an event id from a Calendar tool").describe(
1287
- "Event id from createCalendarEvent or listCalendarEvents. Only events that Junior organizes can be changed."
1288
- ),
1289
- title: z6.string().trim().min(1).max(200).optional(),
1290
- description: z6.string().max(4e3).optional().describe(
1657
+ import { z as z7 } from "zod";
1658
+ var inputSchema5 = z7.object({
1659
+ eventId: ownEventIdSchema,
1660
+ title: z7.string().trim().min(1).max(200).optional(),
1661
+ description: z7.string().max(4e3).optional().describe(
1291
1662
  "Replacement description. Do not include private details the attendees should not see."
1292
1663
  ),
1293
- start: z6.iso.datetime({ offset: true }).optional().describe("New start, RFC 3339 with offset. Give end too."),
1294
- end: z6.iso.datetime({ offset: true }).optional().describe("New end, RFC 3339 with offset. Give start too."),
1664
+ start: z7.iso.datetime({ offset: true }).optional().describe("New start, RFC 3339 with offset. Give end too."),
1665
+ end: z7.iso.datetime({ offset: true }).optional().describe("New end, RFC 3339 with offset. Give start too."),
1295
1666
  timeZone: timeZoneSchema,
1296
- addAttendees: emailListSchema(50).default([]).describe("Email addresses to invite."),
1297
- removeAttendees: emailListSchema(50).default([]).describe("Email addresses to remove from the event.")
1667
+ location: z7.string().trim().max(500).optional().describe(
1668
+ "Replacement location, such as a room name or address. Use an empty string to clear it."
1669
+ ),
1670
+ addAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as required attendees."),
1671
+ addOptionalAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as optional attendees."),
1672
+ removeAttendees: emailListSchema(50).default([]).describe("Email addresses to remove from the event."),
1673
+ addVideoCall: z7.literal(true).optional().describe("Add a Google Meet link when the event has none.")
1298
1674
  }).strict().refine(
1299
1675
  (input) => input.start === void 0 === (input.end === void 0),
1300
1676
  {
1301
1677
  message: "Give both start and end to move an event."
1302
1678
  }
1303
1679
  ).refine(
1304
- (input) => input.title !== void 0 || input.description !== void 0 || input.start !== void 0 || input.addAttendees.length > 0 || input.removeAttendees.length > 0,
1680
+ (input) => input.title !== void 0 || input.description !== void 0 || input.start !== void 0 || input.location !== void 0 || input.addVideoCall !== void 0 || input.addAttendees.length > 0 || input.addOptionalAttendees.length > 0 || input.removeAttendees.length > 0,
1305
1681
  { message: "Give at least one change." }
1306
1682
  );
1307
- var outputSchema4 = pluginToolOutputSchema4.extend({
1308
- target: z6.literal("updateCalendarEvent"),
1683
+ var outputSchema5 = pluginToolOutputSchema5.extend({
1684
+ target: z7.literal("updateCalendarEvent"),
1309
1685
  ...ownEventOutputFields
1310
1686
  });
1311
1687
  function createUpdateCalendarEventTool(ctx) {
1312
- return definePluginTool4({
1688
+ return definePluginTool5({
1313
1689
  annotations: {
1314
1690
  destructiveHint: false,
1315
1691
  idempotentHint: true,
@@ -1321,14 +1697,17 @@ function createUpdateCalendarEventTool(ctx) {
1321
1697
  input.title && `rename it to "${input.title}"`,
1322
1698
  input.start && `move it to ${input.start} \u2013 ${input.end}`,
1323
1699
  input.description !== void 0 && "replace the description",
1700
+ input.location !== void 0 && (input.location ? `set the location to ${input.location}` : "clear the location"),
1701
+ input.addVideoCall && "add a Google Meet link",
1324
1702
  input.addAttendees.length > 0 && `invite ${input.addAttendees.join(", ")}`,
1703
+ input.addOptionalAttendees.length > 0 && `invite ${input.addOptionalAttendees.join(", ")} as optional`,
1325
1704
  input.removeAttendees.length > 0 && `remove ${input.removeAttendees.join(", ")}`
1326
1705
  ].filter(Boolean);
1327
1706
  return `Update Google Calendar event ${input.eventId}: ${changes.join("; ")}. Attendees get an email about the change.`;
1328
1707
  },
1329
- description: "Change a Google Calendar event organized by Junior's own Google account: title, description, time, or attendees. Google emails the attendees about the change. Only people invited to the event can ask for changes, and only people in the company's Google Workspace domains can be invited. Use after the requester confirms the change.",
1330
- inputSchema: inputSchema4,
1331
- outputSchema: outputSchema4,
1708
+ description: "Change a Google Calendar event organized by Junior's own Google account: title, description, time, location, video call, or attendees. Use it to reschedule, add or remove people, or fix details. Google emails the attendees about the change, and existing attendees keep their responses. Only people invited to the event can ask for changes, and only people in the company's Google Workspace domains can be invited. Junior cannot change events other people organize. For a repeating event, an occurrence id changes only that occurrence; its seriesEventId changes every occurrence. Confirm the change with the requester first unless they already stated it exactly.",
1709
+ inputSchema: inputSchema5,
1710
+ outputSchema: outputSchema5,
1332
1711
  async execute(input) {
1333
1712
  if (input.start && input.end) {
1334
1713
  const durationMs = Date.parse(input.end) - Date.parse(input.start);
@@ -1339,39 +1718,19 @@ function createUpdateCalendarEventTool(ctx) {
1339
1718
  throw new PluginToolInputError5("Events can be at most 8 hours long.");
1340
1719
  }
1341
1720
  }
1342
- requireAllowedEmails(input.addAttendees, ctx.allowedDomains);
1343
- const requester = await requesterEmail(ctx);
1344
- if (!requester) {
1345
- throw new PluginToolInputError5(
1346
- "Junior does not know the requester's email, so it cannot check that they may change this event."
1347
- );
1348
- }
1349
- const path = `/calendar/v3/calendars/primary/events/${input.eventId}`;
1350
- const current = await googleApiRequest(ctx, {
1351
- operation: "google.calendar.event.get",
1352
- path
1353
- });
1354
- if (current.status === 404) {
1355
- throw new PluginToolInputError5(
1356
- `Junior's calendar has no event ${input.eventId}.`
1357
- );
1358
- }
1359
- if (current.status !== 200) {
1360
- throw googleApiError("google.calendar.event.get", current);
1361
- }
1362
- const event = ownEventSchema.parse(current.body);
1363
- if (event.organizer?.self !== true) {
1721
+ requireAllowedEmails(
1722
+ [...input.addAttendees, ...input.addOptionalAttendees],
1723
+ ctx.allowedDomains
1724
+ );
1725
+ const read = await readOwnEventForRequester(ctx, input.eventId, "change");
1726
+ if (read.status === "cancelled") {
1364
1727
  throw new PluginToolInputError5(
1365
- "Junior does not organize this event, so it cannot change it."
1728
+ "This event is cancelled. Create a new event instead."
1366
1729
  );
1367
1730
  }
1731
+ const { event, path } = read;
1368
1732
  const attendees = event.attendees ?? [];
1369
1733
  const emailKey = (email) => email.toLowerCase();
1370
- if (!attendees.some((attendee) => emailKey(attendee.email) === requester)) {
1371
- throw new PluginToolInputError5(
1372
- "Only people invited to this event can ask Junior to change it."
1373
- );
1374
- }
1375
1734
  const removed = new Set(input.removeAttendees);
1376
1735
  const kept = attendees.filter(
1377
1736
  (attendee) => !removed.has(emailKey(attendee.email))
@@ -1379,8 +1738,19 @@ function createUpdateCalendarEventTool(ctx) {
1379
1738
  const existing = new Set(
1380
1739
  kept.map((attendee) => emailKey(attendee.email))
1381
1740
  );
1382
- const added = input.addAttendees.filter((email) => !existing.has(email)).map((email) => ({ email }));
1741
+ const added = [
1742
+ ...input.addAttendees.map((email) => ({ email })),
1743
+ ...input.addOptionalAttendees.map((email) => ({
1744
+ email,
1745
+ optional: true
1746
+ }))
1747
+ ].filter((attendee) => {
1748
+ if (existing.has(attendee.email)) return false;
1749
+ existing.add(attendee.email);
1750
+ return true;
1751
+ });
1383
1752
  const attendeesChanged = kept.length !== attendees.length || added.length;
1753
+ const addVideoCall = input.addVideoCall && !event.hangoutLink;
1384
1754
  const response = await googleApiRequest(ctx, {
1385
1755
  body: {
1386
1756
  ...input.title !== void 0 && { summary: input.title },
@@ -1391,11 +1761,23 @@ function createUpdateCalendarEventTool(ctx) {
1391
1761
  end: { dateTime: input.end, timeZone: input.timeZone },
1392
1762
  start: { dateTime: input.start, timeZone: input.timeZone }
1393
1763
  },
1394
- ...attendeesChanged && { attendees: [...kept, ...added] }
1764
+ ...input.location !== void 0 && { location: input.location },
1765
+ ...attendeesChanged && { attendees: [...kept, ...added] },
1766
+ ...addVideoCall && {
1767
+ conferenceData: {
1768
+ createRequest: {
1769
+ conferenceSolutionKey: { type: "hangoutsMeet" },
1770
+ requestId: `${event.id}-meet`
1771
+ }
1772
+ }
1773
+ }
1395
1774
  },
1396
1775
  operation: "google.calendar.event.update",
1397
1776
  path,
1398
- query: { sendUpdates: "all" }
1777
+ query: {
1778
+ ...addVideoCall && { conferenceDataVersion: "1" },
1779
+ sendUpdates: "all"
1780
+ }
1399
1781
  });
1400
1782
  if (response.status !== 200) {
1401
1783
  throw googleApiError("google.calendar.event.update", response);
@@ -1441,11 +1823,13 @@ function googlePlugin() {
1441
1823
  return {};
1442
1824
  }
1443
1825
  const toolContext = {
1826
+ accountEmail: config.accountEmail,
1444
1827
  allowedDomains: config.allowedDomains,
1445
1828
  egress: ctx.egress,
1446
1829
  users: ctx.users
1447
1830
  };
1448
1831
  return {
1832
+ cancelCalendarEvent: createCancelCalendarEventTool(toolContext),
1449
1833
  createCalendarEvent: createCreateCalendarEventTool(toolContext),
1450
1834
  findMeetingTimes: createFindMeetingTimesTool(toolContext),
1451
1835
  listCalendarEvents: createListCalendarEventsTool(toolContext),