octocode-cli 1.3.1 → 1.5.1

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 (102) hide show
  1. package/README.md +193 -34
  2. package/out/chunks/chunk-7476PETK.js +309 -0
  3. package/out/chunks/chunk-CVNNNSMQ.js +26 -0
  4. package/out/chunks/chunk-OQBJTZWK.js +60 -0
  5. package/out/chunks/chunk-UCZCF3BQ.js +9 -0
  6. package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
  7. package/out/chunks/commands-XBFPLHSQ.js +8 -0
  8. package/out/chunks/help-P7TCOYAJ.js +10 -0
  9. package/out/chunks/main-help-ULF5PAQY.js +10 -0
  10. package/out/chunks/prompts-5E6VKRX5.js +8 -0
  11. package/out/chunks/spinner-URV2OX6O.js +8 -0
  12. package/out/chunks/tool-command-M6VA7P2F.js +8 -0
  13. package/out/octocode-cli.js +1 -1
  14. package/package.json +5 -3
  15. package/skills/README.md +13 -1
  16. package/skills/agentic-flow-best-practices/SKILL.md +280 -0
  17. package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
  18. package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
  19. package/skills/agentic-flow-best-practices/references/resources.md +112 -0
  20. package/skills/octocode-chrome-devtools/README.md +541 -0
  21. package/skills/octocode-chrome-devtools/SKILL.md +197 -0
  22. package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
  23. package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
  24. package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
  25. package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
  26. package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
  27. package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
  28. package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
  29. package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
  30. package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
  31. package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
  32. package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
  33. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
  34. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
  35. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
  36. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
  37. package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
  38. package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
  39. package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
  40. package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
  41. package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
  42. package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
  43. package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
  44. package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
  45. package/skills/octocode-design/README.md +2 -2
  46. package/skills/octocode-documentation-writer/README.md +1 -1
  47. package/skills/octocode-engineer/README.md +1 -1
  48. package/skills/octocode-engineer/SKILL.md +4 -2
  49. package/skills/octocode-engineer/references/output-files.md +3 -3
  50. package/skills/octocode-engineer/scripts/run.js +136 -136
  51. package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
  52. package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
  53. package/skills/octocode-install/SKILL.md +1 -1
  54. package/skills/octocode-pull-request-reviewer/README.md +5 -5
  55. package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
  56. package/skills/octocode-research/AGENTS.md +1 -1
  57. package/skills/octocode-research/README.md +2 -2
  58. package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
  59. package/skills/octocode-research/scripts/server.js +191 -246
  60. package/skills/octocode-research/src/routes/github.ts +4 -21
  61. package/skills/octocode-research/src/routes/local.ts +4 -21
  62. package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
  63. package/skills/octocode-search-skill/SKILL.md +245 -229
  64. package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
  65. package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
  66. package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
  67. package/skills/octocode-search-skill/references/install-reference.md +130 -0
  68. package/skills/octocode-search-skill/references/references-template.md +27 -0
  69. package/skills/octocode-search-skill/references/references.md +62 -0
  70. package/skills/octocode-slides/README.md +307 -0
  71. package/skills/octocode-slides/SKILL.md +410 -0
  72. package/skills/octocode-slides/references/01-brief.md +156 -0
  73. package/skills/octocode-slides/references/02-research.md +149 -0
  74. package/skills/octocode-slides/references/03-outline.md +172 -0
  75. package/skills/octocode-slides/references/04-design.md +301 -0
  76. package/skills/octocode-slides/references/05-implementation.md +213 -0
  77. package/skills/octocode-slides/references/06-review.md +258 -0
  78. package/skills/octocode-slides/references/animation.md +281 -0
  79. package/skills/octocode-slides/references/design-system.md +316 -0
  80. package/skills/octocode-slides/references/html-templates.md +673 -0
  81. package/skills/octocode-slides/references/image-generation.md +448 -0
  82. package/skills/octocode-slides/references/resources.md +840 -0
  83. package/skills/octocode-slides/references/slide-rules.md +541 -0
  84. package/skills/octocode-slides/references/wireframes.md +727 -0
  85. package/skills/octocode-slides/scripts/animation.js +182 -0
  86. package/skills/octocode-slides/scripts/base.css +353 -0
  87. package/skills/octocode-slides/scripts/base.html +655 -0
  88. package/skills/octocode-slides/scripts/generate_image.py +221 -0
  89. package/skills/octocode-slides/scripts/navbridge.js +79 -0
  90. package/skills/octocode-slides/scripts/presenter.js +316 -0
  91. package/skills/octocode-slides/scripts/slide.html +248 -0
  92. package/skills/octocode-stats/SKILL.md +73 -0
  93. package/skills/octocode-stats/assets/template.html +1332 -0
  94. package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
  95. package/assets/example.png +0 -0
  96. package/out/chunks/chunk-QCY7Q7YW.js +0 -389
  97. package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
  98. package/out/chunks/commands-OCTZP2TO.js +0 -51
  99. package/out/chunks/help-XPXP46ZT.js +0 -10
  100. package/out/chunks/main-help-35HX2UDQ.js +0 -10
  101. package/out/chunks/tool-command-HOSMVLNK.js +0 -8
  102. package/skills/octocode-search-skill/INSTALL_REFERENCE.md +0 -112
