@commandgarden/cli 2.5.1 → 2.6.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 (19) hide show
  1. package/node_modules/@commandgarden/app/dist/client/assets/index-Dm4TTmUS.css +1 -0
  2. package/node_modules/@commandgarden/app/dist/client/assets/index-tVkaaa2g.js +421 -0
  3. package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
  4. package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.d.ts +1 -1
  5. package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.js +2 -2
  6. package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.d.ts +12 -13
  7. package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.js +70 -17
  8. package/node_modules/@commandgarden/chrome/dist/service-worker.js +68 -13
  9. package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
  10. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +76 -119
  11. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +1 -1
  12. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +150 -138
  13. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +1 -0
  14. package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +144 -50
  15. package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +8 -2
  16. package/node_modules/@commandgarden/daemon/dist/ws-relay.js +1 -1
  17. package/package.json +1 -1
  18. package/node_modules/@commandgarden/app/dist/client/assets/index-DCptfmmK.js +0 -397
  19. package/node_modules/@commandgarden/app/dist/client/assets/index-qunCIeG9.css +0 -1
@@ -1,23 +1,21 @@
1
- // Runs in page context via js_evaluate step.
2
- // Fetches ADO Git commits (via Pushes API, all branches) and completed PRs
3
- // using the browser's ADO session cookies — no PAT or MSAL token required.
1
+ // Runs in page context via js_evaluate step (executed by CDP Runtime.evaluate
2
+ // to bypass ADO's strict-dynamic CSP see chrome-adapter.ts evaluateViaCdp).
4
3
  //
5
- // Strategy: navigate to dev.azure.com to trigger SSO, then call the ADO REST
6
- // API with session cookies (credentials:'include'). The Pushes API captures
7
- // work across ALL branches (unlike the Commits API which only searches the
8
- // default branch).
4
+ // Fetches ADO Git commits directly via the REST API. This works because:
5
+ // 1. Runtime.evaluate bypasses CSP (runs at debugger level, like DevTools console)
6
+ // 2. fetch() is same-origin and uses the page's session cookies for auth
7
+ // 3. No MCAS proxy on dev.azure.com (unlike Outlook)
8
+ //
9
+ // Falls back to window.__cdpCapture if the direct API call fails.
10
+ // Requires cdp: true in the connector YAML.
9
11
  //
10
12
  // Template variables interpolated before execution:
11
- // ${{ args.org }}
12
- // ${{ args.project }}
13
- // ${{ args.repo }}
14
- // ${{ args.fromDate }}
15
- // ${{ args.toDate }}
13
+ // ${{ args.org }}, ${{ args.project }}, ${{ args.repo }}
14
+ // ${{ args.fromDate }}, ${{ args.toDate }}
16
15
  // ${{ args.author | default("") }}
17
16
 
18
17
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
19
18
 
20
- // ── Read args ─────────────────────────────────────────────────────────
21
19
  const org = '${{ args.org }}'.trim();
22
20
  const project = '${{ args.project }}'.trim();
23
21
  const repo = '${{ args.repo }}'.trim();
@@ -25,119 +23,78 @@ const fromDate = '${{ args.fromDate }}'.trim();
25
23
  const toDate = '${{ args.toDate }}'.trim();
26
24
  const authorFilter = '${{ args.author | default("") }}'.trim().toLowerCase();
27
25
 
