glidercli 0.1.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,4 @@
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
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # πŸš€ @vd7/glider
2
+
3
+ Browser automation CLI with autonomous loop execution. Control Chrome via CDP, run YAML task files, execute in Ralph Wiggum loops.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @vd7/glider
9
+ ```
10
+
11
+ ### Requirements
12
+
13
+ - Node.js 18+
14
+ - [bserve](https://github.com/vdutts/glider-crx) relay server
15
+ - Glider Chrome extension
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Check status
21
+ glider status
22
+
23
+ # Navigate
24
+ glider goto "https://google.com"
25
+
26
+ # Execute JavaScript
27
+ glider eval "document.title"
28
+
29
+ # Run a task file
30
+ glider run mytask.yaml
31
+
32
+ # Run in autonomous loop
33
+ glider loop mytask.yaml -n 20
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ ### Server
39
+ | Command | Description |
40
+ |---------|-------------|
41
+ | `glider status` | Check server, extension, tabs |
42
+ | `glider start` | Start relay server |
43
+ | `glider stop` | Stop relay server |
44
+
45
+ ### Navigation
46
+ | Command | Description |
47
+ |---------|-------------|
48
+ | `glider goto <url>` | Navigate to URL |
49
+ | `glider eval <js>` | Execute JavaScript |
50
+ | `glider click <selector>` | Click element |
51
+ | `glider type <sel> <text>` | Type into input |
52
+ | `glider screenshot [path]` | Take screenshot |
53
+ | `glider text` | Get page text |
54
+
55
+ ### Automation
56
+ | Command | Description |
57
+ |---------|-------------|
58
+ | `glider run <task.yaml>` | Execute YAML task file |
59
+ | `glider loop <task> [opts]` | Run in Ralph Wiggum loop |
60
+
61
+ ## Task File Format
62
+
63
+ ```yaml
64
+ name: "Get page data"
65
+ steps:
66
+ - goto: "https://example.com"
67
+ - wait: 2
68
+ - eval: "document.title"
69
+ - click: "button.submit"
70
+ - type: ["#input", "hello world"]
71
+ - screenshot: "/tmp/shot.png"
72
+ - assert: "document.title.includes('Example')"
73
+ - log: "Done"
74
+ ```
75
+
76
+ ## Ralph Wiggum Loop
77
+
78
+ The `loop` command implements autonomous execution:
79
+
80
+ ```bash
81
+ glider loop mytask.yaml -n 20 -t 600 -m "DONE"
82
+ ```
83
+
84
+ Options:
85
+ - `-n, --max-iterations N` - Max iterations (default: 10)
86
+ - `-t, --timeout N` - Max runtime in seconds (default: 3600)
87
+ - `-m, --marker STRING` - Completion marker (default: LOOP_COMPLETE)
88
+
89
+ The loop:
90
+ 1. Executes task steps repeatedly
91
+ 2. Checks for completion marker in output or task file
92
+ 3. Stops when marker found or limits reached
93
+ 4. Saves state to `/tmp/glider-state.json`
94
+ 5. Implements exponential backoff on errors
95
+
96
+ ## Architecture
97
+
98
+ ```
99
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
100
+ β”‚ glider CLI │────▢│ bserve │────▢│ Extension β”‚
101
+ β”‚ (this pkg) β”‚ β”‚ (relay) β”‚ β”‚ (Chrome) β”‚
102
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
103
+ β”‚
104
+ β–Ό
105
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
106
+ β”‚ Browser β”‚
107
+ β”‚ (CDP) β”‚
108
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT
package/bin/glider.js ADDED
@@ -0,0 +1,756 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GLIDER CLI - Browser automation with autonomous loop execution
4
+ *
5
+ * Commands:
6
+ * glider status Check server/extension/tab status
7
+ * glider start Start relay server
8
+ * glider stop Stop relay server
9
+ * glider goto <url> Navigate to URL
10
+ * glider eval <js> Execute JavaScript
11
+ * glider click <selector> Click element
12
+ * glider type <sel> <text> Type into input
13
+ * glider screenshot [path] Take screenshot
14
+ * glider text Get page text
15
+ * glider run <task.yaml> Run YAML task file
16
+ * glider loop <task> [-n N] Run task in Ralph Wiggum loop
17
+ *
18
+ * The loop command implements the Ralph Wiggum pattern:
19
+ * - Continuously executes until task is complete or limits reached
20
+ * - Safety guards: max iterations, timeout, completion detection
21
+ * - Checkpointing and state persistence
22
+ */
23
+
24
+ const { spawn, execSync, exec } = require('child_process');
25
+ const path = require('path');
26
+ const fs = require('fs');
27
+ const os = require('os');
28
+ const http = require('http');
29
+ const WebSocket = require('ws');
30
+ const YAML = require('yaml');
31
+
32
+ // Config
33
+ const PORT = process.env.GLIDER_PORT || 19988;
34
+ const SERVER_URL = `http://127.0.0.1:${PORT}`;
35
+ const SCRIPTS_DIR = process.env.SCRIPTS || path.join(os.homedir(), 'scripts');
36
+ const STATE_FILE = '/tmp/glider-state.json';
37
+ const LOG_FILE = '/tmp/glider.log';
38
+
39
+ // Colors
40
+ const RED = '\x1b[31m';
41
+ const GREEN = '\x1b[32m';
42
+ const YELLOW = '\x1b[33m';
43
+ const BLUE = '\x1b[34m';
44
+ const CYAN = '\x1b[36m';
45
+ const NC = '\x1b[0m';
46
+
47
+ const log = {
48
+ ok: (msg) => console.error(`${GREEN}βœ“${NC} ${msg}`),
49
+ fail: (msg) => console.error(`${RED}βœ—${NC} ${msg}`),
50
+ info: (msg) => console.error(`${BLUE}β†’${NC} ${msg}`),
51
+ warn: (msg) => console.error(`${YELLOW}!${NC} ${msg}`),
52
+ step: (msg) => console.error(`${CYAN}[STEP]${NC} ${msg}`),
53
+ result: (msg) => console.log(msg),
54
+ };
55
+
56
+ // HTTP helpers
57
+ function httpGet(urlPath) {
58
+ return new Promise((resolve, reject) => {
59
+ const url = new URL(urlPath, SERVER_URL);
60
+ http.get(url, { timeout: 2000 }, (res) => {
61
+ let data = '';
62
+ res.on('data', chunk => data += chunk);
63
+ res.on('end', () => {
64
+ try {
65
+ resolve(JSON.parse(data));
66
+ } catch {
67
+ resolve(data);
68
+ }
69
+ });
70
+ }).on('error', reject);
71
+ });
72
+ }
73
+
74
+ function httpPost(urlPath, body) {
75
+ return new Promise((resolve, reject) => {
76
+ const url = new URL(urlPath, SERVER_URL);
77
+ const data = JSON.stringify(body);
78
+ const req = http.request(url, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ timeout: 30000,
82
+ }, (res) => {
83
+ let result = '';
84
+ res.on('data', chunk => result += chunk);
85
+ res.on('end', () => {
86
+ try {
87
+ resolve(JSON.parse(result));
88
+ } catch {
89
+ resolve(result);
90
+ }
91
+ });
92
+ });
93
+ req.on('error', reject);
94
+ req.write(data);
95
+ req.end();
96
+ });
97
+ }
98
+
99
+ // Server checks
100
+ async function checkServer() {
101
+ try {
102
+ await httpGet('/status');
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ async function checkExtension() {
110
+ try {
111
+ const status = await httpGet('/status');
112
+ return status && status.extension === true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ async function checkTab() {
119
+ try {
120
+ const targets = await httpGet('/targets');
121
+ return Array.isArray(targets) && targets.length > 0;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ async function getTargets() {
128
+ try {
129
+ return await httpGet('/targets');
130
+ } catch {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ // Commands
136
+ async function cmdStatus() {
137
+ console.log('═══════════════════════════════════════');
138
+ console.log(' GLIDER STATUS');
139
+ console.log('═══════════════════════════════════════');
140
+
141
+ const serverOk = await checkServer();
142
+ console.log(serverOk ? `${GREEN}βœ“${NC} Server running on port ${PORT}` : `${RED}βœ—${NC} Server not running`);
143
+
144
+ if (serverOk) {
145
+ const extOk = await checkExtension();
146
+ console.log(extOk ? `${GREEN}βœ“${NC} Extension connected` : `${RED}βœ—${NC} Extension not connected`);
147
+
148
+ if (extOk) {
149
+ const targets = await getTargets();
150
+ if (targets.length > 0) {
151
+ console.log(`${GREEN}βœ“${NC} ${targets.length} tab(s) connected:`);
152
+ targets.forEach(t => {
153
+ const url = t.targetInfo?.url || 'unknown';
154
+ console.log(` ${CYAN}${url}${NC}`);
155
+ });
156
+ } else {
157
+ console.log(`${YELLOW}!${NC} No tabs connected`);
158
+ }
159
+ }
160
+ }
161
+ console.log('═══════════════════════════════════════');
162
+ }
163
+
164
+ async function cmdStart() {
165
+ if (await checkServer()) {
166
+ log.ok('Server already running');
167
+ return;
168
+ }
169
+
170
+ log.info('Starting glider server...');
171
+ const bserve = path.join(SCRIPTS_DIR, 'bserve');
172
+
173
+ if (!fs.existsSync(bserve)) {
174
+ log.fail(`bserve not found at ${bserve}`);
175
+ process.exit(1);
176
+ }
177
+
178
+ const child = spawn('node', [bserve], {
179
+ detached: true,
180
+ stdio: ['ignore', fs.openSync(LOG_FILE, 'a'), fs.openSync(LOG_FILE, 'a')],
181
+ });
182
+ child.unref();
183
+
184
+ // Wait for server
185
+ for (let i = 0; i < 10; i++) {
186
+ await new Promise(r => setTimeout(r, 500));
187
+ if (await checkServer()) {
188
+ log.ok('Server started');
189
+ return;
190
+ }
191
+ }
192
+ log.fail('Server failed to start');
193
+ process.exit(1);
194
+ }
195
+
196
+ async function cmdStop() {
197
+ try {
198
+ execSync('pkill -f bserve', { stdio: 'ignore' });
199
+ log.ok('Server stopped');
200
+ } catch {
201
+ log.warn('Server was not running');
202
+ }
203
+ }
204
+
205
+ async function cmdGoto(url) {
206
+ if (!url) {
207
+ log.fail('Usage: glider goto <url>');
208
+ process.exit(1);
209
+ }
210
+
211
+ log.info(`Navigating to: ${url}`);
212
+
213
+ try {
214
+ const result = await httpPost('/cdp', {
215
+ method: 'Page.navigate',
216
+ params: { url }
217
+ });
218
+ console.log(JSON.stringify(result));
219
+ log.ok('Navigated');
220
+ } catch (e) {
221
+ log.fail(`Navigation failed: ${e.message}`);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ async function cmdEval(js) {
227
+ if (!js) {
228
+ log.fail('Usage: glider eval <javascript>');
229
+ process.exit(1);
230
+ }
231
+
232
+ try {
233
+ const result = await httpPost('/cdp', {
234
+ method: 'Runtime.evaluate',
235
+ params: {
236
+ expression: js,
237
+ returnByValue: true,
238
+ awaitPromise: true,
239
+ }
240
+ });
241
+
242
+ if (result.result?.value !== undefined) {
243
+ console.log(JSON.stringify(result.result.value));
244
+ } else if (result.result?.description) {
245
+ console.log(result.result.description);
246
+ } else {
247
+ console.log(JSON.stringify(result));
248
+ }
249
+ } catch (e) {
250
+ log.fail(`Eval failed: ${e.message}`);
251
+ process.exit(1);
252
+ }
253
+ }
254
+
255
+ async function cmdClick(selector) {
256
+ if (!selector) {
257
+ log.fail('Usage: glider click <selector>');
258
+ process.exit(1);
259
+ }
260
+
261
+ const js = `
262
+ (() => {
263
+ const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
264
+ if (!el) return { error: 'Element not found' };
265
+ el.click();
266
+ return { clicked: true };
267
+ })()
268
+ `;
269
+
270
+ try {
271
+ const result = await httpPost('/cdp', {
272
+ method: 'Runtime.evaluate',
273
+ params: { expression: js, returnByValue: true }
274
+ });
275
+
276
+ if (result.result?.value?.error) {
277
+ log.fail(result.result.value.error);
278
+ process.exit(1);
279
+ }
280
+ log.ok(`Clicked: ${selector}`);
281
+ } catch (e) {
282
+ log.fail(`Click failed: ${e.message}`);
283
+ process.exit(1);
284
+ }
285
+ }
286
+
287
+ async function cmdType(selector, text) {
288
+ if (!selector || !text) {
289
+ log.fail('Usage: glider type <selector> <text>');
290
+ process.exit(1);
291
+ }
292
+
293
+ const js = `
294
+ (() => {
295
+ const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
296
+ if (!el) return { error: 'Element not found' };
297
+ el.focus();
298
+ el.value = '${text.replace(/'/g, "\\'")}';
299
+ el.dispatchEvent(new Event('input', { bubbles: true }));
300
+ return { typed: true };
301
+ })()
302
+ `;
303
+
304
+ try {
305
+ const result = await httpPost('/cdp', {
306
+ method: 'Runtime.evaluate',
307
+ params: { expression: js, returnByValue: true }
308
+ });
309
+
310
+ if (result.result?.value?.error) {
311
+ log.fail(result.result.value.error);
312
+ process.exit(1);
313
+ }
314
+ log.ok(`Typed into: ${selector}`);
315
+ } catch (e) {
316
+ log.fail(`Type failed: ${e.message}`);
317
+ process.exit(1);
318
+ }
319
+ }
320
+
321
+ async function cmdScreenshot(outputPath) {
322
+ const filePath = outputPath || `/tmp/glider-screenshot-${Date.now()}.png`;
323
+
324
+ try {
325
+ const result = await httpPost('/cdp', {
326
+ method: 'Page.captureScreenshot',
327
+ params: { format: 'png' }
328
+ });
329
+
330
+ if (result.data) {
331
+ fs.writeFileSync(filePath, Buffer.from(result.data, 'base64'));
332
+ log.ok(`Screenshot saved: ${filePath}`);
333
+ } else {
334
+ log.fail('No screenshot data received');
335
+ process.exit(1);
336
+ }
337
+ } catch (e) {
338
+ log.fail(`Screenshot failed: ${e.message}`);
339
+ process.exit(1);
340
+ }
341
+ }
342
+
343
+ async function cmdText() {
344
+ try {
345
+ const result = await httpPost('/cdp', {
346
+ method: 'Runtime.evaluate',
347
+ params: {
348
+ expression: 'document.body.innerText',
349
+ returnByValue: true,
350
+ }
351
+ });
352
+ console.log(result.result?.value || '');
353
+ } catch (e) {
354
+ log.fail(`Text extraction failed: ${e.message}`);
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ // YAML Task Runner
360
+ async function cmdRun(taskFile) {
361
+ if (!taskFile || !fs.existsSync(taskFile)) {
362
+ log.fail(`Task file not found: ${taskFile}`);
363
+ process.exit(1);
364
+ }
365
+
366
+ const content = fs.readFileSync(taskFile, 'utf8');
367
+ const task = YAML.parse(content);
368
+
369
+ console.log('═══════════════════════════════════════════════════════════');
370
+ console.log(` GLIDER RUN: ${task.name || 'Unnamed task'}`);
371
+ console.log(` Steps: ${task.steps?.length || 0}`);
372
+ console.log('═══════════════════════════════════════════════════════════');
373
+ console.log('');
374
+
375
+ if (!task.steps || !Array.isArray(task.steps)) {
376
+ log.fail('No steps defined in task file');
377
+ process.exit(1);
378
+ }
379
+
380
+ let failed = false;
381
+
382
+ for (let i = 0; i < task.steps.length; i++) {
383
+ const step = task.steps[i];
384
+ const [cmd, arg] = Object.entries(step)[0];
385
+ const stepNum = i + 1;
386
+
387
+ log.step(`[${stepNum}/${task.steps.length}] ${cmd}: ${String(arg).slice(0, 60)}${String(arg).length > 60 ? '...' : ''}`);
388
+
389
+ try {
390
+ switch (cmd) {
391
+ case 'goto':
392
+ case 'navigate':
393
+ await cmdGoto(arg);
394
+ break;
395
+ case 'wait':
396
+ case 'sleep':
397
+ await new Promise(r => setTimeout(r, arg * 1000));
398
+ log.ok(`Waited ${arg}s`);
399
+ break;
400
+ case 'eval':
401
+ case 'js':
402
+ await cmdEval(arg);
403
+ break;
404
+ case 'click':
405
+ await cmdClick(arg);
406
+ break;
407
+ case 'type':
408
+ if (Array.isArray(arg)) {
409
+ await cmdType(arg[0], arg[1]);
410
+ }
411
+ break;
412
+ case 'screenshot':
413
+ await cmdScreenshot(arg);
414
+ break;
415
+ case 'text':
416
+ await cmdText();
417
+ break;
418
+ case 'log':
419
+ case 'echo':
420
+ console.log(`${BLUE}[LOG]${NC} ${arg}`);
421
+ break;
422
+ case 'assert':
423
+ const assertResult = await httpPost('/cdp', {
424
+ method: 'Runtime.evaluate',
425
+ params: { expression: arg, returnByValue: true }
426
+ });
427
+ if (assertResult.result?.value === true) {
428
+ log.ok('Assertion passed');
429
+ } else {
430
+ log.fail(`Assertion failed: ${JSON.stringify(assertResult.result?.value)}`);
431
+ failed = true;
432
+ }
433
+ break;
434
+ default:
435
+ log.warn(`Unknown command: ${cmd}`);
436
+ }
437
+ } catch (e) {
438
+ log.fail(`Step failed: ${e.message}`);
439
+ failed = true;
440
+ }
441
+
442
+ console.log('');
443
+ }
444
+
445
+ console.log('═══════════════════════════════════════════════════════════');
446
+ if (failed) {
447
+ console.log(`${RED} βœ— Task failed${NC}`);
448
+ process.exit(1);
449
+ } else {
450
+ console.log(`${GREEN} βœ“ Task completed successfully${NC}`);
451
+ }
452
+ console.log('═══════════════════════════════════════════════════════════');
453
+ }
454
+
455
+ // Ralph Wiggum Loop - The core autonomous execution pattern
456
+ async function cmdLoop(taskFileOrPrompt, options = {}) {
457
+ const maxIterations = options.maxIterations || 10;
458
+ const maxRuntime = options.maxRuntime || 3600; // 1 hour default
459
+ const checkpointInterval = options.checkpointInterval || 5;
460
+ const completionMarker = options.completionMarker || 'LOOP_COMPLETE';
461
+
462
+ // Initialize state
463
+ const state = {
464
+ iteration: 0,
465
+ startTime: Date.now(),
466
+ completed: [],
467
+ pending: [],
468
+ status: 'running',
469
+ lastOutput: null,
470
+ errors: [],
471
+ };
472
+
473
+ // Save state helper
474
+ const saveState = () => {
475
+ fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
476
+ };
477
+
478
+ // Load task
479
+ let task;
480
+ if (fs.existsSync(taskFileOrPrompt)) {
481
+ const content = fs.readFileSync(taskFileOrPrompt, 'utf8');
482
+ task = YAML.parse(content);
483
+ } else {
484
+ // Inline prompt mode
485
+ task = { name: 'Inline task', prompt: taskFileOrPrompt, steps: [] };
486
+ }
487
+
488
+ console.log('═══════════════════════════════════════════════════════════');
489
+ console.log(' GLIDER LOOP - Ralph Wiggum Autonomous Execution');
490
+ console.log('═══════════════════════════════════════════════════════════');
491
+ console.log(`Task: ${task.name || 'Unnamed'}`);
492
+ console.log(`Max iterations: ${maxIterations}`);
493
+ console.log(`Max runtime: ${maxRuntime}s`);
494
+ console.log(`Completion marker: ${completionMarker}`);
495
+ console.log('');
496
+
497
+ // Main loop
498
+ while (state.status === 'running') {
499
+ state.iteration++;
500
+
501
+ // Safety checks
502
+ const elapsed = (Date.now() - state.startTime) / 1000;
503
+
504
+ if (state.iteration > maxIterations) {
505
+ log.warn(`Max iterations (${maxIterations}) reached`);
506
+ state.status = 'max_iterations';
507
+ break;
508
+ }
509
+
510
+ if (elapsed > maxRuntime) {
511
+ log.warn(`Max runtime (${maxRuntime}s) reached`);
512
+ state.status = 'timeout';
513
+ break;
514
+ }
515
+
516
+ console.log('──────────────────────────────────────────────────────────');
517
+ console.log(` Iteration ${state.iteration} / ${maxIterations} (${elapsed.toFixed(1)}s elapsed)`);
518
+ console.log('──────────────────────────────────────────────────────────');
519
+
520
+ try {
521
+ // Execute steps if defined
522
+ if (task.steps && task.steps.length > 0) {
523
+ for (const step of task.steps) {
524
+ const [cmd, arg] = Object.entries(step)[0];
525
+ log.step(`${cmd}: ${String(arg).slice(0, 50)}`);
526
+
527
+ switch (cmd) {
528
+ case 'goto':
529
+ await cmdGoto(arg);
530
+ break;
531
+ case 'wait':
532
+ await new Promise(r => setTimeout(r, arg * 1000));
533
+ break;
534
+ case 'eval':
535
+ const evalResult = await httpPost('/cdp', {
536
+ method: 'Runtime.evaluate',
537
+ params: { expression: arg, returnByValue: true, awaitPromise: true }
538
+ });
539
+ state.lastOutput = evalResult.result?.value;
540
+ log.result(JSON.stringify(state.lastOutput));
541
+ break;
542
+ case 'click':
543
+ await cmdClick(arg);
544
+ break;
545
+ case 'screenshot':
546
+ await cmdScreenshot(arg);
547
+ break;
548
+ default:
549
+ log.warn(`Unknown: ${cmd}`);
550
+ }
551
+ }
552
+ }
553
+
554
+ // Check for completion marker in last output
555
+ if (state.lastOutput && String(state.lastOutput).includes(completionMarker)) {
556
+ log.ok('Completion marker detected!');
557
+ state.status = 'completed';
558
+ break;
559
+ }
560
+
561
+ // Check for completion marker in task file (if it was modified)
562
+ if (fs.existsSync(taskFileOrPrompt)) {
563
+ const currentContent = fs.readFileSync(taskFileOrPrompt, 'utf8');
564
+ if (currentContent.includes(completionMarker) || currentContent.includes('DONE')) {
565
+ log.ok('Task file marked as complete');
566
+ state.status = 'completed';
567
+ break;
568
+ }
569
+ }
570
+
571
+ state.completed.push({ iteration: state.iteration, success: true });
572
+
573
+ } catch (e) {
574
+ log.fail(`Iteration error: ${e.message}`);
575
+ state.errors.push({ iteration: state.iteration, error: e.message });
576
+
577
+ // Exponential backoff on errors
578
+ const backoff = Math.min(30, Math.pow(2, state.errors.length));
579
+ log.info(`Backing off ${backoff}s before retry...`);
580
+ await new Promise(r => setTimeout(r, backoff * 1000));
581
+ }
582
+
583
+ // Checkpoint
584
+ if (state.iteration % checkpointInterval === 0) {
585
+ saveState();
586
+ log.info(`Checkpoint saved (iteration ${state.iteration})`);
587
+ }
588
+
589
+ // Small delay between iterations
590
+ await new Promise(r => setTimeout(r, 1000));
591
+ }
592
+
593
+ // Final state save
594
+ saveState();
595
+
596
+ console.log('');
597
+ console.log('═══════════════════════════════════════════════════════════');
598
+ console.log(` Loop finished: ${state.status}`);
599
+ console.log(` Iterations: ${state.iteration}`);
600
+ console.log(` Successful: ${state.completed.length}`);
601
+ console.log(` Errors: ${state.errors.length}`);
602
+ console.log(` Runtime: ${((Date.now() - state.startTime) / 1000).toFixed(1)}s`);
603
+ console.log('═══════════════════════════════════════════════════════════');
604
+
605
+ if (state.status === 'completed') {
606
+ console.log(`${GREEN} βœ“ Task completed successfully${NC}`);
607
+ } else {
608
+ console.log(`${YELLOW} ! Task stopped: ${state.status}${NC}`);
609
+ }
610
+ }
611
+
612
+ // Help
613
+ function showHelp() {
614
+ console.log(`
615
+ ${CYAN}GLIDER${NC} - Browser Automation CLI with Autonomous Loop Execution
616
+
617
+ ${YELLOW}USAGE:${NC}
618
+ glider <command> [args]
619
+
620
+ ${YELLOW}SERVER:${NC}
621
+ status Check server, extension, tabs
622
+ start Start relay server
623
+ stop Stop relay server
624
+
625
+ ${YELLOW}NAVIGATION:${NC}
626
+ goto <url> Navigate current tab to URL
627
+ eval <js> Execute JavaScript, return result
628
+ click <selector> Click element
629
+ type <sel> <text> Type into input
630
+ screenshot [path] Take screenshot
631
+ text Get page text content
632
+
633
+ ${YELLOW}AUTOMATION:${NC}
634
+ run <task.yaml> Execute YAML task file
635
+ loop <task> [opts] Run in Ralph Wiggum loop until complete
636
+
637
+ ${YELLOW}LOOP OPTIONS:${NC}
638
+ -n, --max-iterations N Max iterations (default: 10)
639
+ -t, --timeout N Max runtime in seconds (default: 3600)
640
+ -m, --marker STRING Completion marker (default: LOOP_COMPLETE)
641
+
642
+ ${YELLOW}TASK FILE FORMAT:${NC}
643
+ name: "Task name"
644
+ steps:
645
+ - goto: "https://example.com"
646
+ - wait: 2
647
+ - eval: "document.title"
648
+ - click: "button.submit"
649
+ - type: ["#input", "hello"]
650
+ - screenshot: "/tmp/shot.png"
651
+ - assert: "document.title.includes('Example')"
652
+ - log: "Step done"
653
+
654
+ ${YELLOW}EXAMPLES:${NC}
655
+ glider status
656
+ glider start
657
+ glider goto "https://google.com"
658
+ glider eval "document.title"
659
+ glider run mytask.yaml
660
+ glider loop mytask.yaml -n 20 -t 600
661
+
662
+ ${YELLOW}RALPH WIGGUM PATTERN:${NC}
663
+ The loop command implements autonomous execution:
664
+ - Runs until completion marker found or limits reached
665
+ - Safety guards: max iterations, timeout, error backoff
666
+ - State persistence for recovery
667
+ - Checkpointing every N iterations
668
+
669
+ ${YELLOW}REQUIREMENTS:${NC}
670
+ - Node.js 18+
671
+ - bserve relay server (~/scripts/bserve)
672
+ - Glider Chrome extension connected
673
+ `);
674
+ }
675
+
676
+ // Main
677
+ async function main() {
678
+ const args = process.argv.slice(2);
679
+ const cmd = args[0];
680
+
681
+ if (!cmd || cmd === '--help' || cmd === '-h') {
682
+ showHelp();
683
+ process.exit(0);
684
+ }
685
+
686
+ // Ensure server is running for most commands
687
+ if (!['start', 'stop', 'help', '--help', '-h'].includes(cmd)) {
688
+ if (!await checkServer()) {
689
+ log.info('Server not running, starting...');
690
+ await cmdStart();
691
+ }
692
+ }
693
+
694
+ switch (cmd) {
695
+ case 'status':
696
+ await cmdStatus();
697
+ break;
698
+ case 'start':
699
+ await cmdStart();
700
+ break;
701
+ case 'stop':
702
+ await cmdStop();
703
+ break;
704
+ case 'goto':
705
+ case 'navigate':
706
+ await cmdGoto(args[1]);
707
+ break;
708
+ case 'eval':
709
+ case 'js':
710
+ await cmdEval(args.slice(1).join(' '));
711
+ break;
712
+ case 'click':
713
+ await cmdClick(args[1]);
714
+ break;
715
+ case 'type':
716
+ await cmdType(args[1], args.slice(2).join(' '));
717
+ break;
718
+ case 'screenshot':
719
+ await cmdScreenshot(args[1]);
720
+ break;
721
+ case 'text':
722
+ await cmdText();
723
+ break;
724
+ case 'run':
725
+ await cmdRun(args[1]);
726
+ break;
727
+ case 'loop':
728
+ // Parse loop options
729
+ const loopOpts = {
730
+ maxIterations: 10,
731
+ maxRuntime: 3600,
732
+ completionMarker: 'LOOP_COMPLETE',
733
+ };
734
+ let taskArg = args[1];
735
+ for (let i = 2; i < args.length; i++) {
736
+ if (args[i] === '-n' || args[i] === '--max-iterations') {
737
+ loopOpts.maxIterations = parseInt(args[++i], 10);
738
+ } else if (args[i] === '-t' || args[i] === '--timeout') {
739
+ loopOpts.maxRuntime = parseInt(args[++i], 10);
740
+ } else if (args[i] === '-m' || args[i] === '--marker') {
741
+ loopOpts.completionMarker = args[++i];
742
+ }
743
+ }
744
+ await cmdLoop(taskArg, loopOpts);
745
+ break;
746
+ default:
747
+ log.fail(`Unknown command: ${cmd}`);
748
+ showHelp();
749
+ process.exit(1);
750
+ }
751
+ }
752
+
753
+ main().catch(e => {
754
+ log.fail(e.message);
755
+ process.exit(1);
756
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "glidercli",
3
+ "version": "0.1.0",
4
+ "description": "Browser automation CLI with autonomous loop execution. Control Chrome via CDP, run YAML task files, execute in Ralph Wiggum loops.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "glider": "bin/glider.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/glider.js --help"
11
+ },
12
+ "keywords": [
13
+ "browser",
14
+ "automation",
15
+ "cdp",
16
+ "chrome",
17
+ "devtools",
18
+ "websocket",
19
+ "cli",
20
+ "loop",
21
+ "ralph-wiggum",
22
+ "autonomous"
23
+ ],
24
+ "author": "vdutts",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/vdutts/glider-cli.git"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "ws": "^8.18.0",
35
+ "yaml": "^2.7.0"
36
+ }
37
+ }