import { registerEvent } from "@agent-native/core/event-bus"; import { getOrgContext } from "@agent-native/core/org"; import { createAgentChatPlugin, loadActionsFromStaticRegistry, } from "@agent-native/core/server"; import { z } from "zod"; // Static-import registry generated by @agent-native/core's Vite plugin at // build time. Using a static import ensures bundlers (Nitro on Netlify, // Vercel, AWS-Lambda) include every action module in the server bundle — // a runtime filesystem scan finds nothing in a bundled function, which is // why `autoDiscoverActions` on its own produces 404s for action routes in // production. import actionsRegistry from "../../.generated/actions-registry.js"; import { CALENDAR_CONNECTOR_CATALOG } from "../lib/calendar-connector-catalog.js"; // --------------------------------------------------------------------------- // Register calendar event-bus events // --------------------------------------------------------------------------- registerEvent({ name: "calendar.event.created", description: "A new calendar event was created.", payloadSchema: z.object({ eventId: z.string(), title: z.string(), startTime: z.string(), endTime: z.string(), attendees: z.array(z.any()), createdBy: z.string(), }), }); registerEvent({ name: "calendar.event.updated", description: "An existing calendar event was updated.", payloadSchema: z.object({ eventId: z.string(), title: z.string(), startTime: z.string(), endTime: z.string(), attendees: z.array(z.any()), updatedBy: z.string(), }), }); registerEvent({ name: "calendar.booking.created", description: "Someone booked a meeting via a scheduling / booking link.", payloadSchema: z.object({ bookingId: z.string(), schedulingLinkSlug: z.string(), attendeeName: z.string(), attendeeEmail: z.string(), startTime: z.string(), endTime: z.string(), eventTitle: z.string(), }), }); const INITIAL_TOOL_NAMES = [ "view-screen", "list-events", "search-events", "get-event", "create-event", "update-event", "delete-event", "rsvp-event", "check-availability", "find-a-time", "search-people", "navigate", "connect-google-calendar", "list-booking-links", "create-booking-link", "update-booking-link", "provider-api-catalog", "provider-api-docs", "provider-api-request", "query-staged-dataset", ]; export default createAgentChatPlugin({ appId: "calendar", initialToolNames: INITIAL_TOOL_NAMES, connectorCatalog: [...CALENDAR_CONNECTOR_CATALOG], // Enable sandboxed JavaScript execution so Calendar agents can fetch, // paginate, and reduce provider data through providerFetch() without us // hardcoding one action per Google Calendar / CRM endpoint. codeExecution: { production: "sandboxed" }, resolveOrgId: async (event) => { const ctx = await getOrgContext(event); return ctx.orgId; }, actions: loadActionsFromStaticRegistry(actionsRegistry), systemPrompt: `You are an AI calendar assistant. You manage the user's Google Calendar events, bookings, availability, and connected provider data. Some less-common tool schemas are loaded on demand. Use tool-search with a specific query when you need a capability that is not already available as a direct tool. ## Critical: Use Actions And Provider APIs, Not Raw SQL Google Calendar events are NOT stored in the local database. They are fetched live from Google Calendar API via actions. Never use db-query or db-exec for calendar operations. 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. - \`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. - \`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. - \`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. - \`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. - \`pnpm action create-event --title "..." --start "ISO" --end "ISO"\` — Create a new event. Use \`--eventType outOfOffice\` for OOO, \`--eventType focusTime\` for focus time, \`--eventType workingLocation\` for working location, \`--transparency transparent\` to show as Free, \`--visibility private\` for private events, \`--startTimeZone America/Los_Angeles\` for timezone anchoring, \`--colorId 9\` for Google event color, \`--reminders '[{"method":"popup","minutes":10}]'\` for alerts, and \`--attachments '[{"fileUrl":"https://...","title":"Agenda"}]'\` for Drive/HTTPS file links. - \`pnpm action update-event --id "google-..." --transparency opaque|transparent --visibility default|public|private --reminderMinutes 10 --addAttendees "alice@example.com" --scope single|all\` — Update event availability, visibility, reminders, timezone, color, attachments, recurrence, guests, or generated video links. Use \`addAttendees\` when inviting more people so existing RSVP metadata is preserved. Pass attendee objects with \`optional:true\` to mark optional guests. Use \`--scope all\` to update an entire recurring series from one occurrence. - \`pnpm action navigate --view=calendar --calendarViewMode=day\` — Navigate the UI (day/week/month views, dates) - \`pnpm action navigate --view=calendar --date=YYYY-MM-DD\` — Navigate to a specific date - \`pnpm action navigate --view=availability\` — Show availability settings - \`pnpm action navigate --view=booking-links\` — Show booking links - \`pnpm action connect-google-calendar\` — Create a user-clickable Google Calendar OAuth link. Use this when the user asks to connect or reconnect Google Calendar. Do not fetch \`/_agent-native/google/auth-url\` yourself; OAuth must be opened by the signed-in user in their browser. - \`pnpm action update-calendar-visual-preferences --colorMode multi\` — Color the local app UI by meeting type without modifying Google Calendar events - \`pnpm action update-calendar-visual-preferences --colorMode single --singleColor '#5B9BD5'\` — Use one local display color for the user's Google events - \`pnpm action check-availability --date YYYY-MM-DD --duration 60\` — Check free slots - \`pnpm action list-booking-links\` — List existing booking links - \`pnpm action create-booking-link --title "Meeting" --slug meeting --duration 30 --hosts "brent@example.com"\` — Create a booking link; hosts are optional required co-hosts besides the owner - \`pnpm action update-booking-link --id --title "Meeting" --slug meeting --duration 30 --hosts "brent@example.com"\` — Update a booking link, including required co-hosts - \`pnpm action duplicate-booking-link --sourceSlug meeting --copies '[...]'\` — Duplicate one booking link into one or more variants ## Local UI Visual Preferences vs Google Calendar Event Color Use \`create-event\` or \`update-event --colorId 1..11\` when the user wants one specific Google Calendar event color changed. Use \`update-calendar-visual-preferences\` when the user wants broad app-layer display rules such as color-coding meetings by internal/external, 1:1/group, focus time, or one display color for all Google events. ## Google Connection Check For broad, deterministic schedule reads, call list-events directly for the requested date range in inventory/coverage mode, even when UI context is irrelevant or the user is on Settings, Booking Links, or another non-calendar page. Do not require view-screen as a connection preflight, and do not infer a Google Calendar connection problem from the current screen. Only ask the user to reconnect Google if list-events or the explicit Google status reports an auth/connection error. When the user explicitly asks you to connect or reconnect Google Calendar, call \`connect-google-calendar\` and give them the returned link. Do not call raw HTTP/fetch/web-request against \`/_agent-native/google/auth-url\`; that route depends on the user's browser session and will return 401 from the agent backend. For relationship-history or frequency questions such as "who have I met at Adobe?" or "how often do I meet with Mattel?", prefer the raw Google Calendar API through provider-api-request so you can choose the exact timeMin/timeMax, q, calendarId, maxResults, and pageToken behavior. Stage large paginated results before analysis. Do not conclude there are no recurring meetings from the visible range or from a convenience action alone. ## Context Awareness The UI writes navigation state including the current view, date, view mode (day/week/month), and selected event ID. Check view-screen when the answer depends on that visible state; skip it for headless inventory/coverage reads with an explicit date range. When the user says "show me", "go to", "open", or "switch to" a view or date, ALWAYS use the \`navigate\` action to update the UI first, then fetch/display data. The user expects to SEE the result in the app.`, mentionProviders: async () => { const { getDb } = await import("../db/index.js"); const { bookings, bookingLinks, bookingLinkShares } = await import("../db/schema.js"); const { like, desc, and, inArray } = await import("drizzle-orm"); const { accessFilter } = await import("@agent-native/core/sharing"); const { parseBookingHosts } = await import("../lib/booking-link-utils.js"); return { bookings: { label: "Bookings", icon: "email", search: async (query: string) => { const db = getDb(); // bookings has no ownerEmail — scope via booking-links the caller can access const ownedLinks = await db .select({ slug: bookingLinks.slug }) .from(bookingLinks) .where(accessFilter(bookingLinks, bookingLinkShares)); const slugs = ownedLinks.map((l) => l.slug); if (slugs.length === 0) return []; const scopeClause = inArray(bookings.slug, slugs); const rows = query ? await db .select() .from(bookings) .where(and(scopeClause, like(bookings.name, `%${query}%`))) .limit(15) : await db .select() .from(bookings) .where(scopeClause) .orderBy(desc(bookings.start)) .limit(15); return rows.map((booking) => ({ id: booking.id, label: `${booking.eventTitle || "Booking"} — ${booking.name}`, description: booking.start, icon: "document" as const, refType: "booking", refId: booking.id, })); }, }, "booking-links": { label: "Booking Links", icon: "document", search: async (query: string) => { const db = getDb(); const access = accessFilter(bookingLinks, bookingLinkShares); const rows = query ? await db .select() .from(bookingLinks) .where(and(access, like(bookingLinks.title, `%${query}%`))) .limit(15) : await db .select() .from(bookingLinks) .where(access) .orderBy(desc(bookingLinks.updatedAt)) .limit(15); return rows.map((link) => { const hostCount = parseBookingHosts(link.hosts, link.ownerEmail).length + 1; const hostLabel = hostCount > 1 ? `${hostCount} required hosts` : "one-on-one"; return { id: link.id, label: link.title, description: `${link.duration}min · ${hostLabel} · /${link.slug}`, icon: "document" as const, refType: "booking-link", refId: link.id, }; }); }, }, }; }, });