@commandgarden/cli 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/main.js +74 -10
  2. package/node_modules/@commandgarden/app/dist/client/assets/index-Cglxej9D.css +1 -0
  3. package/node_modules/@commandgarden/app/dist/client/assets/index-Cju0_G1Y.js +397 -0
  4. package/node_modules/@commandgarden/app/dist/client/index.html +19 -19
  5. package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +3 -0
  6. package/node_modules/@commandgarden/app/dist/server/routes/index.js +4 -0
  7. package/node_modules/@commandgarden/app/dist/server/routes/security-news-cache.d.ts +3 -0
  8. package/node_modules/@commandgarden/app/dist/server/routes/security-news-cache.js +17 -0
  9. package/node_modules/@commandgarden/app/dist/server/routes/skills.d.ts +2 -0
  10. package/node_modules/@commandgarden/app/dist/server/routes/skills.js +65 -0
  11. package/node_modules/@commandgarden/app/dist/server/store.d.ts +5 -0
  12. package/node_modules/@commandgarden/app/dist/server/store.js +46 -27
  13. package/node_modules/@commandgarden/app/package.json +42 -42
  14. package/node_modules/@commandgarden/chrome/dist/content-script.js.map +1 -1
  15. package/node_modules/@commandgarden/chrome/dist/manifest.json +16 -16
  16. package/node_modules/@commandgarden/chrome/dist/popup.html +218 -197
  17. package/node_modules/@commandgarden/chrome/dist/popup.js.map +1 -1
  18. package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +1 -1
  19. package/node_modules/@commandgarden/chrome/package.json +25 -25
  20. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.eval.js +143 -143
  21. package/node_modules/@commandgarden/daemon/connectors/ado-git-commits.yaml +70 -70
  22. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-content.yaml +58 -58
  23. package/node_modules/@commandgarden/daemon/connectors/gcs-kb-pages.yaml +47 -47
  24. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.eval.js +115 -115
  25. package/node_modules/@commandgarden/daemon/connectors/jira-my-tickets.yaml +54 -54
  26. package/node_modules/@commandgarden/daemon/connectors/lib/msal-token.js +20 -20
  27. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.eval.js +186 -186
  28. package/node_modules/@commandgarden/daemon/connectors/outlook-my-meetings.yaml +41 -41
  29. package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.eval.js +94 -0
  30. package/node_modules/@commandgarden/daemon/connectors/saba-pending-training.yaml +34 -0
  31. package/node_modules/@commandgarden/daemon/connectors/socket-security-news.yaml +63 -63
  32. package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.eval.js +341 -341
  33. package/node_modules/@commandgarden/daemon/connectors/teams-room-availability.yaml +46 -46
  34. package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.eval.js +52 -52
  35. package/node_modules/@commandgarden/daemon/connectors/timetracking-projects.yaml +42 -42
  36. package/node_modules/@commandgarden/daemon/connectors/timetracking-report.eval.js +81 -81
  37. package/node_modules/@commandgarden/daemon/connectors/timetracking-report.yaml +52 -52
  38. package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.eval.js +27 -0
  39. package/node_modules/@commandgarden/daemon/connectors/tldrsec-newsletter.yaml +38 -0
  40. package/node_modules/@commandgarden/daemon/connectors/tokenmaster-clients-list.yaml +55 -55
  41. package/node_modules/@commandgarden/daemon/dist/audit-store.js +8 -8
  42. package/node_modules/@commandgarden/daemon/package.json +32 -32
  43. package/node_modules/@commandgarden/shared/package.json +32 -32
  44. package/package.json +58 -57
  45. package/node_modules/@commandgarden/app/dist/client/assets/index-CThI0UCl.css +0 -1
  46. package/node_modules/@commandgarden/app/dist/client/assets/index-WANip_um.js +0 -252
  47. package/node_modules/@commandgarden/app/dist/server/daemon-client.test.js +0 -47
  48. package/node_modules/@commandgarden/app/dist/server/routes/routes.test.js +0 -137
  49. package/node_modules/@commandgarden/app/dist/server/store.test.js +0 -59
  50. package/node_modules/@commandgarden/chrome/dist/fetch-interceptor.js +0 -44
  51. package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts +0 -24
  52. package/node_modules/@commandgarden/daemon/dist/cdp-client.d.ts.map +0 -1
  53. package/node_modules/@commandgarden/daemon/dist/cdp-client.js +0 -150
  54. package/node_modules/@commandgarden/daemon/dist/cdp-client.js.map +0 -1
