@shin1ohno/sage 0.7.9 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/dist/cli/mcp-handler.d.ts.map +1 -1
- package/dist/cli/mcp-handler.js +141 -987
- package/dist/cli/mcp-handler.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +30 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/update-validation.d.ts +52 -0
- package/dist/config/update-validation.d.ts.map +1 -0
- package/dist/config/update-validation.js +133 -0
- package/dist/config/update-validation.js.map +1 -0
- package/dist/config/validation.d.ts +130 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +53 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/index.js +443 -1632
- package/dist/index.js.map +1 -1
- package/dist/integrations/calendar-event-creator.d.ts +2 -3
- package/dist/integrations/calendar-event-creator.d.ts.map +1 -1
- package/dist/integrations/calendar-event-creator.js +3 -4
- package/dist/integrations/calendar-event-creator.js.map +1 -1
- package/dist/integrations/calendar-event-deleter.d.ts +2 -3
- package/dist/integrations/calendar-event-deleter.d.ts.map +1 -1
- package/dist/integrations/calendar-event-deleter.js +3 -4
- package/dist/integrations/calendar-event-deleter.js.map +1 -1
- package/dist/integrations/calendar-event-response.d.ts +4 -17
- package/dist/integrations/calendar-event-response.d.ts.map +1 -1
- package/dist/integrations/calendar-event-response.js +3 -4
- package/dist/integrations/calendar-event-response.js.map +1 -1
- package/dist/integrations/calendar-service.d.ts +6 -3
- package/dist/integrations/calendar-service.d.ts.map +1 -1
- package/dist/integrations/calendar-service.js +26 -4
- package/dist/integrations/calendar-service.js.map +1 -1
- package/dist/integrations/calendar-source-manager.d.ts +302 -0
- package/dist/integrations/calendar-source-manager.d.ts.map +1 -0
- package/dist/integrations/calendar-source-manager.js +862 -0
- package/dist/integrations/calendar-source-manager.js.map +1 -0
- package/dist/integrations/google-calendar-service.d.ts +176 -0
- package/dist/integrations/google-calendar-service.d.ts.map +1 -0
- package/dist/integrations/google-calendar-service.js +745 -0
- package/dist/integrations/google-calendar-service.js.map +1 -0
- package/dist/integrations/notion-mcp.d.ts +28 -3
- package/dist/integrations/notion-mcp.d.ts.map +1 -1
- package/dist/integrations/notion-mcp.js +21 -5
- package/dist/integrations/notion-mcp.js.map +1 -1
- package/dist/integrations/reminder-manager.d.ts.map +1 -1
- package/dist/integrations/reminder-manager.js +2 -0
- package/dist/integrations/reminder-manager.js.map +1 -1
- package/dist/oauth/google-oauth-handler.d.ts +149 -0
- package/dist/oauth/google-oauth-handler.d.ts.map +1 -0
- package/dist/oauth/google-oauth-handler.js +365 -0
- package/dist/oauth/google-oauth-handler.js.map +1 -0
- package/dist/services/container.d.ts +56 -0
- package/dist/services/container.d.ts.map +1 -0
- package/dist/services/container.js +76 -0
- package/dist/services/container.js.map +1 -0
- package/dist/tools/calendar/handlers.d.ts +186 -0
- package/dist/tools/calendar/handlers.d.ts.map +1 -0
- package/dist/tools/calendar/handlers.js +525 -0
- package/dist/tools/calendar/handlers.js.map +1 -0
- package/dist/tools/calendar/index.d.ts +11 -0
- package/dist/tools/calendar/index.d.ts.map +1 -0
- package/dist/tools/calendar/index.js +10 -0
- package/dist/tools/calendar/index.js.map +1 -0
- package/dist/tools/index.d.ts +23 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +24 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/integrations/handlers.d.ts +57 -0
- package/dist/tools/integrations/handlers.d.ts.map +1 -0
- package/dist/tools/integrations/handlers.js +159 -0
- package/dist/tools/integrations/handlers.js.map +1 -0
- package/dist/tools/integrations/index.d.ts +11 -0
- package/dist/tools/integrations/index.d.ts.map +1 -0
- package/dist/tools/integrations/index.js +10 -0
- package/dist/tools/integrations/index.js.map +1 -0
- package/dist/tools/registry.d.ts +8 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +10 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/reminders/handlers.d.ts +61 -0
- package/dist/tools/reminders/handlers.d.ts.map +1 -0
- package/dist/tools/reminders/handlers.js +148 -0
- package/dist/tools/reminders/handlers.js.map +1 -0
- package/dist/tools/reminders/index.d.ts +11 -0
- package/dist/tools/reminders/index.d.ts.map +1 -0
- package/dist/tools/reminders/index.js +10 -0
- package/dist/tools/reminders/index.js.map +1 -0
- package/dist/tools/setup/handlers.d.ts +81 -0
- package/dist/tools/setup/handlers.d.ts.map +1 -0
- package/dist/tools/setup/handlers.js +172 -0
- package/dist/tools/setup/handlers.js.map +1 -0
- package/dist/tools/setup/index.d.ts +11 -0
- package/dist/tools/setup/index.d.ts.map +1 -0
- package/dist/tools/setup/index.js +10 -0
- package/dist/tools/setup/index.js.map +1 -0
- package/dist/tools/tasks/handlers.d.ts +95 -0
- package/dist/tools/tasks/handlers.d.ts.map +1 -0
- package/dist/tools/tasks/handlers.js +197 -0
- package/dist/tools/tasks/handlers.js.map +1 -0
- package/dist/tools/tasks/index.d.ts +11 -0
- package/dist/tools/tasks/index.d.ts.map +1 -0
- package/dist/tools/tasks/index.js +10 -0
- package/dist/tools/tasks/index.js.map +1 -0
- package/dist/tools/types.d.ts +54 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +9 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/types/calendar.d.ts +41 -0
- package/dist/types/calendar.d.ts.map +1 -0
- package/dist/types/calendar.js +18 -0
- package/dist/types/calendar.js.map +1 -0
- package/dist/types/config.d.ts +15 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +21 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/google-calendar-types.d.ts +139 -0
- package/dist/types/google-calendar-types.d.ts.map +1 -0
- package/dist/types/google-calendar-types.js +46 -0
- package/dist/types/google-calendar-types.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/estimation.d.ts +34 -0
- package/dist/utils/estimation.d.ts.map +1 -1
- package/dist/utils/estimation.js +38 -1
- package/dist/utils/estimation.js.map +1 -1
- package/dist/utils/mcp-response.d.ts +89 -0
- package/dist/utils/mcp-response.d.ts.map +1 -0
- package/dist/utils/mcp-response.js +103 -0
- package/dist/utils/mcp-response.js.map +1 -0
- package/dist/utils/task-splitter.d.ts +65 -4
- package/dist/utils/task-splitter.d.ts.map +1 -1
- package/dist/utils/task-splitter.js +69 -5
- package/dist/utils/task-splitter.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +22 -4
- package/dist/version.js.map +1 -1
- package/package.json +4 -3
- package/dist/cli/http-server.d.ts +0 -74
- package/dist/cli/http-server.d.ts.map +0 -1
- package/dist/cli/http-server.js +0 -407
- package/dist/cli/http-server.js.map +0 -1
- package/dist/remote/remote-mcp-server.d.ts +0 -244
- package/dist/remote/remote-mcp-server.d.ts.map +0 -1
- package/dist/remote/remote-mcp-server.js +0 -507
- package/dist/remote/remote-mcp-server.js.map +0 -1
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar Source Manager
|
|
3
|
+
* Requirements: 9, 11
|
|
4
|
+
* Design: .claude/specs/google-calendar-api/design.md (CalendarSourceManager section)
|
|
5
|
+
*
|
|
6
|
+
* Manages multiple calendar sources (EventKit, Google Calendar) with automatic
|
|
7
|
+
* source selection, fallback handling, and unified MCP tool interface.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Calendar Source Manager
|
|
11
|
+
*
|
|
12
|
+
* Manages multiple calendar sources (EventKit, Google Calendar) with:
|
|
13
|
+
* - Automatic source detection and selection
|
|
14
|
+
* - Unified event operations across sources
|
|
15
|
+
* - Fallback handling for source failures
|
|
16
|
+
* - Event deduplication across sources
|
|
17
|
+
*/
|
|
18
|
+
export class CalendarSourceManager {
|
|
19
|
+
calendarService;
|
|
20
|
+
googleCalendarService;
|
|
21
|
+
config;
|
|
22
|
+
/**
|
|
23
|
+
* Constructor
|
|
24
|
+
*
|
|
25
|
+
* @param options - Optional services and config
|
|
26
|
+
*/
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.calendarService = options?.calendarService;
|
|
29
|
+
this.googleCalendarService = options?.googleCalendarService;
|
|
30
|
+
this.config = options?.config;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detect available calendar sources
|
|
34
|
+
* Requirement: 9 (Platform detection and source availability)
|
|
35
|
+
*
|
|
36
|
+
* Uses detectPlatform() pattern from CalendarService to determine which
|
|
37
|
+
* calendar sources are available on the current platform.
|
|
38
|
+
*
|
|
39
|
+
* @returns Object indicating which sources are available
|
|
40
|
+
*/
|
|
41
|
+
async detectAvailableSources() {
|
|
42
|
+
const result = {
|
|
43
|
+
eventkit: false,
|
|
44
|
+
google: false,
|
|
45
|
+
};
|
|
46
|
+
// Check EventKit availability (macOS only)
|
|
47
|
+
if (this.calendarService) {
|
|
48
|
+
try {
|
|
49
|
+
const platform = await this.calendarService.detectPlatform();
|
|
50
|
+
result.eventkit = platform.platform === 'macos';
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
result.eventkit = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// No CalendarService instance, check platform directly
|
|
58
|
+
if (typeof process !== 'undefined' && process.platform === 'darwin') {
|
|
59
|
+
result.eventkit = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check Google Calendar availability (OAuth configured)
|
|
63
|
+
if (this.googleCalendarService) {
|
|
64
|
+
try {
|
|
65
|
+
result.google = await this.googleCalendarService.isAvailable();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
result.google = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Enable a calendar source
|
|
75
|
+
* Requirement: 11 (Calendar source management)
|
|
76
|
+
*
|
|
77
|
+
* Updates config to enable the specified source and validates that
|
|
78
|
+
* at least one source remains enabled.
|
|
79
|
+
*
|
|
80
|
+
* @param source - Calendar source to enable ('eventkit' | 'google')
|
|
81
|
+
* @throws Error if config is not available
|
|
82
|
+
*/
|
|
83
|
+
async enableSource(source) {
|
|
84
|
+
if (!this.config) {
|
|
85
|
+
throw new Error('Config not available. Cannot enable source.');
|
|
86
|
+
}
|
|
87
|
+
// Ensure calendar.sources exists
|
|
88
|
+
if (!this.config.calendar.sources) {
|
|
89
|
+
this.config.calendar.sources = {
|
|
90
|
+
eventkit: { enabled: false },
|
|
91
|
+
google: {
|
|
92
|
+
enabled: false,
|
|
93
|
+
defaultCalendar: 'primary',
|
|
94
|
+
excludedCalendars: [],
|
|
95
|
+
syncInterval: 300,
|
|
96
|
+
enableNotifications: true,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Enable the specified source
|
|
101
|
+
if (source === 'eventkit') {
|
|
102
|
+
this.config.calendar.sources.eventkit.enabled = true;
|
|
103
|
+
}
|
|
104
|
+
else if (source === 'google') {
|
|
105
|
+
this.config.calendar.sources.google.enabled = true;
|
|
106
|
+
}
|
|
107
|
+
// Note: Config persistence is handled by caller (ConfigManager.save())
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Disable a calendar source
|
|
111
|
+
* Requirement: 11 (Calendar source management)
|
|
112
|
+
*
|
|
113
|
+
* Updates config to disable the specified source and validates that
|
|
114
|
+
* at least one source remains enabled.
|
|
115
|
+
*
|
|
116
|
+
* @param source - Calendar source to disable ('eventkit' | 'google')
|
|
117
|
+
* @throws Error if disabling would leave no sources enabled
|
|
118
|
+
*/
|
|
119
|
+
async disableSource(source) {
|
|
120
|
+
if (!this.config) {
|
|
121
|
+
throw new Error('Config not available. Cannot disable source.');
|
|
122
|
+
}
|
|
123
|
+
// Ensure calendar.sources exists
|
|
124
|
+
if (!this.config.calendar.sources) {
|
|
125
|
+
throw new Error('Calendar sources not configured.');
|
|
126
|
+
}
|
|
127
|
+
// Check if at least one source will remain enabled
|
|
128
|
+
const sources = this.config.calendar.sources;
|
|
129
|
+
const eventkitEnabled = source === 'eventkit' ? false : sources.eventkit.enabled;
|
|
130
|
+
const googleEnabled = source === 'google' ? false : sources.google.enabled;
|
|
131
|
+
if (!eventkitEnabled && !googleEnabled) {
|
|
132
|
+
throw new Error('Cannot disable source: at least one calendar source must be enabled.');
|
|
133
|
+
}
|
|
134
|
+
// Disable the specified source
|
|
135
|
+
if (source === 'eventkit') {
|
|
136
|
+
this.config.calendar.sources.eventkit.enabled = false;
|
|
137
|
+
}
|
|
138
|
+
else if (source === 'google') {
|
|
139
|
+
this.config.calendar.sources.google.enabled = false;
|
|
140
|
+
}
|
|
141
|
+
// Note: Config persistence is handled by caller (ConfigManager.save())
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get enabled calendar sources
|
|
145
|
+
* Requirement: 9 (Source configuration reading)
|
|
146
|
+
*
|
|
147
|
+
* Reads from config.calendar.sources to determine which sources are enabled.
|
|
148
|
+
*
|
|
149
|
+
* @returns Array of enabled source names
|
|
150
|
+
*/
|
|
151
|
+
getEnabledSources() {
|
|
152
|
+
if (!this.config?.calendar?.sources) {
|
|
153
|
+
// Default: EventKit on macOS, Google Calendar elsewhere
|
|
154
|
+
const isMacOS = typeof process !== 'undefined' && process.platform === 'darwin';
|
|
155
|
+
return isMacOS ? ['eventkit'] : ['google'];
|
|
156
|
+
}
|
|
157
|
+
const sources = [];
|
|
158
|
+
const calendarSources = this.config.calendar.sources;
|
|
159
|
+
if (calendarSources.eventkit.enabled) {
|
|
160
|
+
sources.push('eventkit');
|
|
161
|
+
}
|
|
162
|
+
if (calendarSources.google.enabled) {
|
|
163
|
+
sources.push('google');
|
|
164
|
+
}
|
|
165
|
+
return sources;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get events from enabled calendar sources
|
|
169
|
+
* Requirement: 7, 10, 11 (Multi-source event retrieval with deduplication and fallback)
|
|
170
|
+
*
|
|
171
|
+
* Fetches events from all enabled sources with fallback handling and deduplication.
|
|
172
|
+
* - Task 17a: Basic parallel fetching
|
|
173
|
+
* - Task 17b: Deduplication and fallback logic
|
|
174
|
+
*
|
|
175
|
+
* Fallback behavior: If one source fails, continues with other sources.
|
|
176
|
+
* Only throws if ALL sources fail.
|
|
177
|
+
*
|
|
178
|
+
* @param startDate - Start date (ISO 8601)
|
|
179
|
+
* @param endDate - End date (ISO 8601)
|
|
180
|
+
* @param calendarId - Optional calendar ID filter
|
|
181
|
+
* @returns Array of deduplicated calendar events from all enabled sources
|
|
182
|
+
*/
|
|
183
|
+
async getEvents(startDate, endDate, calendarId) {
|
|
184
|
+
const enabledSources = this.getEnabledSources();
|
|
185
|
+
// Check if at least one source is enabled
|
|
186
|
+
if (enabledSources.length === 0) {
|
|
187
|
+
throw new Error('No calendar sources are enabled');
|
|
188
|
+
}
|
|
189
|
+
const allEvents = [];
|
|
190
|
+
const errors = [];
|
|
191
|
+
// Try EventKit with fallback handling
|
|
192
|
+
if (enabledSources.includes('eventkit') && this.calendarService) {
|
|
193
|
+
try {
|
|
194
|
+
const eventkitResponse = await this.calendarService.listEvents({
|
|
195
|
+
startDate,
|
|
196
|
+
endDate,
|
|
197
|
+
calendarName: calendarId,
|
|
198
|
+
});
|
|
199
|
+
allEvents.push(...eventkitResponse.events);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.error('EventKit failed:', error);
|
|
203
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Try Google Calendar with fallback handling
|
|
207
|
+
if (enabledSources.includes('google') && this.googleCalendarService) {
|
|
208
|
+
try {
|
|
209
|
+
const googleEvents = await this.googleCalendarService.listEvents({
|
|
210
|
+
startDate,
|
|
211
|
+
endDate,
|
|
212
|
+
calendarId,
|
|
213
|
+
});
|
|
214
|
+
allEvents.push(...googleEvents);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.error('Google Calendar failed:', error);
|
|
218
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// If all sources failed, throw error
|
|
222
|
+
if (allEvents.length === 0 && errors.length > 0) {
|
|
223
|
+
throw new Error(`All calendar sources failed: ${errors.map((e) => e.message).join(', ')}`);
|
|
224
|
+
}
|
|
225
|
+
// Deduplicate events across sources
|
|
226
|
+
const uniqueEvents = this.deduplicateEvents(allEvents);
|
|
227
|
+
return uniqueEvents;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Check if two events are duplicates
|
|
231
|
+
* Requirement: 10 (Event deduplication)
|
|
232
|
+
*
|
|
233
|
+
* Uses two methods to detect duplicates:
|
|
234
|
+
* 1. iCalUID comparison (most reliable, RFC 5545 standard)
|
|
235
|
+
* 2. Title + time matching (fallback for events without iCalUID)
|
|
236
|
+
*
|
|
237
|
+
* Note: iCalUID support will be added in Task 24. Until then, this uses
|
|
238
|
+
* type assertions to access the optional iCalUID property.
|
|
239
|
+
*
|
|
240
|
+
* @param event1 - First event
|
|
241
|
+
* @param event2 - Second event
|
|
242
|
+
* @returns True if events are duplicates
|
|
243
|
+
*/
|
|
244
|
+
areEventsDuplicate(event1, event2) {
|
|
245
|
+
// Method 1: iCalUID comparison (most reliable)
|
|
246
|
+
// Note: iCalUID will be added to CalendarEvent interface in Task 24
|
|
247
|
+
const iCalUID1 = event1.iCalUID;
|
|
248
|
+
const iCalUID2 = event2.iCalUID;
|
|
249
|
+
if (iCalUID1 && iCalUID2 && iCalUID1 === iCalUID2) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
// Method 2: Title + time matching (fallback)
|
|
253
|
+
const titleMatch = event1.title.toLowerCase() === event2.title.toLowerCase();
|
|
254
|
+
const startMatch = event1.start === event2.start;
|
|
255
|
+
const endMatch = event1.end === event2.end;
|
|
256
|
+
return titleMatch && startMatch && endMatch;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Deduplicate events array
|
|
260
|
+
* Requirement: 10 (Event deduplication)
|
|
261
|
+
*
|
|
262
|
+
* Removes duplicate events keeping the first occurrence.
|
|
263
|
+
* Uses areEventsDuplicate() for comparison.
|
|
264
|
+
*
|
|
265
|
+
* @param events - Array of events to deduplicate
|
|
266
|
+
* @returns Array of unique events
|
|
267
|
+
*/
|
|
268
|
+
deduplicateEvents(events) {
|
|
269
|
+
return events.filter((event, index) => {
|
|
270
|
+
return !events
|
|
271
|
+
.slice(0, index)
|
|
272
|
+
.some((prevEvent) => this.areEventsDuplicate(event, prevEvent));
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Create event in preferred calendar source
|
|
277
|
+
* Requirement: 3, 10, 11 (Multi-source event creation with routing and fallback)
|
|
278
|
+
*
|
|
279
|
+
* Creates an event in the specified source with fallback handling:
|
|
280
|
+
* 1. If preferredSource is specified, try that source first
|
|
281
|
+
* 2. If preferred source fails or not specified, try other enabled sources
|
|
282
|
+
* 3. Throws error if all sources fail
|
|
283
|
+
*
|
|
284
|
+
* Note: EventKit does not support event creation in current implementation,
|
|
285
|
+
* so only Google Calendar is supported for event creation.
|
|
286
|
+
*
|
|
287
|
+
* @param request - Event creation request
|
|
288
|
+
* @param preferredSource - Preferred source ('eventkit' | 'google')
|
|
289
|
+
* @returns Created calendar event
|
|
290
|
+
* @throws Error if all sources fail or no sources are enabled
|
|
291
|
+
*/
|
|
292
|
+
async createEvent(request, preferredSource) {
|
|
293
|
+
const enabledSources = this.getEnabledSources();
|
|
294
|
+
// Check if any sources are enabled
|
|
295
|
+
if (enabledSources.length === 0) {
|
|
296
|
+
throw new Error('No calendar sources enabled. Please enable at least one source.');
|
|
297
|
+
}
|
|
298
|
+
// Determine source order to try
|
|
299
|
+
let sourcesToTry;
|
|
300
|
+
if (preferredSource && enabledSources.includes(preferredSource)) {
|
|
301
|
+
// Try preferred source first, then other enabled sources
|
|
302
|
+
sourcesToTry = [
|
|
303
|
+
preferredSource,
|
|
304
|
+
...enabledSources.filter((s) => s !== preferredSource),
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Try all enabled sources in order
|
|
309
|
+
sourcesToTry = enabledSources;
|
|
310
|
+
}
|
|
311
|
+
const errors = [];
|
|
312
|
+
// Try each source in order
|
|
313
|
+
for (const source of sourcesToTry) {
|
|
314
|
+
try {
|
|
315
|
+
if (source === 'eventkit' && this.calendarService) {
|
|
316
|
+
// Note: EventKit (CalendarService) does not support createEvent in current implementation
|
|
317
|
+
// Skip EventKit and continue to next source
|
|
318
|
+
throw new Error('EventKit does not support event creation in current implementation');
|
|
319
|
+
}
|
|
320
|
+
if (source === 'google' && this.googleCalendarService) {
|
|
321
|
+
// Try Google Calendar
|
|
322
|
+
const event = await this.googleCalendarService.createEvent(request);
|
|
323
|
+
console.log(`Successfully created event in Google Calendar: ${event.id}`);
|
|
324
|
+
return event;
|
|
325
|
+
}
|
|
326
|
+
// Source is enabled but service is not available
|
|
327
|
+
throw new Error(`${source} service is not available (not initialized)`);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
331
|
+
errors.push({ source, error: err });
|
|
332
|
+
console.error(`Failed to create event in ${source}:`, err.message);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// All sources failed
|
|
336
|
+
const errorMessages = errors
|
|
337
|
+
.map((e) => `${e.source}: ${e.error.message}`)
|
|
338
|
+
.join('; ');
|
|
339
|
+
throw new Error(`Failed to create event in all sources. Errors: ${errorMessages}`);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Delete event from calendar source
|
|
343
|
+
* Requirement: 5, 10, 11 (Multi-source event deletion with routing and error handling)
|
|
344
|
+
*
|
|
345
|
+
* Deletes an event from the specified source, or attempts deletion
|
|
346
|
+
* from all sources if source is not specified.
|
|
347
|
+
*
|
|
348
|
+
* If source specified:
|
|
349
|
+
* - Deletes from that source only
|
|
350
|
+
* - Throws error if source is not enabled or service not available
|
|
351
|
+
*
|
|
352
|
+
* If source not specified:
|
|
353
|
+
* - Attempts to delete from ALL enabled sources
|
|
354
|
+
* - Event may exist in both EventKit and Google Calendar (duplicate)
|
|
355
|
+
* - Uses Promise.allSettled() to try all sources
|
|
356
|
+
* - 404 errors (event not found) do not cause failure
|
|
357
|
+
* - Only throws if ALL attempts fail with non-404 errors
|
|
358
|
+
*
|
|
359
|
+
* @param eventId - Event ID to delete
|
|
360
|
+
* @param source - Optional source ('eventkit' | 'google')
|
|
361
|
+
* @throws Error if source specified and not available, or all sources fail with non-404 errors
|
|
362
|
+
*/
|
|
363
|
+
async deleteEvent(eventId, source) {
|
|
364
|
+
const enabledSources = this.getEnabledSources();
|
|
365
|
+
// If source specified, delete from that source only
|
|
366
|
+
if (source) {
|
|
367
|
+
if (!enabledSources.includes(source)) {
|
|
368
|
+
throw new Error(`Source ${source} is not enabled`);
|
|
369
|
+
}
|
|
370
|
+
if (source === 'eventkit' && this.calendarService) {
|
|
371
|
+
// Note: EventKit (CalendarService) does not support deleteEvent in current implementation
|
|
372
|
+
throw new Error('EventKit does not support event deletion in current implementation');
|
|
373
|
+
}
|
|
374
|
+
if (source === 'google' && this.googleCalendarService) {
|
|
375
|
+
await this.googleCalendarService.deleteEvent(eventId);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`Service for ${source} is not available`);
|
|
379
|
+
}
|
|
380
|
+
// No source specified - try all enabled sources
|
|
381
|
+
const promises = [];
|
|
382
|
+
if (enabledSources.includes('eventkit') && this.calendarService) {
|
|
383
|
+
// Note: EventKit does not support deleteEvent yet
|
|
384
|
+
// Skip EventKit for now (no-op)
|
|
385
|
+
promises.push(Promise.resolve().then(() => {
|
|
386
|
+
// EventKit deletion not supported
|
|
387
|
+
console.log('EventKit event deletion not supported, skipping...');
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
if (enabledSources.includes('google') && this.googleCalendarService) {
|
|
391
|
+
promises.push(this.googleCalendarService
|
|
392
|
+
.deleteEvent(eventId)
|
|
393
|
+
.catch((error) => ({ source: 'google', error })));
|
|
394
|
+
}
|
|
395
|
+
// If no promises to execute, nothing to delete
|
|
396
|
+
if (promises.length === 0) {
|
|
397
|
+
throw new Error('No calendar sources available for deletion');
|
|
398
|
+
}
|
|
399
|
+
const results = await Promise.allSettled(promises);
|
|
400
|
+
// Check if at least one deletion succeeded or got 404
|
|
401
|
+
let hasSuccess = false;
|
|
402
|
+
const errors = [];
|
|
403
|
+
for (const result of results) {
|
|
404
|
+
if (result.status === 'fulfilled') {
|
|
405
|
+
// Check if fulfilled value is an error object or void
|
|
406
|
+
const value = result.value;
|
|
407
|
+
if (value && typeof value === 'object' && 'error' in value) {
|
|
408
|
+
// This is an error from .catch()
|
|
409
|
+
const errorResult = value;
|
|
410
|
+
const message = errorResult.error.message.toLowerCase();
|
|
411
|
+
if (message.includes('not found') || message.includes('404')) {
|
|
412
|
+
// 404 is OK - event already deleted
|
|
413
|
+
hasSuccess = true;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Other error - collect it
|
|
417
|
+
errors.push(errorResult);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Successful deletion
|
|
422
|
+
hasSuccess = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// If no success and we have errors, throw
|
|
427
|
+
if (!hasSuccess && errors.length > 0) {
|
|
428
|
+
const errorMessages = errors
|
|
429
|
+
.map((e) => `${e.source}: ${e.error.message}`)
|
|
430
|
+
.join('; ');
|
|
431
|
+
throw new Error(`Failed to delete event from all sources. Errors: ${errorMessages}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Find available time slots considering all enabled sources
|
|
436
|
+
* Requirement: 7 (Multi-source slot detection)
|
|
437
|
+
* Task 20a: Basic filtering implementation
|
|
438
|
+
* Task 20b: Suitability calculation integration
|
|
439
|
+
*
|
|
440
|
+
* Fetches events from all enabled sources, merges and deduplicates,
|
|
441
|
+
* then calculates available slots based on working hours and preferences.
|
|
442
|
+
* Applies suitability scoring and sorts by suitability.
|
|
443
|
+
*
|
|
444
|
+
* @param request - Slot search request
|
|
445
|
+
* @returns Array of available time slots sorted by suitability
|
|
446
|
+
*/
|
|
447
|
+
async findAvailableSlots(request) {
|
|
448
|
+
// Get all events from enabled sources (already merged/deduplicated by getEvents)
|
|
449
|
+
const events = await this.getEvents(request.startDate, request.endDate);
|
|
450
|
+
// Sort events by start time
|
|
451
|
+
const sortedEvents = events.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
452
|
+
// Get working hours from config or use request.workingHours
|
|
453
|
+
const workingHours = request.workingHours || this.getDefaultWorkingHours();
|
|
454
|
+
// Get duration parameters
|
|
455
|
+
const minDuration = request.minDurationMinutes || 25;
|
|
456
|
+
const maxDuration = request.maxDurationMinutes || 480; // 8 hours
|
|
457
|
+
// Generate slots for each day in the date range
|
|
458
|
+
const slots = [];
|
|
459
|
+
const startDate = new Date(request.startDate);
|
|
460
|
+
const endDate = new Date(request.endDate);
|
|
461
|
+
// Iterate through each day in the range
|
|
462
|
+
for (let currentDate = new Date(startDate); currentDate <= endDate; currentDate.setDate(currentDate.getDate() + 1)) {
|
|
463
|
+
const daySlots = this.findDaySlots(currentDate, sortedEvents, workingHours, minDuration, maxDuration);
|
|
464
|
+
slots.push(...daySlots);
|
|
465
|
+
}
|
|
466
|
+
// Apply suitability scoring to all slots
|
|
467
|
+
const slotsWithSuitability = this.calculateSuitabilityForSlots(slots);
|
|
468
|
+
// Sort by suitability (excellent > good > acceptable)
|
|
469
|
+
// Then by start time as secondary sort
|
|
470
|
+
const sortedSlots = slotsWithSuitability.sort((a, b) => {
|
|
471
|
+
const suitabilityOrder = {
|
|
472
|
+
excellent: 0,
|
|
473
|
+
good: 1,
|
|
474
|
+
acceptable: 2,
|
|
475
|
+
};
|
|
476
|
+
const suitabilityDiff = suitabilityOrder[a.suitability] - suitabilityOrder[b.suitability];
|
|
477
|
+
if (suitabilityDiff !== 0) {
|
|
478
|
+
return suitabilityDiff;
|
|
479
|
+
}
|
|
480
|
+
// Secondary sort by start time
|
|
481
|
+
return new Date(a.start).getTime() - new Date(b.start).getTime();
|
|
482
|
+
});
|
|
483
|
+
return sortedSlots;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Get default working hours
|
|
487
|
+
* Requirement: 7
|
|
488
|
+
*
|
|
489
|
+
* Returns working hours from config or default (09:00 - 18:00)
|
|
490
|
+
*
|
|
491
|
+
* @returns Working hours configuration
|
|
492
|
+
*/
|
|
493
|
+
getDefaultWorkingHours() {
|
|
494
|
+
// Check if config has working hours defined
|
|
495
|
+
if (this.config?.calendar?.sources?.google?.enableNotifications !== undefined) {
|
|
496
|
+
// Future: config.workingHours could be added here
|
|
497
|
+
// For now, return default
|
|
498
|
+
}
|
|
499
|
+
return { start: '09:00', end: '18:00' };
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Calculate suitability for all slots
|
|
503
|
+
* Requirement: 7 (Task 20b)
|
|
504
|
+
*
|
|
505
|
+
* Applies suitability scoring to slots based on:
|
|
506
|
+
* - Day type (deep work vs meeting heavy) from config
|
|
507
|
+
* - Time of day (morning slots preferred)
|
|
508
|
+
* - Slot duration (longer slots better for deep work)
|
|
509
|
+
*
|
|
510
|
+
* @param slots - Array of slots to score
|
|
511
|
+
* @returns Array of slots with suitability applied
|
|
512
|
+
*/
|
|
513
|
+
calculateSuitabilityForSlots(slots) {
|
|
514
|
+
// Get working cadence config (deep work days, meeting heavy days)
|
|
515
|
+
const deepWorkDays = this.config?.calendar?.deepWorkDays || [];
|
|
516
|
+
const meetingHeavyDays = this.config?.calendar?.meetingHeavyDays || [];
|
|
517
|
+
return slots.map((slot) => {
|
|
518
|
+
const slotDate = new Date(slot.start);
|
|
519
|
+
const dayName = slotDate.toLocaleDateString('en-US', { weekday: 'long' });
|
|
520
|
+
const hour = slotDate.getHours();
|
|
521
|
+
let suitability = 'good';
|
|
522
|
+
let dayType = 'normal';
|
|
523
|
+
let reason = slot.reason;
|
|
524
|
+
// Determine day type and base suitability
|
|
525
|
+
if (deepWorkDays.includes(dayName)) {
|
|
526
|
+
dayType = 'deep-work';
|
|
527
|
+
// Deep work days are excellent for focused work
|
|
528
|
+
suitability = 'excellent';
|
|
529
|
+
reason = `${dayName} is a deep work day - excellent for focused tasks`;
|
|
530
|
+
}
|
|
531
|
+
else if (meetingHeavyDays.includes(dayName)) {
|
|
532
|
+
dayType = 'meeting-heavy';
|
|
533
|
+
// Meeting heavy days are acceptable (not ideal but usable)
|
|
534
|
+
suitability = 'acceptable';
|
|
535
|
+
reason = `${dayName} is a meeting-heavy day - consider rescheduling for deep work`;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
dayType = 'normal';
|
|
539
|
+
// Normal days are good
|
|
540
|
+
suitability = 'good';
|
|
541
|
+
}
|
|
542
|
+
// Adjust suitability based on time of day and duration
|
|
543
|
+
// Morning slots (before 12:00) are generally better for deep work
|
|
544
|
+
if (hour < 12 && slot.durationMinutes >= 60) {
|
|
545
|
+
if (suitability === 'good') {
|
|
546
|
+
suitability = 'excellent';
|
|
547
|
+
reason = `Morning slot with ${slot.durationMinutes} minutes - ideal for deep work`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Very short slots (<25 min) are less suitable
|
|
551
|
+
if (slot.durationMinutes < 25) {
|
|
552
|
+
if (suitability === 'excellent') {
|
|
553
|
+
suitability = 'good';
|
|
554
|
+
}
|
|
555
|
+
else if (suitability === 'good') {
|
|
556
|
+
suitability = 'acceptable';
|
|
557
|
+
}
|
|
558
|
+
reason = `Short slot (${slot.durationMinutes} minutes) - best for quick tasks`;
|
|
559
|
+
}
|
|
560
|
+
// Very long slots (>4 hours) on deep work days are excellent
|
|
561
|
+
if (slot.durationMinutes > 240 && dayType === 'deep-work') {
|
|
562
|
+
suitability = 'excellent';
|
|
563
|
+
reason = `Extended ${slot.durationMinutes} minute slot on ${dayName} - perfect for deep work`;
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
...slot,
|
|
567
|
+
suitability,
|
|
568
|
+
dayType,
|
|
569
|
+
reason,
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Find available slots for a single day
|
|
575
|
+
* Requirement: 7
|
|
576
|
+
*
|
|
577
|
+
* Calculates gaps between events within working hours for a specific day.
|
|
578
|
+
*
|
|
579
|
+
* @param date - Date to find slots for
|
|
580
|
+
* @param events - All events (sorted by start time)
|
|
581
|
+
* @param workingHours - Working hours configuration
|
|
582
|
+
* @param minDuration - Minimum slot duration in minutes
|
|
583
|
+
* @param maxDuration - Maximum slot duration in minutes
|
|
584
|
+
* @returns Array of available slots for this day
|
|
585
|
+
*/
|
|
586
|
+
findDaySlots(date, events, workingHours, minDuration, maxDuration) {
|
|
587
|
+
const slots = [];
|
|
588
|
+
// Parse working hours
|
|
589
|
+
const [startHour, startMin] = workingHours.start.split(':').map(Number);
|
|
590
|
+
const [endHour, endMin] = workingHours.end.split(':').map(Number);
|
|
591
|
+
// Create working hours boundaries for this day
|
|
592
|
+
const workStart = new Date(date);
|
|
593
|
+
workStart.setHours(startHour, startMin, 0, 0);
|
|
594
|
+
const workEnd = new Date(date);
|
|
595
|
+
workEnd.setHours(endHour, endMin, 0, 0);
|
|
596
|
+
// Filter events for this day (events that overlap with working hours)
|
|
597
|
+
const dayEvents = events
|
|
598
|
+
.filter((e) => {
|
|
599
|
+
const eventStart = new Date(e.start);
|
|
600
|
+
const eventEnd = new Date(e.end);
|
|
601
|
+
// Event must overlap with this day's working hours
|
|
602
|
+
return eventStart < workEnd && eventEnd > workStart;
|
|
603
|
+
})
|
|
604
|
+
.filter((e) => !e.isAllDay) // Exclude all-day events
|
|
605
|
+
.map((e) => ({
|
|
606
|
+
start: new Date(e.start),
|
|
607
|
+
end: new Date(e.end),
|
|
608
|
+
title: e.title,
|
|
609
|
+
}))
|
|
610
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
611
|
+
// If all-day event exists, no slots available
|
|
612
|
+
const hasAllDayEvent = events.some((e) => {
|
|
613
|
+
const eventStart = new Date(e.start);
|
|
614
|
+
const eventEnd = new Date(e.end);
|
|
615
|
+
return e.isAllDay && eventStart <= workStart && eventEnd >= workEnd;
|
|
616
|
+
});
|
|
617
|
+
if (hasAllDayEvent) {
|
|
618
|
+
return [];
|
|
619
|
+
}
|
|
620
|
+
// Find gaps between events
|
|
621
|
+
let currentTime = workStart;
|
|
622
|
+
for (const event of dayEvents) {
|
|
623
|
+
// Calculate gap before this event
|
|
624
|
+
const gapStart = currentTime;
|
|
625
|
+
const gapEnd = event.start < workStart
|
|
626
|
+
? workStart
|
|
627
|
+
: event.start > workEnd
|
|
628
|
+
? workEnd
|
|
629
|
+
: event.start;
|
|
630
|
+
const gapMinutes = (gapEnd.getTime() - gapStart.getTime()) / (1000 * 60);
|
|
631
|
+
// Add slot if gap meets duration requirements
|
|
632
|
+
if (gapMinutes >= minDuration && gapMinutes <= maxDuration) {
|
|
633
|
+
slots.push({
|
|
634
|
+
start: gapStart.toISOString(),
|
|
635
|
+
end: gapEnd.toISOString(),
|
|
636
|
+
durationMinutes: Math.floor(gapMinutes),
|
|
637
|
+
// Note: suitability will be calculated in Task 20b
|
|
638
|
+
suitability: 'good',
|
|
639
|
+
reason: `${Math.floor(gapMinutes)}分の空き時間`,
|
|
640
|
+
conflicts: [],
|
|
641
|
+
dayType: 'normal',
|
|
642
|
+
source: 'eventkit', // Source doesn't matter for merged slots
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// Move current time to end of event
|
|
646
|
+
const eventEnd = event.end > workEnd ? workEnd : event.end;
|
|
647
|
+
currentTime = eventEnd > currentTime ? eventEnd : currentTime;
|
|
648
|
+
}
|
|
649
|
+
// Check remaining time after last event
|
|
650
|
+
const remainingMinutes = (workEnd.getTime() - currentTime.getTime()) / (1000 * 60);
|
|
651
|
+
if (remainingMinutes >= minDuration && remainingMinutes <= maxDuration) {
|
|
652
|
+
slots.push({
|
|
653
|
+
start: currentTime.toISOString(),
|
|
654
|
+
end: workEnd.toISOString(),
|
|
655
|
+
durationMinutes: Math.floor(remainingMinutes),
|
|
656
|
+
suitability: 'good',
|
|
657
|
+
reason: `${Math.floor(remainingMinutes)}分の空き時間`,
|
|
658
|
+
conflicts: [],
|
|
659
|
+
dayType: 'normal',
|
|
660
|
+
source: 'eventkit',
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return slots;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Sync calendars between sources
|
|
667
|
+
* Requirement: 8 (Calendar synchronization)
|
|
668
|
+
*
|
|
669
|
+
* Synchronizes events between EventKit and Google Calendar when both
|
|
670
|
+
* sources are enabled. Handles conflicts and provides sync results.
|
|
671
|
+
*
|
|
672
|
+
* Note: This is a stub implementation. Full sync logic can be added later.
|
|
673
|
+
* Currently, it validates that both sources are enabled and returns empty results.
|
|
674
|
+
*
|
|
675
|
+
* @returns Sync result with statistics and errors
|
|
676
|
+
* @throws Error if both sources are not enabled
|
|
677
|
+
*/
|
|
678
|
+
async syncCalendars() {
|
|
679
|
+
const enabledSources = this.getEnabledSources();
|
|
680
|
+
// Check if both sources are enabled
|
|
681
|
+
if (enabledSources.length < 2) {
|
|
682
|
+
throw new Error('Both EventKit and Google Calendar must be enabled for sync');
|
|
683
|
+
}
|
|
684
|
+
// Verify that both specific sources are enabled
|
|
685
|
+
const hasEventKit = enabledSources.includes('eventkit');
|
|
686
|
+
const hasGoogle = enabledSources.includes('google');
|
|
687
|
+
if (!hasEventKit || !hasGoogle) {
|
|
688
|
+
throw new Error('Both EventKit and Google Calendar must be enabled for sync');
|
|
689
|
+
}
|
|
690
|
+
// For now, return empty result
|
|
691
|
+
// Full sync implementation to be added in future
|
|
692
|
+
return {
|
|
693
|
+
success: true,
|
|
694
|
+
eventsAdded: 0,
|
|
695
|
+
eventsUpdated: 0,
|
|
696
|
+
eventsDeleted: 0,
|
|
697
|
+
conflicts: [],
|
|
698
|
+
errors: [],
|
|
699
|
+
timestamp: new Date().toISOString(),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Get sync status between calendar sources
|
|
704
|
+
* Requirement: 8 (Calendar synchronization)
|
|
705
|
+
*
|
|
706
|
+
* Returns the current sync status including last sync time,
|
|
707
|
+
* next scheduled sync, and source availability.
|
|
708
|
+
*
|
|
709
|
+
* Note: This is a stub implementation. lastSyncTime tracking will be added
|
|
710
|
+
* when full sync implementation is completed.
|
|
711
|
+
*
|
|
712
|
+
* @returns Sync status information
|
|
713
|
+
*/
|
|
714
|
+
async getSyncStatus() {
|
|
715
|
+
const enabledSources = this.getEnabledSources();
|
|
716
|
+
const isEnabled = enabledSources.length >= 2
|
|
717
|
+
&& enabledSources.includes('eventkit')
|
|
718
|
+
&& enabledSources.includes('google');
|
|
719
|
+
// Check EventKit availability
|
|
720
|
+
const eventkitAvailable = enabledSources.includes('eventkit') && !!this.calendarService;
|
|
721
|
+
// Check Google Calendar availability
|
|
722
|
+
const googleAvailable = enabledSources.includes('google') && !!this.googleCalendarService;
|
|
723
|
+
// Calculate next sync time based on config sync interval (default: 300 seconds = 5 minutes)
|
|
724
|
+
const syncInterval = this.config?.calendar?.sources?.google?.syncInterval || 300;
|
|
725
|
+
const nextSyncTime = isEnabled
|
|
726
|
+
? new Date(Date.now() + syncInterval * 1000).toISOString()
|
|
727
|
+
: undefined;
|
|
728
|
+
return {
|
|
729
|
+
isEnabled,
|
|
730
|
+
lastSyncTime: undefined, // TODO: Store last sync time when full sync is implemented
|
|
731
|
+
nextSyncTime,
|
|
732
|
+
sources: {
|
|
733
|
+
eventkit: { available: eventkitAvailable },
|
|
734
|
+
google: { available: googleAvailable },
|
|
735
|
+
},
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Health check for calendar sources
|
|
740
|
+
* Requirement: 10, 11 (Health check for both sources)
|
|
741
|
+
* Task 22: Implementation
|
|
742
|
+
*
|
|
743
|
+
* Checks availability and health of all calendar sources by calling
|
|
744
|
+
* their respective isAvailable() methods. Returns a status object
|
|
745
|
+
* indicating which sources are currently healthy and available.
|
|
746
|
+
*
|
|
747
|
+
* This method never throws errors - it catches all failures and returns
|
|
748
|
+
* false for sources that are unavailable or unhealthy.
|
|
749
|
+
*
|
|
750
|
+
* @returns Object indicating health status of each source
|
|
751
|
+
*/
|
|
752
|
+
async healthCheck() {
|
|
753
|
+
const checks = await Promise.all([
|
|
754
|
+
// EventKit health check
|
|
755
|
+
(async () => {
|
|
756
|
+
if (!this.calendarService) {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
return await this.calendarService.isAvailable();
|
|
761
|
+
}
|
|
762
|
+
catch (error) {
|
|
763
|
+
console.error('EventKit health check failed:', error);
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
})(),
|
|
767
|
+
// Google Calendar health check
|
|
768
|
+
(async () => {
|
|
769
|
+
if (!this.googleCalendarService) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
return await this.googleCalendarService.isAvailable();
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
console.error('Google Calendar health check failed:', error);
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
})(),
|
|
780
|
+
]);
|
|
781
|
+
return {
|
|
782
|
+
eventkit: checks[0],
|
|
783
|
+
google: checks[1],
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Respond to a calendar event
|
|
788
|
+
* Requirement: 6 (Event response/RSVP)
|
|
789
|
+
*
|
|
790
|
+
* Routes event response to the appropriate calendar source.
|
|
791
|
+
* Supports EventKit (via CalendarEventResponseService) and Google Calendar.
|
|
792
|
+
* If source is not specified, attempts to determine source by fetching
|
|
793
|
+
* the event from all enabled sources.
|
|
794
|
+
*
|
|
795
|
+
* @param eventId - Event ID
|
|
796
|
+
* @param response - Response type: 'accept', 'decline', or 'tentative'
|
|
797
|
+
* @param source - Optional source routing ('eventkit' or 'google')
|
|
798
|
+
* @param calendarId - Optional calendar ID (for Google Calendar)
|
|
799
|
+
* @returns Success status and message
|
|
800
|
+
* @throws Error if event not found or response fails
|
|
801
|
+
*/
|
|
802
|
+
async respondToEvent(eventId, response, source, calendarId) {
|
|
803
|
+
const enabledSources = this.getEnabledSources();
|
|
804
|
+
// If source specified, route to that source
|
|
805
|
+
if (source) {
|
|
806
|
+
if (!enabledSources.includes(source)) {
|
|
807
|
+
throw new Error(`Calendar source '${source}' is not enabled`);
|
|
808
|
+
}
|
|
809
|
+
if (source === 'google') {
|
|
810
|
+
if (!this.googleCalendarService) {
|
|
811
|
+
throw new Error('Google Calendar service not initialized');
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
// Convert response type: 'accept' -> 'accepted', etc.
|
|
815
|
+
const googleResponse = response === 'accept' ? 'accepted' :
|
|
816
|
+
response === 'decline' ? 'declined' : 'tentative';
|
|
817
|
+
await this.googleCalendarService.respondToEvent(eventId, googleResponse, calendarId);
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
message: `Successfully responded '${response}' to Google Calendar event`,
|
|
821
|
+
source: 'google',
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
throw new Error(`Failed to respond to Google Calendar event: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
else if (source === 'eventkit') {
|
|
829
|
+
// EventKit response is handled via CalendarEventResponseService
|
|
830
|
+
throw new Error('EventKit event responses should be handled via CalendarEventResponseService directly');
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// No source specified - try to find event in enabled sources
|
|
834
|
+
// For now, prefer Google Calendar if enabled, as EventKit is typically
|
|
835
|
+
// handled via CalendarEventResponseService
|
|
836
|
+
if (enabledSources.includes('google') && this.googleCalendarService) {
|
|
837
|
+
try {
|
|
838
|
+
const googleResponse = response === 'accept' ? 'accepted' :
|
|
839
|
+
response === 'decline' ? 'declined' : 'tentative';
|
|
840
|
+
await this.googleCalendarService.respondToEvent(eventId, googleResponse, calendarId);
|
|
841
|
+
return {
|
|
842
|
+
success: true,
|
|
843
|
+
message: `Successfully responded '${response}' to Google Calendar event`,
|
|
844
|
+
source: 'google',
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
// If Google Calendar fails and EventKit is enabled, suggest trying EventKit
|
|
849
|
+
if (enabledSources.includes('eventkit')) {
|
|
850
|
+
throw new Error(`Event not found in Google Calendar. If this is an EventKit event, please use CalendarEventResponseService. Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
851
|
+
}
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// If only EventKit is enabled
|
|
856
|
+
if (enabledSources.includes('eventkit')) {
|
|
857
|
+
throw new Error('EventKit event responses should be handled via CalendarEventResponseService directly');
|
|
858
|
+
}
|
|
859
|
+
throw new Error('No calendar sources enabled');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
//# sourceMappingURL=calendar-source-manager.js.map
|