@commandgarden/cli 2.14.2 → 2.15.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 (31) hide show
  1. package/node_modules/@commandgarden/app/dist/client/assets/Slides-5KujoV-w.js +30 -0
  2. package/node_modules/@commandgarden/app/dist/client/assets/index-2gEJ4GIc.js +521 -0
  3. package/node_modules/@commandgarden/app/dist/client/assets/{index-rHqHbsBw.css → index-CHt4v0FJ.css} +1 -1
  4. package/node_modules/@commandgarden/app/dist/client/index.html +2 -2
  5. package/node_modules/@commandgarden/app/dist/server/routes/ai-news-cache.d.ts +3 -0
  6. package/node_modules/@commandgarden/app/dist/server/routes/ai-news-cache.js +17 -0
  7. package/node_modules/@commandgarden/app/dist/server/routes/connectors.js +4 -0
  8. package/node_modules/@commandgarden/app/dist/server/routes/index.js +4 -0
  9. package/node_modules/@commandgarden/app/dist/server/routes/roles-cache.d.ts +3 -0
  10. package/node_modules/@commandgarden/app/dist/server/routes/roles-cache.js +17 -0
  11. package/node_modules/@commandgarden/app/dist/server/store.d.ts +12 -0
  12. package/node_modules/@commandgarden/app/dist/server/store.js +44 -0
  13. package/node_modules/@commandgarden/app/package.json +0 -3
  14. package/node_modules/@commandgarden/app/skills/cg/SKILL.md +89 -0
  15. package/node_modules/@commandgarden/app/skills/connector-authoring/RECON-PLAYBOOK.md +364 -0
  16. package/node_modules/@commandgarden/app/skills/connector-authoring/SKILL.md +160 -0
  17. package/node_modules/@commandgarden/chrome/dist/service-worker.js +1 -1
  18. package/node_modules/@commandgarden/chrome/dist/service-worker.js.map +2 -2
  19. package/node_modules/@commandgarden/chrome/package.json +0 -3
  20. package/node_modules/@commandgarden/daemon/connectors/alice-role-list.yaml +62 -0
  21. package/node_modules/@commandgarden/daemon/connectors/every-newsletter.eval.js +87 -0
  22. package/node_modules/@commandgarden/daemon/connectors/every-newsletter.yaml +40 -0
  23. package/node_modules/@commandgarden/daemon/connectors/simonwillison-blog.eval.js +39 -0
  24. package/node_modules/@commandgarden/daemon/connectors/simonwillison-blog.yaml +39 -0
  25. package/node_modules/@commandgarden/daemon/connectors/uis-mic-user-information.eval.js +22 -0
  26. package/node_modules/@commandgarden/daemon/connectors/uis-mic-user-information.yaml +54 -0
  27. package/node_modules/@commandgarden/daemon/package.json +0 -4
  28. package/node_modules/@commandgarden/shared/package.json +0 -3
  29. package/package.json +1 -1
  30. package/node_modules/@commandgarden/app/dist/client/assets/Slides-DMIm_HgI.js +0 -21
  31. package/node_modules/@commandgarden/app/dist/client/assets/index-CupEN1w-.js +0 -511
@@ -5,6 +5,10 @@ const APP_ROUTES = {
5
5
  'saba/pending-training': '/apps/saba',
6
6
  'tokenmaster/clients-list': '/apps/trusted-peer-expiry',
7
7
  'tokenmaster/client-trustedby': '/apps/trusted-peer-expiry',
8
+ 'alice/role-list': '/apps/roles',
9
+ 'uis/mic-user-information': '/apps/roles',
10
+ 'simonwillison/blog': '/apps/ai-news',
11
+ 'every/newsletter': '/apps/ai-news',
8
12
  };
