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,291 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveAuth } from '../lib/auth.js';
|
|
3
|
+
import {
|
|
4
|
+
getCalendarEvent,
|
|
5
|
+
getCalendarEvents,
|
|
6
|
+
getOwaUserInfo,
|
|
7
|
+
type ResponseType,
|
|
8
|
+
respondToEvent
|
|
9
|
+
} from '../lib/ews-client.js';
|
|
10
|
+
import { checkReadOnly } from '../lib/utils.js';
|
|
11
|
+
|
|
12
|
+
function formatTime(dateStr: string): string {
|
|
13
|
+
const date = new Date(dateStr);
|
|
14
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatDate(dateStr: string): string {
|
|
18
|
+
const date = new Date(dateStr);
|
|
19
|
+
return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getResponseIcon(response: string): string {
|
|
23
|
+
switch (response) {
|
|
24
|
+
case 'Accepted':
|
|
25
|
+
return '\u2713';
|
|
26
|
+
case 'Declined':
|
|
27
|
+
return '\u2717';
|
|
28
|
+
case 'TentativelyAccepted':
|
|
29
|
+
return '?';
|
|
30
|
+
case 'None':
|
|
31
|
+
case 'NotResponded':
|
|
32
|
+
return '\u2022';
|
|
33
|
+
default:
|
|
34
|
+
return ' ';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const respondCommand = new Command('respond')
|
|
39
|
+
.description('Respond to calendar invitations (accept/decline/tentative)')
|
|
40
|
+
.argument('[action]', 'Action: list, accept, decline, tentative')
|
|
41
|
+
.argument('[eventIndex]', 'Event index from the list (deprecated; use --id)')
|
|
42
|
+
.option('--id <eventId>', 'Respond to a specific event by stable ID')
|
|
43
|
+
.option('--comment <text>', 'Add a comment to your response')
|
|
44
|
+
.option('--no-notify', "Don't send response to organizer")
|
|
45
|
+
.option('--include-optional', 'Include optional invitations (default)', true)
|
|
46
|
+
.option('--only-required', 'Only show required invitations')
|
|
47
|
+
.option('--json', 'Output as JSON')
|
|
48
|
+
.option('--token <token>', 'Use a specific token')
|
|
49
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
50
|
+
.option('--mailbox <email>', 'Respond to event in shared mailbox calendar')
|
|
51
|
+
.action(
|
|
52
|
+
async (
|
|
53
|
+
action: string | undefined,
|
|
54
|
+
_eventIndex: string | undefined,
|
|
55
|
+
options: {
|
|
56
|
+
id?: string;
|
|
57
|
+
comment?: string;
|
|
58
|
+
notify: boolean;
|
|
59
|
+
includeOptional?: boolean;
|
|
60
|
+
onlyRequired?: boolean;
|
|
61
|
+
json?: boolean;
|
|
62
|
+
token?: string;
|
|
63
|
+
identity?: string;
|
|
64
|
+
mailbox?: string;
|
|
65
|
+
},
|
|
66
|
+
cmd: any
|
|
67
|
+
) => {
|
|
68
|
+
const authResult = await resolveAuth({
|
|
69
|
+
token: options.token,
|
|
70
|
+
identity: options.identity
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!authResult.success) {
|
|
74
|
+
if (options.json) {
|
|
75
|
+
console.log(JSON.stringify({ error: authResult.error }, null, 2));
|
|
76
|
+
} else {
|
|
77
|
+
console.error(`Error: ${authResult.error}`);
|
|
78
|
+
console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
|
|
79
|
+
}
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get user's email to identify their response status
|
|
84
|
+
const userInfo = await getOwaUserInfo(authResult.token!);
|
|
85
|
+
const userEmail = userInfo.ok ? userInfo.data?.email?.toLowerCase() : undefined;
|
|
86
|
+
|
|
87
|
+
// When using a shared mailbox, the attendee email is the mailbox, not the authenticated user
|
|
88
|
+
const attendeeEmail = options.mailbox?.toLowerCase() || userEmail;
|
|
89
|
+
|
|
90
|
+
if (!attendeeEmail) {
|
|
91
|
+
if (options.json) {
|
|
92
|
+
console.log(JSON.stringify({ error: 'Failed to determine user email' }, null, 2));
|
|
93
|
+
} else {
|
|
94
|
+
console.error('Error: Failed to determine user email');
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fetch upcoming events
|
|
100
|
+
const now = new Date();
|
|
101
|
+
const futureDate = new Date(now);
|
|
102
|
+
futureDate.setDate(futureDate.getDate() + 31); // Look 31 days ahead to avoid boundary misses
|
|
103
|
+
|
|
104
|
+
const result = await getCalendarEvents(
|
|
105
|
+
authResult.token!,
|
|
106
|
+
now.toISOString(),
|
|
107
|
+
futureDate.toISOString(),
|
|
108
|
+
options.mailbox
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!result.ok || !result.data) {
|
|
112
|
+
if (options.json) {
|
|
113
|
+
console.log(JSON.stringify({ error: result.error?.message || 'Failed to fetch events' }, null, 2));
|
|
114
|
+
} else {
|
|
115
|
+
console.error(`Error: ${result.error?.message || 'Failed to fetch events'}`);
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Filter to events where user is an attendee (and not organizer)
|
|
121
|
+
const pendingEvents = result.data.filter((event) => {
|
|
122
|
+
if (event.IsCancelled) return false;
|
|
123
|
+
if (event.IsOrganizer) return false;
|
|
124
|
+
|
|
125
|
+
// Find user's attendance record
|
|
126
|
+
const myAttendance = event.Attendees?.find((a) => a.EmailAddress?.Address?.toLowerCase() === attendeeEmail);
|
|
127
|
+
|
|
128
|
+
// Some events don't include attendee records; fall back to event-level ResponseStatus if present
|
|
129
|
+
const eventResponse = (event as any).ResponseStatus?.Response as string | undefined;
|
|
130
|
+
const response = myAttendance?.Status?.Response || eventResponse || 'None';
|
|
131
|
+
|
|
132
|
+
// Include events where response is None or NotResponded
|
|
133
|
+
const isPending = response === 'None' || response === 'NotResponded';
|
|
134
|
+
if (!isPending) return false;
|
|
135
|
+
|
|
136
|
+
// Optional attendance handling (only if we can detect it)
|
|
137
|
+
const isOptional = myAttendance?.Type === 'Optional';
|
|
138
|
+
if (options.onlyRequired && isOptional) return false;
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Default action is 'list'
|
|
144
|
+
const actionLower = (action || 'list').toLowerCase();
|
|
145
|
+
|
|
146
|
+
if (actionLower === 'list') {
|
|
147
|
+
if (options.json) {
|
|
148
|
+
console.log(
|
|
149
|
+
JSON.stringify(
|
|
150
|
+
{
|
|
151
|
+
pendingEvents: pendingEvents.map((e, i) => ({
|
|
152
|
+
index: i + 1,
|
|
153
|
+
id: e.Id,
|
|
154
|
+
subject: e.Subject,
|
|
155
|
+
start: e.Start.DateTime,
|
|
156
|
+
end: e.End.DateTime,
|
|
157
|
+
organizer: e.Organizer?.EmailAddress?.Name || e.Organizer?.EmailAddress?.Address,
|
|
158
|
+
location: e.Location?.DisplayName
|
|
159
|
+
}))
|
|
160
|
+
},
|
|
161
|
+
null,
|
|
162
|
+
2
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('\nCalendar invitations awaiting your response:\n');
|
|
169
|
+
console.log('\u2500'.repeat(60));
|
|
170
|
+
|
|
171
|
+
if (pendingEvents.length === 0) {
|
|
172
|
+
console.log('\n No pending invitations found.\n');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < pendingEvents.length; i++) {
|
|
177
|
+
const event = pendingEvents[i];
|
|
178
|
+
const dateStr = formatDate(event.Start.DateTime);
|
|
179
|
+
const startTime = formatTime(event.Start.DateTime);
|
|
180
|
+
const endTime = formatTime(event.End.DateTime);
|
|
181
|
+
|
|
182
|
+
const myAttendance = event.Attendees?.find((a) => a.EmailAddress?.Address?.toLowerCase() === attendeeEmail);
|
|
183
|
+
const eventResponse = (event as any).ResponseStatus?.Response as string | undefined;
|
|
184
|
+
const response = myAttendance?.Status?.Response || eventResponse || 'None';
|
|
185
|
+
const icon = getResponseIcon(response);
|
|
186
|
+
|
|
187
|
+
console.log(`\n [${i + 1}] ${icon} ${event.Subject}`);
|
|
188
|
+
console.log(` ${dateStr} ${startTime} - ${endTime}`);
|
|
189
|
+
console.log(` ID: ${event.Id}`);
|
|
190
|
+
if (event.Location?.DisplayName) {
|
|
191
|
+
console.log(` Location: ${event.Location.DisplayName}`);
|
|
192
|
+
}
|
|
193
|
+
if (event.Organizer?.EmailAddress) {
|
|
194
|
+
const org = event.Organizer.EmailAddress;
|
|
195
|
+
console.log(` Organizer: ${org.Name || org.Address}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`\n${'\u2500'.repeat(60)}`);
|
|
200
|
+
console.log('\nTo respond, use:');
|
|
201
|
+
console.log(' m365-agent-cli respond accept --id <eventId>');
|
|
202
|
+
console.log(' m365-agent-cli respond decline --id <eventId>');
|
|
203
|
+
console.log(' m365-agent-cli respond tentative --id <eventId>');
|
|
204
|
+
console.log('');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Handle accept/decline/tentative
|
|
209
|
+
if (!['accept', 'decline', 'tentative'].includes(actionLower)) {
|
|
210
|
+
console.error(`Unknown action: ${action}`);
|
|
211
|
+
console.error('Valid actions: list, accept, decline, tentative');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
checkReadOnly(cmd);
|
|
216
|
+
|
|
217
|
+
if (!options.id) {
|
|
218
|
+
console.error('Please specify the event id with --id.');
|
|
219
|
+
console.error('Run `m365-agent-cli respond list` to see pending invitations and IDs.');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Look up the event directly to check IsOrganizer (pendingEvents filters out organizer events)
|
|
224
|
+
const eventResult = await getCalendarEvent(authResult.token!, options.id, options.mailbox);
|
|
225
|
+
if (!eventResult.ok || !eventResult.data) {
|
|
226
|
+
if (options.json) {
|
|
227
|
+
console.log(JSON.stringify({ error: `Invalid event id: ${options.id}` }, null, 2));
|
|
228
|
+
} else {
|
|
229
|
+
console.error(`Invalid event id: ${options.id}`);
|
|
230
|
+
}
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (eventResult.data.IsOrganizer) {
|
|
235
|
+
if (options.json) {
|
|
236
|
+
console.log(
|
|
237
|
+
JSON.stringify(
|
|
238
|
+
{
|
|
239
|
+
error:
|
|
240
|
+
"You are the organizer of this meeting. Use 'm365-agent-cli update-event' instead to modify the meeting."
|
|
241
|
+
},
|
|
242
|
+
null,
|
|
243
|
+
2
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
console.error(
|
|
248
|
+
"You are the organizer of this meeting. Use 'm365-agent-cli update-event' instead to modify the meeting."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const targetEvent = eventResult.data;
|
|
255
|
+
|
|
256
|
+
console.log(`\nResponding to: ${targetEvent.Subject}`);
|
|
257
|
+
console.log(
|
|
258
|
+
` ${formatDate(targetEvent.Start.DateTime)} ${formatTime(targetEvent.Start.DateTime)} - ${formatTime(targetEvent.End.DateTime)}`
|
|
259
|
+
);
|
|
260
|
+
console.log(` Action: ${actionLower}`);
|
|
261
|
+
if (options.comment) {
|
|
262
|
+
console.log(` Comment: ${options.comment}`);
|
|
263
|
+
}
|
|
264
|
+
console.log('');
|
|
265
|
+
|
|
266
|
+
const response = await respondToEvent({
|
|
267
|
+
token: authResult.token!,
|
|
268
|
+
eventId: targetEvent.Id,
|
|
269
|
+
response: actionLower as ResponseType,
|
|
270
|
+
comment: options.comment,
|
|
271
|
+
sendResponse: options.notify,
|
|
272
|
+
mailbox: options.mailbox
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (!response.ok) {
|
|
276
|
+
if (options.json) {
|
|
277
|
+
console.log(JSON.stringify({ error: response.error?.message || 'Failed to respond' }, null, 2));
|
|
278
|
+
} else {
|
|
279
|
+
console.error(`Error: ${response.error?.message || 'Failed to respond'}`);
|
|
280
|
+
}
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const actionPast = actionLower === 'tentative' ? 'tentatively accepted' : `${actionLower}d`;
|
|
285
|
+
if (options.json) {
|
|
286
|
+
console.log(JSON.stringify({ success: true, action: actionLower }, null, 2));
|
|
287
|
+
} else {
|
|
288
|
+
console.log(`\u2713 Successfully ${actionPast} the invitation.`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveGraphAuth } from '../lib/graph-auth.js';
|
|
3
|
+
import {
|
|
4
|
+
findRooms,
|
|
5
|
+
isRoomFree,
|
|
6
|
+
listPlaceRoomLists,
|
|
7
|
+
listRoomsInRoomList,
|
|
8
|
+
type Place,
|
|
9
|
+
type RoomFilters
|
|
10
|
+
} from '../lib/places-client.js';
|
|
11
|
+
|
|
12
|
+
function parseCapacityFilter(value: string | undefined): number | undefined {
|
|
13
|
+
if (!value) return undefined;
|
|
14
|
+
const cleaned = value.replaceAll('+', '').replaceAll('>', '').trim();
|
|
15
|
+
const parsed = parseInt(cleaned, 10);
|
|
16
|
+
if (Number.isNaN(parsed)) {
|
|
17
|
+
throw new Error(`Invalid capacity value: "${value}". Must be a number.`);
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseEquipmentFilter(value: string | undefined): string[] | undefined {
|
|
23
|
+
if (!value) return undefined;
|
|
24
|
+
return value
|
|
25
|
+
.split(',')
|
|
26
|
+
.map((s) => s.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const roomsCommand = new Command('rooms')
|
|
31
|
+
.description('Discover rooms and room lists via Microsoft Graph Places API')
|
|
32
|
+
.argument('[action]', 'Action: lists, rooms, or find')
|
|
33
|
+
.argument('[roomListEmail]', 'Room list email address (required for rooms action)')
|
|
34
|
+
.option('--json', 'Output as JSON')
|
|
35
|
+
.option('--token <token>', 'Use a specific token')
|
|
36
|
+
.option('--building <name>', 'Filter by building name (for find action)')
|
|
37
|
+
.option('--capacity <num>', 'Minimum room capacity (for find action)')
|
|
38
|
+
.option('--equipment <items>', 'Required equipment tags, comma-separated (for find action)')
|
|
39
|
+
.option('--start <iso>', 'Start time ISO string (for find action)')
|
|
40
|
+
.option('--end <iso>', 'End time ISO string (for find action)')
|
|
41
|
+
.option('--identity <name>', 'Graph token cache identity (default: default)')
|
|
42
|
+
.action(
|
|
43
|
+
async (
|
|
44
|
+
action: string,
|
|
45
|
+
roomListEmail: string | undefined,
|
|
46
|
+
options: {
|
|
47
|
+
json?: boolean;
|
|
48
|
+
token?: string;
|
|
49
|
+
identity?: string;
|
|
50
|
+
building?: string;
|
|
51
|
+
capacity?: string;
|
|
52
|
+
equipment?: string;
|
|
53
|
+
start?: string;
|
|
54
|
+
end?: string;
|
|
55
|
+
}
|
|
56
|
+
) => {
|
|
57
|
+
const authResult = await resolveGraphAuth({ token: options.token, identity: options.identity });
|
|
58
|
+
if (!authResult.success) {
|
|
59
|
+
console.error(`Error: ${authResult.error}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (action === 'lists' || action === undefined) {
|
|
64
|
+
console.log('Fetching room lists...');
|
|
65
|
+
const result = await listPlaceRoomLists({ token: authResult.token, identity: options.identity });
|
|
66
|
+
if (!result.ok || !result.data) {
|
|
67
|
+
console.error(`Error: ${result.error?.message || 'Failed to fetch room lists'}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const lists = result.data;
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log(JSON.stringify({ roomLists: lists }, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (lists.length === 0) {
|
|
76
|
+
console.log('No room lists found.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`\nRoom Lists (${lists.length}):\n`);
|
|
80
|
+
console.log('-'.repeat(70));
|
|
81
|
+
for (const list of lists) {
|
|
82
|
+
console.log(` ${list.displayName || '(no name)'}`);
|
|
83
|
+
if (list.emailAddress) console.log(` ${list.emailAddress}`);
|
|
84
|
+
console.log(` ID: ${list.id}`);
|
|
85
|
+
console.log('');
|
|
86
|
+
}
|
|
87
|
+
console.log('-'.repeat(70));
|
|
88
|
+
console.log('\nTip: Use "m365-agent-cli rooms rooms <email>" to see rooms.\n');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (action === 'rooms') {
|
|
93
|
+
if (!roomListEmail) {
|
|
94
|
+
console.error('Error: rooms action requires a room list email address.');
|
|
95
|
+
console.error('Use "m365-agent-cli rooms lists" to see available room lists.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
console.log(`Fetching rooms from list: ${roomListEmail}...`);
|
|
99
|
+
const result = await listRoomsInRoomList(roomListEmail, {
|
|
100
|
+
token: authResult.token,
|
|
101
|
+
identity: options.identity
|
|
102
|
+
});
|
|
103
|
+
if (!result.ok || !result.data) {
|
|
104
|
+
console.error(`Error: ${result.error?.message || 'Failed to fetch rooms'}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const rooms = result.data;
|
|
108
|
+
if (options.json) {
|
|
109
|
+
console.log(JSON.stringify({ rooms }, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (rooms.length === 0) {
|
|
113
|
+
console.log('No rooms found.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`\nRooms (${rooms.length}):\n`);
|
|
117
|
+
console.log('-'.repeat(70));
|
|
118
|
+
for (const room of rooms) {
|
|
119
|
+
console.log(` ${room.displayName || '(no name)'}`);
|
|
120
|
+
if (room.emailAddress) console.log(` ${room.emailAddress}`);
|
|
121
|
+
if (room.capacity) console.log(` Capacity: ${room.capacity}`);
|
|
122
|
+
if (room.bookingType) console.log(` Booking type: ${room.bookingType}`);
|
|
123
|
+
if (room.building) console.log(` Building: ${room.building}`);
|
|
124
|
+
if (room.floorNumber !== undefined) console.log(` Floor: ${room.floorNumber}`);
|
|
125
|
+
if (room.tags && room.tags.length > 0) console.log(` Tags: ${room.tags.join(', ')}`);
|
|
126
|
+
console.log(` ID: ${room.id}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
console.log('-'.repeat(70));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (action === 'find') {
|
|
134
|
+
let filters: RoomFilters;
|
|
135
|
+
try {
|
|
136
|
+
filters = {
|
|
137
|
+
building: options.building,
|
|
138
|
+
capacityMin: parseCapacityFilter(options.capacity),
|
|
139
|
+
equipment: parseEquipmentFilter(options.equipment)
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
const hasFilters = !!(
|
|
146
|
+
filters.building ||
|
|
147
|
+
filters.capacityMin !== undefined ||
|
|
148
|
+
(filters.equipment && filters.equipment.length > 0)
|
|
149
|
+
);
|
|
150
|
+
if (!hasFilters) {
|
|
151
|
+
console.error('Error: find action requires at least one filter (--building, --capacity, or --equipment).');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
console.log('Searching for rooms...');
|
|
155
|
+
const result = await findRooms(filters, { token: authResult.token, identity: options.identity });
|
|
156
|
+
if (!result.ok || !result.data) {
|
|
157
|
+
console.error(`Error: ${result.error?.message || 'Failed to search rooms'}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
let availableRooms = result.data;
|
|
161
|
+
if (options.start && options.end) {
|
|
162
|
+
const freeRooms: Place[] = [];
|
|
163
|
+
let availabilityCheckFailed = false;
|
|
164
|
+
for (const room of availableRooms) {
|
|
165
|
+
if (room.emailAddress) {
|
|
166
|
+
const free = await isRoomFree(authResult.token!, room.emailAddress, options.start, options.end);
|
|
167
|
+
if (free === null) {
|
|
168
|
+
availabilityCheckFailed = true;
|
|
169
|
+
freeRooms.push(room);
|
|
170
|
+
} else if (free) {
|
|
171
|
+
freeRooms.push(room);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
freeRooms.push(room);
|
|
175
|
+
availabilityCheckFailed = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
availableRooms = freeRooms;
|
|
179
|
+
if (availabilityCheckFailed) {
|
|
180
|
+
console.warn(
|
|
181
|
+
'Warning: Could not check availability for some rooms (insufficient permissions or API error).'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (options.json) {
|
|
186
|
+
console.log(JSON.stringify({ rooms: availableRooms }, null, 2));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (availableRooms.length === 0) {
|
|
190
|
+
console.log('No matching rooms found.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
console.log(`\nMatching rooms (${availableRooms.length}):\n`);
|
|
194
|
+
for (const room of availableRooms) {
|
|
195
|
+
const tags = room.tags?.length ? ` [${room.tags.join(', ')}]` : '';
|
|
196
|
+
const cap = room.capacity ? ` (cap: ${room.capacity})` : '';
|
|
197
|
+
console.log(` - ${room.displayName}${cap}${tags}`);
|
|
198
|
+
if (room.emailAddress) console.log(` ${room.emailAddress}`);
|
|
199
|
+
if (room.building) console.log(` Building: ${room.building}`);
|
|
200
|
+
console.log('');
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (action !== undefined) {
|
|
206
|
+
console.error(`Unknown action: "${action}". Use "lists", "rooms", or "find".`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
);
|