m365-agent-cli 1.2.0

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 (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,544 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { Command } from 'commander';
3
+ import { AttachmentLinkSpecError, parseAttachLinkSpec } from '../lib/attach-link-spec.js';
4
+ import { AttachmentPathError, validateAttachmentPath } from '../lib/attachments.js';
5
+ import { resolveAuth } from '../lib/auth.js';
6
+ import { parseDay, parseTimeToDate, toLocalUnzonedISOString, toUTCISOString } from '../lib/dates.js';
7
+ import {
8
+ areRoomsFree,
9
+ createEvent,
10
+ type EmailAttachment,
11
+ getRooms,
12
+ type Recurrence,
13
+ type RecurrencePattern,
14
+ type RecurrenceRange,
15
+ type ReferenceAttachmentInput,
16
+ SENSITIVITY_MAP,
17
+ searchRooms
18
+ } from '../lib/ews-client.js';
19
+ import { lookupMimeType } from '../lib/mime-type.js';
20
+ import { checkReadOnly } from '../lib/utils.js';
21
+
22
+ function formatTime(dateStr: string): string {
23
+ const date = new Date(dateStr);
24
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
25
+ }
26
+
27
+ function formatDate(dateStr: string): string {
28
+ const date = new Date(dateStr);
29
+ return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
30
+ }
31
+
32
+ export const createEventCommand = new Command('create-event')
33
+ .description('Create a new calendar event')
34
+ .argument('<title>', 'Event title/subject')
35
+ .argument('[start]', 'Start time (e.g., 13:00, 1pm) - not needed for all-day events')
36
+ .argument('[end]', 'End time (e.g., 14:00, 2pm) - not needed for all-day events')
37
+ .option('--day <day>', 'Day for the event (today, tomorrow, monday-sunday, YYYY-MM-DD)', 'today')
38
+ .option('--timezone <timezone>', 'Timezone for the event (e.g., "Pacific Standard Time")')
39
+ .option('--description <text>', 'Event description/body')
40
+ .option('--attendees <emails>', 'Comma-separated list of attendee emails')
41
+ .option('--room <room>', 'Meeting room (use --list-rooms to see available)')
42
+ .option('--teams', 'Create as Teams meeting')
43
+ .option('--category <name>', 'Category label (repeatable)', (v, acc) => [...acc, v], [] as string[])
44
+ .option('--all-day', 'Create as an all-day event (no time slots)')
45
+ .option('--sensitivity <level>', 'Sensitivity: normal, personal, private, confidential')
46
+ .option('--list-rooms', 'List available meeting rooms')
47
+ .option('--find-room', 'Find an available room for the time slot')
48
+ .option('--repeat <type>', 'Recurrence: daily, weekly, monthly, yearly')
49
+ .option('--every <n>', 'Repeat every N days/weeks/months (default: 1)', '1')
50
+ .option('--days <days>', 'Days of week for weekly recurrence (mon,tue,wed,thu,fri,sat,sun)')
51
+ .option('--until <date>', 'End date for recurrence (YYYY-MM-DD)')
52
+ .option('--count <n>', 'Number of occurrences (alternative to --until)')
53
+ .option('--json', 'Output as JSON')
54
+ .option('--token <token>', 'Use a specific token')
55
+ .option('--identity <name>', 'Use a specific authentication identity (default: default)')
56
+ .option('--mailbox <email>', 'Create event in shared mailbox calendar')
57
+ .option('--attach <files>', 'Attach file(s), comma-separated paths (relative to cwd)')
58
+ .option(
59
+ '--attach-link <spec>',
60
+ 'Attach link: "Title|https://url" or bare https URL (repeatable)',
61
+ (v: string, prev: string[]) => [...prev, v],
62
+ [] as string[]
63
+ )
64
+ .action(
65
+ async (
66
+ title: string,
67
+ startTime: string | undefined,
68
+ endTime: string | undefined,
69
+ options: {
70
+ day: string;
71
+ timezone?: string;
72
+ description?: string;
73
+ attendees?: string;
74
+ room?: string;
75
+ teams?: boolean;
76
+ allDay?: boolean;
77
+ sensitivity?: string;
78
+ listRooms?: boolean;
79
+ findRoom?: boolean;
80
+ repeat?: string;
81
+ every?: string;
82
+ days?: string;
83
+ until?: string;
84
+ count?: string;
85
+ json?: boolean;
86
+ token?: string;
87
+ identity?: string;
88
+ mailbox?: string;
89
+ category?: string[];
90
+ attach?: string;
91
+ attachLink?: string[];
92
+ },
93
+ cmd: any
94
+ ) => {
95
+ checkReadOnly(cmd);
96
+ const authResult = await resolveAuth({
97
+ token: options.token,
98
+ identity: options.identity
99
+ });
100
+
101
+ if (!authResult.success) {
102
+ if (options.json) {
103
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
104
+ } else {
105
+ console.error(`Error: ${authResult.error}`);
106
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
107
+ }
108
+ process.exit(1);
109
+ }
110
+
111
+ // Handle --list-rooms
112
+ if (options.listRooms) {
113
+ console.log('\nFetching available meeting rooms...\n');
114
+
115
+ // Search with multiple queries to find more rooms
116
+ const allRooms = new Map<string, { Name: string; Address: string }>();
117
+ const queries = ['room', 'meeting', 'vergader', 'nv-', 'conference'];
118
+
119
+ for (const q of queries) {
120
+ const result = await searchRooms(authResult.token!, q);
121
+ if (result.ok && result.data) {
122
+ for (const room of result.data) {
123
+ if (!allRooms.has(room.Address)) {
124
+ allRooms.set(room.Address, room);
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ if (allRooms.size > 0) {
131
+ console.log('Available rooms:');
132
+ const sortedRooms = [...allRooms.values()].sort((a, b) => a.Name.localeCompare(b.Name));
133
+ for (const room of sortedRooms) {
134
+ console.log(` - ${room.Name} (${room.Address})`);
135
+ }
136
+ return;
137
+ }
138
+
139
+ // Fallback to REST API
140
+ const roomsResult = await getRooms(authResult.token!);
141
+ if (roomsResult.ok && roomsResult.data && roomsResult.data.length > 0) {
142
+ console.log('Available rooms:');
143
+ for (const room of roomsResult.data) {
144
+ console.log(` - ${room.Name} (${room.Address})`);
145
+ }
146
+ } else {
147
+ console.log("No meeting rooms found or you don't have access to room lists.");
148
+ console.log('You can still specify a room by email address with --room <email>');
149
+ }
150
+ return;
151
+ }
152
+
153
+ // Validate start/end times for non-all-day events
154
+ if (!options.allDay && (!startTime || !endTime)) {
155
+ if (options.json) {
156
+ console.log(JSON.stringify({ error: 'Start and end times are required for non-all-day events' }, null, 2));
157
+ } else {
158
+ console.error('Error: Start and end times are required for non-all-day events');
159
+ }
160
+ process.exit(1);
161
+ }
162
+
163
+ // Parse date and times
164
+ let baseDate: Date;
165
+ try {
166
+ baseDate = parseDay(options.day, { throwOnInvalid: true });
167
+ } catch (err) {
168
+ const message = err instanceof Error ? err.message : 'Invalid day value';
169
+ if (options.json) {
170
+ console.log(JSON.stringify({ error: message }, null, 2));
171
+ } else {
172
+ console.error(`Error: ${message}`);
173
+ }
174
+ process.exit(1);
175
+ }
176
+
177
+ let start: Date;
178
+ let end: Date;
179
+
180
+ if (options.allDay) {
181
+ // For all-day events, use midnight boundaries regardless of provided times
182
+ start = new Date(baseDate);
183
+ start.setHours(0, 0, 0, 0);
184
+ end = new Date(baseDate);
185
+ end.setHours(23, 59, 59, 999);
186
+ } else {
187
+ // For regular events, parse the provided times
188
+ try {
189
+ start = parseTimeToDate(startTime!, baseDate, { throwOnInvalid: true });
190
+ } catch (err) {
191
+ const message = err instanceof Error ? err.message : 'Invalid start time';
192
+ if (options.json) {
193
+ console.log(JSON.stringify({ error: message }, null, 2));
194
+ } else {
195
+ console.error(`Error: ${message}`);
196
+ }
197
+ process.exit(1);
198
+ }
199
+
200
+ try {
201
+ end = parseTimeToDate(endTime!, baseDate, { throwOnInvalid: true });
202
+ } catch (err) {
203
+ const message = err instanceof Error ? err.message : 'Invalid end time';
204
+ if (options.json) {
205
+ console.log(JSON.stringify({ error: message }, null, 2));
206
+ } else {
207
+ console.error(`Error: ${message}`);
208
+ }
209
+ process.exit(1);
210
+ }
211
+ }
212
+
213
+ // Parse attendees
214
+ const attendees: Array<{ email: string; name?: string; type?: 'Required' | 'Optional' | 'Resource' }> =
215
+ options.attendees ? options.attendees.split(',').map((e) => ({ email: e.trim() })) : [];
216
+
217
+ // Handle --find-room: find an available room
218
+ let roomEmail: string | undefined;
219
+ let roomName: string | undefined;
220
+
221
+ if (options.findRoom) {
222
+ console.log('Searching for available rooms...');
223
+
224
+ const roomsResult = await getRooms(authResult.token!);
225
+
226
+ if (!roomsResult.ok || !roomsResult.data || roomsResult.data.length === 0) {
227
+ console.error('Could not fetch room list.');
228
+ } else {
229
+ // Batch check all rooms in a single EWS request
230
+ const roomEmails = roomsResult.data.map((r) => r.Address);
231
+ const freeMap = await areRoomsFree(authResult.token!, roomEmails, start.toISOString(), end.toISOString());
232
+
233
+ for (const room of roomsResult.data) {
234
+ if (freeMap.get(room.Address)) {
235
+ roomEmail = room.Address;
236
+ roomName = room.Name;
237
+ console.log(`Found available room: ${room.Name}`);
238
+ break;
239
+ }
240
+ }
241
+
242
+ if (!roomEmail) {
243
+ console.log('No available rooms found for this time slot.');
244
+ }
245
+ }
246
+ } else if (options.room) {
247
+ // User specified a room - could be name or email
248
+ roomName = options.room;
249
+ // If it looks like an email, use it directly
250
+ if (options.room.includes('@')) {
251
+ roomEmail = options.room;
252
+ } else {
253
+ // Try to find the room by name - search for it
254
+ let roomsResult = await searchRooms(authResult.token!, options.room);
255
+ if (!roomsResult.ok || !roomsResult.data || roomsResult.data.length === 0) {
256
+ roomsResult = await getRooms(authResult.token!);
257
+ }
258
+
259
+ if (roomsResult.ok && roomsResult.data) {
260
+ const found = roomsResult.data.find((r) =>
261
+ options.room ? r.Name.toLowerCase().includes(options.room.toLowerCase()) : false
262
+ );
263
+ if (found) {
264
+ roomEmail = found.Address;
265
+ roomName = found.Name;
266
+ }
267
+ }
268
+ }
269
+ }
270
+
271
+ // Add room as attendee if found
272
+ if (roomEmail) {
273
+ attendees.push({ email: roomEmail, name: roomName, type: 'Resource' });
274
+ }
275
+
276
+ // Build recurrence if specified
277
+ let recurrence: Recurrence | undefined;
278
+ if (options.repeat) {
279
+ const dayMap: Record<string, string> = {
280
+ mon: 'Monday',
281
+ monday: 'Monday',
282
+ tue: 'Tuesday',
283
+ tuesday: 'Tuesday',
284
+ wed: 'Wednesday',
285
+ wednesday: 'Wednesday',
286
+ thu: 'Thursday',
287
+ thursday: 'Thursday',
288
+ fri: 'Friday',
289
+ friday: 'Friday',
290
+ sat: 'Saturday',
291
+ saturday: 'Saturday',
292
+ sun: 'Sunday',
293
+ sunday: 'Sunday'
294
+ };
295
+
296
+ const patternTypeMap: Record<string, RecurrencePattern['Type']> = {
297
+ daily: 'Daily',
298
+ weekly: 'Weekly',
299
+ monthly: 'AbsoluteMonthly',
300
+ yearly: 'AbsoluteYearly'
301
+ };
302
+
303
+ const patternType = patternTypeMap[options.repeat.toLowerCase()];
304
+ if (!patternType) {
305
+ console.error(`Invalid repeat type: ${options.repeat}`);
306
+ console.error('Valid options: daily, weekly, monthly, yearly');
307
+ process.exit(1);
308
+ }
309
+
310
+ const pattern: RecurrencePattern = {
311
+ Type: patternType,
312
+ Interval: parseInt(options.every || '1', 10) || 1
313
+ };
314
+
315
+ // For weekly recurrence, add days of week
316
+ if (patternType === 'Weekly') {
317
+ if (options.days) {
318
+ const days = options.days
319
+ .split(',')
320
+ .map((d) => dayMap[d.trim().toLowerCase()])
321
+ .filter(Boolean);
322
+ if (days.length > 0) {
323
+ pattern.DaysOfWeek = days;
324
+ }
325
+ } else {
326
+ // Default to the day of the event
327
+ const dayOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][
328
+ start.getDay()
329
+ ];
330
+ pattern.DaysOfWeek = [dayOfWeek];
331
+ }
332
+ }
333
+
334
+ // For monthly, use day of month
335
+ if (patternType === 'AbsoluteMonthly') {
336
+ pattern.DayOfMonth = start.getDate();
337
+ }
338
+
339
+ // For yearly, use month and day
340
+ if (patternType === 'AbsoluteYearly') {
341
+ pattern.Month = start.getMonth() + 1;
342
+ pattern.DayOfMonth = start.getDate();
343
+ }
344
+
345
+ // Build range — use local date to avoid UTC shift for late-evening events
346
+ const year = start.getFullYear();
347
+ const month = String(start.getMonth() + 1).padStart(2, '0');
348
+ const day = String(start.getDate()).padStart(2, '0');
349
+ const localDate = `${year}-${month}-${day}`;
350
+ const range: RecurrenceRange = {
351
+ Type: 'NoEnd',
352
+ StartDate: localDate
353
+ };
354
+
355
+ if (options.until) {
356
+ range.Type = 'EndDate';
357
+ range.EndDate = options.until;
358
+ } else if (options.count) {
359
+ range.Type = 'Numbered';
360
+ range.NumberOfOccurrences = parseInt(options.count, 10);
361
+ }
362
+
363
+ recurrence = { Pattern: pattern, Range: range };
364
+ }
365
+
366
+ const sensitivity = options.sensitivity ? SENSITIVITY_MAP[options.sensitivity.toLowerCase()] : undefined;
367
+
368
+ if (options.sensitivity && !sensitivity) {
369
+ console.error(`Invalid sensitivity: ${options.sensitivity}`);
370
+ process.exit(1);
371
+ }
372
+
373
+ const workingDirectory = process.cwd();
374
+ let fileAttachments: EmailAttachment[] | undefined;
375
+ if (options.attach?.trim()) {
376
+ fileAttachments = [];
377
+ const filePaths = options.attach
378
+ .split(',')
379
+ .map((f) => f.trim())
380
+ .filter(Boolean);
381
+ for (const filePath of filePaths) {
382
+ try {
383
+ const validated = await validateAttachmentPath(filePath, workingDirectory);
384
+ const content = await readFile(validated.absolutePath);
385
+ const contentType = lookupMimeType(validated.fileName);
386
+ fileAttachments.push({
387
+ name: validated.fileName,
388
+ contentType,
389
+ contentBytes: content.toString('base64')
390
+ });
391
+ if (!options.json) {
392
+ console.log(` Attaching file: ${validated.fileName} (${Math.round(validated.size / 1024)} KB)`);
393
+ }
394
+ } catch (err) {
395
+ console.error(`Failed to read attachment: ${filePath}`);
396
+ if (err instanceof AttachmentPathError) {
397
+ console.error(err.message);
398
+ } else {
399
+ console.error(err instanceof Error ? err.message : 'Unknown error');
400
+ }
401
+ process.exit(1);
402
+ }
403
+ }
404
+ }
405
+
406
+ let referenceAttachments: ReferenceAttachmentInput[] | undefined;
407
+ const linkSpecs = options.attachLink ?? [];
408
+ if (linkSpecs.length > 0) {
409
+ referenceAttachments = [];
410
+ for (const spec of linkSpecs) {
411
+ try {
412
+ const { name, url } = parseAttachLinkSpec(spec);
413
+ referenceAttachments.push({ name, url, contentType: 'text/html' });
414
+ if (!options.json) {
415
+ console.log(` Attaching link: ${name}`);
416
+ }
417
+ } catch (err) {
418
+ const msg =
419
+ err instanceof AttachmentLinkSpecError ? err.message : err instanceof Error ? err.message : String(err);
420
+ console.error(`Invalid --attach-link: ${msg}`);
421
+ process.exit(1);
422
+ }
423
+ }
424
+ }
425
+
426
+ // Create the event
427
+ const result = await createEvent({
428
+ token: authResult.token!,
429
+ subject: title,
430
+ start: options.timezone ? toLocalUnzonedISOString(start) : toUTCISOString(start),
431
+ end: options.timezone ? toLocalUnzonedISOString(end) : toUTCISOString(end),
432
+ body: options.description,
433
+ location: roomName,
434
+ attendees: attendees.length > 0 ? attendees : undefined,
435
+ isOnlineMeeting: options.teams,
436
+ isAllDay: options.allDay,
437
+ sensitivity,
438
+ recurrence,
439
+ mailbox: options.mailbox,
440
+ timezone: options.timezone,
441
+ categories: options.category && options.category.length > 0 ? options.category : undefined,
442
+ fileAttachments,
443
+ referenceAttachments
444
+ });
445
+
446
+ if (!result.ok || !result.data) {
447
+ if (options.json) {
448
+ console.log(JSON.stringify({ error: result.error?.message || 'Failed to create event' }, null, 2));
449
+ } else {
450
+ console.error(`Error: ${result.error?.message || 'Failed to create event'}`);
451
+ }
452
+ process.exit(1);
453
+ }
454
+
455
+ if (options.json) {
456
+ console.log(
457
+ JSON.stringify(
458
+ {
459
+ success: true,
460
+ event: {
461
+ id: result.data.Id,
462
+ changeKey: result.data.ChangeKey,
463
+ subject: result.data.Subject,
464
+ start: result.data.Start.DateTime,
465
+ end: result.data.End.DateTime,
466
+ webLink: result.data.WebLink,
467
+ onlineMeetingUrl: result.data.OnlineMeetingUrl,
468
+ fileAttachments: fileAttachments?.length ?? 0,
469
+ referenceAttachments: referenceAttachments?.length ?? 0,
470
+ recurring: !!recurrence,
471
+ recurrence: recurrence
472
+ ? {
473
+ type: recurrence.Pattern.Type,
474
+ interval: recurrence.Pattern.Interval,
475
+ daysOfWeek: recurrence.Pattern.DaysOfWeek,
476
+ endType: recurrence.Range.Type,
477
+ endDate: recurrence.Range.EndDate,
478
+ occurrences: recurrence.Range.NumberOfOccurrences
479
+ }
480
+ : undefined
481
+ }
482
+ },
483
+ null,
484
+ 2
485
+ )
486
+ );
487
+ return;
488
+ }
489
+
490
+ console.log('\n\u2713 Event created successfully!\n');
491
+ console.log(` Title: ${result.data.Subject}`);
492
+ console.log(
493
+ ` When: ${formatDate(result.data.Start.DateTime)} ${formatTime(result.data.Start.DateTime)} - ${formatTime(result.data.End.DateTime)}`
494
+ );
495
+
496
+ if (roomName) {
497
+ console.log(` Room: ${roomName}`);
498
+ }
499
+
500
+ if (attendees.length > 0) {
501
+ const nonRoomAttendees = attendees.filter((a) => a.type !== 'Resource');
502
+ if (nonRoomAttendees.length > 0) {
503
+ console.log(` Attendees: ${nonRoomAttendees.map((a) => a.email).join(', ')}`);
504
+ }
505
+ }
506
+
507
+ if (result.data.OnlineMeetingUrl) {
508
+ console.log(` Teams: ${result.data.OnlineMeetingUrl}`);
509
+ }
510
+
511
+ if (result.data.WebLink) {
512
+ console.log(` Link: ${result.data.WebLink}`);
513
+ }
514
+
515
+ if (recurrence) {
516
+ let recurrenceDesc = `Every ${recurrence.Pattern.Interval > 1 ? `${recurrence.Pattern.Interval} ` : ''}`;
517
+ switch (recurrence.Pattern.Type) {
518
+ case 'Daily':
519
+ recurrenceDesc += recurrence.Pattern.Interval > 1 ? 'days' : 'day';
520
+ break;
521
+ case 'Weekly':
522
+ recurrenceDesc += recurrence.Pattern.Interval > 1 ? 'weeks' : 'week';
523
+ if (recurrence.Pattern.DaysOfWeek) {
524
+ recurrenceDesc += ` on ${recurrence.Pattern.DaysOfWeek.join(', ')}`;
525
+ }
526
+ break;
527
+ case 'AbsoluteMonthly':
528
+ recurrenceDesc += recurrence.Pattern.Interval > 1 ? 'months' : 'month';
529
+ break;
530
+ case 'AbsoluteYearly':
531
+ recurrenceDesc += recurrence.Pattern.Interval > 1 ? 'years' : 'year';
532
+ break;
533
+ }
534
+ if (recurrence.Range.Type === 'EndDate' && recurrence.Range.EndDate) {
535
+ recurrenceDesc += ` until ${recurrence.Range.EndDate}`;
536
+ } else if (recurrence.Range.Type === 'Numbered' && recurrence.Range.NumberOfOccurrences) {
537
+ recurrenceDesc += ` (${recurrence.Range.NumberOfOccurrences} occurrences)`;
538
+ }
539
+ console.log(` Repeat: ${recurrenceDesc}`);
540
+ }
541
+
542
+ console.log();
543
+ }
544
+ );