glidercli 0.3.2 β 0.3.4
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 +271 -5
- 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
|
@@ -50,10 +50,11 @@ if (fs.existsSync(REGISTRY_FILE)) {
|
|
|
50
50
|
// Direct CDP module
|
|
51
51
|
const { DirectCDP, checkChrome } = require(path.join(LIB_DIR, 'cdp-direct.js'));
|
|
52
52
|
|
|
53
|
-
// Domain extensions - load from ~/.
|
|
53
|
+
// Domain extensions - load from ~/.glider/config/domains.json (primary) or legacy paths
|
|
54
54
|
const DOMAIN_CONFIG_PATHS = [
|
|
55
|
-
path.join(os.homedir(), '.
|
|
55
|
+
path.join(os.homedir(), '.glider', 'config', 'domains.json'),
|
|
56
56
|
path.join(os.homedir(), '.glider', 'domains.json'),
|
|
57
|
+
path.join(os.homedir(), '.cursor', 'glider', 'domains.json'), // legacy
|
|
57
58
|
];
|
|
58
59
|
let DOMAINS = {};
|
|
59
60
|
for (const cfgPath of DOMAIN_CONFIG_PATHS) {
|
|
@@ -209,6 +210,91 @@ async function getTargets() {
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
// Auto-connect helper - ensures Chrome is running and connected before commands
|
|
214
|
+
async function ensureConnected() {
|
|
215
|
+
// Check if already connected
|
|
216
|
+
if (await checkTab()) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if server is running
|
|
221
|
+
if (!await checkServer()) {
|
|
222
|
+
log.info('Server not running, starting...');
|
|
223
|
+
await cmdStart();
|
|
224
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check if Chrome is running
|
|
228
|
+
try {
|
|
229
|
+
execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
|
|
230
|
+
} catch {
|
|
231
|
+
log.info('Chrome not running, launching...');
|
|
232
|
+
// Open Chrome with a new window to google.com
|
|
233
|
+
execSync('open -na "Google Chrome" --args --new-window "https://www.google.com"');
|
|
234
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Wait for extension to connect
|
|
238
|
+
for (let i = 0; i < 10; i++) {
|
|
239
|
+
if (await checkExtension()) break;
|
|
240
|
+
await new Promise(r => setTimeout(r, 500));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!await checkExtension()) {
|
|
244
|
+
log.fail('Extension not connected - make sure Glider extension is installed');
|
|
245
|
+
log.info('Install from: chrome://extensions β Load unpacked β ~/glider-crx/glider/');
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if we have tabs now
|
|
250
|
+
if (await checkTab()) {
|
|
251
|
+
log.ok('Auto-connected to existing tab');
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Need to create/attach to a tab
|
|
256
|
+
try {
|
|
257
|
+
const tabUrl = execSync(`osascript -e 'tell application "Google Chrome" to return URL of active tab of front window'`).toString().trim();
|
|
258
|
+
if (tabUrl.startsWith('chrome://') || tabUrl.startsWith('chrome-extension://')) {
|
|
259
|
+
log.info('Creating new tab (current is chrome://)...');
|
|
260
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new tab at front window with properties {URL:"https://www.google.com"}'`);
|
|
261
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// No window exists, create one
|
|
265
|
+
log.info('Creating new Chrome window...');
|
|
266
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new window with properties {URL:"https://www.google.com"}'`);
|
|
267
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Trigger attach via HTTP
|
|
271
|
+
try {
|
|
272
|
+
const result = await fetch(`${SERVER_URL}/attach`, { method: 'POST' });
|
|
273
|
+
const data = await result.json();
|
|
274
|
+
if (data.attached > 0) {
|
|
275
|
+
log.ok('Auto-connected!');
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
} catch {}
|
|
279
|
+
|
|
280
|
+
// Final fallback - create fresh tab
|
|
281
|
+
log.info('Creating fresh tab...');
|
|
282
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new tab at front window with properties {URL:"https://www.google.com"}'`);
|
|
283
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const result = await fetch(`${SERVER_URL}/attach`, { method: 'POST' });
|
|
287
|
+
const data = await result.json();
|
|
288
|
+
if (data.attached > 0) {
|
|
289
|
+
log.ok('Auto-connected!');
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
log.fail('Could not auto-connect');
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
212
298
|
// Commands
|
|
213
299
|
async function cmdStatus() {
|
|
214
300
|
showBanner();
|
|
@@ -287,6 +373,11 @@ async function cmdGoto(url) {
|
|
|
287
373
|
process.exit(1);
|
|
288
374
|
}
|
|
289
375
|
|
|
376
|
+
// Auto-connect if not connected
|
|
377
|
+
if (!await ensureConnected()) {
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
|
|
290
381
|
log.info(`Navigating to: ${url}`);
|
|
291
382
|
|
|
292
383
|
try {
|
|
@@ -308,6 +399,11 @@ async function cmdEval(js) {
|
|
|
308
399
|
process.exit(1);
|
|
309
400
|
}
|
|
310
401
|
|
|
402
|
+
// Auto-connect if not connected
|
|
403
|
+
if (!await ensureConnected()) {
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
|
|
311
407
|
try {
|
|
312
408
|
const result = await httpPost('/cdp', {
|
|
313
409
|
method: 'Runtime.evaluate',
|
|
@@ -337,6 +433,11 @@ async function cmdClick(selector) {
|
|
|
337
433
|
process.exit(1);
|
|
338
434
|
}
|
|
339
435
|
|
|
436
|
+
// Auto-connect if not connected
|
|
437
|
+
if (!await ensureConnected()) {
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
|
|
340
441
|
const js = `
|
|
341
442
|
(() => {
|
|
342
443
|
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
@@ -369,6 +470,11 @@ async function cmdType(selector, text) {
|
|
|
369
470
|
process.exit(1);
|
|
370
471
|
}
|
|
371
472
|
|
|
473
|
+
// Auto-connect if not connected
|
|
474
|
+
if (!await ensureConnected()) {
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
|
|
372
478
|
const js = `
|
|
373
479
|
(() => {
|
|
374
480
|
const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
@@ -400,6 +506,11 @@ async function cmdType(selector, text) {
|
|
|
400
506
|
async function cmdScreenshot(outputPath) {
|
|
401
507
|
const filePath = outputPath || `/tmp/glider-screenshot-${Date.now()}.png`;
|
|
402
508
|
|
|
509
|
+
// Auto-connect if not connected
|
|
510
|
+
if (!await ensureConnected()) {
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
|
|
403
514
|
try {
|
|
404
515
|
const result = await httpPost('/cdp', {
|
|
405
516
|
method: 'Page.captureScreenshot',
|
|
@@ -420,6 +531,11 @@ async function cmdScreenshot(outputPath) {
|
|
|
420
531
|
}
|
|
421
532
|
|
|
422
533
|
async function cmdText() {
|
|
534
|
+
// Auto-connect if not connected
|
|
535
|
+
if (!await ensureConnected()) {
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
|
|
423
539
|
try {
|
|
424
540
|
const result = await httpPost('/cdp', {
|
|
425
541
|
method: 'Runtime.evaluate',
|
|
@@ -779,7 +895,7 @@ async function cmdDomains() {
|
|
|
779
895
|
const domainKeys = Object.keys(DOMAINS);
|
|
780
896
|
if (domainKeys.length === 0) {
|
|
781
897
|
log.warn('No domains configured');
|
|
782
|
-
log.info('Add domains to ~/.
|
|
898
|
+
log.info('Add domains to ~/.glider/config/domains.json');
|
|
783
899
|
return;
|
|
784
900
|
}
|
|
785
901
|
console.log(`${GREEN}${domainKeys.length}${NC} domain(s) configured:\n`);
|
|
@@ -812,6 +928,11 @@ async function cmdOpen(url) {
|
|
|
812
928
|
}
|
|
813
929
|
|
|
814
930
|
async function cmdHtml(selector) {
|
|
931
|
+
// Auto-connect if not connected
|
|
932
|
+
if (!await ensureConnected()) {
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
|
|
815
936
|
try {
|
|
816
937
|
const expression = selector
|
|
817
938
|
? `document.querySelector('${selector.replace(/'/g, "\\'")}')?.outerHTML || 'Element not found'`
|
|
@@ -829,6 +950,11 @@ async function cmdHtml(selector) {
|
|
|
829
950
|
}
|
|
830
951
|
|
|
831
952
|
async function cmdTitle() {
|
|
953
|
+
// Auto-connect if not connected
|
|
954
|
+
if (!await ensureConnected()) {
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
|
|
832
958
|
try {
|
|
833
959
|
const result = await httpPost('/cdp', {
|
|
834
960
|
method: 'Runtime.evaluate',
|
|
@@ -842,6 +968,11 @@ async function cmdTitle() {
|
|
|
842
968
|
}
|
|
843
969
|
|
|
844
970
|
async function cmdUrl() {
|
|
971
|
+
// Auto-connect if not connected
|
|
972
|
+
if (!await ensureConnected()) {
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
|
|
845
976
|
try {
|
|
846
977
|
const result = await httpPost('/cdp', {
|
|
847
978
|
method: 'Runtime.evaluate',
|
|
@@ -901,6 +1032,58 @@ async function cmdFetch(url, opts = []) {
|
|
|
901
1032
|
}
|
|
902
1033
|
}
|
|
903
1034
|
|
|
1035
|
+
// CORS-bypassing fetch via extension context
|
|
1036
|
+
async function cmdCorsFetch(url, opts = []) {
|
|
1037
|
+
if (!url) {
|
|
1038
|
+
log.fail('Usage: glider cfetch <url> [--output file] [--method POST] [--body JSON]');
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
log.info(`CORS Fetch: ${url}`);
|
|
1043
|
+
|
|
1044
|
+
let outputFile = null;
|
|
1045
|
+
let method = 'GET';
|
|
1046
|
+
let body = null;
|
|
1047
|
+
|
|
1048
|
+
for (let i = 0; i < opts.length; i++) {
|
|
1049
|
+
if (opts[i] === '--output' || opts[i] === '-o') {
|
|
1050
|
+
outputFile = opts[++i];
|
|
1051
|
+
} else if (opts[i] === '--method' || opts[i] === '-X') {
|
|
1052
|
+
method = opts[++i];
|
|
1053
|
+
} else if (opts[i] === '--body' || opts[i] === '-d') {
|
|
1054
|
+
body = opts[++i];
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await httpPost('/extension', {
|
|
1060
|
+
method: 'corsFetch',
|
|
1061
|
+
params: {
|
|
1062
|
+
url,
|
|
1063
|
+
options: { method, body, headers: { 'Accept': 'application/json' } }
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
if (result?.error) {
|
|
1068
|
+
log.fail(`Fetch error: ${result.error}`);
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const data = result?.result?.data;
|
|
1073
|
+
const output = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
|
|
1074
|
+
|
|
1075
|
+
if (outputFile) {
|
|
1076
|
+
fs.writeFileSync(outputFile, output);
|
|
1077
|
+
log.ok(`Saved to ${outputFile} (status: ${result?.result?.status})`);
|
|
1078
|
+
} else {
|
|
1079
|
+
console.log(output);
|
|
1080
|
+
}
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
log.fail(`CORS Fetch failed: ${e.message}`);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
904
1087
|
// Spawn multiple tabs
|
|
905
1088
|
async function cmdSpawn(urls) {
|
|
906
1089
|
if (!urls || urls.length === 0) {
|
|
@@ -1471,6 +1654,7 @@ ${B5}USAGE${NC}
|
|
|
1471
1654
|
${B5}SETUP${NC}
|
|
1472
1655
|
${BW}install${NC} Install daemon ${DIM}(runs at login, auto-restarts)${NC}
|
|
1473
1656
|
${BW}uninstall${NC} Remove daemon
|
|
1657
|
+
${BW}update${NC} Update to latest version
|
|
1474
1658
|
${BW}connect${NC} Connect to browser ${DIM}(run once per Chrome session)${NC}
|
|
1475
1659
|
|
|
1476
1660
|
${B5}STATUS${NC}
|
|
@@ -1506,6 +1690,17 @@ ${B5}MULTI-TAB${NC}
|
|
|
1506
1690
|
${BW}explore${NC} <url> Crawl site, capture network
|
|
1507
1691
|
${BW}favicon${NC} <url> [out] Extract favicon from site ${DIM}(webp)${NC}
|
|
1508
1692
|
|
|
1693
|
+
${B5}EXTRACTION PATTERNS${NC} ${DIM}(bulletproof, domain-agnostic)${NC}
|
|
1694
|
+
${BW}reg${NC} List all patterns
|
|
1695
|
+
${BW}reg table${NC} Extract table as JSON ${DIM}(headers β keys)${NC}
|
|
1696
|
+
${BW}reg table-csv${NC} Extract table as CSV
|
|
1697
|
+
${BW}reg table-paginated${NC} Get pagination info ${DIM}(hasNextPage, rowCount)${NC}
|
|
1698
|
+
${BW}reg buttons${NC} List all buttons ${DIM}(text, aria-label)${NC}
|
|
1699
|
+
${BW}reg inputs${NC} List all input fields
|
|
1700
|
+
${BW}reg loading${NC} Check for loading spinners
|
|
1701
|
+
${BW}reg errors${NC} Find error messages
|
|
1702
|
+
${BW}reg data-attrs${NC} Find data-testid elements ${DIM}(stable selectors)${NC}
|
|
1703
|
+
|
|
1509
1704
|
${B5}AUTOMATION${NC}
|
|
1510
1705
|
${BW}run${NC} <task.yaml> Execute YAML task file
|
|
1511
1706
|
${BW}loop${NC} <task> [opts] Autonomous loop ${DIM}(run until complete)${NC}
|
|
@@ -1557,7 +1752,7 @@ ${YELLOW}REQUIREMENTS:${NC}
|
|
|
1557
1752
|
- Glider Chrome extension connected
|
|
1558
1753
|
|
|
1559
1754
|
${YELLOW}DOMAIN EXTENSIONS:${NC}
|
|
1560
|
-
Add custom domain commands via ~/.
|
|
1755
|
+
Add custom domain commands via ~/.glider/config/domains.json:
|
|
1561
1756
|
{
|
|
1562
1757
|
"mysite": { "url": "https://mysite.com/dashboard" },
|
|
1563
1758
|
"mytool": { "script": "~/.cursor/tools/scripts/mytool.sh" }
|
|
@@ -1579,6 +1774,61 @@ ${YELLOW}DOMAIN EXTENSIONS:${NC}
|
|
|
1579
1774
|
}
|
|
1580
1775
|
}
|
|
1581
1776
|
|
|
1777
|
+
// Version check - non-blocking, runs in background
|
|
1778
|
+
async function checkForUpdates() {
|
|
1779
|
+
try {
|
|
1780
|
+
const https = require('https');
|
|
1781
|
+
const pkg = require('../package.json');
|
|
1782
|
+
const current = pkg.version;
|
|
1783
|
+
|
|
1784
|
+
const data = await new Promise((resolve, reject) => {
|
|
1785
|
+
https.get('https://registry.npmjs.org/glidercli/latest', { timeout: 2000 }, (res) => {
|
|
1786
|
+
let body = '';
|
|
1787
|
+
res.on('data', chunk => body += chunk);
|
|
1788
|
+
res.on('end', () => resolve(JSON.parse(body)));
|
|
1789
|
+
}).on('error', reject);
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
const latest = data.version;
|
|
1793
|
+
if (latest && latest !== current) {
|
|
1794
|
+
console.error(`${YELLOW}β¬${NC} Update available: ${DIM}${current}${NC} β ${GREEN}${latest}${NC} ${DIM}(run: glider update)${NC}`);
|
|
1795
|
+
}
|
|
1796
|
+
} catch {} // Silent fail - don't block CLI
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Update command
|
|
1800
|
+
async function cmdUpdate() {
|
|
1801
|
+
log.info('Checking for updates...');
|
|
1802
|
+
try {
|
|
1803
|
+
const pkg = require('../package.json');
|
|
1804
|
+
const current = pkg.version;
|
|
1805
|
+
|
|
1806
|
+
// Check latest
|
|
1807
|
+
const https = require('https');
|
|
1808
|
+
const data = await new Promise((resolve, reject) => {
|
|
1809
|
+
https.get('https://registry.npmjs.org/glidercli/latest', { timeout: 5000 }, (res) => {
|
|
1810
|
+
let body = '';
|
|
1811
|
+
res.on('data', chunk => body += chunk);
|
|
1812
|
+
res.on('end', () => resolve(JSON.parse(body)));
|
|
1813
|
+
}).on('error', reject);
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
const latest = data.version;
|
|
1817
|
+
if (latest === current) {
|
|
1818
|
+
log.ok(`Already on latest version (${current})`);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
log.info(`Updating ${current} β ${latest}...`);
|
|
1823
|
+
execSync('npm update -g glidercli', { stdio: 'inherit' });
|
|
1824
|
+
log.ok(`Updated to ${latest}`);
|
|
1825
|
+
} catch (e) {
|
|
1826
|
+
log.fail(`Update failed: ${e.message}`);
|
|
1827
|
+
log.info('Try manually: npm update -g glidercli');
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1582
1832
|
// Main
|
|
1583
1833
|
async function main() {
|
|
1584
1834
|
const args = process.argv.slice(2);
|
|
@@ -1589,8 +1839,13 @@ async function main() {
|
|
|
1589
1839
|
process.exit(0);
|
|
1590
1840
|
}
|
|
1591
1841
|
|
|
1842
|
+
// Background version check (non-blocking) - skip for update/version commands
|
|
1843
|
+
if (!['update', 'version', '-v', '--version'].includes(cmd)) {
|
|
1844
|
+
checkForUpdates();
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1592
1847
|
// Ensure server is running for most commands
|
|
1593
|
-
if (!['start', 'stop', 'help', '--help', '-h'].includes(cmd)) {
|
|
1848
|
+
if (!['start', 'stop', 'help', '--help', '-h', 'update', 'version', '-v', '--version'].includes(cmd)) {
|
|
1594
1849
|
if (!await checkServer()) {
|
|
1595
1850
|
log.info('Server not running, starting...');
|
|
1596
1851
|
await cmdStart();
|
|
@@ -1621,6 +1876,14 @@ async function main() {
|
|
|
1621
1876
|
case 'uninstall':
|
|
1622
1877
|
await cmdUninstallDaemon();
|
|
1623
1878
|
break;
|
|
1879
|
+
case 'update':
|
|
1880
|
+
await cmdUpdate();
|
|
1881
|
+
break;
|
|
1882
|
+
case 'version':
|
|
1883
|
+
case '-v':
|
|
1884
|
+
case '--version':
|
|
1885
|
+
console.log(require('../package.json').version);
|
|
1886
|
+
break;
|
|
1624
1887
|
case 'connect':
|
|
1625
1888
|
await cmdConnect();
|
|
1626
1889
|
break;
|
|
@@ -1675,6 +1938,9 @@ async function main() {
|
|
|
1675
1938
|
case 'fetch':
|
|
1676
1939
|
await cmdFetch(args[1], args.slice(2));
|
|
1677
1940
|
break;
|
|
1941
|
+
case 'cfetch':
|
|
1942
|
+
await cmdCorsFetch(args[1], args.slice(2));
|
|
1943
|
+
break;
|
|
1678
1944
|
case 'spawn':
|
|
1679
1945
|
await cmdSpawn(args.slice(1));
|
|
1680
1946
|
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