glidercli 0.3.1 → 0.3.2

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/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'));
@@ -982,6 +991,164 @@ async function cmdExtract(opts = []) {
982
991
  }
983
992
  }
984
993
 
994
+ // Registry pattern execution - bulletproof extraction using predefined patterns
995
+ async function cmdRegistry(patternName, opts = []) {
996
+ if (!patternName) {
997
+ // List all patterns
998
+ const patterns = Object.keys(REGISTRY);
999
+ if (patterns.length === 0) {
1000
+ log.warn('No patterns in registry');
1001
+ return;
1002
+ }
1003
+ console.log(`${GREEN}${patterns.length}${NC} pattern(s) available:\n`);
1004
+ for (const name of patterns) {
1005
+ const p = REGISTRY[name];
1006
+ console.log(` ${CYAN}${name}${NC}`);
1007
+ console.log(` ${DIM}${p.description || 'No description'}${NC}`);
1008
+ }
1009
+ return;
1010
+ }
1011
+
1012
+ const pattern = REGISTRY[patternName];
1013
+ if (!pattern) {
1014
+ log.fail(`Pattern not found: ${patternName}`);
1015
+ log.info('Run "glider registry" to see available patterns');
1016
+ process.exit(1);
1017
+ }
1018
+
1019
+ // Parse options - for favicon: glider favicon [output.webp]
1020
+ // The first arg that looks like a file path is output, anything else is URL
1021
+ let outputFile = null;
1022
+ let url = null;
1023
+ for (let i = 0; i < opts.length; i++) {
1024
+ const arg = opts[i];
1025
+ if (arg === '--output' || arg === '-o') {
1026
+ outputFile = opts[++i];
1027
+ } else if (arg.startsWith('-')) {
1028
+ // skip flags
1029
+ } else if (arg.includes('/') && !arg.startsWith('http') && (arg.endsWith('.webp') || arg.endsWith('.png') || arg.endsWith('.ico'))) {
1030
+ // Looks like a file path
1031
+ outputFile = arg;
1032
+ } else if (!url) {
1033
+ url = arg;
1034
+ }
1035
+ }
1036
+
1037
+ // If URL provided, navigate first
1038
+ if (url) {
1039
+ if (!url.startsWith('http')) url = 'https://' + url;
1040
+ log.info(`Navigating to: ${url}`);
1041
+ await cmdGoto(url);
1042
+ await new Promise(r => setTimeout(r, 2000));
1043
+ }
1044
+
1045
+ log.info(`Running pattern: ${patternName}`);
1046
+
1047
+ try {
1048
+ const result = await httpPost('/cdp', {
1049
+ method: 'Runtime.evaluate',
1050
+ params: {
1051
+ expression: pattern.pattern,
1052
+ returnByValue: true,
1053
+ awaitPromise: true,
1054
+ }
1055
+ });
1056
+
1057
+ let value = result?.result?.value;
1058
+
1059
+ if (value === undefined || value === null) {
1060
+ log.fail('Pattern returned no value');
1061
+ process.exit(1);
1062
+ }
1063
+
1064
+ // Handle postprocessing for favicon
1065
+ if (patternName === 'favicon' && pattern.postprocess) {
1066
+ const base64 = value;
1067
+ if (!base64 || base64.length < 50) {
1068
+ log.fail('No favicon data received');
1069
+ process.exit(1);
1070
+ }
1071
+
1072
+ // Determine output path
1073
+ if (!outputFile) {
1074
+ const currentUrl = await httpPost('/cdp', {
1075
+ method: 'Runtime.evaluate',
1076
+ params: { expression: 'window.location.hostname', returnByValue: true }
1077
+ });
1078
+ const hostname = currentUrl?.result?.value?.replace(/^www\./, '').split('.')[0] || 'favicon';
1079
+ outputFile = `/tmp/${hostname}-favicon.webp`;
1080
+ }
1081
+
1082
+ // Save and convert
1083
+ const tempFile = `/tmp/favicon-temp-${Date.now()}`;
1084
+ const buffer = Buffer.from(base64, 'base64');
1085
+
1086
+ // Detect if ICO
1087
+ const isIco = buffer[0] === 0 && buffer[1] === 0 && buffer[2] === 1;
1088
+ const tempPath = isIco ? `${tempFile}.ico` : `${tempFile}.png`;
1089
+ fs.writeFileSync(tempPath, buffer);
1090
+ log.ok(`Downloaded: ${buffer.length} bytes`);
1091
+
1092
+ // Convert to webp
1093
+ if (outputFile.endsWith('.webp')) {
1094
+ try {
1095
+ let pngPath = tempPath;
1096
+ if (isIco) {
1097
+ pngPath = `${tempFile}.png`;
1098
+ execSync(`magick "${tempPath}[0]" -resize 32x32 "${pngPath}" 2>/dev/null || convert "${tempPath}[0]" -resize 32x32 "${pngPath}" 2>/dev/null`);
1099
+ }
1100
+ execSync(`cwebp "${pngPath}" -o "${outputFile}" -q 90 2>/dev/null`);
1101
+ log.ok(`Saved: ${outputFile}`);
1102
+
1103
+ // Cleanup
1104
+ try { fs.unlinkSync(tempPath); } catch {}
1105
+ if (pngPath !== tempPath) try { fs.unlinkSync(pngPath); } catch {}
1106
+ } catch (e) {
1107
+ // Fallback - save as-is
1108
+ const fallback = outputFile.replace('.webp', isIco ? '.ico' : '.png');
1109
+ fs.copyFileSync(tempPath, fallback);
1110
+ log.warn(`Conversion failed, saved as: ${fallback}`);
1111
+ outputFile = fallback;
1112
+ }
1113
+ } else {
1114
+ fs.copyFileSync(tempPath, outputFile);
1115
+ log.ok(`Saved: ${outputFile}`);
1116
+ }
1117
+
1118
+ // Also copy to dist if in spoonfeeder project
1119
+ const distPath = outputFile.replace('/public/', '/dist/web/');
1120
+ if (distPath !== outputFile && fs.existsSync(outputFile)) {
1121
+ try {
1122
+ const distDir = path.dirname(distPath);
1123
+ if (fs.existsSync(distDir)) {
1124
+ fs.copyFileSync(outputFile, distPath);
1125
+ log.ok(`Copied to dist: ${distPath}`);
1126
+ }
1127
+ } catch {}
1128
+ }
1129
+
1130
+ console.log(outputFile);
1131
+ return;
1132
+ }
1133
+
1134
+ // Standard output
1135
+ if (outputFile) {
1136
+ const output = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
1137
+ fs.writeFileSync(outputFile, output);
1138
+ log.ok(`Saved to ${outputFile}`);
1139
+ } else {
1140
+ if (typeof value === 'object') {
1141
+ console.log(JSON.stringify(value, null, 2));
1142
+ } else {
1143
+ console.log(value);
1144
+ }
1145
+ }
1146
+ } catch (e) {
1147
+ log.fail(`Pattern failed: ${e.message}`);
1148
+ process.exit(1);
1149
+ }
1150
+ }
1151
+
985
1152
  // Explore site (clicks around, captures network)
