@commandgarden/cli 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/main.js +0 -0
  2. package/node_modules/@commandgarden/app/dist/client/assets/Slides-0sL-MDMf.js +31 -0
  3. package/node_modules/@commandgarden/app/dist/client/assets/index-Bzw5cF_s.js +461 -0
  4. package/node_modules/@commandgarden/app/dist/client/assets/index-DrFywqdr.css +1 -0
  5. package/node_modules/@commandgarden/app/dist/client/index.html +19 -19
  6. package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.d.ts +1 -1
  7. package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.js +2 -2
  8. package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.d.ts +12 -13
  9. package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.js +70 -17
  10. package/node_modules/@commandgarden/app/dist/server/store.js +35 -35
  11. package/node_modules/@commandgarden/app/package.json +42 -42
  12. package/node_modules/@commandgarden/chrome/dist/content-script.js.map +1 -1
  13. package/node_modules/@commandgarden/chrome/dist/manifest.json +16 -16
  14. package/node_modules/@commandgarden/chrome/dist/popup.html +218 -218
  15. package/node_modules/@commandgarden/chrome/dist/popup.js.map +1 -1
  16. package/node_modules/@commandgarden/chrome/dist/service-worker.js +68 -13
  17. package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
  18. package/node_modules/@commandgarden/chrome/package.json +25 -25
  19. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +100 -143
  20. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +70 -70
  21. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +58 -58
  22. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +47 -47
  23. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -115
  24. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -54
  25. package/node_modules/@commandgarden/daemon/connectors/lib/msal-token.js +20 -20
  26. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +198 -186
  27. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +42 -41
  28. package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.eval.js +94 -94
  29. package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.yaml +34 -34
  30. package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -63
  31. package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +437 -343
  32. package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +53 -47
  33. package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.eval.js +547 -547
  34. package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.yaml +48 -48
  35. package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.eval.js +52 -52
  36. package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.yaml +42 -42
  37. package/node_modules/@commandgarden/daemon/connectors/timetracking-report.eval.js +81 -81
  38. package/node_modules/@commandgarden/daemon/connectors/timetracking-report.yaml +52 -52
  39. package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.eval.js +27 -27
  40. package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.yaml +38 -38
  41. package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -55
  42. package/node_modules/@commandgarden/daemon/dist/audit-store.js +8 -8
  43. package/node_modules/@commandgarden/daemon/dist/ws-relay.js +1 -1
  44. package/node_modules/@commandgarden/daemon/package.json +32 -32
  45. package/node_modules/@commandgarden/shared/package.json +32 -32
  46. package/package.json +58 -58
  47. package/node_modules/@commandgarden/app/dist/client/assets/index-DCptfmmK.js +0 -397
  48. package/node_modules/@commandgarden/app/dist/client/assets/index-qunCIeG9.css +0 -1
  49. package/node_modules/@commandgarden/app/dist/server/daemon-client.test.js +0 -47
  50. package/node_modules/@commandgarden/app/dist/server/routes/routes.test.js +0 -137
  51. package/node_modules/@commandgarden/app/dist/server/store.test.js +0 -59
  52. package/node_modules/@commandgarden/chrome/dist/fetch-interceptor.js +0 -44
  53. package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts +0 -24
  54. package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts.map +0 -1
  55. package/node_modules/@commandgarden/daemon/dist/cdp-client.js +0 -150
  56. package/node_modules/@commandgarden/daemon/dist/cdp-client.js.map +0 -1
