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 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glidercli",
3
- "version": "0.3.2",
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": {