glidercli 0.1.4 → 0.2.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.
- package/README.md +12 -7
- package/bin/glider.js +243 -3
- package/lib/bcdp.js +482 -0
- package/lib/beval.js +78 -0
- package/lib/bserve.js +311 -0
- package/package.json +8 -2
- package/.git-personal-enforced +0 -4
- package/.github/hooks/post-checkout +0 -24
- package/.github/hooks/pre-commit +0 -30
- package/.github/hooks/pre-push +0 -13
- package/.github/scripts/health-check.sh +0 -127
- package/.github/scripts/setup.sh +0 -19
- package/assets/icons/.gitkeep +0 -0
- package/repo.config.json +0 -31
package/lib/bcdp.js
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bcdp.js - Full CDP capabilities for browser automation
|
|
4
|
+
* Direct CDP scripting without external dependencies
|
|
5
|
+
*
|
|
6
|
+
* Capabilities:
|
|
7
|
+
* - evaluate: Run JS in page context
|
|
8
|
+
* - navigate: Go to URL
|
|
9
|
+
* - screenshot: Capture page
|
|
10
|
+
* - click: Click element by selector
|
|
11
|
+
* - type: Type text into element
|
|
12
|
+
* - scroll: Scroll page
|
|
13
|
+
* - wait: Wait for selector/navigation
|
|
14
|
+
* - dom: Query DOM elements
|
|
15
|
+
* - network: Intercept requests
|
|
16
|
+
* - cookies: Get/set cookies
|
|
17
|
+
* - storage: Get/set localStorage/sessionStorage
|
|
18
|
+
* - scripts: List/read/edit page scripts (live patching)
|
|
19
|
+
* - styles: Get computed styles
|
|
20
|
+
* - debug: Set breakpoints, inspect variables
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const WebSocket = require('ws');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
|
|
26
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
27
|
+
|
|
28
|
+
class BrowserCDP {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.ws = null;
|
|
31
|
+
this.messageId = 0;
|
|
32
|
+
this.pending = new Map();
|
|
33
|
+
this.sessionId = null;
|
|
34
|
+
this.targetId = null;
|
|
35
|
+
this.scripts = new Map();
|
|
36
|
+
this.eventHandlers = new Map();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async connect() {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.ws = new WebSocket(RELAY_URL);
|
|
42
|
+
this.ws.on('open', resolve);
|
|
43
|
+
this.ws.on('error', reject);
|
|
44
|
+
this.ws.on('message', (data) => this._handleMessage(JSON.parse(data.toString())));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_handleMessage(msg) {
|
|
49
|
+
if (msg.id !== undefined) {
|
|
50
|
+
const pending = this.pending.get(msg.id);
|
|
51
|
+
if (pending) {
|
|
52
|
+
this.pending.delete(msg.id);
|
|
53
|
+
msg.error ? pending.reject(new Error(msg.error.message)) : pending.resolve(msg.result);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Events
|
|
59
|
+
if (msg.method === 'Target.attachedToTarget') {
|
|
60
|
+
if (!this.sessionId) {
|
|
61
|
+
this.sessionId = msg.params.sessionId;
|
|
62
|
+
this.targetId = msg.params.targetInfo.targetId;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (msg.method === 'Debugger.scriptParsed') {
|
|
67
|
+
const { scriptId, url } = msg.params;
|
|
68
|
+
if (url && !url.startsWith('chrome') && !url.startsWith('devtools')) {
|
|
69
|
+
this.scripts.set(url, scriptId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Custom event handlers
|
|
74
|
+
const handlers = this.eventHandlers.get(msg.method);
|
|
75
|
+
if (handlers) handlers.forEach(h => h(msg.params));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
on(event, handler) {
|
|
79
|
+
if (!this.eventHandlers.has(event)) this.eventHandlers.set(event, new Set());
|
|
80
|
+
this.eventHandlers.get(event).add(handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
off(event, handler) {
|
|
84
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async send(method, params = {}, sessionId = null) {
|
|
88
|
+
const id = ++this.messageId;
|
|
89
|
+
const msg = { id, method, params };
|
|
90
|
+
if (sessionId || this.sessionId) msg.sessionId = sessionId || this.sessionId;
|
|
91
|
+
this.ws.send(JSON.stringify(msg));
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
this.pending.delete(id);
|
|
96
|
+
reject(new Error(`Timeout: ${method}`));
|
|
97
|
+
}, 30000);
|
|
98
|
+
this.pending.set(id, {
|
|
99
|
+
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
100
|
+
reject: (e) => { clearTimeout(timer); reject(e); }
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async init() {
|
|
106
|
+
await this.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true }, null);
|
|
107
|
+
await new Promise(r => setTimeout(r, 500));
|
|
108
|
+
if (!this.sessionId) throw new Error('No browser tab connected');
|
|
109
|
+
await this.send('Runtime.enable');
|
|
110
|
+
await this.send('Page.enable');
|
|
111
|
+
await this.send('DOM.enable');
|
|
112
|
+
await this.send('Network.enable');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
116
|
+
// CORE: Evaluate JS
|
|
117
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
118
|
+
async evaluate(expression, { returnByValue = true, awaitPromise = true } = {}) {
|
|
119
|
+
const result = await this.send('Runtime.evaluate', { expression, returnByValue, awaitPromise });
|
|
120
|
+
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text);
|
|
121
|
+
return result.result.value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
125
|
+
// NAVIGATION
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
127
|
+
async navigate(url, { waitUntil = 'load', timeout = 30000 } = {}) {
|
|
128
|
+
const loadPromise = new Promise(resolve => {
|
|
129
|
+
const handler = () => { this.off('Page.loadEventFired', handler); resolve(); };
|
|
130
|
+
this.on('Page.loadEventFired', handler);
|
|
131
|
+
setTimeout(() => { this.off('Page.loadEventFired', handler); resolve(); }, timeout);
|
|
132
|
+
});
|
|
133
|
+
await this.send('Page.navigate', { url });
|
|
134
|
+
if (waitUntil === 'load') await loadPromise;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async reload() {
|
|
138
|
+
await this.send('Page.reload');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async goBack() {
|
|
142
|
+
const history = await this.send('Page.getNavigationHistory');
|
|
143
|
+
if (history.currentIndex > 0) {
|
|
144
|
+
await this.send('Page.navigateToHistoryEntry', { entryId: history.entries[history.currentIndex - 1].id });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async goForward() {
|
|
149
|
+
const history = await this.send('Page.getNavigationHistory');
|
|
150
|
+
if (history.currentIndex < history.entries.length - 1) {
|
|
151
|
+
await this.send('Page.navigateToHistoryEntry', { entryId: history.entries[history.currentIndex + 1].id });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getUrl() {
|
|
156
|
+
return this.evaluate('window.location.href');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async getTitle() {
|
|
160
|
+
return this.evaluate('document.title');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
164
|
+
// SCREENSHOT
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
166
|
+
async screenshot({ path, format = 'png', quality = 80, fullPage = false } = {}) {
|
|
167
|
+
const params = { format };
|
|
168
|
+
if (format === 'jpeg') params.quality = quality;
|
|
169
|
+
if (fullPage) {
|
|
170
|
+
const metrics = await this.send('Page.getLayoutMetrics');
|
|
171
|
+
params.clip = { x: 0, y: 0, width: metrics.contentSize.width, height: metrics.contentSize.height, scale: 1 };
|
|
172
|
+
}
|
|
173
|
+
const result = await this.send('Page.captureScreenshot', params);
|
|
174
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
175
|
+
if (path) fs.writeFileSync(path, buffer);
|
|
176
|
+
return { data: result.data, buffer };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
180
|
+
// DOM INTERACTION
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
182
|
+
async click(selector, { button = 'left', clickCount = 1 } = {}) {
|
|
183
|
+
const box = await this.evaluate(`
|
|
184
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
185
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
186
|
+
const rect = el.getBoundingClientRect();
|
|
187
|
+
({ x: rect.x + rect.width/2, y: rect.y + rect.height/2 })
|
|
188
|
+
`);
|
|
189
|
+
await this.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: box.x, y: box.y, button, clickCount });
|
|
190
|
+
await this.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: box.x, y: box.y, button, clickCount });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async type(selector, text, { delay = 0 } = {}) {
|
|
194
|
+
await this.click(selector);
|
|
195
|
+
for (const char of text) {
|
|
196
|
+
await this.send('Input.dispatchKeyEvent', { type: 'keyDown', text: char });
|
|
197
|
+
await this.send('Input.dispatchKeyEvent', { type: 'keyUp', text: char });
|
|
198
|
+
if (delay) await new Promise(r => setTimeout(r, delay));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async fill(selector, value) {
|
|
203
|
+
await this.evaluate(`
|
|
204
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
205
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
206
|
+
el.value = ${JSON.stringify(value)};
|
|
207
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
208
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async select(selector, value) {
|
|
213
|
+
await this.evaluate(`
|
|
214
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
215
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
216
|
+
el.value = ${JSON.stringify(value)};
|
|
217
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async hover(selector) {
|
|
222
|
+
const box = await this.evaluate(`
|
|
223
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
224
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
225
|
+
const rect = el.getBoundingClientRect();
|
|
226
|
+
({ x: rect.x + rect.width/2, y: rect.y + rect.height/2 })
|
|
227
|
+
`);
|
|
228
|
+
await this.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: box.x, y: box.y });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async scroll({ x = 0, y = 0, selector } = {}) {
|
|
232
|
+
if (selector) {
|
|
233
|
+
await this.evaluate(`document.querySelector(${JSON.stringify(selector)})?.scrollIntoView({ behavior: 'smooth' })`);
|
|
234
|
+
} else {
|
|
235
|
+
await this.evaluate(`window.scrollBy(${x}, ${y})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async scrollToBottom() {
|
|
240
|
+
await this.evaluate('window.scrollTo(0, document.body.scrollHeight)');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async scrollToTop() {
|
|
244
|
+
await this.evaluate('window.scrollTo(0, 0)');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
248
|
+
// WAIT
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
250
|
+
async waitForSelector(selector, { timeout = 30000, visible = false } = {}) {
|
|
251
|
+
const start = Date.now();
|
|
252
|
+
while (Date.now() - start < timeout) {
|
|
253
|
+
const exists = await this.evaluate(`
|
|
254
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
255
|
+
${visible ? 'el && el.offsetParent !== null' : '!!el'}
|
|
256
|
+
`);
|
|
257
|
+
if (exists) return true;
|
|
258
|
+
await new Promise(r => setTimeout(r, 100));
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Timeout waiting for selector: ${selector}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async waitForNavigation({ timeout = 30000 } = {}) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const timer = setTimeout(() => reject(new Error('Navigation timeout')), timeout);
|
|
266
|
+
const handler = () => { clearTimeout(timer); this.off('Page.loadEventFired', handler); resolve(); };
|
|
267
|
+
this.on('Page.loadEventFired', handler);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async waitForNetwork({ timeout = 5000 } = {}) {
|
|
272
|
+
// Wait for network to be idle (no requests for timeout ms)
|
|
273
|
+
let lastActivity = Date.now();
|
|
274
|
+
const handler = () => { lastActivity = Date.now(); };
|
|
275
|
+
this.on('Network.requestWillBeSent', handler);
|
|
276
|
+
this.on('Network.responseReceived', handler);
|
|
277
|
+
|
|
278
|
+
while (Date.now() - lastActivity < timeout) {
|
|
279
|
+
await new Promise(r => setTimeout(r, 100));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.off('Network.requestWillBeSent', handler);
|
|
283
|
+
this.off('Network.responseReceived', handler);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
287
|
+
// COOKIES & STORAGE
|
|
288
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
289
|
+
async getCookies(urls) {
|
|
290
|
+
const result = await this.send('Network.getCookies', urls ? { urls } : {});
|
|
291
|
+
return result.cookies;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async setCookie(cookie) {
|
|
295
|
+
await this.send('Network.setCookie', cookie);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async deleteCookies(name, url) {
|
|
299
|
+
await this.send('Network.deleteCookies', { name, url });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async clearCookies() {
|
|
303
|
+
await this.send('Network.clearBrowserCookies');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async getLocalStorage() {
|
|
307
|
+
return this.evaluate('Object.fromEntries(Object.entries(localStorage))');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async setLocalStorage(key, value) {
|
|
311
|
+
await this.evaluate(`localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getSessionStorage() {
|
|
315
|
+
return this.evaluate('Object.fromEntries(Object.entries(sessionStorage))');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async setSessionStorage(key, value) {
|
|
319
|
+
await this.evaluate(`sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
323
|
+
// NETWORK INTERCEPTION
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
325
|
+
async setRequestInterception(patterns) {
|
|
326
|
+
await this.send('Fetch.enable', { patterns: patterns.map(p => ({ urlPattern: p })) });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async continueRequest(requestId, { url, method, headers, postData } = {}) {
|
|
330
|
+
await this.send('Fetch.continueRequest', { requestId, url, method, headers, postData });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async fulfillRequest(requestId, { responseCode = 200, responseHeaders = [], body }) {
|
|
334
|
+
const encodedBody = body ? Buffer.from(body).toString('base64') : undefined;
|
|
335
|
+
await this.send('Fetch.fulfillRequest', { requestId, responseCode, responseHeaders, body: encodedBody });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async failRequest(requestId, reason = 'Failed') {
|
|
339
|
+
await this.send('Fetch.failRequest', { requestId, errorReason: reason });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
343
|
+
// SCRIPTS (Live Editing - like playwriter Editor)
|
|
344
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
345
|
+
async enableDebugger() {
|
|
346
|
+
await this.send('Debugger.enable');
|
|
347
|
+
// Wait for scripts to be parsed
|
|
348
|
+
await new Promise(r => setTimeout(r, 200));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async listScripts(search) {
|
|
352
|
+
await this.enableDebugger();
|
|
353
|
+
const scripts = Array.from(this.scripts.entries()).map(([url, id]) => ({ url, scriptId: id }));
|
|
354
|
+
return search ? scripts.filter(s => s.url.toLowerCase().includes(search.toLowerCase())) : scripts;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async getScriptSource(urlOrId) {
|
|
358
|
+
await this.enableDebugger();
|
|
359
|
+
const scriptId = this.scripts.get(urlOrId) || urlOrId;
|
|
360
|
+
const result = await this.send('Debugger.getScriptSource', { scriptId });
|
|
361
|
+
return result.scriptSource;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async setScriptSource(urlOrId, newSource) {
|
|
365
|
+
await this.enableDebugger();
|
|
366
|
+
const scriptId = this.scripts.get(urlOrId) || urlOrId;
|
|
367
|
+
const result = await this.send('Debugger.setScriptSource', { scriptId, scriptSource: newSource });
|
|
368
|
+
return { success: true, stackChanged: result.stackChanged };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async editScript(urlOrId, oldString, newString) {
|
|
372
|
+
const source = await this.getScriptSource(urlOrId);
|
|
373
|
+
const count = (source.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
|
374
|
+
if (count === 0) throw new Error('oldString not found');
|
|
375
|
+
if (count > 1) throw new Error(`oldString found ${count} times - make it unique`);
|
|
376
|
+
const newSource = source.replace(oldString, newString);
|
|
377
|
+
return this.setScriptSource(urlOrId, newSource);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
381
|
+
// DEBUGGING (like playwriter Debugger)
|
|
382
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
383
|
+
async setBreakpoint(url, line, condition) {
|
|
384
|
+
await this.enableDebugger();
|
|
385
|
+
const result = await this.send('Debugger.setBreakpointByUrl', {
|
|
386
|
+
lineNumber: line - 1,
|
|
387
|
+
urlRegex: url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
|
388
|
+
condition
|
|
389
|
+
});
|
|
390
|
+
return result.breakpointId;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async removeBreakpoint(breakpointId) {
|
|
394
|
+
await this.send('Debugger.removeBreakpoint', { breakpointId });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async resume() {
|
|
398
|
+
await this.send('Debugger.resume');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async stepOver() {
|
|
402
|
+
await this.send('Debugger.stepOver');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async stepInto() {
|
|
406
|
+
await this.send('Debugger.stepInto');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async stepOut() {
|
|
410
|
+
await this.send('Debugger.stepOut');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async pause() {
|
|
414
|
+
await this.send('Debugger.pause');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
418
|
+
// FETCH (using browser's authenticated session)
|
|
419
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
420
|
+
async fetch(url, options = {}) {
|
|
421
|
+
const script = `
|
|
422
|
+
(async () => {
|
|
423
|
+
const response = await fetch(${JSON.stringify(url)}, {
|
|
424
|
+
credentials: 'include',
|
|
425
|
+
...${JSON.stringify(options)}
|
|
426
|
+
});
|
|
427
|
+
const contentType = response.headers.get('content-type') || '';
|
|
428
|
+
let body;
|
|
429
|
+
if (contentType.includes('application/json')) {
|
|
430
|
+
body = await response.json();
|
|
431
|
+
} else {
|
|
432
|
+
body = await response.text();
|
|
433
|
+
}
|
|
434
|
+
return { status: response.status, contentType, body, headers: Object.fromEntries(response.headers) };
|
|
435
|
+
})()
|
|
436
|
+
`;
|
|
437
|
+
return this.evaluate(script);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
441
|
+
// UTILITIES
|
|
442
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
443
|
+
async getHTML(selector) {
|
|
444
|
+
if (selector) {
|
|
445
|
+
return this.evaluate(`document.querySelector(${JSON.stringify(selector)})?.outerHTML`);
|
|
446
|
+
}
|
|
447
|
+
return this.evaluate('document.documentElement.outerHTML');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async getText(selector) {
|
|
451
|
+
return this.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async getAttribute(selector, attr) {
|
|
455
|
+
return this.evaluate(`document.querySelector(${JSON.stringify(selector)})?.getAttribute(${JSON.stringify(attr)})`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async querySelectorAll(selector) {
|
|
459
|
+
return this.evaluate(`Array.from(document.querySelectorAll(${JSON.stringify(selector)})).map(el => ({
|
|
460
|
+
tag: el.tagName.toLowerCase(),
|
|
461
|
+
id: el.id,
|
|
462
|
+
class: el.className,
|
|
463
|
+
text: el.textContent?.slice(0, 100),
|
|
464
|
+
href: el.href,
|
|
465
|
+
src: el.src
|
|
466
|
+
}))`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async pdf({ path, format = 'A4', printBackground = true } = {}) {
|
|
470
|
+
const result = await this.send('Page.printToPDF', { format, printBackground });
|
|
471
|
+
const buffer = Buffer.from(result.data, 'base64');
|
|
472
|
+
if (path) fs.writeFileSync(path, buffer);
|
|
473
|
+
return { data: result.data, buffer };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
close() {
|
|
477
|
+
if (this.ws) this.ws.close();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Export for programmatic use
|
|
482
|
+
module.exports = { BrowserCDP };
|
package/lib/beval.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Quick script to run JS in connected browser tab
|
|
3
|
+
const WebSocket = require('ws');
|
|
4
|
+
|
|
5
|
+
const RELAY_URL = 'ws://127.0.0.1:19988/cdp';
|
|
6
|
+
|
|
7
|
+
async function evaluate(script) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const ws = new WebSocket(RELAY_URL);
|
|
10
|
+
let sessionId = null;
|
|
11
|
+
let msgId = 0;
|
|
12
|
+
const pending = new Map();
|
|
13
|
+
|
|
14
|
+
ws.on('open', () => {
|
|
15
|
+
ws.send(JSON.stringify({ id: ++msgId, method: 'Target.setAutoAttach', params: { autoAttach: true, waitForDebuggerOnStart: false, flatten: true } }));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
ws.on('error', reject);
|
|
19
|
+
|
|
20
|
+
ws.on('message', async (data) => {
|
|
21
|
+
const msg = JSON.parse(data.toString());
|
|
22
|
+
|
|
23
|
+
if (msg.method === 'Target.attachedToTarget') {
|
|
24
|
+
sessionId = msg.params.sessionId;
|
|
25
|
+
// Enable Runtime
|
|
26
|
+
ws.send(JSON.stringify({ id: ++msgId, method: 'Runtime.enable', params: {}, sessionId }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (msg.id && pending.has(msg.id)) {
|
|
30
|
+
pending.get(msg.id)(msg);
|
|
31
|
+
pending.delete(msg.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// After Runtime.enable, run our script
|
|
35
|
+
if (msg.id === 2 && sessionId) {
|
|
36
|
+
const evalId = ++msgId;
|
|
37
|
+
ws.send(JSON.stringify({
|
|
38
|
+
id: evalId,
|
|
39
|
+
method: 'Runtime.evaluate',
|
|
40
|
+
params: { expression: script, returnByValue: true, awaitPromise: true },
|
|
41
|
+
sessionId
|
|
42
|
+
}));
|
|
43
|
+
pending.set(evalId, (result) => {
|
|
44
|
+
ws.close();
|
|
45
|
+
if (result.result?.result?.value !== undefined) {
|
|
46
|
+
resolve(result.result.result.value);
|
|
47
|
+
} else if (result.result?.exceptionDetails) {
|
|
48
|
+
reject(new Error(result.result.exceptionDetails.text));
|
|
49
|
+
} else {
|
|
50
|
+
resolve(result);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
ws.close();
|
|
58
|
+
reject(new Error('Timeout'));
|
|
59
|
+
}, 10000);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Export for programmatic use
|
|
64
|
+
module.exports = { evaluate };
|
|
65
|
+
|
|
66
|
+
// CLI mode
|
|
67
|
+
if (require.main === module) {
|
|
68
|
+
const script = process.argv[2] || 'document.title';
|
|
69
|
+
evaluate(script)
|
|
70
|
+
.then(result => {
|
|
71
|
+
console.log(JSON.stringify(result, null, 2));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
})
|
|
74
|
+
.catch(err => {
|
|
75
|
+
console.error('Error:', err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
78
|
+
}
|