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 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
@@ -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 ~/.cursor/glider/domains.json or ~/.glider/domains.json
53
+ // Domain extensions - load from ~/.glider/config/domains.json (primary) or legacy paths
54
54
  const DOMAIN_CONFIG_PATHS = [
55
- path.join(os.homedir(), '.cursor', 'glider', 'domains.json'),
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 ~/.cursor/glider/domains.json or ~/.glider/domains.json');
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 ~/.cursor/glider/domains.json:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glidercli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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": {