@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/README.md +41 -13
- package/dist/credentials.d.ts +5 -0
- package/dist/index.js +625 -236
- package/dist/tools/cancel-event.d.ts +158 -0
- package/dist/tools/create-event.d.ts +48 -6
- package/dist/tools/find-meeting-times.d.ts +53 -16
- package/dist/tools/list-events.d.ts +26 -6
- package/dist/tools/shared.d.ts +144 -5
- package/dist/tools/update-event.d.ts +31 -7
- 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";
|
|
@@ -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
|
|
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."
|
|
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
|
-
|
|
867
|
-
|
|
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
|
|
1078
|
+
"Email addresses of required attendees. Junior adds the requester automatically when it knows their email."
|
|
871
1079
|
),
|
|
872
|
-
|
|
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
|
|
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,12 +1097,15 @@ 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. 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
|
-
|
|
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(
|
|
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:
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
}
|
|
1141
|
+
},
|
|
922
1142
|
...input.description ? { description: input.description } : void 0,
|
|
923
|
-
end: { dateTime: input.end, timeZone
|
|
1143
|
+
end: { dateTime: input.end, timeZone },
|
|
924
1144
|
guestsCanModify: false,
|
|
925
1145
|
id: eventId,
|
|
926
|
-
|
|
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:
|
|
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),
|
|
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
|
|
1183
|
+
definePluginTool as definePluginTool3,
|
|
962
1184
|
PluginToolInputError as PluginToolInputError3,
|
|
963
|
-
pluginToolOutputSchema as
|
|
1185
|
+
pluginToolOutputSchema as pluginToolOutputSchema3
|
|
964
1186
|
} from "@sentry/junior-plugin-api";
|
|
965
|
-
import { z as
|
|
966
|
-
var
|
|
967
|
-
var
|
|
968
|
-
var
|
|
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
|
-
"
|
|
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
|
-
|
|
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."
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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 =
|
|
983
|
-
end:
|
|
984
|
-
label:
|
|
985
|
-
|
|
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
|
|
988
|
-
target:
|
|
989
|
-
checked:
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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 =
|
|
995
|
-
calendars:
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
busy:
|
|
999
|
-
errors:
|
|
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
|
|
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", {
|
|
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
|
|
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
|
|
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
|
|
1341
|
+
while (startMs + durationMs <= input.timeMaxMs) {
|
|
1035
1342
|
const endMs = startMs + durationMs;
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1060
|
-
inputSchema:
|
|
1061
|
-
outputSchema:
|
|
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
|
|
1064
|
-
const
|
|
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
|
-
|
|
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 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
|
-
|
|
1112
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
1502
|
+
definePluginTool as definePluginTool4,
|
|
1139
1503
|
PluginToolInputError as PluginToolInputError4,
|
|
1140
|
-
pluginToolOutputSchema as
|
|
1504
|
+
pluginToolOutputSchema as pluginToolOutputSchema4
|
|
1141
1505
|
} from "@sentry/junior-plugin-api";
|
|
1142
|
-
import { z as
|
|
1506
|
+
import { z as z6 } from "zod";
|
|
1143
1507
|
var MAX_WINDOW_MS2 = 31 * 24 * 60 * 60 * 1e3;
|
|
1144
|
-
var
|
|
1145
|
-
calendar:
|
|
1146
|
-
|
|
1147
|
-
|
|
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:
|
|
1517
|
+
maxResults: z6.number().int().min(1).max(100).default(50)
|
|
1152
1518
|
}).strict();
|
|
1153
|
-
var eventSchema =
|
|
1154
|
-
attendees:
|
|
1155
|
-
end:
|
|
1156
|
-
eventId:
|
|
1157
|
-
label:
|
|
1158
|
-
location:
|
|
1159
|
-
organizer:
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
|
1166
|
-
target:
|
|
1167
|
-
calendar:
|
|
1168
|
-
events:
|
|
1169
|
-
timeZone:
|
|
1170
|
-
truncated:
|
|
1171
|
-
visible:
|
|
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 =
|
|
1174
|
-
date:
|
|
1175
|
-
dateTime:
|
|
1545
|
+
var eventTimeSchema = z6.object({
|
|
1546
|
+
date: z6.string().optional(),
|
|
1547
|
+
dateTime: z6.string().optional()
|
|
1176
1548
|
});
|
|
1177
|
-
var listResponseSchema =
|
|
1178
|
-
items:
|
|
1179
|
-
|
|
1180
|
-
attendees:
|
|
1549
|
+
var listResponseSchema = z6.object({
|
|
1550
|
+
items: z6.array(
|
|
1551
|
+
z6.object({
|
|
1552
|
+
attendees: z6.array(googleAttendeeSchema).optional(),
|
|
1181
1553
|
end: eventTimeSchema,
|
|
1182
|
-
hangoutLink:
|
|
1183
|
-
htmlLink:
|
|
1184
|
-
id:
|
|
1185
|
-
location:
|
|
1186
|
-
organizer:
|
|
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:
|
|
1189
|
-
summary:
|
|
1561
|
+
status: z6.string().optional(),
|
|
1562
|
+
summary: z6.string().optional()
|
|
1190
1563
|
})
|
|
1191
1564
|
).default([]),
|
|
1192
|
-
nextPageToken:
|
|
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
|
|
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
|
|
1221
|
-
inputSchema:
|
|
1222
|
-
outputSchema:
|
|
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
|
-
|
|
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(
|
|
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
|
|
1617
|
+
timeZone
|
|
1243
1618
|
}
|
|
1244
1619
|
});
|
|
1245
1620
|
const result = {
|
|
1246
1621
|
target: "listCalendarEvents",
|
|
1247
|
-
calendar
|
|
1248
|
-
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,
|
|
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
|
-
|
|
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
|
|
1657
|
+
definePluginTool as definePluginTool5,
|
|
1281
1658
|
PluginToolInputError as PluginToolInputError5,
|
|
1282
|
-
pluginToolOutputSchema as
|
|
1659
|
+
pluginToolOutputSchema as pluginToolOutputSchema5
|
|
1283
1660
|
} 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(
|
|
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:
|
|
1294
|
-
end:
|
|
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
|
-
|
|
1297
|
-
|
|
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
|
|
1308
|
-
target:
|
|
1687
|
+
var outputSchema5 = pluginToolOutputSchema5.extend({
|
|
1688
|
+
target: z7.literal("updateCalendarEvent"),
|
|
1309
1689
|
...ownEventOutputFields
|
|
1310
1690
|
});
|
|
1311
1691
|
function createUpdateCalendarEventTool(ctx) {
|
|
1312
|
-
return
|
|
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.
|
|
1330
|
-
inputSchema:
|
|
1331
|
-
outputSchema:
|
|
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(
|
|
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) {
|
|
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
|
-
"
|
|
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 =
|
|
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
|
|
1392
|
-
start: { dateTime: input.start, timeZone
|
|
1766
|
+
end: { dateTime: input.end, timeZone },
|
|
1767
|
+
start: { dateTime: input.start, timeZone }
|
|
1393
1768
|
},
|
|
1394
|
-
...
|
|
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: {
|
|
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),
|
|
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),
|