@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.
- 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/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
|
@@ -75,7 +75,8 @@ function typeText(sel, text) {
|
|
|
75
75
|
// Captures getSchedule GraphQL responses (the page's own authenticated
|
|
76
76
|
// request — the endpoint 401s on replay).
|
|
77
77
|
if (!window.__rfb) {
|
|
78
|
-
window.__rfb = [];
|
|
78
|
+
window.__rfb = []; // getSchedule (free/busy) captures
|
|
79
|
+
window.__rfm = []; // findmeetinglocations (room resolution + capacity) captures
|
|
79
80
|
const origFetch = window.fetch;
|
|
80
81
|
window.fetch = function () {
|
|
81
82
|
const a = arguments;
|
|
@@ -89,6 +90,10 @@ if (!window.__rfb) {
|
|
|
89
90
|
window.__rfb.push({ req: String(rb || ''), body: t });
|
|
90
91
|
}
|
|
91
92
|
}).catch(() => {});
|
|
93
|
+
} else if (String(u).indexOf('findmeetinglocations') > -1) {
|
|
94
|
+
r.clone().text().then(t => {
|
|
95
|
+
window.__rfm.push({ req: String(rb || ''), body: t });
|
|
96
|
+
}).catch(() => {});
|
|
92
97
|
}
|
|
93
98
|
} catch (_e) { /* ignore */ }
|
|
94
99
|
return r;
|
|
@@ -103,6 +108,13 @@ function readCapture() {
|
|
|
103
108
|
return d;
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
/** Read and clear captured findmeetinglocations {req, body} pairs. */
|
|
112
|
+
function readMeetingLocationCapture() {
|
|
113
|
+
const d = window.__rfm || [];
|
|
114
|
+
window.__rfm = [];
|
|
115
|
+
return d;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
/** Map of scheduleId (lowercased) -> schedule object from a captured response body. */
|
|
107
119
|
function schedulesFromPair(pair) {
|
|
108
120
|
let body;
|
|
@@ -181,10 +193,17 @@ function buildTimeline(items, dateIso) {
|
|
|
181
193
|
|
|
182
194
|
// ── Main flow ────────────────────────────────────────────────────────
|
|
183
195
|
|
|
184
|
-
const
|
|
185
|
-
|
|
196
|
+
const roomsArgRaw = '${{ args.rooms | default("") }}'.trim();
|
|
197
|
+
const multiMode = roomsArgRaw.length > 0;
|
|
198
|
+
|
|
199
|
+
const room = '${{ args.room | default("") }}'.trim();
|
|
200
|
+
if (!multiMode && !room) throw new Error('Missing room argument — pass "room" or "rooms"');
|
|
186
201
|
const isEmail = EMAIL_RE.test(room);
|
|
187
202
|
|
|
203
|
+
const roomList = multiMode
|
|
204
|
+
? roomsArgRaw.split(',').map(s => s.trim()).filter(Boolean)
|
|
205
|
+
: [room];
|
|
206
|
+
|
|
188
207
|
let date = '${{ args.date | default("") }}'.trim();
|
|
189
208
|
if (!date) date = todayLocalISO();
|
|
190
209
|
if (!DATE_RE.test(date)) throw new Error(`Invalid date "${date}" — use YYYY-MM-DD`);
|
|
@@ -252,34 +271,14 @@ for (let i = 0; i < 3; i++) {
|
|
|
252
271
|
if (seen.size > 0) break;
|
|
253
272
|
}
|
|
254
273
|
|
|
255
|
-
// ── Add the room
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
clickByName('Expand Required attendees');
|
|
259
|
-
await sleep(400);
|
|
274
|
+
// ── Add the room(s) ───────────────────────────────────────────────────
|
|
275
|
+
/** { email, capacity } per room, pushed in the same order as roomList as each is resolved. */
|
|
276
|
+
const roomMeta = [];
|
|
260
277
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
await sleep(400);
|
|
265
|
-
}
|
|
266
|
-
if (!exists(inputSel)) {
|
|
267
|
-
throw new Error('Could not open the required-attendee field');
|
|
268
|
-
}
|
|
269
|
-
typeText(inputSel, room);
|
|
278
|
+
/** Add one room by name via the room finder (proven path — resolves the resource mailbox). */
|
|
279
|
+
async function addRoomByName(name) {
|
|
280
|
+
readMeetingLocationCapture(); // drop stale captures before triggering a fresh search
|
|
270
281
|
|
|
271
|
-
const optionSel = `[role=option][aria-label*='${room}']`;
|
|
272
|
-
let found = false;
|
|
273
|
-
for (let i = 0; i < 20 && !found; i++) {
|
|
274
|
-
await sleep(400);
|
|
275
|
-
found = exists(optionSel);
|
|
276
|
-
}
|
|
277
|
-
if (!found) {
|
|
278
|
-
throw new Error(`Room "${room}" was not found in the directory`);
|
|
279
|
-
}
|
|
280
|
-
$(optionSel).click();
|
|
281
|
-
} else {
|
|
282
|
-
// Name -> room finder.
|
|
283
282
|
const inputSel = "input[aria-label='Add a room']";
|
|
284
283
|
for (let i = 0; i < 20 && !exists(inputSel); i++) {
|
|
285
284
|
clickByName('Add a room');
|
|
@@ -288,9 +287,26 @@ if (isEmail) {
|
|
|
288
287
|
if (!exists(inputSel)) {
|
|
289
288
|
throw new Error('Could not open the room finder');
|
|
290
289
|
}
|
|
291
|
-
typeText(inputSel,
|
|
290
|
+
typeText(inputSel, name);
|
|
292
291
|
await sleep(1000);
|
|
293
292
|
|
|
293
|
+
// Resolve the authoritative email + real capacity from the page's own
|
|
294
|
+
// findmeetinglocations call (triggered by the search above).
|
|
295
|
+
let resolvedEmail = null;
|
|
296
|
+
let resolvedCapacity = undefined;
|
|
297
|
+
for (let i = 0; i < 15 && !resolvedEmail; i++) {
|
|
298
|
+
await sleep(300);
|
|
299
|
+
for (const pair of readMeetingLocationCapture()) {
|
|
300
|
+
let body;
|
|
301
|
+
try { body = JSON.parse(pair.body); } catch (_e) { continue; }
|
|
302
|
+
const loc = body?.MeetingLocations?.[0]?.MeetingLocation;
|
|
303
|
+
if (loc?.LocationEmailAddress) {
|
|
304
|
+
resolvedEmail = String(loc.LocationEmailAddress).toLowerCase();
|
|
305
|
+
if (typeof loc.Capacity === 'number') resolvedCapacity = loc.Capacity;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
294
310
|
let label = null;
|
|
295
311
|
for (let i = 0; i < 25 && !label; i++) {
|
|
296
312
|
await sleep(400);
|
|
@@ -303,41 +319,119 @@ if (isEmail) {
|
|
|
303
319
|
}
|
|
304
320
|
}
|
|
305
321
|
if (!label) {
|
|
306
|
-
throw new Error(`No room matched "${
|
|
322
|
+
throw new Error(`No room matched "${name}"`);
|
|
323
|
+
}
|
|
324
|
+
if (resolvedCapacity === undefined) {
|
|
325
|
+
// Fallback: parse capacity from the option label if the network capture missed it.
|
|
326
|
+
const capMatch = /capacity:?\s*(\d+)/i.exec(label);
|
|
327
|
+
if (capMatch) resolvedCapacity = Number(capMatch[1]);
|
|
307
328
|
}
|
|
329
|
+
roomMeta.push({ email: resolvedEmail, capacity: resolvedCapacity });
|
|
330
|
+
await sleep(300);
|
|
308
331
|
}
|
|
309
332
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
333
|
+
if (multiMode) {
|
|
334
|
+
for (const name of roomList) {
|
|
335
|
+
await addRoomByName(name);
|
|
336
|
+
}
|
|
337
|
+
} else if (isEmail) {
|
|
338
|
+
// Email -> attendee picker (resolves SMTP). Kept for direct single-room email lookups;
|
|
339
|
+
// capacity is not available via this path.
|
|
340
|
+
clickByName('Expand Required attendees');
|
|
341
|
+
await sleep(400);
|
|
342
|
+
const inputSel = "input[aria-label*='required attendees']";
|
|
343
|
+
for (let i = 0; i < 20 && !exists(inputSel); i++) {
|
|
344
|
+
clickByName('Add required attendee');
|
|
345
|
+
await sleep(400);
|
|
346
|
+
}
|
|
347
|
+
if (!exists(inputSel)) {
|
|
348
|
+
throw new Error('Could not open the required-attendee field');
|
|
349
|
+
}
|
|
350
|
+
typeText(inputSel, room);
|
|
351
|
+
const optionSel = `[role=option][aria-label*='${room}']`;
|
|
352
|
+
let found = false;
|
|
353
|
+
for (let i = 0; i < 20 && !found; i++) {
|
|
354
|
+
await sleep(400);
|
|
355
|
+
found = exists(optionSel);
|
|
356
|
+
}
|
|
357
|
+
if (!found) {
|
|
358
|
+
throw new Error(`Room "${room}" was not found in the directory`);
|
|
359
|
+
}
|
|
360
|
+
$(optionSel).click();
|
|
361
|
+
} else {
|
|
362
|
+
await addRoomByName(room);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ── Collect free/busy from intercepted responses ─────────────────────
|
|
366
|
+
// Any schedule id not present in the pre-add "seen" set is one of the rooms we just added
|
|
367
|
+
// (works uniformly for one room or many, since we only ever add rooms after capturing `seen`).
|
|
368
|
+
const isTargetId = id => (isEmail && !multiMode) ? id === room.toLowerCase() : !seen.has(id);
|
|
369
|
+
const expectedCount = multiMode ? roomList.length : 1;
|
|
370
|
+
|
|
371
|
+
// scheduleId(lower) -> { scheduleId, itemsById: Map, sawView: bool, errMsg: string|null }
|
|
372
|
+
const roomData = new Map();
|
|
316
373
|
|
|
317
374
|
let stable = 0;
|
|
318
|
-
for (let i = 0; i <
|
|
375
|
+
for (let i = 0; i < 60; i++) {
|
|
319
376
|
await sleep(500);
|
|
320
377
|
for (const pair of readCapture()) {
|
|
321
378
|
for (const [id, s] of schedulesFromPair(pair)) {
|
|
322
|
-
if (!
|
|
323
|
-
if (!
|
|
324
|
-
|
|
325
|
-
|
|
379
|
+
if (!isTargetId(id)) continue;
|
|
380
|
+
if (!roomData.has(id)) {
|
|
381
|
+
roomData.set(id, { scheduleId: s.scheduleId, itemsById: new Map(), sawView: false, errMsg: null });
|
|
382
|
+
}
|
|
383
|
+
const rd = roomData.get(id);
|
|
384
|
+
if (s.error) rd.errMsg = s.error.message || s.error.responseCode || 'unknown';
|
|
385
|
+
if (s.availabilityView && s.availabilityView.length) rd.sawView = true;
|
|
326
386
|
for (const it of (s.scheduleItems || [])) {
|
|
327
387
|
const key = it && it.id ? it.id : JSON.stringify(it && [it.startTime, it.endTime, it.subject]);
|
|
328
|
-
if (it) itemsById.set(key, it);
|
|
388
|
+
if (it) rd.itemsById.set(key, it);
|
|
329
389
|
}
|
|
330
390
|
}
|
|
331
391
|
}
|
|
332
|
-
|
|
392
|
+
const allSeen = roomData.size >= expectedCount && [...roomData.values()].every(rd => rd.sawView);
|
|
393
|
+
if (allSeen) { stable += 1; if (stable >= 2) break; } else { stable = 0; }
|
|
333
394
|
}
|
|
334
395
|
|
|
335
|
-
if (
|
|
336
|
-
throw new Error(`No free/busy returned for "${room}" on ${date}`);
|
|
396
|
+
if (roomData.size === 0) {
|
|
397
|
+
throw new Error(`No free/busy returned for ${multiMode ? roomList.join(', ') : `"${room}"`} on ${date}`);
|
|
337
398
|
}
|
|
338
|
-
if (
|
|
339
|
-
|
|
399
|
+
if (!multiMode) {
|
|
400
|
+
const rd = [...roomData.values()][0];
|
|
401
|
+
if (rd.errMsg && !rd.sawView) {
|
|
402
|
+
throw new Error(`Free/busy error for "${room}": ${rd.errMsg}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function emitForRoomData(rd, meta) {
|
|
407
|
+
if (rd.errMsg && !rd.sawView) return; // multi-mode: skip rooms that errored, keep the rest
|
|
408
|
+
const timeline = buildTimeline([...rd.itemsById.values()], date);
|
|
409
|
+
const roomEmail = meta?.email || rd.scheduleId;
|
|
410
|
+
const capacity = meta?.capacity ?? null;
|
|
411
|
+
for (const row of timeline) results.push({ room: roomEmail, capacity, ...row });
|
|
340
412
|
}
|
|
341
413
|
|
|
342
|
-
const
|
|
343
|
-
|
|
414
|
+
const results = [];
|
|
415
|
+
if (multiMode || !isEmail) {
|
|
416
|
+
// Prefer matching by the resolved email (authoritative); fall back to the next
|
|
417
|
+
// unclaimed schedule in insertion order if the network capture missed it.
|
|
418
|
+
const claimed = new Set();
|
|
419
|
+
for (const meta of roomMeta) {
|
|
420
|
+
let rd = null;
|
|
421
|
+
let claimId = null;
|
|
422
|
+
if (meta?.email && roomData.has(meta.email)) {
|
|
423
|
+
claimId = meta.email;
|
|
424
|
+
} else {
|
|
425
|
+
claimId = [...roomData.keys()].find(id => !claimed.has(id)) || null;
|
|
426
|
+
}
|
|
427
|
+
if (claimId) {
|
|
428
|
+
rd = roomData.get(claimId);
|
|
429
|
+
claimed.add(claimId);
|
|
430
|
+
}
|
|
431
|
+
if (rd) emitForRoomData(rd, meta);
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
const rd = roomData.get(room.toLowerCase()) || [...roomData.values()][0];
|
|
435
|
+
if (rd) emitForRoomData(rd, null);
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
@@ -14,8 +14,12 @@ capabilities:
|
|
|
14
14
|
args:
|
|
15
15
|
- name: room
|
|
16
16
|
type: string
|
|
17
|
-
required:
|
|
18
|
-
help: "Room name or mailbox email (e.g. 'MBTMY The Vista' or 'RES-RERE-M6VJ7ZUW@mercedes-benz.com')"
|
|
17
|
+
required: false
|
|
18
|
+
help: "Room name or mailbox email (e.g. 'MBTMY The Vista' or 'RES-RERE-M6VJ7ZUW@mercedes-benz.com'). Required unless 'rooms' is given."
|
|
19
|
+
- name: rooms
|
|
20
|
+
type: string
|
|
21
|
+
required: false
|
|
22
|
+
help: "Comma-separated list of room names to check simultaneously (e.g. for a floor-plan view). Takes precedence over 'room' if both are given."
|
|
19
23
|
- name: date
|
|
20
24
|
type: string
|
|
21
25
|
required: false
|
|
@@ -25,6 +29,8 @@ args:
|
|
|
25
29
|
columns:
|
|
26
30
|
- name: room
|
|
27
31
|
type: string
|
|
32
|
+
- name: capacity
|
|
33
|
+
type: number
|
|
28
34
|
- name: date
|
|
29
35
|
type: string
|
|
30
36
|
- name: state
|
|
@@ -63,7 +63,7 @@ export class WsRelay {
|
|
|
63
63
|
this.pendingApprovals.clear();
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
-
async send(connector, args, approvalConfig, timeoutMs =
|
|
66
|
+
async send(connector, args, approvalConfig, timeoutMs = 300_000, requestId) {
|
|
67
67
|
if (!this.ws)
|
|
68
68
|
throw new Error('Extension not connected');
|
|
69
69
|
const id = requestId ?? randomUUID();
|