glidercli 0.3.1 β 0.3.3
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 +39 -25
- package/bin/glider.js +443 -1
- package/lib/bfavicon.js +252 -0
- package/lib/bserve.js +15 -0
- package/lib/registry.json +176 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,45 +15,56 @@
|
|
|
15
15
|
|
|
16
16
|
<br/>
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## ToC
|
|
19
19
|
|
|
20
20
|
<ol>
|
|
21
|
-
<a href="#about"
|
|
22
|
-
<a href="#install"
|
|
23
|
-
<a href="#usage"
|
|
24
|
-
<a href="#the-loop"
|
|
25
|
-
<a href="#task-files"
|
|
26
|
-
<a href="#commands"
|
|
27
|
-
<a href="#roadmap"
|
|
28
|
-
<a href="#tools-used"
|
|
29
|
-
<a href="#contact"
|
|
21
|
+
<a href="#about">About</a><br/>
|
|
22
|
+
<a href="#install">Install</a><br/>
|
|
23
|
+
<a href="#usage">Usage</a><br/>
|
|
24
|
+
<a href="#the-loop">The loop</a><br/>
|
|
25
|
+
<a href="#task-files">Task files</a><br/>
|
|
26
|
+
<a href="#commands">Commands</a><br/>
|
|
27
|
+
<a href="#roadmap">Roadmap</a><br/>
|
|
28
|
+
<a href="#tools-used">Tools</a><br/>
|
|
29
|
+
<a href="#contact">Contact</a>
|
|
30
30
|
</ol>
|
|
31
31
|
|
|
32
32
|
<br/>
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## About
|
|
35
35
|
|
|
36
36
|
Control Chrome from terminal. Run YAML tasks. Loop until complete (Ralph Wiggum pattern).
|
|
37
37
|
|
|
38
|
-
- **CDP-based** - Direct Chrome DevTools Protocol control
|
|
38
|
+
- **CDP-based** - Direct Chrome DevTools Protocol (CDP) control
|
|
39
39
|
- **YAML tasks** - Define automation steps declaratively
|
|
40
40
|
- **Autonomous loops** - Run until completion marker found
|
|
41
41
|
- **Safety guards** - Max iterations, timeout, exponential backoff
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## Install
|
|
44
44
|
|
|
45
|
+
**One-liner:**
|
|
46
|
+
```bash
|
|
47
|
+
npm i -g glidercli && open "https://chromewebstore.google.com/detail/glider/njbidokkffhgpofcejgcfcgcinmeoalj"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Then:**
|
|
45
51
|
```bash
|
|
46
|
-
npm i -g glidercli
|
|
47
52
|
glider install # start daemon (runs forever, auto-restarts)
|
|
53
|
+
glider connect # connect to Chrome
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Update anytime:**
|
|
57
|
+
```bash
|
|
58
|
+
glider update # pulls latest from npm
|
|
48
59
|
```
|
|
49
60
|
|
|
50
61
|
### Requirements
|
|
51
62
|
|
|
52
63
|
1. **Node 18+**
|
|
53
64
|
|
|
54
|
-
2. **Glider Chrome
|
|
65
|
+
2. **Glider Chrome extension** - [Install from Chrome Web Store](https://chromewebstore.google.com/detail/glider/njbidokkffhgpofcejgcfcgcinmeoalj)
|
|
55
66
|
|
|
56
|
-
##
|
|
67
|
+
## Usage
|
|
57
68
|
|
|
58
69
|
```bash
|
|
59
70
|
glider connect # connect to browser
|
|
@@ -75,7 +86,7 @@ glider uninstall # remove daemon
|
|
|
75
86
|
|
|
76
87
|
Logs: `~/.glider/daemon.log`
|
|
77
88
|
|
|
78
|
-
##
|
|
89
|
+
## The loop
|
|
79
90
|
|
|
80
91
|
The `loop` (or `ralph`) command runs your task repeatedly until:
|
|
81
92
|
- Completion marker found (`LOOP_COMPLETE` or `DONE`)
|
|
@@ -89,7 +100,7 @@ glider ralph task.yaml # same thing
|
|
|
89
100
|
|
|
90
101
|
Safety: max iterations, timeout, exponential backoff on errors, state persistence.
|
|
91
102
|
|
|
92
|
-
##
|
|
103
|
+
## Task files
|
|
93
104
|
|
|
94
105
|
```yaml
|
|
95
106
|
name: "Get timeline"
|
|
@@ -100,13 +111,14 @@ steps:
|
|
|
100
111
|
- screenshot: "/tmp/timeline.png"
|
|
101
112
|
```
|
|
102
113
|
|
|
103
|
-
##
|
|
114
|
+
## Commands
|
|
104
115
|
|
|
105
116
|
### Setup
|
|
106
117
|
| Command | What |
|
|
107
118
|
|---------|------|
|
|
108
119
|
| `glider install` | Install daemon (runs at login) |
|
|
109
120
|
| `glider uninstall` | Remove daemon |
|
|
121
|
+
| `glider update` | Update to latest version |
|
|
110
122
|
| `glider connect` | Connect to browser |
|
|
111
123
|
| `glider status` | Server/extension/tab status |
|
|
112
124
|
| `glider test` | Run diagnostics |
|
|
@@ -123,7 +135,7 @@ steps:
|
|
|
123
135
|
| `glider title` | Get page title |
|
|
124
136
|
| `glider text` | Get page text |
|
|
125
137
|
|
|
126
|
-
### Multi-
|
|
138
|
+
### Multi-tab
|
|
127
139
|
| Command | What |
|
|
128
140
|
|---------|------|
|
|
129
141
|
| `glider fetch <url>` | Fetch URL with browser session (authenticated) |
|
|
@@ -138,7 +150,7 @@ steps:
|
|
|
138
150
|
| `glider loop <file>` | Autonomous loop |
|
|
139
151
|
| `glider ralph <file>` | Alias for loop |
|
|
140
152
|
|
|
141
|
-
##
|
|
153
|
+
## Roadmap
|
|
142
154
|
|
|
143
155
|
- [x] CDP-based browser control via relay
|
|
144
156
|
- [x] YAML task file execution
|
|
@@ -158,17 +170,19 @@ steps:
|
|
|
158
170
|
- [ ] AI-assisted task generation
|
|
159
171
|
- [ ] Web dashboard for monitoring loops
|
|
160
172
|
|
|
161
|
-
##
|
|
173
|
+
## Tools
|
|
162
174
|
|
|
163
175
|
[![Claude Code][claudecode-badge]][claudecode-url]
|
|
164
176
|
[![Claude][claude-badge]][claude-url]
|
|
165
177
|
[![Node.js][nodejs-badge]][nodejs-url]
|
|
166
178
|
[![Chrome DevTools Protocol][cdp-badge]][cdp-url]
|
|
167
179
|
|
|
168
|
-
##
|
|
180
|
+
## Contact
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
<a href="https://vd7.io"><img src="https://img.shields.io/badge/website-000000?style=for-the-badge&logo=data:image/webp;base64,UklGRjAGAABXRUJQVlA4TCQGAAAvP8APEAHFbdtGsOVnuv/A6T1BRP8nQE8zgZUy0U4ktpT4QOHIJzqqDwxnbIyyAzADbAegMbO2BwratpHMH/f+OwChqG0jKXPuPsMf2cJYCP2fAMQe4OKTZIPEb9mq+y3dISZBN7Jt1bYz5rqfxQwWeRiBbEWgABQfm9+UrxiYWfLw3rtn1Tlrrb3vJxtyJEmKJM+lYyb9hbv3Mt91zj8l2rZN21WPbdu2bdsp2XZSsm3btm3bybfNZ+M4lGylbi55EIQLTcH2GyAFeHDJJ6+z//uviigx/hUxuTSVzqSMIdERGfypiZ8OfPnU1reQeKfxvhl8r/V5oj3VzJQ3qbo6RLh4BjevcBE+30F8eL/GcWI01ddkE1IFhmAAA+xPQATifcTO08J+CL8z+OBpEw+zTGuTYteMrhTDAPtVhCg2X5lYDf9fjg+fl/GwkupiUhBSBUUFLukjJFpD/C8W/rWR5kLYlB8/mGzmOzIKyTK5A4MCjKxAv2celbsItx/lUrRTZAT5NITMV3iL0cUAAGI0MRF2rONYBRRlhICQubO1P42kGC7AOMTWV7fSrEKRQ5UzsJ/5UtXWKy9tca6iP5FmDQeCiFQBQQgUfsEAQl1LLLWCAWAAISL17ySvICqUShDAZHV6MYyScQAIggh7j/g5/uevIHzz6A6FXI0LgdJ4g2oCAUFQfQfJM7xvKvGtsMle79ylhLsUx/QChEAQHCaezHD76fSAICgIIGuTJaMbIJfSfAEBCME/V4bnPa5yLoiOEEEoqx1JqrZ/SK1nZApxF/7sAF8r7oD03CorvVesxRAIgits66BaKWyy4FJCctC0e7eAiFef7dytgLviriDkS6lXWHOsDZgeDUEAwYJKeIXpIsiXGUNeEfb1Nk+yZIPrHpwvEDs3C0EhuwhgmdQoBKOAqpjAjMn41PQiVGG3CDlwCc0AGXX8s0Eshc8JPGkNhGJeDexYOudRdiX4+p2tGTvgothaMJs7wchxk9CBMoLZPQhGdIZgA4yGL7JvvhkpYK3xOq86xYIZAd9sCBqJZAA2ln5ldu8CSwEDRRFgF+wEAEKoZoW/8jY05bE3ds2f4uA5DAMAiNIBAYDGXDL0O78AjKlWRg+Y/9/eyL0tKIoUaxtIyKDUFQKgtJZKPmBAMgvZIQKAIJcQKFqGQjf2FELTAy6TnzADZLsnisNPABAZhU1LB6FpugmnUJ0oNedA3QPPVR6+AiBIXbgIAgDCdO7axjeEpLnk9k2nkKgPQ3zV5vvWrkx/wcrcpFT75QrBBibCq1aolkensxvZsN/0L2KDh79aTehXhPnoTggpBgiY+J8PIjdcmfpBofGokzMNMJY619i/AvEH2DD+fNlqCfVUcBEINS0FGPVuNPkE1+cdY+ebIKJqXQhBMBZMAkj7Xn91vN0BCfAC5J5PyHm71ptJJm3m7lCPUiHBTdBdCJlk0gAGEJroomQTxF2feZ4wJi4Y+9FqQoO1/ceoCoC7IOGtpU/m446s5TwXPTQxLgCcOZEBATG1zlfbeUJGcehbv9m6IPzaxLVSxGCPiEg7ThvWYPFehhc2gAIIEdsFob9Nx19YnR0Tf6IcqHIaVhDhhHbHFJa9p6Pj2gJjGsBfZrEAwNQ02UHAyuYLIeNPefgbNPL12lp4n/9uTSKERl3bwKmpAHSAuBODTNzk/1qXSqj2GljiqMsvr50CvcCbM5OSraOuTMJq28Fv48+waTWvrqQ0+8tIC0LxCFzgDAyIOdFqoZbPSUvkL9yB5JFDW682QhBpGAqAFfn7R2pV2u5zBoqlzpHRt78hXCETWJPjVHDiPJit5GQLYmJMNFiVr1bSnGOlCXIdkyyFpcHgtzH0BusCiQzPRUifr61BoW5aAvHxyI/gIjnOPB6chcCYHsJuEQogBM689OtvcKFAytNEB/N26qXQvQITd2a3ruZCMrgUcBVqvLiS6lR9Bi8gaNBrJtIc/GdYDj+AOyQPV61D9BfdguJCft31hHjzyBz7dzgOIeAOymsrKb59V+FKtYyqa6pGlIrKpEiRvk3zt+sL4jX1+G/uQii4C/LBSsp3n2V/NHIchtQAeC7K9/6DGHAPCwA=&logoColor=white" alt="website" /></a>
|
|
184
|
+
<a href="https://x.com/vdutts7"><img src="https://img.shields.io/badge/vdutts7-000000?style=for-the-badge&logo=X&logoColor=white" alt="Twitter" /></a>
|
|
169
185
|
|
|
170
|
-
[![Email][email]][email-url]
|
|
171
|
-
[![Twitter][twitter]][twitter-url]
|
|
172
186
|
|
|
173
187
|
<!-- BADGES -->
|
|
174
188
|
[github]: https://img.shields.io/badge/glidercli-000000?style=for-the-badge&logo=github
|
package/bin/glider.js
CHANGED
|
@@ -37,6 +37,15 @@ const DEBUG_URL = `http://127.0.0.1:${DEBUG_PORT}`;
|
|
|
37
37
|
const LIB_DIR = path.join(__dirname, '..', 'lib');
|
|
38
38
|
const STATE_FILE = '/tmp/glider-state.json';
|
|
39
39
|
const LOG_FILE = '/tmp/glider.log';
|
|
40
|
+
const REGISTRY_FILE = path.join(LIB_DIR, 'registry.json');
|
|
41
|
+
|
|
42
|
+
// Load pattern registry
|
|
43
|
+
let REGISTRY = {};
|
|
44
|
+
if (fs.existsSync(REGISTRY_FILE)) {
|
|
45
|
+
try {
|
|
46
|
+
REGISTRY = JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
|
|
47
|
+
} catch (e) { /* ignore parse errors */ }
|
|
48
|
+
}
|
|
40
49
|
|
|
41
50
|
// Direct CDP module
|
|
42
51
|
const { DirectCDP, checkChrome } = require(path.join(LIB_DIR, 'cdp-direct.js'));
|
|
@@ -200,6 +209,91 @@ async function getTargets() {
|
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
// Auto-connect helper - ensures Chrome is running and connected before commands
|
|
213
|
+
async function ensureConnected() {
|
|
214
|
+
// Check if already connected
|
|
215
|
+
if (await checkTab()) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check if server is running
|
|
220
|
+
if (!await checkServer()) {
|
|
221
|
+
log.info('Server not running, starting...');
|
|
222
|
+
await cmdStart();
|
|
223
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if Chrome is running
|
|
227
|
+
try {
|
|
228
|
+
execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
|
|
229
|
+
} catch {
|
|
230
|
+
log.info('Chrome not running, launching...');
|
|
231
|
+
// Open Chrome with a new window to google.com
|
|
232
|
+
execSync('open -na "Google Chrome" --args --new-window "https://www.google.com"');
|
|
233
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Wait for extension to connect
|
|
237
|
+
for (let i = 0; i < 10; i++) {
|
|
238
|
+
if (await checkExtension()) break;
|
|
239
|
+
await new Promise(r => setTimeout(r, 500));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!await checkExtension()) {
|
|
243
|
+
log.fail('Extension not connected - make sure Glider extension is installed');
|
|
244
|
+
log.info('Install from: chrome://extensions β Load unpacked β ~/glider-crx/glider/');
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check if we have tabs now
|
|
249
|
+
if (await checkTab()) {
|
|
250
|
+
log.ok('Auto-connected to existing tab');
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Need to create/attach to a tab
|
|
255
|
+
try {
|
|
256
|
+
const tabUrl = execSync(`osascript -e 'tell application "Google Chrome" to return URL of active tab of front window'`).toString().trim();
|
|
257
|
+
if (tabUrl.startsWith('chrome://') || tabUrl.startsWith('chrome-extension://')) {
|
|
258
|
+
log.info('Creating new tab (current is chrome://)...');
|
|
259
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new tab at front window with properties {URL:"https://www.google.com"}'`);
|
|
260
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// No window exists, create one
|
|
264
|
+
log.info('Creating new Chrome window...');
|
|
265
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new window with properties {URL:"https://www.google.com"}'`);
|
|
266
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Trigger attach via HTTP
|
|
270
|
+
try {
|
|
271
|
+
const result = await fetch(`${SERVER_URL}/attach`, { method: 'POST' });
|
|
272
|
+
const data = await result.json();
|
|
273
|
+
if (data.attached > 0) {
|
|
274
|
+
log.ok('Auto-connected!');
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
|
|
279
|
+
// Final fallback - create fresh tab
|
|
280
|
+
log.info('Creating fresh tab...');
|
|
281
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new tab at front window with properties {URL:"https://www.google.com"}'`);
|
|
282
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await fetch(`${SERVER_URL}/attach`, { method: 'POST' });
|
|
286
|
+
const data = await result.json();
|
|
287
|
+
if (data.attached > 0) {
|
|
288
|
+
log.ok('Auto-connected!');
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
|
|
293
|
+
log.fail('Could not auto-connect');
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
203
297
|
// Commands
|
|
204
298
|
async function cmdStatus() {
|
|
205
299
|
showBanner();
|
|
@@ -278,6 +372,11 @@ async function cmdGoto(url) {
|
|
|
278
372
|
process.exit(1);
|
|
279
373
|
}
|
|
280
374
|
|
|
375
|
+
// Auto-connect if not connected
|
|
376
|
+
if (!await ensureConnected()) {
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
281
380
|
log.info(`Navigating to: ${url}`);
|
|
282
381
|
|
|
283
382
|
try {
|
|
@@ -299,6 +398,11 @@ async function cmdEval(js) {
|
|
|
299
398
|
process.exit(1);
|
|
300
399
|
}
|
|
301
400
|
|
|
401
|
+
// Auto-connect if not connected
|
|
402
|
+
if (!await ensureConnected()) {
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
302
406
|
try {
|
|
303
407
|
const result = await httpPost('/cdp', {
|
|
304
408
|
method: 'Runtime.evaluate',
|
|
@@ -328,6 +432,11 @@ async function cmdClick(selector) {
|
|
|
328
432
|
process.exit(1);
|
|
329
433
|
}
|
|
330
434
|
|
|
435
|
+
// Auto-connect if not connected
|
|
436
|
+
if (!await ensureConnected()) {
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
331
440
|
const js = `
|
|
332
441
|
(() => {
|
|
333
442
|
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
@@ -360,6 +469,11 @@ async function cmdType(selector, text) {
|
|
|
360
469
|
process.exit(1);
|
|
361
470
|
}
|
|
362
471
|
|
|
472
|
+
// Auto-connect if not connected
|
|
473
|
+
if (!await ensureConnected()) {
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
363
477
|
const js = `
|
|
364
478
|
(() => {
|
|
365
479
|
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
@@ -391,6 +505,11 @@ async function cmdType(selector, text) {
|
|
|
391
505
|
async function cmdScreenshot(outputPath) {
|
|
392
506
|
const filePath = outputPath || `/tmp/glider-screenshot-${Date.now()}.png`;
|
|
393
507
|
|
|
508
|
+
// Auto-connect if not connected
|
|
509
|
+
if (!await ensureConnected()) {
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
|
|
394
513
|
try {
|
|
395
514
|
const result = await httpPost('/cdp', {
|
|
396
515
|
method: 'Page.captureScreenshot',
|
|
@@ -411,6 +530,11 @@ async function cmdScreenshot(outputPath) {
|
|
|
411
530
|
}
|
|
412
531
|
|
|
413
532
|
async function cmdText() {
|
|
533
|
+
// Auto-connect if not connected
|
|
534
|
+
if (!await ensureConnected()) {
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
|
|
414
538
|
try {
|
|
415
539
|
const result = await httpPost('/cdp', {
|
|
416
540
|
method: 'Runtime.evaluate',
|
|
@@ -803,6 +927,11 @@ async function cmdOpen(url) {
|
|
|
803
927
|
}
|
|
804
928
|
|
|
805
929
|
async function cmdHtml(selector) {
|
|
930
|
+
// Auto-connect if not connected
|
|
931
|
+
if (!await ensureConnected()) {
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
|
|
806
935
|
try {
|
|
807
936
|
const expression = selector
|
|
808
937
|
? `document.querySelector('${selector.replace(/'/g, "\\'")}')?.outerHTML || 'Element not found'`
|
|
@@ -820,6 +949,11 @@ async function cmdHtml(selector) {
|
|
|
820
949
|
}
|
|
821
950
|
|
|
822
951
|
async function cmdTitle() {
|
|
952
|
+
// Auto-connect if not connected
|
|
953
|
+
if (!await ensureConnected()) {
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
|
|
823
957
|
try {
|
|
824
958
|
const result = await httpPost('/cdp', {
|
|
825
959
|
method: 'Runtime.evaluate',
|
|
@@ -833,6 +967,11 @@ async function cmdTitle() {
|
|
|
833
967
|
}
|
|
834
968
|
|
|
835
969
|
async function cmdUrl() {
|
|
970
|
+
// Auto-connect if not connected
|
|
971
|
+
if (!await ensureConnected()) {
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
|
|
836
975
|
try {
|
|
837
976
|
const result = await httpPost('/cdp', {
|
|
838
977
|
method: 'Runtime.evaluate',
|
|
@@ -892,6 +1031,58 @@ async function cmdFetch(url, opts = []) {
|
|
|
892
1031
|
}
|
|
893
1032
|
}
|
|
894
1033
|
|
|
1034
|
+
// CORS-bypassing fetch via extension context
|
|
1035
|
+
async function cmdCorsFetch(url, opts = []) {
|
|
1036
|
+
if (!url) {
|
|
1037
|
+
log.fail('Usage: glider cfetch <url> [--output file] [--method POST] [--body JSON]');
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
log.info(`CORS Fetch: ${url}`);
|
|
1042
|
+
|
|
1043
|
+
let outputFile = null;
|
|
1044
|
+
let method = 'GET';
|
|
1045
|
+
let body = null;
|
|
1046
|
+
|
|
1047
|
+
for (let i = 0; i < opts.length; i++) {
|
|
1048
|
+
if (opts[i] === '--output' || opts[i] === '-o') {
|
|
1049
|
+
outputFile = opts[++i];
|
|
1050
|
+
} else if (opts[i] === '--method' || opts[i] === '-X') {
|
|
1051
|
+
method = opts[++i];
|
|
1052
|
+
} else if (opts[i] === '--body' || opts[i] === '-d') {
|
|
1053
|
+
body = opts[++i];
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
try {
|
|
1058
|
+
const result = await httpPost('/extension', {
|
|
1059
|
+
method: 'corsFetch',
|
|
1060
|
+
params: {
|
|
1061
|
+
url,
|
|
1062
|
+
options: { method, body, headers: { 'Accept': 'application/json' } }
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
if (result?.error) {
|
|
1067
|
+
log.fail(`Fetch error: ${result.error}`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const data = result?.result?.data;
|
|
1072
|
+
const output = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
|
|
1073
|
+
|
|
1074
|
+
if (outputFile) {
|
|
1075
|
+
fs.writeFileSync(outputFile, output);
|
|
1076
|
+
log.ok(`Saved to ${outputFile} (status: ${result?.result?.status})`);
|
|
1077
|
+
} else {
|
|
1078
|
+
console.log(output);
|
|
1079
|
+
}
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
log.fail(`CORS Fetch failed: ${e.message}`);
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
895
1086
|
// Spawn multiple tabs
|
|
896
1087
|
async function cmdSpawn(urls) {
|
|
897
1088
|
if (!urls || urls.length === 0) {
|
|
@@ -982,6 +1173,164 @@ async function cmdExtract(opts = []) {
|
|
|
982
1173
|
}
|
|
983
1174
|
}
|
|
984
1175
|
|
|
1176
|
+
// Registry pattern execution - bulletproof extraction using predefined patterns
|
|
1177
|
+
async function cmdRegistry(patternName, opts = []) {
|
|
1178
|
+
if (!patternName) {
|
|
1179
|
+
// List all patterns
|
|
1180
|
+
const patterns = Object.keys(REGISTRY);
|
|
1181
|
+
if (patterns.length === 0) {
|
|
1182
|
+
log.warn('No patterns in registry');
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
console.log(`${GREEN}${patterns.length}${NC} pattern(s) available:\n`);
|
|
1186
|
+
for (const name of patterns) {
|
|
1187
|
+
const p = REGISTRY[name];
|
|
1188
|
+
console.log(` ${CYAN}${name}${NC}`);
|
|
1189
|
+
console.log(` ${DIM}${p.description || 'No description'}${NC}`);
|
|
1190
|
+
}
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const pattern = REGISTRY[patternName];
|
|
1195
|
+
if (!pattern) {
|
|
1196
|
+
log.fail(`Pattern not found: ${patternName}`);
|
|
1197
|
+
log.info('Run "glider registry" to see available patterns');
|
|
1198
|
+
process.exit(1);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Parse options - for favicon: glider favicon [output.webp]
|
|
1202
|
+
// The first arg that looks like a file path is output, anything else is URL
|
|
1203
|
+
let outputFile = null;
|
|
1204
|
+
let url = null;
|
|
1205
|
+
for (let i = 0; i < opts.length; i++) {
|
|
1206
|
+
const arg = opts[i];
|
|
1207
|
+
if (arg === '--output' || arg === '-o') {
|
|
1208
|
+
outputFile = opts[++i];
|
|
1209
|
+
} else if (arg.startsWith('-')) {
|
|
1210
|
+
// skip flags
|
|
1211
|
+
} else if (arg.includes('/') && !arg.startsWith('http') && (arg.endsWith('.webp') || arg.endsWith('.png') || arg.endsWith('.ico'))) {
|
|
1212
|
+
// Looks like a file path
|
|
1213
|
+
outputFile = arg;
|
|
1214
|
+
} else if (!url) {
|
|
1215
|
+
url = arg;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// If URL provided, navigate first
|
|
1220
|
+
if (url) {
|
|
1221
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
1222
|
+
log.info(`Navigating to: ${url}`);
|
|
1223
|
+
await cmdGoto(url);
|
|
1224
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
log.info(`Running pattern: ${patternName}`);
|
|
1228
|
+
|
|
1229
|
+
try {
|
|
1230
|
+
const result = await httpPost('/cdp', {
|
|
1231
|
+
method: 'Runtime.evaluate',
|
|
1232
|
+
params: {
|
|
1233
|
+
expression: pattern.pattern,
|
|
1234
|
+
returnByValue: true,
|
|
1235
|
+
awaitPromise: true,
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
let value = result?.result?.value;
|
|
1240
|
+
|
|
1241
|
+
if (value === undefined || value === null) {
|
|
1242
|
+
log.fail('Pattern returned no value');
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Handle postprocessing for favicon
|
|
1247
|
+
if (patternName === 'favicon' && pattern.postprocess) {
|
|
1248
|
+
const base64 = value;
|
|
1249
|
+
if (!base64 || base64.length < 50) {
|
|
1250
|
+
log.fail('No favicon data received');
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Determine output path
|
|
1255
|
+
if (!outputFile) {
|
|
1256
|
+
const currentUrl = await httpPost('/cdp', {
|
|
1257
|
+
method: 'Runtime.evaluate',
|
|
1258
|
+
params: { expression: 'window.location.hostname', returnByValue: true }
|
|
1259
|
+
});
|
|
1260
|
+
const hostname = currentUrl?.result?.value?.replace(/^www\./, '').split('.')[0] || 'favicon';
|
|
1261
|
+
outputFile = `/tmp/${hostname}-favicon.webp`;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Save and convert
|
|
1265
|
+
const tempFile = `/tmp/favicon-temp-${Date.now()}`;
|
|
1266
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
1267
|
+
|
|
1268
|
+
// Detect if ICO
|
|
1269
|
+
const isIco = buffer[0] === 0 && buffer[1] === 0 && buffer[2] === 1;
|
|
1270
|
+
const tempPath = isIco ? `${tempFile}.ico` : `${tempFile}.png`;
|
|
1271
|
+
fs.writeFileSync(tempPath, buffer);
|
|
1272
|
+
log.ok(`Downloaded: ${buffer.length} bytes`);
|
|
1273
|
+
|
|
1274
|
+
// Convert to webp
|
|
1275
|
+
if (outputFile.endsWith('.webp')) {
|
|
1276
|
+
try {
|
|
1277
|
+
let pngPath = tempPath;
|
|
1278
|
+
if (isIco) {
|
|
1279
|
+
pngPath = `${tempFile}.png`;
|
|
1280
|
+
execSync(`magick "${tempPath}[0]" -resize 32x32 "${pngPath}" 2>/dev/null || convert "${tempPath}[0]" -resize 32x32 "${pngPath}" 2>/dev/null`);
|
|
1281
|
+
}
|
|
1282
|
+
execSync(`cwebp "${pngPath}" -o "${outputFile}" -q 90 2>/dev/null`);
|
|
1283
|
+
log.ok(`Saved: ${outputFile}`);
|
|
1284
|
+
|
|
1285
|
+
// Cleanup
|
|
1286
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
1287
|
+
if (pngPath !== tempPath) try { fs.unlinkSync(pngPath); } catch {}
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
// Fallback - save as-is
|
|
1290
|
+
const fallback = outputFile.replace('.webp', isIco ? '.ico' : '.png');
|
|
1291
|
+
fs.copyFileSync(tempPath, fallback);
|
|
1292
|
+
log.warn(`Conversion failed, saved as: ${fallback}`);
|
|
1293
|
+
outputFile = fallback;
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
fs.copyFileSync(tempPath, outputFile);
|
|
1297
|
+
log.ok(`Saved: ${outputFile}`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Also copy to dist if in spoonfeeder project
|
|
1301
|
+
const distPath = outputFile.replace('/public/', '/dist/web/');
|
|
1302
|
+
if (distPath !== outputFile && fs.existsSync(outputFile)) {
|
|
1303
|
+
try {
|
|
1304
|
+
const distDir = path.dirname(distPath);
|
|
1305
|
+
if (fs.existsSync(distDir)) {
|
|
1306
|
+
fs.copyFileSync(outputFile, distPath);
|
|
1307
|
+
log.ok(`Copied to dist: ${distPath}`);
|
|
1308
|
+
}
|
|
1309
|
+
} catch {}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
console.log(outputFile);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Standard output
|
|
1317
|
+
if (outputFile) {
|
|
1318
|
+
const output = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
|
|
1319
|
+
fs.writeFileSync(outputFile, output);
|
|
1320
|
+
log.ok(`Saved to ${outputFile}`);
|
|
1321
|
+
} else {
|
|
1322
|
+
if (typeof value === 'object') {
|
|
1323
|
+
console.log(JSON.stringify(value, null, 2));
|
|
1324
|
+
} else {
|
|
1325
|
+
console.log(value);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
} catch (e) {
|
|
1329
|
+
log.fail(`Pattern failed: ${e.message}`);
|
|
1330
|
+
process.exit(1);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
985
1334
|
// Explore site (clicks around, captures network)
|
|
986
1335
|
async function cmdExplore(url, opts = []) {
|
|
987
1336
|
if (!url) {
|
|
@@ -1304,6 +1653,7 @@ ${B5}USAGE${NC}
|
|
|
1304
1653
|
${B5}SETUP${NC}
|
|
1305
1654
|
${BW}install${NC} Install daemon ${DIM}(runs at login, auto-restarts)${NC}
|
|
1306
1655
|
${BW}uninstall${NC} Remove daemon
|
|
1656
|
+
${BW}update${NC} Update to latest version
|
|
1307
1657
|
${BW}connect${NC} Connect to browser ${DIM}(run once per Chrome session)${NC}
|
|
1308
1658
|
|
|
1309
1659
|
${B5}STATUS${NC}
|
|
@@ -1337,6 +1687,18 @@ ${B5}MULTI-TAB${NC}
|
|
|
1337
1687
|
${BW}spawn${NC} <urls...> Open multiple tabs
|
|
1338
1688
|
${BW}extract${NC} [opts] Extract from all tabs
|
|
1339
1689
|
${BW}explore${NC} <url> Crawl site, capture network
|
|
1690
|
+
${BW}favicon${NC} <url> [out] Extract favicon from site ${DIM}(webp)${NC}
|
|
1691
|
+
|
|
1692
|
+
${B5}EXTRACTION PATTERNS${NC} ${DIM}(bulletproof, domain-agnostic)${NC}
|
|
1693
|
+
${BW}reg${NC} List all patterns
|
|
1694
|
+
${BW}reg table${NC} Extract table as JSON ${DIM}(headers β keys)${NC}
|
|
1695
|
+
${BW}reg table-csv${NC} Extract table as CSV
|
|
1696
|
+
${BW}reg table-paginated${NC} Get pagination info ${DIM}(hasNextPage, rowCount)${NC}
|
|
1697
|
+
${BW}reg buttons${NC} List all buttons ${DIM}(text, aria-label)${NC}
|
|
1698
|
+
${BW}reg inputs${NC} List all input fields
|
|
1699
|
+
${BW}reg loading${NC} Check for loading spinners
|
|
1700
|
+
${BW}reg errors${NC} Find error messages
|
|
1701
|
+
${BW}reg data-attrs${NC} Find data-testid elements ${DIM}(stable selectors)${NC}
|
|
1340
1702
|
|
|
1341
1703
|
${B5}AUTOMATION${NC}
|
|
1342
1704
|
${BW}run${NC} <task.yaml> Execute YAML task file
|
|
@@ -1411,6 +1773,61 @@ ${YELLOW}DOMAIN EXTENSIONS:${NC}
|
|
|
1411
1773
|
}
|
|
1412
1774
|
}
|
|
1413
1775
|
|
|
1776
|
+
// Version check - non-blocking, runs in background
|
|
1777
|
+
async function checkForUpdates() {
|
|
1778
|
+
try {
|
|
1779
|
+
const https = require('https');
|
|
1780
|
+
const pkg = require('../package.json');
|
|
1781
|
+
const current = pkg.version;
|
|
1782
|
+
|
|
1783
|
+
const data = await new Promise((resolve, reject) => {
|
|
1784
|
+
https.get('https://registry.npmjs.org/glidercli/latest', { timeout: 2000 }, (res) => {
|
|
1785
|
+
let body = '';
|
|
1786
|
+
res.on('data', chunk => body += chunk);
|
|
1787
|
+
res.on('end', () => resolve(JSON.parse(body)));
|
|
1788
|
+
}).on('error', reject);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
const latest = data.version;
|
|
1792
|
+
if (latest && latest !== current) {
|
|
1793
|
+
console.error(`${YELLOW}β¬${NC} Update available: ${DIM}${current}${NC} β ${GREEN}${latest}${NC} ${DIM}(run: glider update)${NC}`);
|
|
1794
|
+
}
|
|
1795
|
+
} catch {} // Silent fail - don't block CLI
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Update command
|
|
1799
|
+
async function cmdUpdate() {
|
|
1800
|
+
log.info('Checking for updates...');
|
|
1801
|
+
try {
|
|
1802
|
+
const pkg = require('../package.json');
|
|
1803
|
+
const current = pkg.version;
|
|
1804
|
+
|
|
1805
|
+
// Check latest
|
|
1806
|
+
const https = require('https');
|
|
1807
|
+
const data = await new Promise((resolve, reject) => {
|
|
1808
|
+
https.get('https://registry.npmjs.org/glidercli/latest', { timeout: 5000 }, (res) => {
|
|
1809
|
+
let body = '';
|
|
1810
|
+
res.on('data', chunk => body += chunk);
|
|
1811
|
+
res.on('end', () => resolve(JSON.parse(body)));
|
|
1812
|
+
}).on('error', reject);
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
const latest = data.version;
|
|
1816
|
+
if (latest === current) {
|
|
1817
|
+
log.ok(`Already on latest version (${current})`);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
log.info(`Updating ${current} β ${latest}...`);
|
|
1822
|
+
execSync('npm update -g glidercli', { stdio: 'inherit' });
|
|
1823
|
+
log.ok(`Updated to ${latest}`);
|
|
1824
|
+
} catch (e) {
|
|
1825
|
+
log.fail(`Update failed: ${e.message}`);
|
|
1826
|
+
log.info('Try manually: npm update -g glidercli');
|
|
1827
|
+
process.exit(1);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1414
1831
|
// Main
|
|
1415
1832
|
async function main() {
|
|
1416
1833
|
const args = process.argv.slice(2);
|
|
@@ -1421,8 +1838,13 @@ async function main() {
|
|
|
1421
1838
|
process.exit(0);
|
|
1422
1839
|
}
|
|
1423
1840
|
|
|
1841
|
+
// Background version check (non-blocking) - skip for update/version commands
|
|
1842
|
+
if (!['update', 'version', '-v', '--version'].includes(cmd)) {
|
|
1843
|
+
checkForUpdates();
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1424
1846
|
// Ensure server is running for most commands
|
|
1425
|
-
if (!['start', 'stop', 'help', '--help', '-h'].includes(cmd)) {
|
|
1847
|
+
if (!['start', 'stop', 'help', '--help', '-h', 'update', 'version', '-v', '--version'].includes(cmd)) {
|
|
1426
1848
|
if (!await checkServer()) {
|
|
1427
1849
|
log.info('Server not running, starting...');
|
|
1428
1850
|
await cmdStart();
|
|
@@ -1453,6 +1875,14 @@ async function main() {
|
|
|
1453
1875
|
case 'uninstall':
|
|
1454
1876
|
await cmdUninstallDaemon();
|
|
1455
1877
|
break;
|
|
1878
|
+
case 'update':
|
|
1879
|
+
await cmdUpdate();
|
|
1880
|
+
break;
|
|
1881
|
+
case 'version':
|
|
1882
|
+
case '-v':
|
|
1883
|
+
case '--version':
|
|
1884
|
+
console.log(require('../package.json').version);
|
|
1885
|
+
break;
|
|
1456
1886
|
case 'connect':
|
|
1457
1887
|
await cmdConnect();
|
|
1458
1888
|
break;
|
|
@@ -1507,6 +1937,9 @@ async function main() {
|
|
|
1507
1937
|
case 'fetch':
|
|
1508
1938
|
await cmdFetch(args[1], args.slice(2));
|
|
1509
1939
|
break;
|
|
1940
|
+
case 'cfetch':
|
|
1941
|
+
await cmdCorsFetch(args[1], args.slice(2));
|
|
1942
|
+
break;
|
|
1510
1943
|
case 'spawn':
|
|
1511
1944
|
await cmdSpawn(args.slice(1));
|
|
1512
1945
|
break;
|
|
@@ -1516,6 +1949,15 @@ async function main() {
|
|
|
1516
1949
|
case 'explore':
|
|
1517
1950
|
await cmdExplore(args[1], args.slice(2));
|
|
1518
1951
|
break;
|
|
1952
|
+
case 'favicon':
|
|
1953
|
+
// Use registry pattern - bulletproof method
|
|
1954
|
+
await cmdRegistry('favicon', args.slice(1));
|
|
1955
|
+
break;
|
|
1956
|
+
case 'registry':
|
|
1957
|
+
case 'reg':
|
|
1958
|
+
// Run a registry pattern
|
|
1959
|
+
await cmdRegistry(args[1], args.slice(2));
|
|
1960
|
+
break;
|
|
1519
1961
|
case 'loop':
|
|
1520
1962
|
case 'ralph': // alias for loop - Ralph Wiggum pattern
|
|
1521
1963
|
// Parse loop options
|
package/lib/bfavicon.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bfavicon.js - Bulletproof favicon extractor via CDP
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ./bfavicon.js <url> [output.webp]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
14
|
+
|
|
15
|
+
const GREEN = '\x1b[32m';
|
|
16
|
+
const RED = '\x1b[31m';
|
|
17
|
+
const CYAN = '\x1b[36m';
|
|
18
|
+
const YELLOW = '\x1b[33m';
|
|
19
|
+
const NC = '\x1b[0m';
|
|
20
|
+
|
|
21
|
+
const log = {
|
|
22
|
+
ok: (msg) => console.error(`${GREEN}β${NC} ${msg}`),
|
|
23
|
+
fail: (msg) => console.error(`${RED}β${NC} ${msg}`),
|
|
24
|
+
info: (msg) => console.error(`${CYAN}β${NC} ${msg}`),
|
|
25
|
+
warn: (msg) => console.error(`${YELLOW}β ${NC} ${msg}`),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class FaviconExtractor {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.ws = null;
|
|
31
|
+
this.messageId = 0;
|
|
32
|
+
this.pending = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async connect() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
this.ws = new WebSocket(RELAY_URL);
|
|
38
|
+
this.ws.on('open', () => resolve());
|
|
39
|
+
this.ws.on('error', (err) => reject(new Error(`Connection failed: ${err.message}`)));
|
|
40
|
+
this.ws.on('message', (data) => {
|
|
41
|
+
const msg = JSON.parse(data.toString());
|
|
42
|
+
if (msg.id !== undefined) {
|
|
43
|
+
const pending = this.pending.get(msg.id);
|
|
44
|
+
if (pending) {
|
|
45
|
+
this.pending.delete(msg.id);
|
|
46
|
+
msg.error ? pending.reject(new Error(msg.error.message)) : pending.resolve(msg.result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async send(method, params = {}) {
|
|
54
|
+
const id = ++this.messageId;
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
this.pending.set(id, { resolve, reject });
|
|
57
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
if (this.pending.has(id)) {
|
|
60
|
+
this.pending.delete(id);
|
|
61
|
+
reject(new Error('Timeout'));
|
|
62
|
+
}
|
|
63
|
+
}, 30000);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async navigate(url) {
|
|
68
|
+
await this.send('Page.navigate', { url });
|
|
69
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async extractFavicon() {
|
|
73
|
+
// Bulletproof extraction - try everything, return first success
|
|
74
|
+
const script = `
|
|
75
|
+
(async () => {
|
|
76
|
+
const origin = window.location.origin;
|
|
77
|
+
const results = [];
|
|
78
|
+
|
|
79
|
+
// Build list of ALL possible favicon URLs
|
|
80
|
+
const urls = new Set();
|
|
81
|
+
|
|
82
|
+
// 1. From link tags
|
|
83
|
+
document.querySelectorAll('link[rel*="icon"], link[rel="apple-touch-icon"], link[rel="apple-touch-icon-precomposed"], link[rel="mask-icon"]')
|
|
84
|
+
.forEach(l => l.href && urls.add(l.href));
|
|
85
|
+
|
|
86
|
+
// 2. From meta tags
|
|
87
|
+
document.querySelectorAll('meta[property="og:image"], meta[name="msapplication-TileImage"]')
|
|
88
|
+
.forEach(m => m.content && urls.add(m.content));
|
|
89
|
+
|
|
90
|
+
// 3. Default locations - try ALL common paths
|
|
91
|
+
const defaults = [
|
|
92
|
+
'/favicon.ico', '/favicon.png', '/favicon.svg', '/favicon.webp',
|
|
93
|
+
'/apple-touch-icon.png', '/apple-touch-icon-precomposed.png',
|
|
94
|
+
'/apple-touch-icon-180x180.png', '/apple-touch-icon-152x152.png',
|
|
95
|
+
'/icon.png', '/icon.ico', '/logo.png', '/logo.ico',
|
|
96
|
+
'/images/favicon.ico', '/images/favicon.png',
|
|
97
|
+
'/assets/favicon.ico', '/assets/favicon.png',
|
|
98
|
+
'/static/favicon.ico', '/static/favicon.png',
|
|
99
|
+
];
|
|
100
|
+
defaults.forEach(p => urls.add(origin + p));
|
|
101
|
+
|
|
102
|
+
// 4. Check manifest
|
|
103
|
+
const manifest = document.querySelector('link[rel="manifest"]');
|
|
104
|
+
if (manifest?.href) {
|
|
105
|
+
try {
|
|
106
|
+
const m = await fetch(manifest.href).then(r => r.json());
|
|
107
|
+
(m.icons || []).forEach(i => urls.add(new URL(i.src, manifest.href).href));
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 5. Look for any img with icon/logo/favicon in src
|
|
112
|
+
document.querySelectorAll('img[src*="icon"], img[src*="logo"], img[src*="favicon"]')
|
|
113
|
+
.forEach(img => img.src && urls.add(img.src));
|
|
114
|
+
|
|
115
|
+
// Try each URL - fetch directly, no HEAD check
|
|
116
|
+
for (const url of urls) {
|
|
117
|
+
try {
|
|
118
|
+
const resp = await fetch(url, { mode: 'cors', credentials: 'include' });
|
|
119
|
+
if (!resp.ok) continue;
|
|
120
|
+
|
|
121
|
+
const blob = await resp.blob();
|
|
122
|
+
if (blob.size < 50) continue; // Skip tiny/empty
|
|
123
|
+
|
|
124
|
+
// Convert to base64
|
|
125
|
+
const base64 = await new Promise((resolve, reject) => {
|
|
126
|
+
const reader = new FileReader();
|
|
127
|
+
reader.onload = () => resolve(reader.result.split(',')[1]);
|
|
128
|
+
reader.onerror = reject;
|
|
129
|
+
reader.readAsDataURL(blob);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { url, base64, size: blob.size, type: blob.type };
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// Silent fail, try next
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return null;
|
|
139
|
+
})()
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
const result = await this.send('Runtime.evaluate', {
|
|
143
|
+
expression: script,
|
|
144
|
+
awaitPromise: true,
|
|
145
|
+
returnByValue: true
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return result?.value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
close() {
|
|
152
|
+
if (this.ws) this.ws.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function main() {
|
|
157
|
+
const args = process.argv.slice(2);
|
|
158
|
+
|
|
159
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
160
|
+
console.log(`Usage: glider favicon <url> [output.webp]`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let url = args[0];
|
|
165
|
+
let outputPath = args[1];
|
|
166
|
+
|
|
167
|
+
if (!url.startsWith('http')) url = 'https://' + url;
|
|
168
|
+
|
|
169
|
+
log.info(`Extracting favicon from: ${url}`);
|
|
170
|
+
|
|
171
|
+
const extractor = new FaviconExtractor();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await extractor.connect();
|
|
175
|
+
log.ok('Connected to relay');
|
|
176
|
+
|
|
177
|
+
await extractor.navigate(url);
|
|
178
|
+
log.ok('Page loaded');
|
|
179
|
+
|
|
180
|
+
const favicon = await extractor.extractFavicon();
|
|
181
|
+
|
|
182
|
+
if (!favicon) {
|
|
183
|
+
log.fail('No favicon found');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
log.ok(`Found: ${favicon.url} (${favicon.size} bytes, ${favicon.type})`);
|
|
188
|
+
|
|
189
|
+
// Determine output path
|
|
190
|
+
if (!outputPath) {
|
|
191
|
+
const hostname = new URL(url).hostname.replace(/^www\./, '').split('.')[0];
|
|
192
|
+
outputPath = `/tmp/${hostname}-favicon.png`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Save the file
|
|
196
|
+
const buffer = Buffer.from(favicon.base64, 'base64');
|
|
197
|
+
const tempFile = `/tmp/favicon-temp-${Date.now()}`;
|
|
198
|
+
|
|
199
|
+
// Detect format and save appropriately
|
|
200
|
+
const isIco = favicon.type.includes('icon') || favicon.url.endsWith('.ico');
|
|
201
|
+
const tempPath = isIco ? `${tempFile}.ico` : `${tempFile}.png`;
|
|
202
|
+
fs.writeFileSync(tempPath, buffer);
|
|
203
|
+
|
|
204
|
+
// Convert to webp if requested
|
|
205
|
+
if (outputPath.endsWith('.webp')) {
|
|
206
|
+
try {
|
|
207
|
+
// If ico, convert to png first
|
|
208
|
+
let pngPath = tempPath;
|
|
209
|
+
if (isIco) {
|
|
210
|
+
pngPath = `${tempFile}.png`;
|
|
211
|
+
execSync(`magick "${tempPath}" -resize 32x32 "${pngPath}" 2>/dev/null || convert "${tempPath}" -resize 32x32 "${pngPath}" 2>/dev/null`);
|
|
212
|
+
}
|
|
213
|
+
execSync(`cwebp "${pngPath}" -o "${outputPath}" -q 90 2>/dev/null`);
|
|
214
|
+
log.ok(`Saved as WebP: ${outputPath}`);
|
|
215
|
+
// Cleanup
|
|
216
|
+
try { fs.unlinkSync(tempPath); } catch {}
|
|
217
|
+
if (pngPath !== tempPath) try { fs.unlinkSync(pngPath); } catch {}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// Fallback - just save as-is
|
|
220
|
+
const fallbackPath = outputPath.replace('.webp', isIco ? '.ico' : '.png');
|
|
221
|
+
fs.copyFileSync(tempPath, fallbackPath);
|
|
222
|
+
log.warn(`Conversion failed, saved as: ${fallbackPath}`);
|
|
223
|
+
outputPath = fallbackPath;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
fs.copyFileSync(tempPath, outputPath);
|
|
227
|
+
log.ok(`Saved: ${outputPath}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Copy to dist too
|
|
231
|
+
const distPath = outputPath.replace('/public/', '/dist/web/');
|
|
232
|
+
if (distPath !== outputPath && fs.existsSync(outputPath)) {
|
|
233
|
+
try {
|
|
234
|
+
const distDir = require('path').dirname(distPath);
|
|
235
|
+
if (fs.existsSync(distDir)) {
|
|
236
|
+
fs.copyFileSync(outputPath, distPath);
|
|
237
|
+
log.ok(`Copied to dist: ${distPath}`);
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(outputPath);
|
|
243
|
+
|
|
244
|
+
} catch (e) {
|
|
245
|
+
log.fail(e.message);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
} finally {
|
|
248
|
+
extractor.close();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
main();
|
package/lib/bserve.js
CHANGED
|
@@ -60,6 +60,21 @@ const server = http.createServer((req, res) => {
|
|
|
60
60
|
res.end(JSON.stringify({ error: e.message }));
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
|
+
} else if (req.url === '/extension' && req.method === 'POST') {
|
|
64
|
+
// HTTP POST endpoint for extension commands (CORS-bypassing fetch, etc)
|
|
65
|
+
let body = '';
|
|
66
|
+
req.on('data', chunk => body += chunk);
|
|
67
|
+
req.on('end', async () => {
|
|
68
|
+
try {
|
|
69
|
+
const { method, params } = JSON.parse(body);
|
|
70
|
+
const result = await sendToExtension({ method, params });
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
72
|
+
res.end(JSON.stringify(result));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
75
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
63
78
|
} else {
|
|
64
79
|
res.writeHead(404);
|
|
65
80
|
res.end('Not Found');
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"description": "Glider extraction patterns - domain-agnostic, bulletproof",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"updated": "2026-01-31"
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
"table": {
|
|
9
|
+
"description": "Extract first table as JSON array of objects (headers become keys)",
|
|
10
|
+
"pattern": "(() => { const t = document.querySelector('table'); if (!t) return {error: 'No table found'}; const headers = Array.from(t.querySelectorAll('thead th, tr:first-child th, tr:first-child td')).map(h => h.textContent.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')); const rows = Array.from(t.querySelectorAll('tbody tr, tr:not(:first-child)')); return rows.map(row => { const cells = Array.from(row.querySelectorAll('td')); const obj = {}; cells.forEach((c, i) => { obj[headers[i] || `col_${i}`] = c.textContent.trim(); }); return obj; }); })()",
|
|
11
|
+
"output": "json"
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"table-all": {
|
|
15
|
+
"description": "Extract ALL tables on page as array of table objects",
|
|
16
|
+
"pattern": "(() => { const tables = Array.from(document.querySelectorAll('table')); return tables.map((t, idx) => { const headers = Array.from(t.querySelectorAll('thead th, tr:first-child th, tr:first-child td')).map(h => h.textContent.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_')); const rows = Array.from(t.querySelectorAll('tbody tr, tr:not(:first-child)')); const data = rows.map(row => { const cells = Array.from(row.querySelectorAll('td')); const obj = {}; cells.forEach((c, i) => { obj[headers[i] || `col_${i}`] = c.textContent.trim(); }); return obj; }); return { tableIndex: idx, headers, rowCount: data.length, data }; }); })()",
|
|
17
|
+
"output": "json"
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"table-raw": {
|
|
21
|
+
"description": "Extract first table as 2D array (no header processing)",
|
|
22
|
+
"pattern": "(() => { const t = document.querySelector('table'); if (!t) return []; return Array.from(t.querySelectorAll('tr')).map(row => Array.from(row.querySelectorAll('th, td')).map(c => c.textContent.trim())); })()",
|
|
23
|
+
"output": "json"
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
"table-csv": {
|
|
27
|
+
"description": "Extract first table as CSV string",
|
|
28
|
+
"pattern": "(() => { const t = document.querySelector('table'); if (!t) return ''; return Array.from(t.querySelectorAll('tr')).map(row => Array.from(row.querySelectorAll('th, td')).map(c => '\"' + c.textContent.trim().replace(/\"/g, '\"\"') + '\"').join(',')).join('\\n'); })()",
|
|
29
|
+
"output": "string"
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"table-paginated": {
|
|
33
|
+
"description": "Get table info including pagination details",
|
|
34
|
+
"pattern": "(() => { const t = document.querySelector('table'); if (!t) return {error: 'No table found'}; const headers = Array.from(t.querySelectorAll('thead th')).map(h => h.textContent.trim()); const rowCount = t.querySelectorAll('tbody tr').length; const pagination = document.body.innerText.match(/(\\d+)\\s*[-β]\\s*(\\d+)\\s*of\\s*(\\d+)/i) || document.body.innerText.match(/page\\s*(\\d+)\\s*of\\s*(\\d+)/i); const nextBtn = document.querySelector('[aria-label*=\"Next\"], [aria-label*=\"next\"], button:has(svg[class*=\"right\"])'); return { headers, rowsOnPage: rowCount, pagination: pagination ? pagination[0] : null, hasNextPage: !!nextBtn && !nextBtn.disabled }; })()",
|
|
35
|
+
"output": "json"
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"buttons": {
|
|
39
|
+
"description": "List all buttons with text and attributes",
|
|
40
|
+
"pattern": "Array.from(document.querySelectorAll('button, [role=\"button\"], input[type=\"button\"], input[type=\"submit\"]')).map(b => ({ text: b.textContent?.trim()?.substring(0, 100), ariaLabel: b.getAttribute('aria-label'), disabled: b.disabled, className: b.className?.substring(0, 50) }))",
|
|
41
|
+
"output": "json"
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"button-click": {
|
|
45
|
+
"description": "Find and click button by text content (case-insensitive)",
|
|
46
|
+
"pattern": "(() => { const text = '{TEXT}'; const btn = Array.from(document.querySelectorAll('button, [role=\"button\"]')).find(b => b.textContent.toLowerCase().includes(text.toLowerCase())); if (btn) { btn.click(); return { clicked: true, text: btn.textContent.trim() }; } return { clicked: false, error: 'Button not found' }; })()",
|
|
47
|
+
"output": "json",
|
|
48
|
+
"params": ["TEXT"]
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
"dropdown-options": {
|
|
52
|
+
"description": "Get all options from select/dropdown elements",
|
|
53
|
+
"pattern": "(() => { const selects = Array.from(document.querySelectorAll('select')); const customs = Array.from(document.querySelectorAll('[role=\"listbox\"], [role=\"menu\"], [class*=\"dropdown\"] [class*=\"option\"]')); return { native: selects.map(s => ({ name: s.name, options: Array.from(s.options).map(o => ({ value: o.value, text: o.text })) })), custom: customs.map(c => c.textContent.trim()) }; })()",
|
|
54
|
+
"output": "json"
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
"inputs": {
|
|
58
|
+
"description": "Get all input fields with their current values",
|
|
59
|
+
"pattern": "Array.from(document.querySelectorAll('input, textarea, select')).map(i => ({ type: i.type || i.tagName.toLowerCase(), name: i.name, id: i.id, placeholder: i.placeholder, value: i.type === 'password' ? '***' : i.value?.substring(0, 100), required: i.required }))",
|
|
60
|
+
"output": "json"
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
"modals": {
|
|
64
|
+
"description": "Detect open modals/dialogs",
|
|
65
|
+
"pattern": "(() => { const modals = Array.from(document.querySelectorAll('[role=\"dialog\"], [class*=\"modal\"], [class*=\"Modal\"], [aria-modal=\"true\"]')); return modals.filter(m => m.offsetParent !== null).map(m => ({ text: m.textContent?.substring(0, 500), className: m.className?.substring(0, 100) })); })()",
|
|
66
|
+
"output": "json"
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
"loading": {
|
|
70
|
+
"description": "Check if page has loading indicators",
|
|
71
|
+
"pattern": "(() => { const indicators = document.querySelectorAll('[class*=\"loading\"], [class*=\"Loading\"], [class*=\"spinner\"], [class*=\"Spinner\"], [aria-busy=\"true\"]'); const visible = Array.from(indicators).filter(i => i.offsetParent !== null); return { isLoading: visible.length > 0, count: visible.length }; })()",
|
|
72
|
+
"output": "json"
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"errors": {
|
|
76
|
+
"description": "Find error messages on page",
|
|
77
|
+
"pattern": "(() => { const errorSelectors = '[class*=\"error\"], [class*=\"Error\"], [role=\"alert\"], [class*=\"warning\"], [class*=\"Warning\"]'; const errors = Array.from(document.querySelectorAll(errorSelectors)).filter(e => e.offsetParent !== null && e.textContent.trim()); return errors.map(e => e.textContent.trim().substring(0, 200)); })()",
|
|
78
|
+
"output": "json"
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
"nav": {
|
|
82
|
+
"description": "Extract navigation structure",
|
|
83
|
+
"pattern": "(() => { const navs = document.querySelectorAll('nav, [role=\"navigation\"]'); return Array.from(navs).map(n => Array.from(n.querySelectorAll('a')).map(a => ({ text: a.textContent.trim(), href: a.href }))); })()",
|
|
84
|
+
"output": "json"
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
"tabs": {
|
|
88
|
+
"description": "Get tab/tablist elements and their states",
|
|
89
|
+
"pattern": "(() => { const tabs = Array.from(document.querySelectorAll('[role=\"tab\"], [class*=\"tab\"]')); return tabs.map(t => ({ text: t.textContent.trim(), selected: t.getAttribute('aria-selected') === 'true' || t.classList.contains('active') || t.classList.contains('selected'), href: t.href || t.getAttribute('data-href') })); })()",
|
|
90
|
+
"output": "json"
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
"data-attrs": {
|
|
94
|
+
"description": "Find elements with data-* attributes (useful for scraping)",
|
|
95
|
+
"pattern": "(() => { const els = document.querySelectorAll('[data-testid], [data-id], [data-value], [data-key]'); return Array.from(els).slice(0, 50).map(e => ({ tag: e.tagName, testid: e.dataset.testid, id: e.dataset.id, value: e.dataset.value, text: e.textContent?.trim()?.substring(0, 50) })); })()",
|
|
96
|
+
"output": "json"
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
"aria": {
|
|
100
|
+
"description": "Find elements with aria-label (stable selectors)",
|
|
101
|
+
"pattern": "Array.from(document.querySelectorAll('[aria-label]')).slice(0, 50).map(e => ({ tag: e.tagName, label: e.getAttribute('aria-label'), role: e.getAttribute('role') }))",
|
|
102
|
+
"output": "json"
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
"favicon": {
|
|
106
|
+
"description": "Extract favicon from current page using browser session",
|
|
107
|
+
"pattern": "(async () => { const resp = await fetch(document.querySelector('link[rel*=icon]')?.href || window.location.origin + '/favicon.ico', { credentials: 'include' }); const blob = await resp.blob(); const reader = new FileReader(); return await new Promise(resolve => { reader.onload = () => resolve(reader.result.split(',')[1]); reader.readAsDataURL(blob); }); })()",
|
|
108
|
+
"output": "base64",
|
|
109
|
+
"postprocess": ["base64-decode", "convert-webp"]
|
|
110
|
+
},
|
|
111
|
+
"favicon-all": {
|
|
112
|
+
"description": "Get all favicon URLs from current page",
|
|
113
|
+
"pattern": "(() => { const urls = []; document.querySelectorAll('link[rel*=icon]').forEach(l => urls.push(l.href)); if (urls.length === 0) urls.push(window.location.origin + '/favicon.ico'); return urls; })()",
|
|
114
|
+
"output": "json"
|
|
115
|
+
},
|
|
116
|
+
"title": {
|
|
117
|
+
"description": "Get page title",
|
|
118
|
+
"pattern": "document.title",
|
|
119
|
+
"output": "string"
|
|
120
|
+
},
|
|
121
|
+
"url": {
|
|
122
|
+
"description": "Get current URL",
|
|
123
|
+
"pattern": "window.location.href",
|
|
124
|
+
"output": "string"
|
|
125
|
+
},
|
|
126
|
+
"links": {
|
|
127
|
+
"description": "Get all links on page",
|
|
128
|
+
"pattern": "Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim(), href: a.href })).filter(l => l.href.startsWith('http'))",
|
|
129
|
+
"output": "json"
|
|
130
|
+
},
|
|
131
|
+
"images": {
|
|
132
|
+
"description": "Get all image URLs",
|
|
133
|
+
"pattern": "Array.from(document.querySelectorAll('img[src]')).map(i => i.src)",
|
|
134
|
+
"output": "json"
|
|
135
|
+
},
|
|
136
|
+
"text": {
|
|
137
|
+
"description": "Get page text content",
|
|
138
|
+
"pattern": "document.body.innerText",
|
|
139
|
+
"output": "string"
|
|
140
|
+
},
|
|
141
|
+
"meta": {
|
|
142
|
+
"description": "Get all meta tags",
|
|
143
|
+
"pattern": "Array.from(document.querySelectorAll('meta')).map(m => ({ name: m.name || m.property, content: m.content })).filter(m => m.content)",
|
|
144
|
+
"output": "json"
|
|
145
|
+
},
|
|
146
|
+
"og": {
|
|
147
|
+
"description": "Get Open Graph metadata",
|
|
148
|
+
"pattern": "(() => { const og = {}; document.querySelectorAll('meta[property^=\"og:\"]').forEach(m => og[m.property.replace('og:','')] = m.content); return og; })()",
|
|
149
|
+
"output": "json"
|
|
150
|
+
},
|
|
151
|
+
"forms": {
|
|
152
|
+
"description": "Get all forms and their inputs",
|
|
153
|
+
"pattern": "Array.from(document.querySelectorAll('form')).map(f => ({ action: f.action, method: f.method, inputs: Array.from(f.querySelectorAll('input,select,textarea')).map(i => ({ name: i.name, type: i.type, id: i.id })) }))",
|
|
154
|
+
"output": "json"
|
|
155
|
+
},
|
|
156
|
+
"cookies": {
|
|
157
|
+
"description": "Get all cookies",
|
|
158
|
+
"pattern": "document.cookie.split(';').map(c => c.trim()).filter(c => c).map(c => { const [k,...v] = c.split('='); return { name: k, value: v.join('=') }; })",
|
|
159
|
+
"output": "json"
|
|
160
|
+
},
|
|
161
|
+
"storage": {
|
|
162
|
+
"description": "Get localStorage keys",
|
|
163
|
+
"pattern": "Object.keys(localStorage)",
|
|
164
|
+
"output": "json"
|
|
165
|
+
},
|
|
166
|
+
"scripts": {
|
|
167
|
+
"description": "Get all script sources",
|
|
168
|
+
"pattern": "Array.from(document.querySelectorAll('script[src]')).map(s => s.src)",
|
|
169
|
+
"output": "json"
|
|
170
|
+
},
|
|
171
|
+
"styles": {
|
|
172
|
+
"description": "Get all stylesheet links",
|
|
173
|
+
"pattern": "Array.from(document.querySelectorAll('link[rel=stylesheet]')).map(l => l.href)",
|
|
174
|
+
"output": "json"
|
|
175
|
+
}
|
|
176
|
+
}
|
package/package.json
CHANGED