chromex-mcp 1.0.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/package.json +50 -0
  4. package/plugins/chromex/skills/chromex/scripts/chromex.mjs +343 -0
  5. package/plugins/chromex/skills/chromex/scripts/lib/browser.mjs +66 -0
  6. package/plugins/chromex/skills/chromex/scripts/lib/client.mjs +98 -0
  7. package/plugins/chromex/skills/chromex/scripts/lib/commands/console.mjs +37 -0
  8. package/plugins/chromex/skills/chromex/scripts/lib/commands/cookies.mjs +77 -0
  9. package/plugins/chromex/skills/chromex/scripts/lib/commands/coverage.mjs +95 -0
  10. package/plugins/chromex/skills/chromex/scripts/lib/commands/cpu.mjs +14 -0
  11. package/plugins/chromex/skills/chromex/scripts/lib/commands/dialog.mjs +38 -0
  12. package/plugins/chromex/skills/chromex/scripts/lib/commands/domsnapshot.mjs +84 -0
  13. package/plugins/chromex/skills/chromex/scripts/lib/commands/download.mjs +25 -0
  14. package/plugins/chromex/skills/chromex/scripts/lib/commands/drag.mjs +71 -0
  15. package/plugins/chromex/skills/chromex/scripts/lib/commands/emulate.mjs +44 -0
  16. package/plugins/chromex/skills/chromex/scripts/lib/commands/evaluate.mjs +31 -0
  17. package/plugins/chromex/skills/chromex/scripts/lib/commands/form.mjs +163 -0
  18. package/plugins/chromex/skills/chromex/scripts/lib/commands/geo.mjs +37 -0
  19. package/plugins/chromex/skills/chromex/scripts/lib/commands/har.mjs +101 -0
  20. package/plugins/chromex/skills/chromex/scripts/lib/commands/heap.mjs +24 -0
  21. package/plugins/chromex/skills/chromex/scripts/lib/commands/highlight.mjs +36 -0
  22. package/plugins/chromex/skills/chromex/scripts/lib/commands/html.mjs +10 -0
  23. package/plugins/chromex/skills/chromex/scripts/lib/commands/inject.mjs +39 -0
  24. package/plugins/chromex/skills/chromex/scripts/lib/commands/interact.mjs +88 -0
  25. package/plugins/chromex/skills/chromex/scripts/lib/commands/intercept.mjs +99 -0
  26. package/plugins/chromex/skills/chromex/scripts/lib/commands/navigate.mjs +45 -0
  27. package/plugins/chromex/skills/chromex/scripts/lib/commands/network.mjs +13 -0
  28. package/plugins/chromex/skills/chromex/scripts/lib/commands/pdf.mjs +16 -0
  29. package/plugins/chromex/skills/chromex/scripts/lib/commands/perf.mjs +98 -0
  30. package/plugins/chromex/skills/chromex/scripts/lib/commands/refs.mjs +67 -0
  31. package/plugins/chromex/skills/chromex/scripts/lib/commands/screenshot.mjs +54 -0
  32. package/plugins/chromex/skills/chromex/scripts/lib/commands/scroll.mjs +44 -0
  33. package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +100 -0
  34. package/plugins/chromex/skills/chromex/scripts/lib/commands/storage.mjs +47 -0
  35. package/plugins/chromex/skills/chromex/scripts/lib/commands/tab.mjs +31 -0
  36. package/plugins/chromex/skills/chromex/scripts/lib/commands/throttle.mjs +38 -0
  37. package/plugins/chromex/skills/chromex/scripts/lib/commands/touch.mjs +62 -0
  38. package/plugins/chromex/skills/chromex/scripts/lib/commands/trace.mjs +51 -0
  39. package/plugins/chromex/skills/chromex/scripts/lib/commands/upload.mjs +43 -0
  40. package/plugins/chromex/skills/chromex/scripts/lib/commands/wait.mjs +84 -0
  41. package/plugins/chromex/skills/chromex/scripts/lib/commands/webauthn.mjs +47 -0
  42. package/plugins/chromex/skills/chromex/scripts/lib/config.mjs +100 -0
  43. package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +368 -0
  44. package/plugins/chromex/skills/chromex/scripts/lib/ipc.mjs +178 -0
  45. package/plugins/chromex/skills/chromex/scripts/lib/launcher.mjs +111 -0
  46. package/plugins/chromex/skills/chromex/scripts/lib/security.mjs +48 -0
  47. package/plugins/chromex/skills/chromex/scripts/lib/utils.mjs +47 -0
  48. package/plugins/chromex/skills/chromex/scripts/mcp-server.mjs +726 -0
