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 CHANGED
@@ -15,45 +15,56 @@
15
15
 
16
16
  <br/>
17
17
 
18
- ## Table of Contents
18
+ ## ToC
19
19
 
20
20
  <ol>
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 used</a><br/>
29
- <a href="#contact">πŸ‘€ Contact</a>
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
- ## πŸ“About
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
- ## πŸ’»Install
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 Extension** - [Install from Chrome Web Store](https://chromewebstore.google.com/detail/glider/njbidokkffhgpofcejgcfcgcinmeoalj)
65
+ 2. **Glider Chrome extension** - [Install from Chrome Web Store](https://chromewebstore.google.com/detail/glider/njbidokkffhgpofcejgcfcgcinmeoalj)
55
66
 
56
- ## πŸš€Usage
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
- ## πŸ”„The Loop
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
- ## πŸ“„Task Files
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
- ## ⚑Commands
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-Tab
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
- ## πŸ—ΊοΈRoadmap
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
- ## πŸ”§Tools Used
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
- ## πŸ‘€Contact
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
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glidercli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Browser automation CLI. Control Chrome from terminal via CDP, run YAML task files, autonomous loops until completion.",
5
5
  "main": "index.js",
6
6
  "bin": {