@agent-native/core 0.176.4 → 0.176.5-nightly-20260902205402
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/corpus/README.md +1 -1
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
- package/corpus/templates/calendar/AGENTS.md +4 -0
- package/corpus/templates/calendar/actions/delete-event.ts +2 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +7 -0
- package/corpus/templates/calendar/actions/get-event.ts +40 -3
- package/corpus/templates/calendar/actions/list-events.ts +49 -2
- package/corpus/templates/calendar/actions/list-google-calendars.ts +24 -0
- package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +52 -1
- package/corpus/templates/calendar/actions/update-event.ts +2 -2
- package/corpus/templates/calendar/actions/view-screen.ts +54 -2
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +33 -19
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +31 -5
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +102 -0
- package/corpus/templates/calendar/app/hooks/use-events.ts +45 -10
- package/corpus/templates/calendar/app/hooks/use-google-auth.ts +8 -1
- package/corpus/templates/calendar/app/hooks/use-google-calendars.ts +41 -0
- package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +56 -4
- package/corpus/templates/calendar/app/lib/shared-calendar-demo.ts +187 -0
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +50 -5
- package/corpus/templates/calendar/changelog/2026-09-01-calendars-shared-with-your-connected-google-accounts-can-now.md +6 -0
- package/corpus/templates/calendar/changelog/2026-09-02-updated-the-booking-link-og-preview-image-with-the-new-monoc.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-og-image.ts +8 -18
- package/corpus/templates/calendar/server/lib/google-api.ts +10 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +215 -21
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/calendar/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/calendar/shared/api.ts +20 -0
- package/corpus/templates/calendar/shared/calendar-view-preferences.ts +28 -1
- package/corpus/templates/calendar/shared/feature-flags.ts +8 -0
- package/corpus/templates/calendar/shared/google-calendar-sources.ts +46 -0
- package/corpus/templates/factory/.env.example +5 -0
- package/corpus/templates/factory/README.md +9 -0
- package/corpus/templates/factory/server/connectors/credentials.ts +28 -6
- package/corpus/templates/factory/server/triage/github-client.ts +144 -7
- package/corpus/templates/forms/server/lib/form-og-image.ts +7 -17
- package/corpus/templates/slides/actions/add-slide.ts +1 -0
- package/corpus/templates/slides/actions/create-deck.ts +1 -0
- package/corpus/templates/slides/actions/get-deck.ts +1 -0
- package/corpus/templates/slides/actions/navigate.ts +1 -0
- package/corpus/templates/slides/actions/patch-deck.ts +1 -0
- package/corpus/templates/slides/actions/update-slide.ts +1 -0
- package/corpus/templates/slides/actions/view-screen.ts +1 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
- package/dist/action.d.ts +4 -0
- package/dist/action.js +3 -0
- package/dist/agent/types.d.ts +1 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/webmcp.js +7 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.js +4 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +1 -0
- package/dist/server/action-routes.js +4 -0
- package/dist/server/agent-chat/mcp-options.d.ts +4 -0
- package/dist/server/agent-chat/mcp-options.js +1 -0
- package/dist/server/agent-chat-plugin.js +2 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/social-og-image.js +6 -20
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/agent-mcp-metadata.d.ts +3 -0
- package/dist/shared/agent-mcp-metadata.js +18 -0
- package/dist/shared/social-meta.d.ts +1 -1
- package/dist/shared/social-meta.js +1 -1
- package/package.json +1 -1
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
shouldShowEventsSkeleton,
|
|
77
77
|
} from "@/hooks/use-events";
|
|
78
78
|
import { useGoogleAuthStatus } from "@/hooks/use-google-auth";
|
|
79
|
+
import { useGoogleCalendars } from "@/hooks/use-google-calendars";
|
|
79
80
|
import { useMeetingStartNotifications } from "@/hooks/use-meeting-start-notifications";
|
|
80
81
|
import { useIsMobile } from "@/hooks/use-mobile";
|
|
81
82
|
import { useOverlayPeople } from "@/hooks/use-overlay-people";
|
|
@@ -439,6 +440,7 @@ export default function CalendarView() {
|
|
|
439
440
|
|
|
440
441
|
const queryClient = useQueryClient();
|
|
441
442
|
const googleStatus = useGoogleAuthStatus();
|
|
443
|
+
const googleCalendars = useGoogleCalendars();
|
|
442
444
|
const defaultAccountEmail = googleStatus.data?.accounts?.[0]?.email;
|
|
443
445
|
const settingsQuery = useSettings();
|
|
444
446
|
const { data: settings } = settingsQuery;
|
|
@@ -455,6 +457,29 @@ export default function CalendarView() {
|
|
|
455
457
|
() => overlayPeople.map((p) => p.email),
|
|
456
458
|
[overlayPeople],
|
|
457
459
|
);
|
|
460
|
+
const enabledGoogleCalendarSourceKeys = useMemo(() => {
|
|
461
|
+
if (!googleCalendars.enabled || !googleCalendars.data) return undefined;
|
|
462
|
+
return googleCalendars.data
|
|
463
|
+
.filter((source) => {
|
|
464
|
+
if (
|
|
465
|
+
hiddenCalendars.accounts.includes(source.accountEmail) ||
|
|
466
|
+
source.accessRole === "freeBusyReader"
|
|
467
|
+
) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
if (source.primary) return true;
|
|
471
|
+
return (
|
|
472
|
+
viewPrefs.googleCalendarVisibility[source.sourceKey] ??
|
|
473
|
+
source.selected
|
|
474
|
+
);
|
|
475
|
+
})
|
|
476
|
+
.map((source) => source.sourceKey);
|
|
477
|
+
}, [
|
|
478
|
+
googleCalendars.data,
|
|
479
|
+
googleCalendars.enabled,
|
|
480
|
+
hiddenCalendars.accounts,
|
|
481
|
+
viewPrefs.googleCalendarVisibility,
|
|
482
|
+
]);
|
|
458
483
|
const createEvent = useCreateEvent();
|
|
459
484
|
const updateEvent = useUpdateEvent();
|
|
460
485
|
const deleteEvent = useDeleteEvent();
|
|
@@ -551,7 +576,7 @@ export default function CalendarView() {
|
|
|
551
576
|
isLoading,
|
|
552
577
|
isFetching,
|
|
553
578
|
isPlaceholderData,
|
|
554
|
-
} = useEvents(from, to, overlayEmails);
|
|
579
|
+
} = useEvents(from, to, overlayEmails, enabledGoogleCalendarSourceKeys);
|
|
555
580
|
const rawEvents = Array.isArray(rawEventsData) ? rawEventsData : [];
|
|
556
581
|
const draftEvent = useMemo(
|
|
557
582
|
() => (eventDraft ? draftToCalendarEvent(eventDraft, selectedDate) : null),
|
|
@@ -619,10 +644,17 @@ export default function CalendarView() {
|
|
|
619
644
|
}
|
|
620
645
|
})();
|
|
621
646
|
for (const range of ranges) {
|
|
622
|
-
void prefetchEvents(
|
|
647
|
+
void prefetchEvents(
|
|
648
|
+
queryClient,
|
|
649
|
+
range.from,
|
|
650
|
+
range.to,
|
|
651
|
+
overlayEmails,
|
|
652
|
+
enabledGoogleCalendarSourceKeys,
|
|
653
|
+
);
|
|
623
654
|
}
|
|
624
655
|
}, [
|
|
625
656
|
displayTimezone,
|
|
657
|
+
enabledGoogleCalendarSourceKeys,
|
|
626
658
|
isLoading,
|
|
627
659
|
overlayEmails,
|
|
628
660
|
queryClient,
|
|
@@ -1085,6 +1117,7 @@ export default function CalendarView() {
|
|
|
1085
1117
|
notificationMessage?: string;
|
|
1086
1118
|
},
|
|
1087
1119
|
) => {
|
|
1120
|
+
if (ev.calendarPrimary === false || ev.calendarReadOnly) return;
|
|
1088
1121
|
const isOrganizer = isCalendarEventOrganizer(ev);
|
|
1089
1122
|
const hasOtherAttendees =
|
|
1090
1123
|
ev.attendees && ev.attendees.filter((a) => !a.self).length > 0;
|
|
@@ -1167,7 +1200,7 @@ export default function CalendarView() {
|
|
|
1167
1200
|
return;
|
|
1168
1201
|
}
|
|
1169
1202
|
const ev = events.find((e) => e.id === eventId);
|
|
1170
|
-
if (!ev) return;
|
|
1203
|
+
if (!ev || ev.calendarPrimary === false || ev.calendarReadOnly) return;
|
|
1171
1204
|
const isRecurring = !!(ev.recurringEventId || ev.recurrence?.length);
|
|
1172
1205
|
const isOrganizer = isCalendarEventOrganizer(ev);
|
|
1173
1206
|
const hasOtherAttendees =
|
|
@@ -1185,7 +1218,13 @@ export default function CalendarView() {
|
|
|
1185
1218
|
// Move event to a new date (drag-and-drop from MonthView)
|
|
1186
1219
|
async function handleEventDrop(eventId: string, newDate: Date) {
|
|
1187
1220
|
const event = events.find((e) => e.id === eventId);
|
|
1188
|
-
if (
|
|
1221
|
+
if (
|
|
1222
|
+
!event ||
|
|
1223
|
+
event.calendarPrimary === false ||
|
|
1224
|
+
event.calendarReadOnly ||
|
|
1225
|
+
!isCalendarEventOrganizer(event) ||
|
|
1226
|
+
updateEvent.isPending
|
|
1227
|
+
)
|
|
1189
1228
|
return;
|
|
1190
1229
|
|
|
1191
1230
|
const moved = moveEventToCalendarDate(event, newDate, displayTimezone);
|
|
@@ -1258,7 +1297,13 @@ export default function CalendarView() {
|
|
|
1258
1297
|
async (eventId: string, newStart: Date, newEnd: Date) => {
|
|
1259
1298
|
// Skip no-op drags (dropped back in same spot)
|
|
1260
1299
|
const event = events.find((e) => e.id === eventId);
|
|
1261
|
-
if (
|
|
1300
|
+
if (
|
|
1301
|
+
!event ||
|
|
1302
|
+
event.calendarPrimary === false ||
|
|
1303
|
+
event.calendarReadOnly ||
|
|
1304
|
+
!isCalendarEventOrganizer(event) ||
|
|
1305
|
+
updateEvent.isPending
|
|
1306
|
+
)
|
|
1262
1307
|
return;
|
|
1263
1308
|
|
|
1264
1309
|
// Guard against a zero/negative duration reaching the server —
|
|
@@ -16,13 +16,11 @@ export interface BookingOgImageInput {
|
|
|
16
16
|
|
|
17
17
|
const WIDTH = 1200;
|
|
18
18
|
const HEIGHT = 630;
|
|
19
|
-
const
|
|
20
|
-
const BRAND_MINT = "#48FFE4";
|
|
21
|
-
const BG = "#000000";
|
|
19
|
+
const BG = "#0A0A0A";
|
|
22
20
|
const SURFACE = "#0a0a0a";
|
|
23
21
|
const BORDER = "#1f1f1f";
|
|
24
|
-
const FG = "#
|
|
25
|
-
const MUTED = "#
|
|
22
|
+
const FG = "#FAF9F5";
|
|
23
|
+
const MUTED = "#9A9997";
|
|
26
24
|
const FONT_FAMILY = "Liberation Sans, Arial, system-ui, sans-serif";
|
|
27
25
|
const FONT_SOURCE_PATHS = [
|
|
28
26
|
"../../assets/fonts/LiberationSans-Regular.ttf",
|
|
@@ -33,8 +31,8 @@ const AVATAR_CY = 170;
|
|
|
33
31
|
const AVATAR_SIZE = 172;
|
|
34
32
|
|
|
35
33
|
const LOGO_MARK = `
|
|
36
|
-
<path d="
|
|
37
|
-
<path d="
|
|
34
|
+
<path d="M26.8789 71.999H0L16.5146 43.1992L41.2793 0L66.2197 43.1992H43.3945L26.8789 71.999Z" fill="white"/>
|
|
35
|
+
<path d="M97.914 0H124.794L83.5143 72H56.6348L97.914 0Z" fill="white"/>
|
|
38
36
|
`;
|
|
39
37
|
|
|
40
38
|
function escapeSvg(value: string): string {
|
|
@@ -239,34 +237,26 @@ export function renderBookingOgImageSvg(input: BookingOgImageInput): string {
|
|
|
239
237
|
titleLines.length === 1
|
|
240
238
|
? `<g transform="translate(0 150)">
|
|
241
239
|
<rect x="0" y="-34" width="${Math.max(246, duration.length * 17 + 54)}" height="58" rx="29" fill="${SURFACE}" stroke="${BORDER}"/>
|
|
242
|
-
<circle cx="31" cy="-5" r="8" fill="${
|
|
240
|
+
<circle cx="31" cy="-5" r="8" fill="${FG}"/>
|
|
243
241
|
<text x="54" y="4" font-family="${FONT_FAMILY}" font-size="27" font-weight="700" fill="${FG}">${escapeSvg(duration)}</text>
|
|
244
242
|
</g>`
|
|
245
243
|
: "";
|
|
246
244
|
const avatarContent = profileImageDataUrl
|
|
247
245
|
? `<image x="${AVATAR_CX - AVATAR_SIZE / 2}" y="${AVATAR_CY - AVATAR_SIZE / 2}" width="${AVATAR_SIZE}" height="${AVATAR_SIZE}" href="${escapeSvg(profileImageDataUrl)}" preserveAspectRatio="xMidYMid slice" mask="url(#avatarMask)"/>`
|
|
248
|
-
: `<circle cx="${AVATAR_CX}" cy="${AVATAR_CY}" r="72" fill="
|
|
246
|
+
: `<circle cx="${AVATAR_CX}" cy="${AVATAR_CY}" r="72" fill="${FG}" fill-opacity="0.12"/>
|
|
249
247
|
<text x="${AVATAR_CX}" y="${AVATAR_CY + 20}" text-anchor="middle" font-family="${FONT_FAMILY}" font-size="56" font-weight="800" fill="${FG}">${escapeSvg(initials)}</text>`;
|
|
250
248
|
|
|
251
249
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}">
|
|
252
250
|
<title>Agent-Native Calendar booking link</title>
|
|
253
251
|
<defs>
|
|
254
|
-
<linearGradient id="brand" x1="101.702" y1="67.4791" x2="113.672" y2="-37.4275" gradientUnits="userSpaceOnUse">
|
|
255
|
-
<stop stop-color="${BRAND_BLUE}"/>
|
|
256
|
-
<stop offset="1" stop-color="${BRAND_MINT}"/>
|
|
257
|
-
</linearGradient>
|
|
258
|
-
<pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">
|
|
259
|
-
<path d="M 48 0 L 0 0 0 48" fill="none" stroke="#ffffff" stroke-opacity="0.07" stroke-width="1"/>
|
|
260
|
-
</pattern>
|
|
261
252
|
<mask id="avatarMask">
|
|
262
253
|
<rect width="${WIDTH}" height="${HEIGHT}" fill="black"/>
|
|
263
254
|
<circle cx="${AVATAR_CX}" cy="${AVATAR_CY}" r="78" fill="white"/>
|
|
264
255
|
</mask>
|
|
265
256
|
</defs>
|
|
266
257
|
<rect width="${WIDTH}" height="${HEIGHT}" fill="${BG}"/>
|
|
267
|
-
<rect width="${WIDTH}" height="${HEIGHT}" fill="url(#grid)"/>
|
|
268
258
|
<g transform="translate(80 86)">
|
|
269
|
-
<g transform="scale(0.62)">
|
|
259
|
+
<g transform="translate(0 14) scale(0.62)">
|
|
270
260
|
${LOGO_MARK}
|
|
271
261
|
</g>
|
|
272
262
|
<text x="90" y="31" font-family="${FONT_FAMILY}" font-size="28" font-weight="800" fill="${FG}">Agent-Native</text>
|
|
@@ -385,6 +385,16 @@ export function calendarListEvents(
|
|
|
385
385
|
);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
export function calendarListCalendars(
|
|
389
|
+
accessToken: string,
|
|
390
|
+
params: { maxResults?: number; pageToken?: string } = {},
|
|
391
|
+
) {
|
|
392
|
+
return googleFetch(
|
|
393
|
+
`${CALENDAR_BASE}/users/me/calendarList${qs(params)}`,
|
|
394
|
+
accessToken,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
388
398
|
export function calendarGetCalendar(accessToken: string, calendarId: string) {
|
|
389
399
|
return googleFetch(
|
|
390
400
|
`${CALENDAR_BASE}/calendars/${encodeURIComponent(calendarId)}`,
|
|
@@ -17,15 +17,21 @@ import { resolveWorkspaceConnectionForApp } from "@agent-native/core/workspace-c
|
|
|
17
17
|
|
|
18
18
|
import type {
|
|
19
19
|
CalendarEvent,
|
|
20
|
+
GoogleCalendarSource,
|
|
20
21
|
GoogleAuthStatus,
|
|
21
22
|
UpdateEventScope,
|
|
22
23
|
} from "../../shared/api.js";
|
|
24
|
+
import {
|
|
25
|
+
createGoogleCalendarSourceKey,
|
|
26
|
+
parseGoogleCalendarSourceKey,
|
|
27
|
+
} from "../../shared/google-calendar-sources.js";
|
|
23
28
|
import { getGoogleEventColorHex } from "../../shared/google-event-colors.js";
|
|
24
29
|
import { isCalendarTimezone } from "../../shared/timezone.js";
|
|
25
30
|
import {
|
|
26
31
|
createOAuth2Client,
|
|
27
32
|
oauth2GetUserInfo,
|
|
28
33
|
calendarListEvents,
|
|
34
|
+
calendarListCalendars,
|
|
29
35
|
calendarFreeBusy,
|
|
30
36
|
calendarGetCalendar,
|
|
31
37
|
calendarGetEvent,
|
|
@@ -1050,6 +1056,77 @@ export async function getClientsForAccountsWithErrors(
|
|
|
1050
1056
|
};
|
|
1051
1057
|
}
|
|
1052
1058
|
|
|
1059
|
+
function asCalendarAccessRole(
|
|
1060
|
+
value: unknown,
|
|
1061
|
+
): GoogleCalendarSource["accessRole"] {
|
|
1062
|
+
return value === "freeBusyReader" ||
|
|
1063
|
+
value === "reader" ||
|
|
1064
|
+
value === "writer" ||
|
|
1065
|
+
value === "owner"
|
|
1066
|
+
? value
|
|
1067
|
+
: "reader";
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** Discover every CalendarList entry visible to each connected Google account. */
|
|
1071
|
+
export async function listGoogleCalendars(forEmail?: string): Promise<{
|
|
1072
|
+
calendars: GoogleCalendarSource[];
|
|
1073
|
+
errors: Array<{ email: string; error: string }>;
|
|
1074
|
+
}> {
|
|
1075
|
+
const { clients, errors: refreshErrors } =
|
|
1076
|
+
await getClientsForAccountsWithErrors(forEmail);
|
|
1077
|
+
const errors = [...refreshErrors];
|
|
1078
|
+
const results = await mapWithConcurrency(clients, async (client) => {
|
|
1079
|
+
try {
|
|
1080
|
+
const items: any[] = [];
|
|
1081
|
+
let pageToken: string | undefined;
|
|
1082
|
+
do {
|
|
1083
|
+
const response = await calendarListCalendars(client.accessToken, {
|
|
1084
|
+
maxResults: 250,
|
|
1085
|
+
pageToken,
|
|
1086
|
+
});
|
|
1087
|
+
items.push(...(response.items ?? []));
|
|
1088
|
+
pageToken =
|
|
1089
|
+
typeof response.nextPageToken === "string"
|
|
1090
|
+
? response.nextPageToken
|
|
1091
|
+
: undefined;
|
|
1092
|
+
} while (pageToken);
|
|
1093
|
+
return items
|
|
1094
|
+
.filter((item) => typeof item.id === "string" && item.id.length > 0)
|
|
1095
|
+
.map((item): GoogleCalendarSource => {
|
|
1096
|
+
const accessRole = asCalendarAccessRole(item.accessRole);
|
|
1097
|
+
return {
|
|
1098
|
+
sourceKey: createGoogleCalendarSourceKey({
|
|
1099
|
+
accountEmail: client.email,
|
|
1100
|
+
calendarId: item.id,
|
|
1101
|
+
}),
|
|
1102
|
+
accountEmail: client.email,
|
|
1103
|
+
calendarId: item.id,
|
|
1104
|
+
name: item.summaryOverride || item.summary || item.id,
|
|
1105
|
+
color: item.backgroundColor || undefined,
|
|
1106
|
+
selected: item.selected === true,
|
|
1107
|
+
primary: item.primary === true,
|
|
1108
|
+
accessRole,
|
|
1109
|
+
readOnly:
|
|
1110
|
+
item.primary !== true ||
|
|
1111
|
+
(accessRole !== "owner" && accessRole !== "writer"),
|
|
1112
|
+
};
|
|
1113
|
+
});
|
|
1114
|
+
} catch (error: any) {
|
|
1115
|
+
errors.push({
|
|
1116
|
+
email: client.email,
|
|
1117
|
+
error: error?.message || "Unable to list Google calendars",
|
|
1118
|
+
});
|
|
1119
|
+
return [];
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
return {
|
|
1123
|
+
calendars: results
|
|
1124
|
+
.flat()
|
|
1125
|
+
.sort((a, b) => a.sourceKey.localeCompare(b.sourceKey)),
|
|
1126
|
+
errors: errors.sort((a, b) => a.email.localeCompare(b.email)),
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1053
1130
|
export async function isConnected(forEmail?: string): Promise<boolean> {
|
|
1054
1131
|
if (!forEmail) return false;
|
|
1055
1132
|
const accounts = await listOAuthAccountsByOwner("google", forEmail);
|
|
@@ -1172,7 +1249,11 @@ export async function listEvents(
|
|
|
1172
1249
|
timeMin: string,
|
|
1173
1250
|
timeMax: string,
|
|
1174
1251
|
forEmail?: string,
|
|
1175
|
-
options: {
|
|
1252
|
+
options: {
|
|
1253
|
+
accountEmails?: string[];
|
|
1254
|
+
calendarSourceKeys?: string[];
|
|
1255
|
+
maxResults?: number;
|
|
1256
|
+
} = {},
|
|
1176
1257
|
): Promise<{
|
|
1177
1258
|
events: CalendarEvent[];
|
|
1178
1259
|
errors: Array<{ email: string; error: string }>;
|
|
@@ -1186,36 +1267,115 @@ export async function listEvents(
|
|
|
1186
1267
|
const errors: Array<{ email: string; error: string }> = [...refreshErrors];
|
|
1187
1268
|
if (clients.length === 0) return { events: [], errors };
|
|
1188
1269
|
|
|
1270
|
+
const requestedSourceKeys = Array.from(
|
|
1271
|
+
new Set((options.calendarSourceKeys ?? []).filter(Boolean)),
|
|
1272
|
+
);
|
|
1273
|
+
let selectedSourcesByAccount = new Map<string, GoogleCalendarSource[]>();
|
|
1274
|
+
if (requestedSourceKeys.length > 0) {
|
|
1275
|
+
const discovered = await listGoogleCalendars(forEmail);
|
|
1276
|
+
errors.push(...discovered.errors);
|
|
1277
|
+
const discoveredByKey = new Map(
|
|
1278
|
+
discovered.calendars.map((source) => [source.sourceKey, source]),
|
|
1279
|
+
);
|
|
1280
|
+
const invalid = requestedSourceKeys.filter((sourceKey) => {
|
|
1281
|
+
const identity = parseGoogleCalendarSourceKey(sourceKey);
|
|
1282
|
+
return !identity || !discoveredByKey.has(sourceKey);
|
|
1283
|
+
});
|
|
1284
|
+
if (invalid.length > 0) {
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
`Google Calendar source is not connected or no longer available: ${invalid.join(", ")}`,
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
for (const sourceKey of requestedSourceKeys) {
|
|
1290
|
+
const source = discoveredByKey.get(sourceKey)!;
|
|
1291
|
+
const accountKey = source.accountEmail.trim().toLowerCase();
|
|
1292
|
+
selectedSourcesByAccount.set(accountKey, [
|
|
1293
|
+
...(selectedSourcesByAccount.get(accountKey) ?? []),
|
|
1294
|
+
source,
|
|
1295
|
+
]);
|
|
1296
|
+
}
|
|
1297
|
+
const availableAccounts = new Set(
|
|
1298
|
+
clients.map((client) => client.email.trim().toLowerCase()),
|
|
1299
|
+
);
|
|
1300
|
+
const excluded = Array.from(selectedSourcesByAccount.keys()).filter(
|
|
1301
|
+
(accountEmail) => !availableAccounts.has(accountEmail),
|
|
1302
|
+
);
|
|
1303
|
+
if (excluded.length > 0) {
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`Google Calendar source account was not selected: ${excluded.join(", ")}`,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1189
1310
|
const allResults = await mapWithConcurrency(
|
|
1190
1311
|
clients,
|
|
1191
1312
|
async ({ email, accessToken }) => {
|
|
1192
1313
|
try {
|
|
1314
|
+
const sources = selectedSourcesByAccount.size
|
|
1315
|
+
? (selectedSourcesByAccount.get(email.trim().toLowerCase()) ?? [])
|
|
1316
|
+
: [undefined];
|
|
1193
1317
|
const events: any[] = [];
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1318
|
+
for (const source of sources) {
|
|
1319
|
+
if (source?.accessRole === "freeBusyReader") {
|
|
1320
|
+
errors.push({
|
|
1321
|
+
email,
|
|
1322
|
+
error: `Google Calendar source ${source.name} (${source.calendarId}) only grants free/busy access; detailed events were not read`,
|
|
1323
|
+
});
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
try {
|
|
1327
|
+
let pageToken: string | undefined;
|
|
1328
|
+
do {
|
|
1329
|
+
const response = await calendarListEvents(
|
|
1330
|
+
accessToken,
|
|
1331
|
+
source?.calendarId ?? "primary",
|
|
1332
|
+
{
|
|
1333
|
+
timeMin,
|
|
1334
|
+
timeMax,
|
|
1335
|
+
singleEvents: true,
|
|
1336
|
+
orderBy: "startTime",
|
|
1337
|
+
maxResults: options.maxResults ?? 2500,
|
|
1338
|
+
pageToken,
|
|
1339
|
+
eventTypes: LIST_EVENT_TYPES,
|
|
1340
|
+
},
|
|
1341
|
+
);
|
|
1342
|
+
events.push(
|
|
1343
|
+
...(response.items || []).map((event: any) => ({
|
|
1344
|
+
...event,
|
|
1345
|
+
__calendarSource: source,
|
|
1346
|
+
})),
|
|
1347
|
+
);
|
|
1348
|
+
pageToken =
|
|
1349
|
+
typeof response.nextPageToken === "string"
|
|
1350
|
+
? response.nextPageToken
|
|
1351
|
+
: undefined;
|
|
1352
|
+
} while (pageToken);
|
|
1353
|
+
} catch (error: any) {
|
|
1354
|
+
errors.push({
|
|
1355
|
+
email,
|
|
1356
|
+
error: source
|
|
1357
|
+
? `Unable to read Google Calendar source ${source.name} (${source.calendarId}): ${error?.message || "Unknown provider error"}`
|
|
1358
|
+
: error?.message || "Unable to load Google Calendar events",
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1211
1362
|
|
|
1212
1363
|
return events.map((event: any) => {
|
|
1364
|
+
const calendarSource = event.__calendarSource as
|
|
1365
|
+
| GoogleCalendarSource
|
|
1366
|
+
| undefined;
|
|
1213
1367
|
// Find the current user's RSVP status from attendees
|
|
1214
1368
|
const selfAttendee = event.attendees?.find(
|
|
1215
1369
|
(a: any) => a.self === true,
|
|
1216
1370
|
);
|
|
1217
1371
|
return {
|
|
1218
|
-
|
|
1372
|
+
// Google event ids are only unique within a calendar. Retain the
|
|
1373
|
+
// primary legacy id while namespacing every selected non-primary
|
|
1374
|
+
// source so client keys and mutations cannot collide.
|
|
1375
|
+
id:
|
|
1376
|
+
calendarSource && !calendarSource.primary
|
|
1377
|
+
? `google-${calendarSource.sourceKey}-${event.id}`
|
|
1378
|
+
: `google-${event.id}`,
|
|
1219
1379
|
title: event.summary || "Untitled",
|
|
1220
1380
|
titleIsGenerated: !event.summary,
|
|
1221
1381
|
description: event.description || "",
|
|
@@ -1229,6 +1389,12 @@ export async function listEvents(
|
|
|
1229
1389
|
googleEventId: event.id || undefined,
|
|
1230
1390
|
htmlLink: event.htmlLink || undefined,
|
|
1231
1391
|
accountEmail: email,
|
|
1392
|
+
calendarSourceKey: calendarSource?.sourceKey,
|
|
1393
|
+
calendarId: calendarSource?.calendarId,
|
|
1394
|
+
calendarName: calendarSource?.name,
|
|
1395
|
+
calendarAccessRole: calendarSource?.accessRole,
|
|
1396
|
+
calendarPrimary: calendarSource?.primary,
|
|
1397
|
+
calendarReadOnly: calendarSource?.readOnly,
|
|
1232
1398
|
responseStatus: selfAttendee?.responseStatus,
|
|
1233
1399
|
transparency: event.transparency || undefined,
|
|
1234
1400
|
...mapColor(event),
|
|
@@ -1500,18 +1666,40 @@ export async function listOverlayEvents(
|
|
|
1500
1666
|
export async function getEvent(
|
|
1501
1667
|
googleEventId: string,
|
|
1502
1668
|
account: GoogleAccountSelection,
|
|
1669
|
+
options: { calendarSourceKey?: string } = {},
|
|
1503
1670
|
): Promise<CalendarEvent> {
|
|
1671
|
+
let calendarSource: GoogleCalendarSource | undefined;
|
|
1672
|
+
if (options.calendarSourceKey) {
|
|
1673
|
+
const discovered = await listGoogleCalendars(account.ownerEmail);
|
|
1674
|
+
calendarSource = discovered.calendars.find(
|
|
1675
|
+
(source) => source.sourceKey === options.calendarSourceKey,
|
|
1676
|
+
);
|
|
1677
|
+
if (
|
|
1678
|
+
!calendarSource ||
|
|
1679
|
+
calendarSource.accountEmail.trim().toLowerCase() !==
|
|
1680
|
+
account.accountEmail.trim().toLowerCase()
|
|
1681
|
+
) {
|
|
1682
|
+
throw new Error(
|
|
1683
|
+
"Google Calendar source is not connected or no longer available",
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (calendarSource.accessRole === "freeBusyReader") {
|
|
1687
|
+
throw new Error("Google Calendar source only grants free/busy access");
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1504
1690
|
const client = await getClientForAccount(account);
|
|
1505
1691
|
|
|
1506
1692
|
const event = await calendarGetEvent(
|
|
1507
1693
|
client.accessToken,
|
|
1508
|
-
"primary",
|
|
1694
|
+
calendarSource?.calendarId ?? "primary",
|
|
1509
1695
|
googleEventId,
|
|
1510
1696
|
);
|
|
1511
1697
|
const selfAttendee = event.attendees?.find((a: any) => a.self === true);
|
|
1512
1698
|
|
|
1513
1699
|
return {
|
|
1514
|
-
id:
|
|
1700
|
+
id: calendarSource
|
|
1701
|
+
? `google-${calendarSource.sourceKey}-${event.id}`
|
|
1702
|
+
: `google-${event.id}`,
|
|
1515
1703
|
title: event.summary || "Untitled",
|
|
1516
1704
|
titleIsGenerated: !event.summary,
|
|
1517
1705
|
description: event.description || "",
|
|
@@ -1525,6 +1713,12 @@ export async function getEvent(
|
|
|
1525
1713
|
googleEventId: event.id || undefined,
|
|
1526
1714
|
htmlLink: event.htmlLink || undefined,
|
|
1527
1715
|
accountEmail: account.accountEmail,
|
|
1716
|
+
calendarSourceKey: calendarSource?.sourceKey,
|
|
1717
|
+
calendarId: calendarSource?.calendarId,
|
|
1718
|
+
calendarName: calendarSource?.name,
|
|
1719
|
+
calendarAccessRole: calendarSource?.accessRole,
|
|
1720
|
+
calendarPrimary: calendarSource?.primary,
|
|
1721
|
+
calendarReadOnly: calendarSource?.readOnly,
|
|
1528
1722
|
responseStatus: selfAttendee?.responseStatus || undefined,
|
|
1529
1723
|
transparency: event.transparency || undefined,
|
|
1530
1724
|
...mapColor(event),
|
|
@@ -62,6 +62,7 @@ registerEvent({
|
|
|
62
62
|
|
|
63
63
|
const INITIAL_TOOL_NAMES = [
|
|
64
64
|
"view-screen",
|
|
65
|
+
"list-google-calendars",
|
|
65
66
|
"list-events",
|
|
66
67
|
"search-events",
|
|
67
68
|
"get-event",
|
|
@@ -113,6 +114,7 @@ Google Calendar events are NOT stored in the local database. They are fetched li
|
|
|
113
114
|
Provider-specific Calendar actions are shortcuts, not limits. If a first-class action cannot express the exact Google Calendar/CRM endpoint, calendar id, filter, request body, pagination mode, attendee search, recurrence field, or API version needed, call \`provider-api-catalog\` and \`provider-api-docs\` as needed, then call \`provider-api-request\` against the provider's real HTTP API. Use this raw provider API escape hatch instead of weakening the answer, broadening filters, or claiming Calendar cannot do something the underlying API can do.
|
|
114
115
|
|
|
115
116
|
- \`pnpm action view-screen\` — See the visible UI state (current view, date, selected event). Use it for questions about what the user is looking at, not as a prerequisite for deterministic schedule reads.
|
|
117
|
+
- \`pnpm action list-google-calendars\` — Discover the primary and shared calendars available through every connected Google account. Pass returned opaque \`sourceKey\` values to \`list-events --calendarSourceKeys '[...]'\`; shared calendars are view-only and excluded from booking availability.
|
|
116
118
|
- \`pnpm action list-events --from YYYY-MM-DD --to YYYY-MM-DD\` — List events from Google Calendar. The --to date is exclusive, so use tomorrow for today's events.
|
|
117
119
|
- \`pnpm action search-events --query "term" --from YYYY-MM-DD --to YYYY-MM-DD\` — Convenience bounded search by title, attendees, organizer, location, or description. For relationship history, all-calendar discovery, exact attendee/domain search, or custom pagination, prefer provider-api-request with provider=google_calendar.
|
|
118
120
|
- \`pnpm action provider-api-catalog\` / \`provider-api-docs\` / \`provider-api-request\` — Inspect and call the real Google Calendar, Apollo, Gong, HubSpot, and Pylon APIs directly. For Google Calendar events.list pagination use provider=google_calendar, path=/calendars/primary/events, query={...}, fetchAllPages={cursorPath:"nextPageToken",cursorParam:"pageToken",itemsPath:"items"}. For large relationship-history scans, pass stageAs and pagination={nextCursorPath:"nextPageToken",cursorParam:"pageToken",maxPages:N} with itemsPath="items", then use query-staged-dataset.
|
|
@@ -19,6 +19,13 @@ export interface CalendarEvent {
|
|
|
19
19
|
/** Absolute Google Calendar web URL for Google events */
|
|
20
20
|
htmlLink?: string;
|
|
21
21
|
accountEmail?: string;
|
|
22
|
+
/** Provenance for a discovered Google calendar source. */
|
|
23
|
+
calendarSourceKey?: string;
|
|
24
|
+
calendarId?: string;
|
|
25
|
+
calendarName?: string;
|
|
26
|
+
calendarAccessRole?: GoogleCalendarSource["accessRole"];
|
|
27
|
+
calendarPrimary?: boolean;
|
|
28
|
+
calendarReadOnly?: boolean;
|
|
22
29
|
/** Set when this event belongs to an overlaid person's calendar */
|
|
23
30
|
overlayEmail?: string;
|
|
24
31
|
/** Client-only marker for overlaid calendar ownership */
|
|
@@ -360,6 +367,19 @@ export interface GoogleAuthStatus {
|
|
|
360
367
|
}>;
|
|
361
368
|
}
|
|
362
369
|
|
|
370
|
+
export interface GoogleCalendarSource {
|
|
371
|
+
sourceKey: string;
|
|
372
|
+
accountEmail: string;
|
|
373
|
+
calendarId: string;
|
|
374
|
+
name: string;
|
|
375
|
+
color?: string;
|
|
376
|
+
selected: boolean;
|
|
377
|
+
primary: boolean;
|
|
378
|
+
accessRole: "freeBusyReader" | "reader" | "writer" | "owner";
|
|
379
|
+
/** Sources without event detail access are discoverable but cannot be read. */
|
|
380
|
+
readOnly: boolean;
|
|
381
|
+
}
|
|
382
|
+
|
|
363
383
|
export interface ExternalCalendar {
|
|
364
384
|
id: string;
|
|
365
385
|
name: string;
|
|
@@ -29,6 +29,8 @@ export interface CalendarViewPreferences {
|
|
|
29
29
|
accountColorModes: Record<CalendarColorSourceKey, CalendarColorMode>;
|
|
30
30
|
/** Per-account fixed color, used when that account's mode is "single" */
|
|
31
31
|
accountColors: Record<CalendarColorSourceKey, string>;
|
|
32
|
+
/** Agent-Native visibility overrides for Google calendar sources. */
|
|
33
|
+
googleCalendarVisibility: Record<string, boolean>;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export const DEFAULT_CALENDAR_VIEW_PREFERENCES: CalendarViewPreferences = {
|
|
@@ -37,6 +39,7 @@ export const DEFAULT_CALENDAR_VIEW_PREFERENCES: CalendarViewPreferences = {
|
|
|
37
39
|
singleColor: CALENDAR_COLORS[0],
|
|
38
40
|
accountColorModes: {},
|
|
39
41
|
accountColors: {},
|
|
42
|
+
googleCalendarVisibility: {},
|
|
40
43
|
};
|
|
41
44
|
|
|
42
45
|
export function isValidCalendarColorMode(
|
|
@@ -71,6 +74,15 @@ function normalizeColorRecord(
|
|
|
71
74
|
return out;
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
function normalizeBooleanRecord(input: unknown): Record<string, boolean> {
|
|
78
|
+
const out: Record<string, boolean> = {};
|
|
79
|
+
if (!input || typeof input !== "object") return out;
|
|
80
|
+
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
|
81
|
+
if (typeof value === "boolean") out[key] = value;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
/**
|
|
75
87
|
* Returns a stable default color for an account that hasn't picked one yet,
|
|
76
88
|
* cycling through the shared palette by the account's position in `keys` so
|
|
@@ -91,6 +103,7 @@ export function normalizeCalendarViewPreferences(
|
|
|
91
103
|
...DEFAULT_CALENDAR_VIEW_PREFERENCES,
|
|
92
104
|
accountColorModes: {},
|
|
93
105
|
accountColors: {},
|
|
106
|
+
googleCalendarVisibility: {},
|
|
94
107
|
};
|
|
95
108
|
if (!input || typeof input !== "object") return next;
|
|
96
109
|
|
|
@@ -105,6 +118,9 @@ export function normalizeCalendarViewPreferences(
|
|
|
105
118
|
}
|
|
106
119
|
next.accountColorModes = normalizeColorModeRecord(input.accountColorModes);
|
|
107
120
|
next.accountColors = normalizeColorRecord(input.accountColors);
|
|
121
|
+
next.googleCalendarVisibility = normalizeBooleanRecord(
|
|
122
|
+
input.googleCalendarVisibility,
|
|
123
|
+
);
|
|
108
124
|
return next;
|
|
109
125
|
}
|
|
110
126
|
|
|
@@ -117,10 +133,21 @@ export function calendarViewPreferencesEqual(
|
|
|
117
133
|
a.colorMode === b.colorMode &&
|
|
118
134
|
a.singleColor === b.singleColor &&
|
|
119
135
|
recordsEqual(a.accountColorModes, b.accountColorModes) &&
|
|
120
|
-
recordsEqual(a.accountColors, b.accountColors)
|
|
136
|
+
recordsEqual(a.accountColors, b.accountColors) &&
|
|
137
|
+
booleanRecordsEqual(a.googleCalendarVisibility, b.googleCalendarVisibility)
|
|
121
138
|
);
|
|
122
139
|
}
|
|
123
140
|
|
|
141
|
+
function booleanRecordsEqual(
|
|
142
|
+
a: Record<string, boolean>,
|
|
143
|
+
b: Record<string, boolean>,
|
|
144
|
+
): boolean {
|
|
145
|
+
const aKeys = Object.keys(a);
|
|
146
|
+
const bKeys = Object.keys(b);
|
|
147
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
148
|
+
return aKeys.every((key) => a[key] === b[key]);
|
|
149
|
+
}
|
|
150
|
+
|
|
124
151
|
function recordsEqual(
|
|
125
152
|
a: Record<string, string>,
|
|
126
153
|
b: Record<string, string>,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineFeatureFlag } from "@agent-native/core/feature-flags/registry";
|
|
2
|
+
|
|
3
|
+
export const SHARED_GOOGLE_CALENDARS = defineFeatureFlag({
|
|
4
|
+
key: "shared-google-calendars",
|
|
5
|
+
displayName: "Shared Google calendars",
|
|
6
|
+
description:
|
|
7
|
+
"Display non-primary calendars available through connected Google accounts.",
|
|
8
|
+
});
|