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/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.1.5",
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
  }
@@ -1,4 +0,0 @@
1
- # This repository enforces personal git config only
2
- # DO NOT use git-amazon or any other corporate git configs
3
- ENFORCED_DATE=2026-01-10
4
- REQUIRED_CONFIG=personal