@commandgarden/cli 2.3.0 → 2.4.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/dist/main.js +7 -6
- package/node_modules/@commandgarden/app/dist/client/assets/index--IDuJWmP.js +397 -0
- package/node_modules/@commandgarden/app/dist/client/assets/index-qunCIeG9.css +1 -0
- package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
- package/node_modules/@commandgarden/chrome/dist/service-worker.js +2 -1
- package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +18 -17
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +1 -0
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.eval.js +439 -0
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.yaml +48 -0
- package/node_modules/@commandgarden/shared/dist/connector.d.ts +3 -0
- package/node_modules/@commandgarden/shared/dist/connector.d.ts.map +1 -1
- package/node_modules/@commandgarden/shared/dist/connector.js +1 -0
- package/node_modules/@commandgarden/shared/dist/connector.js.map +1 -1
- package/package.json +1 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-DlAU-HAE.css +0 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-VwLyBHHl.js +0 -397
|
@@ -194,7 +194,7 @@ const __deadline = Date.now() + 30000;
|
|
|
194
194
|
while (Date.now() < __deadline) {
|
|
195
195
|
if (exists("button[aria-label='Open Scheduling Assistant']") ||
|
|
196
196
|
exists("input[aria-label='Start date']")) break;
|
|
197
|
-
await sleep(
|
|
197
|
+
await sleep(400);
|
|
198
198
|
}
|
|
199
199
|
if (!exists("button[aria-label='Open Scheduling Assistant']") &&
|
|
200
200
|
!exists("input[aria-label='Start date']")) {
|
|
@@ -205,9 +205,9 @@ if (!exists("button[aria-label='Open Scheduling Assistant']") &&
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// Open the Scheduling Assistant (free/busy view).
|
|
208
|
-
for (let i = 0; i <
|
|
208
|
+
for (let i = 0; i < 20 && !schedulingAssistantOpen(); i++) {
|
|
209
209
|
clickByName('Open Scheduling Assistant');
|
|
210
|
-
await sleep(
|
|
210
|
+
await sleep(400);
|
|
211
211
|
}
|
|
212
212
|
if (!schedulingAssistantOpen()) {
|
|
213
213
|
throw new Error('Could not open the Scheduling Assistant');
|
|
@@ -235,7 +235,7 @@ for (let i = 0; i < 30; i++) {
|
|
|
235
235
|
const di = $("input[aria-label='Start date']");
|
|
236
236
|
if (di) di.click();
|
|
237
237
|
}
|
|
238
|
-
await sleep(
|
|
238
|
+
await sleep(500);
|
|
239
239
|
}
|
|
240
240
|
if (!exists(cellSel)) {
|
|
241
241
|
// If the cell is gone, the click succeeded; otherwise error.
|
|
@@ -245,22 +245,23 @@ if (!exists(cellSel)) {
|
|
|
245
245
|
// ── Capture pre-existing mailbox IDs (organizer) ─────────────────────
|
|
246
246
|
const seen = new Set();
|
|
247
247
|
for (let i = 0; i < 3; i++) {
|
|
248
|
-
await sleep(
|
|
248
|
+
await sleep(500);
|
|
249
249
|
for (const pair of readCapture()) {
|
|
250
250
|
for (const id of schedulesFromPair(pair).keys()) seen.add(id);
|
|
251
251
|
}
|
|
252
|
+
if (seen.size > 0) break;
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
// ── Add the room ─────────────────────────────────────────────────────
|
|
255
256
|
if (isEmail) {
|
|
256
257
|
// Email -> attendee picker (resolves SMTP).
|
|
257
258
|
clickByName('Expand Required attendees');
|
|
258
|
-
await sleep(
|
|
259
|
+
await sleep(400);
|
|
259
260
|
|
|
260
261
|
const inputSel = "input[aria-label*='required attendees']";
|
|
261
|
-
for (let i = 0; i <
|
|
262
|
+
for (let i = 0; i < 20 && !exists(inputSel); i++) {
|
|
262
263
|
clickByName('Add required attendee');
|
|
263
|
-
await sleep(
|
|
264
|
+
await sleep(400);
|
|
264
265
|
}
|
|
265
266
|
if (!exists(inputSel)) {
|
|
266
267
|
throw new Error('Could not open the required-attendee field');
|
|
@@ -269,8 +270,8 @@ if (isEmail) {
|
|
|
269
270
|
|
|
270
271
|
const optionSel = `[role=option][aria-label*='${room}']`;
|
|
271
272
|
let found = false;
|
|
272
|
-
for (let i = 0; i <
|
|
273
|
-
await sleep(
|
|
273
|
+
for (let i = 0; i < 20 && !found; i++) {
|
|
274
|
+
await sleep(400);
|
|
274
275
|
found = exists(optionSel);
|
|
275
276
|
}
|
|
276
277
|
if (!found) {
|
|
@@ -280,9 +281,9 @@ if (isEmail) {
|
|
|
280
281
|
} else {
|
|
281
282
|
// Name -> room finder.
|
|
282
283
|
const inputSel = "input[aria-label='Add a room']";
|
|
283
|
-
for (let i = 0; i <
|
|
284
|
+
for (let i = 0; i < 20 && !exists(inputSel); i++) {
|
|
284
285
|
clickByName('Add a room');
|
|
285
|
-
await sleep(
|
|
286
|
+
await sleep(400);
|
|
286
287
|
}
|
|
287
288
|
if (!exists(inputSel)) {
|
|
288
289
|
throw new Error('Could not open the room finder');
|
|
@@ -290,8 +291,8 @@ if (isEmail) {
|
|
|
290
291
|
typeText(inputSel, room);
|
|
291
292
|
|
|
292
293
|
let label = null;
|
|
293
|
-
for (let i = 0; i <
|
|
294
|
-
await sleep(
|
|
294
|
+
for (let i = 0; i < 25 && !label; i++) {
|
|
295
|
+
await sleep(400);
|
|
295
296
|
const opts = Array.from(document.querySelectorAll('[role=option]'));
|
|
296
297
|
const el = opts.find(o => /capacity/i.test(o.getAttribute('aria-label') || '') && o.offsetParent);
|
|
297
298
|
if (el) {
|
|
@@ -313,8 +314,8 @@ const itemsById = new Map();
|
|
|
313
314
|
const isRoomId = id => (isEmail ? id === room.toLowerCase() : !seen.has(id));
|
|
314
315
|
|
|
315
316
|
let stable = 0;
|
|
316
|
-
for (let i = 0; i <
|
|
317
|
-
await sleep(
|
|
317
|
+
for (let i = 0; i < 50; i++) {
|
|
318
|
+
await sleep(500);
|
|
318
319
|
for (const pair of readCapture()) {
|
|
319
320
|
for (const [id, s] of schedulesFromPair(pair)) {
|
|
320
321
|
if (!isRoomId(id)) continue;
|
|
@@ -327,7 +328,7 @@ for (let i = 0; i < 30; i++) {
|
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
}
|
|
330
|
-
if (sawView) { stable += 1; if (stable >=
|
|
331
|
+
if (sawView) { stable += 1; if (stable >= 2) break; }
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
if (!roomId) {
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// Runs in page context via js_evaluate step.
|
|
2
|
+
// Drives the Outlook Scheduling Assistant to fetch free/busy timelines for
|
|
3
|
+
// multiple meeting rooms on a single day. Rooms are added sequentially so we
|
|
4
|
+
// can reliably map each scheduleId back to the input room name.
|
|
5
|
+
//
|
|
6
|
+
// Template variables interpolated before execution:
|
|
7
|
+
// ${{ args.rooms }}
|
|
8
|
+
// ${{ args.date | default("") }}
|
|
9
|
+
|
|
10
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
11
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
12
|
+
const DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
|
|
13
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
14
|
+
|
|
15
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
16
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
17
|
+
|
|
18
|
+
function todayLocalISO() {
|
|
19
|
+
const d = new Date();
|
|
20
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** "2026-06-18" -> "18, June, 2026" (matches the OWA date-cell aria-label). */
|
|
24
|
+
function dateCellLabel(iso) {
|
|
25
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
26
|
+
return `${d}, ${MONTHS[m - 1]}, ${y}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** querySelector shorthand, returns null if not found. */
|
|
30
|
+
function $(sel) { return document.querySelector(sel); }
|
|
31
|
+
|
|
32
|
+
/** True when an element matching the selector exists. */
|
|
33
|
+
function exists(sel) { return !!$(sel); }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Click a button/link by accessible name (aria-label OR visible text).
|
|
37
|
+
* Returns true if an element was found and clicked.
|
|
38
|
+
*/
|
|
39
|
+
function clickByName(name) {
|
|
40
|
+
const els = Array.from(document.querySelectorAll('button,a,[role=button]'));
|
|
41
|
+
const el = els.find(e => {
|
|
42
|
+
if (!e.offsetParent) return false;
|
|
43
|
+
const al = (e.getAttribute('aria-label') || '').trim();
|
|
44
|
+
const tx = (e.textContent || '').replace(/\s+/g, ' ').trim();
|
|
45
|
+
return al === name || al.includes(name) || tx.includes(name);
|
|
46
|
+
});
|
|
47
|
+
if (!el) return false;
|
|
48
|
+
el.scrollIntoView({ block: 'center' });
|
|
49
|
+
el.click();
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** True when the Scheduling Assistant view (with its Start date picker) is rendered. */
|
|
54
|
+
function schedulingAssistantOpen() {
|
|
55
|
+
return Array.from(document.querySelectorAll('input')).some(
|
|
56
|
+
e => (e.getAttribute('aria-label') || '') === 'Start date'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Type text into an input by selector (focus, set value, dispatch events). */
|
|
61
|
+
function typeText(sel, text) {
|
|
62
|
+
const el = $(sel);
|
|
63
|
+
if (!el) throw new Error(`Element "${sel}" not found for typing`);
|
|
64
|
+
el.focus();
|
|
65
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
66
|
+
window.HTMLInputElement.prototype, 'value'
|
|
67
|
+
).set;
|
|
68
|
+
setter.call(el, text);
|
|
69
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
70
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Clear and retype into an input using execCommand, which triggers React's internal events. */
|
|
74
|
+
function reactType(sel, text) {
|
|
75
|
+
const el = $(sel);
|
|
76
|
+
if (!el) throw new Error(`Element "${sel}" not found for typing`);
|
|
77
|
+
el.focus();
|
|
78
|
+
el.select();
|
|
79
|
+
document.execCommand('insertText', false, text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Fetch interceptor ────────────────────────────────────────────────
|
|
83
|
+
// Captures getSchedule GraphQL responses (the page's own authenticated
|
|
84
|
+
// request — the endpoint 401s on replay).
|
|
85
|
+
if (!window.__rfb) {
|
|
86
|
+
window.__rfb = [];
|
|
87
|
+
const origFetch = window.fetch;
|
|
88
|
+
window.fetch = function () {
|
|
89
|
+
const a = arguments;
|
|
90
|
+
const u = (a[0] && a[0].url) || a[0] || '';
|
|
91
|
+
const rb = (a[1] && a[1].body) || '';
|
|
92
|
+
return origFetch.apply(this, a).then(r => {
|
|
93
|
+
try {
|
|
94
|
+
if (String(u).indexOf('graphql') > -1) {
|
|
95
|
+
r.clone().text().then(t => {
|
|
96
|
+
if (t.indexOf('getSchedule') > -1 && t.indexOf('availabilityView') > -1) {
|
|
97
|
+
window.__rfb.push({ req: String(rb || ''), body: t });
|
|
98
|
+
}
|
|
99
|
+
}).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
} catch (_e) { /* ignore */ }
|
|
102
|
+
return r;
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Read and clear captured {req, body} pairs. */
|
|
108
|
+
function readCapture() {
|
|
109
|
+
const d = window.__rfb || [];
|
|
110
|
+
window.__rfb = [];
|
|
111
|
+
return d;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Map of scheduleId (lowercased) -> schedule object from a captured response body. */
|
|
115
|
+
function schedulesFromPair(pair) {
|
|
116
|
+
let body;
|
|
117
|
+
try { body = JSON.parse(pair.body); } catch (_e) { return new Map(); }
|
|
118
|
+
const byId = new Map();
|
|
119
|
+
const nodes = Array.isArray(body) ? body : [body];
|
|
120
|
+
for (const node of nodes) {
|
|
121
|
+
const schedules = node?.data?.getSchedule?.schedules;
|
|
122
|
+
if (Array.isArray(schedules)) {
|
|
123
|
+
for (const s of schedules) {
|
|
124
|
+
if (s?.scheduleId) byId.set(String(s.scheduleId).toLowerCase(), s);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return byId;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build a free/busy timeline (busy/tentative/oof/elsewhere blocks + free gaps)
|
|
133
|
+
* for one local day from getSchedule scheduleItems.
|
|
134
|
+
*/
|
|
135
|
+
function buildTimeline(items, dateIso) {
|
|
136
|
+
const PRIORITY = { oof: 4, busy: 3, tentative: 2, elsewhere: 1, free: 0 };
|
|
137
|
+
const mapStatus = st => ({
|
|
138
|
+
Busy: 'busy', Tentative: 'tentative', Oof: 'oof', WorkingElsewhere: 'elsewhere', Free: 'free',
|
|
139
|
+
}[st] || 'busy');
|
|
140
|
+
|
|
141
|
+
const dayStart = new Date(`${dateIso}T00:00:00`);
|
|
142
|
+
const dayEnd = new Date(dayStart.getTime() + 86400000);
|
|
143
|
+
const fmt = ms => {
|
|
144
|
+
if (ms >= dayEnd.getTime()) return '24:00';
|
|
145
|
+
const d = new Date(ms);
|
|
146
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const blocks = [];
|
|
150
|
+
for (const it of items) {
|
|
151
|
+
if (!it || !it.startTime || !it.endTime) continue;
|
|
152
|
+
const s = new Date(it.startTime.dateTime).getTime();
|
|
153
|
+
const e = new Date(it.endTime.dateTime).getTime();
|
|
154
|
+
const cs = Math.max(s, dayStart.getTime());
|
|
155
|
+
const ce = Math.min(e, dayEnd.getTime());
|
|
156
|
+
if (ce > cs) blocks.push({ state: mapStatus(it.status), s: cs, e: ce });
|
|
157
|
+
}
|
|
158
|
+
blocks.sort((a, b) => a.s - b.s);
|
|
159
|
+
|
|
160
|
+
// Merge overlapping/adjacent busy blocks.
|
|
161
|
+
const merged = [];
|
|
162
|
+
for (const b of blocks) {
|
|
163
|
+
const last = merged[merged.length - 1];
|
|
164
|
+
if (last && b.s <= last.e) {
|
|
165
|
+
last.e = Math.max(last.e, b.e);
|
|
166
|
+
if (PRIORITY[b.state] > PRIORITY[last.state]) last.state = b.state;
|
|
167
|
+
} else {
|
|
168
|
+
merged.push({ ...b });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const rows = [];
|
|
173
|
+
let cursor = dayStart.getTime();
|
|
174
|
+
for (const b of merged) {
|
|
175
|
+
if (b.s > cursor) rows.push({ state: 'free', s: cursor, e: b.s });
|
|
176
|
+
rows.push({ state: b.state, s: b.s, e: b.e });
|
|
177
|
+
cursor = b.e;
|
|
178
|
+
}
|
|
179
|
+
if (cursor < dayEnd.getTime()) rows.push({ state: 'free', s: cursor, e: dayEnd.getTime() });
|
|
180
|
+
|
|
181
|
+
return rows.map(r => ({
|
|
182
|
+
date: dateIso,
|
|
183
|
+
state: r.state,
|
|
184
|
+
start: fmt(r.s),
|
|
185
|
+
end: fmt(r.e),
|
|
186
|
+
durationMin: Math.round((r.e - r.s) / 60000),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Parse arguments ──────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
const rawRooms = '${{ args.rooms }}'.trim();
|
|
193
|
+
if (!rawRooms) throw new Error('Missing rooms argument — pass comma-separated room names or emails');
|
|
194
|
+
const rooms = rawRooms.split(',').map(r => r.trim()).filter(r => r.length > 0);
|
|
195
|
+
if (rooms.length === 0) throw new Error('No rooms provided after parsing');
|
|
196
|
+
|
|
197
|
+
let date = '${{ args.date | default("") }}'.trim();
|
|
198
|
+
if (!date) date = todayLocalISO();
|
|
199
|
+
if (!DATE_RE.test(date)) throw new Error(`Invalid date "${date}" — use YYYY-MM-DD`);
|
|
200
|
+
|
|
201
|
+
// ── Wait for compose form ────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
const __deadline = Date.now() + 30000;
|
|
204
|
+
while (Date.now() < __deadline) {
|
|
205
|
+
if (exists("button[aria-label='Open Scheduling Assistant']") ||
|
|
206
|
+
exists("input[aria-label='Start date']")) break;
|
|
207
|
+
await sleep(400);
|
|
208
|
+
}
|
|
209
|
+
if (!exists("button[aria-label='Open Scheduling Assistant']") &&
|
|
210
|
+
!exists("input[aria-label='Start date']")) {
|
|
211
|
+
if (/login\.|\/oauth2\/|signin|sso/i.test(location.href)) {
|
|
212
|
+
throw new Error('Not signed in to Outlook/Teams — log in and retry');
|
|
213
|
+
}
|
|
214
|
+
throw new Error('Calendar compose form did not load');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Open the Scheduling Assistant ────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < 20 && !schedulingAssistantOpen(); i++) {
|
|
220
|
+
clickByName('Open Scheduling Assistant');
|
|
221
|
+
await sleep(400);
|
|
222
|
+
}
|
|
223
|
+
if (!schedulingAssistantOpen()) {
|
|
224
|
+
throw new Error('Could not open the Scheduling Assistant');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Set the date ─────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
const cellSel = `button[aria-label='${dateCellLabel(date)}']`;
|
|
230
|
+
const target = new Date(`${date}T00:00:00`);
|
|
231
|
+
const now = new Date();
|
|
232
|
+
const navSel = target >= new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
233
|
+
? "button[aria-label^='Go to next month']"
|
|
234
|
+
: "button[aria-label^='Go to previous month']";
|
|
235
|
+
|
|
236
|
+
const dateInput = $("input[aria-label='Start date']");
|
|
237
|
+
if (dateInput) dateInput.click();
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < 30; i++) {
|
|
240
|
+
if (exists(cellSel)) {
|
|
241
|
+
$(cellSel).click();
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (exists(navSel)) {
|
|
245
|
+
try { $(navSel).click(); } catch (_e) { /* retry */ }
|
|
246
|
+
} else {
|
|
247
|
+
const di = $("input[aria-label='Start date']");
|
|
248
|
+
if (di) di.click();
|
|
249
|
+
}
|
|
250
|
+
await sleep(500);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Capture pre-existing mailbox IDs (organizer) ─────────────────────
|
|
254
|
+
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
for (let i = 0; i < 6; i++) {
|
|
257
|
+
await sleep(300);
|
|
258
|
+
for (const pair of readCapture()) {
|
|
259
|
+
for (const id of schedulesFromPair(pair).keys()) seen.add(id);
|
|
260
|
+
}
|
|
261
|
+
if (seen.size > 0) break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Add room by email (required attendees path) ──────────────────────
|
|
265
|
+
|
|
266
|
+
async function addRoomByEmail(email) {
|
|
267
|
+
clickByName('Expand Required attendees');
|
|
268
|
+
await sleep(200);
|
|
269
|
+
|
|
270
|
+
const inputSel = "input[aria-label*='required attendees']";
|
|
271
|
+
for (let i = 0; i < 20 && !exists(inputSel); i++) {
|
|
272
|
+
clickByName('Add required attendee');
|
|
273
|
+
await sleep(200);
|
|
274
|
+
}
|
|
275
|
+
if (!exists(inputSel)) {
|
|
276
|
+
throw new Error('Could not open the required-attendee field');
|
|
277
|
+
}
|
|
278
|
+
typeText(inputSel, email);
|
|
279
|
+
|
|
280
|
+
const optionSel = `[role=option][aria-label*='${email}']`;
|
|
281
|
+
let found = false;
|
|
282
|
+
for (let i = 0; i < 40 && !found; i++) {
|
|
283
|
+
await sleep(200);
|
|
284
|
+
found = exists(optionSel);
|
|
285
|
+
}
|
|
286
|
+
if (!found) {
|
|
287
|
+
throw new Error(`Room "${email}" was not found in the directory`);
|
|
288
|
+
}
|
|
289
|
+
$(optionSel).click();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Add room by name (room finder path) ──────────────────────────────
|
|
293
|
+
|
|
294
|
+
async function addRoomByName(name) {
|
|
295
|
+
const inputSel = "input[aria-label='Add a room']";
|
|
296
|
+
|
|
297
|
+
// If the input is already visible (reuse after dismissRoom), clear and retype.
|
|
298
|
+
// Otherwise, click "Add a room" to open a fresh finder.
|
|
299
|
+
if (exists(inputSel)) {
|
|
300
|
+
reactType(inputSel, name);
|
|
301
|
+
// Wait for OWA to process the search and refresh suggestions.
|
|
302
|
+
await sleep(1000);
|
|
303
|
+
} else {
|
|
304
|
+
for (let i = 0; i < 30 && !exists(inputSel); i++) {
|
|
305
|
+
clickByName('Add a room');
|
|
306
|
+
await sleep(200);
|
|
307
|
+
}
|
|
308
|
+
if (!exists(inputSel)) {
|
|
309
|
+
throw new Error('Could not open the room finder');
|
|
310
|
+
}
|
|
311
|
+
typeText(inputSel, name);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let label = null;
|
|
315
|
+
for (let i = 0; i < 40 && !label; i++) {
|
|
316
|
+
await sleep(200);
|
|
317
|
+
const opts = Array.from(document.querySelectorAll('[role=option]'));
|
|
318
|
+
const el = opts.find(o => /capacity/i.test(o.getAttribute('aria-label') || '') && o.offsetParent);
|
|
319
|
+
if (el) {
|
|
320
|
+
el.scrollIntoView({ block: 'center' });
|
|
321
|
+
el.click();
|
|
322
|
+
label = el.getAttribute('aria-label');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!label) {
|
|
326
|
+
throw new Error(`No room matched "${name}"`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Dismiss room: remove the just-added room so "Add a room" reappears ──
|
|
331
|
+
|
|
332
|
+
async function dismissRoom() {
|
|
333
|
+
// Click the "Remove <room>" button on the most recently added room pill.
|
|
334
|
+
// OWA labels these as e.g. "Remove MY_Site_Building_L4_Room Name."
|
|
335
|
+
const removeBtn = Array.from(document.querySelectorAll('button'))
|
|
336
|
+
.filter(el => el.offsetParent)
|
|
337
|
+
.find(el => (el.getAttribute('aria-label') || '').startsWith('Remove '));
|
|
338
|
+
if (removeBtn) {
|
|
339
|
+
removeBtn.scrollIntoView({ block: 'center' });
|
|
340
|
+
removeBtn.click();
|
|
341
|
+
await sleep(200);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await sleep(200);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Capture schedule for the most recently added room ────────────────
|
|
348
|
+
|
|
349
|
+
async function captureNewSchedule(knownIds) {
|
|
350
|
+
let newId = null;
|
|
351
|
+
let schedule = null;
|
|
352
|
+
let sawView = false;
|
|
353
|
+
let errMsg = null;
|
|
354
|
+
const itemsById = new Map();
|
|
355
|
+
|
|
356
|
+
for (let i = 0; i < 60; i++) {
|
|
357
|
+
await sleep(100);
|
|
358
|
+
for (const pair of readCapture()) {
|
|
359
|
+
for (const [id, s] of schedulesFromPair(pair)) {
|
|
360
|
+
if (knownIds.has(id)) continue;
|
|
361
|
+
if (!newId) newId = id;
|
|
362
|
+
if (id !== newId) continue;
|
|
363
|
+
schedule = s;
|
|
364
|
+
if (s.error) errMsg = s.error.message || s.error.responseCode || 'unknown';
|
|
365
|
+
if (s.availabilityView && s.availabilityView.length) sawView = true;
|
|
366
|
+
for (const it of (s.scheduleItems || [])) {
|
|
367
|
+
const key = it && it.id ? it.id : JSON.stringify(it && [it.startTime, it.endTime, it.subject]);
|
|
368
|
+
if (it) itemsById.set(key, it);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (sawView) break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return { newId, schedule, sawView, errMsg, items: [...itemsById.values()] };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Sequential room processing ───────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
const allRows = [];
|
|
381
|
+
const knownIds = new Set(seen);
|
|
382
|
+
|
|
383
|
+
for (const room of rooms) {
|
|
384
|
+
const isEmail = EMAIL_RE.test(room);
|
|
385
|
+
|
|
386
|
+
// Clear any pending captures before adding this room.
|
|
387
|
+
readCapture();
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
if (isEmail) {
|
|
391
|
+
await addRoomByEmail(room);
|
|
392
|
+
} else {
|
|
393
|
+
await addRoomByName(room);
|
|
394
|
+
}
|
|
395
|
+
} catch (addErr) {
|
|
396
|
+
allRows.push({
|
|
397
|
+
roomName: room, roomEmail: '', date,
|
|
398
|
+
state: 'error', start: addErr.message, end: '', durationMin: 0,
|
|
399
|
+
});
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Capture the getSchedule response immediately after adding (before dismiss).
|
|
404
|
+
const result = await captureNewSchedule(knownIds);
|
|
405
|
+
|
|
406
|
+
// Remove the room so "Add a room" reappears for the next room.
|
|
407
|
+
await dismissRoom();
|
|
408
|
+
|
|
409
|
+
if (!result.newId) {
|
|
410
|
+
allRows.push({
|
|
411
|
+
roomName: room, roomEmail: '', date,
|
|
412
|
+
state: 'error', start: `No free/busy returned for "${room}" on ${date}`,
|
|
413
|
+
end: '', durationMin: 0,
|
|
414
|
+
});
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
knownIds.add(result.newId);
|
|
419
|
+
|
|
420
|
+
if (result.errMsg && !result.sawView) {
|
|
421
|
+
allRows.push({
|
|
422
|
+
roomName: room, roomEmail: result.newId, date,
|
|
423
|
+
state: 'error', start: `Free/busy error: ${result.errMsg}`,
|
|
424
|
+
end: '', durationMin: 0,
|
|
425
|
+
});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const timeline = buildTimeline(result.items, date);
|
|
430
|
+
for (const r of timeline) {
|
|
431
|
+
allRows.push({ roomName: room, roomEmail: result.newId, ...r });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (allRows.length === 0) {
|
|
436
|
+
throw new Error(`No results for any of the ${rooms.length} room(s) on ${date}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return allRows;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
site: teams
|
|
2
|
+
name: rooms-availability
|
|
3
|
+
version: "1.0"
|
|
4
|
+
description: "Meeting-room free/busy timeline for multiple rooms on a given day (Teams/Outlook calendar)"
|
|
5
|
+
access: read
|
|
6
|
+
cdp: true
|
|
7
|
+
|
|
8
|
+
domains:
|
|
9
|
+
- "outlook.cloud.microsoft.mcas.ms"
|
|
10
|
+
capabilities:
|
|
11
|
+
- navigate
|
|
12
|
+
- js_evaluate
|
|
13
|
+
|
|
14
|
+
args:
|
|
15
|
+
- name: rooms
|
|
16
|
+
type: string
|
|
17
|
+
required: true
|
|
18
|
+
help: "Comma-separated room names or emails (e.g. 'MBTMY The Vista,MBTMY The Cliffside' or 'RES-A@mb.com,RES-B@mb.com')"
|
|
19
|
+
- name: date
|
|
20
|
+
type: string
|
|
21
|
+
required: false
|
|
22
|
+
help: "Day to check in YYYY-MM-DD format (default: today)"
|
|
23
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$"
|
|
24
|
+
|
|
25
|
+
columns:
|
|
26
|
+
- name: roomName
|
|
27
|
+
type: string
|
|
28
|
+
- name: roomEmail
|
|
29
|
+
type: string
|
|
30
|
+
- name: date
|
|
31
|
+
type: string
|
|
32
|
+
- name: state
|
|
33
|
+
type: string
|
|
34
|
+
- name: start
|
|
35
|
+
type: string
|
|
36
|
+
- name: end
|
|
37
|
+
type: string
|
|
38
|
+
- name: durationMin
|
|
39
|
+
type: number
|
|
40
|
+
|
|
41
|
+
pipeline:
|
|
42
|
+
- step: navigate
|
|
43
|
+
url: "https://outlook.cloud.microsoft.mcas.ms/calendar/deeplink/compose"
|
|
44
|
+
- step: wait
|
|
45
|
+
selector: "body"
|
|
46
|
+
timeout: 30000
|
|
47
|
+
- step: js_evaluate
|
|
48
|
+
file: teams-rooms-availability.eval.js
|
|
@@ -40,6 +40,7 @@ export declare const connectorSchema: z.ZodObject<{
|
|
|
40
40
|
version: z.ZodString;
|
|
41
41
|
description: z.ZodOptional<z.ZodString>;
|
|
42
42
|
access: z.ZodDefault<z.ZodEnum<["read", "write"]>>;
|
|
43
|
+
cdp: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
43
44
|
vars: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
44
45
|
domains: z.ZodArray<z.ZodString, "many">;
|
|
45
46
|
capabilities: z.ZodArray<z.ZodEnum<["navigate", "cookie_read", "cookie_write", "dom_read", "dom_write", "intercept_response", "js_evaluate"]>, "many">;
|
|
@@ -341,6 +342,7 @@ export declare const connectorSchema: z.ZodObject<{
|
|
|
341
342
|
site: string;
|
|
342
343
|
version: string;
|
|
343
344
|
access: "read" | "write";
|
|
345
|
+
cdp: boolean;
|
|
344
346
|
domains: string[];
|
|
345
347
|
capabilities: ("navigate" | "cookie_read" | "cookie_write" | "dom_read" | "dom_write" | "intercept_response" | "js_evaluate")[];
|
|
346
348
|
args: {
|
|
@@ -530,6 +532,7 @@ export declare const connectorSchema: z.ZodObject<{
|
|
|
530
532
|
})[];
|
|
531
533
|
description?: string | undefined;
|
|
532
534
|
access?: "read" | "write" | undefined;
|
|
535
|
+
cdp?: boolean | undefined;
|
|
533
536
|
vars?: Record<string, unknown> | undefined;
|
|
534
537
|
args?: {
|
|
535
538
|
type: "string" | "number" | "boolean";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;EAQtB,CAAC;AAEH,QAAA,MAAM,qBAAqB;;;;;;;;;EAGzB,CAAC;AAEH,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;EAQtB,CAAC;AAEH,QAAA,MAAM,qBAAqB;;;;;;;;;EAGzB,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAa1B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
|
@@ -21,6 +21,7 @@ export const connectorSchema = z.object({
|
|
|
21
21
|
version: z.string(),
|
|
22
22
|
description: z.string().optional(),
|
|
23
23
|
access: z.enum(['read', 'write']).default('read'),
|
|
24
|
+
cdp: z.boolean().optional().default(false),
|
|
24
25
|
vars: z.record(z.unknown()).optional(),
|
|
25
26
|
domains: z.array(z.string()).min(1),
|
|
26
27
|
capabilities: z.array(capabilityEnum).min(1),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAE5C,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC7C,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACpC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;CAC9C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACjD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;IACtC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACxD,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CAC7C,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"connector.js","sourceRoot":"","sources":["../src/connector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAE5C,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC7C,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACpC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAEH,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;CAC9C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACjD,GAAG,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IAC1C,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE;IACtC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACxD,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9D,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CAC7C,CAAC,CAAC"}
|