@@ -0,0 +1,403 @@
1
+ # CDP Browser Surface Pattern Details
2
+
3
+ ## WebSocket Surveillance
4
+
5
+ ```js
6
+ export async function run(cdp) {
7
+ await cdp.send('Network.enable', {});
8
+
9
+ const sockets = new Map();
10
+ let frameCount = 0;
11
+ let socketCount = 0;
12
+ const pageHost = (() => { try { return new URL(cdp.targetInfo.url).hostname; } catch { return ''; } })();
13
+
14
+ cdp.on('Network.webSocketCreated', ({ requestId, url }) => {
15
+ sockets.set(requestId, url);
16
+ socketCount++;
17
+ console.log(`[NETWORK] WS opened: ${url}`);
18
+ try {
19
+ const host = new URL(url).hostname;
20
+ if (pageHost && host !== pageHost) console.log(`[FINDING] WS_UNKNOWN_HOST: ${url}`);
21
+ } catch {}
22
+ });
23
+
24
+ cdp.on('Network.webSocketFrameSent', ({ requestId, response }) => {
25
+ const url = sockets.get(requestId) ?? 'unknown';
26
+ const payload = response.payloadData ?? '';
27
+ frameCount++;
28
+ console.log(`[NETWORK] WS SENT → ${url} (${payload.length} chars)`);
29
+ if (/token|password|secret|key|auth/i.test(payload))
30
+ console.log(`[FINDING] SENSITIVE_IN_WS_FRAME: sent to ${url}`);
31
+ if (/^[A-Za-z0-9+/]{40,}={0,2}$/.test(payload.trim()))
32
+ console.log(`[FINDING] WS_BASE64_FRAME: possible encoded data sent to ${url}`);
33
+ if (payload.length > 100000)
34
+ console.log(`[FINDING] LARGE_WS_FRAME: ${(payload.length / 1024).toFixed(1)}KB sent to ${url}`);
35
+ });
36
+
37
+ cdp.on('Network.webSocketFrameReceived', ({ requestId, response }) => {
38
+ const url = sockets.get(requestId) ?? 'unknown';
39
+ const payload = response.payloadData ?? '';
40
+ frameCount++;
41
+ console.log(`[NETWORK] WS RECV ← ${url} (${payload.length} chars)`);
42
+ });
43
+
44
+ cdp.on('Network.webSocketClosed', ({ requestId }) => {
45
+ const url = sockets.get(requestId) ?? 'unknown';
46
+ console.log(`[NETWORK] WS closed: ${url}`);
47
+ sockets.delete(requestId);
48
+ });
49
+
50
+ console.log('[FINDING] WebSocket monitoring active — collecting for 15s...');
51
+ await new Promise(r => setTimeout(r, 15000));
52
+ console.log(`[METRIC] WS sockets seen: ${socketCount} Total frames: ${frameCount}`);
53
+ }
54
+ ```
55
+
56
+ ## Search Text Across All Resources
57
+
58
+ ```js
59
+ // Search for a string in all loaded JS, CSS, and network response bodies
60
+ // Requires Debugger.enable + Debugger.setSkipAllPauses (prevents breakpoint hangs)
61
+ export async function run(cdp) {
62
+ await cdp.send('Network.enable', {});
63
+ await cdp.send('Runtime.enable', {});
64
+ await cdp.send('Page.enable', {});
65
+ await cdp.send('DOM.enable', {});
66
+ await cdp.send('CSS.enable', {});
67
+ await cdp.send('Debugger.enable', {});
68
+ await cdp.send('Debugger.setSkipAllPauses', { skip: true }); // CRITICAL: prevents Runtime.evaluate hangs
69
+
70
+ cdp.on('Page.javascriptDialogOpening', () =>
71
+ cdp.send('Page.handleJavaScriptDialog', { accept: true }));
72
+
73
+ const SEARCH_TERM = 'YOUR_TERM_HERE';
74
+ const scripts = {};
75
+ const styleSheets = {};
76
+ const responseIds = [];
77
+
78
+ cdp.on('Debugger.scriptParsed', ({ scriptId, url }) => { scripts[scriptId] = url || '(inline)'; });
79
+ cdp.on('CSS.styleSheetAdded', ({ header }) => { styleSheets[header.styleSheetId] = header.sourceURL || '(inline)'; });
80
+ cdp.on('Network.responseReceived', ({ requestId, response }) => {
81
+ const ct = (response.mimeType ?? '').toLowerCase();
82
+ if (['json','text','html','javascript','xml','css'].some(t => ct.includes(t)))
83
+ responseIds.push([requestId, response.url]);
84
+ });
85
+
86
+ await cdp.send('Page.navigate', { url: 'https://TARGET_URL/' });
87
+ await new Promise(r => setTimeout(r, 4000));
88
+
89
+ // Search in JS
90
+ for (const [scriptId, url] of Object.entries(scripts)) {
91
+ let result;
92
+ try { ({ result } = await cdp.send('Debugger.searchInContent', {
93
+ scriptId, query: SEARCH_TERM, caseSensitive: false, isRegex: false
94
+ })); } catch { continue; }
95
+ if (result?.length)
96
+ result.forEach(r => console.log(`[SEARCH] JS L${r.lineNumber}: ${r.lineContent.trim().slice(0,120)}`));
97
+ }
98
+
99
+ // Search in CSS
100
+ for (const [styleSheetId] of Object.entries(styleSheets)) {
101
+ let text;
102
+ try { ({ text } = await cdp.send('CSS.getStyleSheetText', { styleSheetId })); } catch { continue; }
103
+ if (text?.toLowerCase().includes(SEARCH_TERM.toLowerCase()))
104
+ console.log(`[SEARCH] CSS hit found`);
105
+ }
106
+
107
+ // Search in network bodies
108
+ for (const [requestId, url] of responseIds) {
109
+ try {
110
+ const { body, base64Encoded } = await cdp.send('Network.getResponseBody', { requestId });
111
+ const text = base64Encoded ? Buffer.from(body, 'base64').toString() : (body ?? '');
112
+ if (text.toLowerCase().includes(SEARCH_TERM.toLowerCase()))
113
+ console.log(`[SEARCH] BODY hit in ${url.split('/').pop() || url}`);
114
+ } catch { continue; }
115
+ }
116
+
117
+ // Search in DOM text
118
+ const { result: domRes } = await cdp.send('Runtime.evaluate', {
119
+ expression: `(function() {
120
+ const term = ${JSON.stringify(SEARCH_TERM.toLowerCase())};
121
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
122
+ const hits = []; let node;
123
+ while ((node = walker.nextNode()))
124
+ if (node.textContent.toLowerCase().includes(term))
125
+ hits.push('<' + node.parentElement?.tagName + '> ' + node.textContent.trim().slice(0,80));
126
+ return hits;
127
+ })()`, returnByValue: true
128
+ });
129
+ (domRes.value ?? []).forEach(h => console.log(`[SEARCH] DOM: ${h}`));
130
+ }
131
+ ```
132
+
133
+ ## File Upload
134
+
135
+ Upload a file via a native `input[type="file"]` element using `DOM.setFileInputFiles`. Files must be absolute paths on the machine running Chrome.
136
+
137
+ ```js
138
+ export async function run(cdp) {
139
+ await cdp.send('DOM.enable', {});
140
+ await cdp.send('Runtime.enable', {});
141
+ await cdp.send('Page.enable', {});
142
+
143
+ cdp.on('Page.javascriptDialogOpening', () =>
144
+ cdp.send('Page.handleJavaScriptDialog', { accept: true }));
145
+
146
+ await cdp.send('Page.navigate', { url: 'https://example.com/upload' });
147
+ await new Promise(r => setTimeout(r, 2000));
148
+
149
+ // Find the file input
150
+ const { root } = await cdp.send('DOM.getDocument', { depth: 0 });
151
+ const { nodeId } = await cdp.send('DOM.querySelector', {
152
+ nodeId: root.nodeId,
153
+ selector: 'input[type="file"]',
154
+ });
155
+
156
+ if (nodeId === 0) {
157
+ console.log('[FINDING] NO_FILE_INPUT: no input[type="file"] found on page');
158
+ return;
159
+ }
160
+
161
+ // Set files — absolute paths only
162
+ await cdp.send('DOM.setFileInputFiles', {
163
+ nodeId,
164
+ files: ['/absolute/path/to/your-file.txt'],
165
+ // For multiple files: files: ['/path/a.txt', '/path/b.png']
166
+ });
167
+ console.log('[AUTOMATE] file set on input[type="file"]');
168
+
169
+ // Dispatch change + input events so React/Vue/Angular frameworks detect the selection
170
+ await cdp.send('Runtime.evaluate', {
171
+ expression: `
172
+ const el = document.querySelector('input[type="file"]');
173
+ el.dispatchEvent(new Event('input', { bubbles: true }));
174
+ el.dispatchEvent(new Event('change', { bubbles: true }));
175
+ `,
176
+ });
177
+ console.log('[AUTOMATE] change/input events dispatched');
178
+
179
+ await new Promise(r => setTimeout(r, 1500));
180
+ console.log('[METRIC] File upload step complete');
181
+ }
182
+ ```
183
+
184
+ **Gotchas:**
185
+ - `files` must be **absolute paths** — relative paths and URLs are rejected
186
+ - `multiple` inputs: pass all files in one array `files: ['/a', '/b']`
187
+ - Hidden file inputs triggered by a button: click the button first via `Runtime.evaluate` click, then call `DOM.setFileInputFiles` on the now-visible (or still-hidden) `nodeId`
188
+ - Always dispatch `change` and `input` events after setting files — CDP sets the value silently, frameworks won't react otherwise
189
+ - If the nodeId is 0 and you know the input exists, the page may still be loading — add a `waitForSelector` call before `DOM.querySelector`
190
+
191
+ ## Save Files Screenshots PDFs and Metadata
192
+
193
+ Always use `cdp.outputDir` — it is the only writable location in sandbox mode and works on Windows, macOS, and Linux.
194
+ Output lands in `<TMPDIR>/.octocode-chrome-devtools/<timestamp>/` — the agent reads the `[CDP_RUNNER] Output dir:` line in stderr to find it.
195
+
196
+ ```js
197
+ export async function run(cdp) {
198
+ await cdp.send('Page.enable', {});
199
+
200
+ const { writeFileSync } = await import('fs');
201
+ const { join } = await import('path');
202
+
203
+ // ── Screenshot ────────────────────────────────────────────────────────────
204
+ await cdp.send('Page.navigate', { url: 'https://example.com' });
205
+ await new Promise(r => setTimeout(r, 2000)); // wait for render
206
+
207
+ const { data: pngData } = await cdp.send('Page.captureScreenshot', { format: 'png' });
208
+ const screenshotPath = join(cdp.outputDir, 'screenshot.png');
209
+ writeFileSync(screenshotPath, Buffer.from(pngData, 'base64'));
210
+ console.log(`[SCREENSHOT] ${screenshotPath}`);
211
+
212
+ // ── PDF ───────────────────────────────────────────────────────────────────
213
+ const { data: pdfData } = await cdp.send('Page.printToPDF', { printBackground: true });
214
+ const pdfPath = join(cdp.outputDir, 'page.pdf');
215
+ writeFileSync(pdfPath, Buffer.from(pdfData, 'base64'));
216
+ console.log(`[FINDING] PDF saved → ${pdfPath}`);
217
+
218
+ // ── Metadata / findings JSON ──────────────────────────────────────────────
219
+ const metadata = {
220
+ url: cdp.targetInfo.url,
221
+ timestamp: new Date().toISOString(),
222
+ findings: [], // push [FINDING] items here to get a machine-readable report
223
+ };
224
+ // metadata.findings.push({ type: 'HTTP_ERROR', status: 404, url: '...' });
225
+ const metaPath = join(cdp.outputDir, 'metadata.json');
226
+ writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
227
+ console.log(`[METRIC] metadata saved → ${metaPath}`);
228
+ }
229
+ ```
230
+
231
+ **Key rules:**
232
+ - Use `cdp.outputDir` — never `os.tmpdir()` directly in sandbox mode
233
+ - Write pattern: `const { writeFileSync } = await import('fs'); const { join } = await import('path');`
234
+ - The runner logs `[CDP_RUNNER] Output dir: <path>` to stderr — the agent reads this to locate all output files
235
+
236
+ ## Shadow DOM Querying Inside Shadow Roots
237
+
238
+ `DOM.querySelector / querySelectorAll` do **not** pierce shadow boundaries. Use `Runtime.evaluate` with a recursive traversal, or `DOM.getDocument({ pierce: true })` to inspect the full tree.
239
+
240
+ ```js
241
+ // Requires: DOM.enable, Runtime.enable
242
+
243
+ // Returns a remote objectId — use with Runtime.callFunctionOn or DOM.resolveNode
244
+ async function queryShadowDOM(cdp, selector) {
245
+ const { result } = await cdp.send('Runtime.evaluate', {
246
+ expression: `(function pierce(root, sel) {
247
+ const el = root.querySelector(sel);
248
+ if (el) return el;
249
+ for (const host of root.querySelectorAll('*')) {
250
+ if (host.shadowRoot) {
251
+ const found = pierce(host.shadowRoot, sel);
252
+ if (found) return found;
253
+ }
254
+ }
255
+ return null;
256
+ })(document, ${JSON.stringify(selector)})`,
257
+ returnByValue: false, // keep remote objectId for further CDP calls
258
+ });
259
+ return result.objectId ?? null;
260
+ }
261
+
262
+ export async function run(cdp) {
263
+ await cdp.send('DOM.enable', {});
264
+ await cdp.send('Runtime.enable', {});
265
+
266
+ // Get document tree including shadow roots (pierce: true)
267
+ // Shadow roots appear as DOCUMENT_FRAGMENT nodes (nodeType 11) in the children array
268
+ const { root } = await cdp.send('DOM.getDocument', { depth: 3, pierce: true });
269
+ console.log(`[DOM] Root: ${root.nodeName}, childCount: ${root.childNodeCount}`);
270
+
271
+ // Extract all text from elements inside shadow roots via Runtime.evaluate
272
+ const { result } = await cdp.send('Runtime.evaluate', {
273
+ expression: `(function() {
274
+ const hits = [];
275
+ function walk(root) {
276
+ for (const el of root.querySelectorAll('*')) {
277
+ if (el.shadowRoot) walk(el.shadowRoot);
278
+ }
279
+ // change 'my-component button' to your target selector
280
+ for (const el of root.querySelectorAll('my-component button')) {
281
+ hits.push(el.textContent.trim());
282
+ }
283
+ }
284
+ walk(document);
285
+ return JSON.stringify(hits);
286
+ })()`,
287
+ returnByValue: true,
288
+ });
289
+ const items = JSON.parse(result.value ?? '[]');
290
+ items.forEach(t => console.log(`[SCRAPE] shadow-DOM item: "${t}"`));
291
+ console.log(`[METRIC] Shadow DOM items found: ${items.length}`);
292
+ if (items.length === 0)
293
+ console.log('[FINDING] SHADOW_DOM_EMPTY: selector found nothing in shadow roots — check host element and inner selector');
294
+ }
295
+
296
+ async function clickInShadowDOM(cdp, hostSelector, innerSelector) {
297
+ const { result } = await cdp.send('Runtime.evaluate', {
298
+ expression: `(function() {
299
+ const host = document.querySelector(${JSON.stringify(hostSelector)});
300
+ const el = host?.shadowRoot?.querySelector(${JSON.stringify(innerSelector)});
301
+ if (!el) return { found: false };
302
+ const r = el.getBoundingClientRect();
303
+ return { found: true, x: r.left + r.width / 2, y: r.top + r.height / 2 };
304
+ })()`,
305
+ returnByValue: true,
306
+ });
307
+ if (!result.value?.found) {
308
+ console.log(`[FINDING] SHADOW_DOM_NOT_FOUND: "${innerSelector}" not found in "${hostSelector}" shadow root`);
309
+ return;
310
+ }
311
+ const { x, y } = result.value;
312
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
313
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
314
+ console.log(`[AUTOMATE] clicked "${innerSelector}" inside "${hostSelector}" shadow root`);
315
+ }
316
+ ```
317
+
318
+ **Key facts:**
319
+ - `DOM.getDocument({ pierce: true })` includes shadow roots in the returned tree — shadow roots are `nodeType: 11` (DOCUMENT_FRAGMENT_NODE)
320
+ - `DOM.querySelector / querySelectorAll` do **NOT** cross shadow boundaries even with `pierce: true` on the document
321
+ - `Runtime.evaluate` with recursive traversal is the most reliable approach for querying
322
+ - **Closed shadow roots** (`attachShadow({ mode: 'closed' })`) — JavaScript cannot access `.shadowRoot`; CDP has no bypass
323
+ - **Nested shadows** — the `pierce()` helper above handles arbitrary nesting depth
324
+
325
+ ## Source Map Resolution
326
+
327
+ Resolves minified compiled positions back to original source names and files.
328
+ Requires `sourcemap-resolver.mjs` to be present in the same directory as the script.
329
+ The sandbox runner stages `sourcemap-resolver.mjs` in `$TMPDIR` automatically for generated scripts.
330
+ Works gracefully when maps are absent — always returns `null` instead of throwing.
331
+
332
+ **When to add this pattern:**
333
+ - `js-coverage` intent — show readable function names in DEAD_CODE findings
334
+ - `debug` intent — enrich stack frames with original file + line
335
+ - Any intent where you want to understand *what* a minified script does
336
+
337
+ ```js
338
+ // Import the resolver BEFORE enabling other domains (must register scriptParsed ASAP)
339
+ const { createSourceMapResolver } = await import(
340
+ new URL('./sourcemap-resolver.mjs', import.meta.url).href
341
+ );
342
+ const resolver = await createSourceMapResolver(cdp);
343
+ // Debugger.enable and Debugger.setSkipAllPauses are called internally by createSourceMapResolver
344
+
345
+ // ...enable Network, Profiler, etc. and navigate the page...
346
+
347
+ // After page is fully loaded, wait for all map loads to settle:
348
+ await resolver.settle();
349
+
350
+
351
+ // Option A: resolve a single generated position
352
+ const orig = resolver.resolve(scriptId, lineNumber, columnNumber); // all 0-indexed
353
+ if (orig) {
354
+ const fnName = orig.name ?? '(anonymous)';
355
+ const src = orig.source?.split('/').slice(-2).join('/') ?? 'unknown'; // last 2 path parts
356
+ console.log(`[SOURCEMAP] ${fnName} → ${src}:${orig.line}`);
357
+ }
358
+
359
+ // Option B: enrich Profiler coverage results with source map data
360
+ for (const script of coverageResult) {
361
+ const url = script.url;
362
+ if (!url || url.startsWith('chrome-extension')) continue;
363
+
364
+ for (const fn of script.functions) {
365
+ const isUsed = fn.ranges.some(r => r.count > 0);
366
+ if (isUsed) continue; // only report dead code
367
+
368
+ // Try to resolve the first byte of the function
369
+ const [startLine, startCol] = offsetToLineCol(fn.ranges[0]?.startOffset ?? 0, compiledText);
370
+ const orig = resolver.resolve(script.scriptId, startLine, startCol);
371
+
372
+ const displayName = orig?.name ?? (fn.functionName?.length > 2 ? fn.functionName : null);
373
+ if (!displayName) continue; // skip mangled single/double chars
374
+
375
+ const loc = orig
376
+ ? `${orig.source?.split('/').pop() ?? 'unknown'}:${orig.line}`
377
+ : url.split('/').pop();
378
+ console.log(`[FINDING] DEAD_CODE: ${displayName} in ${loc}`);
379
+ }
380
+ }
381
+
382
+ // Helper: convert character offset to {line, col} (0-indexed) — only needed for Profiler
383
+ function offsetToLineCol(offset, source) {
384
+ let line = 0, col = 0;
385
+ for (let i = 0; i < offset && i < source.length; i++) {
386
+ if (source[i] === '\n') { line++; col = 0; } else col++;
387
+ }
388
+ return [line, col];
389
+ }
390
+
391
+ resolver.printSummary();
392
+ // Emits: [SOURCEMAP] 42 scripts: 12 maps loaded, 3 failed, 27 had no map
393
+ ```
394
+
395
+ **Key facts:**
396
+ - `Debugger.scriptParsed` fires for every script during page load — resolver must be created before navigation
397
+ - `resolver.settle()` must be called after page is loaded to ensure all async map fetches complete
398
+ - Inline `data:application/json;base64,...` maps are decoded instantly with no network call
399
+ - External `.map` URLs are fetched with a 4s timeout; failures are silently counted
400
+ - `sourcesContent` (full original source code) is **always stripped** — never stored or emitted
401
+ - `resolver.resolve()` returns `null` when script has no map or position is outside all segments
402
+ - The Profiler gives `startOffset` (byte offset) not `{line,col}` — use `offsetToLineCol()` helper
403
+ - Short function names (`length <= 2`) after mangling are meaningless — skip unless `orig.name` resolves them
@@ -0,0 +1,275 @@
1
+ # CDP Observation Pattern Details
2
+
3
+ ## Network Console (most common)
4
+
5
+ ```js
6
+ export async function run(cdp) {
7
+ await cdp.send('Network.enable', {});
8
+ await cdp.send('Runtime.enable', {});
9
+ await cdp.send('Log.enable', {});
10
+
11
+ const requests = new Map();
12
+
13
+ cdp.on('Network.requestWillBeSent', ({ requestId, request }) => {
14
+ requests.set(requestId, { url: request.url, method: request.method });
15
+ });
16
+
17
+ cdp.on('Network.responseReceived', ({ requestId, response }) => {
18
+ const r = requests.get(requestId);
19
+ if (!r) return;
20
+ console.log(`[NETWORK] ${response.status} ${r.method} ${r.url}`);
21
+ if (response.status >= 400)
22
+ console.log(`[NETWORK_ERROR] HTTP ${response.status} → ${r.url}`);
23
+ });
24
+
25
+ cdp.on('Network.loadingFailed', ({ requestId, errorText }) => {
26
+ const r = requests.get(requestId);
27
+ console.log(`[NETWORK_FAILED] ${r?.url ?? 'unknown'}: ${errorText}`);
28
+ });
29
+
30
+ cdp.on('Runtime.consoleAPICalled', ({ type, args }) => {
31
+ const msg = args.map(a => a.value ?? a.description ?? '[object]').join(' ');
32
+ console.log(`[CONSOLE:${type.toUpperCase()}] ${msg}`);
33
+ });
34
+
35
+ cdp.on('Runtime.exceptionThrown', ({ exceptionDetails }) => {
36
+ const desc = exceptionDetails.exception?.description ?? exceptionDetails.text;
37
+ console.log(`[EXCEPTION] ${desc}`);
38
+ if (exceptionDetails.stackTrace?.callFrames?.[0]) {
39
+ const f = exceptionDetails.stackTrace.callFrames[0];
40
+ console.log(`[EXCEPTION_LOCATION] ${f.url}:${f.lineNumber}:${f.columnNumber} in ${f.functionName}`);
41
+ }
42
+ });
43
+
44
+ cdp.on('Log.entryAdded', ({ entry }) => {
45
+ if (entry.level === 'error' || entry.level === 'warning')
46
+ console.log(`[LOG:${entry.level.toUpperCase()}] [${entry.source}] ${entry.text}`);
47
+ });
48
+
49
+ // Navigate inside run() when monitoring load events; use --new-tab about:blank for that path
50
+ await cdp.send('Page.enable', {});
51
+ cdp.on('Page.javascriptDialogOpening', () =>
52
+ cdp.send('Page.handleJavaScriptDialog', { accept: true }));
53
+ await cdp.send('Page.navigate', { url: 'https://example.com/' });
54
+
55
+ console.log('[FINDING] Monitoring active — collecting events for 10s...');
56
+ await new Promise(r => setTimeout(r, 10000));
57
+ console.log(`[METRIC] Total requests captured: ${requests.size}`);
58
+ }
59
+ ```
60
+
61
+ ## Performance Audit
62
+
63
+ ```js
64
+ export async function run(cdp) {
65
+ await cdp.send('Performance.enable', {});
66
+ await cdp.send('Runtime.enable', {});
67
+
68
+ const { metrics } = await cdp.send('Performance.getMetrics', {});
69
+ const m = Object.fromEntries(metrics.map(x => [x.name, x.value]));
70
+
71
+ console.log('[PERFORMANCE] JSHeapUsedSize:', (m.JSHeapUsedSize / 1024 / 1024).toFixed(2), 'MB');
72
+ console.log('[PERFORMANCE] TaskDuration:', m.TaskDuration?.toFixed(3), 's');
73
+ console.log('[PERFORMANCE] LayoutCount:', m.LayoutCount);
74
+ console.log('[PERFORMANCE] RecalcStyleCount:', m.RecalcStyleCount);
75
+ console.log('[PERFORMANCE] ScriptDuration:', m.ScriptDuration?.toFixed(3), 's');
76
+ console.log('[PERFORMANCE] Full metrics:', JSON.stringify(m, null, 2));
77
+
78
+ if (m.JSHeapUsedSize > 50_000_000)
79
+ console.log('[FINDING] HIGH_MEMORY: JS heap > 50MB — possible memory leak');
80
+ if (m.LayoutCount > 20)
81
+ console.log('[FINDING] LAYOUT_THRASHING: >20 forced layouts — check for read/write interleaving');
82
+ if (m.ScriptDuration > 2)
83
+ console.log('[FINDING] SLOW_SCRIPTS: script execution > 2s — profile for long tasks');
84
+ }
85
+ ```
86
+
87
+ ## Core Web Vitals (inject before navigate)
88
+
89
+ ```js
90
+ // Add BEFORE Page.navigate — uses Page.addScriptToEvaluateOnNewDocument
91
+ await cdp.send('Page.addScriptToEvaluateOnNewDocument', {
92
+ source: `
93
+ window.__CWV__ = {};
94
+ new PerformanceObserver(list => {
95
+ for (const e of list.getEntries())
96
+ if (e.entryType === 'largest-contentful-paint') window.__CWV__.LCP = e.startTime;
97
+ }).observe({ type: 'largest-contentful-paint', buffered: true });
98
+ new PerformanceObserver(list => {
99
+ for (const e of list.getEntries())
100
+ if (e.entryType === 'layout-shift' && !e.hadRecentInput)
101
+ window.__CWV__.CLS = (window.__CWV__.CLS || 0) + e.value;
102
+ }).observe({ type: 'layout-shift', buffered: true });
103
+ new PerformanceObserver(list => {
104
+ for (const e of list.getEntries())
105
+ if (e.name === 'first-contentful-paint') window.__CWV__.FCP = e.startTime;
106
+ }).observe({ type: 'paint', buffered: true });
107
+ `
108
+ });
109
+ // After page settles, read back:
110
+ const { result } = await cdp.send('Runtime.evaluate', { expression: `window.__CWV__`, returnByValue: true });
111
+ const cwv = result.value ?? {};
112
+ if (cwv.FCP) console.log(`[PERFORMANCE] FCP: ${cwv.FCP.toFixed(0)} ms [${cwv.FCP < 1800 ? 'GOOD' : 'POOR'}]`);
113
+ if (cwv.LCP) console.log(`[PERFORMANCE] LCP: ${cwv.LCP.toFixed(0)} ms [${cwv.LCP < 2500 ? 'GOOD' : 'POOR'}]`);
114
+ if (cwv.CLS != null) console.log(`[PERFORMANCE] CLS: ${cwv.CLS.toFixed(4)} [${cwv.CLS < 0.1 ? 'GOOD' : 'POOR'}]`);
115
+ ```
116
+
117
+ ## DOM Accessibility Audit
118
+
119
+ ```js
120
+ export async function run(cdp) {
121
+ await cdp.send('DOM.enable', {});
122
+ await cdp.send('Runtime.enable', {});
123
+
124
+ const { root } = await cdp.send('DOM.getDocument', { depth: 2 });
125
+ console.log(`[DOM] Root: ${root.nodeName}, children: ${root.childNodeCount}`);
126
+
127
+ const checks = [
128
+ ['Total elements', `document.querySelectorAll('*').length`],
129
+ ['Images missing alt', `document.querySelectorAll('img:not([alt])').length`],
130
+ ['Inputs missing label',`document.querySelectorAll('input:not([aria-label]):not([id])').length`],
131
+ ['Empty buttons', `document.querySelectorAll('button:empty').length`],
132
+ ['Inline scripts', `document.querySelectorAll('script:not([src])').length`],
133
+ ];
134
+
135
+ for (const [label, expr] of checks) {
136
+ const { result } = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true });
137
+ console.log(`[DOM] ${label}: ${result.value}`);
138
+ if (label === 'Total elements' && result.value > 1500)
139
+ console.log('[FINDING] LARGE_DOM: >1500 elements — may hurt rendering performance');
140
+ if (label === 'Images missing alt' && result.value > 0)
141
+ console.log(`[FINDING] ACCESSIBILITY: ${result.value} images missing alt text`);
142
+ }
143
+
144
+ const { result: title } = await cdp.send('Runtime.evaluate', {
145
+ expression: 'document.title', returnByValue: true,
146
+ });
147
+ console.log(`[DOM] Page title: "${title.value}"`);
148
+ }
149
+ ```
150
+
151
+ ## Heap Memory Audit (leak detection)
152
+
153
+ ```js
154
+ export async function run(cdp) {
155
+ await cdp.send('HeapProfiler.enable', {});
156
+
157
+ const chunks = [];
158
+ cdp.on('HeapProfiler.addHeapSnapshotChunk', ({ chunk }) => chunks.push(chunk));
159
+ await cdp.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false });
160
+
161
+ const snapshot = JSON.parse(chunks.join(''));
162
+ const { node_count, edge_count } = snapshot.snapshot.meta;
163
+ console.log(`[METRIC] Heap nodes: ${node_count}, edges: ${edge_count}`);
164
+
165
+ const strings = snapshot.strings;
166
+ const nodeFields = snapshot.snapshot.meta.node_fields;
167
+ const nodeSize = nodeFields.length;
168
+ const nodes = snapshot.nodes;
169
+
170
+ const typeCounts = {};
171
+ for (let i = 0; i < nodes.length; i += nodeSize) {
172
+ const name = strings[nodes[i + 1]];
173
+ typeCounts[name] = (typeCounts[name] ?? 0) + nodes[i + 3];
174
+ }
175
+
176
+ const top = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
177
+ console.log('[PERFORMANCE] Top retained types by self_size:');
178
+ for (const [name, size] of top) {
179
+ console.log(`[PERFORMANCE] ${name}: ${(size / 1024).toFixed(1)} KB`);
180
+ if (size > 5_000_000)
181
+ console.log(`[FINDING] HIGH_RETENTION: "${name}" retains ${(size / 1024 / 1024).toFixed(1)} MB — possible leak`);
182
+ }
183
+
184
+ const detachedIdx = nodeFields.indexOf('detachedness');
185
+ if (detachedIdx !== -1) {
186
+ let detached = 0;
187
+ for (let i = 0; i < nodes.length; i += nodeSize)
188
+ if (nodes[i + detachedIdx] === 1) detached++;
189
+ console.log(`[METRIC] Detached DOM nodes: ${detached}`);
190
+ if (detached > 50)
191
+ console.log(`[FINDING] DETACHED_NODES: ${detached} detached DOM nodes — likely memory leak`);
192
+ }
193
+ }
194
+ ```
195
+
196
+ ## Security Audit
197
+
198
+ ```js
199
+ export async function run(cdp) {
200
+ const TARGET_URL = 'https://example.com'; // ← set target URL
201
+
202
+ await cdp.send('Network.enable', {});
203
+ await cdp.send('Runtime.enable', {});
204
+ await cdp.send('DOM.enable', {});
205
+ await cdp.send('Page.enable', {});
206
+
207
+ const requests = new Map();
208
+
209
+ cdp.on('Network.requestWillBeSent', ({ requestId, request }) => {
210
+ requests.set(requestId, { url: request.url, method: request.method });
211
+ });
212
+
213
+ cdp.on('Network.responseReceived', async ({ requestId, response }) => {
214
+ const r = requests.get(requestId);
215
+ if (!r) return;
216
+ const headers = response.headers ?? {};
217
+ if (!headers['content-security-policy'])
218
+ console.log(`[FINDING] MISSING_CSP: ${r.url}`);
219
+ if (!headers['strict-transport-security'])
220
+ console.log(`[FINDING] MISSING_HSTS: ${r.url}`);
221
+ if (!headers['x-frame-options'] && !headers['content-security-policy']?.includes('frame-ancestors'))
222
+ console.log(`[FINDING] MISSING_XFRAME: ${r.url}`);
223
+ const csp = headers['content-security-policy'] ?? '';
224
+ if (csp.includes('unsafe-eval')) console.log(`[FINDING] WEAK_CSP: unsafe-eval in ${r.url}`);
225
+ if (csp.includes('unsafe-inline')) console.log(`[FINDING] WEAK_CSP: unsafe-inline in ${r.url}`);
226
+ if (r.method === 'POST') {
227
+ try {
228
+ const { body } = await cdp.send('Network.getRequestPostData', { requestId });
229
+ if (/token|password|secret|apikey|jwt|auth/.test(body.toLowerCase()))
230
+ console.log(`[FINDING] SENSITIVE_IN_POST: ${r.url} — body contains sensitive key`);
231
+ } catch {}
232
+ }
233
+ });
234
+
235
+ // Use getCookies scoped to TARGET_URL — getAllCookies returns the entire browser jar
236
+ // and floods output with third-party ad/tracker cookies irrelevant to the audited site
237
+ const { cookies } = await cdp.send('Network.getCookies', { urls: [TARGET_URL] });
238
+ for (const c of cookies) {
239
+ if (!c.httpOnly) console.log(`[FINDING] COOKIE_NO_HTTPONLY: ${c.name}`);
240
+ if (!c.secure) console.log(`[FINDING] COOKIE_NO_SECURE: ${c.name}`);
241
+ if (c.sameSite === 'None' && !c.secure)
242
+ console.log(`[FINDING] COOKIE_SAMESITE_NONE_INSECURE: ${c.name}`);
243
+ }
244
+ console.log(`[SECURITY] Cookies audited: ${cookies.length}`);
245
+
246
+ const { result: ls } = await cdp.send('Runtime.evaluate', {
247
+ expression: `JSON.stringify(Object.entries(localStorage).map(([k,v]) => ({ k, size: new Blob([k+v]).size })))`, returnByValue: true,
248
+ });
249
+ const lsEntries = JSON.parse(ls.value ?? '[]');
250
+ for (const { k } of lsEntries)
251
+ if (/token|auth|jwt|secret|key|password/i.test(k))
252
+ console.log(`[FINDING] SENSITIVE_IN_STORAGE: localStorage key "${k}"`);
253
+ console.log(`[SECURITY] localStorage keys: ${lsEntries.length}`);
254
+
255
+ const { result: proto } = await cdp.send('Runtime.evaluate', {
256
+ expression: `JSON.stringify(Object.keys(Object.getOwnPropertyDescriptors(Object.prototype)).filter(k => !['constructor','__defineGetter__','__defineSetter__','hasOwnProperty','__lookupGetter__','__lookupSetter__','isPrototypeOf','propertyIsEnumerable','toString','valueOf','__proto__','toLocaleString'].includes(k)))`,
257
+ returnByValue: true,
258
+ });
259
+ const polluted = JSON.parse(proto.value ?? '[]');
260
+ if (polluted.length > 0)
261
+ console.log(`[FINDING] PROTOTYPE_POLLUTION: unexpected keys on Object.prototype: ${polluted.join(', ')}`);
262
+
263
+ const { result: docObj } = await cdp.send('Runtime.evaluate', { expression: 'document' });
264
+ const { listeners } = await cdp.send('DOMDebugger.getEventListeners', { objectId: docObj.objectId });
265
+ for (const l of listeners) {
266
+ if (['keydown', 'keyup'].includes(l.type) && l.scriptId)
267
+ console.log(`[FINDING] POSSIBLE_KEYLOGGER: document ${l.type} listener at ${l.scriptId}:${l.lineNumber}`);
268
+ if (['copy', 'paste'].includes(l.type))
269
+ console.log(`[FINDING] CLIPBOARD_LISTENER: document ${l.type} listener — possible hijack`);
270
+ }
271
+
272
+ await new Promise(r => setTimeout(r, 5000));
273
+ console.log(`[METRIC] Security audit complete — requests: ${requests.size}, cookies: ${cookies.length}`);
274
+ }
275
+ ```