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.
package/lib/bserve.js ADDED
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * browser-relay-server.js
4
+ * Minimal CDP relay server - connects to Chrome extension for browser automation
5
+ * Based on playwriter architecture but stripped down for direct scripting
6
+ */
7
+
8
+ const { WebSocketServer, WebSocket } = require('ws');
9
+ const http = require('http');
10
+
11
+ const PORT = process.env.RELAY_PORT || 19988;
12
+ const HOST = '127.0.0.1';
13
+
14
+ // State
15
+ let extensionWs = null;
16
+ const playwrightClients = new Map();
17
+ const connectedTargets = new Map();
18
+ const pendingRequests = new Map();
19
+ let messageId = 0;
20
+
21
+ // Create HTTP server
22
+ const server = http.createServer((req, res) => {
23
+ if (req.url === '/') {
24
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
25
+ res.end('OK');
26
+ } else if (req.url === '/status') {
27
+ res.writeHead(200, { 'Content-Type': 'application/json' });
28
+ res.end(JSON.stringify({
29
+ extension: extensionWs !== null,
30
+ targets: connectedTargets.size,
31
+ clients: playwrightClients.size
32
+ }));
33
+ } else if (req.url === '/targets') {
34
+ res.writeHead(200, { 'Content-Type': 'application/json' });
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
+ })();
48
+ } else if (req.url === '/cdp' && req.method === 'POST') {
49
+ // HTTP POST endpoint for CDP commands
50
+ let body = '';
51
+ req.on('data', chunk => body += chunk);
52
+ req.on('end', async () => {
53
+ try {
54
+ const { method, params, sessionId } = JSON.parse(body);
55
+ const result = await routeCDPCommand({ method, params, sessionId });
56
+ res.writeHead(200, { 'Content-Type': 'application/json' });
57
+ res.end(JSON.stringify(result));
58
+ } catch (e) {
59
+ res.writeHead(500, { 'Content-Type': 'application/json' });
60
+ res.end(JSON.stringify({ error: e.message }));
61
+ }
62
+ });
63
+ } else {
64
+ res.writeHead(404);
65
+ res.end('Not Found');
66
+ }
67
+ });
68
+
69
+ // WebSocket server
70
+ const wss = new WebSocketServer({ server });
71
+
72
+ wss.on('connection', (ws, req) => {
73
+ const path = req.url;
74
+
75
+ if (path === '/extension') {
76
+ handleExtensionConnection(ws);
77
+ } else if (path.startsWith('/cdp')) {
78
+ const clientId = path.split('/')[2] || 'default';
79
+ handleCDPConnection(ws, clientId);
80
+ } else {
81
+ ws.close(1000, 'Unknown path');
82
+ }
83
+ });
84
+
85
+ function handleExtensionConnection(ws) {
86
+ if (extensionWs) {
87
+ console.log('[relay] Replacing existing extension connection');
88
+ extensionWs.close(4001, 'Replaced');
89
+ connectedTargets.clear();
90
+ }
91
+
92
+ extensionWs = ws;
93
+ console.log('[relay] Extension connected');
94
+
95
+ // Ping to keep alive
96
+ const pingInterval = setInterval(() => {
97
+ if (ws.readyState === WebSocket.OPEN) {
98
+ ws.send(JSON.stringify({ method: 'ping' }));
99
+ }
100
+ }, 5000);
101
+
102
+ ws.on('message', (data) => {
103
+ try {
104
+ const msg = JSON.parse(data.toString());
105
+ handleExtensionMessage(msg);
106
+ } catch (e) {
107
+ console.error('[relay] Error parsing extension message:', e);
108
+ }
109
+ });
110
+
111
+ ws.on('close', () => {
112
+ console.log('[relay] Extension disconnected');
113
+ clearInterval(pingInterval);
114
+ extensionWs = null;
115
+ connectedTargets.clear();
116
+
117
+ // Notify all clients
118
+ for (const client of playwrightClients.values()) {
119
+ client.ws.close(1000, 'Extension disconnected');
120
+ }
121
+ playwrightClients.clear();
122
+ });
123
+ }
124
+
125
+ function handleExtensionMessage(msg) {
126
+ console.log('[relay] Extension message:', JSON.stringify(msg).slice(0, 200));
127
+
128
+ // Response to our request
129
+ if (msg.id !== undefined) {
130
+ const pending = pendingRequests.get(msg.id);
131
+ if (pending) {
132
+ pendingRequests.delete(msg.id);
133
+ if (msg.error) {
134
+ pending.reject(new Error(msg.error));
135
+ } else {
136
+ pending.resolve(msg.result);
137
+ }
138
+ }
139
+ return;
140
+ }
141
+
142
+ // Pong
143
+ if (msg.method === 'pong') return;
144
+
145
+ // Log from extension
146
+ if (msg.method === 'log') {
147
+ console.log(`[ext:${msg.params.level}]`, ...msg.params.args);
148
+ return;
149
+ }
150
+
151
+ // CDP event from extension
152
+ if (msg.method === 'forwardCDPEvent') {
153
+ const { method, params, sessionId } = msg.params;
154
+
155
+ // Track targets
156
+ if (method === 'Target.attachedToTarget') {
157
+ connectedTargets.set(params.sessionId, {
158
+ sessionId: params.sessionId,
159
+ targetId: params.targetInfo.targetId,
160
+ targetInfo: params.targetInfo
161
+ });
162
+ console.log(`[relay] Target attached: ${params.targetInfo.url}`);
163
+ } else if (method === 'Target.detachedFromTarget') {
164
+ connectedTargets.delete(params.sessionId);
165
+ console.log(`[relay] Target detached: ${params.sessionId}`);
166
+ } else if (method === 'Target.targetInfoChanged') {
167
+ const target = Array.from(connectedTargets.values())
168
+ .find(t => t.targetId === params.targetInfo.targetId);
169
+ if (target) {
170
+ target.targetInfo = params.targetInfo;
171
+ }
172
+ }
173
+
174
+ // Forward to all CDP clients
175
+ const cdpEvent = { method, params, sessionId };
176
+ for (const client of playwrightClients.values()) {
177
+ client.ws.send(JSON.stringify(cdpEvent));
178
+ }
179
+ }
180
+ }
181
+
182
+ function handleCDPConnection(ws, clientId) {
183
+ if (playwrightClients.has(clientId)) {
184
+ ws.close(1000, 'Client ID already connected');
185
+ return;
186
+ }
187
+
188
+ playwrightClients.set(clientId, { id: clientId, ws });
189
+ console.log(`[relay] CDP client connected: ${clientId}`);
190
+
191
+ ws.on('message', async (data) => {
192
+ try {
193
+ const msg = JSON.parse(data.toString());
194
+ const { id, method, params, sessionId } = msg;
195
+
196
+ if (!extensionWs) {
197
+ ws.send(JSON.stringify({ id, error: { message: 'Extension not connected' } }));
198
+ return;
199
+ }
200
+
201
+ try {
202
+ const result = await routeCDPCommand({ method, params, sessionId });
203
+ ws.send(JSON.stringify({ id, sessionId, result }));
204
+
205
+ // Send attachedToTarget events after setAutoAttach
206
+ if (method === 'Target.setAutoAttach' && !sessionId) {
207
+ for (const target of connectedTargets.values()) {
208
+ ws.send(JSON.stringify({
209
+ method: 'Target.attachedToTarget',
210
+ params: {
211
+ sessionId: target.sessionId,
212
+ targetInfo: { ...target.targetInfo, attached: true },
213
+ waitingForDebugger: false
214
+ }
215
+ }));
216
+ }
217
+ }
218
+ } catch (e) {
219
+ ws.send(JSON.stringify({ id, sessionId, error: { message: e.message } }));
220
+ }
221
+ } catch (e) {
222
+ console.error('[relay] Error handling CDP message:', e);
223
+ }
224
+ });
225
+
226
+ ws.on('close', () => {
227
+ playwrightClients.delete(clientId);
228
+ console.log(`[relay] CDP client disconnected: ${clientId}`);
229
+ });
230
+ }
231
+
232
+ async function sendToExtension({ method, params, timeout = 30000 }) {
233
+ if (!extensionWs) throw new Error('Extension not connected');
234
+
235
+ const id = ++messageId;
236
+ extensionWs.send(JSON.stringify({ id, method, params }));
237
+
238
+ return new Promise((resolve, reject) => {
239
+ const timer = setTimeout(() => {
240
+ pendingRequests.delete(id);
241
+ reject(new Error(`Timeout: ${method}`));
242
+ }, timeout);
243
+
244
+ pendingRequests.set(id, {
245
+ resolve: (result) => { clearTimeout(timer); resolve(result); },
246
+ reject: (error) => { clearTimeout(timer); reject(error); }
247
+ });
248
+ });
249
+ }
250
+
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
+
270
+ // Handle some commands locally
271
+ switch (method) {
272
+ case 'Browser.getVersion':
273
+ return {
274
+ protocolVersion: '1.3',
275
+ product: 'Chrome/Extension-Bridge',
276
+ revision: '1.0.0',
277
+ userAgent: 'CDP-Bridge/1.0.0',
278
+ jsVersion: 'V8'
279
+ };
280
+
281
+ case 'Target.setAutoAttach':
282
+ case 'Target.setDiscoverTargets':
283
+ return {};
284
+
285
+ case 'Target.getTargets':
286
+ return {
287
+ targetInfos: Array.from(connectedTargets.values())
288
+ .map(t => ({ ...t.targetInfo, attached: true }))
289
+ };
290
+
291
+ case 'Target.attachToTarget':
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;
296
+
297
+ case 'Target.getTargetInfo':
298
+ if (params?.targetId) {
299
+ for (const target of connectedTargets.values()) {
300
+ if (target.targetId === params.targetId) {
301
+ return { targetInfo: target.targetInfo };
302
+ }
303
+ }
304
+ }
305
+ if (sessionId) {
306
+ const target = connectedTargets.get(sessionId);
307
+ if (target) return { targetInfo: target.targetInfo };
308
+ }
309
+ const first = Array.from(connectedTargets.values())[0];
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;
318
+ }
319
+
320
+ // Forward to extension
321
+ return await sendToExtension({
322
+ method: 'forwardCDPCommand',
323
+ params: { sessionId, method, params }
324
+ });
325
+ }
326
+
327
+ // Export for use as module
328
+ module.exports = { server, wss, routeCDPCommand };
329
+
330
+ // Start server if run directly
331
+ if (require.main === module) {
332
+ server.listen(PORT, HOST, () => {
333
+ console.log(`[relay] CDP relay server running on ws://${HOST}:${PORT}`);
334
+ console.log('[relay] Endpoints:');
335
+ console.log(` - Extension: ws://${HOST}:${PORT}/extension`);
336
+ console.log(` - CDP: ws://${HOST}:${PORT}/cdp`);
337
+ console.log(` - Status: http://${HOST}:${PORT}/status`);
338
+ console.log(` - Targets: http://${HOST}:${PORT}/targets`);
339
+ });
340
+
341
+ process.on('SIGINT', () => {
342
+ console.log('\n[relay] Shutting down...');
343
+ wss.close();
344
+ server.close();
345
+ process.exit(0);
346
+ });
347
+ }
package/lib/bspawn.js ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bspawn.js - Spawn multiple browser tabs in parallel via Glider
4
+ *
5
+ * Usage:
6
+ * bspawn <url1> <url2> ... # Spawn tabs for each URL
7
+ * bspawn -f urls.txt # Read URLs from file (one per line)
8
+ * bspawn --json '["url1","url2"]' # URLs as JSON array
9
+ * cat urls.txt | bspawn - # Read from stdin
10
+ *
11
+ * Options:
12
+ * --wait <ms> Wait time after spawning (default: 3000)
13
+ * --status Show status after spawning
14
+ * --quiet Suppress output
15
+ *
16
+ * Examples:
17
+ * bspawn https://example.com https://google.com
18
+ * bspawn -f /tmp/orr-urls.txt --wait 5000
19
+ * echo "https://example.com" | bspawn -
20
+ */
21
+
22
+ const WebSocket = require('ws');
23
+ const fs = require('fs');
24
+
25
+ const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
26
+ const DEFAULT_WAIT = 3000;
27
+
28
+ async function spawnTabs(urls, options = {}) {
29
+ const { wait = DEFAULT_WAIT, quiet = false } = options;
30
+
31
+ return new Promise((resolve, reject) => {
32
+ const ws = new WebSocket(RELAY_URL);
33
+ let id = 0;
34
+ const results = [];
35
+
36
+ ws.on('open', () => {
37
+ if (!quiet) console.error(`[bspawn] Spawning ${urls.length} tabs...`);
38
+ urls.forEach(url => {
39
+ ws.send(JSON.stringify({
40
+ id: ++id,
41
+ method: 'Target.createTarget',
42
+ params: { url }
43
+ }));
44
+ });
45
+ });
46
+
47
+ ws.on('message', (data) => {
48
+ const msg = JSON.parse(data.toString());
49
+ if (msg.result?.targetId) {
50
+ results.push({ id: msg.id, targetId: msg.result.targetId });
51
+ if (!quiet) console.error(`[bspawn] Tab ${results.length}/${urls.length} created`);
52
+ }
53
+ if (msg.method === 'Target.attachedToTarget') {
54
+ if (!quiet) console.error(`[bspawn] Attached: ${msg.params?.targetInfo?.url?.substring(0, 60)}...`);
55
+ }
56
+ if (results.length === urls.length) {
57
+ setTimeout(() => {
58
+ ws.close();
59
+ resolve(results);
60
+ }, wait);
61
+ }
62
+ });
63
+
64
+ ws.on('error', (err) => reject(new Error(`WebSocket error: ${err.message}`)));
65
+
66
+ // Timeout after 30s
67
+ setTimeout(() => {
68
+ ws.close();
69
+ if (results.length > 0) resolve(results);
70
+ else reject(new Error('Timeout waiting for tabs to spawn'));
71
+ }, 30000);
72
+ });
73
+ }
74
+
75
+ async function getStatus() {
76
+ const http = require('http');
77
+ return new Promise((resolve) => {
78
+ http.get('http://127.0.0.1:19988/targets', (res) => {
79
+ let data = '';
80
+ res.on('data', chunk => data += chunk);
81
+ res.on('end', () => resolve(JSON.parse(data)));
82
+ }).on('error', () => resolve([]));
83
+ });
84
+ }
85
+
86
+ async function main() {
87
+ const args = process.argv.slice(2);
88
+ let urls = [];
89
+ let wait = DEFAULT_WAIT;
90
+ let showStatus = false;
91
+ let quiet = false;
92
+
93
+ for (let i = 0; i < args.length; i++) {
94
+ const arg = args[i];
95
+ if (arg === '--wait' || arg === '-w') {
96
+ wait = parseInt(args[++i], 10);
97
+ } else if (arg === '--status' || arg === '-s') {
98
+ showStatus = true;
99
+ } else if (arg === '--quiet' || arg === '-q') {
100
+ quiet = true;
101
+ } else if (arg === '-f' || arg === '--file') {
102
+ const file = args[++i];
103
+ urls = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean);
104
+ } else if (arg === '--json' || arg === '-j') {
105
+ urls = JSON.parse(args[++i]);
106
+ } else if (arg === '-') {
107
+ // Read from stdin
108
+ urls = fs.readFileSync(0, 'utf8').trim().split('\n').filter(Boolean);
109
+ } else if (arg === '--help' || arg === '-h') {
110
+ console.log(`
111
+ bspawn - Spawn multiple browser tabs in parallel via Glider
112
+
113
+ Usage:
114
+ bspawn <url1> <url2> ... # Spawn tabs for each URL
115
+ bspawn -f urls.txt # Read URLs from file
116
+ bspawn --json '["url1","url2"]' # URLs as JSON array
117
+ cat urls.txt | bspawn - # Read from stdin
118
+
119
+ Options:
120
+ -w, --wait <ms> Wait time after spawning (default: 3000)
121
+ -s, --status Show status after spawning
122
+ -q, --quiet Suppress output
123
+ -h, --help Show this help
124
+ `);
125
+ process.exit(0);
126
+ } else if (arg.startsWith('http')) {
127
+ urls.push(arg);
128
+ }
129
+ }
130
+
131
+ if (urls.length === 0) {
132
+ console.error('Error: No URLs provided');
133
+ process.exit(1);
134
+ }
135
+
136
+ try {
137
+ const results = await spawnTabs(urls, { wait, quiet });
138
+
139
+ if (showStatus) {
140
+ const targets = await getStatus();
141
+ console.log(JSON.stringify(targets.map(t => ({
142
+ sessionId: t.sessionId,
143
+ url: t.targetInfo?.url
144
+ })), null, 2));
145
+ } else if (!quiet) {
146
+ console.log(JSON.stringify(results, null, 2));
147
+ }
148
+ } catch (err) {
149
+ console.error(`Error: ${err.message}`);
150
+ process.exit(1);
151
+ }
152
+ }
153
+
154
+ main();