@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.
Files changed (69) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  3. package/corpus/templates/calendar/AGENTS.md +4 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +2 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +7 -0
  6. package/corpus/templates/calendar/actions/get-event.ts +40 -3
  7. package/corpus/templates/calendar/actions/list-events.ts +49 -2
  8. package/corpus/templates/calendar/actions/list-google-calendars.ts +24 -0
  9. package/corpus/templates/calendar/actions/update-calendar-visual-preferences.ts +52 -1
  10. package/corpus/templates/calendar/actions/update-event.ts +2 -2
  11. package/corpus/templates/calendar/actions/view-screen.ts +54 -2
  12. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +33 -19
  13. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +31 -5
  14. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +102 -0
  15. package/corpus/templates/calendar/app/hooks/use-events.ts +45 -10
  16. package/corpus/templates/calendar/app/hooks/use-google-auth.ts +8 -1
  17. package/corpus/templates/calendar/app/hooks/use-google-calendars.ts +41 -0
  18. package/corpus/templates/calendar/app/hooks/use-view-preferences.ts +56 -4
  19. package/corpus/templates/calendar/app/lib/shared-calendar-demo.ts +187 -0
  20. package/corpus/templates/calendar/app/pages/CalendarView.tsx +50 -5
  21. package/corpus/templates/calendar/changelog/2026-09-01-calendars-shared-with-your-connected-google-accounts-can-now.md +6 -0
  22. package/corpus/templates/calendar/changelog/2026-09-02-updated-the-booking-link-og-preview-image-with-the-new-monoc.md +6 -0
  23. package/corpus/templates/calendar/server/lib/booking-og-image.ts +8 -18
  24. package/corpus/templates/calendar/server/lib/google-api.ts +10 -0
  25. package/corpus/templates/calendar/server/lib/google-calendar.ts +215 -21
  26. package/corpus/templates/calendar/server/plugins/agent-chat.ts +2 -0
  27. package/corpus/templates/calendar/server/plugins/feature-flags.ts +5 -0
  28. package/corpus/templates/calendar/shared/api.ts +20 -0
  29. package/corpus/templates/calendar/shared/calendar-view-preferences.ts +28 -1
  30. package/corpus/templates/calendar/shared/feature-flags.ts +8 -0
  31. package/corpus/templates/calendar/shared/google-calendar-sources.ts +46 -0
  32. package/corpus/templates/factory/.env.example +5 -0
  33. package/corpus/templates/factory/README.md +9 -0
  34. package/corpus/templates/factory/server/connectors/credentials.ts +28 -6
  35. package/corpus/templates/factory/server/triage/github-client.ts +144 -7
  36. package/corpus/templates/forms/server/lib/form-og-image.ts +7 -17
  37. package/corpus/templates/slides/actions/add-slide.ts +1 -0
  38. package/corpus/templates/slides/actions/create-deck.ts +1 -0
  39. package/corpus/templates/slides/actions/get-deck.ts +1 -0
  40. package/corpus/templates/slides/actions/navigate.ts +1 -0
  41. package/corpus/templates/slides/actions/patch-deck.ts +1 -0
  42. package/corpus/templates/slides/actions/update-slide.ts +1 -0
  43. package/corpus/templates/slides/actions/view-screen.ts +1 -0
  44. package/corpus/templates/slides/server/plugins/agent-chat.ts +2 -0
  45. package/dist/action.d.ts +4 -0
  46. package/dist/action.js +3 -0
  47. package/dist/agent/types.d.ts +1 -0
  48. package/dist/automation/index.d.ts +1 -1
  49. package/dist/client/webmcp.js +7 -2
  50. package/dist/collab/awareness.d.ts +2 -2
  51. package/dist/mcp/build-server.d.ts +2 -0
  52. package/dist/mcp/build-server.js +4 -0
  53. package/dist/notifications/routes.d.ts +3 -3
  54. package/dist/observability/routes.d.ts +6 -6
  55. package/dist/provider-api/actions/provider-api.d.ts +1 -1
  56. package/dist/secrets/routes.d.ts +9 -9
  57. package/dist/server/action-routes.d.ts +1 -0
  58. package/dist/server/action-routes.js +4 -0
  59. package/dist/server/agent-chat/mcp-options.d.ts +4 -0
  60. package/dist/server/agent-chat/mcp-options.js +1 -0
  61. package/dist/server/agent-chat-plugin.js +2 -0
  62. package/dist/server/realtime-token.d.ts +1 -1
  63. package/dist/server/social-og-image.js +6 -20
  64. package/dist/server/transcribe-voice.d.ts +1 -1
  65. package/dist/shared/agent-mcp-metadata.d.ts +3 -0
  66. package/dist/shared/agent-mcp-metadata.js +18 -0
  67. package/dist/shared/social-meta.d.ts +1 -1
  68. package/dist/shared/social-meta.js +1 -1
  69. 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(queryClient, range.from, range.to, overlayEmails);
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 (!event || !isCalendarEventOrganizer(event) || updateEvent.isPending)
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 (!event || !isCalendarEventOrganizer(event) || updateEvent.isPending)
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 —
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-09-01
4
+ ---
5
+
6
+ Calendars shared with your connected Google accounts can now appear alongside your primary calendars.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-09-02
4
+ ---
5
+
6
+ Updated the booking-link OG preview image with the new monochrome logo and dark background.
@@ -16,13 +16,11 @@ export interface BookingOgImageInput {
16
16
 
17
17
  const WIDTH = 1200;
18
18
  const HEIGHT = 630;
19
- const BRAND_BLUE = "#00B5FF";
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 = "#ededed";
25
- const MUTED = "#a0a0a0";
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="M24.5537 65.7695H0L15.0859 39.4619L37.708 0L60.4912 39.4619H39.6396L24.5537 65.7695Z" fill="white"/>
37
- <path d="M89.446 0H114L76.2921 65.7704H51.7383L89.446 0Z" fill="url(#brand)"/>
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="${BRAND_MINT}"/>
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="url(#brand)" fill-opacity="0.2"/>
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: { accountEmails?: string[]; maxResults?: number } = {},
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
- let pageToken: string | undefined;
1195
- do {
1196
- const response = await calendarListEvents(accessToken, "primary", {
1197
- timeMin,
1198
- timeMax,
1199
- singleEvents: true,
1200
- orderBy: "startTime",
1201
- maxResults: options.maxResults ?? 2500,
1202
- pageToken,
1203
- eventTypes: LIST_EVENT_TYPES,
1204
- });
1205
- events.push(...(response.items || []));
1206
- pageToken =
1207
- typeof response.nextPageToken === "string"
1208
- ? response.nextPageToken
1209
- : undefined;
1210
- } while (pageToken);
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
- id: `google-${event.id}`,
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: `google-${event.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.
@@ -0,0 +1,5 @@
1
+ import { createFeatureFlagsPlugin } from "@agent-native/core/server";
2
+
3
+ import { SHARED_GOOGLE_CALENDARS } from "../../shared/feature-flags.js";
4
+
5
+ export default createFeatureFlagsPlugin({ flags: [SHARED_GOOGLE_CALENDARS] });
@@ -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
+ });