28
- if (!org || !project || !repo || !fromDate || !toDate) {
29
- throw new Error('Missing required args: org, project, repo, fromDate, toDate');
30
- }
31
-
32
- // ── Wait for ADO to finish SSO/redirect (up to 30s) ──────────────────
33
- // ADO uses httpOnly session cookies — no MSAL tokens in sessionStorage.
34
- // We just need the browser to be logged in; session cookies are sent
35
- // automatically with same-origin fetch (credentials: 'include').
36
- const __deadline = Date.now() + 30000;
37
- while (Date.now() < __deadline) {
38
- if (location.hostname === 'dev.azure.com') break;
39
- await sleep(1000);
40
- }
41
- if (location.hostname !== 'dev.azure.com') {
42
- throw new Error('Not signed in to Azure DevOps — log in at dev.azure.com and retry');
43
- }
44
-
45
- // ADO REST API: dev.azure.com/{org}/{project}/_apis/...
46
- const apiBase = `https://dev.azure.com/${org}/${project}/_apis`;
47
- // Use session cookies for auth (same-origin, no Bearer token needed)
48
- const authHeaders = { Accept: 'application/json' };
49
-
50
- // ── Step 1: List pushes in the date range ─────────────────────────────
51
- const listUrl =
52
- `${apiBase}/git/repositories/${repo}/pushes` +
53
- `?searchCriteria.fromDate=${fromDate}T00:00:00Z` +
54
- `&searchCriteria.toDate=${toDate}T23:59:59Z` +
55
- `&api-version=7.1`;
56
-
57
- const listResp = await fetch(listUrl, { headers: authHeaders, credentials: 'include' });
58
- if (!listResp.ok) throw new Error(`Pushes list HTTP ${listResp.status}: ${await listResp.text()}`);
59
- const listBody = await listResp.json();
60
- let pushes = listBody.value || [];
61
-
62
- // Filter by author if provided (case-insensitive substring match on
63
- // pushedBy.uniqueName or pushedBy.displayName)
64
- if (authorFilter) {
65
- pushes = pushes.filter(p => {
66
- const un = (p.pushedBy?.uniqueName || '').toLowerCase();
67
- const dn = (p.pushedBy?.displayName || '').toLowerCase();
68
- return un.includes(authorFilter) || dn.includes(authorFilter);
69
- });
26
+ // ── Fetch commits via ADO REST API ────────────────────────────────────
27
+ // Direct same-origin fetch with session cookies avoids the problem where
28
+ // ADO embeds initial commit data in the HTML (no separate _apis/ XHR),
29
+ // making CDP Fetch.enable interception unable to capture commit data.
30
+ const allItems = [];
31
+ try {
32
+ const apiUrl = `https://dev.azure.com/${encodeURIComponent(org)}/${encodeURIComponent(project)}`
33
+ + `/_apis/git/repositories/${encodeURIComponent(repo)}/commits`
34
+ + `?searchCriteria.fromDate=${fromDate}&searchCriteria.toDate=${toDate}`
35
+ + `&$top=1000&api-version=7.1`;
36
+ const resp = await fetch(apiUrl);
37
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
38
+ const data = await resp.json();
39
+ for (const item of (data.value || [])) {
40
+ if (item.commitId) allItems.push(item);
41
+ }
42
+ } catch (fetchErr) {
43
+ // Direct fetch failed — fall back to CDP-captured data (window.__cdpCapture
44
+ // is populated by chrome-adapter.ts Fetch.enable when _apis/ XHRs occur).
45
+ for (let i = 0; i < 30; i++) {
46
+ await sleep(1000);
47
+ if (window.__cdpCapture && window.__cdpCapture.length > 0) break;
48
+ }
49
+ for (const raw of (window.__cdpCapture || [])) {
50
+ try {
51
+ const data = JSON.parse(raw);
52
+ const items = data.value || data.results || [];
53
+ if (Array.isArray(items)) {
54
+ for (const item of items) {
55
+ if (item.commitId) allItems.push(item);
56
+ }
57
+ }
58
+ } catch (_) {}
59
+ }
60
+ window.__cdpCapture = [];
70
61
  }
71
62
 
72
- // ── Step 2: Fetch each push's commits ─────────────────────────────────
63
+ // ── Deduplicate and filter commits ────────────────────────────────────
73
64
  const seen = new Set();
74
- const commits = [];
65
+ const rows = [];
75
66
 
76
- for (const push of pushes) {
77
- const detailUrl = `${apiBase}/git/repositories/${repo}/pushes/${push.pushId}?api-version=7.1`;
78
- try {
79
- const detailResp = await fetch(detailUrl, { headers: authHeaders, credentials: 'include' });
80
- if (!detailResp.ok) continue;
81
- const detail = await detailResp.json();
82
- for (const c of (detail.commits || [])) {
83
- // Deduplicate: same commit can appear in multiple pushes (e.g. after merge)
84
- if (seen.has(c.commitId)) continue;
85
- seen.add(c.commitId);
86
- commits.push(c);
87
- }
88
- } catch (_) { /* skip failed push detail fetches */ }
89
- }
67
+ for (const item of allItems) {
68
+ if (seen.has(item.commitId)) continue;
69
+ seen.add(item.commitId);
90
70
 
91
- // ── Step 3: Fetch completed PRs in the date range ─────────────────────
92
- const prUrl =
93
- `${apiBase}/git/repositories/${repo}/pullrequests` +
94
- `?searchCriteria.status=completed` +
95
- `&api-version=7.1`;
71
+ const authorName = item.author?.name || item.committer?.name || '';
72
+ const authorEmail = item.author?.email || item.committer?.email || '';
73
+ const dateRaw = item.author?.date || item.committer?.date || '';
74
+ const date = dateRaw.slice(0, 10);
96
75
 
97
- let prRows = [];
98
- try {
99
- const prResp = await fetch(prUrl, { headers: authHeaders, credentials: 'include' });
100
- if (prResp.ok) {
101
- const prBody = await prResp.json();
102
- const prs = (prBody.value || []).filter(pr => {
103
- if (!pr.closedDate) return false;
104
- return pr.closedDate >= `${fromDate}T00:00:00Z` && pr.closedDate <= `${toDate}T23:59:59Z`;
105
- });
106
- // Optionally filter PRs by author
107
- const filtered = authorFilter
108
- ? prs.filter(pr => {
109
- const un = (pr.createdBy?.uniqueName || '').toLowerCase();
110
- return un.includes(authorFilter);
111
- })
112
- : prs;
113
- prRows = filtered.map(pr => ({
114
- commitId: '',
115
- authorName: pr.createdBy?.displayName || '',
116
- authorEmail: pr.createdBy?.uniqueName || '',
117
- date: (pr.closedDate || '').slice(0, 10),
118
- message: `PR #${pr.pullRequestId}: ${pr.title || '(no title)'}`,
119
- filesAdded: 0,
120
- filesEdited: 0,
121
- filesDeleted: 0,
122
- isPR: 'true',
123
- prId: pr.pullRequestId,
124
- }));
76
+ // Filter by date range
77
+ if (date && (date < fromDate || date > toDate)) continue;
78
+
79
+ // Filter by author if provided
80
+ if (authorFilter) {
81
+ const nameMatch = authorName.toLowerCase().includes(authorFilter);
82
+ const emailMatch = authorEmail.toLowerCase().includes(authorFilter);
83
+ if (!nameMatch && !emailMatch) continue;
125
84
  }
126
- } catch (_) { /* PRs are supplemental — don't fail on error */ }
127
85
 
128
- // ── Build output rows ─────────────────────────────────────────────────
129
- const commitRows = commits.map(c => ({
130
- commitId: c.commitId || '',
131
- authorName: c.author?.name || '',
132
- authorEmail: c.author?.email || '',
133
- date: (c.author?.date || '').slice(0, 10),
134
- message: (c.comment || '').split('\n')[0].slice(0, 200),
135
- filesAdded: c.changeCounts?.Add || 0,
136
- filesEdited: c.changeCounts?.Edit || 0,
137
- filesDeleted: c.changeCounts?.Delete || 0,
138
- isPR: 'false',
139
- prId: 0,
140
- }));
86
+ rows.push({
87
+ commitId: item.commitId,
88
+ authorName,
89
+ authorEmail,
90
+ date,
91
+ message: (item.comment || '').split('\n')[0].slice(0, 200),
92
+ filesAdded: item.changeCounts?.Add || 0,
93
+ filesEdited: item.changeCounts?.Edit || 0,
94
+ filesDeleted: item.changeCounts?.Delete || 0,
95
+ isPR: 'false',
96
+ prId: 0,
97
+ });
98
+ }
141
99
 
142
- const rows = [...commitRows, ...prRows];
143
100
  return rows;
@@ -62,7 +62,7 @@ columns:
62
62
 
63
63
  pipeline:
64
64
  - step: navigate
65
- url: "https://dev.azure.com/${{ args.org }}"
65
+ url: "https://dev.azure.com/${{ args.org }}/${{ args.project }}/_git/${{ args.repo }}/commits"
66
66
  - step: wait
67
67
  selector: "body"
68
68
  timeout: 30000
@@ -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.
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
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.
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
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.
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.
10
13
  //
11
14
  // Template variables interpolated before execution:
12
15
  // ${{ args.date }}
13
16
 
14
- function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
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
+
15
21
  function pad2(n) { return String(n).padStart(2, '0'); }
22
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
16
23
 
17
- const DATE_RE = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
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 ────────────────────────────────────────────────────────
18
85
  const date = '${{ args.date }}'.trim();
19
86
  if (!DATE_RE.test(date)) throw new Error(`Invalid date "${date}" — use YYYY-MM-DD`);
20
87
 
21
- // ── Wait for OWA to finish SSO/redirect (up to 30s) ──────────────────
88
+ // ── Wait for compose form (handles SSO redirect delays) ──────────────
22
89
  const __deadline = Date.now() + 30000;
23
90
  while (Date.now() < __deadline) {
24
- if (location.hostname.includes('outlook') && !(/login\.|\/oauth2\/|signin|sso/i.test(location.href))) break;
25
- await sleep(1000);
91
+ if (exists("button[aria-label='Open Scheduling Assistant']") ||
92
+ exists("input[aria-label='Start date']")) break;
93
+ await sleep(400);
26
94
  }
27
- if (/login\.|\/oauth2\/|signin|sso/i.test(location.href)) {
28
- throw new Error('Not signed in to Outlook — log in and retry');
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');
29
101
  }
30
102
 
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
- };
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');
83
110
  }
