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/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
+ }