glidercli 0.2.0 → 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.
- package/README.md +70 -9
- package/bin/glider.js +615 -68
- package/lib/bexplore.js +815 -0
- package/lib/bextract.js +236 -0
- package/lib/bfetch.js +274 -0
- package/lib/bserve.js +43 -7
- package/lib/bspawn.js +154 -0
- package/lib/bwindow.js +335 -0
- package/lib/cdp-direct.js +305 -0
- package/lib/glider-daemon.sh +31 -0
- package/package.json +1 -1
package/lib/bextract.js
ADDED
|
@@ -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();
|
package/lib/bserve.js
CHANGED
|
@@ -33,6 +33,18 @@ const server = http.createServer((req, res) => {
|
|
|
33
33
|
} else if (req.url === '/targets') {
|
|
34
34
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
35
35
|
res.end(JSON.stringify(Array.from(connectedTargets.values())));
|
|
36
|
+
} else if (req.url === '/attach' && req.method === 'POST') {
|
|
37
|
+
// Trigger extension to attach active tab
|
|
38
|
+
(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const result = await sendToExtension({ method: 'attachActiveTab', params: {} });
|
|
41
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
42
|
+
res.end(JSON.stringify(result));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
36
48
|
} else if (req.url === '/cdp' && req.method === 'POST') {
|
|
37
49
|
// HTTP POST endpoint for CDP commands
|
|
38
50
|
let body = '';
|
|
@@ -111,6 +123,8 @@ function handleExtensionConnection(ws) {
|
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
function handleExtensionMessage(msg) {
|
|
126
|
+
console.log('[relay] Extension message:', JSON.stringify(msg).slice(0, 200));
|
|
127
|
+
|
|
114
128
|
// Response to our request
|
|
115
129
|
if (msg.id !== undefined) {
|
|
116
130
|
const pending = pendingRequests.get(msg.id);
|
|
@@ -235,6 +249,24 @@ async function sendToExtension({ method, params, timeout = 30000 }) {
|
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
async function routeCDPCommand({ method, params, sessionId }) {
|
|
252
|
+
// Target.* commands that operate at browser level don't need sessionId
|
|
253
|
+
const browserLevelCommands = [
|
|
254
|
+
'Target.createTarget',
|
|
255
|
+
'Target.closeTarget',
|
|
256
|
+
'Target.activateTarget',
|
|
257
|
+
'Target.getTargets',
|
|
258
|
+
'Target.setAutoAttach',
|
|
259
|
+
'Target.setDiscoverTargets',
|
|
260
|
+
'Target.attachToTarget',
|
|
261
|
+
'Target.getTargetInfo',
|
|
262
|
+
'Browser.getVersion'
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
// Auto-pick first session only for session-scoped commands
|
|
266
|
+
if (!sessionId && connectedTargets.size > 0 && !browserLevelCommands.includes(method)) {
|
|
267
|
+
sessionId = Array.from(connectedTargets.values())[0].sessionId;
|
|
268
|
+
}
|
|
269
|
+
|
|
238
270
|
// Handle some commands locally
|
|
239
271
|
switch (method) {
|
|
240
272
|
case 'Browser.getVersion':
|
|
@@ -257,13 +289,10 @@ async function routeCDPCommand({ method, params, sessionId }) {
|
|
|
257
289
|
};
|
|
258
290
|
|
|
259
291
|
case 'Target.attachToTarget':
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
throw new Error(`Target ${targetId} not found`);
|
|
292
|
+
// Forward to extension - it handles the actual attachment
|
|
293
|
+
// The extension will send back Target.attachedToTarget event
|
|
294
|
+
console.log(`[relay] Forwarding Target.attachToTarget to extension for target: ${params?.targetId}`);
|
|
295
|
+
break;
|
|
267
296
|
|
|
268
297
|
case 'Target.getTargetInfo':
|
|
269
298
|
if (params?.targetId) {
|
|
@@ -279,6 +308,13 @@ async function routeCDPCommand({ method, params, sessionId }) {
|
|
|
279
308
|
}
|
|
280
309
|
const first = Array.from(connectedTargets.values())[0];
|
|
281
310
|
return { targetInfo: first?.targetInfo };
|
|
311
|
+
|
|
312
|
+
case 'Target.createTarget':
|
|
313
|
+
case 'Target.closeTarget':
|
|
314
|
+
case 'Target.activateTarget':
|
|
315
|
+
// These MUST go to extension - they control browser windows/tabs
|
|
316
|
+
console.log(`[relay] Forwarding ${method} to extension:`, JSON.stringify(params).slice(0, 100));
|
|
317
|
+
break;
|
|
282
318
|
}
|
|
283
319
|
|
|
284
320
|
// Forward to extension
|