84
111
 
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
- }
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();
94
137
  }
138
+ await sleep(500);
95
139
  }
96
140
 
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) {
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)
104
152
  for (const it of (s.scheduleItems || [])) {
105
153
  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
- });
154
+ const key = JSON.stringify([it.startTime, it.endTime, it.subject]);
155
+ allItems.set(key, it);
117
156
  }
118
157
  }
158
+ if (s.availabilityView && s.availabilityView.length) gotData = true;
119
159
  }
120
160
  }
161
+ if (gotData) break;
121
162
  }
122
163
 
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;
164
+ if (!gotData) {
165
+ throw new Error('No schedule data returned check Outlook login');
127
166
  }
128
167
 
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 ──────────────────
168
+ // ── Filter to target date and build output rows ─────────────────────
169
+ const STATUS_MAP = { Busy: 'busy', Tentative: 'tentative', Oof: 'oof',
170
+ WorkingElsewhere: 'elsewhere', Free: 'free' };
145
171
  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');
172
+
173
+ for (const it of allItems.values()) {
174
+ const startDt = new Date(it.startTime.dateTime);
175
+ const endDt = new Date(it.endTime.dateTime);
168
176
  const evDate = `${startDt.getFullYear()}-${pad2(startDt.getMonth() + 1)}-${pad2(startDt.getDate())}`;
169
177
 
170
- // Filter to the target date
171
178
  if (evDate !== date) continue;
172
179
 
180
+ const state = STATUS_MAP[it.status] || 'busy';
181
+ if (state === 'free') continue;
182
+
173
183
  const durationMin = Math.round((endDt.getTime() - startDt.getTime()) / 60000);
174
184
  if (durationMin < 5 || durationMin > 480) continue;
175
185
 
176
186
  rows.push({
177
187
  date: evDate,
178
- subject: subject,
188
+ subject: it.subject || '(meeting)',
179
189
  start: `${pad2(startDt.getHours())}:${pad2(startDt.getMinutes())}`,
180
190
  end: `${pad2(endDt.getHours())}:${pad2(endDt.getMinutes())}`,
181
191
  durationMin,
182
- state: showAsLower,
192
+ state,
183
193
  });
184
194
  }
185
195
 
196
+ // Sort by start time
197
+ rows.sort((a, b) => a.start.localeCompare(b.start));
186
198
  return rows;
@@ -3,6 +3,7 @@ name: my-meetings
3
3
  version: "1.0"
4
4
  description: "User's own meeting schedule from Outlook Calendar for a given day"
5
5
  access: read
6
+ cdp: true
6
7
 
7
8
  domains:
8
9
  - "outlook.cloud.microsoft.mcas.ms"