@commandgarden/cli 2.5.0 → 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.
- package/dist/main.js +6 -6
- package/node_modules/@commandgarden/app/dist/client/assets/index-Dm4TTmUS.css +1 -0
- package/node_modules/@commandgarden/app/dist/client/assets/index-tVkaaa2g.js +421 -0
- package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
- package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.d.ts +1 -1
- package/node_modules/@commandgarden/app/dist/server/journal/sources/git.source.js +2 -2
- package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.d.ts +12 -13
- package/node_modules/@commandgarden/app/dist/server/journal/sources/meetings.source.js +70 -17
- package/node_modules/@commandgarden/chrome/dist/service-worker.js +68 -13
- package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +76 -119
- package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +1 -1
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +150 -138
- package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +1 -0
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +144 -50
- package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +8 -2
- package/node_modules/@commandgarden/daemon/connectors/teams-rooms-availability.eval.js +2 -0
- package/node_modules/@commandgarden/daemon/dist/ws-relay.js +1 -1
- package/package.json +1 -1
- package/node_modules/@commandgarden/app/dist/client/assets/index-DCptfmmK.js +0 -397
- 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
|
-
//
|
|
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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
// ──
|
|
63
|
+
// ── Deduplicate and filter commits ────────────────────────────────────
|
|
73
64
|
const seen = new Set();
|
|
74
|
-
const
|
|
65
|
+
const rows = [];
|
|
75
66
|
|
|
76
|
-
for (const
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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;
|
|
@@ -1,186 +1,198 @@
|
|
|
1
|
-
// Runs in page context via js_evaluate step.
|
|
2
|
-
//
|
|
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:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
88
|
+
// ── Wait for compose form (handles SSO redirect delays) ──────────────
|
|
22
89
|
const __deadline = Date.now() + 30000;
|
|
23
90
|
while (Date.now() < __deadline) {
|
|
24
|
-
if (
|
|
25
|
-
|
|
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 (
|
|
28
|
-
|
|
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
|
-
// ──
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
//
|
|
130
|
-
const
|
|
131
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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
|
|
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;
|