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.
- package/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- 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
|
+
);
|