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/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
- // 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() {
@@ -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
- console.log('═══════════════════════════════════════');
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 ? `${GREEN}[1/4]${NC} Server: OK` : `${RED}[1/4]${NC} Server: FAIL`);
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 ? `${GREEN}[2/4]${NC} Extension: OK` : `${RED}[2/4]${NC} Extension: NOT CONNECTED`);
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 ? `${GREEN}[3/4]${NC} Tab: OK` : `${RED}[3/4]${NC} Tab: NO TABS`);
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
- ${YELLOW}USAGE:${NC}
1301
+ ${B5}USAGE${NC}
807
1302
  glider <command> [args]
808
1303
 
809
- ${YELLOW}SERVER:${NC}
810
- status Check server, extension, tabs
811
- start Start relay server
812
- stop Stop relay server
813
- restart Stop then start relay server
814
- test Run connectivity test
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
- ${YELLOW}NAVIGATION:${NC}
817
- goto <url> Navigate current tab to URL
818
- open <url> Open URL in default browser
819
- eval <js> Execute JavaScript, return result
820
- click <selector> Click element
821
- type <sel> <text> Type into input
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
- ${YELLOW}PAGE INFO:${NC}
825
- text Get page text content
826
- html [selector] Get page HTML (or element HTML)
827
- title Get page title
828
- url Get current URL
829
- tabs List connected tabs
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
- ${YELLOW}AUTOMATION:${NC}
832
- run <task.yaml> Execute YAML task file
833
- 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
834
1340
 
835
- ${YELLOW}CONFIG:${NC}
836
- domains List configured domain shortcuts
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
- ${YELLOW}LOOP OPTIONS:${NC}
839
- -n, --max-iterations N Max iterations (default: 10)
840
- -t, --timeout N Max runtime in seconds (default: 3600)
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,