@@ -1,186 +1,186 @@
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 opens — we 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.
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 opens — we 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,41 +1,41 @@
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
+
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
@@ -0,0 +1,94 @@
1
+ // Runs in Saba Cloud page context via js_evaluate step.
2
+ // Fetches pending mandatory learning items from Saba Cloud (v64+) using two
3
+ // confirmed API endpoints:
4
+ //
5
+ // 1. GET /Saba/api/ui/torque/uicontext/currentuser
6
+ // → userInfo.userId (e.g. "emplo000000001653633")
7
+ //
8
+ // 2. GET /Saba/api/common/todocontroller/detail/{userId}?context=learning&expand=savetabpref
9
+ // → searchResults[1] (Saba list format: ["list", [...items]])
10
+
11
+ const ORIGIN = 'https://daimler.sabacloud.com';
12
+
13
+ // ── Wait for valid Saba session (SSO / redirects, up to 60s) ─────────────
14
+
15
+ const __deadline = Date.now() + 60000;
16
+ while (Date.now() < __deadline) {
17
+ if (location.hostname === 'daimler.sabacloud.com' && document.readyState === 'complete') break;
18
+ await new Promise(r => setTimeout(r, 1000));
19
+ }
20
+ if (location.hostname !== 'daimler.sabacloud.com') {
21
+ throw new Error('AUTH_REQUIRED: Not signed in to Saba — log in at daimler.sabacloud.com and retry');
22
+ }
23
+
24
+ // ── Step 1: Resolve current user ID ──────────────────────────────────────
25
+
26
+ const meResp = await fetch(`${ORIGIN}/Saba/api/ui/torque/uicontext/currentuser`, {
27
+ headers: { Accept: 'application/json' },
28
+ credentials: 'include',
29
+ });
30
+ if (meResp.status === 401 || meResp.status === 403) {
31
+ throw new Error('AUTH_REQUIRED: Saba session expired — log in and retry');
32
+ }
33
+ if (!meResp.ok) {
34
+ throw new Error(`currentuser API returned HTTP ${meResp.status}`);
35
+ }
36
+ const meData = await meResp.json();
37
+ const userId = meData?.userInfo?.userId;
38
+ if (!userId) {
39
+ throw new Error('Could not read userInfo.userId from currentuser response');
40
+ }
41
+
42
+ // ── Step 2: Fetch pending learning to-do items ────────────────────────────
43
+
44
+ const todoResp = await fetch(
45
+ `${ORIGIN}/Saba/api/common/todocontroller/detail/${encodeURIComponent(userId)}?context=learning&expand=savetabpref`,
46
+ { headers: { Accept: 'application/json' }, credentials: 'include' }
47
+ );
48
+ if (!todoResp.ok) {
49
+ throw new Error(`todocontroller API returned HTTP ${todoResp.status} for userId=${userId}`);
50
+ }
51
+ const todoData = await todoResp.json();
52
+
53
+ // searchResults is serialised as ["list", [item, item, ...]]
54
+ const rawList = todoData?.searchResults;
55
+ const items = Array.isArray(rawList)
56
+ ? (rawList[0] === 'list' ? rawList[1] : rawList)
57
+ : [];
58
+
59
+ if (!Array.isArray(items)) {
60
+ throw new Error('Unexpected todocontroller response shape — searchResults is not a list');
61
+ }
62
+
63
+ // ── Step 3: Process and return rows ──────────────────────────────────────
64
+
65
+ const DONE_STATUSES = new Set([
66
+ 'completed', 'passed', 'withdrawn', 'cancelled', 'waived', 'successful',
67
+ ]);
68
+
69
+ const today = new Date();
70
+ today.setHours(0, 0, 0, 0);
71
+
72
+ return items
73
+ .filter(item => {
74
+ if (!item.mandatory) return false;
75
+ const status = (item.itemDisplayStatus || '').toLowerCase();
76
+ return !DONE_STATUSES.has(status);
77
+ })
78
+ .map(item => {
79
+ const dueDateMs = item.dueDate?.date ?? null;
80
+ const dueDateObj = dueDateMs ? new Date(dueDateMs) : null;
81
+ const daysUntilDue = item.dueDateTs?.localDaysDifference
82
+ ?? (dueDateObj !== null
83
+ ? Math.ceil((dueDateObj.getTime() - today.getTime()) / 86400000)
84
+ : null);
85
+ return {
86
+ title: item.itemName || '(unknown)',
87
+ type: item.itemType || 'Training',
88
+ status: item.itemDisplayStatus || 'Pending',
89
+ dueDate: dueDateObj ? dueDateObj.toISOString().slice(0, 10) : '',
90
+ daysUntilDue: daysUntilDue ?? 9999,
91
+ isOverdue: item.overdue ?? (daysUntilDue !== null && daysUntilDue < 0),
92
+ };
93
+ })
94
+ .sort((a, b) => a.daysUntilDue - b.daysUntilDue);
@@ -0,0 +1,34 @@
1
+ site: saba
2
+ name: pending-training
3
+ version: "1.0"
4
+ description: "Pending mandatory learning plan items from Saba Cloud (daimler.sabacloud.com)"
5
+ access: read
6
+
7
+ domains:
8
+ - "daimler.sabacloud.com"
9
+ capabilities:
10
+ - navigate
11
+ - js_evaluate
12
+
13
+ columns:
14
+ - name: title
15
+ type: string
16
+ - name: type
17
+ type: string
18
+ - name: status
19
+ type: string
20
+ - name: dueDate
21
+ type: string
22
+ - name: daysUntilDue
23
+ type: number
24
+ - name: isOverdue
25
+ type: boolean
26
+
27
+ pipeline:
28
+ - step: navigate
29
+ url: "https://daimler.sabacloud.com/"
30
+ - step: wait
31
+ selector: "body"
32
+ timeout: 60000
33
+ - step: js_evaluate
34
+ file: saba-pending-training.eval.js