9
13
  export function connectorRoutes(app, daemon) {
10
14
  app.get('/api/connectors', async () => {
@@ -8,7 +8,9 @@ import { goalsRoutes } from './goals.js';
8
8
  import { timetrackingCacheRoutes } from './timetracking-cache.js';
9
9
  import { journalRoutes } from './journal.js';
10
10
  import { securityNewsCacheRoutes } from './security-news-cache.js';
11
+ import { aiNewsCacheRoutes } from './ai-news-cache.js';
11
12
  import { trustedPeersCacheRoutes } from './trusted-peers-cache.js';
13
+ import { rolesCacheRoutes } from './roles-cache.js';
12
14
  import { roomAvailabilityCacheRoutes } from './room-availability-cache.js';
13
15
  import { skillsRoutes } from './skills.js';
14
16
  export function registerRoutes(app, daemon, store) {
@@ -22,7 +24,9 @@ export function registerRoutes(app, daemon, store) {
22
24
  timetrackingCacheRoutes(app, store);
23
25
  journalRoutes(app, daemon, store);
24
26
  securityNewsCacheRoutes(app, store);
27
+ aiNewsCacheRoutes(app, store);
25
28
  trustedPeersCacheRoutes(app, store);
29
+ rolesCacheRoutes(app, store);
26
30
  roomAvailabilityCacheRoutes(app, store);
27
31
  skillsRoutes(app);
28
32
  }
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { AppStore } from '../store.js';
3
+ export declare function rolesCacheRoutes(app: FastifyInstance, store: AppStore): void;
@@ -0,0 +1,17 @@
1
+ export function rolesCacheRoutes(app, store) {
2
+ app.get('/api/roles/cache', async () => {
3
+ const cached = store.getCachedRoles();
4
+ if (!cached)
5
+ return { ok: true, userId: null, uisData: null, aliceData: null, fetchedAt: null };
6
+ return { ok: true, userId: cached.userId, uisData: cached.uisData, aliceData: cached.aliceData, fetchedAt: cached.fetchedAt };
7
+ });
8
+ app.post('/api/roles/cache', async (req, reply) => {
9
+ const { userId, uisData, aliceData } = req.body;
10
+ if (!userId || typeof userId !== 'string') {
11
+ reply.code(400);
12
+ return { ok: false, error: 'Missing userId' };
13
+ }
14
+ store.cacheRoles(userId, uisData ?? null, aliceData ?? null);
15
+ return { ok: true };
16
+ });
17
+ }
@@ -48,11 +48,23 @@ export declare class AppStore {
48
48
  data: Record<string, unknown>[];
49
49
  fetchedAt: string;
50
50
  } | null;
51
+ cacheRoles(userId: string, uisData: Record<string, unknown> | null, aliceData: Record<string, unknown>[] | null): void;
52
+ getCachedRoles(): {
53
+ userId: string;
54
+ uisData: Record<string, unknown> | null;
55
+ aliceData: Record<string, unknown>[] | null;
56
+ fetchedAt: string;
57
+ } | null;
51
58
  cacheSecurityNews(data: Record<string, unknown>[]): void;
52
59
  getCachedSecurityNews(): {
53
60
  data: Record<string, unknown>[];
54
61
  fetchedAt: string;
55
62
  } | null;
63
+ cacheAiNews(data: Record<string, unknown>[]): void;
64
+ getCachedAiNews(): {
65
+ data: Record<string, unknown>[];
66
+ fetchedAt: string;
67
+ } | null;
56
68
  cacheRoomAvailability(date: string, data: Record<string, unknown>[]): void;
57
69
  getCachedRoomAvailability(date: string): {
58
70
  data: Record<string, unknown>[];
@@ -101,11 +101,23 @@ export class AppStore {
101
101
  id INTEGER PRIMARY KEY CHECK (id = 1),
102
102
  data TEXT NOT NULL,
103
103
  fetched_at TEXT NOT NULL
104
+ )`);
105
+ this.db.run(`CREATE TABLE IF NOT EXISTS ai_news_cache (
106
+ id INTEGER PRIMARY KEY CHECK (id = 1),
107
+ data TEXT NOT NULL,
108
+ fetched_at TEXT NOT NULL
104
109
  )`);
105
110
  this.db.run(`CREATE TABLE IF NOT EXISTS trusted_peers_cache (
106
111
  id INTEGER PRIMARY KEY CHECK (id = 1),
107
112
  data TEXT NOT NULL,
108
113
  fetched_at TEXT NOT NULL
114
+ )`);
115
+ this.db.run(`CREATE TABLE IF NOT EXISTS roles_cache (
116
+ id INTEGER PRIMARY KEY CHECK (id = 1),
117
+ user_id TEXT NOT NULL,
118
+ uis_data TEXT,
119
+ alice_data TEXT,
120
+ fetched_at TEXT NOT NULL
109
121
  )`);
110
122
  this.db.run(`CREATE TABLE IF NOT EXISTS room_availability_cache (
111
123
  date TEXT PRIMARY KEY,
@@ -258,6 +270,24 @@ export class AppStore {
258
270
  fetchedAt: rows[0].fetched_at,
259
271
  };
260
272
  }
273
+ cacheRoles(userId, uisData, aliceData) {
274
+ const now = new Date().toISOString();
275
+ this.db.run('INSERT OR REPLACE INTO roles_cache (id, user_id, uis_data, alice_data, fetched_at) VALUES (1, ?, ?, ?, ?)', [userId, uisData ? JSON.stringify(uisData) : null, aliceData ? JSON.stringify(aliceData) : null, now]);
276
+ this.persist();
277
+ }
278
+ getCachedRoles() {
279
+ const rows = this.query('SELECT user_id, uis_data, alice_data, fetched_at FROM roles_cache WHERE id = 1');
280
+ if (rows.length === 0)
281
+ return null;
282
+ const uisRaw = rows[0].uis_data;
283
+ const aliceRaw = rows[0].alice_data;
284
+ return {
285
+ userId: rows[0].user_id,
286
+ uisData: uisRaw ? JSON.parse(uisRaw) : null,
287
+ aliceData: aliceRaw ? JSON.parse(aliceRaw) : null,
288
+ fetchedAt: rows[0].fetched_at,
289
+ };
290
+ }
261
291
  cacheSecurityNews(data) {
262
292
  const now = new Date().toISOString();
263
293
  this.db.run('INSERT OR REPLACE INTO security_news_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
@@ -272,6 +302,20 @@ export class AppStore {
272
302
  fetchedAt: rows[0].fetched_at,
273
303
  };
274
304
  }
305
+ cacheAiNews(data) {
306
+ const now = new Date().toISOString();
307
+ this.db.run('INSERT OR REPLACE INTO ai_news_cache (id, data, fetched_at) VALUES (1, ?, ?)', [JSON.stringify(data), now]);
308
+ this.persist();
309
+ }
310
+ getCachedAiNews() {
311
+ const rows = this.query('SELECT data, fetched_at FROM ai_news_cache WHERE id = 1');
312
+ if (rows.length === 0)
313
+ return null;
314
+ return {
315
+ data: JSON.parse(rows[0].data),
316
+ fetchedAt: rows[0].fetched_at,
317
+ };
318
+ }
275
319
  cacheRoomAvailability(date, data) {
276
320
  const now = new Date().toISOString();
277
321
  this.db.run('INSERT OR REPLACE INTO room_availability_cache (date, data, fetched_at) VALUES (?, ?, ?)', [date, JSON.stringify(data), now]);
@@ -3,9 +3,6 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
- "files": [
7
- "dist"
8
- ],
9
6
  "scripts": {
10
7
  "build": "vite build && tsc -p tsconfig.server.json",
11
8
  "dev": "concurrently \"vite\" \"tsx watch src/server/main.ts\"",
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: cg
3
+ description: "Run commandGarden connectors via `cg` to query enterprise data from authenticated browser sessions. Use when the user mentions `cg`, commandGarden, connectors, or wants to fetch timetracking, room availability, security news, or data behind SSO."
4
+ ---
5
+
6
+ `cg` queries enterprise websites through **connectors** — declarative YAML pipelines executed via a Chrome extension using the user's live browser session. No credentials stored.
7
+
8
+ ## Workflow
9
+
10
+ ### 1. Verify readiness
11
+
12
+ Run `cg daemon status`. Proceed only when output shows "Daemon: running" and "Extension: connected". If not running, run `cg up`.
13
+
14
+ ### 2. Discover
15
+
16
+ Run `cg list` to see all installed connectors. Each connector has a key in `site/name` format (e.g. `timetracking/report`). Filter large output: `cg list | Select-String "keyword"` (PowerShell) or `cg list | grep -i "keyword"` (bash).
17
+
18
+ ### 3. Inspect
19
+
20
+ Run `cg inspect <site/name>` before running any connector. Shows required arguments, defaults, output columns, and pipeline steps. Never guess argument names.
21
+
22
+ ### 4. Run
23
+
24
+ ```
25
+ cg run <site/name> --format json [--arg value ...]
26
+ ```
27
+
28
+ **Always `--format json`**. Output shape:
29
+
30
+ ```json
31
+ {"ok": true, "connector": "site/name", "rowCount": N, "columns": [...], "data": [{...}, ...]}
32
+ ```
33
+
34
+ ### 5. Parse
35
+
36
+ Read the `data` array from JSON output. Column names match `columns`. `rowCount` confirms expected cardinality.
37
+
38
+ Errors return plain text starting with `Error:` — not JSON.
39
+
40
+ ## Connectors
41
+
42
+ | Key | Description | Args |
43
+ |-----|-------------|------|
44
+ | `ado/git-commits` | ADO Git commits & PRs for a date range | `org`, `project`, `repo`, `fromDate`, `toDate`, `author` |
45
+ | `alice/role-list` | Role assignments for a user from Alice | `userId` |
46
+ | `every/newsletter` | Latest blog posts from Every | `sort` (optional) |
47
+ | `gcs/kb-pages` | GCS Knowledge Base page index | — |
48
+ | `gcs/kb-content` | Retrieve one GCS KB page as Markdown | `path` |
49
+ | `jira/my-tickets` | Jira tickets assigned to user | `fromDate`, `toDate`, `assignee` |
50
+ | `outlook/my-meetings` | User's Outlook Calendar for a day | `date` |
51
+ | `saba/pending-training` | Pending mandatory training from Saba | — |
52
+ | `socket/security-news` | Latest security news from Socket.dev | `tag` (optional) |
53
+ | `teams/room-availability` | Room free/busy for one room | `room`, `date` |
54
+ | `teams/rooms-availability` | Room free/busy for multiple rooms | `rooms`, `email`, `date` |
55
+ | `timetracking/projects` | Available projects & activities | `month` |
56
+ | `timetracking/report` | Monthly time-tracking report | `month` |
57
+ | `tldrsec/newsletter` | tl;dr sec newsletter issues | — |
58
+ | `tokenmaster/client-trustedby` | Clients that trust a given client | `clientid`, `region` |
59
+ | `tokenmaster/clients-list` | Clients managed by current user | `region` |
60
+ | `uis/mic-user-information` | User identity, department, groups, and scopes from UIS | `userId` |
61
+ | `simonwillison/blog` | Simon Willison's blog posts (Atom feed) | `tag` (optional) |
62
+ | `wiz/blog-security` | Wiz security blog posts | `tag` (optional) |
63
+
64
+ ## Errors
65
+
66
+ | Output contains | Fix |
67
+ |---|---|
68
+ | "not running" / "No session token" | `cg up` |
69
+ | "not connected" | Open Chrome with the extension loaded |
70
+ | "not approved" | Add connector key to `security.approvedHighRisk` in `~/.commandgarden/config.yaml` |
71
+ | "back/forward cache" | Retry the command |
72
+
73
+ ## Commands
74
+
75
+ | Command | What it does |
76
+ |---|---|
77
+ | `cg up` / `cg down` | Start / stop all services |
78
+ | `cg daemon status` | Health check (daemon + extension) |
79
+ | `cg list` | All installed connectors |
80
+ | `cg inspect <key>` | Connector args, columns, pipeline |
81
+ | `cg run <key> --format json` | Execute a connector |
82
+ | `cg validate <file>` | Validate a connector YAML file |
83
+ | `cg audit list --since <dur>` | Recent audit events (e.g. `7d`, `1h`) |
84
+ | `cg audit export --format json --since <dur>` | Export audit events as JSON |
85
+ | `cg config show` | View current configuration |
86
+
87
+ ## Writing connectors
88
+
89
+ Use the [connector-authoring skill](../connector-authoring/SKILL.md) for the full recon→scaffold→implement→debug→polish sequence. Reference docs: [`connector-authoring.md`](../../docs/connector-authoring.md) and [`pipeline-reference.md`](../../docs/pipeline-reference.md). Place new connector YAML files in `~/.commandgarden/connectors/`. Validate with `cg validate <file>` before use.
@@ -0,0 +1,364 @@
1
+ # Recon Playbook
2
+
3
+ Ground-layer debugging for commandGarden connectors. Use when the audit log and basic probing (step 4 of SKILL.md) aren't enough to get data flowing.
4
+
5
+ ---
6
+
7
+ ## Network interception hierarchy
8
+
9
+ From most visible (page JS) to most powerful (network stack):
10
+
11
+ ```
12
+ window.fetch patch → Sees only page-initiated fetch() calls
13
+ ↓ Invisible: SW-mediated, MCAS-wrapped, XHR
14
+ CDP Network.enable → Sees per-target requests (tab OR SW, not both)
15
+ ↓ Invisible: may miss timing-dependent SW events
16
+ CDP Fetch.enable → Sees ALL requests at the network stack level
17
+ Sees: SW-mediated, MCAS-wrapped, proxied — everything
18
+ Caveat: response bodies may be base64-encoded
19
+ ```
20
+
21
+ Always start at the top. Escalate down only when the higher level captures nothing.
22
+
23
+ ---
24
+
25
+ ## Technique 1: Strip the pipeline
26
+
27
+ Remove steps from the end until data flows. Isolate which step breaks.
28
+
29
+ ```bash
30
+ # Keep only: navigate + wait
31
+ # Expected: ok: true, data: [], 0 rows — navigation works
32
+ cg run <site>/<name> --format json
33
+
34
+ # Add: fetch or extract
35
+ # Expected: ok: true, data: [...], N rows — data source works
36
+ cg run <site>/<name> --format json
37
+
38
+ # Add: map
39
+ # Check: field values aren't "undefined" — field names match
40
+ cg run <site>/<name> --format json
41
+ ```
42
+
43
+ If `navigate + wait` fails, the problem is auth (SSO redirect, wrong URL, page blocked).
44
+ If `fetch` returns 0 rows, the problem is the API call (wrong URL, content type, wrapper object).
45
+ If `map` produces `"undefined"`, the problem is field name casing.
46
+
47
+ ---
48
+
49
+ ## Technique 2: Inspect raw API responses
50
+
51
+ For `fetch` connectors — remove the `map` step and examine raw output:
52
+
53
+ ```bash
54
+ cg run <site>/<name> --format json
55
+ ```
56
+
57
+ **0 rows, no error:**
58
+ - API returned non-JSON → add `headers: { Accept: "application/json" }` to the `fetch` step.
59
+ - API returned a wrapper object like `{ "items": [...] }` → add `dataPath: "items"`.
60
+ - `fetch` ran on the wrong tab or URL → check the audit log `steps[]` for the fetch step's error/timing.
61
+
62
+ **Data present but wrong shape:**
63
+ - Field names are case-sensitive. Compare the raw JSON keys with your `map` expressions exactly.
64
+ - Arrays in responses (e.g. `["ALICE", "BOB"]`) auto-join to `"ALICE,BOB"` in map expressions.
65
+
66
+ ---
67
+
68
+ ## Technique 3: Console logging in js_evaluate
69
+
70
+ Add `console.log()` to the eval.js file. Output appears in the **target tab's** DevTools console (not the extension's service worker console).
71
+
72
+ ```js
73
+ console.log('[cg] token:', token ? 'found (' + token.slice(0, 8) + '...)' : 'MISSING');
74
+ console.log('[cg] API status:', resp.status);
75
+ console.log('[cg] response preview:', JSON.stringify(data).slice(0, 300));
76
+ console.log('[cg] row count:', rows.length);
77
+ ```
78
+
79
+ After the debug session, remove all `[cg]` logs — they're visible to anyone with DevTools open.
80
+
81
+ **DevTools not showing logs?** The eval.js runs in the page's MAIN world. Make sure you're looking at the correct tab's console, not the extension's background page.
82
+
83
+ ---
84
+
85
+ ## Technique 4: window.fetch interception
86
+
87
+ Capture data from requests the page makes on its own — not your `fetch` step, but the page's own API calls triggered by UI interactions.
88
+
89
+ ```js
90
+ // Install the interceptor BEFORE triggering the UI action
91
+ if (!window.__rfb) {
92
+ window.__rfb = [];
93
+ const origFetch = window.fetch;
94
+ window.fetch = function() {
95
+ const args = arguments;
96
+ const url = (args[0]?.url || args[0] || '').toString();
97
+ const body = (args[1]?.body || '').toString();
98
+ return origFetch.apply(this, args).then(r => {
99
+ if (url.includes('YOUR_KEYWORD')) {
100
+ r.clone().text().then(t => {
101
+ window.__rfb.push({ url, reqBody: body, resBody: t });
102
+ }).catch(() => {});
103
+ }
104
+ return r;
105
+ });
106
+ };
107
+ }
108
+ ```
109
+
110
+ Then trigger the UI action (click a button, type in a field), wait, and read:
111
+
112
+ ```js
113
+ await new Promise(r => setTimeout(r, 5000));
114
+ const captured = window.__rfb;
115
+ window.__rfb = [];
116
+ if (!captured.length) throw new Error('No requests captured — check URL keyword');
117
+
118
+ // Parse and process
119
+ const data = JSON.parse(captured[0].resBody);
120
+ ```
121
+
122
+ **Gotcha: timing.** The interceptor must be installed before the page makes the request. If the page fires requests on load, install the interceptor, then re-trigger (e.g., navigate, click a refresh button).
123
+
124
+ **Gotcha: cloning.** Always use `r.clone().text()` — reading the original response body consumes it and breaks the page.
125
+
126
+ ---
127
+
128
+ ## Technique 5: CDP Fetch.enable
129
+
130
+ When `window.fetch` interception captures nothing — the request uses a transport invisible to page-level JS.
131
+
132
+ **Common causes:**
133
+ - **MCAS proxy** (`*.mcas.ms`) — Microsoft Defender for Cloud Apps injects `js-wrapper.js` that handles requests outside `window.fetch`
134
+ - **Service Worker** — SW intercepts and replays requests on a separate CDP target
135
+ - **Custom XHR wrappers** — legacy apps using `XMLHttpRequest` with custom transport layers
136
+
137
+ ### How to verify the gap
138
+
139
+ Widen the interceptor to log ALL fetch calls, not just your keyword:
140
+
141
+ ```js
142
+ window.fetch = function() {
143
+ const url = (arguments[0]?.url || arguments[0] || '').toString();
144
+ console.log('[cg fetch]', url.slice(0, 120));
145
+ return origFetch.apply(this, arguments);
146
+ };
147
+ ```
148
+
149
+ If the request you're looking for doesn't appear in the console but the data renders in the UI → it's using a transport below `window.fetch`.
150
+
151
+ ### The fix: CDP Fetch domain
152
+
153
+ Requires modifying `chrome/src/background/chrome-adapter.ts`. The `Fetch` CDP domain intercepts at the browser's network stack — below the SW and MCAS layers.
154
+
155
+ ```typescript
156
+ // In attachDebugger():
157
+ await chrome.debugger.sendCommand({ tabId }, 'Fetch.enable', {
158
+ patterns: [{ urlPattern: '*your-pattern*', requestStage: 'Response' }],
159
+ });
160
+ ```
161
+
162
+ Handle `Fetch.requestPaused` events:
163
+
164
+ ```typescript
165
+ // 1. Read the response body
166
+ const result = await chrome.debugger.sendCommand(
167
+ debuggee, 'Fetch.getResponseBody', { requestId }
168
+ );
169
+ const raw = result as { body: string; base64Encoded: boolean };
170
+
171
+ // 2. Decode if base64 (MCAS proxied responses are always base64)
172
+ const body = raw.base64Encoded ? atob(raw.body) : (raw.body || '');
173
+
174
+ // 3. Check for your signal
175
+ if (body.includes('yourKeyword')) {
176
+ // Inject into page context via chrome.scripting.executeScript
177
+ await chrome.scripting.executeScript({
178
+ target: { tabId },
179
+ world: 'MAIN',
180
+ func: (data) => { window.__rfb.push(data); },
181
+ args: [{ body }],
182
+ });
183
+ }
184
+
185
+ // 4. ALWAYS continue the request or the page hangs
186
+ await chrome.debugger.sendCommand(
187
+ debuggee, 'Fetch.continueRequest', { requestId }
188
+ ).catch(() => {}); // swallow errors on detached targets
189
+ ```
190
+
191
+ ### Critical gotchas
192
+
193
+ 1. **base64 decoding** — `Fetch.getResponseBody` returns base64 for MCAS-proxied responses. Check the `base64Encoded` flag. Always `atob()` before string matching.
194
+
195
+ 2. **Must continue** — every `Fetch.requestPaused` must be followed by `Fetch.continueRequest`, `Fetch.fulfillRequest`, or `Fetch.failRequest`. Missing this hangs the page permanently. Use `.catch()` to swallow errors from detached targets.
196
+
197
+ 3. **TypeScript** — `atob` is available in the Chrome extension service worker at runtime but not typed. Add at file top:
198
+ ```typescript
199
+ declare function atob(data: string): string;
200
+ ```
201
+
202
+ 4. **Service Worker targets** — SW network events are on a separate CDP target. Attach to both the tab and the SW:
203
+ ```typescript
204
+ const targets = await chrome.debugger.getTargets();
205
+ const sw = targets.find(t =>
206
+ t.type === 'service_worker' && t.url.startsWith(origin)
207
+ );
208
+ if (sw?.id) {
209
+ await chrome.debugger.attach({ targetId: sw.id }, '1.3');
210
+ await chrome.debugger.sendCommand(
211
+ { targetId: sw.id }, 'Fetch.enable',
212
+ { patterns: [{ urlPattern: '*pattern*', requestStage: 'Response' }] }
213
+ );
214
+ }
215
+ ```
216
+
217
+ 5. **Chrome `debugger` permission** — the extension manifest needs:
218
+ ```json
219
+ { "permissions": ["debugger"] }
220
+ ```
221
+
222
+ Full investigation writeup: `docs/teams-room-availability-fix.md`.
223
+
224
+ ---
225
+
226
+ ## Technique 6: Auth patterns
227
+
228
+ ### SSO/MFA redirect polling
229
+
230
+ The first `navigate` triggers SSO. Build in wait time:
231
+
232
+ ```js
233
+ // Poll for a signal that auth completed
234
+ const deadline = Date.now() + 30000;
235
+ while (Date.now() < deadline) {
236
+ // Signal: a DOM element that only renders when logged in
237
+ if (document.querySelector('.user-avatar')) break;
238
+ // Or: check if we're still on a login page
239
+ if (/login|oauth|signin|sso/i.test(location.href)) {
240
+ await new Promise(r => setTimeout(r, 2000));
241
+ continue;
242
+ }
243
+ break;
244
+ }
245
+ ```
246
+
247
+ ### MSAL token extraction (Azure AD apps)
248
+
249
+ Many Microsoft/enterprise apps store tokens in sessionStorage. See `connectors/lib/msal-token.js` for the canonical pattern:
250
+
251
+ ```js
252
+ const deadline = Date.now() + 60000;
253
+ let token;
254
+ while (Date.now() < deadline) {
255
+ const key = Object.keys(sessionStorage).find(k => k.includes('accesstoken'));
256
+ if (key) {
257
+ try {
258
+ const data = JSON.parse(sessionStorage.getItem(key));
259
+ // Check token isn't about to expire (30s buffer)
260
+ if (Number(data.expiresOn) > Math.floor(Date.now() / 1000) + 30) {
261
+ token = data.secret;
262
+ break;
263
+ }
264
+ } catch (_) {}
265
+ }
266
+ await new Promise(r => setTimeout(r, 1000));
267
+ }
268
+ if (!token) throw new Error('No valid MSAL access token — log in and retry');
269
+ ```
270
+
271
+ Use the token as a Bearer header:
272
+ ```js
273
+ const resp = await fetch(apiUrl, {
274
+ headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' }
275
+ });
276
+ ```
277
+
278
+ ### Session cookies (fetch pattern)
279
+
280
+ For `fetch` pattern connectors, session cookies are sent automatically — `fetchFromPage` uses `credentials: 'include'`. Just `navigate` to the domain first to establish the session through SSO.
281
+
282
+ ---
283
+
284
+ ## Technique 7: SPA rendering delays
285
+
286
+ SPAs (React, Angular, Vue) render data after the initial page load. The `wait` step checks that a selector *exists* in the DOM — not that data has populated.
287
+
288
+ ### In eval.js — poll for content
289
+
290
+ ```js
291
+ const deadline = Date.now() + 30000;
292
+ while (Date.now() < deadline) {
293
+ const rows = document.querySelectorAll('.data-table tbody tr');
294
+ if (rows.length > 0) break;
295
+ await new Promise(r => setTimeout(r, 1000));
296
+ }
297
+ ```
298
+
299
+ ### In YAML — chain wait + extract
300
+
301
+ ```yaml
302
+ - step: wait
303
+ selector: ".data-table tbody tr" # wait for actual rows, not just the table
304
+ timeout: 30000
305
+ - step: extract
306
+ selector: ".data-table tbody tr"
307
+ fields:
308
+ name: "td:nth-child(1)"
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Technique 8: Driving UI interactions in eval.js
314
+
315
+ When you need to click buttons, type in fields, or navigate within an SPA — all from eval.js. See `connectors/teams-room-availability.eval.js` for a production example of all three.
316
+
317
+ ### Clicking by accessible name
318
+
319
+ ```js
320
+ function clickByName(name) {
321
+ const els = Array.from(document.querySelectorAll('button, a, [role=button]'));
322
+ const el = els.find(e => {
323
+ if (!e.offsetParent) return false; // skip hidden
324
+ const label = (e.getAttribute('aria-label') || '').trim();
325
+ const text = (e.textContent || '').replace(/\s+/g, ' ').trim();
326
+ return label === name || label.includes(name) || text.includes(name);
327
+ });
328
+ if (!el) return false;
329
+ el.scrollIntoView({ block: 'center' });
330
+ el.click();
331
+ return true;
332
+ }
333
+ ```
334
+
335
+ ### Typing into React-controlled inputs
336
+
337
+ React overrides the native `value` setter. Use the prototype setter to bypass:
338
+
339
+ ```js
340
+ function typeText(selector, text) {
341
+ const el = document.querySelector(selector);
342
+ if (!el) throw new Error(`Element "${selector}" not found`);
343
+ el.focus();
344
+ const setter = Object.getOwnPropertyDescriptor(
345
+ window.HTMLInputElement.prototype, 'value'
346
+ ).set;
347
+ setter.call(el, text);
348
+ el.dispatchEvent(new Event('input', { bubbles: true }));
349
+ el.dispatchEvent(new Event('change', { bubbles: true }));
350
+ }
351
+ ```
352
+
353
+ ### Waiting for a UI reaction
354
+
355
+ After clicking or typing, poll for the expected outcome — never use fixed sleeps alone:
356
+
357
+ ```js
358
+ clickByName('Submit');
359
+ const deadline = Date.now() + 10000;
360
+ while (Date.now() < deadline) {
361
+ if (document.querySelector('.results-loaded')) break;
362
+ await new Promise(r => setTimeout(r, 500));
363
+ }
364
+ ```