veryfront 0.0.45 → 0.0.47

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 (61) hide show
  1. package/dist/ai/components.js +3 -3
  2. package/dist/ai/components.js.map +2 -2
  3. package/dist/ai/index.d.ts +18 -3
  4. package/dist/ai/index.js +12 -2
  5. package/dist/ai/index.js.map +2 -2
  6. package/dist/cli.js +4 -5
  7. package/dist/components.js +1 -1
  8. package/dist/components.js.map +1 -1
  9. package/dist/config.d.ts +7 -0
  10. package/dist/config.js +1 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/data.js +1 -1
  13. package/dist/data.js.map +1 -1
  14. package/dist/index.js +2 -5
  15. package/dist/index.js.map +2 -2
  16. package/dist/integrations/_base/connector.json +11 -0
  17. package/dist/integrations/_base/files/SETUP.md +132 -0
  18. package/dist/integrations/_base/files/app/api/integrations/status/route.ts +38 -0
  19. package/dist/integrations/_base/files/app/setup/page.tsx +461 -0
  20. package/dist/integrations/_base/files/lib/oauth.ts +145 -0
  21. package/dist/integrations/_base/files/lib/token-store.ts +109 -0
  22. package/dist/integrations/calendar/connector.json +77 -0
  23. package/dist/integrations/calendar/files/ai/tools/create-event.ts +83 -0
  24. package/dist/integrations/calendar/files/ai/tools/find-free-time.ts +108 -0
  25. package/dist/integrations/calendar/files/ai/tools/list-events.ts +98 -0
  26. package/dist/integrations/calendar/files/app/api/auth/calendar/callback/route.ts +114 -0
  27. package/dist/integrations/calendar/files/app/api/auth/calendar/route.ts +29 -0
  28. package/dist/integrations/calendar/files/lib/calendar-client.ts +309 -0
  29. package/dist/integrations/calendar/files/lib/oauth.ts +145 -0
  30. package/dist/integrations/calendar/files/lib/token-store.ts +109 -0
  31. package/dist/integrations/github/connector.json +84 -0
  32. package/dist/integrations/github/files/ai/tools/create-issue.ts +75 -0
  33. package/dist/integrations/github/files/ai/tools/get-pr-diff.ts +82 -0
  34. package/dist/integrations/github/files/ai/tools/list-prs.ts +93 -0
  35. package/dist/integrations/github/files/ai/tools/list-repos.ts +81 -0
  36. package/dist/integrations/github/files/app/api/auth/github/callback/route.ts +132 -0
  37. package/dist/integrations/github/files/app/api/auth/github/route.ts +29 -0
  38. package/dist/integrations/github/files/lib/github-client.ts +282 -0
  39. package/dist/integrations/github/files/lib/oauth.ts +145 -0
  40. package/dist/integrations/github/files/lib/token-store.ts +109 -0
  41. package/dist/integrations/gmail/connector.json +78 -0
  42. package/dist/integrations/gmail/files/ai/tools/list-emails.ts +92 -0
  43. package/dist/integrations/gmail/files/ai/tools/search-emails.ts +92 -0
  44. package/dist/integrations/gmail/files/ai/tools/send-email.ts +77 -0
  45. package/dist/integrations/gmail/files/app/api/auth/gmail/callback/route.ts +114 -0
  46. package/dist/integrations/gmail/files/app/api/auth/gmail/route.ts +29 -0
  47. package/dist/integrations/gmail/files/lib/gmail-client.ts +259 -0
  48. package/dist/integrations/gmail/files/lib/oauth.ts +145 -0
  49. package/dist/integrations/gmail/files/lib/token-store.ts +109 -0
  50. package/dist/integrations/slack/connector.json +74 -0
  51. package/dist/integrations/slack/files/ai/tools/get-messages.ts +65 -0
  52. package/dist/integrations/slack/files/ai/tools/list-channels.ts +63 -0
  53. package/dist/integrations/slack/files/ai/tools/send-message.ts +49 -0
  54. package/dist/integrations/slack/files/app/api/auth/slack/callback/route.ts +132 -0
  55. package/dist/integrations/slack/files/app/api/auth/slack/route.ts +29 -0
  56. package/dist/integrations/slack/files/lib/oauth.ts +145 -0
  57. package/dist/integrations/slack/files/lib/slack-client.ts +215 -0
  58. package/dist/integrations/slack/files/lib/token-store.ts +109 -0
  59. package/dist/templates/ai/app/page.tsx +4 -4
  60. package/dist/templates/ai/tsconfig.json +1 -0
  61. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ /**
2
+ * OAuth Helper Functions
3
+ *
4
+ * Provides utilities for OAuth 2.0 authorization flows.
5
+ */
6
+
7
+ import { type OAuthToken, tokenStore } from "./token-store.ts";
8
+
9
+ export interface OAuthProvider {
10
+ name: string;
11
+ authorizationUrl: string;
12
+ tokenUrl: string;
13
+ clientId: string;
14
+ clientSecret: string;
15
+ scopes: string[];
16
+ callbackPath: string;
17
+ }
18
+
19
+ /**
20
+ * Generate OAuth authorization URL
21
+ */
22
+ export function getAuthorizationUrl(
23
+ provider: OAuthProvider,
24
+ state: string,
25
+ redirectUri: string,
26
+ ): string {
27
+ const params = new URLSearchParams({
28
+ client_id: provider.clientId,
29
+ redirect_uri: redirectUri,
30
+ response_type: "code",
31
+ scope: provider.scopes.join(" "),
32
+ state,
33
+ access_type: "offline",
34
+ prompt: "consent",
35
+ });
36
+
37
+ return `${provider.authorizationUrl}?${params.toString()}`;
38
+ }
39
+
40
+ /**
41
+ * Exchange authorization code for tokens
42
+ */
43
+ export async function exchangeCodeForTokens(
44
+ provider: OAuthProvider,
45
+ code: string,
46
+ redirectUri: string,
47
+ ): Promise<OAuthToken> {
48
+ const response = await fetch(provider.tokenUrl, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/x-www-form-urlencoded",
52
+ },
53
+ body: new URLSearchParams({
54
+ client_id: provider.clientId,
55
+ client_secret: provider.clientSecret,
56
+ code,
57
+ grant_type: "authorization_code",
58
+ redirect_uri: redirectUri,
59
+ }),
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const error = await response.text();
64
+ throw new Error(`Token exchange failed: ${response.status} - ${error}`);
65
+ }
66
+
67
+ const data = await response.json();
68
+
69
+ return {
70
+ accessToken: data.access_token,
71
+ refreshToken: data.refresh_token,
72
+ expiresAt: data.expires_in ? Date.now() + (data.expires_in * 1000) : undefined,
73
+ tokenType: data.token_type || "Bearer",
74
+ scope: data.scope,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Refresh an expired access token
80
+ */
81
+ export async function refreshAccessToken(
82
+ provider: OAuthProvider,
83
+ refreshToken: string,
84
+ ): Promise<OAuthToken> {
85
+ const response = await fetch(provider.tokenUrl, {
86
+ method: "POST",
87
+ headers: {
88
+ "Content-Type": "application/x-www-form-urlencoded",
89
+ },
90
+ body: new URLSearchParams({
91
+ client_id: provider.clientId,
92
+ client_secret: provider.clientSecret,
93
+ refresh_token: refreshToken,
94
+ grant_type: "refresh_token",
95
+ }),
96
+ });
97
+
98
+ if (!response.ok) {
99
+ const error = await response.text();
100
+ throw new Error(`Token refresh failed: ${response.status} - ${error}`);
101
+ }
102
+
103
+ const data = await response.json();
104
+
105
+ return {
106
+ accessToken: data.access_token,
107
+ refreshToken: data.refresh_token || refreshToken,
108
+ expiresAt: data.expires_in ? Date.now() + (data.expires_in * 1000) : undefined,
109
+ tokenType: data.token_type || "Bearer",
110
+ scope: data.scope,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Get a valid access token, refreshing if necessary
116
+ */
117
+ export async function getValidToken(
118
+ provider: OAuthProvider,
119
+ userId: string,
120
+ service: string,
121
+ ): Promise<string | null> {
122
+ const token = await tokenStore.getToken(userId, service);
123
+
124
+ if (!token) {
125
+ return null;
126
+ }
127
+
128
+ // Check if token is expired (with 5 minute buffer)
129
+ // If no expiresAt, token doesn't expire (e.g., GitHub)
130
+ const isExpired = token.expiresAt ? token.expiresAt < Date.now() + 5 * 60 * 1000 : false;
131
+
132
+ if (isExpired && token.refreshToken) {
133
+ try {
134
+ const newToken = await refreshAccessToken(provider, token.refreshToken);
135
+ await tokenStore.setToken(userId, service, newToken);
136
+ return newToken.accessToken;
137
+ } catch {
138
+ // Refresh failed, user needs to re-authorize
139
+ await tokenStore.revokeToken(userId, service);
140
+ return null;
141
+ }
142
+ }
143
+
144
+ return token.accessToken;
145
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * OAuth Token Store
3
+ *
4
+ * Simple in-memory token store for development.
5
+ * Replace with a database or KV store for production.
6
+ */
7
+
8
+ export interface OAuthToken {
9
+ accessToken: string;
10
+ refreshToken?: string;
11
+ expiresAt?: number;
12
+ tokenType?: string;
13
+ scope?: string;
14
+ }
15
+
16
+ export interface TokenStore {
17
+ getToken(userId: string, service: string): Promise<OAuthToken | null>;
18
+ setToken(userId: string, service: string, token: OAuthToken): Promise<void>;
19
+ revokeToken(userId: string, service: string): Promise<void>;
20
+ isConnected(userId: string, service: string): Promise<boolean>;
21
+ }
22
+
23
+ // In-memory storage for development
24
+ const tokens = new Map<string, OAuthToken>();
25
+
26
+ function getKey(userId: string, service: string): string {
27
+ return `${userId}:${service}`;
28
+ }
29
+
30
+ /**
31
+ * Simple in-memory token store
32
+ *
33
+ * NOTE: This is for development only. In production, use:
34
+ * - Database (Postgres, SQLite, etc.)
35
+ * - KV store (Cloudflare Workers KV, Vercel KV, etc.)
36
+ * - Encrypted file storage
37
+ */
38
+ export const tokenStore: TokenStore = {
39
+ getToken(userId: string, service: string): Promise<OAuthToken | null> {
40
+ const key = getKey(userId, service);
41
+ return Promise.resolve(tokens.get(key) || null);
42
+ },
43
+
44
+ setToken(
45
+ userId: string,
46
+ service: string,
47
+ token: OAuthToken,
48
+ ): Promise<void> {
49
+ const key = getKey(userId, service);
50
+ tokens.set(key, token);
51
+ return Promise.resolve();
52
+ },
53
+
54
+ revokeToken(userId: string, service: string): Promise<void> {
55
+ const key = getKey(userId, service);
56
+ tokens.delete(key);
57
+ return Promise.resolve();
58
+ },
59
+
60
+ async isConnected(userId: string, service: string): Promise<boolean> {
61
+ const token = await this.getToken(userId, service);
62
+ if (!token) return false;
63
+ // Check if token is not expired (if no expiry, token doesn't expire)
64
+ return !token.expiresAt || token.expiresAt > Date.now();
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Factory function to create a custom token store
70
+ */
71
+ export function createTokenStore(options: {
72
+ get: (key: string) => Promise<string | null>;
73
+ set: (key: string, value: string) => Promise<void>;
74
+ delete: (key: string) => Promise<void>;
75
+ }): TokenStore {
76
+ return {
77
+ async getToken(userId: string, service: string): Promise<OAuthToken | null> {
78
+ const key = getKey(userId, service);
79
+ const data = await options.get(key);
80
+ if (!data) return null;
81
+ try {
82
+ return JSON.parse(data) as OAuthToken;
83
+ } catch {
84
+ return null;
85
+ }
86
+ },
87
+
88
+ async setToken(
89
+ userId: string,
90
+ service: string,
91
+ token: OAuthToken,
92
+ ): Promise<void> {
93
+ const key = getKey(userId, service);
94
+ await options.set(key, JSON.stringify(token));
95
+ },
96
+
97
+ async revokeToken(userId: string, service: string): Promise<void> {
98
+ const key = getKey(userId, service);
99
+ await options.delete(key);
100
+ },
101
+
102
+ async isConnected(userId: string, service: string): Promise<boolean> {
103
+ const token = await this.getToken(userId, service);
104
+ if (!token) return false;
105
+ // Check if token is not expired (if no expiry, token doesn't expire)
106
+ return !token.expiresAt || token.expiresAt > Date.now();
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "calendar",
3
+ "displayName": "Google Calendar",
4
+ "icon": "calendar.svg",
5
+ "description": "Manage events, find free time, and schedule meetings",
6
+ "auth": {
7
+ "type": "oauth2",
8
+ "provider": "google",
9
+ "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
10
+ "tokenUrl": "https://oauth2.googleapis.com/token",
11
+ "scopes": [
12
+ "https://www.googleapis.com/auth/calendar.readonly",
13
+ "https://www.googleapis.com/auth/calendar.events"
14
+ ],
15
+ "callbackPath": "/api/auth/calendar/callback"
16
+ },
17
+ "envVars": [
18
+ {
19
+ "name": "GOOGLE_CLIENT_ID",
20
+ "description": "Google OAuth Client ID",
21
+ "required": true,
22
+ "sensitive": false,
23
+ "docsUrl": "https://console.cloud.google.com/apis/credentials"
24
+ },
25
+ {
26
+ "name": "GOOGLE_CLIENT_SECRET",
27
+ "description": "Google OAuth Client Secret",
28
+ "required": true,
29
+ "sensitive": true,
30
+ "docsUrl": "https://console.cloud.google.com/apis/credentials"
31
+ }
32
+ ],
33
+ "tools": [
34
+ {
35
+ "id": "list-events",
36
+ "name": "List Events",
37
+ "description": "Get upcoming calendar events",
38
+ "requiresWrite": false
39
+ },
40
+ {
41
+ "id": "create-event",
42
+ "name": "Create Event",
43
+ "description": "Schedule a new calendar event",
44
+ "requiresWrite": true
45
+ },
46
+ {
47
+ "id": "find-free-time",
48
+ "name": "Find Free Time",
49
+ "description": "Find available time slots in calendar",
50
+ "requiresWrite": false
51
+ }
52
+ ],
53
+ "prompts": [
54
+ {
55
+ "id": "block-deep-work",
56
+ "title": "Block time for deep work",
57
+ "prompt": "Find a 2-hour block for focused work this week and add it to my calendar.",
58
+ "category": "productivity",
59
+ "icon": "clock"
60
+ },
61
+ {
62
+ "id": "schedule-meeting",
63
+ "title": "Schedule a meeting",
64
+ "prompt": "Help me schedule a meeting. Find available time slots and create the calendar event.",
65
+ "category": "productivity",
66
+ "icon": "users"
67
+ },
68
+ {
69
+ "id": "today-agenda",
70
+ "title": "What's on my calendar today?",
71
+ "prompt": "Show me my calendar for today and summarize my schedule.",
72
+ "category": "productivity",
73
+ "icon": "calendar"
74
+ }
75
+ ],
76
+ "suggestedWith": ["gmail", "slack"]
77
+ }
@@ -0,0 +1,83 @@
1
+ import { tool } from "veryfront/ai";
2
+ import { z } from "zod";
3
+ import { createCalendarClient } from "../../lib/calendar-client.ts";
4
+
5
+ export default tool({
6
+ id: "create-event",
7
+ description: "Create a new event in Google Calendar",
8
+ inputSchema: z.object({
9
+ title: z
10
+ .string()
11
+ .min(1)
12
+ .describe("Event title"),
13
+ startTime: z
14
+ .string()
15
+ .describe("Start time in ISO 8601 format (e.g., '2024-01-15T09:00:00')"),
16
+ endTime: z
17
+ .string()
18
+ .describe("End time in ISO 8601 format (e.g., '2024-01-15T10:00:00')"),
19
+ description: z
20
+ .string()
21
+ .optional()
22
+ .describe("Event description"),
23
+ location: z
24
+ .string()
25
+ .optional()
26
+ .describe("Event location"),
27
+ attendees: z
28
+ .array(z.string().email())
29
+ .optional()
30
+ .describe("Email addresses of attendees to invite"),
31
+ timeZone: z
32
+ .string()
33
+ .default("UTC")
34
+ .describe("Time zone for the event (e.g., 'America/New_York')"),
35
+ }),
36
+ execute: async (
37
+ { title, startTime, endTime, description, location, attendees, timeZone },
38
+ context,
39
+ ) => {
40
+ const userId = context?.userId as string | undefined;
41
+ if (!userId) {
42
+ return {
43
+ error: "User not authenticated. Please log in first.",
44
+ };
45
+ }
46
+
47
+ try {
48
+ const calendar = createCalendarClient(userId);
49
+
50
+ const event = await calendar.createEvent({
51
+ summary: title,
52
+ start: startTime,
53
+ end: endTime,
54
+ description,
55
+ location,
56
+ attendees,
57
+ timeZone,
58
+ });
59
+
60
+ return {
61
+ success: true,
62
+ event: {
63
+ id: event.id,
64
+ title: event.summary,
65
+ start: event.start.dateTime || event.start.date,
66
+ end: event.end.dateTime || event.end.date,
67
+ url: event.htmlLink,
68
+ location: event.location,
69
+ attendees: event.attendees?.map((a: { email: string }) => a.email) || [],
70
+ },
71
+ message: `Event "${title}" created successfully.`,
72
+ };
73
+ } catch (error) {
74
+ if (error instanceof Error && error.message.includes("not connected")) {
75
+ return {
76
+ error: "Calendar not connected. Please connect your Google Calendar.",
77
+ connectUrl: "/api/auth/calendar",
78
+ };
79
+ }
80
+ throw error;
81
+ }
82
+ },
83
+ });
@@ -0,0 +1,108 @@
1
+ import { tool } from "veryfront/ai";
2
+ import { z } from "zod";
3
+ import { createCalendarClient } from "../../lib/calendar-client.ts";
4
+
5
+ export default tool({
6
+ id: "find-free-time",
7
+ description: "Find available time slots in the calendar for scheduling",
8
+ inputSchema: z.object({
9
+ durationMinutes: z
10
+ .number()
11
+ .min(15)
12
+ .max(480)
13
+ .default(60)
14
+ .describe("Duration needed in minutes"),
15
+ daysToSearch: z
16
+ .number()
17
+ .min(1)
18
+ .max(14)
19
+ .default(7)
20
+ .describe("Number of days to search ahead"),
21
+ workingHoursOnly: z
22
+ .boolean()
23
+ .default(true)
24
+ .describe("Only show slots during working hours (9 AM - 6 PM)"),
25
+ }),
26
+ execute: async ({ durationMinutes, daysToSearch, workingHoursOnly }, context) => {
27
+ const userId = context?.userId as string | undefined;
28
+ if (!userId) {
29
+ return {
30
+ error: "User not authenticated. Please log in first.",
31
+ };
32
+ }
33
+
34
+ try {
35
+ const calendar = createCalendarClient(userId);
36
+
37
+ const now = new Date();
38
+ const searchEnd = new Date();
39
+ searchEnd.setDate(searchEnd.getDate() + (daysToSearch ?? 7));
40
+
41
+ type FreeSlot = { start: Date; end: Date };
42
+ const freeSlots = (await calendar.findFreeSlots({
43
+ timeMin: now,
44
+ timeMax: searchEnd,
45
+ durationMinutes: durationMinutes ?? 60,
46
+ })) as FreeSlot[];
47
+
48
+ // Filter to working hours if requested
49
+ let filteredSlots = freeSlots;
50
+ if (workingHoursOnly) {
51
+ filteredSlots = freeSlots.filter((slot: FreeSlot) => {
52
+ const startHour = slot.start.getHours();
53
+ const endHour = slot.end.getHours();
54
+ return startHour >= 9 && endHour <= 18;
55
+ });
56
+ }
57
+
58
+ // Format slots for display
59
+ const formattedSlots = filteredSlots.slice(0, 10).map((slot: FreeSlot) => {
60
+ const duration = Math.round(
61
+ (slot.end.getTime() - slot.start.getTime()) / (1000 * 60),
62
+ );
63
+ return {
64
+ start: slot.start.toISOString(),
65
+ end: slot.end.toISOString(),
66
+ durationMinutes: duration,
67
+ date: slot.start.toLocaleDateString("en-US", {
68
+ weekday: "long",
69
+ month: "short",
70
+ day: "numeric",
71
+ }),
72
+ timeRange: `${
73
+ slot.start.toLocaleTimeString("en-US", {
74
+ hour: "numeric",
75
+ minute: "2-digit",
76
+ })
77
+ } - ${
78
+ slot.end.toLocaleTimeString("en-US", {
79
+ hour: "numeric",
80
+ minute: "2-digit",
81
+ })
82
+ }`,
83
+ };
84
+ });
85
+
86
+ return {
87
+ freeSlots: formattedSlots,
88
+ count: formattedSlots.length,
89
+ searchCriteria: {
90
+ durationMinutes,
91
+ daysToSearch,
92
+ workingHoursOnly,
93
+ },
94
+ message: formattedSlots.length > 0
95
+ ? `Found ${formattedSlots.length} available slot(s) of ${durationMinutes} minutes or more.`
96
+ : `No free slots of ${durationMinutes} minutes found in the next ${daysToSearch} days.`,
97
+ };
98
+ } catch (error) {
99
+ if (error instanceof Error && error.message.includes("not connected")) {
100
+ return {
101
+ error: "Calendar not connected. Please connect your Google Calendar.",
102
+ connectUrl: "/api/auth/calendar",
103
+ };
104
+ }
105
+ throw error;
106
+ }
107
+ },
108
+ });
@@ -0,0 +1,98 @@
1
+ import { tool } from "veryfront/ai";
2
+ import { z } from "zod";
3
+ import { createCalendarClient } from "../../lib/calendar-client.ts";
4
+
5
+ export default tool({
6
+ id: "list-events",
7
+ description: "List upcoming calendar events. By default shows events from now onwards.",
8
+ inputSchema: z.object({
9
+ maxResults: z
10
+ .number()
11
+ .min(1)
12
+ .max(100)
13
+ .default(10)
14
+ .describe("Maximum number of events to return"),
15
+ daysAhead: z
16
+ .number()
17
+ .min(1)
18
+ .max(30)
19
+ .default(7)
20
+ .describe("Number of days to look ahead"),
21
+ todayOnly: z
22
+ .boolean()
23
+ .default(false)
24
+ .describe("Only show events for today"),
25
+ }),
26
+ execute: async ({ maxResults, daysAhead, todayOnly }, context) => {
27
+ const userId = context?.userId as string | undefined;
28
+ if (!userId) {
29
+ return {
30
+ error: "User not authenticated. Please log in first.",
31
+ };
32
+ }
33
+
34
+ try {
35
+ const calendar = createCalendarClient(userId);
36
+
37
+ let events;
38
+
39
+ if (todayOnly) {
40
+ events = await calendar.getTodayEvents();
41
+ } else {
42
+ const now = new Date();
43
+ const futureDate = new Date();
44
+ futureDate.setDate(futureDate.getDate() + (daysAhead ?? 7));
45
+
46
+ events = await calendar.listEvents({
47
+ maxResults,
48
+ timeMin: now,
49
+ timeMax: futureDate,
50
+ });
51
+ }
52
+
53
+ type CalendarEvent = {
54
+ id: string;
55
+ summary: string;
56
+ description?: string;
57
+ location?: string;
58
+ start: { dateTime?: string; date?: string };
59
+ end: { dateTime?: string; date?: string };
60
+ status: string;
61
+ htmlLink: string;
62
+ attendees?: Array<{ email: string; displayName?: string; responseStatus?: string }>;
63
+ };
64
+ return {
65
+ events: (events as CalendarEvent[]).map((event) => ({
66
+ id: event.id,
67
+ title: event.summary,
68
+ description: event.description || null,
69
+ location: event.location || null,
70
+ start: event.start.dateTime || event.start.date,
71
+ end: event.end.dateTime || event.end.date,
72
+ isAllDay: !event.start.dateTime,
73
+ status: event.status,
74
+ url: event.htmlLink,
75
+ attendees: event.attendees?.map((
76
+ a: { email: string; displayName?: string; responseStatus?: string },
77
+ ) => ({
78
+ email: a.email,
79
+ name: a.displayName,
80
+ status: a.responseStatus,
81
+ })) || [],
82
+ })),
83
+ count: events.length,
84
+ message: todayOnly
85
+ ? `Found ${events.length} event(s) for today.`
86
+ : `Found ${events.length} event(s) in the next ${daysAhead} days.`,
87
+ };
88
+ } catch (error) {
89
+ if (error instanceof Error && error.message.includes("not connected")) {
90
+ return {
91
+ error: "Calendar not connected. Please connect your Google Calendar.",
92
+ connectUrl: "/api/auth/calendar",
93
+ };
94
+ }
95
+ throw error;
96
+ }
97
+ },
98
+ });