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.
- package/README.md +70 -9
- package/bin/glider.js +615 -68
- package/lib/bexplore.js +815 -0
- package/lib/bextract.js +236 -0
- package/lib/bfetch.js +274 -0
- package/lib/bserve.js +43 -7
- 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 +1 -1
package/bin/glider.js
CHANGED
|
@@ -31,11 +31,16 @@ const YAML = require('yaml');
|
|
|
31
31
|
|
|
32
32
|
// Config
|
|
33
33
|
const PORT = process.env.GLIDER_PORT || 19988;
|
|
34
|
+
const DEBUG_PORT = process.env.GLIDER_DEBUG_PORT || 9222;
|
|
34
35
|
const SERVER_URL = `http://127.0.0.1:${PORT}`;
|
|
36
|
+
const DEBUG_URL = `http://127.0.0.1:${DEBUG_PORT}`;
|
|
35
37
|
const LIB_DIR = path.join(__dirname, '..', 'lib');
|
|
36
38
|
const STATE_FILE = '/tmp/glider-state.json';
|
|
37
39
|
const LOG_FILE = '/tmp/glider.log';
|
|
38
40
|
|
|
41
|
+
// Direct CDP module
|
|
42
|
+
const { DirectCDP, checkChrome } = require(path.join(LIB_DIR, 'cdp-direct.js'));
|
|
43
|
+
|
|
39
44
|
// Domain extensions - load from ~/.cursor/glider/domains.json or ~/.glider/domains.json
|
|
40
45
|
const DOMAIN_CONFIG_PATHS = [
|
|
41
46
|
path.join(os.homedir(), '.cursor', 'glider', 'domains.json'),
|
|
@@ -51,7 +56,7 @@ for (const cfgPath of DOMAIN_CONFIG_PATHS) {
|
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
// Colors
|
|
59
|
+
// Colors - matching the deep blue gradient logo
|
|
55
60
|
const RED = '\x1b[31m';
|
|
56
61
|
const GREEN = '\x1b[32m';
|
|
57
62
|
const YELLOW = '\x1b[33m';
|
|
@@ -63,25 +68,30 @@ const BOLD = '\x1b[1m';
|
|
|
63
68
|
const DIM = '\x1b[2m';
|
|
64
69
|
const NC = '\x1b[0m';
|
|
65
70
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
71
|
+
// Deep blue gradient (matching logo)
|
|
72
|
+
const B1 = '\x1b[38;5;17m'; // darkest navy
|
|
73
|
+
const B2 = '\x1b[38;5;18m'; // dark navy
|
|
74
|
+
const B3 = '\x1b[38;5;19m'; // navy
|
|
75
|
+
const B4 = '\x1b[38;5;20m'; // blue
|
|
76
|
+
const B5 = '\x1b[38;5;27m'; // bright blue
|
|
77
|
+
const B6 = '\x1b[38;5;33m'; // sky blue
|
|
78
|
+
const BW = '\x1b[38;5;255m'; // white (for glider icon)
|
|
73
79
|
|
|
74
|
-
// Banner -
|
|
80
|
+
// Banner - hang glider ASCII art matching logo
|
|
75
81
|
const BANNER = `
|
|
76
|
-
${
|
|
77
|
-
${
|
|
78
|
-
${
|
|
79
|
-
${
|
|
80
|
-
${
|
|
81
|
-
${
|
|
82
|
-
${
|
|
83
|
-
${
|
|
84
|
-
${
|
|
82
|
+
${B1} ╔══════════════════════════════════════════════════════════╗${NC}
|
|
83
|
+
${B2} ║${NC} ${B2}║${NC}
|
|
84
|
+
${B3} ║${NC} ${BW} ___________________________________${NC} ${B3}║${NC}
|
|
85
|
+
${B4} ║${NC} ${BW} ╲ ╲${NC} ${B4}║${NC}
|
|
86
|
+
${B5} ║${NC} ${BW} ╲___________________________________╲${NC} ${B5}║${NC}
|
|
87
|
+
${B5} ║${NC} ${BW} ╲ ╱${NC} ${B5}║${NC}
|
|
88
|
+
${B6} ║${NC} ${BW} ╲_______________________________╱${NC} ${B6}║${NC}
|
|
89
|
+
${B6} ║${NC} ${B6}║${NC}
|
|
90
|
+
${B5} ║${NC} ${BW}${BOLD}G L I D E R${NC} ${B5}║${NC}
|
|
91
|
+
${B4} ║${NC} ${DIM}Browser Automation CLI${NC} ${B5}v${require('../package.json').version}${NC} ${B4}║${NC}
|
|
92
|
+
${B3} ║${NC} ${DIM}github.com/vdutts7/glidercli${NC} ${B3}║${NC}
|
|
93
|
+
${B2} ║${NC} ${B2}║${NC}
|
|
94
|
+
${B1} ╚══════════════════════════════════════════════════════════╝${NC}
|
|
85
95
|
`;
|
|
86
96
|
|
|
87
97
|
function showBanner() {
|
|
@@ -91,12 +101,26 @@ function showBanner() {
|
|
|
91
101
|
const log = {
|
|
92
102
|
ok: (msg) => console.error(`${GREEN}✓${NC} ${msg}`),
|
|
93
103
|
fail: (msg) => console.error(`${RED}✗${NC} ${msg}`),
|
|
94
|
-
info: (msg) => console.error(`${
|
|
95
|
-
warn: (msg) => console.error(`${YELLOW}
|
|
96
|
-
step: (msg) => console.error(`${
|
|
104
|
+
info: (msg) => console.error(`${B5}→${NC} ${msg}`),
|
|
105
|
+
warn: (msg) => console.error(`${YELLOW}⚠${NC} ${msg}`),
|
|
106
|
+
step: (msg) => console.error(`${B6}▸${NC} ${msg}`),
|
|
97
107
|
result: (msg) => console.log(msg),
|
|
108
|
+
box: (title) => {
|
|
109
|
+
const line = '─'.repeat(50);
|
|
110
|
+
console.log(`${B3}┌${line}┐${NC}`);
|
|
111
|
+
console.log(`${B4}│${NC} ${BW}${BOLD}${title.padEnd(48)}${NC} ${B4}│${NC}`);
|
|
112
|
+
console.log(`${B5}└${line}┘${NC}`);
|
|
113
|
+
},
|
|
98
114
|
};
|
|
99
115
|
|
|
116
|
+
// macOS notification helper
|
|
117
|
+
function notify(title, message, sound = false) {
|
|
118
|
+
try {
|
|
119
|
+
const soundFlag = sound ? 'sound name "Ping"' : '';
|
|
120
|
+
execSync(`osascript -e 'display notification "${message}" with title "${title}" ${soundFlag}'`, { stdio: 'ignore' });
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
// HTTP helpers
|
|
101
125
|
function httpGet(urlPath) {
|
|
102
126
|
return new Promise((resolve, reject) => {
|
|
@@ -179,31 +203,32 @@ async function getTargets() {
|
|
|
179
203
|
// Commands
|
|
180
204
|
async function cmdStatus() {
|
|
181
205
|
showBanner();
|
|
182
|
-
|
|
183
|
-
console.log(' STATUS');
|
|
184
|
-
console.log('═══════════════════════════════════════');
|
|
206
|
+
log.box('STATUS');
|
|
185
207
|
|
|
186
208
|
const serverOk = await checkServer();
|
|
187
|
-
console.log(serverOk ?
|
|
209
|
+
console.log(serverOk ? ` ${GREEN}✓${NC} Server running on port ${PORT}` : ` ${RED}✗${NC} Server not running`);
|
|
188
210
|
|
|
189
211
|
if (serverOk) {
|
|
190
212
|
const extOk = await checkExtension();
|
|
191
|
-
console.log(extOk ?
|
|
213
|
+
console.log(extOk ? ` ${GREEN}✓${NC} Extension connected` : ` ${RED}✗${NC} Extension not connected`);
|
|
192
214
|
|
|
193
215
|
if (extOk) {
|
|
194
216
|
const targets = await getTargets();
|
|
195
217
|
if (targets.length > 0) {
|
|
196
|
-
console.log(
|
|
218
|
+
console.log(` ${GREEN}✓${NC} ${targets.length} tab(s) connected:`);
|
|
197
219
|
targets.forEach(t => {
|
|
198
220
|
const url = t.targetInfo?.url || 'unknown';
|
|
199
|
-
console.log(` ${
|
|
221
|
+
console.log(` ${B5}${url}${NC}`);
|
|
200
222
|
});
|
|
201
223
|
} else {
|
|
202
|
-
console.log(
|
|
224
|
+
console.log(` ${YELLOW}⚠${NC} No tabs connected`);
|
|
225
|
+
console.log(` ${DIM}Run: glider connect${NC}`);
|
|
203
226
|
}
|
|
204
227
|
}
|
|
228
|
+
} else {
|
|
229
|
+
console.log(` ${DIM}Run: glider install${NC}`);
|
|
205
230
|
}
|
|
206
|
-
console.log(
|
|
231
|
+
console.log();
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
async function cmdStart() {
|
|
@@ -411,15 +436,197 @@ async function cmdRestart() {
|
|
|
411
436
|
await cmdStart();
|
|
412
437
|
}
|
|
413
438
|
|
|
439
|
+
// Daemon management - runs forever, respawns on crash
|
|
440
|
+
async function cmdInstallDaemon() {
|
|
441
|
+
const home = os.homedir();
|
|
442
|
+
const daemonScript = path.join(LIB_DIR, 'glider-daemon.sh');
|
|
443
|
+
const logDir = path.join(home, '.glider');
|
|
444
|
+
const pidFile = path.join(logDir, 'daemon.pid');
|
|
445
|
+
|
|
446
|
+
// Create log directory
|
|
447
|
+
if (!fs.existsSync(logDir)) {
|
|
448
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Kill existing daemon
|
|
452
|
+
if (fs.existsSync(pidFile)) {
|
|
453
|
+
try {
|
|
454
|
+
const pid = fs.readFileSync(pidFile, 'utf8').trim();
|
|
455
|
+
execSync(`kill ${pid} 2>/dev/null || true`, { stdio: 'ignore' });
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Start daemon in background, detached from terminal
|
|
460
|
+
const child = spawn('nohup', [daemonScript], {
|
|
461
|
+
detached: true,
|
|
462
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
463
|
+
cwd: home
|
|
464
|
+
});
|
|
465
|
+
child.unref();
|
|
466
|
+
|
|
467
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
468
|
+
|
|
469
|
+
if (fs.existsSync(pidFile)) {
|
|
470
|
+
log.ok('Daemon started');
|
|
471
|
+
log.info('Relay will auto-restart on crash');
|
|
472
|
+
log.info(`Logs: ${logDir}/daemon.log`);
|
|
473
|
+
log.info(`PID: ${fs.readFileSync(pidFile, 'utf8').trim()}`);
|
|
474
|
+
} else {
|
|
475
|
+
log.fail('Daemon failed to start');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function cmdUninstallDaemon() {
|
|
480
|
+
const home = os.homedir();
|
|
481
|
+
const pidFile = path.join(home, '.glider', 'daemon.pid');
|
|
482
|
+
|
|
483
|
+
if (!fs.existsSync(pidFile)) {
|
|
484
|
+
log.info('Daemon not running');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const pid = fs.readFileSync(pidFile, 'utf8').trim();
|
|
490
|
+
execSync(`kill ${pid}`, { stdio: 'ignore' });
|
|
491
|
+
fs.unlinkSync(pidFile);
|
|
492
|
+
log.ok('Daemon stopped');
|
|
493
|
+
} catch (e) {
|
|
494
|
+
log.fail(`Failed to stop: ${e.message}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function cmdConnect() {
|
|
499
|
+
// Bulletproof connect: relay + Chrome + trigger attach via HTTP
|
|
500
|
+
log.info('Connecting...');
|
|
501
|
+
|
|
502
|
+
// 1. Ensure relay is running
|
|
503
|
+
if (!await checkServer()) {
|
|
504
|
+
await cmdStart();
|
|
505
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 2. Ensure Chrome is running
|
|
509
|
+
try {
|
|
510
|
+
execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
|
|
511
|
+
} catch {
|
|
512
|
+
log.info('Starting Chrome...');
|
|
513
|
+
execSync('open -a "Google Chrome"');
|
|
514
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 3. Wait for extension to connect to relay
|
|
518
|
+
for (let i = 0; i < 10; i++) {
|
|
519
|
+
if (await checkExtension()) break;
|
|
520
|
+
await new Promise(r => setTimeout(r, 500));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!await checkExtension()) {
|
|
524
|
+
log.fail('Extension not connected to relay');
|
|
525
|
+
log.info('Make sure Glider extension is installed in Chrome');
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
log.ok('Extension connected');
|
|
529
|
+
|
|
530
|
+
// Wait for extension to fully initialize
|
|
531
|
+
await new Promise(r => setTimeout(r, 500));
|
|
532
|
+
|
|
533
|
+
// 4. Check if already have targets
|
|
534
|
+
if (await checkTab()) {
|
|
535
|
+
log.ok('Already connected to tab(s)');
|
|
536
|
+
const targets = await getTargets();
|
|
537
|
+
targets.slice(0, 3).forEach(t => {
|
|
538
|
+
console.log(` ${CYAN}${t.targetInfo?.url || 'unknown'}${NC}`);
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 5. Ensure we have a real tab (not chrome://)
|
|
544
|
+
try {
|
|
545
|
+
const tabUrl = execSync(`osascript -e 'tell application "Google Chrome" to return URL of active tab of front window'`).toString().trim();
|
|
546
|
+
if (tabUrl.startsWith('chrome://') || tabUrl.startsWith('chrome-extension://')) {
|
|
547
|
+
log.info('Creating new tab...');
|
|
548
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new tab at front window with properties {URL:"https://google.com"}'`);
|
|
549
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
// No window, create one
|
|
553
|
+
log.info('Creating new window...');
|
|
554
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new window with properties {URL:"https://google.com"}'`);
|
|
555
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 6. Trigger attach via HTTP endpoint (no pixel clicking needed!)
|
|
559
|
+
log.info('Attaching to tab...');
|
|
560
|
+
try {
|
|
561
|
+
const result = await fetch(`${SERVER_URL}/attach`, { method: 'POST' });
|
|
562
|
+
const data = await result.json();
|
|
563
|
+
|
|
564
|
+
if (data.attached > 0) {
|
|
565
|
+
log.ok('Connected!');
|
|
566
|
+
const targets = await getTargets();
|
|
567
|
+
targets.slice(0, 3).forEach(t => {
|
|
568
|
+
console.log(` ${CYAN}${t.targetInfo?.url || 'unknown'}${NC}`);
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
} catch (e) {
|
|
573
|
+
log.warn(`Attach failed: ${e.message}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 7. Fallback: create fresh tab and retry
|
|
577
|
+
log.info('Creating fresh tab...');
|
|
578
|
+
execSync(`osascript -e 'tell application "Google Chrome" to make new tab at front window with properties {URL:"https://google.com"}'`);
|
|
579
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
580
|
+
|
|
581
|
+
try {
|
|
582
|
+
const result = await fetch(`${SERVER_URL}/attach`, { method: 'POST' });
|
|
583
|
+
const data = await result.json();
|
|
584
|
+
|
|
585
|
+
if (data.attached > 0) {
|
|
586
|
+
log.ok('Connected!');
|
|
587
|
+
const targets = await getTargets();
|
|
588
|
+
targets.slice(0, 3).forEach(t => {
|
|
589
|
+
console.log(` ${CYAN}${t.targetInfo?.url || 'unknown'}${NC}`);
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
} catch {}
|
|
594
|
+
|
|
595
|
+
// 8. Need manual click - open Chrome and show instructions
|
|
596
|
+
log.warn('Click the Glider extension icon in Chrome');
|
|
597
|
+
console.log(` ${B5}(on any real webpage, not chrome:// pages)${NC}`);
|
|
598
|
+
execSync(`osascript -e 'tell application "Google Chrome" to activate'`);
|
|
599
|
+
|
|
600
|
+
// Send macOS notification so user sees it even if not looking at terminal
|
|
601
|
+
notify('Glider', 'Click the extension icon in Chrome to connect', true);
|
|
602
|
+
|
|
603
|
+
// Wait for user to click
|
|
604
|
+
log.info('Waiting for connection...');
|
|
605
|
+
for (let i = 0; i < 30; i++) {
|
|
606
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
607
|
+
if (await checkTab()) {
|
|
608
|
+
log.ok('Connected!');
|
|
609
|
+
notify('Glider', 'Connected to browser');
|
|
610
|
+
const targets = await getTargets();
|
|
611
|
+
targets.slice(0, 3).forEach(t => {
|
|
612
|
+
console.log(` ${B5}${t.targetInfo?.url || 'unknown'}${NC}`);
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
log.fail('Timed out waiting for connection');
|
|
619
|
+
notify('Glider', 'Connection timed out - click extension icon', true);
|
|
620
|
+
log.info('Make sure you clicked the extension icon on a real webpage');
|
|
621
|
+
}
|
|
622
|
+
|
|
414
623
|
async function cmdTest() {
|
|
415
624
|
showBanner();
|
|
416
|
-
|
|
417
|
-
console.log(' GLIDER TEST');
|
|
418
|
-
console.log('═══════════════════════════════════════');
|
|
625
|
+
log.box('DIAGNOSTICS');
|
|
419
626
|
|
|
420
627
|
// Test 1: Server
|
|
421
628
|
const serverOk = await checkServer();
|
|
422
|
-
console.log(serverOk ?
|
|
629
|
+
console.log(serverOk ? ` ${GREEN}✓${NC} ${B5}[1/4]${NC} Server` : ` ${RED}✗${NC} ${B5}[1/4]${NC} Server`);
|
|
423
630
|
if (!serverOk) {
|
|
424
631
|
log.info('Starting server...');
|
|
425
632
|
await cmdStart();
|
|
@@ -427,11 +634,11 @@ async function cmdTest() {
|
|
|
427
634
|
|
|
428
635
|
// Test 2: Extension
|
|
429
636
|
const extOk = await checkExtension();
|
|
430
|
-
console.log(extOk ?
|
|
637
|
+
console.log(extOk ? ` ${GREEN}✓${NC} ${B5}[2/4]${NC} Extension` : ` ${RED}✗${NC} ${B5}[2/4]${NC} Extension`);
|
|
431
638
|
|
|
432
639
|
// Test 3: Tab
|
|
433
640
|
const tabOk = await checkTab();
|
|
434
|
-
console.log(tabOk ?
|
|
641
|
+
console.log(tabOk ? ` ${GREEN}✓${NC} ${B5}[3/4]${NC} Tab attached` : ` ${RED}✗${NC} ${B5}[3/4]${NC} No tabs`);
|
|
435
642
|
|
|
436
643
|
// Test 4: CDP command
|
|
437
644
|
if (tabOk) {
|
|
@@ -467,6 +674,98 @@ async function cmdTabs() {
|
|
|
467
674
|
});
|
|
468
675
|
}
|
|
469
676
|
|
|
677
|
+
async function cmdWindow(args) {
|
|
678
|
+
const { WindowManager } = require(path.join(LIB_DIR, 'bwindow.js'));
|
|
679
|
+
const wm = new WindowManager();
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
await wm.connect();
|
|
683
|
+
await wm.init();
|
|
684
|
+
|
|
685
|
+
const subcmd = args[0] || 'list';
|
|
686
|
+
|
|
687
|
+
switch (subcmd) {
|
|
688
|
+
case 'new':
|
|
689
|
+
case 'create': {
|
|
690
|
+
const url = args[1] || 'about:blank';
|
|
691
|
+
log.info(`Creating new window: ${url}`);
|
|
692
|
+
const result = await wm.createWindow(url);
|
|
693
|
+
log.ok(`Window created: ${result.targetId}`);
|
|
694
|
+
console.log(JSON.stringify(result, null, 2));
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
case 'tab': {
|
|
699
|
+
const url = args[1] || 'about:blank';
|
|
700
|
+
log.info(`Creating new tab: ${url}`);
|
|
701
|
+
const result = await wm.createTab(url);
|
|
702
|
+
log.ok(`Tab created: ${result.targetId}`);
|
|
703
|
+
console.log(JSON.stringify(result, null, 2));
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
case 'close': {
|
|
708
|
+
const targetId = args[1];
|
|
709
|
+
if (!targetId) {
|
|
710
|
+
log.fail('targetId required');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
log.info(`Closing: ${targetId}`);
|
|
714
|
+
const result = await wm.closeTarget(targetId);
|
|
715
|
+
if (result.success) {
|
|
716
|
+
log.ok(`Closed: ${targetId}`);
|
|
717
|
+
} else {
|
|
718
|
+
log.fail(`Failed to close: ${result.error}`);
|
|
719
|
+
}
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
case 'closeall': {
|
|
724
|
+
log.info('Closing all Glider-created tabs...');
|
|
725
|
+
const results = await wm.closeAll();
|
|
726
|
+
const success = results.filter(r => r.success).length;
|
|
727
|
+
log.ok(`Closed ${success}/${results.length} tabs`);
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
case 'focus': {
|
|
732
|
+
const targetId = args[1];
|
|
733
|
+
if (!targetId) {
|
|
734
|
+
log.fail('targetId required');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const result = await wm.focusTarget(targetId);
|
|
738
|
+
if (result.success) {
|
|
739
|
+
log.ok(`Focused: ${targetId}`);
|
|
740
|
+
} else {
|
|
741
|
+
log.fail(`Failed to focus: ${result.error}`);
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
case 'list':
|
|
747
|
+
default: {
|
|
748
|
+
const targets = wm.list();
|
|
749
|
+
if (targets.length === 0) {
|
|
750
|
+
log.warn('No windows/tabs tracked');
|
|
751
|
+
} else {
|
|
752
|
+
console.log(`${GREEN}${targets.length}${NC} target(s):\n`);
|
|
753
|
+
targets.forEach((t, i) => {
|
|
754
|
+
const marker = t.createdByGlider ? `${GREEN}●${NC}` : `${DIM}○${NC}`;
|
|
755
|
+
console.log(` ${marker} ${CYAN}${t.targetId.substring(0, 16)}...${NC}`);
|
|
756
|
+
console.log(` ${DIM}${t.url || 'unknown'}${NC}`);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
} catch (err) {
|
|
763
|
+
log.fail(err.message);
|
|
764
|
+
} finally {
|
|
765
|
+
wm.close();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
470
769
|
async function cmdDomains() {
|
|
471
770
|
const domainKeys = Object.keys(DOMAINS);
|
|
472
771
|
if (domainKeys.length === 0) {
|
|
@@ -546,6 +845,202 @@ async function cmdUrl() {
|
|
|
546
845
|
}
|
|
547
846
|
}
|
|
548
847
|
|
|
848
|
+
// Fetch URL using browser session (authenticated)
|
|
849
|
+
async function cmdFetch(url, opts = []) {
|
|
850
|
+
if (!url) {
|
|
851
|
+
log.fail('Usage: glider fetch <url> [--output file]');
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
log.info(`Fetching: ${url}`);
|
|
856
|
+
|
|
857
|
+
let outputFile = null;
|
|
858
|
+
for (let i = 0; i < opts.length; i++) {
|
|
859
|
+
if (opts[i] === '--output' || opts[i] === '-o') {
|
|
860
|
+
outputFile = opts[++i];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
const result = await httpPost('/cdp', {
|
|
866
|
+
method: 'Runtime.evaluate',
|
|
867
|
+
params: {
|
|
868
|
+
expression: `
|
|
869
|
+
(async () => {
|
|
870
|
+
const resp = await fetch(${JSON.stringify(url)});
|
|
871
|
+
const text = await resp.text();
|
|
872
|
+
try { return JSON.parse(text); } catch { return text; }
|
|
873
|
+
})()
|
|
874
|
+
`,
|
|
875
|
+
awaitPromise: true,
|
|
876
|
+
returnByValue: true
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const data = result?.result?.value;
|
|
881
|
+
const output = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
|
|
882
|
+
|
|
883
|
+
if (outputFile) {
|
|
884
|
+
fs.writeFileSync(outputFile, output);
|
|
885
|
+
log.ok(`Saved to ${outputFile}`);
|
|
886
|
+
} else {
|
|
887
|
+
console.log(output);
|
|
888
|
+
}
|
|
889
|
+
} catch (e) {
|
|
890
|
+
log.fail(`Fetch failed: ${e.message}`);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Spawn multiple tabs
|
|
896
|
+
async function cmdSpawn(urls) {
|
|
897
|
+
if (!urls || urls.length === 0) {
|
|
898
|
+
log.fail('Usage: glider spawn <url1> <url2> ...');
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Handle file input
|
|
903
|
+
if (urls[0] === '-f' && urls[1]) {
|
|
904
|
+
const content = fs.readFileSync(urls[1], 'utf8');
|
|
905
|
+
urls = content.split('\n').filter(u => u.trim());
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
log.info(`Spawning ${urls.length} tab(s)...`);
|
|
909
|
+
|
|
910
|
+
const results = [];
|
|
911
|
+
for (const url of urls) {
|
|
912
|
+
try {
|
|
913
|
+
const result = await httpPost('/cdp', {
|
|
914
|
+
method: 'Target.createTarget',
|
|
915
|
+
params: { url }
|
|
916
|
+
});
|
|
917
|
+
results.push({ url, targetId: result?.targetId });
|
|
918
|
+
log.ok(`Spawned: ${url}`);
|
|
919
|
+
} catch (e) {
|
|
920
|
+
log.warn(`Failed: ${url} - ${e.message}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
console.log(JSON.stringify(results, null, 2));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Extract from multiple tabs
|
|
928
|
+
async function cmdExtract(opts = []) {
|
|
929
|
+
let js = 'document.body.innerText';
|
|
930
|
+
let selector = null;
|
|
931
|
+
let limit = 10000;
|
|
932
|
+
let asJson = false;
|
|
933
|
+
|
|
934
|
+
for (let i = 0; i < opts.length; i++) {
|
|
935
|
+
if (opts[i] === '--js') js = opts[++i];
|
|
936
|
+
else if (opts[i] === '--selector' || opts[i] === '-s') selector = opts[++i];
|
|
937
|
+
else if (opts[i] === '--limit' || opts[i] === '-l') limit = parseInt(opts[++i], 10);
|
|
938
|
+
else if (opts[i] === '--json') asJson = true;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (selector) {
|
|
942
|
+
js = `document.querySelector(${JSON.stringify(selector)})?.innerText || ''`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
log.info('Extracting from connected tabs...');
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
const targets = await getTargets();
|
|
949
|
+
if (targets.length === 0) {
|
|
950
|
+
log.warn('No tabs connected');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const results = [];
|
|
955
|
+
for (const target of targets) {
|
|
956
|
+
const url = target.targetInfo?.url || 'unknown';
|
|
957
|
+
try {
|
|
958
|
+
const result = await httpPost('/cdp', {
|
|
959
|
+
method: 'Runtime.evaluate',
|
|
960
|
+
params: {
|
|
961
|
+
expression: js,
|
|
962
|
+
returnByValue: true
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
const text = String(result?.result?.value || '').slice(0, limit);
|
|
966
|
+
results.push({ url, text });
|
|
967
|
+
if (!asJson) {
|
|
968
|
+
console.log(`\n--- ${url} ---`);
|
|
969
|
+
console.log(text);
|
|
970
|
+
}
|
|
971
|
+
} catch (e) {
|
|
972
|
+
results.push({ url, error: e.message });
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (asJson) {
|
|
977
|
+
console.log(JSON.stringify(results, null, 2));
|
|
978
|
+
}
|
|
979
|
+
} catch (e) {
|
|
980
|
+
log.fail(`Extract failed: ${e.message}`);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Explore site (clicks around, captures network)
|
|
986
|
+
async function cmdExplore(url, opts = []) {
|
|
987
|
+
if (!url) {
|
|
988
|
+
log.fail('Usage: glider explore <url> [--depth N] [--output dir] [--har file]');
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
let depth = 2;
|
|
993
|
+
let outputDir = '/tmp/glider-explore';
|
|
994
|
+
let harFile = null;
|
|
995
|
+
|
|
996
|
+
for (let i = 0; i < opts.length; i++) {
|
|
997
|
+
if (opts[i] === '--depth' || opts[i] === '-d') depth = parseInt(opts[++i], 10);
|
|
998
|
+
else if (opts[i] === '--output' || opts[i] === '-o') outputDir = opts[++i];
|
|
999
|
+
else if (opts[i] === '--har') harFile = opts[++i];
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
log.info(`Exploring: ${url} (depth: ${depth})`);
|
|
1003
|
+
|
|
1004
|
+
// Use the bexplore.js library
|
|
1005
|
+
const bexplorePath = path.join(LIB_DIR, 'bexplore.js');
|
|
1006
|
+
if (fs.existsSync(bexplorePath)) {
|
|
1007
|
+
const { spawn } = require('child_process');
|
|
1008
|
+
const spawnArgs = [bexplorePath, url, '--depth', String(depth), '--output', outputDir];
|
|
1009
|
+
if (harFile) spawnArgs.push('--har', harFile);
|
|
1010
|
+
|
|
1011
|
+
const child = spawn('node', spawnArgs, {
|
|
1012
|
+
stdio: 'inherit'
|
|
1013
|
+
});
|
|
1014
|
+
await new Promise((resolve, reject) => {
|
|
1015
|
+
child.on('close', code => code === 0 ? resolve() : reject(new Error(`Exit code: ${code}`)));
|
|
1016
|
+
});
|
|
1017
|
+
} else {
|
|
1018
|
+
// Fallback: simple exploration
|
|
1019
|
+
await cmdGoto(url);
|
|
1020
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1021
|
+
|
|
1022
|
+
// Get all links
|
|
1023
|
+
const result = await httpPost('/cdp', {
|
|
1024
|
+
method: 'Runtime.evaluate',
|
|
1025
|
+
params: {
|
|
1026
|
+
expression: `Array.from(document.querySelectorAll('a[href]')).map(a => a.href).filter(h => h.startsWith('http'))`,
|
|
1027
|
+
returnByValue: true
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
const links = result?.result?.value || [];
|
|
1032
|
+
log.ok(`Found ${links.length} links`);
|
|
1033
|
+
|
|
1034
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1035
|
+
fs.writeFileSync(path.join(outputDir, 'links.json'), JSON.stringify(links, null, 2));
|
|
1036
|
+
|
|
1037
|
+
// Screenshot
|
|
1038
|
+
await cmdScreenshot(path.join(outputDir, 'screenshot.png'));
|
|
1039
|
+
|
|
1040
|
+
log.ok(`Output saved to ${outputDir}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
549
1044
|
// YAML Task Runner
|
|
550
1045
|
async function cmdRun(taskFile) {
|
|
551
1046
|
if (!taskFile || !fs.existsSync(taskFile)) {
|
|
@@ -803,42 +1298,63 @@ async function cmdLoop(taskFileOrPrompt, options = {}) {
|
|
|
803
1298
|
function showHelp() {
|
|
804
1299
|
showBanner();
|
|
805
1300
|
console.log(`
|
|
806
|
-
${
|
|
1301
|
+
${B5}USAGE${NC}
|
|
807
1302
|
glider <command> [args]
|
|
808
1303
|
|
|
809
|
-
${
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1304
|
+
${B5}SETUP${NC}
|
|
1305
|
+
${BW}install${NC} Install daemon ${DIM}(runs at login, auto-restarts)${NC}
|
|
1306
|
+
${BW}uninstall${NC} Remove daemon
|
|
1307
|
+
${BW}connect${NC} Connect to browser ${DIM}(run once per Chrome session)${NC}
|
|
1308
|
+
|
|
1309
|
+
${B5}STATUS${NC}
|
|
1310
|
+
${BW}status${NC} Check server, extension, tabs
|
|
1311
|
+
${BW}test${NC} Run diagnostics
|
|
1312
|
+
|
|
1313
|
+
${B5}NAVIGATION${NC}
|
|
1314
|
+
${BW}goto${NC} <url> Navigate to URL
|
|
1315
|
+
${BW}eval${NC} <js> Execute JavaScript
|
|
1316
|
+
${BW}click${NC} <selector> Click element
|
|
1317
|
+
${BW}type${NC} <sel> <text> Type into input
|
|
1318
|
+
${BW}screenshot${NC} [path] Take screenshot
|
|
815
1319
|
|
|
816
|
-
${
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
screenshot [path] Take screenshot
|
|
1320
|
+
${B5}PAGE INFO${NC}
|
|
1321
|
+
${BW}text${NC} Get page text
|
|
1322
|
+
${BW}html${NC} [selector] Get HTML
|
|
1323
|
+
${BW}title${NC} Get page title
|
|
1324
|
+
${BW}url${NC} Get current URL
|
|
1325
|
+
${BW}tabs${NC} List connected tabs
|
|
823
1326
|
|
|
824
|
-
${
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1327
|
+
${B5}MULTI-WINDOW${NC}
|
|
1328
|
+
${BW}window new${NC} [url] Create new browser window ${DIM}(closeable)${NC}
|
|
1329
|
+
${BW}window tab${NC} [url] Create tab in current window
|
|
1330
|
+
${BW}window close${NC} <id> Close specific tab/window
|
|
1331
|
+
${BW}window closeall${NC} Close all Glider-created tabs
|
|
1332
|
+
${BW}window focus${NC} <id> Bring tab to foreground
|
|
1333
|
+
${BW}window list${NC} List all windows/tabs
|
|
830
1334
|
|
|
831
|
-
${
|
|
832
|
-
|
|
833
|
-
|
|
1335
|
+
${B5}MULTI-TAB${NC}
|
|
1336
|
+
${BW}fetch${NC} <url> Fetch URL with browser session ${DIM}(auth)${NC}
|
|
1337
|
+
${BW}spawn${NC} <urls...> Open multiple tabs
|
|
1338
|
+
${BW}extract${NC} [opts] Extract from all tabs
|
|
1339
|
+
${BW}explore${NC} <url> Crawl site, capture network
|
|
834
1340
|
|
|
835
|
-
${
|
|
836
|
-
|
|
1341
|
+
${B5}AUTOMATION${NC}
|
|
1342
|
+
${BW}run${NC} <task.yaml> Execute YAML task file
|
|
1343
|
+
${BW}loop${NC} <task> [opts] Autonomous loop ${DIM}(run until complete)${NC}
|
|
1344
|
+
${BW}ralph${NC} <task> ${DIM}Alias for loop${NC}
|
|
837
1345
|
|
|
838
|
-
${
|
|
839
|
-
-n, --max-iterations N Max iterations (default: 10)
|
|
840
|
-
-t, --timeout N
|
|
841
|
-
-m, --marker STRING Completion marker (default: LOOP_COMPLETE)
|
|
1346
|
+
${B5}LOOP OPTIONS${NC}
|
|
1347
|
+
-n, --max-iterations N Max iterations ${DIM}(default: 10)${NC}
|
|
1348
|
+
-t, --timeout N Timeout in seconds ${DIM}(default: 3600)${NC}
|
|
1349
|
+
-m, --marker STRING Completion marker ${DIM}(default: LOOP_COMPLETE)${NC}
|
|
1350
|
+
|
|
1351
|
+
${B5}EXAMPLES${NC}
|
|
1352
|
+
${DIM}$${NC} glider install ${DIM}# one-time setup${NC}
|
|
1353
|
+
${DIM}$${NC} glider connect ${DIM}# connect to Chrome${NC}
|
|
1354
|
+
${DIM}$${NC} glider goto "https://x.com" ${DIM}# navigate${NC}
|
|
1355
|
+
${DIM}$${NC} glider eval "document.title"${DIM}# run JS${NC}
|
|
1356
|
+
${DIM}$${NC} glider run scrape.yaml ${DIM}# run task${NC}
|
|
1357
|
+
${DIM}$${NC} glider loop task.yaml -n 50 ${DIM}# autonomous loop${NC}
|
|
842
1358
|
|
|
843
1359
|
${YELLOW}TASK FILE FORMAT:${NC}
|
|
844
1360
|
name: "Task name"
|
|
@@ -881,11 +1397,11 @@ ${YELLOW}DOMAIN EXTENSIONS:${NC}
|
|
|
881
1397
|
Then: glider mysite -> navigates to that URL
|
|
882
1398
|
glider mytool -> runs that script
|
|
883
1399
|
`);
|
|
884
|
-
|
|
885
|
-
// Show loaded domains if any
|
|
1400
|
+
|
|
1401
|
+
// Show loaded domains if any (from local config)
|
|
886
1402
|
const domainKeys = Object.keys(DOMAINS);
|
|
887
1403
|
if (domainKeys.length > 0) {
|
|
888
|
-
console.log(`${YELLOW}LOADED DOMAINS:${NC} (from config)`);
|
|
1404
|
+
console.log(`${YELLOW}LOADED DOMAINS:${NC} (from local config)`);
|
|
889
1405
|
for (const key of domainKeys) {
|
|
890
1406
|
const d = DOMAINS[key];
|
|
891
1407
|
const desc = d.description || d.url || d.script || '';
|
|
@@ -914,6 +1430,11 @@ async function main() {
|
|
|
914
1430
|
}
|
|
915
1431
|
|
|
916
1432
|
switch (cmd) {
|
|
1433
|
+
case 'help':
|
|
1434
|
+
case '--help':
|
|
1435
|
+
case '-h':
|
|
1436
|
+
showHelp();
|
|
1437
|
+
break;
|
|
917
1438
|
case 'status':
|
|
918
1439
|
await cmdStatus();
|
|
919
1440
|
break;
|
|
@@ -926,12 +1447,25 @@ async function main() {
|
|
|
926
1447
|
case 'restart':
|
|
927
1448
|
await cmdRestart();
|
|
928
1449
|
break;
|
|
1450
|
+
case 'install':
|
|
1451
|
+
await cmdInstallDaemon();
|
|
1452
|
+
break;
|
|
1453
|
+
case 'uninstall':
|
|
1454
|
+
await cmdUninstallDaemon();
|
|
1455
|
+
break;
|
|
1456
|
+
case 'connect':
|
|
1457
|
+
await cmdConnect();
|
|
1458
|
+
break;
|
|
929
1459
|
case 'test':
|
|
930
1460
|
await cmdTest();
|
|
931
1461
|
break;
|
|
932
1462
|
case 'tabs':
|
|
933
1463
|
await cmdTabs();
|
|
934
1464
|
break;
|
|
1465
|
+
case 'window':
|
|
1466
|
+
case 'win':
|
|
1467
|
+
await cmdWindow(args.slice(1));
|
|
1468
|
+
break;
|
|
935
1469
|
case 'domains':
|
|
936
1470
|
await cmdDomains();
|
|
937
1471
|
break;
|
|
@@ -970,7 +1504,20 @@ async function main() {
|
|
|
970
1504
|
case 'run':
|
|
971
1505
|
await cmdRun(args[1]);
|
|
972
1506
|
break;
|
|
1507
|
+
case 'fetch':
|
|
1508
|
+
await cmdFetch(args[1], args.slice(2));
|
|
1509
|
+
break;
|
|
1510
|
+
case 'spawn':
|
|
1511
|
+
await cmdSpawn(args.slice(1));
|
|
1512
|
+
break;
|
|
1513
|
+
case 'extract':
|
|
1514
|
+
await cmdExtract(args.slice(1));
|
|
1515
|
+
break;
|
|
1516
|
+
case 'explore':
|
|
1517
|
+
await cmdExplore(args[1], args.slice(2));
|
|
1518
|
+
break;
|
|
973
1519
|
case 'loop':
|
|
1520
|
+
case 'ralph': // alias for loop - Ralph Wiggum pattern
|
|
974
1521
|
// Parse loop options
|
|
975
1522
|
const loopOpts = {
|
|
976
1523
|
maxIterations: 10,
|