@sentry/junior-google 0.238.1 → 0.240.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";
@@ -741,8 +744,8 @@ import {
741
744
  } from "@sentry/junior-plugin-api";
742
745
  import { z as z2 } from "zod";
743
746
  var emailListSchema = (max) => z2.array(z2.string().trim().toLowerCase().pipe(z2.email())).max(max).describe("Attendee email addresses.");
744
- var timeZoneSchema = z2.string().min(1).refine(isTimeZone, "timeZone must be an IANA time zone").describe(
745
- "IANA time zone used to read and show times, such as America/Los_Angeles."
747
+ var timeZoneSchema = z2.string().min(1).refine(isTimeZone, "timeZone must be an IANA time zone").optional().describe(
748
+ "IANA time zone used to read and show times, such as America/Los_Angeles. Omit to use the requester's time zone."
746
749
  );
747
750
  function isTimeZone(value) {
748
751
  try {
@@ -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."
988
+ ),
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 timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
1007
+ const read = await readOwnEventForRequester(ctx, input.eventId, "cancel");
1008
+ if (read.status === "cancelled") {
1009
+ return {
1010
+ target: "cancelCalendarEvent",
1011
+ cancelled: false,
1012
+ eventId: read.eventId
1013
+ };
1014
+ }
1015
+ const { event, path } = read;
1016
+ const result = {
1017
+ target: "cancelCalendarEvent",
1018
+ ...ownEventResult(event, timeZone)
1019
+ };
1020
+ const response = await googleApiRequest(ctx, {
1021
+ operation: "google.calendar.event.delete",
1022
+ path,
1023
+ query: { sendUpdates: "all" }
1024
+ });
1025
+ if (response.status !== 204 && response.status !== 410) {
1026
+ throw googleApiError("google.calendar.event.delete", response);
1027
+ }
1028
+ return {
1029
+ ...result,
1030
+ // Keep the card in the Conversation, and show that the event is gone.
1031
+ objectAnnotations: result.objectAnnotations.map((annotation) => ({
1032
+ ...annotation,
1033
+ status: "cancelled"
1034
+ })),
1035
+ cancelled: response.status === 204
1036
+ };
1037
+ }
1038
+ });
1039
+ }
1040
+
1041
+ // src/tools/create-event.ts
1042
+ import { createHash as createHash2, randomUUID } from "crypto";
1043
+ import {
1044
+ definePluginTool as definePluginTool2,
1045
+ PluginToolInputError as PluginToolInputError2,
1046
+ pluginToolOutputSchema as pluginToolOutputSchema2
1047
+ } from "@sentry/junior-plugin-api";
1048
+ import { z as z4 } from "zod";
1049
+ var repeatSchema = z4.object({
1050
+ frequency: z4.enum(["weekdays", "weekly", "monthly"]).describe(
1051
+ "`weekdays` repeats Monday to Friday. `weekly` repeats on the start's weekday. `monthly` repeats on the start's day of the month."
1052
+ ),
1053
+ interval: z4.number().int().min(1).max(12).default(1).describe(
1054
+ "Repeat every N weeks or months. 2 with `weekly` means every other week. Ignored for `weekdays`."
865
1055
  ),
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."),
1056
+ count: z4.number().int().min(2).max(200).optional().describe("Number of occurrences. Omit when the series has no end.")
1057
+ }).strict();
1058
+ function recurrenceRule(repeat) {
1059
+ const parts = repeat.frequency === "weekdays" ? ["FREQ=WEEKLY", "BYDAY=MO,TU,WE,TH,FR"] : [
1060
+ `FREQ=${repeat.frequency === "weekly" ? "WEEKLY" : "MONTHLY"}`,
1061
+ ...repeat.interval > 1 ? [`INTERVAL=${repeat.interval}`] : []
1062
+ ];
1063
+ if (repeat.count) parts.push(`COUNT=${repeat.count}`);
1064
+ return `RRULE:${parts.join(";")}`;
1065
+ }
1066
+ var inputSchema2 = z4.object({
1067
+ title: z4.string().trim().min(1).max(200).describe(
1068
+ 'Specific title that says who and what, such as "Alice / Bob: Q4 hiring plan". Avoid generic titles such as "Meeting" or "Sync".'
1069
+ ),
1070
+ description: z4.string().max(4e3).optional().describe(
1071
+ "Purpose, agenda, and links attendees need to prepare. Do not include private details the attendees should not see."
1072
+ ),
1073
+ location: z4.string().trim().max(500).optional().describe("Room name or address for an in-person meeting."),
1074
+ start: z4.iso.datetime({ offset: true }).describe("Event start, RFC 3339 with offset."),
1075
+ end: z4.iso.datetime({ offset: true }).describe("Event end, RFC 3339 with offset."),
868
1076
  timeZone: timeZoneSchema,
869
1077
  attendees: emailListSchema(50).describe(
870
- "Email addresses to invite. Junior adds the requester automatically when it knows their email."
1078
+ "Email addresses of required attendees. Junior adds the requester automatically when it knows their email."
871
1079
  ),
872
- addVideoCall: z3.boolean().default(true).describe("Add a Google Meet link.")
1080
+ optionalAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as optional attendees."),
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,12 +1097,15 @@ 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. Every event gets a Google Meet link. 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) {
1108
+ const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
897
1109
  const startMs = Date.parse(input.start);
898
1110
  const endMs = Date.parse(input.end);
899
1111
  if (endMs <= startMs) {
@@ -903,33 +1115,43 @@ function createCreateCalendarEventTool(ctx) {
903
1115
  throw new PluginToolInputError2("Events can be at most 8 hours long.");
904
1116
  }
905
1117
  const attendees = await withRequester(ctx, input.attendees);
906
- if (attendees.length === 0) {
1118
+ const required = new Set(attendees);
1119
+ const optionalAttendees = [...new Set(input.optionalAttendees)].filter(
1120
+ (email) => !required.has(email)
1121
+ );
1122
+ if (attendees.length + optionalAttendees.length === 0) {
907
1123
  throw new PluginToolInputError2("Invite at least one attendee.");
908
1124
  }
909
- requireAllowedEmails(attendees, ctx.allowedDomains);
1125
+ requireAllowedEmails(
1126
+ [...attendees, ...optionalAttendees],
1127
+ ctx.allowedDomains
1128
+ );
910
1129
  const eventId = calendarEventId(options.toolCallId);
911
1130
  const response = await googleApiRequest(ctx, {
912
1131
  body: {
913
- attendees: attendees.map((email) => ({ email })),
914
- ...input.addVideoCall ? {
915
- conferenceData: {
916
- createRequest: {
917
- conferenceSolutionKey: { type: "hangoutsMeet" },
918
- requestId: eventId
919
- }
1132
+ attendees: [
1133
+ ...attendees.map((email) => ({ email })),
1134
+ ...optionalAttendees.map((email) => ({ email, optional: true }))
1135
+ ],
1136
+ conferenceData: {
1137
+ createRequest: {
1138
+ conferenceSolutionKey: { type: "hangoutsMeet" },
1139
+ requestId: eventId
920
1140
  }
921
- } : void 0,
1141
+ },
922
1142
  ...input.description ? { description: input.description } : void 0,
923
- end: { dateTime: input.end, timeZone: input.timeZone },
1143
+ end: { dateTime: input.end, timeZone },
924
1144
  guestsCanModify: false,
925
1145
  id: eventId,
926
- start: { dateTime: input.start, timeZone: input.timeZone },
1146
+ ...input.location ? { location: input.location } : void 0,
1147
+ ...input.repeat ? { recurrence: [recurrenceRule(input.repeat)] } : void 0,
1148
+ start: { dateTime: input.start, timeZone },
927
1149
  summary: input.title
928
1150
  },
929
1151
  operation: "google.calendar.event.create",
930
1152
  path: "/calendar/v3/calendars/primary/events",
931
1153
  query: {
932
- conferenceDataVersion: input.addVideoCall ? "1" : "0",
1154
+ conferenceDataVersion: "1",
933
1155
  sendUpdates: "all"
934
1156
  }
935
1157
  });
@@ -950,7 +1172,7 @@ function createCreateCalendarEventTool(ctx) {
950
1172
  return {
951
1173
  target: "createCalendarEvent",
952
1174
  created,
953
- ...ownEventResult(ownEventSchema.parse(event.body), input.timeZone)
1175
+ ...ownEventResult(ownEventSchema.parse(event.body), timeZone)
954
1176
  };
955
1177
  }
956
1178
  });
@@ -958,48 +1180,73 @@ function createCreateCalendarEventTool(ctx) {
958
1180
 
959
1181
  // src/tools/find-meeting-times.ts
960
1182
  import {
961
- definePluginTool as definePluginTool2,
1183
+ definePluginTool as definePluginTool3,
962
1184
  PluginToolInputError as PluginToolInputError3,
963
- pluginToolOutputSchema as pluginToolOutputSchema2
1185
+ pluginToolOutputSchema as pluginToolOutputSchema3
964
1186
  } 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({
1187
+ import { z as z5 } from "zod";
1188
+ var DAY_MS = 24 * 60 * 60 * 1e3;
1189
+ var MAX_WINDOW_MS = 14 * DAY_MS;
1190
+ var DEFAULT_WINDOW_MS = 7 * DAY_MS;
1191
+ var clockTimeSchema = z5.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use 24-hour HH:MM");
1192
+ var inputSchema3 = z5.object({
969
1193
  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."
1194
+ "Work emails of the people who must attend. Resolve names with userLookup. Junior adds the requester automatically when it knows their email."
971
1195
  ),
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."
1196
+ optionalAttendees: emailListSchema(20).default([]).describe(
1197
+ '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
1198
  ),
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)
1199
+ durationMinutes: z5.number().int().min(15).max(480).default(30),
1200
+ timeMin: z5.iso.datetime({ offset: true }).optional().describe(
1201
+ "Start of the search window, RFC 3339 with offset. Defaults to now. For an exact requested time, search only that slot."
1202
+ ),
1203
+ timeMax: z5.iso.datetime({ offset: true }).optional().describe(
1204
+ "End of the search window, RFC 3339 with offset. Defaults to 7 days after timeMin. At most 14 days after timeMin."
1205
+ ),
1206
+ timeZone: timeZoneSchema.describe(
1207
+ "IANA time zone for slot labels, such as America/Los_Angeles. Omit to use the requester's time zone. It is also the working-hours zone for anyone whose calendar time zone Junior cannot read."
1208
+ ),
1209
+ workdayStart: clockTimeSchema.default("09:00").describe(
1210
+ "Earliest local start, 24-hour HH:MM, in each attendee's own calendar time zone."
1211
+ ),
1212
+ workdayEnd: clockTimeSchema.default("17:00").describe(
1213
+ "Latest local end, 24-hour HH:MM, in each attendee's own calendar time zone."
1214
+ ),
1215
+ maxResults: z5.number().int().min(1).max(20).default(3)
981
1216
  }).strict();
982
- var slotSchema = z4.object({
983
- end: z4.string(),
984
- label: z4.string(),
985
- start: z4.string()
1217
+ var slotSchema = z5.object({
1218
+ end: z5.string(),
1219
+ label: z5.string().describe("The slot in the requester's time zone."),
1220
+ otherTimeZoneLabels: z5.array(z5.string()).optional().describe("The slot in the other attendees' time zones."),
1221
+ optionalConflicts: z5.array(z5.string()).optional().describe(
1222
+ "Optional attendees who are busy or outside their working hours at this time."
1223
+ ),
1224
+ start: z5.string()
986
1225
  });
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() }))
1226
+ var outputSchema3 = pluginToolOutputSchema3.extend({
1227
+ target: z5.literal("findMeetingTimes"),
1228
+ checked: z5.array(
1229
+ z5.object({
1230
+ email: z5.string(),
1231
+ timeZone: z5.string().optional().describe(
1232
+ "Time zone of the person's calendar. Missing when Junior cannot read it; their hours then use the requester's time zone."
1233
+ )
1234
+ })
1235
+ ),
1236
+ slots: z5.array(slotSchema),
1237
+ timeZone: z5.string(),
1238
+ unavailable: z5.array(z5.object({ email: z5.string(), reason: z5.string() }))
993
1239
  });
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()
1240
+ var freeBusyResponseSchema = z5.object({
1241
+ calendars: z5.record(
1242
+ z5.string(),
1243
+ z5.object({
1244
+ busy: z5.array(z5.object({ end: z5.string(), start: z5.string() })).default([]),
1245
+ errors: z5.array(z5.object({ reason: z5.string() })).optional()
1000
1246
  })
1001
1247
  )
1002
1248
  });
1249
+ var calendarTimeZoneSchema = z5.object({ timeZone: z5.string().optional() });
1003
1250
  function localParts(ms, format) {
1004
1251
  const parts = Object.fromEntries(
1005
1252
  format.formatToParts(ms).map((part) => [part.type, part.value])
@@ -1010,58 +1257,151 @@ function localParts(ms, format) {
1010
1257
  weekday: parts.weekday ?? ""
1011
1258
  };
1012
1259
  }
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", {
1260
+ function localFormat(timeZone) {
1261
+ return new Intl.DateTimeFormat("en-US", {
1021
1262
  day: "2-digit",
1022
1263
  hour: "2-digit",
1023
1264
  hourCycle: "h23",
1024
1265
  minute: "2-digit",
1025
1266
  month: "2-digit",
1026
- timeZone: input.timeZone,
1267
+ timeZone,
1027
1268
  weekday: "short",
1028
1269
  year: "numeric"
1029
1270
  });
1271
+ }
1272
+ function clockMinutes(value) {
1273
+ const [hours, minutes] = value.split(":").map(Number);
1274
+ return hours * 60 + minutes;
1275
+ }
1276
+ function overlaps(busy, startMs, endMs) {
1277
+ return busy.endMs > startMs && busy.startMs < endMs;
1278
+ }
1279
+ function farthestOption(options, chosen) {
1280
+ if (chosen.length === 0) return options[0];
1281
+ let best;
1282
+ let bestGap = -1;
1283
+ for (const option of options) {
1284
+ const gap = Math.min(
1285
+ ...chosen.map((slot) => Math.abs(slot.startMs - option.startMs))
1286
+ );
1287
+ if (gap > bestGap) {
1288
+ best = option;
1289
+ bestGap = gap;
1290
+ }
1291
+ }
1292
+ return best;
1293
+ }
1294
+ function spreadAcrossDays(candidates, max) {
1295
+ const days = /* @__PURE__ */ new Map();
1296
+ for (const candidate of candidates) {
1297
+ days.set(candidate.date, [...days.get(candidate.date) ?? [], candidate]);
1298
+ }
1299
+ const chosen = /* @__PURE__ */ new Map();
1300
+ const result = [];
1301
+ let progress = true;
1302
+ while (result.length < max && progress) {
1303
+ progress = false;
1304
+ for (const [date, options] of days) {
1305
+ if (result.length >= max) break;
1306
+ const picked = chosen.get(date) ?? [];
1307
+ const next = farthestOption(options, picked);
1308
+ if (!next) continue;
1309
+ options.splice(options.indexOf(next), 1);
1310
+ picked.push(next);
1311
+ chosen.set(date, picked);
1312
+ result.push(next);
1313
+ progress = true;
1314
+ }
1315
+ }
1316
+ return result;
1317
+ }
1318
+ function findFreeSlots(input) {
1319
+ const durationMs = input.durationMinutes * 6e4;
1320
+ const stepMs = (input.durationMinutes < 30 ? 15 : 30) * 6e4;
1030
1321
  const dayStart = clockMinutes(input.workdayStart);
1031
1322
  const dayEnd = clockMinutes(input.workdayEnd);
1032
- const slots = [];
1323
+ const formats = /* @__PURE__ */ new Map();
1324
+ const partsIn = (ms, timeZone) => {
1325
+ let format = formats.get(timeZone);
1326
+ if (!format) {
1327
+ format = localFormat(timeZone);
1328
+ formats.set(timeZone, format);
1329
+ }
1330
+ return localParts(ms, format);
1331
+ };
1332
+ const available = (attendee, startMs2, endMs) => {
1333
+ const start = partsIn(startMs2, attendee.timeZone);
1334
+ const end = partsIn(endMs, attendee.timeZone);
1335
+ 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));
1336
+ };
1337
+ const required = input.attendees.filter((attendee) => attendee.required);
1338
+ const optional = input.attendees.filter((attendee) => !attendee.required);
1339
+ const candidates = [];
1033
1340
  let startMs = Math.ceil(input.timeMinMs / stepMs) * stepMs;
1034
- while (startMs + durationMs <= input.timeMaxMs && slots.length < input.maxResults) {
1341
+ while (startMs + durationMs <= input.timeMaxMs) {
1035
1342
  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 });
1343
+ if (required.every((attendee) => available(attendee, startMs, endMs))) {
1344
+ candidates.push({
1345
+ date: partsIn(startMs, input.timeZone).date,
1346
+ endMs,
1347
+ optionalConflicts: optional.filter((attendee) => !available(attendee, startMs, endMs)).map((attendee) => attendee.email),
1348
+ startMs
1349
+ });
1044
1350
  startMs = endMs;
1045
1351
  } else {
1046
1352
  startMs += stepMs;
1047
1353
  }
1048
1354
  }
1049
- return slots;
1355
+ const picked = spreadAcrossDays(
1356
+ candidates.filter((slot) => slot.optionalConflicts.length === 0),
1357
+ input.maxResults
1358
+ );
1359
+ picked.push(
1360
+ ...spreadAcrossDays(
1361
+ candidates.filter((slot) => slot.optionalConflicts.length > 0),
1362
+ input.maxResults - picked.length
1363
+ )
1364
+ );
1365
+ return picked.sort((a, b) => a.startMs - b.startMs).map(({ endMs, optionalConflicts, startMs: startMs2 }) => ({
1366
+ endMs,
1367
+ optionalConflicts,
1368
+ startMs: startMs2
1369
+ }));
1370
+ }
1371
+ async function calendarTimeZone(ctx, email) {
1372
+ const response = await googleApiRequest(ctx, {
1373
+ operation: "google.calendar.events.list",
1374
+ path: `/calendar/v3/calendars/${encodeURIComponent(email)}/events`,
1375
+ // No `timeZone` parameter: Google then answers with the calendar's own.
1376
+ query: { fields: "timeZone", maxResults: "1" }
1377
+ });
1378
+ if (response.status === 403 || response.status === 404) {
1379
+ return void 0;
1380
+ }
1381
+ if (response.status !== 200) {
1382
+ throw googleApiError("google.calendar.events.list", response);
1383
+ }
1384
+ const { timeZone } = calendarTimeZoneSchema.parse(response.body);
1385
+ return timeZone && isTimeZone(timeZone) ? timeZone : void 0;
1050
1386
  }
1051
1387
  function createFindMeetingTimesTool(ctx) {
1052
- return definePluginTool2({
1388
+ return definePluginTool3({
1053
1389
  annotations: {
1054
1390
  destructiveHint: false,
1055
1391
  idempotentHint: true,
1056
1392
  openWorldHint: true,
1057
1393
  readOnlyHint: true
1058
1394
  },
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,
1395
+ 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.",
1396
+ inputSchema: inputSchema3,
1397
+ outputSchema: outputSchema3,
1062
1398
  async execute(input) {
1063
- const timeMinMs = Math.max(Date.parse(input.timeMin), Date.now());
1064
- const timeMaxMs = Date.parse(input.timeMax);
1399
+ const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
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,57 +1416,81 @@ 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 calendarZone = timeZones[index];
1452
+ checked.push({
1453
+ email,
1454
+ ...calendarZone ? { timeZone: calendarZone } : void 0
1455
+ });
1456
+ slotAttendees.push({
1457
+ busy: calendar.busy.map((interval) => ({
1106
1458
  endMs: Date.parse(interval.end),
1107
1459
  startMs: Date.parse(interval.start)
1108
- });
1109
- }
1110
- }
1111
- const slots = checked.length === 0 ? [] : findFreeSlots({
1112
- busy,
1460
+ })),
1461
+ email,
1462
+ required: required.has(email),
1463
+ timeZone: calendarZone ?? timeZone
1464
+ });
1465
+ });
1466
+ const slots = !slotAttendees.some((attendee) => attendee.required) ? [] : findFreeSlots({
1467
+ attendees: slotAttendees,
1113
1468
  durationMinutes: input.durationMinutes,
1114
1469
  maxResults: input.maxResults,
1115
1470
  timeMaxMs,
1116
1471
  timeMinMs,
1117
- timeZone: input.timeZone,
1472
+ timeZone,
1118
1473
  workdayEnd: input.workdayEnd,
1119
1474
  workdayStart: input.workdayStart
1120
1475
  });
1476
+ const otherTimeZones = [
1477
+ ...new Set(slotAttendees.map((attendee) => attendee.timeZone))
1478
+ ].filter((zone) => zone !== timeZone);
1121
1479
  return {
1122
1480
  target: "findMeetingTimes",
1123
1481
  checked,
1124
1482
  slots: slots.map((slot) => ({
1125
1483
  end: new Date(slot.endMs).toISOString(),
1126
- label: formatInterval(slot.startMs, slot.endMs, input.timeZone),
1484
+ label: formatInterval(slot.startMs, slot.endMs, timeZone),
1485
+ ...otherTimeZones.length ? {
1486
+ otherTimeZoneLabels: otherTimeZones.map(
1487
+ (zone) => formatInterval(slot.startMs, slot.endMs, zone)
1488
+ )
1489
+ } : void 0,
1490
+ ...slot.optionalConflicts.length ? { optionalConflicts: slot.optionalConflicts } : void 0,
1127
1491
  start: new Date(slot.startMs).toISOString()
1128
1492
  })),
1129
- timeZone: input.timeZone,
1493
+ timeZone,
1130
1494
  unavailable: unavailable2
1131
1495
  };
1132
1496
  }
@@ -1135,61 +1499,70 @@ function createFindMeetingTimesTool(ctx) {
1135
1499
 
1136
1500
  // src/tools/list-events.ts
1137
1501
  import {
1138
- definePluginTool as definePluginTool3,
1502
+ definePluginTool as definePluginTool4,
1139
1503
  PluginToolInputError as PluginToolInputError4,
1140
- pluginToolOutputSchema as pluginToolOutputSchema3
1504
+ pluginToolOutputSchema as pluginToolOutputSchema4
1141
1505
  } from "@sentry/junior-plugin-api";
1142
- import { z as z5 } from "zod";
1506
+ import { z as z6 } from "zod";
1143
1507
  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(
1508
+ var inputSchema4 = z6.object({
1509
+ calendar: z6.string().trim().toLowerCase().pipe(z6.email()).optional().describe(
1510
+ "Email address of the person whose calendar to read. Omit to read Junior's own calendar, which holds every event Junior organizes."
1511
+ ),
1512
+ timeMin: z6.iso.datetime({ offset: true }).describe("Start of the window, RFC 3339 with offset."),
1513
+ timeMax: z6.iso.datetime({ offset: true }).describe(
1148
1514
  "End of the window, RFC 3339 with offset. At most 31 days after timeMin."
1149
1515
  ),
1150
1516
  timeZone: timeZoneSchema,
1151
- maxResults: z5.number().int().min(1).max(100).default(50)
1517
+ maxResults: z6.number().int().min(1).max(100).default(50)
1152
1518
  }).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()
1519
+ var eventSchema = z6.object({
1520
+ attendees: z6.array(attendeeOutputSchema).optional(),
1521
+ end: z6.string(),
1522
+ eventId: z6.string(),
1523
+ label: z6.string(),
1524
+ location: z6.string().optional(),
1525
+ organizer: z6.string().optional(),
1526
+ organizedByJunior: z6.boolean().describe(
1527
+ "True when Junior organizes the event, so Junior can change or cancel it with this eventId."
1528
+ ),
1529
+ seriesEventId: z6.string().optional().describe(
1530
+ "Set when this is one occurrence of a repeating event. It is the id of the whole series."
1531
+ ),
1532
+ start: z6.string(),
1533
+ title: z6.string().optional(),
1534
+ url: z6.string().optional(),
1535
+ videoCallUrl: z6.string().optional()
1164
1536
  });
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()
1537
+ var outputSchema4 = pluginToolOutputSchema4.extend({
1538
+ target: z6.literal("listCalendarEvents"),
1539
+ calendar: z6.string(),
1540
+ events: z6.array(eventSchema),
1541
+ timeZone: z6.string(),
1542
+ truncated: z6.boolean(),
1543
+ visible: z6.boolean()
1172
1544
  });
1173
- var eventTimeSchema = z5.object({
1174
- date: z5.string().optional(),
1175
- dateTime: z5.string().optional()
1545
+ var eventTimeSchema = z6.object({
1546
+ date: z6.string().optional(),
1547
+ dateTime: z6.string().optional()
1176
1548
  });
1177
- var listResponseSchema = z5.object({
1178
- items: z5.array(
1179
- z5.object({
1180
- attendees: z5.array(z5.object({ email: z5.string() })).optional(),
1549
+ var listResponseSchema = z6.object({
1550
+ items: z6.array(
1551
+ z6.object({
1552
+ attendees: z6.array(googleAttendeeSchema).optional(),
1181
1553
  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(),
1554
+ hangoutLink: z6.string().optional(),
1555
+ htmlLink: z6.string().optional(),
1556
+ id: z6.string(),
1557
+ location: z6.string().optional(),
1558
+ organizer: z6.object({ email: z6.string().optional() }).optional(),
1559
+ recurringEventId: z6.string().optional(),
1187
1560
  start: eventTimeSchema,
1188
- status: z5.string().optional(),
1189
- summary: z5.string().optional()
1561
+ status: z6.string().optional(),
1562
+ summary: z6.string().optional()
1190
1563
  })
1191
1564
  ).default([]),
1192
- nextPageToken: z5.string().optional()
1565
+ nextPageToken: z6.string().optional()
1193
1566
  });
1194
1567
  function lastAllDayDate(end) {
1195
1568
  if (!end.date) return void 0;
@@ -1210,17 +1583,18 @@ function eventLabel(start, end, timeZone) {
1210
1583
  return last && last !== first ? `${first} to ${last}, all day` : `${first}, all day`;
1211
1584
  }
1212
1585
  function createListCalendarEventsTool(ctx) {
1213
- return definePluginTool3({
1586
+ return definePluginTool4({
1214
1587
  annotations: {
1215
1588
  destructiveHint: false,
1216
1589
  idempotentHint: true,
1217
1590
  openWorldHint: true,
1218
1591
  readOnlyHint: true
1219
1592
  },
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,
1593
+ 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.",
1594
+ inputSchema: inputSchema4,
1595
+ outputSchema: outputSchema4,
1223
1596
  async execute(input) {
1597
+ const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
1224
1598
  const timeMinMs = Date.parse(input.timeMin);
1225
1599
  const timeMaxMs = Date.parse(input.timeMax);
1226
1600
  if (timeMaxMs <= timeMinMs) {
@@ -1229,23 +1603,24 @@ function createListCalendarEventsTool(ctx) {
1229
1603
  if (timeMaxMs - timeMinMs > MAX_WINDOW_MS2) {
1230
1604
  throw new PluginToolInputError4("Read at most 31 days at a time.");
1231
1605
  }
1232
- requireAllowedEmails([input.calendar], ctx.allowedDomains);
1606
+ const calendar = input.calendar ?? ctx.accountEmail;
1607
+ requireAllowedEmails([calendar], ctx.allowedDomains);
1233
1608
  const response = await googleApiRequest(ctx, {
1234
1609
  operation: "google.calendar.events.list",
1235
- path: `/calendar/v3/calendars/${encodeURIComponent(input.calendar)}/events`,
1610
+ path: `/calendar/v3/calendars/${calendar === ctx.accountEmail ? "primary" : encodeURIComponent(calendar)}/events`,
1236
1611
  query: {
1237
1612
  maxResults: String(input.maxResults),
1238
1613
  orderBy: "startTime",
1239
1614
  singleEvents: "true",
1240
1615
  timeMax: new Date(timeMaxMs).toISOString(),
1241
1616
  timeMin: new Date(timeMinMs).toISOString(),
1242
- timeZone: input.timeZone
1617
+ timeZone
1243
1618
  }
1244
1619
  });
1245
1620
  const result = {
1246
1621
  target: "listCalendarEvents",
1247
- calendar: input.calendar,
1248
- timeZone: input.timeZone
1622
+ calendar,
1623
+ timeZone
1249
1624
  };
1250
1625
  if (response.status === 404) {
1251
1626
  return { ...result, events: [], truncated: false, visible: false };
@@ -1259,12 +1634,14 @@ function createListCalendarEventsTool(ctx) {
1259
1634
  events: items.filter((event) => event.status !== "cancelled").map((event) => ({
1260
1635
  end: event.end.dateTime ?? lastAllDayDate(event.end) ?? "",
1261
1636
  eventId: event.id,
1262
- label: eventLabel(event.start, event.end, input.timeZone),
1637
+ label: eventLabel(event.start, event.end, timeZone),
1263
1638
  start: event.start.dateTime ?? event.start.date ?? "",
1264
1639
  ...event.summary ? { title: event.summary } : void 0,
1265
1640
  ...event.location ? { location: event.location } : void 0,
1266
1641
  ...event.organizer?.email ? { organizer: event.organizer.email } : void 0,
1267
- ...event.attendees ? { attendees: event.attendees.map((person) => person.email) } : void 0,
1642
+ organizedByJunior: event.organizer?.email?.toLowerCase() === ctx.accountEmail,
1643
+ ...event.recurringEventId ? { seriesEventId: event.recurringEventId } : void 0,
1644
+ ...event.attendees ? { attendees: attendeeResults(event.attendees) } : void 0,
1268
1645
  ...event.htmlLink ? { url: event.htmlLink } : void 0,
1269
1646
  ...event.hangoutLink ? { videoCallUrl: event.hangoutLink } : void 0
1270
1647
  })),
@@ -1277,39 +1654,42 @@ function createListCalendarEventsTool(ctx) {
1277
1654
 
1278
1655
  // src/tools/update-event.ts
1279
1656
  import {
1280
- definePluginTool as definePluginTool4,
1657
+ definePluginTool as definePluginTool5,
1281
1658
  PluginToolInputError as PluginToolInputError5,
1282
- pluginToolOutputSchema as pluginToolOutputSchema4
1659
+ pluginToolOutputSchema as pluginToolOutputSchema5
1283
1660
  } 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(
1661
+ import { z as z7 } from "zod";
1662
+ var inputSchema5 = z7.object({
1663
+ eventId: ownEventIdSchema,
1664
+ title: z7.string().trim().min(1).max(200).optional(),
1665
+ description: z7.string().max(4e3).optional().describe(
1291
1666
  "Replacement description. Do not include private details the attendees should not see."
1292
1667
  ),
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."),
1668
+ start: z7.iso.datetime({ offset: true }).optional().describe("New start, RFC 3339 with offset. Give end too."),
1669
+ end: z7.iso.datetime({ offset: true }).optional().describe("New end, RFC 3339 with offset. Give start too."),
1295
1670
  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.")
1671
+ location: z7.string().trim().max(500).optional().describe(
1672
+ "Replacement location, such as a room name or address. Use an empty string to clear it."
1673
+ ),
1674
+ addAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as required attendees."),
1675
+ addOptionalAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as optional attendees."),
1676
+ removeAttendees: emailListSchema(50).default([]).describe("Email addresses to remove from the event."),
1677
+ addVideoCall: z7.literal(true).optional().describe("Add a Google Meet link when the event has none.")
1298
1678
  }).strict().refine(
1299
1679
  (input) => input.start === void 0 === (input.end === void 0),
1300
1680
  {
1301
1681
  message: "Give both start and end to move an event."
1302
1682
  }
1303
1683
  ).refine(
1304
- (input) => input.title !== void 0 || input.description !== void 0 || input.start !== void 0 || input.addAttendees.length > 0 || input.removeAttendees.length > 0,
1684
+ (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
1685
  { message: "Give at least one change." }
1306
1686
  );
1307
- var outputSchema4 = pluginToolOutputSchema4.extend({
1308
- target: z6.literal("updateCalendarEvent"),
1687
+ var outputSchema5 = pluginToolOutputSchema5.extend({
1688
+ target: z7.literal("updateCalendarEvent"),
1309
1689
  ...ownEventOutputFields
1310
1690
  });
1311
1691
  function createUpdateCalendarEventTool(ctx) {
1312
- return definePluginTool4({
1692
+ return definePluginTool5({
1313
1693
  annotations: {
1314
1694
  destructiveHint: false,
1315
1695
  idempotentHint: true,
@@ -1321,15 +1701,19 @@ function createUpdateCalendarEventTool(ctx) {
1321
1701
  input.title && `rename it to "${input.title}"`,
1322
1702
  input.start && `move it to ${input.start} \u2013 ${input.end}`,
1323
1703
  input.description !== void 0 && "replace the description",
1704
+ input.location !== void 0 && (input.location ? `set the location to ${input.location}` : "clear the location"),
1705
+ input.addVideoCall && "add a Google Meet link",
1324
1706
  input.addAttendees.length > 0 && `invite ${input.addAttendees.join(", ")}`,
1707
+ input.addOptionalAttendees.length > 0 && `invite ${input.addOptionalAttendees.join(", ")} as optional`,
1325
1708
  input.removeAttendees.length > 0 && `remove ${input.removeAttendees.join(", ")}`
1326
1709
  ].filter(Boolean);
1327
1710
  return `Update Google Calendar event ${input.eventId}: ${changes.join("; ")}. Attendees get an email about the change.`;
1328
1711
  },
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,
1712
+ 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.",
1713
+ inputSchema: inputSchema5,
1714
+ outputSchema: outputSchema5,
1332
1715
  async execute(input) {
1716
+ const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
1333
1717
  if (input.start && input.end) {
1334
1718
  const durationMs = Date.parse(input.end) - Date.parse(input.start);
1335
1719
  if (durationMs <= 0) {
@@ -1339,39 +1723,19 @@ function createUpdateCalendarEventTool(ctx) {
1339
1723
  throw new PluginToolInputError5("Events can be at most 8 hours long.");
1340
1724
  }
1341
1725
  }
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) {
1726
+ requireAllowedEmails(
1727
+ [...input.addAttendees, ...input.addOptionalAttendees],
1728
+ ctx.allowedDomains
1729
+ );
1730
+ const read = await readOwnEventForRequester(ctx, input.eventId, "change");
1731
+ if (read.status === "cancelled") {
1364
1732
  throw new PluginToolInputError5(
1365
- "Junior does not organize this event, so it cannot change it."
1733
+ "This event is cancelled. Create a new event instead."
1366
1734
  );
1367
1735
  }
1736
+ const { event, path } = read;
1368
1737
  const attendees = event.attendees ?? [];
1369
1738
  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
1739
  const removed = new Set(input.removeAttendees);
1376
1740
  const kept = attendees.filter(
1377
1741
  (attendee) => !removed.has(emailKey(attendee.email))
@@ -1379,8 +1743,19 @@ function createUpdateCalendarEventTool(ctx) {
1379
1743
  const existing = new Set(
1380
1744
  kept.map((attendee) => emailKey(attendee.email))
1381
1745
  );
1382
- const added = input.addAttendees.filter((email) => !existing.has(email)).map((email) => ({ email }));
1746
+ const added = [
1747
+ ...input.addAttendees.map((email) => ({ email })),
1748
+ ...input.addOptionalAttendees.map((email) => ({
1749
+ email,
1750
+ optional: true
1751
+ }))
1752
+ ].filter((attendee) => {
1753
+ if (existing.has(attendee.email)) return false;
1754
+ existing.add(attendee.email);
1755
+ return true;
1756
+ });
1383
1757
  const attendeesChanged = kept.length !== attendees.length || added.length;
1758
+ const addVideoCall = input.addVideoCall && !event.hangoutLink;
1384
1759
  const response = await googleApiRequest(ctx, {
1385
1760
  body: {
1386
1761
  ...input.title !== void 0 && { summary: input.title },
@@ -1388,21 +1763,33 @@ function createUpdateCalendarEventTool(ctx) {
1388
1763
  description: input.description
1389
1764
  },
1390
1765
  ...input.start && input.end && {
1391
- end: { dateTime: input.end, timeZone: input.timeZone },
1392
- start: { dateTime: input.start, timeZone: input.timeZone }
1766
+ end: { dateTime: input.end, timeZone },
1767
+ start: { dateTime: input.start, timeZone }
1393
1768
  },
1394
- ...attendeesChanged && { attendees: [...kept, ...added] }
1769
+ ...input.location !== void 0 && { location: input.location },
1770
+ ...attendeesChanged && { attendees: [...kept, ...added] },
1771
+ ...addVideoCall && {
1772
+ conferenceData: {
1773
+ createRequest: {
1774
+ conferenceSolutionKey: { type: "hangoutsMeet" },
1775
+ requestId: `${event.id}-meet`
1776
+ }
1777
+ }
1778
+ }
1395
1779
  },
1396
1780
  operation: "google.calendar.event.update",
1397
1781
  path,
1398
- query: { sendUpdates: "all" }
1782
+ query: {
1783
+ ...addVideoCall && { conferenceDataVersion: "1" },
1784
+ sendUpdates: "all"
1785
+ }
1399
1786
  });
1400
1787
  if (response.status !== 200) {
1401
1788
  throw googleApiError("google.calendar.event.update", response);
1402
1789
  }
1403
1790
  return {
1404
1791
  target: "updateCalendarEvent",
1405
- ...ownEventResult(ownEventSchema.parse(response.body), input.timeZone)
1792
+ ...ownEventResult(ownEventSchema.parse(response.body), timeZone)
1406
1793
  };
1407
1794
  }
1408
1795
  });
@@ -1441,11 +1828,13 @@ function googlePlugin() {
1441
1828
  return {};
1442
1829
  }
1443
1830
  const toolContext = {
1831
+ accountEmail: config.accountEmail,
1444
1832
  allowedDomains: config.allowedDomains,
1445
1833
  egress: ctx.egress,
1446
1834
  users: ctx.users
1447
1835
  };
1448
1836
  return {
1837
+ cancelCalendarEvent: createCancelCalendarEventTool(toolContext),
1449
1838
  createCalendarEvent: createCreateCalendarEventTool(toolContext),
1450
1839
  findMeetingTimes: createFindMeetingTimesTool(toolContext),
1451
1840
  listCalendarEvents: createListCalendarEventsTool(toolContext),