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/README.md +70 -9
- package/bin/glider.js +785 -57
- package/lib/bcdp.js +482 -0
- package/lib/beval.js +78 -0
- package/lib/bexplore.js +815 -0
- package/lib/bextract.js +236 -0
- package/lib/bfetch.js +274 -0
- package/lib/bserve.js +347 -0
- 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 +8 -2
- package/.git-personal-enforced +0 -4
- package/.github/hooks/post-checkout +0 -24
- package/.github/hooks/post-commit +0 -13
- package/.github/hooks/pre-commit +0 -30
- package/.github/hooks/pre-push +0 -13
- package/.github/scripts/health-check.sh +0 -127
- package/.github/scripts/setup.sh +0 -19
- package/.github/workflows/release.yml +0 -19
- package/assets/icons/.gitkeep +0 -0
- package/assets/icons/claude.webp +0 -0
- package/assets/icons/glider-blue-squircle.webp +0 -0
- package/assets/icons/ralph-wiggum.webp +0 -0
- package/repo.config.json +0 -31
package/lib/bwindow.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bwindow.js - Multi-window/tab management for Glider
|
|
4
|
+
*
|
|
5
|
+
* Commands:
|
|
6
|
+
* glider window new [url] Create new browser window
|
|
7
|
+
* glider window close <targetId> Close specific tab/window
|
|
8
|
+
* glider window closeall Close all tabs created by Glider
|
|
9
|
+
* glider window list List all windows/tabs
|
|
10
|
+
* glider window focus <targetId> Bring tab to foreground
|
|
11
|
+
*
|
|
12
|
+
* The key insight: tabs created with newWindow:true CAN be closed via Target.closeTarget
|
|
13
|
+
* Tabs created in the main window CANNOT be closed (Chrome security)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const WebSocket = require('ws');
|
|
17
|
+
|
|
18
|
+
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
|
|
19
|
+
|
|
20
|
+
class WindowManager {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.ws = null;
|
|
23
|
+
this.messageId = 0;
|
|
24
|
+
this.pending = new Map();
|
|
25
|
+
this.targets = new Map(); // targetId -> { sessionId, url, windowId, createdByGlider }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async connect() {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
this.ws = new WebSocket(RELAY_URL);
|
|
31
|
+
this.ws.on('open', resolve);
|
|
32
|
+
this.ws.on('error', reject);
|
|
33
|
+
this.ws.on('message', (data) => this._handleMessage(JSON.parse(data.toString())));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_handleMessage(msg) {
|
|
38
|
+
if (msg.id !== undefined) {
|
|
39
|
+
const pending = this.pending.get(msg.id);
|
|
40
|
+
if (pending) {
|
|
41
|
+
this.pending.delete(msg.id);
|
|
42
|
+
msg.error ? pending.reject(new Error(JSON.stringify(msg.error))) : pending.resolve(msg.result);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Track targets
|
|
48
|
+
if (msg.method === 'Target.targetCreated') {
|
|
49
|
+
const info = msg.params.targetInfo;
|
|
50
|
+
if (info.type === 'page') {
|
|
51
|
+
this.targets.set(info.targetId, {
|
|
52
|
+
targetId: info.targetId,
|
|
53
|
+
url: info.url,
|
|
54
|
+
type: info.type
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (msg.method === 'Target.attachedToTarget') {
|
|
60
|
+
const { sessionId, targetInfo } = msg.params;
|
|
61
|
+
if (this.targets.has(targetInfo.targetId)) {
|
|
62
|
+
this.targets.get(targetInfo.targetId).sessionId = sessionId;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (msg.method === 'Target.targetDestroyed') {
|
|
67
|
+
this.targets.delete(msg.params.targetId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async send(method, params = {}) {
|
|
72
|
+
const id = ++this.messageId;
|
|
73
|
+
const msg = { id, method, params };
|
|
74
|
+
this.ws.send(JSON.stringify(msg));
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
this.pending.delete(id);
|
|
79
|
+
reject(new Error(`Timeout: ${method}`));
|
|
80
|
+
}, 30000);
|
|
81
|
+
this.pending.set(id, {
|
|
82
|
+
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
83
|
+
reject: (e) => { clearTimeout(timer); reject(e); }
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async init() {
|
|
89
|
+
// Enable target discovery
|
|
90
|
+
await this.send('Target.setDiscoverTargets', { discover: true });
|
|
91
|
+
await new Promise(r => setTimeout(r, 300));
|
|
92
|
+
|
|
93
|
+
// Get existing targets
|
|
94
|
+
try {
|
|
95
|
+
const { targetInfos } = await this.send('Target.getTargets');
|
|
96
|
+
for (const info of targetInfos) {
|
|
97
|
+
if (info.type === 'page') {
|
|
98
|
+
this.targets.set(info.targetId, {
|
|
99
|
+
targetId: info.targetId,
|
|
100
|
+
url: info.url,
|
|
101
|
+
type: info.type
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
// Target.getTargets may not be supported by relay
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a new browser window (not just a tab)
|
|
112
|
+
* Tabs in this window CAN be closed via Target.closeTarget
|
|
113
|
+
*/
|
|
114
|
+
async createWindow(url = 'about:blank') {
|
|
115
|
+
const { targetId } = await this.send('Target.createTarget', {
|
|
116
|
+
url,
|
|
117
|
+
newWindow: true // CRITICAL: creates separate window
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Wait for target to be ready
|
|
121
|
+
await new Promise(r => setTimeout(r, 500));
|
|
122
|
+
|
|
123
|
+
// Attach to get sessionId
|
|
124
|
+
try {
|
|
125
|
+
const { sessionId } = await this.send('Target.attachToTarget', {
|
|
126
|
+
targetId,
|
|
127
|
+
flatten: true
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.targets.set(targetId, {
|
|
131
|
+
targetId,
|
|
132
|
+
sessionId,
|
|
133
|
+
url,
|
|
134
|
+
createdByGlider: true,
|
|
135
|
+
isWindow: true
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { targetId, sessionId };
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// May already be attached
|
|
141
|
+
return { targetId };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create a tab in the current window (will go to focused window)
|
|
147
|
+
*/
|
|
148
|
+
async createTab(url = 'about:blank') {
|
|
149
|
+
const { targetId } = await this.send('Target.createTarget', { url });
|
|
150
|
+
await new Promise(r => setTimeout(r, 500));
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const { sessionId } = await this.send('Target.attachToTarget', {
|
|
154
|
+
targetId,
|
|
155
|
+
flatten: true
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.targets.set(targetId, {
|
|
159
|
+
targetId,
|
|
160
|
+
sessionId,
|
|
161
|
+
url,
|
|
162
|
+
createdByGlider: true,
|
|
163
|
+
isWindow: false
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return { targetId, sessionId };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { targetId };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Close a specific tab/window
|
|
174
|
+
* Only works for tabs created with newWindow:true or via Target.createTarget
|
|
175
|
+
*/
|
|
176
|
+
async closeTarget(targetId) {
|
|
177
|
+
try {
|
|
178
|
+
await this.send('Target.closeTarget', { targetId });
|
|
179
|
+
this.targets.delete(targetId);
|
|
180
|
+
return { success: true, targetId };
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Fallback: try window.close() via Runtime.evaluate
|
|
183
|
+
const target = this.targets.get(targetId);
|
|
184
|
+
if (target?.sessionId) {
|
|
185
|
+
try {
|
|
186
|
+
await this.send('Runtime.evaluate', {
|
|
187
|
+
expression: 'window.close()',
|
|
188
|
+
returnByValue: true
|
|
189
|
+
}, target.sessionId);
|
|
190
|
+
this.targets.delete(targetId);
|
|
191
|
+
return { success: true, targetId, method: 'window.close' };
|
|
192
|
+
} catch (e2) {
|
|
193
|
+
return { success: false, targetId, error: e2.message };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { success: false, targetId, error: e.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Close all tabs created by Glider
|
|
202
|
+
*/
|
|
203
|
+
async closeAll() {
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const [targetId, info] of this.targets) {
|
|
206
|
+
if (info.createdByGlider) {
|
|
207
|
+
const result = await this.closeTarget(targetId);
|
|
208
|
+
results.push(result);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Bring a tab to the foreground
|
|
216
|
+
*/
|
|
217
|
+
async focusTarget(targetId) {
|
|
218
|
+
try {
|
|
219
|
+
await this.send('Target.activateTarget', { targetId });
|
|
220
|
+
return { success: true, targetId };
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return { success: false, targetId, error: e.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* List all targets
|
|
228
|
+
*/
|
|
229
|
+
list() {
|
|
230
|
+
return Array.from(this.targets.values());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
close() {
|
|
234
|
+
if (this.ws) this.ws.close();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// CLI
|
|
239
|
+
async function main() {
|
|
240
|
+
const args = process.argv.slice(2);
|
|
241
|
+
const cmd = args[0];
|
|
242
|
+
|
|
243
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
244
|
+
console.log(`
|
|
245
|
+
bwindow - Multi-window/tab management for Glider
|
|
246
|
+
|
|
247
|
+
Commands:
|
|
248
|
+
new [url] Create new browser window (closeable)
|
|
249
|
+
tab [url] Create new tab in current window
|
|
250
|
+
close <targetId> Close specific tab/window
|
|
251
|
+
closeall Close all Glider-created tabs
|
|
252
|
+
list List all windows/tabs
|
|
253
|
+
focus <targetId> Bring tab to foreground
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
bwindow new https://google.com
|
|
257
|
+
bwindow close ABC123DEF456
|
|
258
|
+
bwindow list
|
|
259
|
+
`);
|
|
260
|
+
process.exit(0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const wm = new WindowManager();
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await wm.connect();
|
|
267
|
+
await wm.init();
|
|
268
|
+
|
|
269
|
+
switch (cmd) {
|
|
270
|
+
case 'new':
|
|
271
|
+
case 'window': {
|
|
272
|
+
const url = args[1] || 'about:blank';
|
|
273
|
+
const result = await wm.createWindow(url);
|
|
274
|
+
console.log(JSON.stringify(result, null, 2));
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'tab': {
|
|
279
|
+
const url = args[1] || 'about:blank';
|
|
280
|
+
const result = await wm.createTab(url);
|
|
281
|
+
console.log(JSON.stringify(result, null, 2));
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
case 'close': {
|
|
286
|
+
const targetId = args[1];
|
|
287
|
+
if (!targetId) {
|
|
288
|
+
console.error('Error: targetId required');
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const result = await wm.closeTarget(targetId);
|
|
292
|
+
console.log(JSON.stringify(result, null, 2));
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case 'closeall': {
|
|
297
|
+
const results = await wm.closeAll();
|
|
298
|
+
console.log(JSON.stringify(results, null, 2));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
case 'list': {
|
|
303
|
+
const targets = wm.list();
|
|
304
|
+
console.log(JSON.stringify(targets, null, 2));
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
case 'focus': {
|
|
309
|
+
const targetId = args[1];
|
|
310
|
+
if (!targetId) {
|
|
311
|
+
console.error('Error: targetId required');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
const result = await wm.focusTarget(targetId);
|
|
315
|
+
console.log(JSON.stringify(result, null, 2));
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
default:
|
|
320
|
+
console.error(`Unknown command: ${cmd}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error(`Error: ${err.message}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
} finally {
|
|
327
|
+
wm.close();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
main();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = { WindowManager };
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cdp-direct.js - Direct Chrome DevTools Protocol connection
|
|
4
|
+
* No relay, no extension, no bullshit. Just straight to Chrome.
|
|
5
|
+
*
|
|
6
|
+
* Chrome must be running with --remote-debugging-port=9222
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
const http = require('http');
|
|
11
|
+
|
|
12
|
+
const DEBUG_PORT = process.env.GLIDER_DEBUG_PORT || 9222;
|
|
13
|
+
const DEBUG_HOST = '127.0.0.1';
|
|
14
|
+
|
|
15
|
+
class DirectCDP {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ws = null;
|
|
18
|
+
this.messageId = 0;
|
|
19
|
+
this.pending = new Map();
|
|
20
|
+
this.targetId = null;
|
|
21
|
+
this.sessionId = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get list of debuggable targets from Chrome
|
|
25
|
+
async getTargets() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
http.get(`http://${DEBUG_HOST}:${DEBUG_PORT}/json/list`, (res) => {
|
|
28
|
+
let data = '';
|
|
29
|
+
res.on('data', chunk => data += chunk);
|
|
30
|
+
res.on('end', () => {
|
|
31
|
+
try {
|
|
32
|
+
resolve(JSON.parse(data));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
reject(new Error('Failed to parse targets'));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}).on('error', reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get Chrome version info
|
|
42
|
+
async getVersion() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
http.get(`http://${DEBUG_HOST}:${DEBUG_PORT}/json/version`, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', chunk => data += chunk);
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
resolve(JSON.parse(data));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
reject(new Error('Failed to parse version'));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}).on('error', reject);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Connect to a specific target (tab)
|
|
59
|
+
async connect(targetOrUrl) {
|
|
60
|
+
let wsUrl;
|
|
61
|
+
|
|
62
|
+
if (typeof targetOrUrl === 'string' && targetOrUrl.startsWith('ws://')) {
|
|
63
|
+
wsUrl = targetOrUrl;
|
|
64
|
+
} else {
|
|
65
|
+
// Find target by URL pattern or use first page
|
|
66
|
+
const targets = await this.getTargets();
|
|
67
|
+
let target;
|
|
68
|
+
|
|
69
|
+
if (typeof targetOrUrl === 'string') {
|
|
70
|
+
target = targets.find(t => t.url?.includes(targetOrUrl) && t.type === 'page');
|
|
71
|
+
}
|
|
72
|
+
if (!target) {
|
|
73
|
+
target = targets.find(t => t.type === 'page' && !t.url?.startsWith('chrome://') && !t.url?.startsWith('devtools://'));
|
|
74
|
+
}
|
|
75
|
+
if (!target) {
|
|
76
|
+
target = targets.find(t => t.type === 'page');
|
|
77
|
+
}
|
|
78
|
+
if (!target) {
|
|
79
|
+
throw new Error('No debuggable page found');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
wsUrl = target.webSocketDebuggerUrl;
|
|
83
|
+
this.targetId = target.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
this.ws = new WebSocket(wsUrl);
|
|
88
|
+
|
|
89
|
+
this.ws.on('open', async () => {
|
|
90
|
+
// Enable required domains
|
|
91
|
+
await this.send('Runtime.enable');
|
|
92
|
+
await this.send('Page.enable');
|
|
93
|
+
resolve();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.ws.on('error', reject);
|
|
97
|
+
|
|
98
|
+
this.ws.on('message', (data) => {
|
|
99
|
+
const msg = JSON.parse(data.toString());
|
|
100
|
+
if (msg.id !== undefined) {
|
|
101
|
+
const pending = this.pending.get(msg.id);
|
|
102
|
+
if (pending) {
|
|
103
|
+
this.pending.delete(msg.id);
|
|
104
|
+
if (msg.error) {
|
|
105
|
+
pending.reject(new Error(msg.error.message));
|
|
106
|
+
} else {
|
|
107
|
+
pending.resolve(msg.result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.ws.on('close', () => {
|
|
114
|
+
this.ws = null;
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Send CDP command
|
|
120
|
+
async send(method, params = {}) {
|
|
121
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
122
|
+
throw new Error('Not connected');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const id = ++this.messageId;
|
|
126
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
this.pending.delete(id);
|
|
131
|
+
reject(new Error(`Timeout: ${method}`));
|
|
132
|
+
}, 30000);
|
|
133
|
+
|
|
134
|
+
this.pending.set(id, {
|
|
135
|
+
resolve: (r) => { clearTimeout(timer); resolve(r); },
|
|
136
|
+
reject: (e) => { clearTimeout(timer); reject(e); }
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// High-level helpers
|
|
142
|
+
async evaluate(expression) {
|
|
143
|
+
const result = await this.send('Runtime.evaluate', {
|
|
144
|
+
expression,
|
|
145
|
+
returnByValue: true,
|
|
146
|
+
awaitPromise: true
|
|
147
|
+
});
|
|
148
|
+
if (result.exceptionDetails) {
|
|
149
|
+
throw new Error(result.exceptionDetails.text || 'Evaluation failed');
|
|
150
|
+
}
|
|
151
|
+
return result.result?.value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async navigate(url) {
|
|
155
|
+
return this.send('Page.navigate', { url });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async screenshot(format = 'png') {
|
|
159
|
+
return this.send('Page.captureScreenshot', { format });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getTitle() {
|
|
163
|
+
return this.evaluate('document.title');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getUrl() {
|
|
167
|
+
return this.evaluate('window.location.href');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getText() {
|
|
171
|
+
return this.evaluate('document.body.innerText');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getHtml(selector) {
|
|
175
|
+
if (selector) {
|
|
176
|
+
return this.evaluate(`document.querySelector('${selector}')?.outerHTML`);
|
|
177
|
+
}
|
|
178
|
+
return this.evaluate('document.documentElement.outerHTML');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async click(selector) {
|
|
182
|
+
return this.evaluate(`
|
|
183
|
+
(() => {
|
|
184
|
+
const el = document.querySelector('${selector}');
|
|
185
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
186
|
+
el.click();
|
|
187
|
+
return true;
|
|
188
|
+
})()
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async type(selector, text) {
|
|
193
|
+
return this.evaluate(`
|
|
194
|
+
(() => {
|
|
195
|
+
const el = document.querySelector('${selector}');
|
|
196
|
+
if (!el) throw new Error('Element not found: ${selector}');
|
|
197
|
+
el.focus();
|
|
198
|
+
el.value = '${text.replace(/'/g, "\\'")}';
|
|
199
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
200
|
+
return true;
|
|
201
|
+
})()
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
close() {
|
|
206
|
+
if (this.ws) {
|
|
207
|
+
this.ws.close();
|
|
208
|
+
this.ws = null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if Chrome debugging is available
|
|
214
|
+
async function checkChrome() {
|
|
215
|
+
try {
|
|
216
|
+
const cdp = new DirectCDP();
|
|
217
|
+
const version = await cdp.getVersion();
|
|
218
|
+
return { ok: true, version };
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return { ok: false, error: e.message };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Export for use as module
|
|
225
|
+
module.exports = { DirectCDP, checkChrome, DEBUG_PORT, DEBUG_HOST };
|
|
226
|
+
|
|
227
|
+
// CLI mode
|
|
228
|
+
if (require.main === module) {
|
|
229
|
+
const cmd = process.argv[2];
|
|
230
|
+
const arg = process.argv.slice(3).join(' ');
|
|
231
|
+
|
|
232
|
+
(async () => {
|
|
233
|
+
const cdp = new DirectCDP();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
if (cmd === 'check' || cmd === 'status') {
|
|
237
|
+
const check = await checkChrome();
|
|
238
|
+
if (check.ok) {
|
|
239
|
+
console.log('Chrome debugging available');
|
|
240
|
+
console.log('Browser:', check.version.Browser);
|
|
241
|
+
const targets = await cdp.getTargets();
|
|
242
|
+
console.log('Tabs:', targets.filter(t => t.type === 'page').length);
|
|
243
|
+
} else {
|
|
244
|
+
console.error('Chrome debugging not available:', check.error);
|
|
245
|
+
console.error('Run: glider chrome-start');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (cmd === 'targets' || cmd === 'tabs') {
|
|
252
|
+
const targets = await cdp.getTargets();
|
|
253
|
+
targets.filter(t => t.type === 'page').forEach((t, i) => {
|
|
254
|
+
console.log(`[${i + 1}] ${t.title}`);
|
|
255
|
+
console.log(` ${t.url}`);
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Commands that need connection
|
|
261
|
+
await cdp.connect();
|
|
262
|
+
|
|
263
|
+
switch (cmd) {
|
|
264
|
+
case 'eval':
|
|
265
|
+
const result = await cdp.evaluate(arg || 'document.title');
|
|
266
|
+
console.log(JSON.stringify(result, null, 2));
|
|
267
|
+
break;
|
|
268
|
+
case 'title':
|
|
269
|
+
console.log(await cdp.getTitle());
|
|
270
|
+
break;
|
|
271
|
+
case 'url':
|
|
272
|
+
console.log(await cdp.getUrl());
|
|
273
|
+
break;
|
|
274
|
+
case 'text':
|
|
275
|
+
console.log(await cdp.getText());
|
|
276
|
+
break;
|
|
277
|
+
case 'html':
|
|
278
|
+
console.log(await cdp.getHtml(arg));
|
|
279
|
+
break;
|
|
280
|
+
case 'goto':
|
|
281
|
+
await cdp.navigate(arg);
|
|
282
|
+
console.log('Navigated to:', arg);
|
|
283
|
+
break;
|
|
284
|
+
case 'click':
|
|
285
|
+
await cdp.click(arg);
|
|
286
|
+
console.log('Clicked:', arg);
|
|
287
|
+
break;
|
|
288
|
+
case 'screenshot':
|
|
289
|
+
const ss = await cdp.screenshot();
|
|
290
|
+
const path = arg || `/tmp/screenshot-${Date.now()}.png`;
|
|
291
|
+
require('fs').writeFileSync(path, Buffer.from(ss.data, 'base64'));
|
|
292
|
+
console.log('Screenshot saved:', path);
|
|
293
|
+
break;
|
|
294
|
+
default:
|
|
295
|
+
console.log('Usage: cdp-direct <command> [args]');
|
|
296
|
+
console.log('Commands: check, targets, eval, title, url, text, html, goto, click, screenshot');
|
|
297
|
+
}
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.error('Error:', e.message);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
} finally {
|
|
302
|
+
cdp.close();
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Glider daemon - respawns relay forever, fuck launchd throttling
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
BSERVE="$SCRIPT_DIR/bserve.js"
|
|
6
|
+
LOG_DIR="$HOME/.glider"
|
|
7
|
+
PID_FILE="$LOG_DIR/daemon.pid"
|
|
8
|
+
|
|
9
|
+
mkdir -p "$LOG_DIR"
|
|
10
|
+
|
|
11
|
+
# Kill any existing
|
|
12
|
+
if [ -f "$PID_FILE" ]; then
|
|
13
|
+
kill $(cat "$PID_FILE") 2>/dev/null
|
|
14
|
+
rm "$PID_FILE"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo $$ > "$PID_FILE"
|
|
18
|
+
|
|
19
|
+
cleanup() {
|
|
20
|
+
rm -f "$PID_FILE"
|
|
21
|
+
exit 0
|
|
22
|
+
}
|
|
23
|
+
trap cleanup SIGTERM SIGINT
|
|
24
|
+
|
|
25
|
+
while true; do
|
|
26
|
+
echo "[$(date)] Starting relay..." >> "$LOG_DIR/daemon.log"
|
|
27
|
+
node "$BSERVE" >> "$LOG_DIR/daemon.log" 2>&1
|
|
28
|
+
EXIT_CODE=$?
|
|
29
|
+
echo "[$(date)] Relay exited with code $EXIT_CODE, restarting in 2s..." >> "$LOG_DIR/daemon.log"
|
|
30
|
+
sleep 2
|
|
31
|
+
done
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glidercli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Browser automation CLI with autonomous loop execution. Control Chrome via CDP, run YAML task files, execute in Ralph Wiggum loops.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,5 +33,11 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"ws": "^8.18.0",
|
|
35
35
|
"yaml": "^2.7.0"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"bin/",
|
|
39
|
+
"lib/",
|
|
40
|
+
"index.js",
|
|
41
|
+
"README.md"
|
|
42
|
+
]
|
|
37
43
|
}
|
package/.git-personal-enforced
DELETED