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.
- package/.git-personal-enforced +4 -0
- package/README.md +113 -0
- package/bin/glider.js +756 -0
- package/package.json +37 -0
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
|
+
}
|