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/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}`;
35
- const SCRIPTS_DIR = process.env.SCRIPTS || path.join(os.homedir(), 'scripts');
36
+ const DEBUG_URL = `http://127.0.0.1:${DEBUG_PORT}`;
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
- // Gradient colors for rainbow effect
67
- const G1 = '\x1b[38;5;51m'; // cyan
68
- const G2 = '\x1b[38;5;45m'; // teal
69
- const G3 = '\x1b[38;5;39m'; // blue
70
- const G4 = '\x1b[38;5;33m'; // deeper blue
71
- const G5 = '\x1b[38;5;27m'; // indigo
72
- const G6 = '\x1b[38;5;21m'; // purple
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 - simple ASCII, works everywhere
80
+ // Banner - hang glider ASCII art matching logo
75
81
  const BANNER = `
76
- ${CYAN} ------------------------------------------------------->${NC}
77
- ${CYAN} _____ ${BLUE}__ ${MAGENTA}__ ${CYAN}____ ${BLUE}_____ ${MAGENTA}____ ${NC}
78
- ${CYAN} / ____|${BLUE}| | ${MAGENTA}| |${CYAN}| _ \\${BLUE}| ____|${MAGENTA}| _ \\ ${NC}
79
- ${CYAN} | | __ ${BLUE}| | ${MAGENTA}| |${CYAN}| | | ${BLUE}| _| ${MAGENTA}| |_) |${NC}
80
- ${CYAN} | | |_ |${BLUE}| | ${MAGENTA}| |${CYAN}| | | ${BLUE}| |___${MAGENTA}| _ < ${NC}
81
- ${CYAN} | |__| |${BLUE}| |___${MAGENTA}| |${CYAN}| |_| ${BLUE}| ____|${MAGENTA}| | \\ \\${NC}
82
- ${CYAN} \\_____|${BLUE}|_____|${MAGENTA}__|${CYAN}|____/${BLUE}|_____|${MAGENTA}|_| \\_\\${NC}
83
- ${CYAN} ------------------------------------------------------->${NC}
84
- ${DIM} Browser Automation CLI ${WHITE}v${require('../package.json').version}${NC} ${DIM}|${NC} ${CYAN}github.com/vdutts7/glidercli${NC}
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(`${BLUE}→${NC} ${msg}`),
95
- warn: (msg) => console.error(`${YELLOW}!${NC} ${msg}`),
96
- step: (msg) => console.error(`${CYAN}[STEP]${NC} ${msg}`),
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
- console.log('═══════════════════════════════════════');
183
- console.log(' STATUS');
184
- console.log('═══════════════════════════════════════');
206
+ log.box('STATUS');
185
207
 
186
208
  const serverOk = await checkServer();
187
- console.log(serverOk ? `${GREEN}✓${NC} Server running on port ${PORT}` : `${RED}✗${NC} Server not running`);
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 ? `${GREEN}✓${NC} Extension connected` : `${RED}✗${NC} Extension not connected`);
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(`${GREEN}✓${NC} ${targets.length} tab(s) connected:`);
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(` ${CYAN}${url}${NC}`);
221
+ console.log(` ${B5}${url}${NC}`);
200
222
  });
201
223
  } else {
202
- console.log(`${YELLOW}!${NC} No tabs connected`);
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() {
@@ -213,7 +238,7 @@ async function cmdStart() {
213
238
  }
214
239
 
215
240
  log.info('Starting glider server...');
216
- const bserve = path.join(SCRIPTS_DIR, 'bserve');
241
+ const bserve = path.join(LIB_DIR, 'bserve.js');
217
242
 
218
243
  if (!fs.existsSync(bserve)) {
219
244
  log.fail(`bserve not found at ${bserve}`);
@@ -401,6 +426,621 @@ async function cmdText() {
401
426
  }
402
427
  }
403
428
 
429
+ // ═══════════════════════════════════════════════════════════════════
430
+ // NEW COMMANDS: restart, test, tabs, domains, open, html, title, url
431
+ // ═══════════════════════════════════════════════════════════════════
432
+
433
+ async function cmdRestart() {
434
+ await cmdStop();
435
+ await new Promise(r => setTimeout(r, 500));
436
+ await cmdStart();
437
+ }
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
+
623
+ async function cmdTest() {
624
+ showBanner();
625
+ log.box('DIAGNOSTICS');
626
+
627
+ // Test 1: Server
628
+ const serverOk = await checkServer();
629
+ console.log(serverOk ? ` ${GREEN}✓${NC} ${B5}[1/4]${NC} Server` : ` ${RED}✗${NC} ${B5}[1/4]${NC} Server`);
630
+ if (!serverOk) {
631
+ log.info('Starting server...');
632
+ await cmdStart();
633
+ }
634
+
635
+ // Test 2: Extension
636
+ const extOk = await checkExtension();
637
+ console.log(extOk ? ` ${GREEN}✓${NC} ${B5}[2/4]${NC} Extension` : ` ${RED}✗${NC} ${B5}[2/4]${NC} Extension`);
638
+
639
+ // Test 3: Tab
640
+ const tabOk = await checkTab();
641
+ console.log(tabOk ? ` ${GREEN}✓${NC} ${B5}[3/4]${NC} Tab attached` : ` ${RED}✗${NC} ${B5}[3/4]${NC} No tabs`);
642
+
643
+ // Test 4: CDP command
644
+ if (tabOk) {
645
+ try {
646
+ const result = await httpPost('/cdp', {
647
+ method: 'Runtime.evaluate',
648
+ params: { expression: '1+1', returnByValue: true }
649
+ });
650
+ const cdpOk = result.result?.value === 2;
651
+ console.log(cdpOk ? `${GREEN}[4/4]${NC} CDP: OK` : `${RED}[4/4]${NC} CDP: FAIL`);
652
+ } catch {
653
+ console.log(`${RED}[4/4]${NC} CDP: FAIL`);
654
+ }
655
+ } else {
656
+ console.log(`${YELLOW}[4/4]${NC} CDP: SKIPPED (no tab)`);
657
+ }
658
+
659
+ console.log('═══════════════════════════════════════');
660
+ }
661
+
662
+ async function cmdTabs() {
663
+ const targets = await getTargets();
664
+ if (targets.length === 0) {
665
+ log.warn('No tabs connected');
666
+ return;
667
+ }
668
+ console.log(`${GREEN}${targets.length}${NC} tab(s) connected:\n`);
669
+ targets.forEach((t, i) => {
670
+ const url = t.targetInfo?.url || 'unknown';
671
+ const title = t.targetInfo?.title || '';
672
+ console.log(` ${CYAN}[${i + 1}]${NC} ${title}`);
673
+ console.log(` ${DIM}${url}${NC}`);
674
+ });
675
+ }
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
+
769
+ async function cmdDomains() {
770
+ const domainKeys = Object.keys(DOMAINS);
771
+ if (domainKeys.length === 0) {
772
+ log.warn('No domains configured');
773
+ log.info('Add domains to ~/.cursor/glider/domains.json or ~/.glider/domains.json');
774
+ return;
775
+ }
776
+ console.log(`${GREEN}${domainKeys.length}${NC} domain(s) configured:\n`);
777
+ for (const key of domainKeys) {
778
+ const d = DOMAINS[key];
779
+ const type = d.script ? 'script' : 'url';
780
+ const target = d.script || d.url || '';
781
+ console.log(` ${CYAN}${key}${NC} ${DIM}(${type})${NC}`);
782
+ if (d.description) console.log(` ${d.description}`);
783
+ console.log(` ${DIM}${target}${NC}`);
784
+ }
785
+ }
786
+
787
+ async function cmdOpen(url) {
788
+ if (!url) {
789
+ log.fail('Usage: glider open <url>');
790
+ process.exit(1);
791
+ }
792
+
793
+ // Open URL in default browser (not in connected tab)
794
+ const { exec } = require('child_process');
795
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
796
+ exec(`${cmd} "${url}"`, (err) => {
797
+ if (err) {
798
+ log.fail(`Failed to open: ${err.message}`);
799
+ process.exit(1);
800
+ }
801
+ log.ok(`Opened: ${url}`);
802
+ });
803
+ }
804
+
805
+ async function cmdHtml(selector) {
806
+ try {
807
+ const expression = selector
808
+ ? `document.querySelector('${selector.replace(/'/g, "\\'")}')?.outerHTML || 'Element not found'`
809
+ : 'document.documentElement.outerHTML';
810
+
811
+ const result = await httpPost('/cdp', {
812
+ method: 'Runtime.evaluate',
813
+ params: { expression, returnByValue: true }
814
+ });
815
+ console.log(result.result?.value || '');
816
+ } catch (e) {
817
+ log.fail(`HTML extraction failed: ${e.message}`);
818
+ process.exit(1);
819
+ }
820
+ }
821
+
822
+ async function cmdTitle() {
823
+ try {
824
+ const result = await httpPost('/cdp', {
825
+ method: 'Runtime.evaluate',
826
+ params: { expression: 'document.title', returnByValue: true }
827
+ });
828
+ console.log(result.result?.value || '');
829
+ } catch (e) {
830
+ log.fail(`Title extraction failed: ${e.message}`);
831
+ process.exit(1);
832
+ }
833
+ }
834
+
835
+ async function cmdUrl() {
836
+ try {
837
+ const result = await httpPost('/cdp', {
838
+ method: 'Runtime.evaluate',
839
+ params: { expression: 'window.location.href', returnByValue: true }
840
+ });
841
+ console.log(result.result?.value || '');
842
+ } catch (e) {
843
+ log.fail(`URL extraction failed: ${e.message}`);
844
+ process.exit(1);
845
+ }
846
+ }
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
+
404
1044
  // YAML Task Runner
