glidercli 0.3.2 β 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 +266 -1
- package/lib/bserve.js +15 -0
- package/lib/registry.json +103 -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,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&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
|
@@ -209,6 +209,91 @@ async function getTargets() {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
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
|
+
|
|
212
297
|
// Commands
|
|
213
298
|
async function cmdStatus() {
|
|
214
299
|
showBanner();
|
|
@@ -287,6 +372,11 @@ async function cmdGoto(url) {
|
|
|
287
372
|
process.exit(1);
|
|
288
373
|
}
|
|
289
374
|
|
|
375
|
+
// Auto-connect if not connected
|
|
376
|
+
if (!await ensureConnected()) {
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
290
380
|
log.info(`Navigating to: ${url}`);
|
|
291
381
|
|
|
292
382
|
try {
|
|
@@ -308,6 +398,11 @@ async function cmdEval(js) {
|
|
|
308
398
|
process.exit(1);
|
|
309
399
|
}
|
|
310
400
|
|
|
401
|
+
// Auto-connect if not connected
|
|
402
|
+
if (!await ensureConnected()) {
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
311
406
|
try {
|
|
312
407
|
const result = await httpPost('/cdp', {
|
|
313
408
|
method: 'Runtime.evaluate',
|
|
@@ -337,6 +432,11 @@ async function cmdClick(selector) {
|
|
|
337
432
|
process.exit(1);
|
|
338
433
|
}
|
|
339
434
|
|
|
435
|
+
// Auto-connect if not connected
|
|
436
|
+
if (!await ensureConnected()) {
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
340
440
|
const js = `
|
|
341
441
|
(() => {
|
|
342
442
|
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
@@ -369,6 +469,11 @@ async function cmdType(selector, text) {
|
|
|
369
469
|
process.exit(1);
|
|
370
470
|
}
|
|
371
471
|
|
|
472
|
+
// Auto-connect if not connected
|
|
473
|
+
if (!await ensureConnected()) {
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
372
477
|
const js = `
|
|
373
478
|
(() => {
|
|
374
479
|
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
@@ -400,6 +505,11 @@ async function cmdType(selector, text) {
|
|
|
400
505
|
async function cmdScreenshot(outputPath) {
|
|
401
506
|
const filePath = outputPath || `/tmp/glider-screenshot-${Date.now()}.png`;
|
|
402
507
|
|
|
508
|
+
// Auto-connect if not connected
|
|
509
|
+
if (!await ensureConnected()) {
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
|
|
403
513
|
try {
|
|
404
514
|
const result = await httpPost('/cdp', {
|
|
405
515
|
method: 'Page.captureScreenshot',
|
|
@@ -420,6 +530,11 @@ async function cmdScreenshot(outputPath) {
|
|
|
420
530
|
}
|
|
421
531
|
|
|
422
532
|
async function cmdText() {
|
|
533
|
+
// Auto-connect if not connected
|
|
534
|
+
if (!await ensureConnected()) {
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
|
|
423
538
|
try {
|
|
424
539
|
const result = await httpPost('/cdp', {
|
|
425
540
|
method: 'Runtime.evaluate',
|
|
@@ -812,6 +927,11 @@ async function cmdOpen(url) {
|
|
|
812
927
|
}
|
|
813
928
|
|
|
814
929
|
async function cmdHtml(selector) {
|
|
930
|
+
// Auto-connect if not connected
|
|
931
|
+
if (!await ensureConnected()) {
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
|
|
815
935
|
try {
|
|
816
936
|
const expression = selector
|
|
817
937
|
? `document.querySelector('${selector.replace(/'/g, "\\'")}')?.outerHTML || 'Element not found'`
|
|
@@ -829,6 +949,11 @@ async function cmdHtml(selector) {
|
|
|
829
949
|
}
|
|
830
950
|
|
|
831
951
|
async function cmdTitle() {
|
|
952
|
+
// Auto-connect if not connected
|
|
953
|
+
if (!await ensureConnected()) {
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
|
|
832
957
|
try {
|
|
833
958
|
const result = await httpPost('/cdp', {
|
|
834
959
|
method: 'Runtime.evaluate',
|
|
@@ -842,6 +967,11 @@ async function cmdTitle() {
|
|
|
842
967
|
}
|
|
843
968
|
|
|
844
969
|
async function cmdUrl() {
|
|
970
|
+
// Auto-connect if not connected
|
|
971
|
+
if (!await ensureConnected()) {
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
|
|
845
975
|
try {
|
|
846
976
|
const result = await httpPost('/cdp', {
|
|
847
977
|
method: 'Runtime.evaluate',
|
|
@@ -901,6 +1031,58 @@ async function cmdFetch(url, opts = []) {
|
|
|
901
1031
|
}
|
|
902
1032
|
}
|
|
903
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
|
+
|
|
904
1086
|
// Spawn multiple tabs
|
|
905
1087
|
async function cmdSpawn(urls) {
|
|
906
1088
|
if (!urls || urls.length === 0) {
|
|
@@ -1471,6 +1653,7 @@ ${B5}USAGE${NC}
|
|
|
1471
1653
|
${B5}SETUP${NC}
|
|
1472
1654
|
${BW}install${NC} Install daemon ${DIM}(runs at login, auto-restarts)${NC}
|
|
1473
1655
|
${BW}uninstall${NC} Remove daemon
|
|
1656
|
+
${BW}update${NC} Update to latest version
|
|
1474
1657
|
${BW}connect${NC} Connect to browser ${DIM}(run once per Chrome session)${NC}
|
|
1475
1658
|
|
|
1476
1659
|
${B5}STATUS${NC}
|
|
@@ -1506,6 +1689,17 @@ ${B5}MULTI-TAB${NC}
|
|
|
1506
1689
|
${BW}explore${NC} <url> Crawl site, capture network
|
|
1507
1690
|
${BW}favicon${NC} <url> [out] Extract favicon from site ${DIM}(webp)${NC}
|
|
1508
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}
|
|
1702
|
+
|
|
1509
1703
|
${B5}AUTOMATION${NC}
|
|
1510
1704
|
${BW}run${NC} <task.yaml> Execute YAML task file
|
|
1511
1705
|
${BW}loop${NC} <task> [opts] Autonomous loop ${DIM}(run until complete)${NC}
|
|
@@ -1579,6 +1773,61 @@ ${YELLOW}DOMAIN EXTENSIONS:${NC}
|
|
|
1579
1773
|
}
|
|
1580
1774
|
}
|
|
1581
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
|
+
|
|
1582
1831
|
// Main
|
|
1583
1832
|
async function main() {
|
|
1584
1833
|
const args = process.argv.slice(2);
|
|
@@ -1589,8 +1838,13 @@ async function main() {
|
|
|
1589
1838
|
process.exit(0);
|
|
1590
1839
|
}
|
|
1591
1840
|
|
|
1841
|
+
// Background version check (non-blocking) - skip for update/version commands
|
|
1842
|
+
if (!['update', 'version', '-v', '--version'].includes(cmd)) {
|
|
1843
|
+
checkForUpdates();
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1592
1846
|
// Ensure server is running for most commands
|
|
1593
|
-
if (!['start', 'stop', 'help', '--help', '-h'].includes(cmd)) {
|
|
1847
|
+
if (!['start', 'stop', 'help', '--help', '-h', 'update', 'version', '-v', '--version'].includes(cmd)) {
|
|
1594
1848
|
if (!await checkServer()) {
|
|
1595
1849
|
log.info('Server not running, starting...');
|
|
1596
1850
|
await cmdStart();
|
|
@@ -1621,6 +1875,14 @@ async function main() {
|
|
|
1621
1875
|
case 'uninstall':
|
|
1622
1876
|
await cmdUninstallDaemon();
|
|
1623
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;
|
|
1624
1886
|
case 'connect':
|
|
1625
1887
|
await cmdConnect();
|
|
1626
1888
|
break;
|
|
@@ -1675,6 +1937,9 @@ async function main() {
|
|
|
1675
1937
|
case 'fetch':
|
|
1676
1938
|
await cmdFetch(args[1], args.slice(2));
|
|
1677
1939
|
break;
|
|
1940
|
+
case 'cfetch':
|
|
1941
|
+
await cmdCorsFetch(args[1], args.slice(2));
|
|
1942
|
+
break;
|
|
1678
1943
|
case 'spawn':
|
|
1679
1944
|
await cmdSpawn(args.slice(1));
|
|
1680
1945
|
break;
|
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');
|
package/lib/registry.json
CHANGED
|
@@ -1,4 +1,107 @@
|
|
|
1
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
|
+
|
|
2
105
|
"favicon": {
|
|
3
106
|
"description": "Extract favicon from current page using browser session",
|
|
4
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); }); })()",
|
package/package.json
CHANGED