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.
@@ -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.2.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": {