glidercli 0.3.7 → 0.3.9

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 CHANGED
@@ -8,8 +8,8 @@
8
8
  <h1 align="center">glider CLI</h1>
9
9
  <p align="center"><i><b>Browser automation CLI with autonomous loop execution</b></i></p>
10
10
 
11
- [![Github][github]][github-url]
12
- [![npm][npm]][npm-url]
11
+ <a href="https://github.com/vdutts7/glidercli"><img src="./assets/badges/github.badge.svg" alt="GitHub" height="34" /></a> &nbsp; <a href="https://www.npmjs.com/package/glidercli"><img src="./assets/badges/npm.badge.svg" alt="npm install" height="34" /></a>
12
+
13
13
  </div>
14
14
 
15
15
  <br/>
@@ -53,17 +53,16 @@
53
53
 
54
54
  ### Browser support
55
55
 
56
- Icons: `https://raw.githubusercontent.com/vdutts7/squircle/main/webp/{slug}.webp` ([squircle](https://github.com/vdutts7/squircle)).
57
-
58
56
  **Extension:** Install [Glider](https://chromewebstore.google.com/detail/glider/njbidokkffhgpofcejgcfcgcinmeoalj) from the Chrome Web Store in each browser/profile you plan to automate
59
57
 
60
58
  | | Browser | Config |
61
59
  |:---:|--------|--------|
62
- | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/chrome.webp" width="16" alt=""> | Google Chrome | Default for `glider connect`. |
60
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/chrome.webp" width="16" alt=""> | Google Chrome | Default for `glider connect`|
61
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/chrome-canary.webp" width="16" alt=""> | Chrome Canary | n/a |
63
62
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/arc.webp" width="16" alt=""> | Arc | [browser.json](config/browser.json.example) (`{ "use": "arc" }`) |
64
63
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/microsoft/microsoft-edge.webp" width="16" alt=""> | Microsoft Edge | n/a |
65
64
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/brave.webp" width="16" alt=""> | Brave | n/a |
66
- | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/opera-gx.webp" width="16" alt=""> | Opera | n/a |
65
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/operagx.webp" width="16" alt=""> | Opera | n/a |
67
66
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/vivaldi.webp" width="16" alt=""> | Vivaldi | n/a |
68
67
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/chromium.webp" width="16" alt=""> | Other Chromium | Must support installing extensions from the Chrome Web Store. |
69
68
 
@@ -86,10 +85,14 @@ Icons: `https://raw.githubusercontent.com/vdutts7/squircle/main/webp/{slug}.webp
86
85
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/tor-browser.webp" width="16" alt=""> | Tor Browser | Gecko- same constraints as Firefox |
87
86
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/mullvad-browser.webp" width="16" alt=""> | Mullvad Browser | Gecko- same constraints as Firefox |
88
87
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/icecat.webp" width="16" alt=""> | IceCat | Gecko- same constraints as Firefox |
89
- | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/safari.webp" width="16" alt=""> | Safari | WebKit (Apple desktop). Not Chromium |
88
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/safari.webp" width="16" alt=""> | Safari | WebKit (Apple desktop). Not Chromium |
89
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/safari-technology-preview.webp" width="16" alt=""> | Safari Technology Preview | WebKit preview channel (Apple desktop). Not Chromium |
90
90
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/orion.webp" width="16" alt=""> | Orion | WebKit-based desktop browser. Not Chromium |
91
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/dia-browser.webp" width="16" alt=""> | Dia | AI-first browser, not in Glider’s supported Chromium + CWS model today |
91
92
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/chatgpt-atlas.webp" width="16" alt=""> | ChatGPT Atlas | AI-first browser, not in Glider’s supported Chromium + CWS model today |
92
93
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/perplexity-comet.webp" width="16" alt=""> | Perplexity Comet | AI-first browser, not in Glider’s supported Chromium + CWS model today |
94
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/whale-browser.webp" width="16" alt=""> | Whale Browser | Chromium-based, support not implemented in Glider today |
95
+ | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/yandex-browser.webp" width="16" alt=""> | Yandex Browser | Chromium-based, support not implemented in Glider today |
93
96
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/bromite.webp" width="16" alt=""> | Bromite | Chromium-derived, no practical Chrome Web Store path for Glider |
94
97
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/chromium.webp" width="16" alt=""> | Chromite | Chromium-derived, no practical Chrome Web Store path for Glider |
95
98
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/grapheneos.webp" width="16" alt=""> | Vanadium | Chromium-derived (GrapheneOS), no practical Chrome Web Store path for Glider |
@@ -113,7 +116,7 @@ Set browser by key from the browsers registry. Run `glider use <key>` to write t
113
116
  }
114
117
  ```
115
118
 
116
- Registry is loaded from (first found): `$HOME/.cursor/registry/browsers.json`, `$HOME/.glider/config/browsers-registry.json`. Keys are predefined (e.g. `arc`, `brave`, `chrome`, `edge`, `opera`, `vivaldi`, `chromium`). Edit the registry to add or change paths
119
+ Registry is loaded from: `$HOME/.glider/config/browsers-registry.json`. Keys are predefined (e.g. `arc`, `brave`, `chrome`, `edge`, `opera`, `vivaldi`, `chromium`). Edit the registry to add or change paths
117
120
 
118
121
  **Option B- Explicit name/path:**
119
122
 
@@ -147,11 +150,10 @@ Check app name in Finder/Spotlight, process name: `ps aux | grep -i <name>`
147
150
 
148
151
  ### Browser registry (keymap)
149
152
 
150
- A single registry file can define all Chromium browsers, Glider picks one by key
153
+ A single registry file can define all Chromium browsers, Glider picks one by **key**
151
154
 
152
- **Registry location (first found):**
155
+ **Registry location:**
153
156
 
154
- - `$HOME/.cursor/registry/browsers.json`
155
157
  - `$HOME/.glider/config/browsers-registry.json`
156
158
 
157
159
  **Registry format:**
@@ -192,7 +194,7 @@ Add or edit entries in the registry to match your machine (e.g. custom install p
192
194
  |-------|--------|
193
195
  | Supported | Chromium-based + Chrome Web Store extension (see table above). |
194
196
  | Not supported | See **Future** in this README. |
195
- | Configure | `$HOME/.glider/config/browser.json` (use key or name/path). Registry: `$HOME/.cursor/registry/browsers.json` or `$HOME/.glider/config/browsers-registry.json` |
197
+ | Configure | `$HOME/.glider/config/browser.json` (use key or name/path). Registry: `$HOME/.glider/config/browsers-registry.json` |
196
198
  | Switch | `glider use <key>` (e.g. `glider use arc`, `glider use brave`). |
197
199
  | Path | Optional, use when app is not in default location |
198
200
 
@@ -260,20 +262,14 @@ Full list: `glider --help`
260
262
 
261
263
  ## Roadmap
262
264
 
263
- | Status | Area |
264
- |--------|------|
265
- | Done | CDP relay + extension, YAML tasks, loop, daemon, multi-tab |
266
- | Todo | Linux / Windows, headless, task chaining, scraping templates |
265
+ - [x] CDP relay + extension, YAML tasks, loop, daemon, multi-tab
266
+ - [ ] Linux / Windows
267
+ - [ ] headless (cloud)
268
+ - [ ] task chaining
269
+ - [ ] crawling templates
267
270
 
268
271
  ---
269
272
 
270
273
  ## Contact
271
274
 
272
275
  <a href="https://vd7.io"><img src="https://res.cloudinary.com/ddyc1es5v/image/upload/v1773910810/readme-badges/readme-badge-vd7.png" alt="vd7.io" height="40" /></a> &nbsp; <a href="https://x.com/vdutts7"><img src="https://res.cloudinary.com/ddyc1es5v/image/upload/v1773910817/readme-badges/readme-badge-x.png" alt="/vdutts7" height="40" /></a>
273
-
274
-
275
- <!-- BADGES -->
276
- [github]: https://img.shields.io/badge/glidercli-000000?style=for-the-badge&logo=github
277
- [github-url]: https://github.com/vdutts7/glidercli
278
- [npm]: https://img.shields.io/badge/npm%20i%20--g%20glidercli-CB3837?style=for-the-badge&logo=npm
279
- [npm-url]: https://www.npmjs.com/package/glidercli
package/bin/glider.js CHANGED
@@ -39,6 +39,14 @@ const STATE_FILE = '/tmp/glider-state.json';
39
39
  const LOG_FILE = '/tmp/glider.log';
40
40
  const REGISTRY_FILE = path.join(LIB_DIR, 'registry.json');
41
41
 
42
+ // Active CDP session (multi-tab). Env or --session / --session-id on CLI.
43
+ let activeSessionId = process.env.GLIDER_SESSION_ID || null;
44
+ let jsonOutput = false;
45
+ let allowedDomainList = null;
46
+ const SESSION_STORE = path.join(os.homedir(), '.glider', 'config', 'active-session.json');
47
+ const { resolveAllowedDomains, assertUrlAllowed, urlAllowed } = require(path.join(LIB_DIR, 'guard.js'));
48
+ const { buildSnapshotExpression, formatSnapshotText } = require(path.join(LIB_DIR, 'bsnapshot.js'));
49
+
42
50
  // Load pattern registry
43
51
  let REGISTRY = {};
44
52
  if (fs.existsSync(REGISTRY_FILE)) {
@@ -65,7 +73,7 @@ for (const cfgPath of DOMAIN_CONFIG_PATHS) {
65
73
  }
66
74
  }
67
75
 
68
- // Browser config - which browser to launch/use (must be Chromium-based, see docs/BROWSERS.md)
76
+ // Browser config- which browser to launch/use (must be Chromium-based, see README.md#Browsers)
69
77
  const BROWSER_CONFIG_PATHS = [
70
78
  path.join(os.homedir(), '.glider', 'config', 'browser.json'),
71
79
  path.join(os.homedir(), '.glider', 'browser.json'),
@@ -197,10 +205,63 @@ function httpGet(urlPath) {
197
205
  });
198
206
  }
199
207
 
208
+ function loadPersistedSession() {
209
+ if (activeSessionId) return;
210
+ if (!fs.existsSync(SESSION_STORE)) return;
211
+ try {
212
+ const data = JSON.parse(fs.readFileSync(SESSION_STORE, 'utf8'));
213
+ if (data.sessionId) activeSessionId = data.sessionId;
214
+ } catch { /* ignore */ }
215
+ }
216
+
217
+ function persistSession(sessionId) {
218
+ activeSessionId = sessionId;
219
+ fs.mkdirSync(path.dirname(SESSION_STORE), { recursive: true });
220
+ fs.writeFileSync(SESSION_STORE, JSON.stringify({ sessionId, updated: new Date().toISOString() }, null, 2));
221
+ }
222
+
223
+ function emitJson(ok, observation, error = null, warnings = []) {
224
+ console.log(JSON.stringify({ ok, observation, error, warnings }, null, 2));
225
+ if (!ok) process.exit(1);
226
+ }
227
+
228
+ async function assertCurrentUrlAllowed(action) {
229
+ if (!allowedDomainList) return;
230
+ const result = await httpPost('/cdp', {
231
+ method: 'Runtime.evaluate',
232
+ params: { expression: 'location.href', returnByValue: true },
233
+ });
234
+ const url = result?.result?.value;
235
+ if (url) assertUrlAllowed(url, allowedDomainList, action);
236
+ }
237
+
238
+ function parseGlobalFlags(argv) {
239
+ const rest = [];
240
+ const cliDomains = [];
241
+ for (let i = 0; i < argv.length; i++) {
242
+ const a = argv[i];
243
+ if ((a === '--session' || a === '--session-id') && argv[i + 1]) {
244
+ activeSessionId = argv[++i];
245
+ } else if (a === '--json') {
246
+ jsonOutput = true;
247
+ } else if (a === '--allowed-domains' && argv[i + 1]) {
248
+ cliDomains.push(...String(argv[++i]).split(',').map((s) => s.trim()).filter(Boolean));
249
+ } else {
250
+ rest.push(a);
251
+ }
252
+ }
253
+ allowedDomainList = resolveAllowedDomains(cliDomains);
254
+ return rest;
255
+ }
256
+
200
257
  function httpPost(urlPath, body) {
258
+ const payload = { ...body };
259
+ if (urlPath === '/cdp' && activeSessionId && payload.sessionId == null) {
260
+ payload.sessionId = activeSessionId;
261
+ }
201
262
  return new Promise((resolve, reject) => {
202
263
  const url = new URL(urlPath, SERVER_URL);
203
- const data = JSON.stringify(body);
264
+ const data = JSON.stringify(payload);
204
265
  const req = http.request(url, {
205
266
  method: 'POST',
206
267
  headers: { 'Content-Type': 'application/json' },
@@ -421,21 +482,33 @@ async function cmdGoto(url) {
421
482
  process.exit(1);
422
483
  }
423
484
 
424
- // Auto-connect if not connected
425
485
  if (!await ensureConnected()) {
426
486
  process.exit(1);
427
487
  }
488
+
489
+ try {
490
+ assertUrlAllowed(url, allowedDomainList, 'goto');
491
+ } catch (e) {
492
+ if (jsonOutput) emitJson(false, null, e.message);
493
+ log.fail(e.message);
494
+ process.exit(1);
495
+ }
428
496
 
429
- log.info(`Navigating to: ${url}`);
497
+ if (!jsonOutput) log.info(`Navigating to: ${url}`);
430
498
 
431
499
  try {
432
500
  const result = await httpPost('/cdp', {
433
501
  method: 'Page.navigate',
434
502
  params: { url }
435
503
  });
436
- console.log(JSON.stringify(result));
437
- log.ok('Navigated');
504
+ if (jsonOutput) {
505
+ emitJson(true, { url, navigate: result });
506
+ } else {
507
+ console.log(JSON.stringify(result));
508
+ log.ok('Navigated');
509
+ }
438
510
  } catch (e) {
511
+ if (jsonOutput) emitJson(false, null, e.message);
439
512
  log.fail(`Navigation failed: ${e.message}`);
440
513
  process.exit(1);
441
514
  }
@@ -462,14 +535,18 @@ async function cmdEval(js) {
462
535
  }
463
536
  });
464
537
 
465
- if (result.result?.value !== undefined) {
466
- console.log(JSON.stringify(result.result.value));
538
+ const value = result.result?.value;
539
+ if (jsonOutput) {
540
+ emitJson(true, value !== undefined ? value : result.result);
541
+ } else if (value !== undefined) {
542
+ console.log(JSON.stringify(value));
467
543
  } else if (result.result?.description) {
468
544
  console.log(result.result.description);
469
545
  } else {
470
546
  console.log(JSON.stringify(result));
471
547
  }
472
548
  } catch (e) {
549
+ if (jsonOutput) emitJson(false, null, e.message);
473
550
  log.fail(`Eval failed: ${e.message}`);
474
551
  process.exit(1);
475
552
  }
@@ -481,10 +558,17 @@ async function cmdClick(selector) {
481
558
  process.exit(1);
482
559
  }
483
560
 
484
- // Auto-connect if not connected
485
561
  if (!await ensureConnected()) {
486
562
  process.exit(1);
487
563
  }
564
+
565
+ try {
566
+ await assertCurrentUrlAllowed('click');
567
+ } catch (e) {
568
+ if (jsonOutput) emitJson(false, null, e.message);
569
+ log.fail(e.message);
570
+ process.exit(1);
571
+ }
488
572
 
489
573
  const js = `
490
574
  (() => {
@@ -502,11 +586,17 @@ async function cmdClick(selector) {
502
586
  });
503
587
 
504
588
  if (result.result?.value?.error) {
589
+ if (jsonOutput) emitJson(false, null, result.result.value.error);
505
590
  log.fail(result.result.value.error);
506
591
  process.exit(1);
507
592
  }
508
- log.ok(`Clicked: ${selector}`);
593
+ if (jsonOutput) {
594
+ emitJson(true, { selector, clicked: true });
595
+ } else {
596
+ log.ok(`Clicked: ${selector}`);
597
+ }
509
598
  } catch (e) {
599
+ if (jsonOutput) emitJson(false, null, e.message);
510
600
  log.fail(`Click failed: ${e.message}`);
511
601
  process.exit(1);
512
602
  }
@@ -678,7 +768,7 @@ async function cmdConnect() {
678
768
  await new Promise(r => setTimeout(r, 1000));
679
769
  }
680
770
 
681
- // 2. Ensure browser is running (see getBrowserConfig + docs/BROWSERS.md)
771
+ // 2. Ensure browser is running (see getBrowserConfig + README.md#Browsers)
682
772
  const browser = getBrowserConfig();
683
773
  try {
684
774
  execSync(`pgrep -x "${browser.processName}"`, { stdio: 'ignore' });
@@ -700,7 +790,7 @@ async function cmdConnect() {
700
790
 
701
791
  if (!await checkExtension()) {
702
792
  log.fail('Extension not connected to relay');
703
- log.info(`Make sure Glider extension is installed in ${browser.name} (Chromium-based only; see glider docs/BROWSERS.md)`);
793
+ log.info(`Make sure Glider extension is installed in ${browser.name} (Chromium-based only; see glider README.md#Browsers)`);
704
794
  process.exit(1);
705
795
  }
706
796
  log.ok('Extension connected');
@@ -1055,7 +1145,6 @@ async function cmdTitle() {
1055
1145
  }
1056
1146
 
1057
1147
  async function cmdUrl() {
1058
- // Auto-connect if not connected
1059
1148
  if (!await ensureConnected()) {
1060
1149
  process.exit(1);
1061
1150
  }
@@ -1065,13 +1154,106 @@ async function cmdUrl() {
1065
1154
  method: 'Runtime.evaluate',
1066
1155
  params: { expression: 'window.location.href', returnByValue: true }
1067
1156
  });
1068
- console.log(result.result?.value || '');
1157
+ const url = result.result?.value || '';
1158
+ if (jsonOutput) emitJson(true, { url });
1159
+ else console.log(url);
1069
1160
  } catch (e) {
1161
+ if (jsonOutput) emitJson(false, null, e.message);
1070
1162
  log.fail(`URL extraction failed: ${e.message}`);
1071
1163
  process.exit(1);
1072
1164
  }
1073
1165
  }
1074
1166
 
1167
+ async function cmdSnapshot(opts = []) {
1168
+ let interactiveOnly = false;
1169
+ for (const o of opts) {
1170
+ if (o === '--interactive-only' || o === '-i') interactiveOnly = true;
1171
+ }
1172
+ if (!await ensureConnected()) process.exit(1);
1173
+ try {
1174
+ const result = await httpPost('/cdp', {
1175
+ method: 'Runtime.evaluate',
1176
+ params: {
1177
+ expression: buildSnapshotExpression(interactiveOnly),
1178
+ returnByValue: true,
1179
+ awaitPromise: true,
1180
+ },
1181
+ });
1182
+ const data = result?.result?.value;
1183
+ if (!data || data.error) {
1184
+ const err = data?.error || 'snapshot failed';
1185
+ if (jsonOutput) emitJson(false, null, err);
1186
+ log.fail(err);
1187
+ process.exit(1);
1188
+ }
1189
+ if (jsonOutput) {
1190
+ emitJson(true, data);
1191
+ } else {
1192
+ console.log(formatSnapshotText(data));
1193
+ }
1194
+ } catch (e) {
1195
+ if (jsonOutput) emitJson(false, null, e.message);
1196
+ log.fail(`Snapshot failed: ${e.message}`);
1197
+ process.exit(1);
1198
+ }
1199
+ }
1200
+
1201
+ async function cmdTargets() {
1202
+ const raw = await httpGet('/targets');
1203
+ const targets = (Array.isArray(raw) ? raw : []).map((t) => ({
1204
+ sessionId: t.sessionId,
1205
+ targetId: t.targetId,
1206
+ title: t.title || t.targetInfo?.title || '',
1207
+ url: t.url || t.targetInfo?.url || '',
1208
+ }));
1209
+ if (jsonOutput) {
1210
+ emitJson(true, { targets, activeSessionId });
1211
+ } else {
1212
+ if (!targets.length) {
1213
+ log.warn('No targets');
1214
+ return;
1215
+ }
1216
+ console.log(`${GREEN}${targets.length}${NC} target(s):\n`);
1217
+ for (const t of targets) {
1218
+ const mark = t.sessionId === activeSessionId ? `${GREEN}*${NC} ` : ' ';
1219
+ console.log(`${mark}${CYAN}${t.sessionId}${NC} ${t.title}`);
1220
+ console.log(` ${DIM}${t.url}${NC}`);
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ async function cmdUseSession(arg, opts = []) {
1226
+ let sessionId = arg;
1227
+ const urlIdx = opts.indexOf('--url');
1228
+ if (urlIdx >= 0 && opts[urlIdx + 1]) {
1229
+ const needle = opts[urlIdx + 1];
1230
+ const raw = await httpGet('/targets');
1231
+ const targets = Array.isArray(raw) ? raw : [];
1232
+ const hit = targets.find((t) => {
1233
+ const u = t.url || t.targetInfo?.url || '';
1234
+ return u.includes(needle);
1235
+ });
1236
+ if (!hit) {
1237
+ const msg = `no target matching --url ${needle}`;
1238
+ if (jsonOutput) emitJson(false, null, msg);
1239
+ log.fail(msg);
1240
+ process.exit(1);
1241
+ }
1242
+ sessionId = hit.sessionId;
1243
+ }
1244
+ if (!sessionId) {
1245
+ log.fail('Usage: glider use-session <sessionId> | glider use-session --url <host-fragment>');
1246
+ process.exit(1);
1247
+ }
1248
+ persistSession(sessionId);
1249
+ if (jsonOutput) {
1250
+ emitJson(true, { sessionId, persisted: SESSION_STORE });
1251
+ } else {
1252
+ log.ok(`Active session: ${sessionId}`);
1253
+ console.log(SESSION_STORE);
1254
+ }
1255
+ }
1256
+
1075
1257
  // Fetch URL using browser session (authenticated)
1076
1258
  async function cmdFetch(url, opts = []) {
1077
1259
  if (!url) {
@@ -1079,7 +1261,15 @@ async function cmdFetch(url, opts = []) {
1079
1261
  process.exit(1);
1080
1262
  }
1081
1263
 
1082
- log.info(`Fetching: ${url}`);
1264
+ try {
1265
+ assertUrlAllowed(url, allowedDomainList, 'fetch');
1266
+ } catch (e) {
1267
+ if (jsonOutput) emitJson(false, null, e.message);
1268
+ log.fail(e.message);
1269
+ process.exit(1);
1270
+ }
1271
+
1272
+ if (!jsonOutput) log.info(`Fetching: ${url}`);
1083
1273
 
1084
1274
  let outputFile = null;
1085
1275
  for (let i = 0; i < opts.length; i++) {
@@ -1107,13 +1297,16 @@ async function cmdFetch(url, opts = []) {
1107
1297
  const data = result?.result?.value;
1108
1298
  const output = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
1109
1299
 
1110
- if (outputFile) {
1300
+ if (jsonOutput) {
1301
+ emitJson(true, { url, data });
1302
+ } else if (outputFile) {
1111
1303
  fs.writeFileSync(outputFile, output);
1112
1304
  log.ok(`Saved to ${outputFile}`);
1113
1305
  } else {
1114
1306
  console.log(output);
1115
1307
  }
1116
1308
  } catch (e) {
1309
+ if (jsonOutput) emitJson(false, null, e.message);
1117
1310
  log.fail(`Fetch failed: ${e.message}`);
1118
1311
  process.exit(1);
1119
1312
  }
@@ -1422,7 +1615,7 @@ async function cmdRegistry(patternName, opts = []) {
1422
1615
  // Explore site (clicks around, captures network)
1423
1616
  async function cmdExplore(url, opts = []) {
1424
1617
  if (!url) {
1425
- log.fail('Usage: glider explore <url> [--depth N] [--output dir] [--har file]');
1618
+ log.fail('Usage: glider explore <url> [--depth N] [--output dir] [--har file] [--session-id id]');
1426
1619
  process.exit(1);
1427
1620
  }
1428
1621
 
@@ -1435,7 +1628,16 @@ async function cmdExplore(url, opts = []) {
1435
1628
  if (opts[i] === '--depth' || opts[i] === '-d') depth = parseInt(opts[++i], 10);
1436
1629
  else if (opts[i] === '--output' || opts[i] === '-o') outputDir = opts[++i];
1437
1630
  else if (opts[i] === '--har') harFile = opts[++i];
1438
- else if (opts[i] === '--session-id') sessionId = opts[++i];
1631
+ else if (opts[i] === '--session-id' || opts[i] === '--session') sessionId = opts[++i];
1632
+ }
1633
+ if (!sessionId && activeSessionId) sessionId = activeSessionId;
1634
+
1635
+ try {
1636
+ assertUrlAllowed(url, allowedDomainList, 'explore');
1637
+ } catch (e) {
1638
+ if (jsonOutput) emitJson(false, null, e.message);
1639
+ log.fail(e.message);
1640
+ process.exit(1);
1439
1641
  }
1440
1642
 
1441
1643
  log.info(`Exploring: ${url} (depth: ${depth})`);
@@ -1739,7 +1941,8 @@ function showHelp() {
1739
1941
  showBanner();
1740
1942
  console.log(`
1741
1943
  ${B5}USAGE${NC}
1742
- glider <command> [args]
1944
+ glider [--session <id>] [--json] [--allowed-domains 'host,*'] <command> [args]
1945
+ ${DIM}GLIDER_SESSION_ID=session-N${NC} ${DIM}pin tab for all /cdp commands${NC}
1743
1946
 
1744
1947
  ${B5}SETUP${NC}
1745
1948
  ${BW}install${NC} Install daemon ${DIM}(runs at login, auto-restarts)${NC}
@@ -1761,6 +1964,7 @@ ${B5}NAVIGATION${NC}
1761
1964
  ${BW}screenshot${NC} [path] Take screenshot
1762
1965
 
1763
1966
  ${B5}PAGE INFO${NC}
1967
+ ${BW}snapshot${NC} [opts] Page index for agents ${DIM}(--json, --interactive-only)${NC}
1764
1968
  ${BW}text${NC} Get page text
1765
1969
  ${BW}html${NC} [selector] Get HTML
1766
1970
  ${BW}title${NC} Get page title
@@ -1776,6 +1980,8 @@ ${B5}MULTI-WINDOW${NC}
1776
1980
  ${BW}window list${NC} List all windows/tabs
1777
1981
 
1778
1982
  ${B5}MULTI-TAB${NC}
1983
+ ${BW}targets${NC} List targets ${DIM}(sessionId, title, url)${NC}
1984
+ ${BW}use-session${NC} <id> Pin session ${DIM}(or --url host-fragment)${NC}
1779
1985
  ${BW}fetch${NC} <url> Fetch URL with browser session ${DIM}(auth)${NC}
1780
1986
  ${BW}spawn${NC} <urls...> Open multiple tabs
1781
1987
  ${BW}extract${NC} [opts] Extract from all tabs
@@ -1923,7 +2129,8 @@ async function cmdUpdate() {
1923
2129
 
1924
2130
  // Main
1925
2131
  async function main() {
1926
- const args = process.argv.slice(2);
2132
+ const args = parseGlobalFlags(process.argv.slice(2));
2133
+ loadPersistedSession();
1927
2134
  const cmd = args[0];
1928
2135
 
1929
2136
  if (!cmd || cmd === '--help' || cmd === '-h') {
@@ -1991,6 +2198,15 @@ async function main() {
1991
2198
  case 'tabs':
1992
2199
  await cmdTabs();
1993
2200
  break;
2201
+ case 'targets':
2202
+ await cmdTargets();
2203
+ break;
2204
+ case 'use-session':
2205
+ await cmdUseSession(args[1], args.slice(2));
2206
+ break;
2207
+ case 'snapshot':
2208
+ await cmdSnapshot(args.slice(1));
2209
+ break;
1994
2210
  case 'window':
1995
2211
  case 'win':
1996
2212
  await cmdWindow(args.slice(1));
package/lib/bexplore.js CHANGED
@@ -10,8 +10,10 @@
10
10
  const WebSocket = require('ws');
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const http = require('http');
13
14
 
14
15
  const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:19988/cdp';
16
+ const RELAY_HTTP = process.env.RELAY_HTTP || 'http://127.0.0.1:19988';
15
17
 
16
18
  class SiteExplorer {
17
19
  constructor(options = {}) {
@@ -24,7 +26,7 @@ class SiteExplorer {
24
26
  // Exploration state
25
27
  this.visited = new Set();
26
28
  this.toVisit = [];
27
- this.depth = options.depth || 3;
29
+ this.depth = Number.isFinite(options.depth) ? options.depth : 3;
28
30
  this.outputDir = options.outputDir || '/tmp/explore';
29
31
  this.harFile = options.harFile;
30
32
  this.wantedSessionId = options.sessionId || null;
@@ -168,17 +170,18 @@ class SiteExplorer {
168
170
  this.eventHandlers.get(event).add(handler);
169
171
  }
170
172
 
171
- async send(method, params = {}) {
173
+ async send(method, params = {}, options = {}) {
172
174
  const id = ++this.messageId;
173
175
  const msg = { id, method, params };
174
176
  if (this.sessionId) msg.sessionId = this.sessionId;
175
177
  this.ws.send(JSON.stringify(msg));
176
-
178
+ const timeoutMs = options.timeoutMs ?? 30000;
179
+
177
180
  return new Promise((resolve, reject) => {
178
181
  const timer = setTimeout(() => {
179
182
  this.pending.delete(id);
180
183
  reject(new Error(`Timeout: ${method}`));
181
- }, 30000);
184
+ }, timeoutMs);
182
185
  this.pending.set(id, {
183
186
  resolve: (r) => { clearTimeout(timer); resolve(r); },
184
187
  reject: (e) => { clearTimeout(timer); reject(e); }
@@ -186,29 +189,92 @@ class SiteExplorer {
186
189
  });
187
190
  }
188
191
 
192
+ async sendWithRetry(method, params = {}, options = {}) {
193
+ const retries = options.retries ?? 3;
194
+ const delayMs = options.delayMs ?? 400;
195
+ let lastErr;
196
+ for (let attempt = 1; attempt <= retries; attempt++) {
197
+ try {
198
+ return await this.send(method, params, options);
199
+ } catch (err) {
200
+ lastErr = err;
201
+ if (attempt < retries) {
202
+ console.error(`[explore] ${method} attempt ${attempt}/${retries} failed: ${err.message}`);
203
+ await new Promise((r) => setTimeout(r, delayMs * attempt));
204
+ }
205
+ }
206
+ }
207
+ throw lastErr;
208
+ }
209
+
210
+ async fetchTargets() {
211
+ return new Promise((resolve, reject) => {
212
+ const url = new URL('/targets', RELAY_HTTP);
213
+ http.get(url, (res) => {
214
+ let body = '';
215
+ res.on('data', (chunk) => { body += chunk; });
216
+ res.on('end', () => {
217
+ try {
218
+ resolve(JSON.parse(body));
219
+ } catch (e) {
220
+ reject(new Error(`Invalid /targets JSON: ${e.message}`));
221
+ }
222
+ });
223
+ }).on('error', reject);
224
+ });
225
+ }
226
+
227
+ async prefetchSessionFromTargets() {
228
+ if (!this.wantedSessionId) return false;
229
+ const targets = await this.fetchTargets();
230
+ if (!Array.isArray(targets)) {
231
+ throw new Error('/targets did not return an array');
232
+ }
233
+ const hit = targets.find((t) => t.sessionId === this.wantedSessionId);
234
+ if (!hit) {
235
+ const available = targets.map((t) => t.sessionId).filter(Boolean).join(', ') || 'none';
236
+ throw new Error(`Session ${this.wantedSessionId} not in /targets (available: ${available})`);
237
+ }
238
+ this.sessionId = this.wantedSessionId;
239
+ console.error(`[explore] Session pinned from /targets: ${this.sessionId} (${hit.url || hit.title || 'tab'})`);
240
+ return true;
241
+ }
242
+
189
243
  async init() {
244
+ if (this.wantedSessionId) {
245
+ await this.prefetchSessionFromTargets();
246
+ }
247
+
190
248
  await this.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
191
- await new Promise((resolve, reject) => {
192
- this.sessionResolved = resolve;
193
- setTimeout(() => {
194
- if (!this.sessionId) reject(new Error('Timeout waiting for tab attachment'));
195
- else resolve();
196
- }, 10000);
197
- });
249
+
250
+ if (!this.sessionId) {
251
+ await new Promise((resolve, reject) => {
252
+ this.sessionResolved = resolve;
253
+ setTimeout(() => {
254
+ if (!this.sessionId) reject(new Error('Timeout waiting for tab attachment'));
255
+ else resolve();
256
+ }, 10000);
257
+ });
258
+ } else if (this.sessionResolved) {
259
+ this.sessionResolved();
260
+ }
261
+
198
262
  if (!this.sessionId) throw new Error('No browser tab connected');
199
-
200
- // Enable ALL the things
201
- await Promise.all([
202
- this.send('Runtime.enable'),
203
- this.send('Page.enable'),
204
- this.send('DOM.enable'),
205
- this.send('CSS.enable'),
206
- this.send('Network.enable'),
207
- this.send('Debugger.enable'),
208
- this.send('Log.enable'),
209
- ]);
210
-
211
- // Preserve log - don't clear on navigation
263
+
264
+ const domainEnables = [
265
+ ['Runtime.enable', {}],
266
+ ['Page.enable', {}],
267
+ ['DOM.enable', {}],
268
+ ['CSS.enable', {}],
269
+ ['Network.enable', {}],
270
+ ['Debugger.enable', {}],
271
+ ['Log.enable', {}],
272
+ ];
273
+ for (const [method, params] of domainEnables) {
274
+ const retries = method === 'CSS.enable' ? 4 : 2;
275
+ await this.sendWithRetry(method, params, { retries, delayMs: 500 });
276
+ }
277
+
212
278
  await this.send('Network.setCacheDisabled', { cacheDisabled: false });
213
279
 
214
280
  console.error('[explore] All CDP domains enabled');
@@ -492,7 +558,9 @@ class SiteExplorer {
492
558
  }
493
559
 
494
560
  async explore(startUrl, options = {}) {
495
- console.error(`[explore] Starting exploration of ${startUrl}`);
561
+ const depth = this.depth;
562
+ this.clicksPerformed = 0;
563
+ console.error(`[explore] Starting exploration of ${startUrl} (depth=${depth})`);
496
564
  fs.mkdirSync(this.outputDir, { recursive: true });
497
565
 
498
566
  // If we want fresh network capture, reload the page
@@ -512,63 +580,70 @@ class SiteExplorer {
512
580
  const elements = await this.discoverElements();
513
581
  console.error(`[explore] Found: ${elements.tabs.length} tabs, ${elements.buttons.length} buttons, ${elements.links.length} links`);
514
582
 
515
- // Click through tabs
516
- for (const tab of elements.tabs.slice(0, 10)) {
517
- if (tab.text) {
518
- console.error(`[explore] Clicking tab: ${tab.text}`);
519
- try {
520
- await this.clickByText(tab.text);
521
- await new Promise(r => setTimeout(r, 1000));
522
- await this.screenshot(`tab-${tab.text.replace(/[^a-z0-9]/gi, '-').slice(0, 20)}`);
523
- await this.discoverElements(); // Re-discover after tab change
524
- } catch (e) {
525
- console.error(`[explore] Tab click failed: ${e.message}`);
583
+ if (depth === 0) {
584
+ console.error('[explore] depth=0 passive — no clicks, scroll, or toggles');
585
+ } else {
586
+ // Click through tabs
587
+ for (const tab of elements.tabs.slice(0, depth)) {
588
+ if (tab.text) {
589
+ console.error(`[explore] Clicking tab: ${tab.text}`);
590
+ try {
591
+ await this.clickByText(tab.text);
592
+ this.clicksPerformed++;
593
+ await new Promise(r => setTimeout(r, 1000));
594
+ await this.screenshot(`tab-${tab.text.replace(/[^a-z0-9]/gi, '-').slice(0, 20)}`);
595
+ await this.discoverElements(); // Re-discover after tab change
596
+ } catch (e) {
597
+ console.error(`[explore] Tab click failed: ${e.message}`);
598
+ }
526
599
  }
527
600
  }
528
- }
529
-
530
- // Click through buttons (non-destructive ones)
531
- const safeButtons = elements.buttons.filter(b => {
532
- const text = (b.text || '').toLowerCase();
533
- return !text.includes('delete') && !text.includes('remove') && !text.includes('submit') && !text.includes('save') && !text.includes('menu');
534
- });
535
-
536
- for (const btn of safeButtons.slice(0, 10)) {
537
- if (btn.text) {
538
- console.error(`[explore] Clicking button: ${btn.text}`);
539
- try {
540
- await this.clickByText(btn.text);
541
- await new Promise(r => setTimeout(r, 500));
542
- } catch (e) {
543
- console.error(`[explore] Button click failed: ${e.message}`);
601
+
602
+ // Click through buttons (non-destructive ones)
603
+ const safeButtons = elements.buttons.filter(b => {
604
+ const text = (b.text || '').toLowerCase();
605
+ return !text.includes('delete') && !text.includes('remove') && !text.includes('submit') && !text.includes('save') && !text.includes('menu');
606
+ });
607
+
608
+ for (const btn of safeButtons.slice(0, depth)) {
609
+ if (btn.text) {
610
+ console.error(`[explore] Clicking button: ${btn.text}`);
611
+ try {
612
+ await this.clickByText(btn.text);
613
+ this.clicksPerformed++;
614
+ await new Promise(r => setTimeout(r, 500));
615
+ } catch (e) {
616
+ console.error(`[explore] Button click failed: ${e.message}`);
617
+ }
544
618
  }
545
619
  }
546
- }
547
-
548
- // Expand dropdowns
549
- for (const dropdown of elements.dropdowns.slice(0, 5)) {
550
- console.error(`[explore] Opening dropdown`);
551
- // Click to open, then click away
552
- }
553
-
554
- // Scroll to trigger lazy loading
555
- console.error('[explore] Scrolling to trigger lazy loading...');
556
- await this.scrollToBottom();
557
- await this.screenshot('scrolled');
558
-
559
- // Check all checkboxes (to see what filters do)
560
- for (const cb of elements.checkboxes.slice(0, 5)) {
561
- if (cb.label) {
562
- console.error(`[explore] Toggling checkbox: ${cb.label}`);
563
- try {
564
- await this.evaluate(`
620
+
621
+ // Expand dropdowns
622
+ for (const dropdown of elements.dropdowns.slice(0, Math.min(depth, 5))) {
623
+ console.error(`[explore] Opening dropdown`);
624
+ // Click to open, then click away
625
+ }
626
+
627
+ // Scroll to trigger lazy loading
628
+ console.error('[explore] Scrolling to trigger lazy loading...');
629
+ await this.scrollToBottom();
630
+ await this.screenshot('scrolled');
631
+
632
+ // Check all checkboxes (to see what filters do)
633
+ for (const cb of elements.checkboxes.slice(0, Math.min(depth, 5))) {
634
+ if (cb.label) {
635
+ console.error(`[explore] Toggling checkbox: ${cb.label}`);
636
+ try {
637
+ await this.evaluate(`
565
638
  const cb = Array.from(document.querySelectorAll('input[type="checkbox"]'))
566
639
  .find(e => e.labels?.[0]?.textContent?.includes(${JSON.stringify(cb.label)}));
567
640
  if (cb) cb.click();
568
641
  `);
569
- await new Promise(r => setTimeout(r, 500));
570
- } catch (e) {
571
- console.error(`[explore] Checkbox toggle failed: ${e.message}`);
642
+ this.clicksPerformed++;
643
+ await new Promise(r => setTimeout(r, 500));
644
+ } catch (e) {
645
+ console.error(`[explore] Checkbox toggle failed: ${e.message}`);
646
+ }
572
647
  }
573
648
  }
574
649
  }
@@ -584,6 +659,10 @@ class SiteExplorer {
584
659
  url: this.siteMap.url,
585
660
  title: this.siteMap.title,
586
661
  timestamp: new Date().toISOString(),
662
+ depth: this.depth,
663
+ clicksPerformed: this.clicksPerformed ?? 0,
664
+ passive: this.depth === 0,
665
+ sessionId: this.sessionId,
587
666
 
588
667
  // Site structure
589
668
  structure: {
@@ -673,14 +752,16 @@ class SiteExplorer {
673
752
  steps.push({ wait: 2 });
674
753
 
675
754
  // Add clicks for key buttons (with selectors)
676
- const actionButtons = (report.raw.buttons || [])
677
- .filter(b => b.selector && b.qa && !b.qa.includes('history') && !b.qa.includes('search'))
678
- .slice(0, 10);
679
-
680
- for (const btn of actionButtons) {
681
- steps.push({ log: `Click: ${btn.text || btn.label}` });
682
- steps.push({ click: btn.selector });
683
- steps.push({ wait: 0.5 });
755
+ if (this.depth > 0) {
756
+ const actionButtons = (report.raw.buttons || [])
757
+ .filter(b => b.selector && b.qa && !b.qa.includes('history') && !b.qa.includes('search'))
758
+ .slice(0, this.depth);
759
+
760
+ for (const btn of actionButtons) {
761
+ steps.push({ log: `Click: ${btn.text || btn.label}` });
762
+ steps.push({ click: btn.selector });
763
+ steps.push({ wait: 0.5 });
764
+ }
684
765
  }
685
766
 
686
767
  // Add screenshot at end
@@ -746,22 +827,27 @@ Usage:
746
827
  node bexplore.js [options]
747
828
 
748
829
  Options:
749
- --depth N Exploration depth (default: 3)
830
+ --depth N Exploration depth (default: 3; 0 = passive, no clicks)
750
831
  --output DIR Output directory (default: /tmp/explore)
751
832
  --har FILE Save HAR file
833
+ --session-id ID CDP session (e.g. session-6 from GET /targets)
752
834
  --help Show this help
753
835
 
836
+ Depth 0 (passive): reload, discover DOM, screenshots, HAR — zero clicks.
837
+ Depth N>0: up to N tab/button/checkbox interactions per category.
838
+
754
839
  The tool will:
755
840
  1. Discover all tabs, buttons, links, forms, tables
756
- 2. Click through tabs to reveal content
757
- 3. Click safe buttons to trigger XHR
758
- 4. Scroll to trigger lazy loading
759
- 5. Toggle checkboxes/filters
841
+ 2. Click through tabs to reveal content (depth > 0 only)
842
+ 3. Click safe buttons to trigger XHR (depth > 0 only)
843
+ 4. Scroll to trigger lazy loading (depth > 0 only)
844
+ 5. Toggle checkboxes/filters (depth > 0 only)
760
845
  6. Capture all network requests
761
846
  7. Save screenshots at each step
762
847
  8. Generate a comprehensive report
763
848
 
764
849
  Examples:
850
+ node bexplore.js --depth 0 --output /tmp/passive --har /tmp/passive.har --session-id session-3
765
851
  node bexplore.js --output /tmp/sage-explore --har /tmp/sage.har
766
852
  node bexplore.js --depth 5 --output ~/explore-results
767
853
  `);
@@ -774,7 +860,7 @@ Examples:
774
860
  let sessionId = null;
775
861
 
776
862
  for (let i = 0; i < args.length; i++) {
777
- if (args[i] === '--depth') depth = parseInt(args[++i]);
863
+ if (args[i] === '--depth') depth = parseInt(args[++i], 10);
778
864
  else if (args[i] === '--output') outputDir = args[++i];
779
865
  else if (args[i] === '--har') harFile = args[++i];
780
866
  else if (args[i] === '--session-id') sessionId = args[++i];
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ function buildSnapshotExpression(interactiveOnly) {
4
+ return `(() => {
5
+ const interactiveOnly = ${interactiveOnly ? 'true' : 'false'};
6
+ const vis = (el) => {
7
+ if (!el) return false;
8
+ const s = getComputedStyle(el);
9
+ if (s.visibility === 'hidden' || s.display === 'none') return false;
10
+ const r = el.getBoundingClientRect();
11
+ return r.width > 0 && r.height > 0;
12
+ };
13
+ const cssPath = (el) => {
14
+ if (el.id) return '#' + CSS.escape(el.id);
15
+ const tid = el.getAttribute('data-testid');
16
+ if (tid) return '[data-testid="' + tid.replace(/"/g, '\\\\"') + '"]';
17
+ const parts = [];
18
+ let cur = el;
19
+ while (cur && cur.nodeType === 1 && cur !== document.body) {
20
+ let part = cur.tagName.toLowerCase();
21
+ const parent = cur.parentElement;
22
+ if (parent) {
23
+ const sibs = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
24
+ if (sibs.length > 1) part += ':nth-of-type(' + (sibs.indexOf(cur) + 1) + ')';
25
+ }
26
+ parts.unshift(part);
27
+ cur = parent;
28
+ if (parts.length >= 4) break;
29
+ }
30
+ return parts.join(' > ');
31
+ };
32
+ const roleOf = (el) => el.getAttribute('role') || ({ A: 'link', BUTTON: 'button', INPUT: 'textbox', SELECT: 'combobox', TEXTAREA: 'textbox' }[el.tagName] || el.tagName.toLowerCase());
33
+ const textOf = (el) => (el.getAttribute('aria-label') || el.textContent || el.value || el.getAttribute('placeholder') || '').trim().replace(/\\s+/g, ' ').slice(0, 120);
34
+ const sel = interactiveOnly
35
+ ? 'a[href],button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[contenteditable="true"]'
36
+ : 'a[href],button,input,select,textarea,[role],[onclick],h1,h2,h3,label,summary';
37
+ const seen = new Set();
38
+ const elements = [];
39
+ let n = 0;
40
+ for (const el of document.querySelectorAll(sel)) {
41
+ if (!vis(el)) continue;
42
+ const selector = cssPath(el);
43
+ if (seen.has(selector)) continue;
44
+ seen.add(selector);
45
+ n += 1;
46
+ elements.push({
47
+ id: n,
48
+ role: roleOf(el),
49
+ text: textOf(el),
50
+ selector,
51
+ aria: el.getAttribute('aria-label') || null,
52
+ tag: el.tagName.toLowerCase(),
53
+ href: el.href || null,
54
+ });
55
+ if (n >= 200) break;
56
+ }
57
+ return { url: location.href, title: document.title, elements };
58
+ })()`;
59
+ }
60
+
61
+ function formatSnapshotText(data) {
62
+ const lines = [`URL: ${data.url}`, `Title: ${data.title}`, '', 'Elements:'];
63
+ for (const el of data.elements || []) {
64
+ lines.push(`[${el.id}]<${el.role}> ${el.text || '(empty)'} ${el.selector}`);
65
+ }
66
+ return lines.join('\n');
67
+ }
68
+
69
+ module.exports = { buildSnapshotExpression, formatSnapshotText };
@@ -1,45 +1,91 @@
1
1
  #!/bin/bash
2
+
2
3
  # Glider daemon - respawns relay forever with log rotation
3
4
 
5
+
6
+
4
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+
5
9
  BSERVE="$SCRIPT_DIR/bserve.js"
10
+
6
11
  LOG_DIR="$HOME/.glider"
12
+
7
13
  LOG_FILE="$LOG_DIR/daemon.log"
14
+
8
15
  PID_FILE="$LOG_DIR/daemon.pid"
16
+
9
17
  MAX_LOG_SIZE=10485760 # 10MB
10
18
 
19
+
20
+
11
21
  mkdir -p "$LOG_DIR"
12
22
 
23
+
24
+
13
25
  # Rotate log if > 10MB
26
+
14
27
  rotate_log() {
28
+
15
29
  if [ -f "$LOG_FILE" ] && [ $(stat -f%z "$LOG_FILE" 2>/dev/null || echo 0) -gt $MAX_LOG_SIZE ]; then
16
- rm -f "$LOG_FILE.3"
30
+
31
+ /bin/unlink "$LOG_FILE.3"
32
+
17
33
  [ -f "$LOG_FILE.2" ] && mv "$LOG_FILE.2" "$LOG_FILE.3"
34
+
18
35
  [ -f "$LOG_FILE.1" ] && mv "$LOG_FILE.1" "$LOG_FILE.2"
36
+
19
37
  mv "$LOG_FILE" "$LOG_FILE.1"
38
+
20
39
  echo "[$(date)] Log rotated" > "$LOG_FILE"
40
+
21
41
  fi
42
+
22
43
  }
23
44
 
45
+
46
+
24
47
  # Kill any existing
48
+
25
49
  if [ -f "$PID_FILE" ]; then
50
+
26
51
  kill $(cat "$PID_FILE") 2>/dev/null
52
+
27
53
  rm "$PID_FILE"
54
+
28
55
  fi
29
56
 
57
+
58
+
30
59
  echo $$ > "$PID_FILE"
31
60
 
61
+
62
+
32
63
  cleanup() {
33
- rm -f "$PID_FILE"
64
+
65
+ /bin/unlink "$PID_FILE"
66
+
34
67
  exit 0
68
+
35
69
  }
70
+
36
71
  trap cleanup SIGTERM SIGINT
37
72
 
73
+
74
+
38
75
  while true; do
76
+
39
77
  rotate_log
78
+
40
79
  echo "[$(date)] Starting relay..." >> "$LOG_FILE"
80
+
41
81
  node "$BSERVE" >> "$LOG_FILE" 2>&1
82
+
42
83
  EXIT_CODE=$?
84
+
43
85
  echo "[$(date)] Relay exited with code $EXIT_CODE, restarting in 2s..." >> "$LOG_FILE"
86
+
44
87
  sleep 2
88
+
45
89
  done
90
+
91
+
package/lib/guard.js ADDED
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const DOMAIN_CONFIG = path.join(os.homedir(), '.glider', 'config', 'allowed-domains.json');
8
+
9
+ function parseDomainList(raw) {
10
+ if (!raw) return [];
11
+ return String(raw)
12
+ .split(',')
13
+ .map((s) => s.trim())
14
+ .filter(Boolean);
15
+ }
16
+
17
+ function loadConfigDomains() {
18
+ if (!fs.existsSync(DOMAIN_CONFIG)) return [];
19
+ try {
20
+ const data = JSON.parse(fs.readFileSync(DOMAIN_CONFIG, 'utf8'));
21
+ return Array.isArray(data) ? data : data.allowed || data.domains || [];
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ function resolveAllowedDomains(cliList) {
28
+ const fromEnv = parseDomainList(process.env.GLIDER_ALLOWED_DOMAINS);
29
+ const fromCli = cliList && cliList.length ? cliList : [];
30
+ const fromFile = loadConfigDomains();
31
+ if (fromCli.length) return fromCli;
32
+ if (fromEnv.length) return fromEnv;
33
+ if (fromFile.length) return fromFile;
34
+ return null;
35
+ }
36
+
37
+ function hostMatchesPattern(host, pattern) {
38
+ const h = (host || '').toLowerCase();
39
+ const p = (pattern || '').toLowerCase();
40
+ if (!h || !p) return false;
41
+ if (p.includes('*')) {
42
+ const re = new RegExp(`^${p.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`);
43
+ return re.test(h);
44
+ }
45
+ return h === p || h.endsWith(`.${p}`);
46
+ }
47
+
48
+ function urlAllowed(url, allowed) {
49
+ if (!allowed || !allowed.length) return true;
50
+ let host;
51
+ try {
52
+ host = new URL(url).hostname;
53
+ } catch {
54
+ return false;
55
+ }
56
+ return allowed.some((p) => hostMatchesPattern(host, p));
57
+ }
58
+
59
+ function assertUrlAllowed(url, allowed, action) {
60
+ if (urlAllowed(url, allowed)) return;
61
+ const host = (() => {
62
+ try { return new URL(url).hostname; } catch { return url; }
63
+ })();
64
+ throw new Error(`blocked by allowed_domains: ${action} on ${host}`);
65
+ }
66
+
67
+ module.exports = {
68
+ parseDomainList,
69
+ resolveAllowedDomains,
70
+ hostMatchesPattern,
71
+ urlAllowed,
72
+ assertUrlAllowed,
73
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glidercli",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Browser automation CLI. Control Chrome from terminal via CDP, run YAML task files, autonomous loops until completion.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/docs/BROWSERS.md DELETED
@@ -1,7 +0,0 @@
1
- # Browser docs moved
2
-
3
- Browser support and configuration now live in the main README:
4
-
5
- - [README `## Browsers`](../README.md#browsers)
6
-
7
- Reason: npmjs displays README prominently; putting browser setup there makes it much easier for users to find.