405
1045
  async function cmdRun(taskFile) {
406
1046
  if (!taskFile || !fs.existsSync(taskFile)) {
@@ -658,30 +1298,63 @@ async function cmdLoop(taskFileOrPrompt, options = {}) {
658
1298
  function showHelp() {
659
1299
  showBanner();
660
1300
  console.log(`
661
- ${YELLOW}USAGE:${NC}
1301
+ ${B5}USAGE${NC}
662
1302
  glider <command> [args]
663
1303
 
664
- ${YELLOW}SERVER:${NC}
665
- status Check server, extension, tabs
666
- start Start relay server
667
- stop Stop relay server
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
1319
+
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
668
1326
 
669
- ${YELLOW}NAVIGATION:${NC}
670
- goto <url> Navigate current tab to URL
671
- eval <js> Execute JavaScript, return result
672
- click <selector> Click element
673
- type <sel> <text> Type into input
674
- screenshot [path] Take screenshot
675
- text Get page text content
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
676
1334
 
677
- ${YELLOW}AUTOMATION:${NC}
678
- run <task.yaml> Execute YAML task file
679
- loop <task> [opts] Run in Ralph Wiggum loop until complete
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
680
1340
 
681
- ${YELLOW}LOOP OPTIONS:${NC}
682
- -n, --max-iterations N Max iterations (default: 10)
683
- -t, --timeout N Max runtime in seconds (default: 3600)
684
- -m, --marker STRING Completion marker (default: LOOP_COMPLETE)
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}
1345
+
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}
685
1358
 
686
1359
  ${YELLOW}TASK FILE FORMAT:${NC}
687
1360
  name: "Task name"
@@ -700,6 +1373,7 @@ ${YELLOW}EXAMPLES:${NC}
700
1373
  glider start
701
1374
  glider goto "https://google.com"
702
1375
  glider eval "document.title"
1376
+ glider html "div.main"
703
1377
  glider run mytask.yaml
704
1378
  glider loop mytask.yaml -n 20 -t 600
705
1379
 
@@ -712,7 +1386,6 @@ ${YELLOW}RALPH WIGGUM PATTERN:${NC}
712
1386
 
713
1387
  ${YELLOW}REQUIREMENTS:${NC}
714
1388
  - Node.js 18+
715
- - bserve relay server (~/scripts/bserve)
716
1389
  - Glider Chrome extension connected
717
1390
 
718
1391
  ${YELLOW}DOMAIN EXTENSIONS:${NC}
@@ -721,14 +1394,14 @@ ${YELLOW}DOMAIN EXTENSIONS:${NC}
721
1394
  "mysite": { "url": "https://mysite.com/dashboard" },
722
1395
  "mytool": { "script": "~/.cursor/tools/scripts/mytool.sh" }
723
1396
  }
724
- Then: glider mysite navigates to that URL
725
- glider mytool runs that script
1397
+ Then: glider mysite -> navigates to that URL
1398
+ glider mytool -> runs that script
726
1399
  `);
727
-
728
- // Show loaded domains if any
1400
+
1401
+ // Show loaded domains if any (from local config)
729
1402
  const domainKeys = Object.keys(DOMAINS);
730
1403
  if (domainKeys.length > 0) {
731
- console.log(`${YELLOW}LOADED DOMAINS:${NC} (from config)`);
1404
+ console.log(`${YELLOW}LOADED DOMAINS:${NC} (from local config)`);
732
1405
  for (const key of domainKeys) {
733
1406
  const d = DOMAINS[key];
734
1407
  const desc = d.description || d.url || d.script || '';
@@ -757,6 +1430,11 @@ async function main() {
757
1430
  }
758
1431
 
759
1432
  switch (cmd) {
1433
+ case 'help':
1434
+ case '--help':
1435
+ case '-h':
1436
+ showHelp();
1437
+ break;
760
1438
  case 'status':
761
1439
  await cmdStatus();
762
1440
  break;
@@ -766,10 +1444,38 @@ async function main() {
766
1444
  case 'stop':
767
1445
  await cmdStop();
768
1446
  break;
1447
+ case 'restart':
1448
+ await cmdRestart();
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;
1459
+ case 'test':
1460
+ await cmdTest();
1461
+ break;
1462
+ case 'tabs':
1463
+ await cmdTabs();
1464
+ break;
1465
+ case 'window':
1466
+ case 'win':
1467
+ await cmdWindow(args.slice(1));
1468
+ break;
1469
+ case 'domains':
1470
+ await cmdDomains();
1471
+ break;
769
1472
  case 'goto':
770
1473
  case 'navigate':
771
1474
  await cmdGoto(args[1]);
772
1475
  break;
1476
+ case 'open':
1477
+ await cmdOpen(args[1]);
1478
+ break;
773
1479
  case 'eval':
774
1480
  case 'js':
775
1481
  await cmdEval(args.slice(1).join(' '));
@@ -786,10 +1492,32 @@ async function main() {
786
1492
  case 'text':
787
1493
  await cmdText();
788
1494
  break;
1495
+ case 'html':
1496
+ await cmdHtml(args[1]);
1497
+ break;
1498
+ case 'title':
1499
+ await cmdTitle();
1500
+ break;
1501
+ case 'url':
1502
+ await cmdUrl();
1503
+ break;
789
1504
  case 'run':
790
1505
  await cmdRun(args[1]);
791
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;
792
1519
  case 'loop':
1520
+ case 'ralph': // alias for loop - Ralph Wiggum pattern
793
1521
  // Parse loop options
794
1522
  const loopOpts = {
795
1523
  maxIterations: 10,