@@ -1,186 +1,198 @@
1
- // Runs in page context via js_evaluate step.
2
- // Captures the user's calendar events by intercepting OWA's own data fetches.
3
- //
4
- // Strategy: patch fetch() and XMLHttpRequest to capture responses containing
5
- // calendar event data. OWA loads calendar events automatically when the
6
- // calendar page openswe just wait and read what it fetched.
7
- //
8
- // This avoids all auth issues (no tokens, no CANARY, no cross-origin) because
9
- // we're reading data OWA already fetched for itself.
10
- //
11
- // Template variables interpolated before execution:
12
- // ${{ args.date }}
13
-
14
- function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
15
- function pad2(n) { return String(n).padStart(2, '0'); }
16
-
17
- const DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
18
- const date = '${{ args.date }}'.trim();
19
- if (!DATE_RE.test(date)) throw new Error(`Invalid date "${date}" — use YYYY-MM-DD`);
20
-
21
- // ── Wait for OWA to finish SSO/redirect (up to 30s) ──────────────────
22
- const __deadline = Date.now() + 30000;
23
- while (Date.now() < __deadline) {
24
- if (location.hostname.includes('outlook') && !(/login\.|\/oauth2\/|signin|sso/i.test(location.href))) break;
25
- await sleep(1000);
26
- }
27
- if (/login\.|\/oauth2\/|signin|sso/i.test(location.href)) {
28
- throw new Error('Not signed in to Outlook — log in and retry');
29
- }
30
-
31
- // ── Install interceptors to capture calendar event data ───────────────
32
- // OWA uses both fetch() and XMLHttpRequest for API calls. We patch both
33
- // to capture any response containing calendar events (Items, calendarView,
34
- // getSchedule, FindItem).
35
- if (!window.__calEvents) {
36
- window.__calEvents = [];
37
-
38
- // Patch fetch
39
- const origFetch = window.fetch;
40
- window.fetch = function () {
41
- const args = arguments;
42
- return origFetch.apply(this, args).then(r => {
43
- try {
44
- r.clone().text().then(t => {
45
- // Capture responses that look like calendar event data
46
- if (t.includes('"Subject"') && (t.includes('"Start"') || t.includes('"StartTime"'))) {
47
- try {
48
- const parsed = JSON.parse(t);
49
- extractEvents(parsed);
50
- } catch (_) {}
51
- }
52
- // Also capture getSchedule GraphQL responses (fallback)
53
- if (t.includes('getSchedule') && t.includes('scheduleItems')) {
54
- try {
55
- const parsed = JSON.parse(t);
56
- extractScheduleItems(parsed);
57
- } catch (_) {}
58
- }
59
- }).catch(() => {});
60
- } catch (_) {}
61
- return r;
62
- });
63
- };
64
-
65
- // Patch XMLHttpRequest (OWA uses this for some API calls)
66
- const origXHROpen = XMLHttpRequest.prototype.open;
67
- const origXHRSend = XMLHttpRequest.prototype.send;
68
- XMLHttpRequest.prototype.open = function () {
69
- this.__url = arguments[1] || '';
70
- return origXHROpen.apply(this, arguments);
71
- };
72
- XMLHttpRequest.prototype.send = function () {
73
- this.addEventListener('load', function () {
74
- try {
75
- const t = this.responseText || '';
76
- if (t.includes('"Subject"') && (t.includes('"Start"') || t.includes('"StartTime"'))) {
77
- try { extractEvents(JSON.parse(t)); } catch (_) {}
78
- }
79
- } catch (_) {}
80
- });
81
- return origXHRSend.apply(this, arguments);
82
- };
83
- }
84
-
85
- /** Extract calendar events from OWA API response shapes. */
86
- function extractEvents(data) {
87
- // OWA returns events in various shapes; try common patterns
88
- const items = data.value || data.Items || data.items || [];
89
- const arr = Array.isArray(data) ? data : (Array.isArray(items) ? items : []);
90
- for (const item of arr) {
91
- if (item && (item.Subject || item.subject) && (item.Start || item.start)) {
92
- window.__calEvents.push(item);
93
- }
94
- }
95
- }
96
-
97
- /** Extract events from getSchedule GraphQL responses (fallback path). */
98
- function extractScheduleItems(data) {
99
- const nodes = Array.isArray(data) ? data : [data];
100
- for (const node of nodes) {
101
- const schedules = node?.data?.getSchedule?.schedules;
102
- if (!Array.isArray(schedules)) continue;
103
- for (const s of schedules) {
104
- for (const it of (s.scheduleItems || [])) {
105
- if (it && it.startTime && it.endTime) {
106
- // Convert getSchedule format to calendarView-like format
107
- window.__calEvents.push({
108
- subject: it.subject || '(meeting)',
109
- start: it.startTime,
110
- end: it.endTime,
111
- showAs: it.status || 'Busy',
112
- responseStatus: { response: 'Accepted' },
113
- isAllDayEvent: false,
114
- isCancelled: false,
115
- _fromSchedule: true,
116
- });
117
- }
118
- }
119
- }
120
- }
121
- }
122
-
123
- // ── Wait for OWA to load calendar data (up to 20s) ───────────────────
124
- for (let i = 0; i < 20; i++) {
125
- await sleep(1000);
126
- if (window.__calEvents.length > 0) break;
127
- }
128
-
129
- // Deduplicate events by start+end time
130
- const seen = new Set();
131
- const uniqueEvents = [];
132
- for (const ev of window.__calEvents) {
133
- const startStr = ev.Start?.DateTime || ev.start?.dateTime || ev.start?.DateTime || '';
134
- const endStr = ev.End?.DateTime || ev.end?.dateTime || ev.end?.DateTime || '';
135
- const key = startStr + '|' + endStr;
136
- if (seen.has(key)) continue;
137
- seen.add(key);
138
- uniqueEvents.push(ev);
139
- }
140
-
141
- // Clear captured events for next run
142
- window.__calEvents = [];
143
-
144
- // ── Filter to the target date and map to output rows ──────────────────
145
- const rows = [];
146
- for (const ev of uniqueEvents) {
147
- // Normalize field names (OWA mixes PascalCase and camelCase)
148
- const subject = ev.Subject || ev.subject || '(meeting)';
149
- const startRaw = ev.Start?.DateTime || ev.start?.dateTime || ev.start?.DateTime || '';
150
- const endRaw = ev.End?.DateTime || ev.end?.dateTime || ev.end?.DateTime || '';
151
- const showAs = (ev.ShowAs || ev.showAs || 'busy');
152
- const isCancelled = ev.IsCancelled || ev.isCancelled || false;
153
- const isAllDay = ev.IsAllDayEvent || ev.isAllDayEvent || false;
154
- const responseType = ev.ResponseStatus?.Response || ev.responseStatus?.response || '';
155
-
156
- if (isCancelled || isAllDay) continue;
157
-
158
- // Filter by response status if available (accepted/organizer only)
159
- // If responseStatus is missing (e.g. from getSchedule), include the event
160
- if (responseType && responseType !== 'Organizer' && responseType !== 'Accepted') continue;
161
-
162
- // Only show busy/tentative items (skip free blocks from getSchedule)
163
- const showAsLower = String(showAs).toLowerCase();
164
- if (showAsLower === 'free') continue;
165
-
166
- const startDt = new Date(startRaw.endsWith('Z') ? startRaw : startRaw + 'Z');
167
- const endDt = new Date(endRaw.endsWith('Z') ? endRaw : endRaw + 'Z');
168
- const evDate = `${startDt.getFullYear()}-${pad2(startDt.getMonth() + 1)}-${pad2(startDt.getDate())}`;
169
-
170
- // Filter to the target date
171
- if (evDate !== date) continue;
172
-
173
- const durationMin = Math.round((endDt.getTime() - startDt.getTime()) / 60000);
174
- if (durationMin < 5 || durationMin > 480) continue;
175
-
176
- rows.push({
177
- date: evDate,
178
- subject: subject,
179
- start: `${pad2(startDt.getHours())}:${pad2(startDt.getMinutes())}`,
180
- end: `${pad2(endDt.getHours())}:${pad2(endDt.getMinutes())}`,
181
- durationMin,
182
- state: showAsLower,
183
- });
184
- }
185
-
186
- return rows;
1
+ // Runs in page context via js_evaluate step (executed by CDP Runtime.evaluate
2
+ // to bypass Outlook's CSP see chrome-adapter.ts evaluateViaCdp).
3
+ //
4
+ // Strategy: drives the OWA Scheduling Assistant (same as room-availability)
5
+ // to trigger a getSchedule GraphQL call. The organizer's own schedule is
6
+ // always the first entry no room/attendee needs to be added.
7
+ //
8
+ // CDP Fetch.enable intercepts getSchedule at the network stack level
9
+ // (below MCAS proxy), and chrome-adapter.ts injects matching responses
10
+ // into window.__rfb. This eval.js reads from __rfb.
11
+ //
12
+ // Requires cdp: true in the connector YAML.
13
+ //
14
+ // Template variables interpolated before execution:
15
+ // ${{ args.date }}
16
+
17
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June',
18
+ 'July', 'August', 'September', 'October', 'November', 'December'];
19
+ const DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
20
+
21
+ function pad2(n) { return String(n).padStart(2, '0'); }
22
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
23
+
24
+ function todayLocalISO() {
25
+ const d = new Date();
26
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
27
+ }
28
+
29
+ /** "2026-07-10" -> "10, July, 2026" (matches the OWA date-cell aria-label). */
30
+ function dateCellLabel(iso) {
31
+ const [y, m, d] = iso.split('-').map(Number);
32
+ return `${d}, ${MONTHS[m - 1]}, ${y}`;
33
+ }
34
+
35
+ function $(sel) { return document.querySelector(sel); }
36
+ function exists(sel) { return !!$(sel); }
37
+
38
+ /** Click a button/link by accessible name. Returns true if found. */
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 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
+ /** Read and clear captured {req, body} pairs from CDP layer. */
61
+ function readCapture() {
62
+ const d = window.__rfb || [];
63
+ window.__rfb = [];
64
+ return d;
65
+ }
66
+
67
+ /** Parse getSchedule response → Map<scheduleId, schedule>. */
68
+ function schedulesFromPair(pair) {
69
+ let body;
70
+ try { body = JSON.parse(pair.body); } catch (_e) { return new Map(); }
71
+ const byId = new Map();
72
+ const nodes = Array.isArray(body) ? body : [body];
73
+ for (const node of nodes) {
74
+ const schedules = node?.data?.getSchedule?.schedules;
75
+ if (Array.isArray(schedules)) {
76
+ for (const s of schedules) {
77
+ if (s?.scheduleId) byId.set(String(s.scheduleId).toLowerCase(), s);
78
+ }
79
+ }
80
+ }
81
+ return byId;
82
+ }
83
+
84
+ // ── Parse args ────────────────────────────────────────────────────────
85
+ const date = '${{ args.date }}'.trim();
86
+ if (!DATE_RE.test(date)) throw new Error(`Invalid date "${date}" — use YYYY-MM-DD`);
87
+
88
+ // ── Wait for compose form (handles SSO redirect delays) ──────────────
89
+ const __deadline = Date.now() + 30000;
90
+ while (Date.now() < __deadline) {
91
+ if (exists("button[aria-label='Open Scheduling Assistant']") ||
92
+ exists("input[aria-label='Start date']")) break;
93
+ await sleep(400);
94
+ }
95
+ if (!exists("button[aria-label='Open Scheduling Assistant']") &&
96
+ !exists("input[aria-label='Start date']")) {
97
+ if (/login\.|\/oauth2\/|signin|sso/i.test(location.href)) {
98
+ throw new Error('Not signed in to Outlook — log in and retry');
99
+ }
100
+ throw new Error('Calendar compose form did not load');
101
+ }
102
+
103
+ // ── Open the Scheduling Assistant ────────────────────────────────────
104
+ for (let i = 0; i < 20 && !schedulingAssistantOpen(); i++) {
105
+ clickByName('Open Scheduling Assistant');
106
+ await sleep(400);
107
+ }
108
+ if (!schedulingAssistantOpen()) {
109
+ throw new Error('Could not open the Scheduling Assistant');
110
+ }
111
+
112
+ // ── Set the date ─────────────────────────────────────────────────────
113
+ // Clear any initial __rfb data from the default date (usually today),
114
+ // then set the target date so getSchedule fires fresh.
115
+ readCapture();
116
+
117
+ const cellSel = `button[aria-label='${dateCellLabel(date)}']`;
118
+ const target = new Date(`${date}T00:00:00`);
119
+ const now = new Date();
120
+ const navSel = target >= new Date(now.getFullYear(), now.getMonth(), now.getDate())
121
+ ? "button[aria-label^='Go to next month']"
122
+ : "button[aria-label^='Go to previous month']";
123
+
124
+ const dateInput = $("input[aria-label='Start date']");
125
+ if (dateInput) dateInput.click();
126
+
127
+ for (let i = 0; i < 30; i++) {
128
+ if (exists(cellSel)) {
129
+ $(cellSel).click();
130
+ break;
131
+ }
132
+ if (exists(navSel)) {
133
+ try { $(navSel).click(); } catch (_e) { /* retry */ }
134
+ } else {
135
+ const di = $("input[aria-label='Start date']");
136
+ if (di) di.click();
137
+ }
138
+ await sleep(500);
139
+ }
140
+
141
+ // ── Read the organizer's schedule from CDP-captured getSchedule ──────
142
+ // The Scheduling Assistant always includes the organizer's own schedule
143
+ // as the first entry in getSchedule. No room/attendee needed.
144
+ const allItems = new Map();
145
+ let gotData = false;
146
+
147
+ for (let i = 0; i < 50; i++) {
148
+ await sleep(500);
149
+ for (const pair of readCapture()) {
150
+ for (const [id, s] of schedulesFromPair(pair)) {
151
+ // Collect all scheduleItems from all schedules (organizer is first)
152
+ for (const it of (s.scheduleItems || [])) {
153
+ if (it && it.startTime && it.endTime) {
154
+ const key = JSON.stringify([it.startTime, it.endTime, it.subject]);
155
+ allItems.set(key, it);
156
+ }
157
+ }
158
+ if (s.availabilityView && s.availabilityView.length) gotData = true;
159
+ }
160
+ }
161
+ if (gotData) break;
162
+ }
163
+
164
+ if (!gotData) {
165
+ throw new Error('No schedule data returned — check Outlook login');
166
+ }
167
+
168
+ // ── Filter to target date and build output rows ─────────────────────
169
+ const STATUS_MAP = { Busy: 'busy', Tentative: 'tentative', Oof: 'oof',
170
+ WorkingElsewhere: 'elsewhere', Free: 'free' };
171
+ const rows = [];
172
+
173
+ for (const it of allItems.values()) {
174
+ const startDt = new Date(it.startTime.dateTime);
175
+ const endDt = new Date(it.endTime.dateTime);
176
+ const evDate = `${startDt.getFullYear()}-${pad2(startDt.getMonth() + 1)}-${pad2(startDt.getDate())}`;
177
+
178
+ if (evDate !== date) continue;
179
+
180
+ const state = STATUS_MAP[it.status] || 'busy';
181
+ if (state === 'free') continue;
182
+
183
+ const durationMin = Math.round((endDt.getTime() - startDt.getTime()) / 60000);
184
+ if (durationMin < 5 || durationMin > 480) continue;
185
+
186
+ rows.push({
187
+ date: evDate,
188
+ subject: it.subject || '(meeting)',
189
+ start: `${pad2(startDt.getHours())}:${pad2(startDt.getMinutes())}`,
190
+ end: `${pad2(endDt.getHours())}:${pad2(endDt.getMinutes())}`,
191
+ durationMin,
192
+ state,
193
+ });
194
+ }
195
+
196
+ // Sort by start time
197
+ rows.sort((a, b) => a.start.localeCompare(b.start));
198
+ return rows;
@@ -1,41 +1,42 @@
1
- site: outlook
2
- name: my-meetings
3
- version: "1.0"
4
- description: "User's own meeting schedule from Outlook Calendar for a given day"
5
- access: read
6
-
7
- domains:
8
- - "outlook.cloud.microsoft.mcas.ms"
9
- capabilities:
10
- - navigate
11
- - js_evaluate
12
-
13
- args:
14
- - name: date
15
- type: string
16
- required: true
17
- help: "Day to fetch in YYYY-MM-DD format"
18
- pattern: "^\\d{4}-\\d{2}-\\d{2}$"
19
-
20
- columns:
21
- - name: date
22
- type: string
23
- - name: subject
24
- type: string
25
- - name: start
26
- type: string
27
- - name: end
28
- type: string
29
- - name: durationMin
30
- type: number
31
- - name: state
32
- type: string
33
-
34
- pipeline:
35
- - step: navigate
36
- url: "https://outlook.cloud.microsoft.mcas.ms/calendar/deeplink/compose"
37
- - step: wait
38
- selector: "body"
39
- timeout: 30000
40
- - step: js_evaluate
41
- file: outlook-my-meetings.eval.js
1
+ site: outlook
2
+ name: my-meetings
3
+ version: "1.0"
4
+ description: "User's own meeting schedule from Outlook Calendar for a given day"
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: date
16
+ type: string
17
+ required: true
18
+ help: "Day to fetch in YYYY-MM-DD format"
19
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$"
20
+
21
+ columns:
22
+ - name: date
23
+ type: string
24
+ - name: subject
25
+ type: string
26
+ - name: start
27
+ type: string
28
+ - name: end
29
+ type: string
30
+ - name: durationMin
31
+ type: number
32
+ - name: state
33
+ type: string
34
+
35
+ pipeline:
36
+ - step: navigate
37
+ url: "https://outlook.cloud.microsoft.mcas.ms/calendar/deeplink/compose"
38
+ - step: wait
39
+ selector: "body"
40
+ timeout: 30000
41
+ - step: js_evaluate
42
+ file: outlook-my-meetings.eval.js