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,195 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import { expandGroup, searchGroups, searchPeople, searchUsers } from '../lib/graph-directory.js';
4
+
5
+ export const findCommand = new Command('find')
6
+ .description('Search for people or groups in the directory')
7
+ .argument('<query>', 'Search query (name, email, etc.)')
8
+ .option('--people', 'Only search people/users')
9
+ .option('--groups', 'Only search groups')
10
+ .option('--expand', 'Expand group members if the query matches a group')
11
+ .option('--json', 'Output as JSON')
12
+ .option('--token <token>', 'Use a specific token')
13
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
14
+ .action(
15
+ async (
16
+ query: string,
17
+ options: {
18
+ people?: boolean;
19
+ groups?: boolean;
20
+ expand?: boolean;
21
+ json?: boolean;
22
+ token?: string;
23
+ identity?: string;
24
+ }
25
+ ) => {
26
+ const authResult = await resolveGraphAuth({
27
+ token: options.token,
28
+ identity: options.identity
29
+ });
30
+
31
+ if (!authResult.success) {
32
+ if (options.json) {
33
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
34
+ } else {
35
+ console.error(`Error: ${authResult.error}`);
36
+ console.error('\nCheck your .env file or run m365-agent-cli auth.');
37
+ }
38
+ process.exit(1);
39
+ }
40
+
41
+ const token = authResult.token!;
42
+ try {
43
+ const results: any[] = [];
44
+ const errors: string[] = [];
45
+
46
+ const searchAll = !options.people && !options.groups;
47
+
48
+ if (searchAll || options.people) {
49
+ const peopleRes = await searchPeople(token, query);
50
+ if (peopleRes.ok && peopleRes.data) {
51
+ results.push(
52
+ ...peopleRes.data.map((p) => ({
53
+ id: p.id,
54
+ type: 'Person',
55
+ name: p.displayName,
56
+ email: p.userPrincipalName || p.scoredEmailAddresses?.[0]?.address,
57
+ title: p.jobTitle,
58
+ department: p.department,
59
+ userPrincipalName: p.userPrincipalName
60
+ }))
61
+ );
62
+ } else if (peopleRes.error) {
63
+ if (peopleRes.error.status === 403) {
64
+ // Default auth scopes may not cover People API - suppress 403 unless --people was explicitly requested
65
+ if (options.people) {
66
+ errors.push(`People search failed: ${peopleRes.error.message}`);
67
+ }
68
+ } else {
69
+ errors.push(`People search failed: ${peopleRes.error.message}`);
70
+ }
71
+ }
72
+
73
+ const usersRes = await searchUsers(token, query);
74
+ if (usersRes.ok && usersRes.data) {
75
+ for (const u of usersRes.data) {
76
+ const userEmail = u.mail || u.userPrincipalName;
77
+ const userUpn = u.userPrincipalName;
78
+ if (
79
+ !results.find(
80
+ (r) =>
81
+ (r.userPrincipalName && userUpn && r.userPrincipalName.toLowerCase() === userUpn.toLowerCase()) ||
82
+ (r.email && userEmail && r.email.toLowerCase() === userEmail.toLowerCase())
83
+ )
84
+ ) {
85
+ results.push({
86
+ id: u.id,
87
+ type: 'Person',
88
+ name: u.displayName,
89
+ email: userEmail,
90
+ title: u.jobTitle,
91
+ department: u.department,
92
+ userPrincipalName: u.userPrincipalName
93
+ });
94
+ }
95
+ }
96
+ } else if (usersRes.error) {
97
+ if (usersRes.error.status === 403) {
98
+ // Default auth scopes may not cover Users API - suppress 403 unless --people was explicitly requested
99
+ if (options.people) {
100
+ errors.push(`Users search failed: ${usersRes.error.message}`);
101
+ }
102
+ } else {
103
+ errors.push(`Users search failed: ${usersRes.error.message}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ if (searchAll || options.groups) {
109
+ const groupsRes = await searchGroups(token, query);
110
+ if (groupsRes.ok && groupsRes.data) {
111
+ // Only expand members of the first group to avoid N+1 API calls
112
+ const groupsToExpand = options.expand ? groupsRes.data.slice(0, 1) : [];
113
+ for (const g of groupsRes.data) {
114
+ const groupItem: any = {
115
+ id: g.id,
116
+ type: 'Group',
117
+ name: g.displayName,
118
+ email: g.mail,
119
+ description: g.description
120
+ };
121
+
122
+ if (groupsToExpand.some((ge) => ge.id === g.id)) {
123
+ const membersRes = await expandGroup(token, g.id);
124
+ if (membersRes.ok && membersRes.data) {
125
+ groupItem.members = membersRes.data.map((m: any) => ({
126
+ id: m.id,
127
+ name: m.displayName,
128
+ email: m.mail || m.userPrincipalName
129
+ }));
130
+ }
131
+ }
132
+ results.push(groupItem);
133
+ }
134
+ } else if (groupsRes.error) {
135
+ if (groupsRes.error.status === 403) {
136
+ // Default auth scopes may not cover Groups API - suppress 403 unless --groups was explicitly requested
137
+ if (options.groups) {
138
+ errors.push(`Groups search failed: ${groupsRes.error.message}`);
139
+ }
140
+ } else {
141
+ errors.push(`Groups search failed: ${groupsRes.error.message}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ if (options.json) {
147
+ const output: any = { results };
148
+ if (errors.length > 0) output.errors = errors;
149
+ console.log(JSON.stringify(output, null, 2));
150
+ return;
151
+ }
152
+
153
+ if (errors.length > 0) {
154
+ console.error(`Warnings:`);
155
+ for (const e of errors) console.error(` - ${e}`);
156
+ }
157
+
158
+ if (results.length === 0) {
159
+ console.log(`\nNo results found for "${query}"\n`);
160
+ return;
161
+ }
162
+
163
+ console.log(`\nSearch results for "${query}":\n`);
164
+ console.log('\u2500'.repeat(80));
165
+
166
+ for (const res of results) {
167
+ const isGroup = res.type === 'Group';
168
+ const icon = isGroup ? '\u{1F465}' : '\u{1F464}';
169
+
170
+ console.log(`\n ${icon} ${res.name} [${res.type}]`);
171
+ if (res.email) console.log(` Email: ${res.email}`);
172
+ if (res.title) console.log(` Title: ${res.title}`);
173
+ if (res.department) console.log(` Dept: ${res.department}`);
174
+ if (res.description) console.log(` Desc: ${res.description}`);
175
+
176
+ if (isGroup && res.members) {
177
+ console.log(` Members (${res.members.length}):`);
178
+ for (const member of res.members) {
179
+ console.log(` - ${member.name} ${member.email ? `(${member.email})` : ''}`);
180
+ }
181
+ }
182
+ }
183
+
184
+ console.log(`\n${'\u2500'.repeat(80)}\n`);
185
+ } catch (err) {
186
+ const message = err instanceof Error ? err.message : 'Unknown error';
187
+ if (options.json) {
188
+ console.log(JSON.stringify({ error: message }, null, 2));
189
+ } else {
190
+ console.error(`Error: ${message}`);
191
+ }
192
+ process.exit(1);
193
+ }
194
+ }
195
+ );
@@ -0,0 +1,270 @@
1
+ import { Command } from 'commander';
2
+ import { resolveAuth } from '../lib/auth.js';
3
+ import { parseDay } from '../lib/dates.js';
4
+ import { getOwaUserInfo, getScheduleViaOutlook } from '../lib/ews-client.js';
5
+
6
+ function formatTime(dateStr: string): string {
7
+ const date = new Date(dateStr);
8
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
9
+ }
10
+
11
+ function formatDate(dateStr: string): string {
12
+ const date = new Date(dateStr);
13
+ return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
14
+ }
15
+
16
+ function _formatDateTime(dateStr: string): string {
17
+ const _date = new Date(dateStr);
18
+ return `${formatDate(dateStr)} ${formatTime(dateStr)}`;
19
+ }
20
+
21
+ function isValidEmail(value: string): boolean {
22
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
23
+ }
24
+
25
+ function getDateRange(startDay: string, endDay?: string): { start: Date; end: Date; label: string } {
26
+ const now = new Date();
27
+
28
+ switch (startDay.toLowerCase()) {
29
+ case 'week':
30
+ case 'thisweek': {
31
+ const start = new Date(now);
32
+ const dayOfWeek = start.getDay();
33
+ start.setDate(start.getDate() + (dayOfWeek === 0 ? 1 : 8 - dayOfWeek));
34
+ start.setHours(0, 0, 0, 0);
35
+ const end = new Date(start);
36
+ end.setDate(end.getDate() + 4);
37
+ end.setHours(23, 59, 59, 999);
38
+ return { start, end, label: 'This Week (Mon-Fri)' };
39
+ }
40
+ case 'nextweek': {
41
+ const start = new Date(now);
42
+ const dayOfWeek = start.getDay();
43
+ const daysUntilNextMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
44
+ start.setDate(start.getDate() + daysUntilNextMonday);
45
+ start.setHours(0, 0, 0, 0);
46
+ const end = new Date(start);
47
+ end.setDate(end.getDate() + 4);
48
+ end.setHours(23, 59, 59, 999);
49
+ return { start, end, label: 'Next Week (Mon-Fri)' };
50
+ }
51
+ }
52
+
53
+ const startDate = parseDay(startDay, { throwOnInvalid: true });
54
+ startDate.setHours(0, 0, 0, 0);
55
+
56
+ if (endDay) {
57
+ const endDate = parseDay(endDay, { baseDate: startDate, weekdayDirection: 'next', throwOnInvalid: true });
58
+ endDate.setHours(23, 59, 59, 999);
59
+ return {
60
+ start: startDate,
61
+ end: endDate,
62
+ label: `${formatDate(startDate.toISOString())} - ${formatDate(endDate.toISOString())}`
63
+ };
64
+ }
65
+
66
+ const endDate = new Date(startDate);
67
+ endDate.setHours(23, 59, 59, 999);
68
+ return { start: startDate, end: endDate, label: formatDate(startDate.toISOString()) };
69
+ }
70
+
71
+ export const findtimeCommand = new Command('findtime')
72
+ .description('Find available meeting times with one or more people')
73
+ .argument('[start]', 'Start: today, tomorrow, monday-sunday, week, nextweek, or YYYY-MM-DD', 'nextweek')
74
+ .argument('[endOrEmails...]', 'End day for range AND/OR email addresses')
75
+ .option('--duration <minutes>', 'Meeting duration in minutes', '30')
76
+ .option('--start <hour>', 'Work day start hour (0-23)', '9')
77
+ .option('--end <hour>', 'Work day end hour (0-23)', '17')
78
+ .option('--solo', "Only check specified people, don't include yourself")
79
+ .option('--json', 'Output as JSON')
80
+ .option('--token <token>', 'Use a specific token')
81
+ .option('--identity <name>', 'Use a specific authentication identity (default: default)')
82
+ .option('--mailbox <email>', 'EWS anchor mailbox (delegated / shared mailbox context)')
83
+ .action(
84
+ async (
85
+ startDay: string,
86
+ endOrEmails: string[],
87
+ options: {
88
+ duration: string;
89
+ start: string;
90
+ end: string;
91
+ solo?: boolean;
92
+ json?: boolean;
93
+ token?: string;
94
+ identity?: string;
95
+ mailbox?: string;
96
+ },
97
+ _cmd: any
98
+ ) => {
99
+ const authResult = await resolveAuth({
100
+ token: options.token,
101
+ identity: options.identity
102
+ });
103
+
104
+ if (!authResult.success) {
105
+ if (options.json) {
106
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
107
+ } else {
108
+ console.error(`Error: ${authResult.error}`);
109
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
110
+ }
111
+ process.exit(1);
112
+ }
113
+
114
+ // Parse arguments: figure out which are dates vs emails
115
+ const dateKeywords = [
116
+ 'today',
117
+ 'tomorrow',
118
+ 'monday',
119
+ 'tuesday',
120
+ 'wednesday',
121
+ 'thursday',
122
+ 'friday',
123
+ 'saturday',
124
+ 'sunday',
125
+ 'week',
126
+ 'thisweek',
127
+ 'nextweek'
128
+ ];
129
+ const isDateArg = (arg: string) => {
130
+ if (dateKeywords.includes(arg.toLowerCase())) return true;
131
+ if (/^\d{4}-\d{2}-\d{2}$/.test(arg)) return true;
132
+ return false;
133
+ };
134
+
135
+ let endDay: string | undefined;
136
+ const emails: string[] = [];
137
+
138
+ for (const arg of endOrEmails) {
139
+ if (isDateArg(arg) && !endDay) {
140
+ endDay = arg;
141
+ continue;
142
+ }
143
+
144
+ if (!isValidEmail(arg)) {
145
+ console.error(`Error: Invalid attendee email: ${arg}`);
146
+ console.error('All attendee arguments must be valid email addresses.');
147
+ process.exit(1);
148
+ }
149
+
150
+ emails.push(arg);
151
+ }
152
+
153
+ // Get current user's email to include in search (unless --solo)
154
+ if (!options.solo) {
155
+ const userInfo = await getOwaUserInfo(authResult.token!);
156
+ if (!userInfo.ok || !userInfo.data?.email) {
157
+ if (options.json) {
158
+ console.log(JSON.stringify({ error: 'Failed to determine user email' }, null, 2));
159
+ } else {
160
+ console.error('Error: Failed to determine user email');
161
+ }
162
+ process.exit(1);
163
+ }
164
+ // Add current user if not already in the list
165
+ if (!emails.includes(userInfo.data.email)) {
166
+ emails.unshift(userInfo.data.email);
167
+ }
168
+ }
169
+
170
+ if (emails.length === 0) {
171
+ console.error('Error: Please provide at least one email address.');
172
+ console.error('\nUsage: m365-agent-cli findtime nextweek user@example.com');
173
+ process.exit(1);
174
+ }
175
+
176
+ let start: Date;
177
+ let end: Date;
178
+ let label: string;
179
+
180
+ try {
181
+ ({ start, end, label } = getDateRange(startDay, endDay));
182
+ } catch (err) {
183
+ const message = err instanceof Error ? err.message : 'Invalid date value';
184
+ if (options.json) {
185
+ console.log(JSON.stringify({ error: message }, null, 2));
186
+ } else {
187
+ console.error(`Error: ${message}`);
188
+ }
189
+ process.exit(1);
190
+ }
191
+ const duration = parseInt(options.duration, 10);
192
+
193
+ const result = await getScheduleViaOutlook(
194
+ authResult.token!,
195
+ emails,
196
+ start.toISOString(),
197
+ end.toISOString(),
198
+ duration,
199
+ undefined,
200
+ options.mailbox
201
+ );
202
+
203
+ if (!result.ok || !result.data) {
204
+ if (options.json) {
205
+ console.log(JSON.stringify({ error: result.error?.message || 'Failed to find meeting times' }, null, 2));
206
+ } else {
207
+ console.error(`Error: ${result.error?.message || 'Failed to find meeting times'}`);
208
+ }
209
+ process.exit(1);
210
+ }
211
+
212
+ // Extract free slots from the response, filtered to working hours
213
+ const workStart = parseInt(options.start, 10);
214
+ const workEnd = parseInt(options.end, 10);
215
+
216
+ const freeSlots = (result.data[0]?.scheduleItems || []).filter((item) => {
217
+ if (item.status !== 'Free') return false;
218
+ const hour = new Date(item.start.dateTime).getHours();
219
+ return hour >= workStart && hour < workEnd;
220
+ });
221
+
222
+ if (options.json) {
223
+ console.log(
224
+ JSON.stringify(
225
+ {
226
+ attendees: emails,
227
+ duration: duration,
228
+ dateRange: { start: start.toISOString(), end: end.toISOString() },
229
+ availableSlots: freeSlots.map((s) => ({
230
+ start: s.start.dateTime,
231
+ end: s.end.dateTime
232
+ }))
233
+ },
234
+ null,
235
+ 2
236
+ )
237
+ );
238
+ return;
239
+ }
240
+
241
+ console.log(`\n🗓️ Finding ${duration}-minute meeting times`);
242
+ console.log(` Attendees: ${emails.join(', ')}`);
243
+ console.log(` Date range: ${label}`);
244
+ console.log('─'.repeat(50));
245
+
246
+ if (freeSlots.length === 0) {
247
+ console.log('\n ❌ No available times found for all attendees.');
248
+ console.log(' Try a longer date range or shorter meeting duration.');
249
+ } else {
250
+ console.log(`\n ✅ Found ${freeSlots.length} available slot${freeSlots.length > 1 ? 's' : ''}:\n`);
251
+
252
+ // Group by day
253
+ const byDay = new Map<string, typeof freeSlots>();
254
+ for (const slot of freeSlots) {
255
+ const day = slot.start.dateTime.split('T')[0];
256
+ if (!byDay.has(day)) byDay.set(day, []);
257
+ byDay.get(day)?.push(slot);
258
+ }
259
+
260
+ for (const [day, slots] of byDay) {
261
+ const dayLabel = formatDate(new Date(day).toISOString());
262
+ console.log(` ${dayLabel}:`);
263
+ for (const slot of slots) {
264
+ console.log(` 🟢 ${formatTime(slot.start.dateTime)} - ${formatTime(slot.end.dateTime)}`);
265
+ }
266
+ }
267
+ }
268
+ console.log();
269
+ }
270
+ );
@@ -0,0 +1,177 @@
1
+ import { Command } from 'commander';
2
+ import { resolveAuth } from '../lib/auth.js';
3
+ import { createMailFolder, deleteMailFolder, getMailFolders, updateMailFolder } from '../lib/ews-client.js';
4
+ import { checkReadOnly } from '../lib/utils.js';
5
+
6
+ export const foldersCommand = new Command('folders')
7
+ .description('Manage mail folders')
8
+ .option('--create <name>', 'Create a new folder')
9
+ .option('--rename <name>', 'Rename a folder (use with --to)')
10
+ .option('--delete <name>', 'Delete a folder')
11
+ .option('--to <newname>', 'New name for rename operation')
12
+ .option('--json', 'Output as JSON')
13
+ .option('--token <token>', 'Use a specific token')
14
+ .option('--identity <name>', 'Use a specific authentication identity (default: default)')
15
+ .option('--mailbox <email>', 'Delegated or shared mailbox')
16
+ .action(
17
+ async (
18
+ options: {
19
+ create?: string;
20
+ rename?: string;
21
+ delete?: string;
22
+ to?: string;
23
+ json?: boolean;
24
+ token?: string;
25
+ identity?: string;
26
+ mailbox?: string;
27
+ },
28
+ cmd: any
29
+ ) => {
30
+ const authResult = await resolveAuth({
31
+ token: options.token,
32
+ identity: options.identity
33
+ });
34
+
35
+ if (!authResult.success) {
36
+ if (options.json) {
37
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
38
+ } else {
39
+ console.error(`Error: ${authResult.error}`);
40
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
41
+ }
42
+ process.exit(1);
43
+ }
44
+
45
+ // Get all folders first (needed for most operations)
46
+ const foldersResult = await getMailFolders(authResult.token!, undefined, options.mailbox);
47
+ if (!foldersResult.ok || !foldersResult.data) {
48
+ console.error(`Error: ${foldersResult.error?.message || 'Failed to fetch folders'}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const folders = foldersResult.data.value;
53
+
54
+ // Helper to find folder by name (case-insensitive)
55
+ const findFolder = (name: string) => {
56
+ return folders.find((f) => f.DisplayName.toLowerCase() === name.toLowerCase());
57
+ };
58
+
59
+ // Handle create
60
+ if (options.create) {
61
+ checkReadOnly(cmd);
62
+ const existing = findFolder(options.create);
63
+ if (existing) {
64
+ console.error(`Folder "${options.create}" already exists.`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const result = await createMailFolder(authResult.token!, options.create, undefined, options.mailbox);
69
+ if (!result.ok || !result.data) {
70
+ console.error(`Error: ${result.error?.message || 'Failed to create folder'}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ if (options.json) {
75
+ console.log(JSON.stringify({ success: true, folder: result.data }, null, 2));
76
+ } else {
77
+ console.log(`\u2713 Created folder: ${result.data.DisplayName}`);
78
+ }
79
+ return;
80
+ }
81
+
82
+ // Handle rename
83
+ if (options.rename) {
84
+ checkReadOnly(cmd);
85
+ if (!options.to) {
86
+ console.error('Please specify new name with --to');
87
+ console.error('Example: m365-agent-cli folders --rename "Old Name" --to "New Name"');
88
+ process.exit(1);
89
+ }
90
+
91
+ const folder = findFolder(options.rename);
92
+ if (!folder) {
93
+ console.error(`Folder "${options.rename}" not found.`);
94
+ process.exit(1);
95
+ }
96
+
97
+ const result = await updateMailFolder(authResult.token!, folder.Id, options.to, options.mailbox);
98
+ if (!result.ok || !result.data) {
99
+ console.error(`Error: ${result.error?.message || 'Failed to rename folder'}`);
100
+ process.exit(1);
101
+ }
102
+
103
+ if (options.json) {
104
+ console.log(JSON.stringify({ success: true, folder: result.data }, null, 2));
105
+ } else {
106
+ console.log(`\u2713 Renamed "${options.rename}" to "${result.data.DisplayName}"`);
107
+ }
108
+ return;
109
+ }
110
+
111
+ // Handle delete
112
+ if (options.delete) {
113
+ checkReadOnly(cmd);
114
+ const folder = findFolder(options.delete);
115
+ if (!folder) {
116
+ console.error(`Folder "${options.delete}" not found.`);
117
+ process.exit(1);
118
+ }
119
+
120
+ // Prevent deleting system folders
121
+ const systemFolders = ['inbox', 'drafts', 'sent items', 'deleted items', 'junk email', 'archive', 'outbox'];
122
+ if (systemFolders.includes(folder.DisplayName.toLowerCase())) {
123
+ console.error(`Cannot delete system folder "${folder.DisplayName}".`);
124
+ process.exit(1);
125
+ }
126
+
127
+ const result = await deleteMailFolder(authResult.token!, folder.Id, options.mailbox);
128
+ if (!result.ok) {
129
+ console.error(`Error: ${result.error?.message || 'Failed to delete folder'}`);
130
+ process.exit(1);
131
+ }
132
+
133
+ if (options.json) {
134
+ console.log(JSON.stringify({ success: true, deleted: options.delete }, null, 2));
135
+ } else {
136
+ console.log(`\u2713 Deleted folder: ${options.delete}`);
137
+ }
138
+ return;
139
+ }
140
+
141
+ // List folders (default action)
142
+ if (options.json) {
143
+ console.log(
144
+ JSON.stringify(
145
+ {
146
+ folders: folders.map((f) => ({
147
+ id: f.Id,
148
+ name: f.DisplayName,
149
+ unread: f.UnreadItemCount,
150
+ total: f.TotalItemCount,
151
+ childFolders: f.ChildFolderCount
152
+ }))
153
+ },
154
+ null,
155
+ 2
156
+ )
157
+ );
158
+ return;
159
+ }
160
+
161
+ console.log(`\n\ud83d\udcc1 Mail Folders${options.mailbox ? ` — ${options.mailbox}` : ''}:\n`);
162
+ console.log('\u2500'.repeat(50));
163
+
164
+ for (const folder of folders) {
165
+ const unreadBadge = folder.UnreadItemCount > 0 ? ` (${folder.UnreadItemCount} unread)` : '';
166
+ console.log(` ${folder.DisplayName}${unreadBadge}`);
167
+ console.log(` ${folder.TotalItemCount} items`);
168
+ }
169
+
170
+ console.log(`\n${'\u2500'.repeat(50)}`);
171
+ console.log('\nCommands:');
172
+ console.log(' m365-agent-cli folders --create "Folder Name"');
173
+ console.log(' m365-agent-cli folders --rename "Old" --to "New"');
174
+ console.log(' m365-agent-cli folders --delete "Folder Name"');
175
+ console.log('');
176
+ }
177
+ );
@@ -0,0 +1,49 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import { forwardEvent } from '../lib/graph-event.js';
4
+ import { checkReadOnly } from '../lib/utils.js';
5
+
6
+ export const forwardEventCommand = new Command('forward-event')
7
+ .description('Forward a calendar event to additional recipients')
8
+ .alias('forward')
9
+ .argument('<eventId>', 'The ID of the event to forward')
10
+ .argument('<recipients...>', 'Email addresses to forward the event to')
11
+ .option('--comment <text>', 'Optional comment to include in the forwarded invitation')
12
+ .option('--token <token>', 'Use a specific token')
13
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
14
+ .option('--user <email>', 'Mailbox whose calendar contains the event (delegation)')
15
+ .action(
16
+ async (
17
+ eventId: string,
18
+ recipients: string[],
19
+ options: { comment?: string; token?: string; identity?: string; user?: string },
20
+ cmd: any
21
+ ) => {
22
+ checkReadOnly(cmd);
23
+ const authResult = await resolveGraphAuth({ token: options.token, identity: options.identity });
24
+ if (!authResult.success) {
25
+ console.error(`Error: ${authResult.error}`);
26
+ process.exit(1);
27
+ }
28
+
29
+ console.log(`Forwarding event...`);
30
+ console.log(` Event ID: ${eventId}`);
31
+ console.log(` Recipients: ${recipients.join(', ')}`);
32
+ if (options.comment) console.log(` Comment: ${options.comment}`);
33
+
34
+ const response = await forwardEvent({
35
+ token: authResult.token!,
36
+ eventId,
37
+ toRecipients: recipients,
38
+ comment: options.comment,
39
+ user: options.user
40
+ });
41
+
42
+ if (!response.ok) {
43
+ console.error(`\nError: ${response.error?.message || 'Failed to forward event'}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log('\n\u2713 Successfully forwarded the event.');
48
+ }
49
+ );