@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/README.md +38 -13
- package/dist/credentials.d.ts +5 -0
- package/dist/index.js +598 -214
- package/dist/tools/cancel-event.d.ts +158 -0
- package/dist/tools/create-event.d.ts +47 -4
- package/dist/tools/find-meeting-times.d.ts +52 -15
- package/dist/tools/list-events.d.ts +25 -5
- package/dist/tools/shared.d.ts +142 -4
- package/dist/tools/update-event.d.ts +30 -6
- package/package.json +2 -2
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
892
|
+
objectAnnotations: [annotation],
|
|
893
|
+
attendees: attendeeResults(event.attendees),
|
|
836
894
|
end: event.end.dateTime,
|
|
837
895
|
eventId: event.id,
|
|
838
|
-
label:
|
|
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(
|
|
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/
|
|
979
|
+
// src/tools/cancel-event.ts
|
|
861
980
|
var inputSchema = z3.object({
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
|
1077
|
+
"Email addresses of required attendees. Junior adds the requester automatically when it knows their email."
|
|
871
1078
|
),
|
|
872
|
-
|
|
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
|
|
875
|
-
target:
|
|
876
|
-
created:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
1184
|
+
definePluginTool as definePluginTool3,
|
|
962
1185
|
PluginToolInputError as PluginToolInputError3,
|
|
963
|
-
pluginToolOutputSchema as
|
|
1186
|
+
pluginToolOutputSchema as pluginToolOutputSchema3
|
|
964
1187
|
} from "@sentry/junior-plugin-api";
|
|
965
|
-
import { z as
|
|
966
|
-
var
|
|
967
|
-
var
|
|
968
|
-
var
|
|
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
|
-
"
|
|
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
|
-
|
|
973
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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 =
|
|
983
|
-
end:
|
|
984
|
-
label:
|
|
985
|
-
|
|
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
|
|
988
|
-
target:
|
|
989
|
-
checked:
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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 =
|
|
995
|
-
calendars:
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
busy:
|
|
999
|
-
errors:
|
|
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
|
|
1014
|
-
|
|
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
|
|
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
|
|
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
|
|
1342
|
+
while (startMs + durationMs <= input.timeMaxMs) {
|
|
1035
1343
|
const endMs = startMs + durationMs;
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1060
|
-
inputSchema:
|
|
1061
|
-
outputSchema:
|
|
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(
|
|
1064
|
-
|
|
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
|
-
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
|
1096
|
-
|
|
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
|
-
|
|
1449
|
+
return;
|
|
1102
1450
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
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
|
|
1499
|
+
definePluginTool as definePluginTool4,
|
|
1139
1500
|
PluginToolInputError as PluginToolInputError4,
|
|
1140
|
-
pluginToolOutputSchema as
|
|
1501
|
+
pluginToolOutputSchema as pluginToolOutputSchema4
|
|
1141
1502
|
} from "@sentry/junior-plugin-api";
|
|
1142
|
-
import { z as
|
|
1503
|
+
import { z as z6 } from "zod";
|
|
1143
1504
|
var MAX_WINDOW_MS2 = 31 * 24 * 60 * 60 * 1e3;
|
|
1144
|
-
var
|
|
1145
|
-
calendar:
|
|
1146
|
-
|
|
1147
|
-
|
|
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:
|
|
1514
|
+
maxResults: z6.number().int().min(1).max(100).default(50)
|
|
1152
1515
|
}).strict();
|
|
1153
|
-
var eventSchema =
|
|
1154
|
-
attendees:
|
|
1155
|
-
end:
|
|
1156
|
-
eventId:
|
|
1157
|
-
label:
|
|
1158
|
-
location:
|
|
1159
|
-
organizer:
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1166
|
-
target:
|
|
1167
|
-
calendar:
|
|
1168
|
-
events:
|
|
1169
|
-
timeZone:
|
|
1170
|
-
truncated:
|
|
1171
|
-
visible:
|
|
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 =
|
|
1174
|
-
date:
|
|
1175
|
-
dateTime:
|
|
1542
|
+
var eventTimeSchema = z6.object({
|
|
1543
|
+
date: z6.string().optional(),
|
|
1544
|
+
dateTime: z6.string().optional()
|
|
1176
1545
|
});
|
|
1177
|
-
var listResponseSchema =
|
|
1178
|
-
items:
|
|
1179
|
-
|
|
1180
|
-
attendees:
|
|
1546
|
+
var listResponseSchema = z6.object({
|
|
1547
|
+
items: z6.array(
|
|
1548
|
+
z6.object({
|
|
1549
|
+
attendees: z6.array(googleAttendeeSchema).optional(),
|
|
1181
1550
|
end: eventTimeSchema,
|
|
1182
|
-
hangoutLink:
|
|
1183
|
-
htmlLink:
|
|
1184
|
-
id:
|
|
1185
|
-
location:
|
|
1186
|
-
organizer:
|
|
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:
|
|
1189
|
-
summary:
|
|
1558
|
+
status: z6.string().optional(),
|
|
1559
|
+
summary: z6.string().optional()
|
|
1190
1560
|
})
|
|
1191
1561
|
).default([]),
|
|
1192
|
-
nextPageToken:
|
|
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
|
|
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
|
|
1221
|
-
inputSchema:
|
|
1222
|
-
outputSchema:
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
1653
|
+
definePluginTool as definePluginTool5,
|
|
1281
1654
|
PluginToolInputError as PluginToolInputError5,
|
|
1282
|
-
pluginToolOutputSchema as
|
|
1655
|
+
pluginToolOutputSchema as pluginToolOutputSchema5
|
|
1283
1656
|
} from "@sentry/junior-plugin-api";
|
|
1284
|
-
import { z as
|
|
1285
|
-
var
|
|
1286
|
-
eventId:
|
|
1287
|
-
|
|
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:
|
|
1294
|
-
end:
|
|
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
|
-
|
|
1297
|
-
|
|
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
|
|
1308
|
-
target:
|
|
1683
|
+
var outputSchema5 = pluginToolOutputSchema5.extend({
|
|
1684
|
+
target: z7.literal("updateCalendarEvent"),
|
|
1309
1685
|
...ownEventOutputFields
|
|
1310
1686
|
});
|
|
1311
1687
|
function createUpdateCalendarEventTool(ctx) {
|
|
1312
|
-
return
|
|
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.
|
|
1330
|
-
inputSchema:
|
|
1331
|
-
outputSchema:
|
|
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(
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
"
|
|
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 =
|
|
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
|
-
...
|
|
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: {
|
|
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),
|