986
1153
  async function cmdExplore(url, opts = []) {
987
1154
  if (!url) {
@@ -1337,6 +1504,7 @@ ${B5}MULTI-TAB${NC}
1337
1504
  ${BW}spawn${NC} <urls...> Open multiple tabs
1338
1505
  ${BW}extract${NC} [opts] Extract from all tabs
1339
1506
  ${BW}explore${NC} <url> Crawl site, capture network
1507
+ ${BW}favicon${NC} <url> [out] Extract favicon from site ${DIM}(webp)${NC}
1340
1508
 
1341
1509
  ${B5}AUTOMATION${NC}
1342
1510
  ${BW}run${NC} <task.yaml> Execute YAML task file
@@ -1516,6 +1684,15 @@ async function main() {
1516
1684
  case 'explore':
1517
1685
  await cmdExplore(args[1], args.slice(2));
1518
1686
  break;
1687
+ case 'favicon':
1688
+ // Use registry pattern - bulletproof method
1689
+ await cmdRegistry('favicon', args.slice(1));
1690
+ break;
1691
+ case 'registry':
1692
+ case 'reg':
1693
+ // Run a registry pattern
1694
+ await cmdRegistry(args[1], args.slice(2));
1695
+ break;
1519
1696
  case 'loop':
1520
1697
  case 'ralph': // alias for loop - Ralph Wiggum pattern
1521
1698
  // 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();
@@ -0,0 +1,73 @@
1
+ {
2
+ "favicon": {
3
+ "description": "Extract favicon from current page using browser session",
4
+ "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); }); })()",
5
+ "output": "base64",
6
+ "postprocess": ["base64-decode", "convert-webp"]
7
+ },
8
+ "favicon-all": {
9
+ "description": "Get all favicon URLs from current page",
10
+ "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; })()",
11
+ "output": "json"
12
+ },
13
+ "title": {
14
+ "description": "Get page title",
15
+ "pattern": "document.title",
16
+ "output": "string"
17
+ },
18
+ "url": {
19
+ "description": "Get current URL",
20
+ "pattern": "window.location.href",
21
+ "output": "string"
22
+ },
23
+ "links": {
24
+ "description": "Get all links on page",
25
+ "pattern": "Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim(), href: a.href })).filter(l => l.href.startsWith('http'))",
26
+ "output": "json"
27
+ },
28
+ "images": {
29
+ "description": "Get all image URLs",
30
+ "pattern": "Array.from(document.querySelectorAll('img[src]')).map(i => i.src)",
31
+ "output": "json"
32
+ },
33
+ "text": {
34
+ "description": "Get page text content",
35
+ "pattern": "document.body.innerText",
36
+ "output": "string"
37
+ },
38
+ "meta": {
39
+ "description": "Get all meta tags",
40
+ "pattern": "Array.from(document.querySelectorAll('meta')).map(m => ({ name: m.name || m.property, content: m.content })).filter(m => m.content)",
41
+ "output": "json"
42
+ },
43
+ "og": {
44
+ "description": "Get Open Graph metadata",
45
+ "pattern": "(() => { const og = {}; document.querySelectorAll('meta[property^=\"og:\"]').forEach(m => og[m.property.replace('og:','')] = m.content); return og; })()",
46
+ "output": "json"
47
+ },
48
+ "forms": {
49
+ "description": "Get all forms and their inputs",
50
+ "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 })) }))",
51
+ "output": "json"
52
+ },
53
+ "cookies": {
54
+ "description": "Get all cookies",
55
+ "pattern": "document.cookie.split(';').map(c => c.trim()).filter(c => c).map(c => { const [k,...v] = c.split('='); return { name: k, value: v.join('=') }; })",
56
+ "output": "json"
57
+ },
58
+ "storage": {
59
+ "description": "Get localStorage keys",
60
+ "pattern": "Object.keys(localStorage)",
61
+ "output": "json"
62
+ },
63
+ "scripts": {
64
+ "description": "Get all script sources",
65
+ "pattern": "Array.from(document.querySelectorAll('script[src]')).map(s => s.src)",
66
+ "output": "json"
67
+ },
68
+ "styles": {
69
+ "description": "Get all stylesheet links",
70
+ "pattern": "Array.from(document.querySelectorAll('link[rel=stylesheet]')).map(l => l.href)",
71
+ "output": "json"
72
+ }
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glidercli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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": {