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,343 @@
1
+ #!/usr/bin/env node
2
+ // chromex -- Chrome DevTools Protocol CLI for AI agents
3
+ // Zero dependencies. Node 22+ (built-in WebSocket).
4
+
5
+ import { writeFileSync, existsSync, readFileSync } from 'fs';
6
+ import { loadConfig } from './lib/config.mjs';
7
+ import { CDP } from './lib/client.mjs';
8
+ import { getWsUrl, getPages, formatPageList } from './lib/browser.mjs';
9
+ import { audit } from './lib/security.mjs';
10
+ import { resolvePrefix, listDaemonSockets } from './lib/utils.mjs';
11
+ import { runDaemon } from './lib/daemon.mjs';
12
+ import { getOrStartTabDaemon, sendCommand, stopDaemons, findAnyDaemonSocket, checkTargetDomain } from './lib/ipc.mjs';
13
+ import { launchBrowser, incognitoContext } from './lib/launcher.mjs';
14
+
15
+ const config = loadConfig();
16
+
17
+ // Comandos que precisam de target (tab)
18
+ const NEEDS_TARGET = new Set([
19
+ 'snap', 'snapshot', 'eval', 'shot', 'screenshot', 'html', 'nav', 'navigate',
20
+ 'net', 'network', 'click', 'clickxy', 'type', 'loadall', 'evalraw', 'waitfor',
21
+ 'fill', 'clear', 'select', 'check', 'form',
22
+ 'scroll', 'cookies', 'pdf', 'console', 'storage', 'emulate', 'perf',
23
+ // Tier 1
24
+ 'wait', 'dialog', 'upload',
25
+ 'geo', 'timezone', 'locale', 'throttle', 'cpu', 'inject', 'download',
26
+ // Tier 2
27
+ 'intercept', 'har', 'coverage',
28
+ // Tier 3
29
+ 'trace', 'heap', 'webauthn', 'drag', 'touch', 'domsnapshot', 'highlight',
30
+ 'hover',
31
+ ]);
32
+
33
+ const USAGE = `chromex - Chrome DevTools Protocol CLI for AI agents
34
+
35
+ Usage: chromex <command> [args]
36
+
37
+ PAGES
38
+ list List open pages (shows unique target prefixes)
39
+ open <url> Open new tab
40
+ close <target> Close tab
41
+ focus <target> Activate/focus tab
42
+ launch [options] Launch browser with remote debugging
43
+ --incognito Launch in incognito mode
44
+ --browser chrome|brave|edge Choose browser
45
+ --profile NAME Use named profile
46
+ --url URL Open URL on launch
47
+ incognito [url] Create isolated browser context (no relaunch)
48
+
49
+ INSPECT
50
+ snap <target> Accessibility tree snapshot (compact)
51
+ html <target> [selector] Get HTML (full page or CSS selector)
52
+ shot <target> [file] [--full] Screenshot (viewport or full page)
53
+ net <target> Network performance entries
54
+ perf <target> Core Web Vitals + performance metrics
55
+ console <target> [duration_ms] Capture console output (default 5000ms)
56
+ domsnapshot <target> [--styles] Structured DOM snapshot with bounding rects
57
+ highlight <target> <sel|clear> Highlight element with overlay
58
+
59
+ EVALUATE
60
+ eval <target> <expr> Evaluate JS expression
61
+ evalraw <target> <method> [json] Raw CDP command (some methods blocked)
62
+
63
+ NAVIGATE
64
+ nav <target> <url> Navigate to URL and wait for load
65
+ waitfor <target> <selector> [ms] Wait for CSS selector to appear
66
+ wait <target> <event> [ms] Wait for: networkidle, load, domready, fcp
67
+ scroll <target> <dir> [amount] Scroll: up, down, top, bottom, to <selector>
68
+
69
+ INTERACT
70
+ click <target> <selector> Click element by CSS selector
71
+ clickxy <target> <x> <y> Click at CSS pixel coordinates
72
+ type <target> <text> Type text at current focus
73
+ drag <target> <from> <to> Drag & drop (selectors or x1,y1 x2,y2)
74
+ touch <target> <gesture> [args] Touch: tap, swipe, pinch, longpress
75
+ dialog <target> <action> [text] Handle dialogs: accept, dismiss, auto
76
+
77
+ FORMS
78
+ fill <target> <selector> <value> Fill input/textarea
79
+ clear <target> <selector> Clear input field
80
+ select <target> <selector> <value> Select option in <select>
81
+ check <target> <selector> [bool] Toggle checkbox/radio (default: true)
82
+ form <target> <json> Batch fill: {"#email":"x","#terms":true}
83
+ upload <target> <selector> <files> Upload file(s) to input[type=file]
84
+ loadall <target> <selector> [ms] Click "load more" until gone
85
+
86
+ DATA
87
+ cookies <target> [action] [arg] Cookies: list, set <json>, clear [domain]
88
+ storage <target> <type> Storage: local, session, clear
89
+ pdf <target> [file] Save page as PDF
90
+
91
+ NETWORK
92
+ throttle <target> <preset|reset> Throttle: 3g, slow-3g, 4g, offline, custom, reset
93
+ intercept <target> <action> [args] Intercept: on, block, mock, off, rules
94
+ har <target> start|stop [file] Record HTTP traffic as HAR file
95
+
96
+ EMULATE
97
+ emulate <target> <device|reset> Emulate device (iphone-14, pixel-7, etc.)
98
+ geo <target> <lat> <lon>|reset Set geolocation override
99
+ timezone <target> <tz|reset> Set timezone (e.g. America/Sao_Paulo)
100
+ locale <target> <locale|reset> Set locale (e.g. pt-BR)
101
+ cpu <target> <rate|reset> CPU throttle (1=normal, 4=4x slower, 6=mobile)
102
+
103
+ ADVANCED
104
+ inject <target> <script|flags> Inject JS on every page load (--file, --remove, --list)
105
+ download <target> allow|deny|reset Control download behavior
106
+ coverage <target> start|stop CSS/JS code coverage report
107
+ trace <target> start|stop [file] Performance trace (chrome://tracing format)
108
+ heap <target> snapshot [file] Heap snapshot for memory analysis
109
+ webauthn <target> enable|creds|dis Virtual authenticator for passkey testing
110
+
111
+ DAEMON
112
+ stop [target] Stop daemon(s)
113
+
114
+ <target> is a unique targetId prefix from "chromex list". Ambiguous prefixes are rejected.
115
+
116
+ SECURITY
117
+ Config: ~/.chromex/config.json
118
+ - blockedDomains / allowedDomains: domain filtering
119
+ - blockedCdpMethods: CDP methods blocked in evalraw
120
+ - socketAuth / auditLog: daemon security
121
+
122
+ COORDINATES
123
+ shot captures at native resolution: image px = CSS px * DPR.
124
+ clickxy takes CSS pixels. CSS px = screenshot px / DPR.
125
+ `;
126
+
127
+ async function main() {
128
+ const [cmd, ...args] = process.argv.slice(2);
129
+
130
+ // Daemon mode (internal)
131
+ if (cmd === '_daemon') {
132
+ await runDaemon(args[0], config);
133
+ return;
134
+ }
135
+
136
+ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
137
+ console.log(USAGE);
138
+ process.exit(0);
139
+ }
140
+
141
+ // --- Comandos sem target ---
142
+
143
+ // List
144
+ if (cmd === 'list' || cmd === 'ls') {
145
+ let pages;
146
+ const existingSock = findAnyDaemonSocket(config);
147
+ if (existingSock) {
148
+ try {
149
+ const conn = await connectAndAuthFromSocket(existingSock);
150
+ const resp = await sendCommand(conn, { cmd: 'list_raw' });
151
+ if (resp.ok) pages = JSON.parse(resp.result);
152
+ } catch { /* fallback para conexão direta */ }
153
+ }
154
+ if (!pages) {
155
+ const cdp = new CDP(config.commandTimeout);
156
+ await cdp.connect(getWsUrl());
157
+ pages = await getPages(cdp);
158
+ cdp.close();
159
+ }
160
+ writeFileSync(config._pagesCachePath, JSON.stringify(pages));
161
+ audit('list', null, [], { ok: true }, config);
162
+ console.log(formatPageList(pages, config));
163
+ setTimeout(() => process.exit(0), 100);
164
+ return;
165
+ }
166
+
167
+ // Launch
168
+ if (cmd === 'launch') {
169
+ const options = parseFlags(args, ['incognito'], ['browser', 'profile', 'url']);
170
+ const result = await launchBrowser(options);
171
+ console.log(result);
172
+ return;
173
+ }
174
+
175
+ // Incognito (sem target -- cria novo context)
176
+ if (cmd === 'incognito') {
177
+ const cdp = new CDP(config.commandTimeout);
178
+ await cdp.connect(getWsUrl());
179
+ const result = await incognitoContext(cdp, args[0]);
180
+ console.log(result.message);
181
+ cdp.close();
182
+ return;
183
+ }
184
+
185
+ // Tab management (no daemon needed -- direct browser WebSocket)
186
+ if (cmd === 'open' || cmd === 'close' || cmd === 'focus') {
187
+ const { openTabStr, closeTabStr, focusTabStr } = await import('./lib/commands/tab.mjs');
188
+ const cdp = new CDP(config.commandTimeout);
189
+ await cdp.connect(getWsUrl());
190
+ let result;
191
+ if (cmd === 'open') {
192
+ if (!args[0]) { console.error('Error: URL required'); process.exit(1); }
193
+ result = await openTabStr(cdp, args[0]);
194
+ } else if (cmd === 'close') {
195
+ if (!args[0]) { console.error('Error: target prefix required'); process.exit(1); }
196
+ result = await closeTabStr(cdp, args[0]);
197
+ } else {
198
+ if (!args[0]) { console.error('Error: target prefix required'); process.exit(1); }
199
+ result = await focusTabStr(cdp, args[0]);
200
+ }
201
+ console.log(result);
202
+ cdp.close();
203
+ return;
204
+ }
205
+
206
+ // Stop
207
+ if (cmd === 'stop') {
208
+ await stopDaemons(args[0], config);
209
+ audit('stop', args[0] || 'all', [], { ok: true }, config);
210
+ return;
211
+ }
212
+
213
+ // --- Comandos com target ---
214
+ if (!NEEDS_TARGET.has(cmd)) {
215
+ console.error(`Unknown command: ${cmd}\n`);
216
+ console.log(USAGE);
217
+ process.exit(1);
218
+ }
219
+
220
+ const targetPrefix = args[0];
221
+ if (!targetPrefix) {
222
+ console.error('Error: target ID required. Run "chromex list" first.');
223
+ process.exit(1);
224
+ }
225
+
226
+ // Resolver prefix -> targetId
227
+ let targetId;
228
+ const daemonTargetIds = listDaemonSockets(config._socketDir).map(d => d.targetId);
229
+ const daemonMatches = daemonTargetIds.filter(id => id.toUpperCase().startsWith(targetPrefix.toUpperCase()));
230
+
231
+ if (daemonMatches.length > 0) {
232
+ targetId = resolvePrefix(targetPrefix, daemonTargetIds, 'daemon');
233
+ } else {
234
+ if (!existsSync(config._pagesCachePath)) {
235
+ console.error('No page list cached. Run "chromex list" first.');
236
+ process.exit(1);
237
+ }
238
+ const pages = JSON.parse(readFileSync(config._pagesCachePath, 'utf8'));
239
+ targetId = resolvePrefix(targetPrefix, pages.map(p => p.targetId), 'target', 'Run "chromex list".');
240
+ }
241
+
242
+ // Checar domínio da tab
243
+ checkTargetDomain(targetId, config);
244
+
245
+ const conn = await getOrStartTabDaemon(targetId, config);
246
+
247
+ const cmdArgs = args.slice(1);
248
+
249
+ // Juntar argumentos para comandos que aceitam texto livre
250
+ if (cmd === 'eval') {
251
+ const expr = cmdArgs.join(' ');
252
+ if (!expr) { console.error('Error: expression required'); process.exit(1); }
253
+ cmdArgs[0] = expr;
254
+ cmdArgs.length = 1;
255
+ } else if (cmd === 'type') {
256
+ const text = cmdArgs.join(' ');
257
+ if (!text) { console.error('Error: text required'); process.exit(1); }
258
+ cmdArgs[0] = text;
259
+ cmdArgs.length = 1;
260
+ } else if (cmd === 'fill') {
261
+ // fill <selector> <value...>
262
+ if (cmdArgs.length < 2) { console.error('Error: selector and value required'); process.exit(1); }
263
+ const selector = cmdArgs[0];
264
+ const value = cmdArgs.slice(1).join(' ');
265
+ cmdArgs[0] = selector;
266
+ cmdArgs[1] = value;
267
+ cmdArgs.length = 2;
268
+ } else if (cmd === 'evalraw') {
269
+ if (!cmdArgs[0]) { console.error('Error: CDP method required'); process.exit(1); }
270
+ if (cmdArgs.length > 2) {
271
+ cmdArgs[1] = cmdArgs.slice(1).join(' ');
272
+ cmdArgs.length = 2;
273
+ }
274
+ }
275
+
276
+ if ((cmd === 'nav' || cmd === 'navigate') && !cmdArgs[0]) {
277
+ console.error('Error: URL required');
278
+ process.exit(1);
279
+ }
280
+
281
+ const response = await sendCommand(conn, { cmd, args: cmdArgs });
282
+
283
+ if (response.ok) {
284
+ if (response.result) console.log(response.result);
285
+ } else {
286
+ console.error('Error:', response.error);
287
+ process.exitCode = 1;
288
+ }
289
+ }
290
+
291
+ // Parser de flags simples: --flag (boolean) e --key value
292
+ function parseFlags(args, booleanFlags, valueFlags) {
293
+ const result = {};
294
+ let i = 0;
295
+ while (i < args.length) {
296
+ const arg = args[i];
297
+ if (arg.startsWith('--')) {
298
+ const key = arg.slice(2);
299
+ if (booleanFlags.includes(key)) {
300
+ result[key] = true;
301
+ i++;
302
+ } else if (valueFlags.includes(key)) {
303
+ result[key] = args[i + 1];
304
+ i += 2;
305
+ } else {
306
+ i++;
307
+ }
308
+ } else {
309
+ // Argumento posicional -> assume URL
310
+ if (!result.url) result.url = arg;
311
+ i++;
312
+ }
313
+ }
314
+ return result;
315
+ }
316
+
317
+ // Helper para list -- conectar via socket existente
318
+ async function connectAndAuthFromSocket(socketPath) {
319
+ const { getOrCreateToken } = await import('./lib/daemon.mjs');
320
+ const authToken = getOrCreateToken(config);
321
+ const net = await import('net');
322
+ return new Promise((resolve, reject) => {
323
+ const conn = net.connect(socketPath);
324
+ conn.on('connect', () => {
325
+ if (!authToken) { resolve(conn); return; }
326
+ let buf = '';
327
+ conn.on('data', (chunk) => {
328
+ buf += chunk.toString();
329
+ const idx = buf.indexOf('\n');
330
+ if (idx === -1) return;
331
+ try {
332
+ const resp = JSON.parse(buf.slice(0, idx));
333
+ if (resp.ok) resolve(conn);
334
+ else reject(new Error('Auth failed'));
335
+ } catch { reject(new Error('Invalid response')); }
336
+ });
337
+ conn.write(JSON.stringify({ auth: authToken, id: 0 }) + '\n');
338
+ });
339
+ conn.on('error', reject);
340
+ });
341
+ }
342
+
343
+ main().catch(e => { console.error(e.message); process.exit(1); });
@@ -0,0 +1,66 @@
1
+ // Detecção de browser e listagem de páginas
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { homedir } from 'os';
5
+ import { checkDomain } from './security.mjs';
6
+ import { getDisplayPrefixLength } from './utils.mjs';
7
+
8
+ // Gera candidatos para cada browser: path base + Default/ subfolder
9
+ function candidates(base) {
10
+ return [resolve(base, 'DevToolsActivePort'), resolve(base, 'Default/DevToolsActivePort')];
11
+ }
12
+
13
+ const home = homedir();
14
+ const DEVTOOLS_CANDIDATES = [
15
+ // Brave
16
+ ...candidates(resolve(home, 'Library/Application Support/BraveSoftware/Brave-Browser')),
17
+ ...candidates(resolve(home, '.config/BraveSoftware/Brave-Browser')),
18
+ // Chrome
19
+ ...candidates(resolve(home, 'Library/Application Support/Google/Chrome')),
20
+ ...candidates(resolve(home, '.config/google-chrome')),
21
+ // Chrome Canary
22
+ ...candidates(resolve(home, 'Library/Application Support/Google/Chrome Canary')),
23
+ // Chromium
24
+ ...candidates(resolve(home, 'Library/Application Support/Chromium')),
25
+ ...candidates(resolve(home, '.config/chromium')),
26
+ // Edge
27
+ ...candidates(resolve(home, 'Library/Application Support/Microsoft Edge')),
28
+ ...candidates(resolve(home, '.config/microsoft-edge')),
29
+ // Vivaldi
30
+ ...candidates(resolve(home, 'Library/Application Support/Vivaldi')),
31
+ ...candidates(resolve(home, '.config/vivaldi')),
32
+ ];
33
+
34
+ export function getWsUrl() {
35
+ // Override via env var para setups não-padrão
36
+ if (process.env.CDP_PORT_FILE && existsSync(process.env.CDP_PORT_FILE)) {
37
+ const lines = readFileSync(process.env.CDP_PORT_FILE, 'utf8').trim().split('\n');
38
+ return `ws://127.0.0.1:${lines[0]}${lines[1]}`;
39
+ }
40
+
41
+ const portFile = DEVTOOLS_CANDIDATES.find(path => existsSync(path));
42
+ if (!portFile) {
43
+ throw new Error(
44
+ 'Could not find DevToolsActivePort file.\n' +
45
+ 'Enable remote debugging: chrome://inspect/#remote-debugging\n' +
46
+ 'Or launch with: chromex launch'
47
+ );
48
+ }
49
+ const lines = readFileSync(portFile, 'utf8').trim().split('\n');
50
+ return `ws://127.0.0.1:${lines[0]}${lines[1]}`;
51
+ }
52
+
53
+ export async function getPages(cdp) {
54
+ const { targetInfos } = await cdp.send('Target.getTargets');
55
+ return targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://'));
56
+ }
57
+
58
+ export function formatPageList(pages, config) {
59
+ const prefixLen = getDisplayPrefixLength(pages.map(p => p.targetId));
60
+ return pages.map(p => {
61
+ const id = p.targetId.slice(0, prefixLen).padEnd(prefixLen);
62
+ const title = p.title.substring(0, 54).padEnd(54);
63
+ const blocked = checkDomain(p.url, config) ? ' [BLOCKED]' : '';
64
+ return `${id} ${title} ${p.url}${blocked}`;
65
+ }).join('\n');
66
+ }
@@ -0,0 +1,98 @@
1
+ // CDP WebSocket client -- protocolo raw, zero dependencies
2
+ // Requer Node 22+ (WebSocket nativo)
3
+
4
+ const DEFAULT_TIMEOUT = 15000;
5
+
6
+ export class CDP {
7
+ #ws;
8
+ #id = 0;
9
+ #pending = new Map();
10
+ #eventHandlers = new Map();
11
+ #closeHandlers = [];
12
+ #timeout;
13
+
14
+ constructor(timeout = DEFAULT_TIMEOUT) {
15
+ this.#timeout = timeout;
16
+ }
17
+
18
+ async connect(wsUrl) {
19
+ return new Promise((res, rej) => {
20
+ this.#ws = new WebSocket(wsUrl);
21
+ this.#ws.onopen = () => res();
22
+ this.#ws.onerror = (e) => rej(new Error('WebSocket error: ' + (e.message || e.type)));
23
+ this.#ws.onclose = () => this.#closeHandlers.forEach(h => h());
24
+ this.#ws.onmessage = (ev) => {
25
+ const msg = JSON.parse(ev.data);
26
+ if (msg.id && this.#pending.has(msg.id)) {
27
+ const { resolve, reject } = this.#pending.get(msg.id);
28
+ this.#pending.delete(msg.id);
29
+ if (msg.error) reject(new Error(msg.error.message));
30
+ else resolve(msg.result);
31
+ } else if (msg.method && this.#eventHandlers.has(msg.method)) {
32
+ for (const handler of [...this.#eventHandlers.get(msg.method)]) {
33
+ handler(msg.params || {}, msg);
34
+ }
35
+ }
36
+ };
37
+ });
38
+ }
39
+
40
+ send(method, params = {}, sessionId) {
41
+ const id = ++this.#id;
42
+ return new Promise((resolve, reject) => {
43
+ this.#pending.set(id, { resolve, reject });
44
+ const msg = { id, method, params };
45
+ if (sessionId) msg.sessionId = sessionId;
46
+ this.#ws.send(JSON.stringify(msg));
47
+ setTimeout(() => {
48
+ if (this.#pending.has(id)) {
49
+ this.#pending.delete(id);
50
+ reject(new Error(`Timeout: ${method}`));
51
+ }
52
+ }, this.#timeout);
53
+ });
54
+ }
55
+
56
+ onEvent(method, handler) {
57
+ if (!this.#eventHandlers.has(method)) this.#eventHandlers.set(method, new Set());
58
+ const handlers = this.#eventHandlers.get(method);
59
+ handlers.add(handler);
60
+ return () => {
61
+ handlers.delete(handler);
62
+ if (handlers.size === 0) this.#eventHandlers.delete(method);
63
+ };
64
+ }
65
+
66
+ waitForEvent(method, timeout = this.#timeout) {
67
+ let settled = false;
68
+ let off;
69
+ let timer;
70
+ const promise = new Promise((resolve, reject) => {
71
+ off = this.onEvent(method, (params) => {
72
+ if (settled) return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ off();
76
+ resolve(params);
77
+ });
78
+ timer = setTimeout(() => {
79
+ if (settled) return;
80
+ settled = true;
81
+ off();
82
+ reject(new Error(`Timeout waiting for event: ${method}`));
83
+ }, timeout);
84
+ });
85
+ return {
86
+ promise,
87
+ cancel() {
88
+ if (settled) return;
89
+ settled = true;
90
+ clearTimeout(timer);
91
+ off?.();
92
+ },
93
+ };
94
+ }
95
+
96
+ onClose(handler) { this.#closeHandlers.push(handler); }
97
+ close() { this.#ws.close(); }
98
+ }
@@ -0,0 +1,37 @@
1
+ // Captura de console.log/error/warn
2
+
3
+ import { sleep } from '../utils.mjs';
4
+
5
+ export async function consoleStr(cdp, sid, durationMs = 5000) {
6
+ const duration = parseInt(durationMs) || 5000;
7
+ const entries = [];
8
+
9
+ await cdp.send('Runtime.enable', {}, sid);
10
+
11
+ const off = cdp.onEvent('Runtime.consoleAPICalled', (params) => {
12
+ const type = params.type; // log, error, warn, info, debug
13
+ const args = (params.args || []).map(a => {
14
+ if (a.type === 'string') return a.value;
15
+ if (a.type === 'number') return String(a.value);
16
+ if (a.type === 'boolean') return String(a.value);
17
+ if (a.type === 'undefined') return 'undefined';
18
+ if (a.subtype === 'null') return 'null';
19
+ return a.description || JSON.stringify(a.value) || `[${a.type}]`;
20
+ });
21
+ entries.push({
22
+ ts: new Date().toISOString().slice(11, 23),
23
+ type,
24
+ msg: args.join(' '),
25
+ });
26
+ });
27
+
28
+ await sleep(duration);
29
+ off();
30
+
31
+ if (entries.length === 0) return `No console output captured in ${duration}ms.`;
32
+
33
+ return entries.map(e => {
34
+ const prefix = e.type === 'error' ? 'ERR' : e.type === 'warn' ? 'WRN' : e.type.toUpperCase().slice(0, 3);
35
+ return `[${e.ts}] ${prefix.padEnd(3)} ${e.msg.substring(0, 200)}`;
36
+ }).join('\n');
37
+ }
@@ -0,0 +1,77 @@
1
+ // Cookie management via CDP Network domain
2
+
3
+ import { evalStr } from './evaluate.mjs';
4
+
5
+ export async function cookiesStr(cdp, sid, action, arg) {
6
+ switch (action) {
7
+ case undefined:
8
+ case 'list': {
9
+ // Obter URL atual para filtrar cookies
10
+ const url = await evalStr(cdp, sid, 'window.location.href');
11
+ await cdp.send('Network.enable', {}, sid);
12
+ const { cookies } = await cdp.send('Network.getCookies', { urls: [url] }, sid);
13
+ await cdp.send('Network.disable', {}, sid);
14
+
15
+ if (cookies.length === 0) return 'No cookies found for this page.';
16
+
17
+ return cookies.map(c => {
18
+ const flags = [
19
+ c.httpOnly ? 'HttpOnly' : '',
20
+ c.secure ? 'Secure' : '',
21
+ c.sameSite !== 'None' ? `SameSite=${c.sameSite}` : '',
22
+ ].filter(Boolean).join(' ');
23
+ const exp = c.expires > 0
24
+ ? new Date(c.expires * 1000).toISOString().slice(0, 19)
25
+ : 'Session';
26
+ const val = c.value.length > 40 ? c.value.slice(0, 40) + '...' : c.value;
27
+ return `${c.name.padEnd(30)} ${val.padEnd(44)} ${exp} ${flags}`;
28
+ }).join('\n');
29
+ }
30
+
31
+ case 'set': {
32
+ if (!arg) throw new Error('Cookie JSON required: {"name":"x","value":"y","domain":".example.com"}');
33
+ let cookie;
34
+ try { cookie = JSON.parse(arg); }
35
+ catch { throw new Error(`Invalid JSON: ${arg}`); }
36
+
37
+ if (!cookie.name || cookie.value == null) throw new Error('Cookie must have "name" and "value"');
38
+
39
+ // Default domain da página atual
40
+ if (!cookie.domain) {
41
+ const host = await evalStr(cdp, sid, 'window.location.hostname');
42
+ cookie.domain = host;
43
+ }
44
+ if (!cookie.path) cookie.path = '/';
45
+
46
+ await cdp.send('Network.enable', {}, sid);
47
+ const result = await cdp.send('Network.setCookie', cookie, sid);
48
+ await cdp.send('Network.disable', {}, sid);
49
+
50
+ if (!result.success) throw new Error('Failed to set cookie');
51
+ return `Cookie "${cookie.name}" set on ${cookie.domain}`;
52
+ }
53
+
54
+ case 'clear': {
55
+ const url = await evalStr(cdp, sid, 'window.location.href');
56
+ await cdp.send('Network.enable', {}, sid);
57
+ const { cookies } = await cdp.send('Network.getCookies', { urls: [url] }, sid);
58
+
59
+ const domain = arg; // Filtro opcional por domínio
60
+ const toDelete = domain
61
+ ? cookies.filter(c => c.domain === domain || c.domain === '.' + domain)
62
+ : cookies;
63
+
64
+ for (const c of toDelete) {
65
+ await cdp.send('Network.deleteCookies', {
66
+ name: c.name, domain: c.domain, path: c.path,
67
+ }, sid);
68
+ }
69
+ await cdp.send('Network.disable', {}, sid);
70
+
71
+ return `Cleared ${toDelete.length} cookie(s)${domain ? ` for domain ${domain}` : ''}`;
72
+ }
73
+
74
+ default:
75
+ throw new Error(`Unknown cookies action: ${action}. Use: list, set, clear`);
76
+ }
77
+ }