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,608 @@
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
+ addCalendarEventAttachments,
9
+ type EmailAttachment,
10
+ getCalendarEvent,
11
+ getCalendarEvents,
12
+ getRooms,
13
+ type ReferenceAttachmentInput,
14
+ SENSITIVITY_MAP,
15
+ searchRooms,
16
+ updateEvent
17
+ } from '../lib/ews-client.js';
18
+ import { lookupMimeType } from '../lib/mime-type.js';
19
+ import { checkReadOnly } from '../lib/utils.js';
20
+
21
+ function formatTime(dateStr: string): string {
22
+ const date = new Date(dateStr);
23
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
24
+ }
25
+
26
+ function formatDate(dateStr: string): string {
27
+ const date = new Date(dateStr);
28
+ return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
29
+ }
30
+
31
+ export const updateEventCommand = new Command('update-event')
32
+ .description('Update a calendar event')
33
+ .argument('[eventIndex]', 'Event index from the list (deprecated; use --id)')
34
+ .option('--id <eventId>', 'Update event by stable ID')
35
+ .option(
36
+ '--day <day>',
37
+ 'Day to show events from (today, tomorrow, YYYY-MM-DD) - note: may miss multi-day events crossing midnight',
38
+ 'today'
39
+ )
40
+ .option('--title <text>', 'New title/subject')
41
+ .option('--description <text>', 'New description/body')
42
+ .option('--start <time>', 'New start time (e.g., 14:00, 2pm)')
43
+ .option('--end <time>', 'New end time (e.g., 15:00, 3pm)')
44
+ .option(
45
+ '--add-attendee <email>',
46
+ 'Add an attendee (can be used multiple times)',
47
+ (val, arr: string[]) => [...arr, val],
48
+ []
49
+ )
50
+ .option(
51
+ '--remove-attendee <email>',
52
+ 'Remove an attendee by email (can be used multiple times)',
53
+ (val, arr: string[]) => [...arr, val],
54
+ []
55
+ )
56
+ .option('--room <room>', 'Set/change meeting room (name or email)')
57
+ .option('--location <text>', 'Set location text')
58
+ .option('--timezone <timezone>', 'Timezone for the event (e.g., "Pacific Standard Time")')
59
+ .option('--occurrence <index>', 'Update only the Nth occurrence of a recurring event')
60
+ .option('--instance <date>', 'Update only the occurrence on a specific date (YYYY-MM-DD)')
61
+ .option('--teams', 'Make it a Teams meeting')
62
+ .option('--no-teams', 'Remove Teams meeting')
63
+ .option('--all-day', 'Mark as an all-day event')
64
+ .option('--no-all-day', 'Remove all-day flag')
65
+ .option('--category <name>', 'Category label (repeatable)', (v, acc) => [...acc, v], [] as string[])
66
+ .option('--clear-categories', 'Clear all categories')
67
+ .option('--sensitivity <level>', 'Set sensitivity: normal, personal, private, confidential')
68
+ .option('--json', 'Output as JSON')
69
+ .option('--token <token>', 'Use a specific token')
70
+ .option('--identity <name>', 'Use a specific authentication identity (default: default)')
71
+ .option('--mailbox <email>', 'Update event in shared mailbox calendar')
72
+ .option('--attach <files>', 'Add file attachment(s), comma-separated paths (relative to cwd)')
73
+ .option(
74
+ '--attach-link <spec>',
75
+ 'Add link attachment: "Title|https://url" or bare https URL (repeatable)',
76
+ (v: string, prev: string[]) => [...prev, v],
77
+ [] as string[]
78
+ )
79
+ .action(
80
+ async (
81
+ _eventIndex: string | undefined,
82
+ options: {
83
+ id?: string;
84
+ day: string;
85
+ timezone?: string;
86
+ title?: string;
87
+ description?: string;
88
+ start?: string;
89
+ end?: string;
90
+ addAttendee: string[];
91
+ removeAttendee: string[];
92
+ room?: string;
93
+ location?: string;
94
+ occurrence?: string;
95
+ instance?: string;
96
+ teams?: boolean;
97
+ allDay?: boolean;
98
+ sensitivity?: string;
99
+ json?: boolean;
100
+ token?: string;
101
+ identity?: string;
102
+ mailbox?: string;
103
+ category?: string[];
104
+ clearCategories?: boolean;
105
+ attach?: string;
106
+ attachLink?: string[];
107
+ },
108
+ cmd: any
109
+ ) => {
110
+ checkReadOnly(cmd);
111
+ const authResult = await resolveAuth({
112
+ token: options.token,
113
+ identity: options.identity
114
+ });
115
+
116
+ if (!authResult.success) {
117
+ if (options.json) {
118
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
119
+ } else {
120
+ console.error(`Error: ${authResult.error}`);
121
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
122
+ }
123
+ process.exit(1);
124
+ }
125
+
126
+ // Get events for the day
127
+ let baseDate: Date;
128
+ try {
129
+ baseDate = parseDay(options.day, { throwOnInvalid: true });
130
+ } catch (err) {
131
+ const message = err instanceof Error ? err.message : 'Invalid day value';
132
+ if (options.json) {
133
+ console.log(JSON.stringify({ error: message }, null, 2));
134
+ } else {
135
+ console.error(`Error: ${message}`);
136
+ }
137
+ process.exit(1);
138
+ }
139
+ const startOfDay = new Date(baseDate);
140
+ startOfDay.setHours(0, 0, 0, 0);
141
+ const endOfDay = new Date(baseDate);
142
+ endOfDay.setHours(23, 59, 59, 999);
143
+
144
+ const result = await getCalendarEvents(
145
+ authResult.token!,
146
+ startOfDay.toISOString(),
147
+ endOfDay.toISOString(),
148
+ options.mailbox
149
+ );
150
+
151
+ if (!result.ok || !result.data) {
152
+ if (options.json) {
153
+ console.log(JSON.stringify({ error: result.error?.message || 'Failed to fetch events' }, null, 2));
154
+ } else {
155
+ console.error(`Error: ${result.error?.message || 'Failed to fetch events'}`);
156
+ }
157
+ process.exit(1);
158
+ }
159
+
160
+ // Filter to events the user owns
161
+ const events = result.data.filter((e) => e.IsOrganizer && !e.IsCancelled);
162
+
163
+ // If no id provided, list events
164
+ if (!options.id) {
165
+ if (options.json) {
166
+ console.log(
167
+ JSON.stringify(
168
+ {
169
+ events: events.map((e, i) => ({
170
+ index: i + 1,
171
+ id: e.Id,
172
+ subject: e.Subject,
173
+ start: e.Start.DateTime,
174
+ end: e.End.DateTime,
175
+ attendees: e.Attendees?.map((a) => a.EmailAddress?.Address)
176
+ }))
177
+ },
178
+ null,
179
+ 2
180
+ )
181
+ );
182
+ return;
183
+ }
184
+
185
+ console.log(`\nYour events for ${formatDate(baseDate.toISOString())}:\n`);
186
+ console.log('\u2500'.repeat(60));
187
+
188
+ if (events.length === 0) {
189
+ console.log('\n No events found that you can update.');
190
+ console.log(' (You can only update events you organized)\n');
191
+ return;
192
+ }
193
+
194
+ for (let i = 0; i < events.length; i++) {
195
+ const event = events[i];
196
+ const startTime = formatTime(event.Start.DateTime);
197
+ const endTime = formatTime(event.End.DateTime);
198
+
199
+ console.log(`\n [${i + 1}] ${event.Subject}`);
200
+ console.log(` ${startTime} - ${endTime}`);
201
+ console.log(` ID: ${event.Id}`);
202
+ if (event.Location?.DisplayName) {
203
+ console.log(` Location: ${event.Location.DisplayName}`);
204
+ }
205
+ if (event.Attendees && event.Attendees.length > 0) {
206
+ const attendeeList = event.Attendees.filter((a) => a.Type !== 'Resource')
207
+ .map((a) => a.EmailAddress?.Address)
208
+ .filter(Boolean);
209
+ if (attendeeList.length > 0) {
210
+ console.log(` Attendees: ${attendeeList.join(', ')}`);
211
+ }
212
+ }
213
+ }
214
+
215
+ console.log(`\n${'\u2500'.repeat(60)}`);
216
+ console.log('\nTo update an event:');
217
+ console.log(' m365-agent-cli update-event <number> --title "New Title"');
218
+ console.log(' m365-agent-cli update-event <number> --add-attendee user@example.com');
219
+ console.log(' m365-agent-cli update-event <number> --room "Taxi"');
220
+ console.log(' m365-agent-cli update-event <number> --start 14:00 --end 15:00');
221
+ console.log('');
222
+ return;
223
+ }
224
+
225
+ let targetEvent = events.find((e) => e.Id === options.id);
226
+ let occurrenceItemId: string | undefined;
227
+ let displayEvent = targetEvent;
228
+
229
+ if (options.occurrence || options.instance) {
230
+ // Find the specific occurrence, ensuring it matches the provided event ID
231
+ if (options.instance) {
232
+ let instanceDate: Date;
233
+ try {
234
+ instanceDate = parseDay(options.instance, { throwOnInvalid: true });
235
+ } catch (err) {
236
+ const message = err instanceof Error ? err.message : 'Invalid instance date';
237
+ if (options.json) {
238
+ console.log(JSON.stringify({ error: message }, null, 2));
239
+ } else {
240
+ console.error(`Error: ${message}`);
241
+ }
242
+ process.exit(1);
243
+ }
244
+ instanceDate.setHours(0, 0, 0, 0);
245
+ const occEvent = events.find((e) => {
246
+ const eventDate = new Date(e.Start.DateTime);
247
+ eventDate.setHours(0, 0, 0, 0);
248
+ return eventDate.getTime() === instanceDate.getTime() && e.Id === options.id;
249
+ });
250
+ if (!occEvent) {
251
+ console.error(
252
+ `No occurrence found on ${options.instance} with ID ${options.id}. Try expanding the date range with --day.`
253
+ );
254
+ process.exit(1);
255
+ }
256
+ occurrenceItemId = occEvent.Id;
257
+ displayEvent = occEvent;
258
+ console.log(`\nUpdating single occurrence of: ${occEvent.Subject}`);
259
+ console.log(
260
+ ` ${formatDate(occEvent.Start.DateTime)} ${formatTime(occEvent.Start.DateTime)} - ${formatTime(occEvent.End.DateTime)}`
261
+ );
262
+ } else if (options.occurrence) {
263
+ const idx = parseInt(options.occurrence, 10);
264
+ if (Number.isNaN(idx) || idx < 1 || idx > events.length) {
265
+ console.error(`Invalid --occurrence index: ${options.occurrence}. Valid range: 1-${events.length}.`);
266
+ process.exit(1);
267
+ }
268
+ const occEvent = events[idx - 1];
269
+ if (occEvent.Id !== options.id) {
270
+ console.error(`Occurrence ${idx} does not match the provided event ID ${options.id}.`);
271
+ process.exit(1);
272
+ }
273
+ occurrenceItemId = occEvent.Id;
274
+ displayEvent = occEvent;
275
+ console.log(`\nUpdating occurrence ${idx} of: ${occEvent.Subject}`);
276
+ console.log(
277
+ ` ${formatDate(occEvent.Start.DateTime)} ${formatTime(occEvent.Start.DateTime)} - ${formatTime(occEvent.End.DateTime)}`
278
+ );
279
+ }
280
+ } else if (!targetEvent && options.id) {
281
+ const fetched = await getCalendarEvent(authResult.token!, options.id, options.mailbox);
282
+ if (!fetched.ok || !fetched.data) {
283
+ console.error(`Invalid event id: ${options.id}`);
284
+ process.exit(1);
285
+ }
286
+ displayEvent = fetched.data;
287
+ targetEvent = fetched.data;
288
+ } else if (!targetEvent) {
289
+ console.error(`Invalid event id: ${options.id}`);
290
+ process.exit(1);
291
+ }
292
+
293
+ const hasFieldUpdates =
294
+ options.title ||
295
+ options.description ||
296
+ options.start ||
297
+ options.end ||
298
+ options.addAttendee.length > 0 ||
299
+ options.removeAttendee.length > 0 ||
300
+ options.room ||
301
+ options.location ||
302
+ options.timezone ||
303
+ options.teams !== undefined ||
304
+ options.allDay !== undefined ||
305
+ (options.category && options.category.length > 0) ||
306
+ options.clearCategories ||
307
+ !!options.sensitivity;
308
+
309
+ const wantsFileAttach = !!options.attach?.trim();
310
+ const linkSpecs = options.attachLink ?? [];
311
+ const wantsLinkAttach = linkSpecs.length > 0;
312
+ const wantsAttachments = wantsFileAttach || wantsLinkAttach;
313
+
314
+ if (!hasFieldUpdates && !wantsAttachments) {
315
+ // Show current event details
316
+ console.log(`\nEvent: ${displayEvent!.Subject}`);
317
+ console.log(
318
+ ` When: ${formatDate(displayEvent!.Start.DateTime)} ${formatTime(displayEvent!.Start.DateTime)} - ${formatTime(displayEvent!.End.DateTime)}`
319
+ );
320
+ if (displayEvent!.Location?.DisplayName) {
321
+ console.log(` Location: ${displayEvent!.Location.DisplayName}`);
322
+ }
323
+ if (displayEvent!.Attendees && displayEvent!.Attendees.length > 0) {
324
+ console.log(' Attendees:');
325
+ for (const a of displayEvent!.Attendees) {
326
+ const typeLabel = a.Type === 'Resource' ? ' (Room)' : '';
327
+ console.log(` - ${a.EmailAddress?.Address}${typeLabel}`);
328
+ }
329
+ }
330
+ console.log('\nUse options like --title, --add-attendee, --room, --attach, or --attach-link to update.');
331
+ return;
332
+ }
333
+
334
+ let fileAttachments: EmailAttachment[] | undefined;
335
+ if (wantsFileAttach) {
336
+ fileAttachments = [];
337
+ const workingDirectory = process.cwd();
338
+ const filePaths = options
339
+ .attach!.split(',')
340
+ .map((f) => f.trim())
341
+ .filter(Boolean);
342
+ for (const filePath of filePaths) {
343
+ try {
344
+ const validated = await validateAttachmentPath(filePath, workingDirectory);
345
+ const content = await readFile(validated.absolutePath);
346
+ const contentType = lookupMimeType(validated.fileName);
347
+ fileAttachments.push({
348
+ name: validated.fileName,
349
+ contentType,
350
+ contentBytes: content.toString('base64')
351
+ });
352
+ if (!options.json) {
353
+ console.log(` Adding file attachment: ${validated.fileName}`);
354
+ }
355
+ } catch (err) {
356
+ console.error(`Failed to read attachment: ${filePath}`);
357
+ if (err instanceof AttachmentPathError) {
358
+ console.error(err.message);
359
+ } else {
360
+ console.error(err instanceof Error ? err.message : 'Unknown error');
361
+ }
362
+ process.exit(1);
363
+ }
364
+ }
365
+ }
366
+
367
+ let referenceAttachments: ReferenceAttachmentInput[] | undefined;
368
+ if (wantsLinkAttach) {
369
+ referenceAttachments = [];
370
+ for (const spec of linkSpecs) {
371
+ try {
372
+ const { name, url } = parseAttachLinkSpec(spec);
373
+ referenceAttachments.push({ name, url, contentType: 'text/html' });
374
+ if (!options.json) {
375
+ console.log(` Adding link attachment: ${name}`);
376
+ }
377
+ } catch (err) {
378
+ const msg =
379
+ err instanceof AttachmentLinkSpecError ? err.message : err instanceof Error ? err.message : String(err);
380
+ console.error(`Invalid --attach-link: ${msg}`);
381
+ process.exit(1);
382
+ }
383
+ }
384
+ }
385
+
386
+ let updateResult: Awaited<ReturnType<typeof updateEvent>> | undefined;
387
+
388
+ if (hasFieldUpdates) {
389
+ const updateOptions: Parameters<typeof updateEvent>[0] = {
390
+ token: authResult.token!,
391
+ eventId: targetEvent ? targetEvent.Id : displayEvent!.Id,
392
+ changeKey: displayEvent!.ChangeKey,
393
+ occurrenceItemId,
394
+ mailbox: options.mailbox,
395
+ categories: options.clearCategories
396
+ ? []
397
+ : options.category && options.category.length > 0
398
+ ? options.category
399
+ : undefined
400
+ };
401
+
402
+ if (options.title) {
403
+ updateOptions.subject = options.title;
404
+ }
405
+
406
+ if (options.timezone) {
407
+ updateOptions.timezone = options.timezone;
408
+ }
409
+
410
+ if (options.description) {
411
+ updateOptions.body = options.description;
412
+ }
413
+
414
+ if (options.start || options.end) {
415
+ const eventDate = new Date(displayEvent!.Start.DateTime);
416
+
417
+ if (options.start) {
418
+ try {
419
+ const newStart = parseTimeToDate(options.start, eventDate, { throwOnInvalid: true });
420
+ updateOptions.start = options.timezone ? toLocalUnzonedISOString(newStart) : toUTCISOString(newStart);
421
+ } catch (err) {
422
+ const message = err instanceof Error ? err.message : 'Invalid start time';
423
+ if (options.json) {
424
+ console.log(JSON.stringify({ error: message }, null, 2));
425
+ } else {
426
+ console.error(`Error: ${message}`);
427
+ }
428
+ process.exit(1);
429
+ }
430
+ }
431
+
432
+ if (options.end) {
433
+ try {
434
+ const newEnd = parseTimeToDate(options.end, eventDate, { throwOnInvalid: true });
435
+ updateOptions.end = options.timezone ? toLocalUnzonedISOString(newEnd) : toUTCISOString(newEnd);
436
+ } catch (err) {
437
+ const message = err instanceof Error ? err.message : 'Invalid end time';
438
+ if (options.json) {
439
+ console.log(JSON.stringify({ error: message }, null, 2));
440
+ } else {
441
+ console.error(`Error: ${message}`);
442
+ }
443
+ process.exit(1);
444
+ }
445
+ }
446
+ }
447
+
448
+ if (options.location) {
449
+ updateOptions.location = options.location;
450
+ }
451
+
452
+ if (options.allDay !== undefined) {
453
+ updateOptions.isAllDay = options.allDay;
454
+ }
455
+
456
+ if (options.sensitivity) {
457
+ const sensitivity = SENSITIVITY_MAP[options.sensitivity.toLowerCase()];
458
+ if (!sensitivity) {
459
+ console.error(`Invalid sensitivity: ${options.sensitivity}`);
460
+ process.exit(1);
461
+ }
462
+ updateOptions.sensitivity = sensitivity;
463
+ }
464
+
465
+ let roomEmail: string | undefined;
466
+ let roomName: string | undefined;
467
+
468
+ if (options.room) {
469
+ if (options.room.includes('@')) {
470
+ roomEmail = options.room;
471
+ roomName = options.room;
472
+ } else {
473
+ let roomsResult = await searchRooms(authResult.token!, options.room);
474
+ if (!roomsResult.ok || !roomsResult.data || roomsResult.data.length === 0) {
475
+ roomsResult = await getRooms(authResult.token!);
476
+ }
477
+
478
+ if (roomsResult.ok && roomsResult.data) {
479
+ const found = roomsResult.data.find((r) =>
480
+ options.room ? r.Name.toLowerCase().includes(options.room.toLowerCase()) : false
481
+ );
482
+ if (found) {
483
+ roomEmail = found.Address;
484
+ roomName = found.Name;
485
+ } else {
486
+ console.error(`Room not found: ${options.room}`);
487
+ process.exit(1);
488
+ }
489
+ }
490
+ }
491
+
492
+ if (roomName) {
493
+ updateOptions.location = roomName;
494
+ }
495
+ }
496
+
497
+ if (options.addAttendee.length > 0 || options.removeAttendee.length > 0 || roomEmail) {
498
+ const existingAttendees: Array<{ email: string; name?: string; type: 'Required' | 'Optional' | 'Resource' }> =
499
+ (displayEvent!.Attendees || []).map((a) => ({
500
+ email: a.EmailAddress?.Address || '',
501
+ name: a.EmailAddress?.Name,
502
+ type: a.Type as 'Required' | 'Optional' | 'Resource'
503
+ }));
504
+
505
+ for (const email of options.removeAttendee) {
506
+ const idx = existingAttendees.findIndex((a) => a.email.toLowerCase() === email.toLowerCase());
507
+ if (idx !== -1) existingAttendees.splice(idx, 1);
508
+ }
509
+
510
+ for (const email of options.addAttendee) {
511
+ if (!existingAttendees.find((a) => a.email.toLowerCase() === email.toLowerCase())) {
512
+ existingAttendees.push({ email, type: 'Required' });
513
+ }
514
+ }
515
+
516
+ if (roomEmail) {
517
+ const withoutRooms = existingAttendees.filter((a) => a.type !== 'Resource');
518
+ withoutRooms.push({ email: roomEmail, name: roomName, type: 'Resource' });
519
+ updateOptions.attendees = withoutRooms;
520
+ } else {
521
+ updateOptions.attendees = existingAttendees;
522
+ }
523
+ }
524
+
525
+ if (options.teams !== undefined) {
526
+ updateOptions.isOnlineMeeting = options.teams;
527
+ }
528
+
529
+ console.log(`\nUpdating: ${displayEvent!.Subject}`);
530
+
531
+ updateResult = await updateEvent(updateOptions);
532
+
533
+ if (!updateResult.ok) {
534
+ if (options.json) {
535
+ console.log(JSON.stringify({ error: updateResult.error?.message || 'Failed to update event' }, null, 2));
536
+ } else {
537
+ console.error(`\nError: ${updateResult.error?.message || 'Failed to update event'}`);
538
+ }
539
+ process.exit(1);
540
+ }
541
+ }
542
+
543
+ const eventIdForAttach = occurrenceItemId || updateResult?.data?.Id || displayEvent!.Id;
544
+
545
+ if (wantsAttachments) {
546
+ const attachResult = await addCalendarEventAttachments(
547
+ authResult.token!,
548
+ eventIdForAttach,
549
+ options.mailbox,
550
+ fileAttachments ?? [],
551
+ referenceAttachments ?? []
552
+ );
553
+ if (!attachResult.ok) {
554
+ if (options.json) {
555
+ console.log(JSON.stringify({ error: attachResult.error?.message || 'Failed to add attachments' }, null, 2));
556
+ } else {
557
+ console.error(`\nError: ${attachResult.error?.message || 'Failed to add attachments'}`);
558
+ }
559
+ process.exit(1);
560
+ }
561
+ }
562
+
563
+ if (options.json) {
564
+ const dr = updateResult?.data;
565
+ const de = displayEvent!;
566
+ console.log(
567
+ JSON.stringify(
568
+ {
569
+ success: true,
570
+ event: {
571
+ id: occurrenceItemId || dr?.Id || de.Id,
572
+ changeKey: dr?.ChangeKey,
573
+ subject: dr?.Subject ?? de.Subject,
574
+ start: dr?.Start.DateTime ?? de.Start.DateTime,
575
+ end: dr?.End.DateTime ?? de.End.DateTime,
576
+ fieldUpdatesApplied: hasFieldUpdates,
577
+ fileAttachmentsAdded: fileAttachments?.length ?? 0,
578
+ referenceAttachmentsAdded: referenceAttachments?.length ?? 0
579
+ }
580
+ },
581
+ null,
582
+ 2
583
+ )
584
+ );
585
+ } else {
586
+ if (hasFieldUpdates) {
587
+ console.log('\n\u2713 Event updated successfully.');
588
+ }
589
+ if (wantsAttachments) {
590
+ console.log('\n\u2713 Attachment(s) added to calendar event.');
591
+ }
592
+ const dr = updateResult?.data;
593
+ const de = displayEvent!;
594
+ if (dr) {
595
+ console.log(`\n Title: ${dr.Subject}`);
596
+ console.log(
597
+ ` When: ${formatDate(dr.Start.DateTime)} ${formatTime(dr.Start.DateTime)} - ${formatTime(dr.End.DateTime)}`
598
+ );
599
+ } else if (wantsAttachments) {
600
+ console.log(`\n Title: ${de.Subject}`);
601
+ console.log(
602
+ ` When: ${formatDate(de.Start.DateTime)} ${formatTime(de.Start.DateTime)} - ${formatTime(de.End.DateTime)}`
603
+ );
604
+ }
605
+ console.log('');
606
+ }
607
+ }
608
+ );
@@ -0,0 +1,88 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { Command } from 'commander';
3
+ import semver from 'semver';
4
+ import { getPackageVersion } from '../lib/package-info.js';
5
+
6
+ const NPM_PKG = 'm365-agent-cli';
7
+ const NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PKG}/latest`;
8
+
9
+ function compareVersions(a: string, b: string): number {
10
+ const va = semver.valid(semver.coerce(a));
11
+ const vb = semver.valid(semver.coerce(b));
12
+ if (va && vb) return semver.compare(va, vb);
13
+ return a.localeCompare(b);
14
+ }
15
+
16
+ async function fetchLatestVersion(): Promise<string> {
17
+ const ac = new AbortController();
18
+ const t = setTimeout(() => ac.abort(), 15_000);
19
+ try {
20
+ const r = await fetch(NPM_LATEST_URL, { signal: ac.signal });
21
+ if (!r.ok) {
22
+ throw new Error(`npm registry returned ${r.status}`);
23
+ }
24
+ const j = (await r.json()) as { version?: string };
25
+ if (!j.version?.trim()) {
26
+ throw new Error('missing version in registry response');
27
+ }
28
+ return j.version.trim();
29
+ } finally {
30
+ clearTimeout(t);
31
+ }
32
+ }
33
+
34
+ function runGlobalInstall(): Promise<number> {
35
+ const pkg = `${NPM_PKG}@latest`;
36
+ const useBun = process.versions.bun !== undefined;
37
+ const cmd = useBun ? 'bun' : 'npm';
38
+ const args = ['install', '-g', pkg];
39
+
40
+ return new Promise((resolve, reject) => {
41
+ const child = spawn(cmd, args, {
42
+ stdio: 'inherit',
43
+ shell: process.platform === 'win32',
44
+ env: process.env
45
+ });
46
+ child.on('error', reject);
47
+ child.on('close', (code) => resolve(code ?? 1));
48
+ });
49
+ }
50
+
51
+ export const updateCommand = new Command('update')
52
+ .description('Check for and install the latest npm release of this CLI')
53
+ .option('-c, --check', 'Only check if a newer version exists on npm (exit 1 if newer is available)')
54
+ .action(async (opts: { check?: boolean }) => {
55
+ const current = await getPackageVersion();
56
+ let latest: string;
57
+ try {
58
+ latest = await fetchLatestVersion();
59
+ } catch (e) {
60
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ const cmp = compareVersions(current, latest);
65
+ if (cmp === 0) {
66
+ console.log(`m365-agent-cli is up to date (${current}).`);
67
+ process.exit(0);
68
+ }
69
+ if (cmp > 0) {
70
+ console.log(`m365-agent-cli local version ${current} is newer than npm latest (${latest}).`);
71
+ process.exit(0);
72
+ }
73
+
74
+ if (opts.check) {
75
+ console.log(`Update available: ${current} → ${latest}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ console.log(`Updating ${current} → ${latest}…`);
80
+ const code = await runGlobalInstall();
81
+ if (code !== 0) {
82
+ console.error(`Error: ${process.versions.bun ? 'bun' : 'npm'} install exited with code ${code}`);
83
+ console.error('Try manually: npm install -g m365-agent-cli@latest or bun install -g m365-agent-cli@latest');
84
+ process.exit(1);
85
+ }
86
+ console.log(`Updated to ${latest}. Run again to use the new version.`);
87
+ process.exit(0);
88
+ });