@@ -0,0 +1,101 @@
1
+ // HAR (HTTP Archive) recording via Network domain
2
+
3
+ import { writeFileSync } from 'fs';
4
+
5
+ let recording = false;
6
+ const entries = [];
7
+ const requestMap = new Map();
8
+ let cleanupFns = [];
9
+
10
+ export async function harStr(cdp, sid, action, filePath) {
11
+ if (!action) throw new Error('Usage: har <target> start | stop [file]');
12
+
13
+ switch (action) {
14
+ case 'start': {
15
+ if (recording) return 'HAR recording already active.';
16
+ entries.length = 0;
17
+ requestMap.clear();
18
+
19
+ await cdp.send('Network.enable', {}, sid);
20
+ recording = true;
21
+
22
+ const off1 = cdp.onEvent('Network.requestWillBeSent', (params) => {
23
+ requestMap.set(params.requestId, {
24
+ requestId: params.requestId,
25
+ url: params.request.url,
26
+ method: params.request.method,
27
+ headers: params.request.headers,
28
+ postData: params.request.postData,
29
+ startTime: params.timestamp,
30
+ wallTime: params.wallTime,
31
+ });
32
+ });
33
+
34
+ const off2 = cdp.onEvent('Network.responseReceived', (params) => {
35
+ const req = requestMap.get(params.requestId);
36
+ if (req) {
37
+ req.status = params.response.status;
38
+ req.statusText = params.response.statusText;
39
+ req.responseHeaders = params.response.headers;
40
+ req.mimeType = params.response.mimeType;
41
+ req.protocol = params.response.protocol;
42
+ }
43
+ });
44
+
45
+ const off3 = cdp.onEvent('Network.loadingFinished', (params) => {
46
+ const req = requestMap.get(params.requestId);
47
+ if (req) {
48
+ req.endTime = params.timestamp;
49
+ req.encodedDataLength = params.encodedDataLength;
50
+ entries.push(req);
51
+ requestMap.delete(params.requestId);
52
+ }
53
+ });
54
+
55
+ cleanupFns = [off1, off2, off3];
56
+ return `HAR recording started. Use "har <target> stop [file]" to save.`;
57
+ }
58
+
59
+ case 'stop': {
60
+ if (!recording) return 'No HAR recording active.';
61
+ recording = false;
62
+ cleanupFns.forEach(fn => fn());
63
+ cleanupFns = [];
64
+
65
+ const har = {
66
+ log: {
67
+ version: '1.2',
68
+ creator: { name: 'chromex', version: '1.0.0' },
69
+ entries: entries.map(e => ({
70
+ startedDateTime: e.wallTime ? new Date(e.wallTime * 1000).toISOString() : new Date().toISOString(),
71
+ time: e.endTime && e.startTime ? Math.round((e.endTime - e.startTime) * 1000) : 0,
72
+ request: {
73
+ method: e.method,
74
+ url: e.url,
75
+ headers: Object.entries(e.headers || {}).map(([n, v]) => ({ name: n, value: v })),
76
+ postData: e.postData ? { mimeType: 'application/x-www-form-urlencoded', text: e.postData } : undefined,
77
+ },
78
+ response: {
79
+ status: e.status || 0,
80
+ statusText: e.statusText || '',
81
+ headers: Object.entries(e.responseHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
82
+ content: { size: e.encodedDataLength || 0, mimeType: e.mimeType || '' },
83
+ },
84
+ cache: {},
85
+ timings: {
86
+ send: 0, wait: 0,
87
+ receive: e.endTime && e.startTime ? Math.round((e.endTime - e.startTime) * 1000) : 0,
88
+ },
89
+ })),
90
+ },
91
+ };
92
+
93
+ const out = filePath || '/tmp/chromex.har';
94
+ writeFileSync(out, JSON.stringify(har, null, 2));
95
+ return `HAR saved to ${out} (${entries.length} entries).`;
96
+ }
97
+
98
+ default:
99
+ throw new Error('Usage: har <target> start | stop [file]');
100
+ }
101
+ }
@@ -0,0 +1,24 @@
1
+ // Heap snapshot via HeapProfiler domain
2
+
3
+ import { writeFileSync } from 'fs';
4
+
5
+ export async function heapStr(cdp, sid, action, filePath) {
6
+ if (!action) throw new Error('Usage: heap <target> snapshot [file]');
7
+
8
+ if (action === 'snapshot') {
9
+ const chunks = [];
10
+ const off = cdp.onEvent('HeapProfiler.addHeapSnapshotChunk', (params) => {
11
+ chunks.push(params.chunk);
12
+ });
13
+
14
+ await cdp.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false }, sid);
15
+ off();
16
+
17
+ const out = filePath || '/tmp/chromex-heap.heapsnapshot';
18
+ writeFileSync(out, chunks.join(''));
19
+ const sizeMB = (Buffer.byteLength(chunks.join('')) / (1024 * 1024)).toFixed(1);
20
+ return `Heap snapshot saved to ${out} (${sizeMB}MB). Open in Chrome DevTools > Memory tab.`;
21
+ }
22
+
23
+ throw new Error('Usage: heap <target> snapshot [file]');
24
+ }
@@ -0,0 +1,36 @@
1
+ // Element highlight overlay via Overlay domain
2
+
3
+ export async function highlightStr(cdp, sid, selectorOrAction) {
4
+ if (!selectorOrAction) throw new Error('Usage: highlight <target> <selector> | clear');
5
+
6
+ if (selectorOrAction === 'clear') {
7
+ await cdp.send('Overlay.hideHighlight', {}, sid);
8
+ return 'Highlight cleared.';
9
+ }
10
+
11
+ // Encontrar o nodeId do elemento
12
+ await cdp.send('DOM.enable', {}, sid);
13
+ await cdp.send('Overlay.enable', {}, sid);
14
+
15
+ const { root } = await cdp.send('DOM.getDocument', {}, sid);
16
+ const { nodeId } = await cdp.send('DOM.querySelector', {
17
+ nodeId: root.nodeId,
18
+ selector: selectorOrAction,
19
+ }, sid);
20
+
21
+ if (!nodeId) throw new Error(`Element not found: ${selectorOrAction}`);
22
+
23
+ await cdp.send('Overlay.highlightNode', {
24
+ highlightConfig: {
25
+ contentColor: { r: 111, g: 168, b: 220, a: 0.66 },
26
+ paddingColor: { r: 147, g: 196, b: 125, a: 0.55 },
27
+ borderColor: { r: 255, g: 229, b: 153, a: 0.66 },
28
+ marginColor: { r: 246, g: 178, b: 107, a: 0.66 },
29
+ showInfo: true,
30
+ showStyles: true,
31
+ },
32
+ nodeId,
33
+ }, sid);
34
+
35
+ return `Highlighting "${selectorOrAction}". Use "highlight <target> clear" to remove.`;
36
+ }
@@ -0,0 +1,10 @@
1
+ // HTML extraction
2
+
3
+ import { evalStr } from './evaluate.mjs';
4
+
5
+ export async function htmlStr(cdp, sid, selector) {
6
+ const expr = selector
7
+ ? `document.querySelector(${JSON.stringify(selector)})?.outerHTML || 'Element not found'`
8
+ : `document.documentElement.outerHTML`;
9
+ return evalStr(cdp, sid, expr);
10
+ }
@@ -0,0 +1,39 @@
1
+ // Script injection via Page domain (runs before page scripts on every navigation)
2
+
3
+ import { readFileSync, existsSync } from 'fs';
4
+
5
+ // Mantido em memória no daemon via closure no handleCommand
6
+ const injectedScripts = new Map();
7
+
8
+ export async function injectStr(cdp, sid, action, arg) {
9
+ if (!action) throw new Error('Usage: inject <target> <script> | --file <path> | --remove <id> | --list');
10
+
11
+ if (action === '--list') {
12
+ if (injectedScripts.size === 0) return 'No injected scripts.';
13
+ return Array.from(injectedScripts.entries())
14
+ .map(([id, snippet]) => `${id} ${snippet}`)
15
+ .join('\n');
16
+ }
17
+
18
+ if (action === '--remove') {
19
+ if (!arg) throw new Error('Script identifier required');
20
+ await cdp.send('Page.removeScriptToEvaluateOnNewDocument', { identifier: arg }, sid);
21
+ injectedScripts.delete(arg);
22
+ return `Removed injected script ${arg}.`;
23
+ }
24
+
25
+ let source;
26
+ if (action === '--file') {
27
+ if (!arg) throw new Error('File path required');
28
+ if (!existsSync(arg)) throw new Error(`File not found: ${arg}`);
29
+ source = readFileSync(arg, 'utf8');
30
+ } else {
31
+ // action é o próprio script (pode ter arg como continuação)
32
+ source = arg ? `${action} ${arg}` : action;
33
+ }
34
+
35
+ await cdp.send('Page.enable', {}, sid);
36
+ const { identifier } = await cdp.send('Page.addScriptToEvaluateOnNewDocument', { source }, sid);
37
+ injectedScripts.set(identifier, source.substring(0, 60) + (source.length > 60 ? '...' : ''));
38
+ return `Script injected (id: ${identifier}). Runs on every new document load.`;
39
+ }
@@ -0,0 +1,88 @@
1
+ // Interação: click, clickxy, type, loadall, waitfor
2
+
3
+ import { sleep } from '../utils.mjs';
4
+ import { evalStr } from './evaluate.mjs';
5
+
6
+ export async function clickStr(cdp, sid, selector) {
7
+ if (!selector) throw new Error('CSS selector required');
8
+ const expr = `
9
+ (function() {
10
+ const el = document.querySelector(${JSON.stringify(selector)});
11
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(selector)} };
12
+ el.scrollIntoView({ block: 'center' });
13
+ el.click();
14
+ return { ok: true, tag: el.tagName, text: el.textContent.trim().substring(0, 80) };
15
+ })()
16
+ `;
17
+ const result = await evalStr(cdp, sid, expr);
18
+ const r = JSON.parse(result);
19
+ if (!r.ok) throw new Error(r.error);
20
+ return `Clicked <${r.tag}> "${r.text}"`;
21
+ }
22
+
23
+ export async function clickXyStr(cdp, sid, x, y) {
24
+ const cx = parseFloat(x);
25
+ const cy = parseFloat(y);
26
+ if (isNaN(cx) || isNaN(cy)) throw new Error('x and y must be numbers (CSS pixels)');
27
+ const base = { x: cx, y: cy, button: 'left', clickCount: 1, modifiers: 0 };
28
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseMoved' }, sid);
29
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mousePressed' }, sid);
30
+ await sleep(50);
31
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseReleased' }, sid);
32
+ return `Clicked at CSS (${cx}, ${cy})`;
33
+ }
34
+
35
+ export async function typeStr(cdp, sid, text) {
36
+ if (text == null || text === '') throw new Error('text required');
37
+ await cdp.send('Input.insertText', { text }, sid);
38
+ return `Typed ${text.length} characters`;
39
+ }
40
+
41
+ export async function loadAllStr(cdp, sid, selector, intervalMs = 1500) {
42
+ if (!selector) throw new Error('CSS selector required');
43
+ let clicks = 0;
44
+ const deadline = Date.now() + 5 * 60 * 1000;
45
+ while (Date.now() < deadline) {
46
+ const exists = await evalStr(cdp, sid,
47
+ `!!document.querySelector(${JSON.stringify(selector)})`
48
+ );
49
+ if (exists !== 'true') break;
50
+ const clickExpr = `
51
+ (function() {
52
+ const el = document.querySelector(${JSON.stringify(selector)});
53
+ if (!el) return false;
54
+ el.scrollIntoView({ block: 'center' });
55
+ el.click();
56
+ return true;
57
+ })()
58
+ `;
59
+ const clicked = await evalStr(cdp, sid, clickExpr);
60
+ if (clicked !== 'true') break;
61
+ clicks++;
62
+ await sleep(intervalMs);
63
+ }
64
+ return `Clicked "${selector}" ${clicks} time(s) until it disappeared`;
65
+ }
66
+
67
+ export async function waitForStr(cdp, sid, selector, timeoutMs, config) {
68
+ if (!selector) throw new Error('CSS selector required');
69
+ const timeout = timeoutMs || config?.commandTimeout || 15000;
70
+ const deadline = Date.now() + timeout;
71
+ while (Date.now() < deadline) {
72
+ const exists = await evalStr(cdp, sid,
73
+ `!!document.querySelector(${JSON.stringify(selector)})`
74
+ );
75
+ if (exists === 'true') {
76
+ const info = await evalStr(cdp, sid, `
77
+ (function() {
78
+ const el = document.querySelector(${JSON.stringify(selector)});
79
+ return { tag: el.tagName, text: el.textContent.trim().substring(0, 80) };
80
+ })()
81
+ `);
82
+ const r = JSON.parse(info);
83
+ return `Found <${r.tag}> "${r.text}" (waited ${Date.now() - deadline + timeout}ms)`;
84
+ }
85
+ await sleep(200);
86
+ }
87
+ throw new Error(`Timeout (${timeout}ms) waiting for selector: ${selector}`);
88
+ }
@@ -0,0 +1,99 @@
1
+ // Network interception -- mock, block, or modify requests via Fetch domain
2
+
3
+ const rules = [];
4
+ let fetchEnabled = false;
5
+ let handlerRegistered = false;
6
+
7
+ export async function interceptStr(cdp, sid, action, pattern, body) {
8
+ if (!action) throw new Error('Usage: intercept <target> on [pattern] | block <pattern> | mock <url> <json> | off | rules');
9
+
10
+ switch (action) {
11
+ case 'on': {
12
+ const patterns = pattern
13
+ ? [{ urlPattern: pattern, requestStage: 'Request' }]
14
+ : [{ urlPattern: '*', requestStage: 'Request' }];
15
+ await cdp.send('Fetch.enable', { patterns }, sid);
16
+ fetchEnabled = true;
17
+ if (!handlerRegistered) {
18
+ registerHandler(cdp, sid);
19
+ handlerRegistered = true;
20
+ }
21
+ return `Interception enabled${pattern ? ` for ${pattern}` : ' for all requests'}.`;
22
+ }
23
+
24
+ case 'block': {
25
+ if (!pattern) throw new Error('URL pattern required');
26
+ rules.push({ type: 'block', pattern });
27
+ if (!fetchEnabled) {
28
+ await cdp.send('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] }, sid);
29
+ fetchEnabled = true;
30
+ if (!handlerRegistered) { registerHandler(cdp, sid); handlerRegistered = true; }
31
+ }
32
+ return `Blocking requests matching: ${pattern} (${rules.length} rule(s) total)`;
33
+ }
34
+
35
+ case 'mock': {
36
+ if (!pattern) throw new Error('URL pattern required');
37
+ if (!body) throw new Error('Response body (JSON) required');
38
+ rules.push({ type: 'mock', pattern, body });
39
+ if (!fetchEnabled) {
40
+ await cdp.send('Fetch.enable', { patterns: [{ urlPattern: '*', requestStage: 'Request' }] }, sid);
41
+ fetchEnabled = true;
42
+ if (!handlerRegistered) { registerHandler(cdp, sid); handlerRegistered = true; }
43
+ }
44
+ return `Mocking ${pattern} with custom response (${rules.length} rule(s) total)`;
45
+ }
46
+
47
+ case 'off': {
48
+ await cdp.send('Fetch.disable', {}, sid);
49
+ fetchEnabled = false;
50
+ rules.length = 0;
51
+ return 'Interception disabled. All rules cleared.';
52
+ }
53
+
54
+ case 'rules': {
55
+ if (rules.length === 0) return 'No interception rules.';
56
+ return rules.map((r, i) => `${i + 1}. ${r.type.toUpperCase()} ${r.pattern}${r.body ? ' -> ' + r.body.substring(0, 50) : ''}`).join('\n');
57
+ }
58
+
59
+ default:
60
+ throw new Error('Usage: intercept <target> on [pattern] | block <pattern> | mock <url> <json> | off | rules');
61
+ }
62
+ }
63
+
64
+ function registerHandler(cdp, sid) {
65
+ cdp.onEvent('Fetch.requestPaused', async (params) => {
66
+ const { requestId, request } = params;
67
+ const url = request.url;
68
+
69
+ for (const rule of rules) {
70
+ if (urlMatches(url, rule.pattern)) {
71
+ if (rule.type === 'block') {
72
+ try { await cdp.send('Fetch.failRequest', { requestId, errorReason: 'BlockedByClient' }, sid); } catch {}
73
+ return;
74
+ }
75
+ if (rule.type === 'mock') {
76
+ try {
77
+ await cdp.send('Fetch.fulfillRequest', {
78
+ requestId,
79
+ responseCode: 200,
80
+ responseHeaders: [{ name: 'Content-Type', value: 'application/json' }],
81
+ body: Buffer.from(rule.body).toString('base64'),
82
+ }, sid);
83
+ } catch {}
84
+ return;
85
+ }
86
+ }
87
+ }
88
+
89
+ // Sem regra: continuar normalmente
90
+ try { await cdp.send('Fetch.continueRequest', { requestId }, sid); } catch {}
91
+ });
92
+ }
93
+
94
+ function urlMatches(url, pattern) {
95
+ if (pattern === '*') return true;
96
+ // Converter glob simples para regex: * -> .*, ? -> .
97
+ const regex = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
98
+ return regex.test(url);
99
+ }
@@ -0,0 +1,45 @@
1
+ // Navegação + wait for document ready
2
+
3
+ import { sleep } from '../utils.mjs';
4
+ import { checkDomain } from '../security.mjs';
5
+ import { evalStr } from './evaluate.mjs';
6
+
7
+ export async function waitForDocumentReady(cdp, sid, timeoutMs = 30000) {
8
+ const deadline = Date.now() + timeoutMs;
9
+ let lastState = '';
10
+ let lastError;
11
+ while (Date.now() < deadline) {
12
+ try {
13
+ const state = await evalStr(cdp, sid, 'document.readyState');
14
+ lastState = state;
15
+ if (state === 'complete') return;
16
+ } catch (e) {
17
+ lastError = e;
18
+ }
19
+ await sleep(200);
20
+ }
21
+
22
+ if (lastState) throw new Error(`Timed out waiting for navigation (last readyState: ${lastState})`);
23
+ if (lastError) throw new Error(`Timed out waiting for navigation (${lastError.message})`);
24
+ throw new Error('Timed out waiting for navigation');
25
+ }
26
+
27
+ export async function navStr(cdp, sid, url, config) {
28
+ const domainError = checkDomain(url, config);
29
+ if (domainError) throw new Error(domainError);
30
+
31
+ await cdp.send('Page.enable', {}, sid);
32
+ const loadEvent = cdp.waitForEvent('Page.loadEventFired', config.navigationTimeout);
33
+ const result = await cdp.send('Page.navigate', { url }, sid);
34
+ if (result.errorText) {
35
+ loadEvent.cancel();
36
+ throw new Error(result.errorText);
37
+ }
38
+ if (result.loaderId) {
39
+ await loadEvent.promise;
40
+ } else {
41
+ loadEvent.cancel();
42
+ }
43
+ await waitForDocumentReady(cdp, sid, 5000);
44
+ return `Navigated to ${url}`;
45
+ }
@@ -0,0 +1,13 @@
1
+ // Resource timing entries
2
+
3
+ import { evalStr } from './evaluate.mjs';
4
+
5
+ export async function netStr(cdp, sid) {
6
+ const raw = await evalStr(cdp, sid, `JSON.stringify(performance.getEntriesByType('resource').map(e => ({
7
+ name: e.name.substring(0, 120), type: e.initiatorType,
8
+ duration: Math.round(e.duration), size: e.transferSize
9
+ })))`);
10
+ return JSON.parse(raw).map(e =>
11
+ `${String(e.duration).padStart(5)}ms ${String(e.size || '?').padStart(8)}B ${e.type.padEnd(8)} ${e.name}`
12
+ ).join('\n');
13
+ }
@@ -0,0 +1,16 @@
1
+ // Gerar PDF da página via CDP
2
+
3
+ import { writeFileSync } from 'fs';
4
+
5
+ export async function pdfStr(cdp, sid, filePath) {
6
+ const out = filePath || '/tmp/page.pdf';
7
+
8
+ const { data } = await cdp.send('Page.printToPDF', {
9
+ landscape: false,
10
+ printBackground: true,
11
+ preferCSSPageSize: true,
12
+ }, sid);
13
+
14
+ writeFileSync(out, Buffer.from(data, 'base64'));
15
+ return `PDF saved to ${out}`;
16
+ }
@@ -0,0 +1,98 @@
1
+ // Core Web Vitals + performance metrics
2
+
3
+ import { evalStr } from './evaluate.mjs';
4
+
5
+ export async function perfStr(cdp, sid) {
6
+ // Métricas do CDP
7
+ await cdp.send('Performance.enable', {}, sid);
8
+ const { metrics } = await cdp.send('Performance.getMetrics', {}, sid);
9
+ await cdp.send('Performance.disable', {}, sid);
10
+
11
+ // Core Web Vitals via PerformanceObserver
12
+ const vitals = await evalStr(cdp, sid, `
13
+ (function() {
14
+ const result = {};
15
+ const nav = performance.getEntriesByType('navigation')[0];
16
+ if (nav) {
17
+ result.ttfb = Math.round(nav.responseStart - nav.requestStart);
18
+ result.domContentLoaded = Math.round(nav.domContentLoadedEventEnd - nav.fetchStart);
19
+ result.load = Math.round(nav.loadEventEnd - nav.fetchStart);
20
+ result.domInteractive = Math.round(nav.domInteractive - nav.fetchStart);
21
+ }
22
+
23
+ // LCP
24
+ const lcpEntries = performance.getEntriesByType('largest-contentful-paint');
25
+ if (lcpEntries.length > 0) {
26
+ const lcp = lcpEntries[lcpEntries.length - 1];
27
+ result.lcp = Math.round(lcp.startTime);
28
+ result.lcpElement = lcp.element?.tagName || 'unknown';
29
+ }
30
+
31
+ // CLS
32
+ let cls = 0;
33
+ for (const entry of performance.getEntriesByType('layout-shift')) {
34
+ if (!entry.hadRecentInput) cls += entry.value;
35
+ }
36
+ result.cls = Math.round(cls * 1000) / 1000;
37
+
38
+ // FCP
39
+ const fcpEntries = performance.getEntriesByType('paint');
40
+ const fcp = fcpEntries.find(e => e.name === 'first-contentful-paint');
41
+ if (fcp) result.fcp = Math.round(fcp.startTime);
42
+
43
+ // Contadores
44
+ result.resources = performance.getEntriesByType('resource').length;
45
+ result.transferSize = performance.getEntriesByType('resource')
46
+ .reduce((sum, r) => sum + (r.transferSize || 0), 0);
47
+
48
+ return JSON.stringify(result);
49
+ })()
50
+ `);
51
+
52
+ const v = JSON.parse(vitals);
53
+ const cdpMetrics = {};
54
+ for (const m of metrics) cdpMetrics[m.name] = m.value;
55
+
56
+ const lines = ['## Core Web Vitals'];
57
+
58
+ if (v.lcp != null) lines.push(`LCP: ${v.lcp}ms (${v.lcpElement})${v.lcp <= 2500 ? ' [GOOD]' : v.lcp <= 4000 ? ' [NEEDS IMPROVEMENT]' : ' [POOR]'}`);
59
+ if (v.fcp != null) lines.push(`FCP: ${v.fcp}ms${v.fcp <= 1800 ? ' [GOOD]' : v.fcp <= 3000 ? ' [NEEDS IMPROVEMENT]' : ' [POOR]'}`);
60
+ if (v.cls != null) lines.push(`CLS: ${v.cls}${v.cls <= 0.1 ? ' [GOOD]' : v.cls <= 0.25 ? ' [NEEDS IMPROVEMENT]' : ' [POOR]'}`);
61
+ if (v.ttfb != null) lines.push(`TTFB: ${v.ttfb}ms${v.ttfb <= 800 ? ' [GOOD]' : v.ttfb <= 1800 ? ' [NEEDS IMPROVEMENT]' : ' [POOR]'}`);
62
+
63
+ lines.push('');
64
+ lines.push('## Navigation Timing');
65
+ if (v.domInteractive != null) lines.push(`DOM Interactive: ${v.domInteractive}ms`);
66
+ if (v.domContentLoaded != null) lines.push(`DOMContentLoaded: ${v.domContentLoaded}ms`);
67
+ if (v.load != null) lines.push(`Load: ${v.load}ms`);
68
+
69
+ lines.push('');
70
+ lines.push('## Resources');
71
+ lines.push(`Total requests: ${v.resources}`);
72
+ lines.push(`Transfer size: ${formatBytes(v.transferSize)}`);
73
+
74
+ if (cdpMetrics.JSHeapUsedSize) {
75
+ lines.push('');
76
+ lines.push('## Memory');
77
+ lines.push(`JS Heap Used: ${formatBytes(cdpMetrics.JSHeapUsedSize)}`);
78
+ lines.push(`JS Heap Total: ${formatBytes(cdpMetrics.JSHeapTotalSize)}`);
79
+ }
80
+
81
+ if (cdpMetrics.Nodes) {
82
+ lines.push('');
83
+ lines.push('## DOM');
84
+ lines.push(`DOM Nodes: ${cdpMetrics.Nodes}`);
85
+ lines.push(`Documents: ${cdpMetrics.Documents}`);
86
+ lines.push(`Frames: ${cdpMetrics.Frames}`);
87
+ lines.push(`Listeners: ${cdpMetrics.JSEventListeners}`);
88
+ }
89
+
90
+ return lines.join('\n');
91
+ }
92
+
93
+ function formatBytes(bytes) {
94
+ if (bytes == null) return '?';
95
+ if (bytes < 1024) return `${bytes}B`;
96
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
97
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
98
+ }
@@ -0,0 +1,67 @@
1
+ // Ref-based element resolution: @e1, @e2... -> backendNodeId -> coordinates/actions
2
+
3
+ import { sleep } from '../utils.mjs';
4
+
5
+ // Resolve a ref (@eN) to center coordinates using DOM.getBoxModel
6
+ export async function resolveRefToCoords(cdp, sid, refMap, refNum) {
7
+ const ref = refMap.get(refNum);
8
+ if (!ref) throw new Error(`Ref @e${refNum} not found. Run "snap --refs" first to assign refs.`);
9
+ if (!ref.backendNodeId) throw new Error(`Ref @e${refNum} has no DOM node (role: ${ref.role}).`);
10
+
11
+ await cdp.send('DOM.enable', {}, sid);
12
+ const { model } = await cdp.send('DOM.getBoxModel', { backendNodeId: ref.backendNodeId }, sid);
13
+ // content quad: [x1,y1, x2,y2, x3,y3, x4,y4] -- use center
14
+ const q = model.content;
15
+ const x = (q[0] + q[2] + q[4] + q[6]) / 4;
16
+ const y = (q[1] + q[3] + q[5] + q[7]) / 4;
17
+ return { x, y, ref };
18
+ }
19
+
20
+ // Click an element by ref
21
+ export async function clickRefStr(cdp, sid, refMap, refNum) {
22
+ const { x, y, ref } = await resolveRefToCoords(cdp, sid, refMap, refNum);
23
+ const base = { x, y, button: 'left', clickCount: 1, modifiers: 0 };
24
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mousePressed' }, sid);
25
+ await sleep(50);
26
+ await cdp.send('Input.dispatchMouseEvent', { ...base, type: 'mouseReleased' }, sid);
27
+ return `Clicked @e${refNum} [${ref.role}] "${ref.name}" at (${Math.round(x)}, ${Math.round(y)})`;
28
+ }
29
+
30
+ // Hover over an element by ref
31
+ export async function hoverRefStr(cdp, sid, refMap, refNum) {
32
+ const { x, y, ref } = await resolveRefToCoords(cdp, sid, refMap, refNum);
33
+ await cdp.send('Input.dispatchMouseEvent', {
34
+ type: 'mouseMoved', x, y, button: 'none', modifiers: 0,
35
+ }, sid);
36
+ return `Hovering @e${refNum} [${ref.role}] "${ref.name}" at (${Math.round(x)}, ${Math.round(y)})`;
37
+ }
38
+
39
+ // Focus an element by ref (for fill/type)
40
+ export async function focusRefStr(cdp, sid, refMap, refNum) {
41
+ const ref = refMap.get(refNum);
42
+ if (!ref) throw new Error(`Ref @e${refNum} not found. Run "snap --refs" first.`);
43
+ if (!ref.backendNodeId) throw new Error(`Ref @e${refNum} has no DOM node.`);
44
+
45
+ await cdp.send('DOM.enable', {}, sid);
46
+ await cdp.send('DOM.focus', { backendNodeId: ref.backendNodeId }, sid);
47
+ return ref;
48
+ }
49
+
50
+ // Fill an element by ref
51
+ export async function fillRefStr(cdp, sid, refMap, refNum, value) {
52
+ const ref = await focusRefStr(cdp, sid, refMap, refNum);
53
+ // Select all + insert text
54
+ const modKey = process.platform === 'darwin' ? 4 : 2;
55
+ await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'a', code: 'KeyA', modifiers: modKey }, sid);
56
+ await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'a', code: 'KeyA', modifiers: 0 }, sid);
57
+ await sleep(50);
58
+ await cdp.send('Input.insertText', { text: String(value) }, sid);
59
+ return `Filled @e${refNum} [${ref.role}] "${ref.name}" with "${String(value).substring(0, 50)}"`;
60
+ }
61
+
62
+ // Parse @eN from string, returns null if not a ref
63
+ export function parseRef(str) {
64
+ if (!str) return null;
65
+ const m = str.match(/^@e(\d+)$/i);
66
+ return m ? parseInt(m[1]) : null;
67
+ }