glidercli 0.1.5 → 0.3.0

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.
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bextract.js - Extract content from multiple browser tabs in parallel via Glider
4
+ *
5
+ * Usage:
6
+ * bextract # Extract from all connected tabs
7
+ * bextract --sessions s1,s2,s3 # Extract from specific sessions
8
+ * bextract --exclude session-1 # Exclude specific sessions
9
+ * bextract --js 'document.title' # Custom JS expression
10
+ * bextract --selector '.content' # Extract specific element
11
+ *
12
+ * Options:
13
+ * --js <expr> JavaScript expression to evaluate (default: document.body.innerText)
14
+ * --selector <sel> CSS selector to extract
15
+ * --sessions <list> Comma-separated session IDs
16
+ * --exclude <list> Comma-separated sessions to exclude
17
+ * --limit <n> Max characters per result (default: 10000)
18
+ * --timeout <ms> Timeout per extraction (default: 15000)
19
+ * --json Output as JSON
20
+ * --quiet Suppress progress output
21
+ *
22
+ * Examples:
23
+ * bextract --exclude session-1 --limit 5000
24
+ * bextract --js 'document.title' --json
25
+ * bextract --selector 'article' --sessions session-2,session-3
26
+ */
27
+
28
+ const WebSocket = require('ws');
29
+ const http = require('http');
30
+
31
+ const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
32
+ const DEFAULT_LIMIT = 10000;
33
+ const DEFAULT_TIMEOUT = 15000;
34
+
35
+ async function getTargets() {
36
+ return new Promise((resolve) => {
37
+ http.get('http://127.0.0.1:19988/targets', (res) => {
38
+ let data = '';
39
+ res.on('data', chunk => data += chunk);
40
+ res.on('end', () => resolve(JSON.parse(data)));
41
+ }).on('error', () => resolve([]));
42
+ });
43
+ }
44
+
45
+ async function extractFromSession(sessionId, jsExpr, options = {}) {
46
+ const { timeout = DEFAULT_TIMEOUT, limit = DEFAULT_LIMIT } = options;
47
+
48
+ return new Promise((resolve, reject) => {
49
+ const ws = new WebSocket(RELAY_URL);
50
+ let resolved = false;
51
+
52
+ const timer = setTimeout(() => {
53
+ if (!resolved) {
54
+ resolved = true;
55
+ ws.close();
56
+ reject(new Error(`Timeout for ${sessionId}`));
57
+ }
58
+ }, timeout);
59
+
60
+ ws.on('open', () => {
61
+ ws.send(JSON.stringify({
62
+ id: 1,
63
+ sessionId,
64
+ method: 'Runtime.evaluate',
65
+ params: {
66
+ expression: jsExpr,
67
+ returnByValue: true
68
+ }
69
+ }));
70
+ });
71
+
72
+ ws.on('message', (data) => {
73
+ const msg = JSON.parse(data.toString());
74
+ if (msg.id === 1 && !resolved) {
75
+ resolved = true;
76
+ clearTimeout(timer);
77
+ ws.close();
78
+
79
+ if (msg.error) {
80
+ reject(new Error(msg.error.message));
81
+ } else {
82
+ let value = msg.result?.result?.value;
83
+ if (typeof value === 'string' && value.length > limit) {
84
+ value = value.substring(0, limit) + `\n... [truncated at ${limit} chars]`;
85
+ }
86
+ resolve(value);
87
+ }
88
+ }
89
+ });
90
+
91
+ ws.on('error', (err) => {
92
+ if (!resolved) {
93
+ resolved = true;
94
+ clearTimeout(timer);
95
+ reject(err);
96
+ }
97
+ });
98
+ });
99
+ }
100
+
101
+ async function extractParallel(sessions, jsExpr, options = {}) {
102
+ const { quiet = false } = options;
103
+ const results = {};
104
+
105
+ const promises = sessions.map(async ({ sessionId, url }) => {
106
+ try {
107
+ if (!quiet) console.error(`[bextract] Extracting from ${sessionId}...`);
108
+ const content = await extractFromSession(sessionId, jsExpr, options);
109
+ results[sessionId] = { url, content, error: null };
110
+ } catch (err) {
111
+ results[sessionId] = { url, content: null, error: err.message };
112
+ }
113
+ });
114
+
115
+ await Promise.all(promises);
116
+ return results;
117
+ }
118
+
119
+ async function main() {
120
+ const args = process.argv.slice(2);
121
+
122
+ let jsExpr = 'document.body.innerText';
123
+ let selector = null;
124
+ let sessions = null;
125
+ let exclude = [];
126
+ let limit = DEFAULT_LIMIT;
127
+ let timeout = DEFAULT_TIMEOUT;
128
+ let outputJson = false;
129
+ let quiet = false;
130
+
131
+ for (let i = 0; i < args.length; i++) {
132
+ const arg = args[i];
133
+ if (arg === '--js' || arg === '-e') {
134
+ jsExpr = args[++i];
135
+ } else if (arg === '--selector' || arg === '-s') {
136
+ selector = args[++i];
137
+ } else if (arg === '--sessions') {
138
+ sessions = args[++i].split(',');
139
+ } else if (arg === '--exclude' || arg === '-x') {
140
+ exclude = args[++i].split(',');
141
+ } else if (arg === '--limit' || arg === '-l') {
142
+ limit = parseInt(args[++i], 10);
143
+ } else if (arg === '--timeout' || arg === '-t') {
144
+ timeout = parseInt(args[++i], 10);
145
+ } else if (arg === '--json' || arg === '-j') {
146
+ outputJson = true;
147
+ } else if (arg === '--quiet' || arg === '-q') {
148
+ quiet = true;
149
+ } else if (arg === '--help' || arg === '-h') {
150
+ console.log(`
151
+ bextract - Extract content from multiple browser tabs in parallel
152
+
153
+ Usage:
154
+ bextract # Extract from all connected tabs
155
+ bextract --sessions s1,s2,s3 # Extract from specific sessions
156
+ bextract --exclude session-1 # Exclude specific sessions
157
+ bextract --js 'document.title' # Custom JS expression
158
+ bextract --selector '.content' # Extract specific element
159
+
160
+ Options:
161
+ -e, --js <expr> JavaScript expression (default: document.body.innerText)
162
+ -s, --selector <sel> CSS selector to extract
163
+ --sessions <list> Comma-separated session IDs
164
+ -x, --exclude <list> Sessions to exclude
165
+ -l, --limit <n> Max chars per result (default: ${DEFAULT_LIMIT})
166
+ -t, --timeout <ms> Timeout per extraction (default: ${DEFAULT_TIMEOUT})
167
+ -j, --json Output as JSON
168
+ -q, --quiet Suppress progress output
169
+ -h, --help Show this help
170
+ `);
171
+ process.exit(0);
172
+ }
173
+ }
174
+
175
+ // Build JS expression
176
+ if (selector) {
177
+ jsExpr = `document.querySelector('${selector}')?.innerText || ''`;
178
+ }
179
+
180
+ try {
181
+ // Get targets
182
+ const targets = await getTargets();
183
+ if (targets.length === 0) {
184
+ console.error('Error: No browser tabs connected. Click extension icon on tabs first.');
185
+ process.exit(1);
186
+ }
187
+
188
+ // Filter sessions
189
+ let filteredTargets = targets;
190
+ if (sessions) {
191
+ filteredTargets = targets.filter(t => sessions.includes(t.sessionId));
192
+ }
193
+ if (exclude.length > 0) {
194
+ filteredTargets = filteredTargets.filter(t => !exclude.includes(t.sessionId));
195
+ }
196
+
197
+ if (filteredTargets.length === 0) {
198
+ console.error('Error: No matching sessions found');
199
+ process.exit(1);
200
+ }
201
+
202
+ if (!quiet) {
203
+ console.error(`[bextract] Extracting from ${filteredTargets.length} tabs in parallel...`);
204
+ }
205
+
206
+ // Extract in parallel
207
+ const results = await extractParallel(
208
+ filteredTargets.map(t => ({ sessionId: t.sessionId, url: t.targetInfo?.url })),
209
+ jsExpr,
210
+ { limit, timeout, quiet }
211
+ );
212
+
213
+ // Output
214
+ if (outputJson) {
215
+ console.log(JSON.stringify(results, null, 2));
216
+ } else {
217
+ for (const [sessionId, data] of Object.entries(results)) {
218
+ console.log(`\n${'='.repeat(60)}`);
219
+ console.log(`SESSION: ${sessionId}`);
220
+ console.log(`URL: ${data.url}`);
221
+ console.log('='.repeat(60));
222
+ if (data.error) {
223
+ console.log(`ERROR: ${data.error}`);
224
+ } else {
225
+ console.log(data.content);
226
+ }
227
+ }
228
+ }
229
+
230
+ } catch (err) {
231
+ console.error(`Error: ${err.message}`);
232
+ process.exit(1);
233
+ }
234
+ }
235
+
236
+ main();
package/lib/bfetch.js ADDED
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fetch-via-browser.js
4
+ * Fetch URLs using your authenticated browser session via CDP relay
5
+ *
6
+ * Usage:
7
+ * ./fetch-via-browser.js <url> [--output file.json]
8
+ * ./fetch-via-browser.js https://www.reddit.com/r/programming.json
9
+ */
10
+
11
+ const WebSocket = require('ws');
12
+
13
+ const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
14
+
15
+ class BrowserFetcher {
16
+ constructor() {
17
+ this.ws = null;
18
+ this.messageId = 0;
19
+ this.pending = new Map();
20
+ this.sessionId = null;
21
+ this.targetId = null;
22
+ }
23
+
24
+ async connect() {
25
+ return new Promise((resolve, reject) => {
26
+ this.ws = new WebSocket(RELAY_URL);
27
+
28
+ this.ws.on('open', () => {
29
+ console.error('[fetcher] Connected to relay');
30
+ resolve();
31
+ });
32
+
33
+ this.ws.on('error', (err) => {
34
+ reject(new Error(`Connection failed: ${err.message}`));
35
+ });
36
+
37
+ this.ws.on('message', (data) => {
38
+ const msg = JSON.parse(data.toString());
39
+
40
+ // Response to our command
41
+ if (msg.id !== undefined) {
42
+ const pending = this.pending.get(msg.id);
43
+ if (pending) {
44
+ this.pending.delete(msg.id);
45
+ if (msg.error) {
46
+ pending.reject(new Error(msg.error.message));
47
+ } else {
48
+ pending.resolve(msg.result);
49
+ }
50
+ }
51
+ return;
52
+ }
53
+
54
+ // Event
55
+ if (msg.method === 'Target.attachedToTarget') {
56
+ if (!this.sessionId) {
57
+ this.sessionId = msg.params.sessionId;
58
+ this.targetId = msg.params.targetInfo.targetId;
59
+ console.error(`[fetcher] Got target: ${msg.params.targetInfo.url}`);
60
+ }
61
+ }
62
+ });
63
+ });
64
+ }
65
+
66
+ async send(method, params = {}, sessionId = null) {
67
+ const id = ++this.messageId;
68
+ const msg = { id, method, params };
69
+ if (sessionId) msg.sessionId = sessionId;
70
+
71
+ this.ws.send(JSON.stringify(msg));
72
+
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => {
75
+ this.pending.delete(id);
76
+ reject(new Error(`Timeout: ${method}`));
77
+ }, 30000);
78
+
79
+ this.pending.set(id, {
80
+ resolve: (result) => { clearTimeout(timer); resolve(result); },
81
+ reject: (error) => { clearTimeout(timer); reject(error); }
82
+ });
83
+ });
84
+ }
85
+
86
+ async init() {
87
+ // Initialize connection to browser
88
+ await this.send('Target.setAutoAttach', {
89
+ autoAttach: true,
90
+ waitForDebuggerOnStart: false,
91
+ flatten: true
92
+ });
93
+
94
+ // Wait for target
95
+ if (!this.sessionId) {
96
+ await new Promise((resolve) => setTimeout(resolve, 500));
97
+ }
98
+
99
+ if (!this.sessionId) {
100
+ throw new Error('No browser tab connected. Click the extension icon on a tab first.');
101
+ }
102
+
103
+ // Enable Runtime for evaluation
104
+ await this.send('Runtime.enable', {}, this.sessionId);
105
+ }
106
+
107
+ async fetch(url) {
108
+ console.error(`[fetcher] Fetching: ${url}`);
109
+
110
+ // Use browser's fetch API with its cookies
111
+ const script = `
112
+ (async () => {
113
+ const response = await fetch(${JSON.stringify(url)}, {
114
+ credentials: 'include',
115
+ headers: {
116
+ 'Accept': 'application/json, text/plain, */*'
117
+ }
118
+ });
119
+
120
+ const contentType = response.headers.get('content-type') || '';
121
+ const status = response.status;
122
+
123
+ let body;
124
+ if (contentType.includes('application/json')) {
125
+ body = await response.json();
126
+ } else {
127
+ body = await response.text();
128
+ }
129
+
130
+ return { status, contentType, body };
131
+ })()
132
+ `;
133
+
134
+ const result = await this.send('Runtime.evaluate', {
135
+ expression: script,
136
+ awaitPromise: true,
137
+ returnByValue: true
138
+ }, this.sessionId);
139
+
140
+ if (result.exceptionDetails) {
141
+ throw new Error(result.exceptionDetails.text || 'Evaluation failed');
142
+ }
143
+
144
+ return result.result.value;
145
+ }
146
+
147
+ async navigate(url) {
148
+ console.error(`[fetcher] Navigating to: ${url}`);
149
+ await this.send('Page.navigate', { url }, this.sessionId);
150
+ await new Promise(resolve => setTimeout(resolve, 2000));
151
+ }
152
+
153
+ async getPageContent() {
154
+ const result = await this.send('Runtime.evaluate', {
155
+ expression: 'document.documentElement.outerHTML',
156
+ returnByValue: true
157
+ }, this.sessionId);
158
+ return result.result.value;
159
+ }
160
+
161
+ async screenshot(path) {
162
+ const result = await this.send('Page.captureScreenshot', {
163
+ format: 'png'
164
+ }, this.sessionId);
165
+
166
+ const fs = require('fs');
167
+ fs.writeFileSync(path, Buffer.from(result.data, 'base64'));
168
+ console.error(`[fetcher] Screenshot saved: ${path}`);
169
+ }
170
+
171
+ close() {
172
+ if (this.ws) {
173
+ this.ws.close();
174
+ }
175
+ }
176
+ }
177
+
178
+ async function main() {
179
+ const args = process.argv.slice(2);
180
+
181
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
182
+ console.log(`
183
+ Usage: fetch-via-browser [options] <url>
184
+
185
+ Fetch URLs using your authenticated browser session.
186
+ Requires the relay server running and extension connected.
187
+
188
+ Options:
189
+ --output, -o <file> Save output to file
190
+ --raw Output raw response (don't pretty-print JSON)
191
+ --navigate Navigate to URL instead of fetching via XHR
192
+ --screenshot <file> Take screenshot after fetching
193
+ --help, -h Show this help
194
+
195
+ Examples:
196
+ fetch-via-browser https://www.reddit.com/r/programming.json
197
+ fetch-via-browser -o result.json https://www.reddit.com/user/spez/about.json
198
+ fetch-via-browser --navigate --screenshot shot.png https://old.reddit.com/r/webdev
199
+ `);
200
+ process.exit(0);
201
+ }
202
+
203
+ let url = null;
204
+ let outputFile = null;
205
+ let raw = false;
206
+ let navigate = false;
207
+ let screenshot = null;
208
+
209
+ for (let i = 0; i < args.length; i++) {
210
+ if (args[i] === '--output' || args[i] === '-o') {
211
+ outputFile = args[++i];
212
+ } else if (args[i] === '--raw') {
213
+ raw = true;
214
+ } else if (args[i] === '--navigate') {
215
+ navigate = true;
216
+ } else if (args[i] === '--screenshot') {
217
+ screenshot = args[++i];
218
+ } else if (!args[i].startsWith('-')) {
219
+ url = args[i];
220
+ }
221
+ }
222
+
223
+ if (!url) {
224
+ console.error('Error: URL required');
225
+ process.exit(1);
226
+ }
227
+
228
+ const fetcher = new BrowserFetcher();
229
+
230
+ try {
231
+ await fetcher.connect();
232
+ await fetcher.init();
233
+
234
+ let result;
235
+
236
+ if (navigate) {
237
+ await fetcher.navigate(url);
238
+ result = await fetcher.getPageContent();
239
+ } else {
240
+ result = await fetcher.fetch(url);
241
+ }
242
+
243
+ if (screenshot) {
244
+ await fetcher.screenshot(screenshot);
245
+ }
246
+
247
+ // Output
248
+ let output;
249
+ if (typeof result === 'object') {
250
+ if (result.body && typeof result.body === 'object') {
251
+ output = raw ? JSON.stringify(result.body) : JSON.stringify(result.body, null, 2);
252
+ } else {
253
+ output = raw ? JSON.stringify(result) : JSON.stringify(result, null, 2);
254
+ }
255
+ } else {
256
+ output = result;
257
+ }
258
+
259
+ if (outputFile) {
260
+ require('fs').writeFileSync(outputFile, output);
261
+ console.error(`[fetcher] Saved to: ${outputFile}`);
262
+ } else {
263
+ console.log(output);
264
+ }
265
+
266
+ } catch (err) {
267
+ console.error(`Error: ${err.message}`);
268
+ process.exit(1);
269
+ } finally {
270
+ fetcher.close();
271
+ }
272
+ }
273
+
274
+ main();