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,576 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { resolveAuth } from '../lib/auth.js';
|
|
5
|
+
import {
|
|
6
|
+
businessDaysBackward,
|
|
7
|
+
businessDaysForward,
|
|
8
|
+
calendarDaysBackward,
|
|
9
|
+
calendarDaysForward,
|
|
10
|
+
isWeekRangeKeyword
|
|
11
|
+
} from '../lib/calendar-range.js';
|
|
12
|
+
import { parseDay, parseLocalDate } from '../lib/dates.js';
|
|
13
|
+
import {
|
|
14
|
+
type CalendarAttendee,
|
|
15
|
+
type CalendarEvent,
|
|
16
|
+
getAttachment,
|
|
17
|
+
getAttachments,
|
|
18
|
+
getCalendarEvent,
|
|
19
|
+
getCalendarEvents
|
|
20
|
+
} from '../lib/ews-client.js';
|
|
21
|
+
|
|
22
|
+
function sanitizeFileComponent(name: string): string {
|
|
23
|
+
const s = name.replace(/[/\\?%*:|"<>]/g, '_').trim();
|
|
24
|
+
return s.length > 0 ? s : 'attachment';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function pathExists(p: string): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
await access(p);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatTime(dateStr: string): string {
|
|
37
|
+
const date = parseLocalDate(dateStr);
|
|
38
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatDate(dateStr: string): string {
|
|
42
|
+
const date = parseLocalDate(dateStr);
|
|
43
|
+
return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert a local-midnight Date to a UTC ISO string for EWS CalendarView.
|
|
48
|
+
*
|
|
49
|
+
* EWS CalendarView StartDate/EndDate are UTC-datetime strings.
|
|
50
|
+
* CalendarView is exclusive on EndDate, so we set end = next day's local midnight.
|
|
51
|
+
*
|
|
52
|
+
* Example for UTC-5 (EST) on March 15:
|
|
53
|
+
* start = 2024-03-15T00:00 local = 2024-03-15T05:00:00Z
|
|
54
|
+
* end = 2024-03-16T00:00 local = 2024-03-16T05:00:00Z
|
|
55
|
+
*/
|
|
56
|
+
function toEWSRange(localMidnight: Date): { start: string; end: string } {
|
|
57
|
+
const start = new Date(localMidnight);
|
|
58
|
+
start.setHours(0, 0, 0, 0);
|
|
59
|
+
|
|
60
|
+
const end = new Date(start);
|
|
61
|
+
end.setDate(end.getDate() + 1);
|
|
62
|
+
end.setHours(0, 0, 0, 0);
|
|
63
|
+
|
|
64
|
+
return { start: start.toISOString(), end: end.toISOString() };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getDateRange(startDay: string, endDay?: string): { start: string; end: string; label: string } {
|
|
68
|
+
const now = new Date();
|
|
69
|
+
|
|
70
|
+
// Handle special range keywords
|
|
71
|
+
switch (startDay.toLowerCase()) {
|
|
72
|
+
case 'week':
|
|
73
|
+
case 'thisweek': {
|
|
74
|
+
const start = new Date(now);
|
|
75
|
+
const dayOfWeek = start.getDay();
|
|
76
|
+
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; // Monday
|
|
77
|
+
start.setDate(start.getDate() + diff);
|
|
78
|
+
start.setHours(0, 0, 0, 0);
|
|
79
|
+
const end = new Date(start);
|
|
80
|
+
end.setDate(end.getDate() + 7); // exclusive end = next Monday midnight
|
|
81
|
+
return { start: start.toISOString(), end: end.toISOString(), label: 'This Week' };
|
|
82
|
+
}
|
|
83
|
+
case 'lastweek': {
|
|
84
|
+
const start = new Date(now);
|
|
85
|
+
const dayOfWeek = start.getDay();
|
|
86
|
+
const diff = dayOfWeek === 0 ? -13 : -6 - dayOfWeek; // Last Monday
|
|
87
|
+
start.setDate(start.getDate() + diff);
|
|
88
|
+
start.setHours(0, 0, 0, 0);
|
|
89
|
+
const end = new Date(start);
|
|
90
|
+
end.setDate(end.getDate() + 7); // exclusive end = next Monday midnight
|
|
91
|
+
return { start: start.toISOString(), end: end.toISOString(), label: 'Last Week' };
|
|
92
|
+
}
|
|
93
|
+
case 'nextweek': {
|
|
94
|
+
const start = new Date(now);
|
|
95
|
+
const dayOfWeek = start.getDay();
|
|
96
|
+
const diff = dayOfWeek === 0 ? 1 : 8 - dayOfWeek; // Next Monday
|
|
97
|
+
start.setDate(start.getDate() + diff);
|
|
98
|
+
start.setHours(0, 0, 0, 0);
|
|
99
|
+
const end = new Date(start);
|
|
100
|
+
end.setDate(end.getDate() + 7); // exclusive end = next Monday midnight
|
|
101
|
+
return { start: start.toISOString(), end: end.toISOString(), label: 'Next Week' };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Single day or start of range
|
|
106
|
+
const startDate = parseDay(startDay, { weekdayDirection: 'previous' });
|
|
107
|
+
startDate.setHours(0, 0, 0, 0);
|
|
108
|
+
|
|
109
|
+
if (endDay) {
|
|
110
|
+
// Date range - use nearestForward for end date
|
|
111
|
+
const endDate = parseDay(endDay, { baseDate: startDate, weekdayDirection: 'nearestForward' });
|
|
112
|
+
endDate.setHours(0, 0, 0, 0);
|
|
113
|
+
// Exclusive end: next day's midnight
|
|
114
|
+
const endExclusive = new Date(endDate);
|
|
115
|
+
endExclusive.setDate(endExclusive.getDate() + 1);
|
|
116
|
+
|
|
117
|
+
const label = `${formatDate(startDate.toISOString())} - ${formatDate(endDate.toISOString())}`;
|
|
118
|
+
return { start: startDate.toISOString(), end: endExclusive.toISOString(), label };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Single day ā use toEWSRange for consistent UTC conversion
|
|
122
|
+
const { end: endISO } = toEWSRange(startDate);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
start: startDate.toISOString(),
|
|
126
|
+
end: endISO,
|
|
127
|
+
label: formatDate(startDate.toISOString())
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseCalendarRangeInt(v: string | boolean | undefined, flag: string): number | undefined {
|
|
132
|
+
if (v === undefined || v === '') {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (typeof v === 'boolean') {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const n = parseInt(String(v), 10);
|
|
139
|
+
if (!Number.isFinite(n) || n < 1 || n > 366) {
|
|
140
|
+
throw new Error(`${flag} must be an integer between 1 and 366`);
|
|
141
|
+
}
|
|
142
|
+
return n;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseAnchorDateForDynamicRange(startDay: string): Date {
|
|
146
|
+
const startDate = parseDay(startDay, { weekdayDirection: 'previous' });
|
|
147
|
+
startDate.setHours(0, 0, 0, 0);
|
|
148
|
+
return startDate;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve EWS window when using --days / --business-days / etc., or delegate to getDateRange.
|
|
153
|
+
*/
|
|
154
|
+
function resolveCalendarQueryRange(
|
|
155
|
+
startDay: string,
|
|
156
|
+
endDay: string | undefined,
|
|
157
|
+
rangeOpts: {
|
|
158
|
+
days?: number;
|
|
159
|
+
previousDays?: number;
|
|
160
|
+
businessDays?: number;
|
|
161
|
+
previousBusinessDays?: number;
|
|
162
|
+
}
|
|
163
|
+
): { start: string; end: string; label: string } {
|
|
164
|
+
const { days, previousDays, businessDays, previousBusinessDays } = rangeOpts;
|
|
165
|
+
const modeCount = [days, previousDays, businessDays, previousBusinessDays].filter((x) => x !== undefined).length;
|
|
166
|
+
|
|
167
|
+
if (modeCount === 0) {
|
|
168
|
+
return getDateRange(startDay, endDay);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (modeCount > 1) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'Use only one of: --days, --previous-days, --business-days (--busness-days), --previous-business-days'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (endDay !== undefined) {
|
|
178
|
+
throw new Error('Do not pass an end date argument when using --days / --business-days / --previous-days / ...');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (isWeekRangeKeyword(startDay)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
'Week keywords (week, thisweek, lastweek, nextweek) cannot be combined with --days / --business-days / ... ā use a single day (e.g. today) as start'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const anchor = parseAnchorDateForDynamicRange(startDay);
|
|
188
|
+
let result: { start: Date; endExclusive: Date };
|
|
189
|
+
let title: string;
|
|
190
|
+
|
|
191
|
+
if (days !== undefined) {
|
|
192
|
+
result = calendarDaysForward(anchor, days);
|
|
193
|
+
title = `Next ${days} calendar day(s)`;
|
|
194
|
+
} else if (previousDays !== undefined) {
|
|
195
|
+
result = calendarDaysBackward(anchor, previousDays);
|
|
196
|
+
title = `Previous ${previousDays} calendar day(s)`;
|
|
197
|
+
} else if (businessDays !== undefined) {
|
|
198
|
+
result = businessDaysForward(anchor, businessDays);
|
|
199
|
+
title = `Next ${businessDays} business day(s)`;
|
|
200
|
+
} else {
|
|
201
|
+
result = businessDaysBackward(anchor, previousBusinessDays!);
|
|
202
|
+
title = `Previous ${previousBusinessDays} business day(s)`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const lastInclusive = new Date(result.endExclusive);
|
|
206
|
+
lastInclusive.setDate(lastInclusive.getDate() - 1);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
start: result.start.toISOString(),
|
|
210
|
+
end: result.endExclusive.toISOString(),
|
|
211
|
+
label: `${title} (${formatDate(result.start.toISOString())} ā ${formatDate(lastInclusive.toISOString())})`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getResponseIcon(response: string): string {
|
|
216
|
+
switch (response) {
|
|
217
|
+
case 'Accepted':
|
|
218
|
+
return 'ā';
|
|
219
|
+
case 'Declined':
|
|
220
|
+
return 'ā';
|
|
221
|
+
case 'TentativelyAccepted':
|
|
222
|
+
return '?';
|
|
223
|
+
case 'NotResponded':
|
|
224
|
+
return 'Ā·';
|
|
225
|
+
case 'Organizer':
|
|
226
|
+
return 'ā
';
|
|
227
|
+
default:
|
|
228
|
+
return 'Ā·';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function displayEvent(event: CalendarEvent, verbose: boolean): void {
|
|
233
|
+
const startTime = formatTime(event.Start.DateTime);
|
|
234
|
+
const endTime = formatTime(event.End.DateTime);
|
|
235
|
+
const location = event.Location?.DisplayName || '';
|
|
236
|
+
const cancelled = event.IsCancelled ? ' [CANCELLED]' : '';
|
|
237
|
+
|
|
238
|
+
if (event.IsAllDay) {
|
|
239
|
+
console.log(` š
All day: ${event.Subject}${cancelled}`);
|
|
240
|
+
} else {
|
|
241
|
+
console.log(` ${startTime} - ${endTime}: ${event.Subject}${cancelled}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (location) {
|
|
245
|
+
console.log(` š ${location}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (event.HasAttachments) {
|
|
249
|
+
console.log(` š Has attachments`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (verbose) {
|
|
253
|
+
// Show organizer if not self
|
|
254
|
+
if (!event.IsOrganizer && event.Organizer?.EmailAddress?.Name) {
|
|
255
|
+
console.log(` š¤ Organizer: ${event.Organizer.EmailAddress.Name}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Show recurrence info
|
|
259
|
+
if (event.RecurrenceDescription) {
|
|
260
|
+
console.log(` š ${event.RecurrenceDescription}`);
|
|
261
|
+
} else if (event.FirstOccurrence || event.LastOccurrence) {
|
|
262
|
+
// Series bounds without full description
|
|
263
|
+
const first = event.FirstOccurrence ? ` from ${formatDate(event.FirstOccurrence.Start)}` : '';
|
|
264
|
+
const last = event.LastOccurrence ? ` until ${formatDate(event.LastOccurrence.Start)}` : '';
|
|
265
|
+
if (first || last) {
|
|
266
|
+
console.log(` š Recurring series${first}${last}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Show attendees
|
|
271
|
+
if (event.Attendees && event.Attendees.length > 0) {
|
|
272
|
+
const attendeeList = event.Attendees.map(
|
|
273
|
+
(a: CalendarAttendee) => `${getResponseIcon(a.Status.Response)} ${a.EmailAddress.Name}`
|
|
274
|
+
).join(', ');
|
|
275
|
+
console.log(` š„ ${attendeeList}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Show categories
|
|
279
|
+
if (event.Categories && event.Categories.length > 0) {
|
|
280
|
+
console.log(` š·ļø ${event.Categories.join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Show body preview if available
|
|
284
|
+
if (event.BodyPreview) {
|
|
285
|
+
const preview = event.BodyPreview.substring(0, 80).replace(/\n/g, ' ');
|
|
286
|
+
console.log(` š ${preview}${event.BodyPreview.length > 80 ? '...' : ''}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Show recurrence series info if available
|
|
290
|
+
if (event.FirstOccurrence || event.LastOccurrence) {
|
|
291
|
+
const first = event.FirstOccurrence ? event.FirstOccurrence.Start.substring(0, 10) : 'unknown';
|
|
292
|
+
const last = event.LastOccurrence ? event.LastOccurrence.Start.substring(0, 10) : 'unknown';
|
|
293
|
+
console.log(` š Series: First: ${first}, Last: ${last}`);
|
|
294
|
+
}
|
|
295
|
+
if (event.ModifiedOccurrences && event.ModifiedOccurrences.length > 0) {
|
|
296
|
+
console.log(
|
|
297
|
+
` āļø Modified exceptions: ${event.ModifiedOccurrences.map((o) => o.OriginalStart.substring(0, 10)).join(', ')}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (event.DeletedOccurrences && event.DeletedOccurrences.length > 0) {
|
|
301
|
+
console.log(
|
|
302
|
+
` šļø Deleted exceptions: ${event.DeletedOccurrences.map((o) => o.Start.substring(0, 10)).join(', ')}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const calendarCommand = new Command('calendar')
|
|
309
|
+
.description('View calendar events')
|
|
310
|
+
.argument(
|
|
311
|
+
'[start]',
|
|
312
|
+
'Start day: today, yesterday, tomorrow, monday-sunday, week, lastweek, nextweek, or YYYY-MM-DD',
|
|
313
|
+
'today'
|
|
314
|
+
)
|
|
315
|
+
.argument('[end]', 'End day for range (optional)')
|
|
316
|
+
.option('-v, --verbose', 'Show attendees and more details')
|
|
317
|
+
.option('--list-attachments <eventId>', 'List file and link attachments on an event by id')
|
|
318
|
+
.option('--download-attachments <eventId>', 'Download file attachments; links saved as .url shortcuts')
|
|
319
|
+
.option('-o, --output <dir>', 'Output directory for --download-attachments', '.')
|
|
320
|
+
.option('--force', 'Overwrite existing files when downloading calendar attachments')
|
|
321
|
+
.option('--json', 'Output as JSON')
|
|
322
|
+
.option('--token <token>', 'Use a specific token')
|
|
323
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
324
|
+
.option('--mailbox <email>', 'Delegated or shared mailbox calendar')
|
|
325
|
+
.option(
|
|
326
|
+
'--days <n>',
|
|
327
|
+
'Show N consecutive calendar days forward from start (includes start day; not for use with week keywords)'
|
|
328
|
+
)
|
|
329
|
+
.option('--previous-days <n>', 'Show N consecutive calendar days ending on start day')
|
|
330
|
+
.option(
|
|
331
|
+
'--business-days <n>',
|
|
332
|
+
'Show N weekdays (MonāFri) forward from start (skip weekend; use for ānext N working daysā)'
|
|
333
|
+
)
|
|
334
|
+
.option('--busness-days <n>', 'Same as --business-days (common typo)')
|
|
335
|
+
.option('--previous-business-days <n>', 'Show N weekdays backward ending on the last weekday on or before start')
|
|
336
|
+
.action(
|
|
337
|
+
async (
|
|
338
|
+
startDay: string,
|
|
339
|
+
endDay: string | undefined,
|
|
340
|
+
options: {
|
|
341
|
+
json?: boolean;
|
|
342
|
+
token?: string;
|
|
343
|
+
identity?: string;
|
|
344
|
+
verbose?: boolean;
|
|
345
|
+
mailbox?: string;
|
|
346
|
+
listAttachments?: string;
|
|
347
|
+
downloadAttachments?: string;
|
|
348
|
+
output: string;
|
|
349
|
+
force?: boolean;
|
|
350
|
+
days?: string;
|
|
351
|
+
previousDays?: string;
|
|
352
|
+
businessDays?: string;
|
|
353
|
+
busnessDays?: string;
|
|
354
|
+
previousBusinessDays?: string;
|
|
355
|
+
}
|
|
356
|
+
) => {
|
|
357
|
+
const authResult = await resolveAuth({
|
|
358
|
+
token: options.token,
|
|
359
|
+
identity: options.identity
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (!authResult.success) {
|
|
363
|
+
if (options.json) {
|
|
364
|
+
console.log(JSON.stringify({ error: authResult.error }, null, 2));
|
|
365
|
+
} else {
|
|
366
|
+
console.error(`Error: ${authResult.error}`);
|
|
367
|
+
console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
|
|
368
|
+
}
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const token = authResult.token!;
|
|
373
|
+
const mailbox = options.mailbox;
|
|
374
|
+
|
|
375
|
+
if (options.listAttachments) {
|
|
376
|
+
const eventId = options.listAttachments.trim();
|
|
377
|
+
const eventRes = await getCalendarEvent(token, eventId, mailbox);
|
|
378
|
+
if (!eventRes.ok || !eventRes.data) {
|
|
379
|
+
console.error(`Error: ${eventRes.error?.message || 'Event not found'}`);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const attsRes = await getAttachments(token, eventId, mailbox);
|
|
383
|
+
if (!attsRes.ok || !attsRes.data) {
|
|
384
|
+
console.error(`Error: ${attsRes.error?.message || 'Failed to list attachments'}`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
const atts = attsRes.data.value.filter((a) => !a.IsInline);
|
|
388
|
+
if (options.json) {
|
|
389
|
+
console.log(JSON.stringify({ eventId, subject: eventRes.data.Subject, attachments: atts }, null, 2));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
console.log(`\nAttachments ā ${eventRes.data.Subject}`);
|
|
393
|
+
console.log('ā'.repeat(40));
|
|
394
|
+
if (atts.length === 0) {
|
|
395
|
+
console.log(' (none)');
|
|
396
|
+
} else {
|
|
397
|
+
for (const a of atts) {
|
|
398
|
+
if (a.Kind === 'reference' && a.AttachLongPathName) {
|
|
399
|
+
console.log(` š ${a.Name}`);
|
|
400
|
+
console.log(` ${a.AttachLongPathName}`);
|
|
401
|
+
} else {
|
|
402
|
+
const sizeKB = Math.round(a.Size / 1024);
|
|
403
|
+
console.log(` š ${a.Name} (${sizeKB} KB)`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
console.log();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (options.downloadAttachments) {
|
|
412
|
+
const eventId = options.downloadAttachments.trim();
|
|
413
|
+
const eventRes = await getCalendarEvent(token, eventId, mailbox);
|
|
414
|
+
if (!eventRes.ok || !eventRes.data) {
|
|
415
|
+
console.error(`Error: ${eventRes.error?.message || 'Event not found'}`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
if (!eventRes.data.HasAttachments) {
|
|
419
|
+
console.log('This event has no attachments.');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const attsRes = await getAttachments(token, eventId, mailbox);
|
|
423
|
+
if (!attsRes.ok || !attsRes.data) {
|
|
424
|
+
console.error(`Error: ${attsRes.error?.message || 'Failed to fetch attachments'}`);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
const attachments = attsRes.data.value.filter((a) => !a.IsInline);
|
|
428
|
+
if (attachments.length === 0) {
|
|
429
|
+
console.log('No downloadable attachments (inline-only).');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
await mkdir(options.output, { recursive: true });
|
|
433
|
+
const usedPaths = new Set<string>();
|
|
434
|
+
console.log(`\nDownloading ${attachments.length} attachment(s) to ${options.output}/\n`);
|
|
435
|
+
for (const att of attachments) {
|
|
436
|
+
if (att.Kind === 'reference' || att.AttachLongPathName) {
|
|
437
|
+
let url = att.AttachLongPathName;
|
|
438
|
+
if (!url) {
|
|
439
|
+
const full = await getAttachment(token, eventId, att.Id, mailbox);
|
|
440
|
+
if (full.ok && full.data?.AttachLongPathName) {
|
|
441
|
+
url = full.data.AttachLongPathName;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!url) {
|
|
445
|
+
console.error(` Failed to resolve link: ${att.Name}`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const base = sanitizeFileComponent(att.Name || 'link');
|
|
449
|
+
let filePath = join(options.output, `${base}.url`);
|
|
450
|
+
let counter = 1;
|
|
451
|
+
while (usedPaths.has(filePath) || (!options.force && (await pathExists(filePath)))) {
|
|
452
|
+
filePath = join(options.output, `${base} (${counter}).url`);
|
|
453
|
+
counter++;
|
|
454
|
+
}
|
|
455
|
+
usedPaths.add(filePath);
|
|
456
|
+
const content = `[InternetShortcut]\r\nURL=${url}\r\n`;
|
|
457
|
+
await writeFile(filePath, content, 'utf8');
|
|
458
|
+
console.log(` ā ${filePath.split(/[\\/]/).pop()} (link)`);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const fullAtt = await getAttachment(token, eventId, att.Id, mailbox);
|
|
463
|
+
if (!fullAtt.ok || !fullAtt.data?.ContentBytes) {
|
|
464
|
+
console.error(` Failed to download: ${att.Name}`);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const content = Buffer.from(fullAtt.data.ContentBytes, 'base64');
|
|
468
|
+
let filePath = join(options.output, att.Name);
|
|
469
|
+
let counter = 1;
|
|
470
|
+
while (true) {
|
|
471
|
+
if (usedPaths.has(filePath)) {
|
|
472
|
+
const ext = extname(att.Name);
|
|
473
|
+
const base = att.Name.slice(0, att.Name.length - ext.length);
|
|
474
|
+
filePath = join(options.output, `${base} (${counter})${ext}`);
|
|
475
|
+
counter++;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (!options.force) {
|
|
479
|
+
try {
|
|
480
|
+
await access(filePath);
|
|
481
|
+
const ext = extname(att.Name);
|
|
482
|
+
const base = att.Name.slice(0, att.Name.length - ext.length);
|
|
483
|
+
filePath = join(options.output, `${base} (${counter})${ext}`);
|
|
484
|
+
counter++;
|
|
485
|
+
continue;
|
|
486
|
+
} catch {
|
|
487
|
+
// missing ā ok
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
usedPaths.add(filePath);
|
|
493
|
+
await writeFile(filePath, content);
|
|
494
|
+
const sizeKB = Math.round(content.length / 1024);
|
|
495
|
+
console.log(` ā ${filePath.split(/[\\/]/).pop()} (${sizeKB} KB)`);
|
|
496
|
+
}
|
|
497
|
+
console.log('\nDone.\n');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let start: string;
|
|
502
|
+
let end: string;
|
|
503
|
+
let label: string;
|
|
504
|
+
try {
|
|
505
|
+
const resolved = resolveCalendarQueryRange(startDay, endDay, {
|
|
506
|
+
days: parseCalendarRangeInt(options.days, '--days'),
|
|
507
|
+
previousDays: parseCalendarRangeInt(options.previousDays, '--previous-days'),
|
|
508
|
+
businessDays: parseCalendarRangeInt(options.businessDays ?? options.busnessDays, '--business-days'),
|
|
509
|
+
previousBusinessDays: parseCalendarRangeInt(options.previousBusinessDays, '--previous-business-days')
|
|
510
|
+
});
|
|
511
|
+
start = resolved.start;
|
|
512
|
+
end = resolved.end;
|
|
513
|
+
label = resolved.label;
|
|
514
|
+
} catch (err) {
|
|
515
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
516
|
+
if (options.json) {
|
|
517
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
518
|
+
} else {
|
|
519
|
+
console.error(`Error: ${message}`);
|
|
520
|
+
}
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const result = await getCalendarEvents(authResult.token!, start, end, options.mailbox);
|
|
525
|
+
|
|
526
|
+
if (!result.ok || !result.data) {
|
|
527
|
+
if (options.json) {
|
|
528
|
+
console.log(JSON.stringify({ error: result.error?.message || 'Failed to fetch events' }, null, 2));
|
|
529
|
+
} else {
|
|
530
|
+
console.error(`Error: ${result.error?.message || 'Failed to fetch events'}`);
|
|
531
|
+
}
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const events = result.data.filter((e) => !e.IsCancelled);
|
|
536
|
+
|
|
537
|
+
if (options.json) {
|
|
538
|
+
console.log(JSON.stringify(events, null, 2));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
console.log(`\nš Calendar for ${label}${options.mailbox ? ` ā ${options.mailbox}` : ''}`);
|
|
543
|
+
console.log('ā'.repeat(40));
|
|
544
|
+
|
|
545
|
+
if (events.length === 0) {
|
|
546
|
+
console.log(' No events scheduled.');
|
|
547
|
+
} else {
|
|
548
|
+
// Group by date for multi-day ranges
|
|
549
|
+
const eventsByDate = new Map<string, CalendarEvent[]>();
|
|
550
|
+
for (const event of events) {
|
|
551
|
+
const localDate = parseLocalDate(event.Start.DateTime);
|
|
552
|
+
const dateKey = `${localDate.getFullYear()}-${String(localDate.getMonth() + 1).padStart(2, '0')}-${String(localDate.getDate()).padStart(2, '0')}`;
|
|
553
|
+
if (!eventsByDate.has(dateKey)) {
|
|
554
|
+
eventsByDate.set(dateKey, []);
|
|
555
|
+
}
|
|
556
|
+
eventsByDate.get(dateKey)?.push(event);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check if multiple days
|
|
560
|
+
if (eventsByDate.size > 1) {
|
|
561
|
+
for (const [dateKey, dayEvents] of eventsByDate) {
|
|
562
|
+
const dayLabel = formatDate(dateKey);
|
|
563
|
+
console.log(`\n ${dayLabel}`);
|
|
564
|
+
for (const event of dayEvents) {
|
|
565
|
+
displayEvent(event, options.verbose ?? false);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
for (const event of events) {
|
|
570
|
+
displayEvent(event, options.verbose ?? false);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
console.log();
|
|
575
|
+
}
|
|
576
|
+
);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { parseDay, parseTimeToDate } from '../lib/dates.js';
|
|
3
|
+
import { resolveGraphAuth } from '../lib/graph-auth.js';
|
|
4
|
+
import { proposeNewTime } from '../lib/graph-event.js';
|
|
5
|
+
import { checkReadOnly } from '../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
export const counterCommand = new Command('counter')
|
|
8
|
+
.description('Propose a new time for a calendar event')
|
|
9
|
+
.alias('propose-new-time')
|
|
10
|
+
.argument('<eventId>', 'The ID of the event')
|
|
11
|
+
.argument('<start>', 'Proposed start time (e.g., 13:00, 1pm)')
|
|
12
|
+
.argument('<end>', 'Proposed end time (e.g., 14:00, 2pm)')
|
|
13
|
+
.option('--day <day>', 'Day for the proposed time (today, tomorrow, YYYY-MM-DD)', 'today')
|
|
14
|
+
.option('--token <token>', 'Use a specific token')
|
|
15
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
16
|
+
.option('--user <email>', 'Mailbox whose calendar contains the event (delegation)')
|
|
17
|
+
.action(
|
|
18
|
+
async (
|
|
19
|
+
eventId: string,
|
|
20
|
+
startTime: string,
|
|
21
|
+
endTime: string,
|
|
22
|
+
options: { day: string; token?: string; json?: boolean; identity?: string; user?: string },
|
|
23
|
+
cmd: any
|
|
24
|
+
) => {
|
|
25
|
+
checkReadOnly(cmd);
|
|
26
|
+
const authResult = await resolveGraphAuth({ token: options.token, identity: options.identity });
|
|
27
|
+
if (!authResult.success) {
|
|
28
|
+
console.error(`Error: ${authResult.error}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse dates and times
|
|
33
|
+
let baseDate: Date;
|
|
34
|
+
try {
|
|
35
|
+
baseDate = parseDay(options.day, { throwOnInvalid: true });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : 'Invalid day value';
|
|
38
|
+
console.error(`Error: ${message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let start: Date;
|
|
43
|
+
try {
|
|
44
|
+
start = parseTimeToDate(startTime, baseDate, { throwOnInvalid: true });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : 'Invalid start time';
|
|
47
|
+
console.error(`Error: ${message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let end: Date;
|
|
52
|
+
try {
|
|
53
|
+
end = parseTimeToDate(endTime, baseDate, { throwOnInvalid: true });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : 'Invalid end time';
|
|
56
|
+
console.error(`Error: ${message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The Graph API expects times with the time zone context. For simplicity, we can pass
|
|
61
|
+
// the ISO 8601 string and UTC as the time zone.
|
|
62
|
+
const startDateTime = start.toISOString();
|
|
63
|
+
const endDateTime = end.toISOString();
|
|
64
|
+
const timeZone = 'UTC';
|
|
65
|
+
|
|
66
|
+
console.log(`Proposing new time for event...`);
|
|
67
|
+
console.log(` Event ID: ${eventId}`);
|
|
68
|
+
console.log(` Proposed Start: ${start.toLocaleString()}`);
|
|
69
|
+
console.log(` Proposed End: ${end.toLocaleString()}`);
|
|
70
|
+
|
|
71
|
+
const response = await proposeNewTime({
|
|
72
|
+
token: authResult.token!,
|
|
73
|
+
eventId,
|
|
74
|
+
startDateTime,
|
|
75
|
+
endDateTime,
|
|
76
|
+
timeZone,
|
|
77
|
+
user: options.user
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
console.error(`\nError: ${response.error?.message || 'Failed to propose new time'}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('\n\u2713 Successfully proposed a new time for the event.');
|
|
86
|
+
}
|
|
87
|
+
);
|