@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
@@ -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 room = '${{ args.room }}'.trim();
185
- if (!room) throw new Error('Missing room argument — pass a room name or email');
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
- if (isEmail) {
257
- // Email -> attendee picker (resolves SMTP).
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
- const inputSel = "input[aria-label*='required attendees']";
262
- for (let i = 0; i < 20 && !exists(inputSel); i++) {
263
- clickByName('Add required attendee');
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, room);
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 "${room}"`);
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
- // ── Collect the room's free/busy from intercepted responses ──────────
311
- let roomId = null;
312
- let errMsg = null;
313
- let sawView = false;
314
- const itemsById = new Map();
315
- const isRoomId = id => (isEmail ? id === room.toLowerCase() : !seen.has(id));
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 < 50; 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 (!isRoomId(id)) continue;
323
- if (!roomId) roomId = s.scheduleId;
324
- if (s.error) errMsg = s.error.message || s.error.responseCode || 'unknown';
325
- if (s.availabilityView && s.availabilityView.length) sawView = true;
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
- if (sawView) { stable += 1; if (stable >= 2) break; }
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 (!roomId) {
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 (errMsg && !sawView) {
339
- throw new Error(`Free/busy error for "${room}": ${errMsg}`);
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 timeline = buildTimeline([...itemsById.values()], date);
343
- return timeline.map(r => ({ room: roomId, ...r }));
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: true
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 = 120_000, requestId) {
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commandgarden/cli",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "Enterprise browser automation CLI",
5
5
  "type": "module",
6
6
  "license": "MIT",