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,54 @@
1
+ // Screenshot (viewport + full page)
2
+
3
+ import { writeFileSync } from 'fs';
4
+ import { evalStr } from './evaluate.mjs';
5
+
6
+ export async function shotStr(cdp, sid, filePath, fullPage = false, config) {
7
+ let dpr = 1;
8
+ try {
9
+ const metrics = await cdp.send('Page.getLayoutMetrics', {}, sid);
10
+ dpr = metrics.visualViewport?.clientWidth
11
+ ? metrics.cssVisualViewport?.clientWidth
12
+ ? Math.round((metrics.visualViewport.clientWidth / metrics.cssVisualViewport.clientWidth) * 100) / 100
13
+ : 1
14
+ : 1;
15
+ const { deviceScaleFactor } = await cdp.send('Emulation.getDeviceMetricsOverride', {}, sid).catch(() => ({}));
16
+ if (deviceScaleFactor) dpr = deviceScaleFactor;
17
+ } catch { /* fallback */ }
18
+ if (dpr === 1) {
19
+ try {
20
+ const raw = await evalStr(cdp, sid, 'window.devicePixelRatio');
21
+ const parsed = parseFloat(raw);
22
+ if (parsed > 0) dpr = parsed;
23
+ } catch { /* fallback */ }
24
+ }
25
+
26
+ const screenshotParams = { format: 'png' };
27
+
28
+ if (fullPage) {
29
+ try {
30
+ const metrics = await cdp.send('Page.getLayoutMetrics', {}, sid);
31
+ const width = metrics.contentSize?.width || metrics.cssContentSize?.width;
32
+ const height = metrics.contentSize?.height || metrics.cssContentSize?.height;
33
+ if (width && height) {
34
+ screenshotParams.clip = { x: 0, y: 0, width, height, scale: 1 };
35
+ screenshotParams.captureBeyondViewport = true;
36
+ }
37
+ } catch { /* fallback para viewport */ }
38
+ }
39
+
40
+ const { data } = await cdp.send('Page.captureScreenshot', screenshotParams, sid);
41
+ const out = filePath || config?.defaultScreenshotPath || '/tmp/screenshot.png';
42
+ writeFileSync(out, Buffer.from(data, 'base64'));
43
+
44
+ const lines = [out];
45
+ lines.push(`Screenshot saved${fullPage ? ' (full page)' : ' (viewport only)'}. DPR: ${dpr}`);
46
+ if (!fullPage) {
47
+ lines.push(`Coordinate mapping: CSS px = screenshot px / ${dpr}`);
48
+ lines.push(` e.g. screenshot (${Math.round(100 * dpr)}, ${Math.round(200 * dpr)}) -> clickxy <target> 100 200`);
49
+ if (dpr !== 1) {
50
+ lines.push(` On this ${dpr}x display: CSS px = screenshot px * ${Math.round(100 / dpr) / 100}`);
51
+ }
52
+ }
53
+ return lines.join('\n');
54
+ }
@@ -0,0 +1,44 @@
1
+ // Scroll controlado
2
+
3
+ import { evalStr } from './evaluate.mjs';
4
+
5
+ export async function scrollStr(cdp, sid, direction, amountOrSelector) {
6
+ if (!direction) throw new Error('Direction required: up, down, top, bottom, to');
7
+
8
+ switch (direction.toLowerCase()) {
9
+ case 'down': {
10
+ const px = parseInt(amountOrSelector) || 500;
11
+ await evalStr(cdp, sid, `window.scrollBy(0, ${px})`);
12
+ const pos = await evalStr(cdp, sid, 'Math.round(window.scrollY)');
13
+ return `Scrolled down ${px}px (position: ${pos}px)`;
14
+ }
15
+ case 'up': {
16
+ const px = parseInt(amountOrSelector) || 500;
17
+ await evalStr(cdp, sid, `window.scrollBy(0, -${px})`);
18
+ const pos = await evalStr(cdp, sid, 'Math.round(window.scrollY)');
19
+ return `Scrolled up ${px}px (position: ${pos}px)`;
20
+ }
21
+ case 'top':
22
+ await evalStr(cdp, sid, 'window.scrollTo(0, 0)');
23
+ return 'Scrolled to top';
24
+ case 'bottom':
25
+ await evalStr(cdp, sid, 'window.scrollTo(0, document.documentElement.scrollHeight)');
26
+ return 'Scrolled to bottom';
27
+ case 'to': {
28
+ if (!amountOrSelector) throw new Error('CSS selector required for "scroll to"');
29
+ const result = await evalStr(cdp, sid, `
30
+ (function() {
31
+ const el = document.querySelector(${JSON.stringify(amountOrSelector)});
32
+ if (!el) return { ok: false, error: 'Element not found: ' + ${JSON.stringify(amountOrSelector)} };
33
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
34
+ return { ok: true, tag: el.tagName, text: el.textContent.trim().substring(0, 50) };
35
+ })()
36
+ `);
37
+ const r = JSON.parse(result);
38
+ if (!r.ok) throw new Error(r.error);
39
+ return `Scrolled to <${r.tag}> "${r.text}"`;
40
+ }
41
+ default:
42
+ throw new Error(`Unknown scroll direction: ${direction}. Use: up, down, top, bottom, to`);
43
+ }
44
+ }
@@ -0,0 +1,100 @@
1
+ // Accessibility tree snapshot with optional interactive refs (@e1, @e2...)
2
+
3
+ const INTERACTIVE_ROLES = new Set([
4
+ 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox',
5
+ 'menuitem', 'tab', 'switch', 'searchbox', 'slider', 'spinbutton',
6
+ 'option', 'menuitemcheckbox', 'menuitemradio', 'treeitem',
7
+ ]);
8
+
9
+ function shouldShowAxNode(node, compact = false) {
10
+ const role = node.role?.value || '';
11
+ const name = node.name?.value ?? '';
12
+ const value = node.value?.value;
13
+ if (compact && role === 'InlineTextBox') return false;
14
+ return role !== 'none' && role !== 'generic' && !(name === '' && (value === '' || value == null));
15
+ }
16
+
17
+ function formatAxNode(node, depth, refIndex, refs) {
18
+ const role = node.role?.value || '';
19
+ const name = node.name?.value ?? '';
20
+ const value = node.value?.value;
21
+ const indent = ' '.repeat(Math.min(depth, 10));
22
+
23
+ let refTag = '';
24
+ if (refs && INTERACTIVE_ROLES.has(role.toLowerCase())) {
25
+ refTag = `@e${refIndex.value} `;
26
+ refIndex.value++;
27
+ }
28
+
29
+ let line = `${indent}${refTag}[${role}]`;
30
+ if (name !== '') line += ` ${name}`;
31
+ if (!(value === '' || value == null)) line += ` = ${JSON.stringify(value)}`;
32
+ return line;
33
+ }
34
+
35
+ function orderedAxChildren(node, nodesById, childrenByParent) {
36
+ const children = [];
37
+ const seen = new Set();
38
+ for (const childId of node.childIds || []) {
39
+ const child = nodesById.get(childId);
40
+ if (child && !seen.has(child.nodeId)) {
41
+ seen.add(child.nodeId);
42
+ children.push(child);
43
+ }
44
+ }
45
+ for (const child of childrenByParent.get(node.nodeId) || []) {
46
+ if (!seen.has(child.nodeId)) {
47
+ seen.add(child.nodeId);
48
+ children.push(child);
49
+ }
50
+ }
51
+ return children;
52
+ }
53
+
54
+ // refMap is populated when refs=true: { refNumber -> { backendNodeId, role, name } }
55
+ // The caller (daemon) stores this map for later ref resolution.
56
+ export async function snapshotStr(cdp, sid, compact = true, refs = false) {
57
+ const { nodes } = await cdp.send('Accessibility.getFullAXTree', {}, sid);
58
+ const nodesById = new Map(nodes.map(node => [node.nodeId, node]));
59
+ const childrenByParent = new Map();
60
+ for (const node of nodes) {
61
+ if (!node.parentId) continue;
62
+ if (!childrenByParent.has(node.parentId)) childrenByParent.set(node.parentId, []);
63
+ childrenByParent.get(node.parentId).push(node);
64
+ }
65
+
66
+ const refIndex = { value: 1 };
67
+ const refMap = new Map();
68
+ const lines = [];
69
+ const visited = new Set();
70
+
71
+ function visit(node, depth) {
72
+ if (!node || visited.has(node.nodeId)) return;
73
+ visited.add(node.nodeId);
74
+ if (shouldShowAxNode(node, compact)) {
75
+ const role = node.role?.value || '';
76
+ const currentRef = refIndex.value;
77
+
78
+ lines.push(formatAxNode(node, depth, refIndex, refs));
79
+
80
+ // If ref was assigned (refIndex advanced), record the mapping
81
+ if (refs && refIndex.value > currentRef) {
82
+ refMap.set(currentRef, {
83
+ backendNodeId: node.backendDOMNodeId,
84
+ nodeId: node.nodeId,
85
+ role,
86
+ name: node.name?.value ?? '',
87
+ });
88
+ }
89
+ }
90
+ for (const child of orderedAxChildren(node, nodesById, childrenByParent)) {
91
+ visit(child, depth + 1);
92
+ }
93
+ }
94
+
95
+ const roots = nodes.filter(node => !node.parentId || !nodesById.has(node.parentId));
96
+ for (const root of roots) visit(root, 0);
97
+ for (const node of nodes) visit(node, 0);
98
+
99
+ return { text: lines.join('\n'), refMap };
100
+ }
@@ -0,0 +1,47 @@
1
+ // LocalStorage / SessionStorage management
2
+
3
+ import { evalStr } from './evaluate.mjs';
4
+
5
+ export async function storageStr(cdp, sid, action) {
6
+ switch (action) {
7
+ case 'local': {
8
+ const raw = await evalStr(cdp, sid, `
9
+ JSON.stringify(Object.fromEntries(
10
+ Object.keys(localStorage).map(k => [k, localStorage.getItem(k)?.substring(0, 200)])
11
+ ))
12
+ `);
13
+ const data = JSON.parse(raw);
14
+ const keys = Object.keys(data);
15
+ if (keys.length === 0) return 'localStorage is empty.';
16
+ return keys.map(k => {
17
+ const v = data[k];
18
+ const val = v && v.length > 80 ? v.slice(0, 80) + '...' : v;
19
+ return `${k.padEnd(40)} ${val}`;
20
+ }).join('\n');
21
+ }
22
+
23
+ case 'session': {
24
+ const raw = await evalStr(cdp, sid, `
25
+ JSON.stringify(Object.fromEntries(
26
+ Object.keys(sessionStorage).map(k => [k, sessionStorage.getItem(k)?.substring(0, 200)])
27
+ ))
28
+ `);
29
+ const data = JSON.parse(raw);
30
+ const keys = Object.keys(data);
31
+ if (keys.length === 0) return 'sessionStorage is empty.';
32
+ return keys.map(k => {
33
+ const v = data[k];
34
+ const val = v && v.length > 80 ? v.slice(0, 80) + '...' : v;
35
+ return `${k.padEnd(40)} ${val}`;
36
+ }).join('\n');
37
+ }
38
+
39
+ case 'clear': {
40
+ await evalStr(cdp, sid, 'localStorage.clear(); sessionStorage.clear()');
41
+ return 'Cleared localStorage and sessionStorage.';
42
+ }
43
+
44
+ default:
45
+ throw new Error(`Unknown storage action: ${action}. Use: local, session, clear`);
46
+ }
47
+ }
@@ -0,0 +1,31 @@
1
+ // Multi-tab management via Target domain
2
+
3
+ export async function openTabStr(cdp, url) {
4
+ if (!url) throw new Error('URL required');
5
+ const { targetId } = await cdp.send('Target.createTarget', { url });
6
+ return `Opened new tab (targetId: ${targetId.slice(0, 8)}). URL: ${url}`;
7
+ }
8
+
9
+ export async function closeTabStr(cdp, targetPrefix) {
10
+ if (!targetPrefix) throw new Error('Target ID required');
11
+ const targetId = await resolveTarget(cdp, targetPrefix);
12
+ const { success } = await cdp.send('Target.closeTarget', { targetId });
13
+ if (!success) throw new Error(`Failed to close target ${targetId.slice(0, 8)}`);
14
+ return `Closed tab ${targetId.slice(0, 8)}.`;
15
+ }
16
+
17
+ export async function focusTabStr(cdp, targetPrefix) {
18
+ if (!targetPrefix) throw new Error('Target ID required');
19
+ const targetId = await resolveTarget(cdp, targetPrefix);
20
+ await cdp.send('Target.activateTarget', { targetId });
21
+ return `Focused tab ${targetId.slice(0, 8)}.`;
22
+ }
23
+
24
+ async function resolveTarget(cdp, prefix) {
25
+ const { targetInfos } = await cdp.send('Target.getTargets');
26
+ const pages = targetInfos.filter(t => t.type === 'page');
27
+ const matches = pages.filter(p => p.targetId.toUpperCase().startsWith(prefix.toUpperCase()));
28
+ if (matches.length === 0) throw new Error(`No target matching prefix "${prefix}"`);
29
+ if (matches.length > 1) throw new Error(`Ambiguous prefix "${prefix}" — matches ${matches.length} targets`);
30
+ return matches[0].targetId;
31
+ }
@@ -0,0 +1,38 @@
1
+ // Network throttling via Network domain
2
+
3
+ const PRESETS = {
4
+ '3g': { offline: false, latency: 100, downloadThroughput: 750 * 1024 / 8, uploadThroughput: 250 * 1024 / 8 },
5
+ 'slow-3g': { offline: false, latency: 2000, downloadThroughput: 50 * 1024 / 8, uploadThroughput: 50 * 1024 / 8 },
6
+ '4g': { offline: false, latency: 20, downloadThroughput: 4000 * 1024 / 8, uploadThroughput: 3000 * 1024 / 8 },
7
+ 'offline': { offline: true, latency: 0, downloadThroughput: 0, uploadThroughput: 0 },
8
+ };
9
+
10
+ export async function throttleStr(cdp, sid, preset, ...customArgs) {
11
+ if (!preset || preset === 'reset') {
12
+ await cdp.send('Network.emulateNetworkConditions', {
13
+ offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1,
14
+ }, sid);
15
+ return 'Network throttling reset.';
16
+ }
17
+
18
+ if (preset === 'custom') {
19
+ const [latency, down, up] = customArgs.map(Number);
20
+ if (isNaN(latency) || isNaN(down) || isNaN(up)) {
21
+ throw new Error('Usage: throttle <target> custom <latency_ms> <down_kbps> <up_kbps>');
22
+ }
23
+ await cdp.send('Network.enable', {}, sid);
24
+ await cdp.send('Network.emulateNetworkConditions', {
25
+ offline: false, latency, downloadThroughput: down * 1024 / 8, uploadThroughput: up * 1024 / 8,
26
+ }, sid);
27
+ return `Network throttled: ${latency}ms latency, ${down}kbps down, ${up}kbps up.`;
28
+ }
29
+
30
+ const conditions = PRESETS[preset.toLowerCase()];
31
+ if (!conditions) {
32
+ throw new Error(`Unknown preset: ${preset}. Available: ${Object.keys(PRESETS).join(', ')}, custom, reset`);
33
+ }
34
+
35
+ await cdp.send('Network.enable', {}, sid);
36
+ await cdp.send('Network.emulateNetworkConditions', conditions, sid);
37
+ return `Network throttled to ${preset}${conditions.offline ? ' (offline)' : ''}.`;
38
+ }
@@ -0,0 +1,62 @@
1
+ // Touch gestures via Input domain
2
+
3
+ export async function touchStr(cdp, sid, gesture, ...args) {
4
+ if (!gesture) throw new Error('Usage: touch <target> tap <x> <y> | swipe <x1>,<y1> <x2>,<y2> | pinch <x> <y> <scale> | longpress <x> <y> [ms]');
5
+
6
+ // Enable touch emulation
7
+ await cdp.send('Emulation.setTouchEmulationEnabled', { enabled: true, maxTouchPoints: 5 }, sid);
8
+
9
+ switch (gesture.toLowerCase()) {
10
+ case 'tap': {
11
+ const [x, y] = args.map(Number);
12
+ if (isNaN(x) || isNaN(y)) throw new Error('Usage: touch <target> tap <x> <y>');
13
+ await cdp.send('Input.synthesizeTapGesture', {
14
+ x, y, duration: 50, tapCount: 1,
15
+ }, sid);
16
+ return `Tapped at (${x}, ${y}).`;
17
+ }
18
+
19
+ case 'swipe': {
20
+ const [fromStr, toStr] = args;
21
+ if (!fromStr || !toStr) throw new Error('Usage: touch <target> swipe <x1>,<y1> <x2>,<y2>');
22
+ const [x1, y1] = fromStr.split(',').map(Number);
23
+ const [x2, y2] = toStr.split(',').map(Number);
24
+ if ([x1, y1, x2, y2].some(isNaN)) throw new Error('Invalid coordinates');
25
+
26
+ await cdp.send('Input.synthesizeScrollGesture', {
27
+ x: x1, y: y1,
28
+ xDistance: x2 - x1,
29
+ yDistance: y2 - y1,
30
+ speed: 800,
31
+ preventFling: true,
32
+ gestureSourceType: 'touch',
33
+ }, sid);
34
+ return `Swiped from (${x1},${y1}) to (${x2},${y2}).`;
35
+ }
36
+
37
+ case 'pinch': {
38
+ const [x, y, scale] = args.map(Number);
39
+ if (isNaN(x) || isNaN(y) || isNaN(scale)) throw new Error('Usage: touch <target> pinch <x> <y> <scale>');
40
+ await cdp.send('Input.synthesizePinchGesture', {
41
+ x, y, scaleFactor: scale, relativeSpeed: 300,
42
+ gestureSourceType: 'touch',
43
+ }, sid);
44
+ return `Pinch ${scale > 1 ? 'zoom in' : 'zoom out'} at (${x},${y}) scale=${scale}.`;
45
+ }
46
+
47
+ case 'longpress': {
48
+ const [x, y, durationStr] = args;
49
+ const cx = parseFloat(x);
50
+ const cy = parseFloat(y);
51
+ const duration = parseInt(durationStr) || 1000;
52
+ if (isNaN(cx) || isNaN(cy)) throw new Error('Usage: touch <target> longpress <x> <y> [ms]');
53
+ await cdp.send('Input.synthesizeTapGesture', {
54
+ x: cx, y: cy, duration, tapCount: 1,
55
+ }, sid);
56
+ return `Long press at (${cx},${cy}) for ${duration}ms.`;
57
+ }
58
+
59
+ default:
60
+ throw new Error(`Unknown gesture: ${gesture}. Available: tap, swipe, pinch, longpress`);
61
+ }
62
+ }
@@ -0,0 +1,51 @@
1
+ // Performance tracing via Tracing domain
2
+
3
+ import { writeFileSync } from 'fs';
4
+
5
+ let tracing = false;
6
+ const chunks = [];
7
+
8
+ export async function traceStr(cdp, sid, action, fileOrCategories) {
9
+ if (!action) throw new Error('Usage: trace <target> start [categories] | stop [file]');
10
+
11
+ switch (action) {
12
+ case 'start': {
13
+ if (tracing) return 'Tracing already active.';
14
+ chunks.length = 0;
15
+ tracing = true;
16
+
17
+ cdp.onEvent('Tracing.dataCollected', (params) => {
18
+ if (params.value) chunks.push(...params.value);
19
+ });
20
+
21
+ const categories = fileOrCategories || 'devtools.timeline,v8.execute';
22
+ await cdp.send('Tracing.start', {
23
+ traceConfig: {
24
+ recordMode: 'recordUntilFull',
25
+ includedCategories: categories.split(','),
26
+ },
27
+ }, sid);
28
+ return `Tracing started (categories: ${categories.substring(0, 60)}...). Use "trace <target> stop [file]" to save.`;
29
+ }
30
+
31
+ case 'stop': {
32
+ if (!tracing) return 'No trace active.';
33
+ tracing = false;
34
+
35
+ await cdp.send('Tracing.end', {}, sid);
36
+ // Aguardar Tracing.tracingComplete
37
+ try {
38
+ await cdp.waitForEvent('Tracing.tracingComplete', 30000).promise;
39
+ } catch { /* timeout ok, já temos os chunks */ }
40
+
41
+ const out = fileOrCategories || '/tmp/chromex-trace.json';
42
+ writeFileSync(out, JSON.stringify(chunks));
43
+ const count = chunks.length;
44
+ chunks.length = 0;
45
+ return `Trace saved to ${out} (${count} events). Open in chrome://tracing or Perfetto UI.`;
46
+ }
47
+
48
+ default:
49
+ throw new Error('Usage: trace <target> start [categories] | stop [file]');
50
+ }
51
+ }
@@ -0,0 +1,43 @@
1
+ // File upload via DOM domain
2
+
3
+ import { existsSync } from 'fs';
4
+ import { resolve } from 'path';
5
+ import { evalStr } from './evaluate.mjs';
6
+
7
+ export async function uploadStr(cdp, sid, selector, ...filePaths) {
8
+ if (!selector) throw new Error('CSS selector required');
9
+ if (filePaths.length === 0) throw new Error('At least one file path required');
10
+
11
+ // Validar que arquivos existem
12
+ const resolvedPaths = filePaths.map(f => resolve(f));
13
+ for (const fp of resolvedPaths) {
14
+ if (!existsSync(fp)) throw new Error(`File not found: ${fp}`);
15
+ }
16
+
17
+ // Encontrar o backendNodeId do input
18
+ await cdp.send('DOM.enable', {}, sid);
19
+ const { root } = await cdp.send('DOM.getDocument', {}, sid);
20
+ const { nodeId } = await cdp.send('DOM.querySelector', {
21
+ nodeId: root.nodeId,
22
+ selector,
23
+ }, sid);
24
+
25
+ if (!nodeId) throw new Error(`Element not found: ${selector}`);
26
+
27
+ const { node } = await cdp.send('DOM.describeNode', { nodeId }, sid);
28
+
29
+ await cdp.send('DOM.setFileInputFiles', {
30
+ files: resolvedPaths,
31
+ backendNodeId: node.backendNodeId,
32
+ }, sid);
33
+
34
+ // Disparar change event
35
+ await evalStr(cdp, sid, `
36
+ (function() {
37
+ const el = document.querySelector(${JSON.stringify(selector)});
38
+ if (el) el.dispatchEvent(new Event('change', { bubbles: true }));
39
+ })()
40
+ `);
41
+
42
+ return `Uploaded ${resolvedPaths.length} file(s) to ${selector}: ${resolvedPaths.map(f => f.split('/').pop()).join(', ')}`;
43
+ }
@@ -0,0 +1,84 @@
1
+ // Wait for lifecycle events (networkidle, load, domready)
2
+
3
+ import { sleep } from '../utils.mjs';
4
+ import { evalStr } from './evaluate.mjs';
5
+
6
+ export async function waitLifecycleStr(cdp, sid, event, timeoutMs, config) {
7
+ const timeout = parseInt(timeoutMs) || config?.navigationTimeout || 30000;
8
+ const eventMap = {
9
+ 'networkidle': 'networkIdle',
10
+ 'network-idle': 'networkIdle',
11
+ 'load': 'load',
12
+ 'domready': 'DOMContentLoaded',
13
+ 'dom-ready': 'DOMContentLoaded',
14
+ 'domcontentloaded': 'DOMContentLoaded',
15
+ 'fcp': 'firstContentfulPaint',
16
+ 'firstcontentfulpaint': 'firstContentfulPaint',
17
+ };
18
+
19
+ if (!event) throw new Error('Event required: networkidle, load, domready, fcp');
20
+
21
+ const cdpEvent = eventMap[event.toLowerCase()];
22
+ if (!cdpEvent) {
23
+ throw new Error(`Unknown event: ${event}. Available: ${Object.keys(eventMap).join(', ')}`);
24
+ }
25
+
26
+ // Checar se o estado já foi atingido (para load/domready)
27
+ if (cdpEvent === 'load' || cdpEvent === 'DOMContentLoaded') {
28
+ try {
29
+ const state = await evalStr(cdp, sid, 'document.readyState');
30
+ if (cdpEvent === 'DOMContentLoaded' && (state === 'interactive' || state === 'complete')) {
31
+ return `${event} already reached (readyState: ${state})`;
32
+ }
33
+ if (cdpEvent === 'load' && state === 'complete') {
34
+ return `${event} already reached (readyState: complete)`;
35
+ }
36
+ } catch { /* pagina pode estar navegando */ }
37
+ }
38
+
39
+ // Para networkIdle: poll baseado em performance API
40
+ if (cdpEvent === 'networkIdle') {
41
+ const start = Date.now();
42
+ const deadline = start + timeout;
43
+ let idleCount = 0;
44
+ while (Date.now() < deadline) {
45
+ try {
46
+ const pending = await evalStr(cdp, sid,
47
+ 'performance.getEntriesByType("resource").filter(e => e.responseEnd === 0).length'
48
+ );
49
+ if (pending === '0') {
50
+ idleCount++;
51
+ if (idleCount >= 3) return `networkidle reached (waited ${Date.now() - start}ms)`;
52
+ } else {
53
+ idleCount = 0;
54
+ }
55
+ } catch { /* pagina pode estar navegando */ }
56
+ await sleep(500);
57
+ }
58
+ throw new Error(`Timeout (${timeout}ms) waiting for networkidle`);
59
+ }
60
+
61
+ // Para outros eventos: usar Page.lifecycleEvent
62
+ await cdp.send('Page.enable', {}, sid);
63
+ await cdp.send('Page.setLifecycleEventsEnabled', { enabled: true }, sid);
64
+
65
+ const start = Date.now();
66
+ return new Promise((resolve, reject) => {
67
+ let settled = false;
68
+ const off = cdp.onEvent('Page.lifecycleEvent', (params) => {
69
+ if (params.name === cdpEvent && !settled) {
70
+ settled = true;
71
+ off();
72
+ resolve(`${event} reached (waited ${Date.now() - start}ms)`);
73
+ }
74
+ });
75
+
76
+ setTimeout(() => {
77
+ if (!settled) {
78
+ settled = true;
79
+ off();
80
+ reject(new Error(`Timeout (${timeout}ms) waiting for ${event}`));
81
+ }
82
+ }, timeout);
83
+ });
84
+ }
@@ -0,0 +1,47 @@
1
+ // WebAuthn / Passkey testing via WebAuthn domain
2
+
3
+ let authenticatorId = null;
4
+
5
+ export async function webauthnStr(cdp, sid, action) {
6
+ if (!action) throw new Error('Usage: webauthn <target> enable | creds | disable');
7
+
8
+ switch (action) {
9
+ case 'enable': {
10
+ await cdp.send('WebAuthn.enable', { enableUI: false }, sid);
11
+ const result = await cdp.send('WebAuthn.addVirtualAuthenticator', {
12
+ options: {
13
+ protocol: 'ctap2',
14
+ transport: 'internal',
15
+ hasResidentKey: true,
16
+ hasUserVerification: true,
17
+ isUserVerified: true,
18
+ automaticPresenceSimulation: true,
19
+ },
20
+ }, sid);
21
+ authenticatorId = result.authenticatorId;
22
+ return `Virtual authenticator created (id: ${authenticatorId}). Passkey flows will work automatically.`;
23
+ }
24
+
25
+ case 'creds': {
26
+ if (!authenticatorId) throw new Error('No authenticator active. Run "webauthn enable" first.');
27
+ const { credentials } = await cdp.send('WebAuthn.getCredentials', { authenticatorId }, sid);
28
+ if (credentials.length === 0) return 'No credentials stored.';
29
+ return credentials.map((c, i) => {
30
+ const id = Buffer.from(c.credentialId, 'base64').toString('hex').slice(0, 16);
31
+ return `${i + 1}. ${id} rpId=${c.rpId} userHandle=${c.userHandle || 'none'}`;
32
+ }).join('\n');
33
+ }
34
+
35
+ case 'disable': {
36
+ if (authenticatorId) {
37
+ await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }, sid);
38
+ authenticatorId = null;
39
+ }
40
+ await cdp.send('WebAuthn.disable', {}, sid);
41
+ return 'WebAuthn virtual authenticator removed.';
42
+ }
43
+
44
+ default:
45
+ throw new Error('Usage: webauthn <target> enable | creds | disable');
46
+ }
47
+ }