glidercli 0.3.8 → 0.3.10

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
@@ -58,6 +58,7 @@
58
58
  | | Browser | Config |
59
59
  |:---:|--------|--------|
60
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 |
61
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" }`) |
62
63
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/microsoft/microsoft-edge.webp" width="16" alt=""> | Microsoft Edge | n/a |
63
64
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/brave.webp" width="16" alt=""> | Brave | n/a |
@@ -85,9 +86,13 @@
85
86
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/mullvad-browser.webp" width="16" alt=""> | Mullvad Browser | Gecko- same constraints as Firefox |
86
87
  | <img src="https://raw.githubusercontent.com/vdutts7/squircle/main/webp/icecat.webp" width="16" alt=""> | IceCat | Gecko- same constraints as Firefox |
87
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 |
88
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 |
89
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 |
90
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 |
91
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 |
92
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 |
93
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 |
@@ -111,7 +116,7 @@ Set browser by key from the browsers registry. Run `glider use <key>` to write t
111
116
  }
112
117
  ```
113
118
 
114
- 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
115
120
 
116
121
  **Option B- Explicit name/path:**
117
122
 
@@ -147,9 +152,8 @@ Check app name in Finder/Spotlight, process name: `ps aux | grep -i <name>`
147
152
 
148
153
  A single registry file can define all Chromium browsers, Glider picks one by **key**
149
154
 
150
- **Registry location (first found):**
155
+ **Registry location:**
151
156
 
152
- - `$HOME/.cursor/registry/browsers.json`
153
157
  - `$HOME/.glider/config/browsers-registry.json`
154
158
 
155
159
  **Registry format:**
@@ -190,7 +194,7 @@ Add or edit entries in the registry to match your machine (e.g. custom install p
190
194
  |-------|--------|
191
195
  | Supported | Chromium-based + Chrome Web Store extension (see table above). |
192
196
  | Not supported | See **Future** in this README. |
193
- | 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` |
194
198
  | Switch | `glider use <key>` (e.g. `glider use arc`, `glider use brave`). |
195
199
  | Path | Optional, use when app is not in default location |
196
200
 
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)) {
@@ -49,6 +57,7 @@ if (fs.existsSync(REGISTRY_FILE)) {
49
57
 
50
58
  // Direct CDP module
51
59
  const { DirectCDP, checkChrome } = require(path.join(LIB_DIR, 'cdp-direct.js'));
60
+ const { resolveDomain } = require(path.join(LIB_DIR, 'domain-resolve.js'));
52
61
 
53
62
  // Domain extensions - load from ~/.glider/config/domains.json
54
63
  const DOMAIN_CONFIG_PATHS = [
@@ -197,10 +206,63 @@ function httpGet(urlPath) {
197
206
  });
198
207
  }
199
208
 
209
+ function loadPersistedSession() {
210
+ if (activeSessionId) return;
211
+ if (!fs.existsSync(SESSION_STORE)) return;
212
+ try {
213
+ const data = JSON.parse(fs.readFileSync(SESSION_STORE, 'utf8'));
214
+ if (data.sessionId) activeSessionId = data.sessionId;
215
+ } catch { /* ignore */ }
216
+ }
217
+
218
+ function persistSession(sessionId) {
219
+ activeSessionId = sessionId;
220
+ fs.mkdirSync(path.dirname(SESSION_STORE), { recursive: true });
221
+ fs.writeFileSync(SESSION_STORE, JSON.stringify({ sessionId, updated: new Date().toISOString() }, null, 2));
222
+ }
223
+
224
+ function emitJson(ok, observation, error = null, warnings = []) {
225
+ console.log(JSON.stringify({ ok, observation, error, warnings }, null, 2));
226
+ if (!ok) process.exit(1);
227
+ }
228
+
229
+ async function assertCurrentUrlAllowed(action) {
230
+ if (!allowedDomainList) return;
231
+ const result = await httpPost('/cdp', {
232
+ method: 'Runtime.evaluate',
233
+ params: { expression: 'location.href', returnByValue: true },
234
+ });
235
+ const url = result?.result?.value;
236
+ if (url) assertUrlAllowed(url, allowedDomainList, action);
237
+ }
238
+
239
+ function parseGlobalFlags(argv) {
240
+ const rest = [];
241
+ const cliDomains = [];
242
+ for (let i = 0; i < argv.length; i++) {
243
+ const a = argv[i];
244
+ if ((a === '--session' || a === '--session-id') && argv[i + 1]) {
245
+ activeSessionId = argv[++i];
246
+ } else if (a === '--json') {
247
+ jsonOutput = true;
248
+ } else if (a === '--allowed-domains' && argv[i + 1]) {
249
+ cliDomains.push(...String(argv[++i]).split(',').map((s) => s.trim()).filter(Boolean));
250
+ } else {
251
+ rest.push(a);
252
+ }
253
+ }
254
+ allowedDomainList = resolveAllowedDomains(cliDomains);
255
+ return rest;
256
+ }
257
+
200
258
  function httpPost(urlPath, body) {
259
+ const payload = { ...body };
260
+ if (urlPath === '/cdp' && activeSessionId && payload.sessionId == null) {
261
+ payload.sessionId = activeSessionId;
262
+ }
201
263
  return new Promise((resolve, reject) => {
202
264
  const url = new URL(urlPath, SERVER_URL);
203
- const data = JSON.stringify(body);
265
+ const data = JSON.stringify(payload);
204
266
  const req = http.request(url, {
205
267
  method: 'POST',
206
268
  headers: { 'Content-Type': 'application/json' },
@@ -421,21 +483,33 @@ async function cmdGoto(url) {
421
483
  process.exit(1);
422
484
  }
423
485
 
424
- // Auto-connect if not connected
425
486
  if (!await ensureConnected()) {
426
487
  process.exit(1);
427
488
  }
489
+
490
+ try {
491
+ assertUrlAllowed(url, allowedDomainList, 'goto');
492
+ } catch (e) {
493
+ if (jsonOutput) emitJson(false, null, e.message);
494
+ log.fail(e.message);
495
+ process.exit(1);
496
+ }
428
497
 
429
- log.info(`Navigating to: ${url}`);
498
+ if (!jsonOutput) log.info(`Navigating to: ${url}`);
430
499
 
431
500
  try {
432
501
  const result = await httpPost('/cdp', {
433
502
  method: 'Page.navigate',
434
503
  params: { url }
435
504
  });
436
- console.log(JSON.stringify(result));
437
- log.ok('Navigated');
505
+ if (jsonOutput) {
506
+ emitJson(true, { url, navigate: result });
507
+ } else {
508
+ console.log(JSON.stringify(result));
509
+ log.ok('Navigated');
510
+ }
438
511
  } catch (e) {
512
+ if (jsonOutput) emitJson(false, null, e.message);
439
513
  log.fail(`Navigation failed: ${e.message}`);
440
514
  process.exit(1);
441
515
  }
@@ -462,14 +536,18 @@ async function cmdEval(js) {
462
536
  }
463
537
  });
464
538
 
465
- if (result.result?.value !== undefined) {
466
- console.log(JSON.stringify(result.result.value));
539
+ const value = result.result?.value;
540
+ if (jsonOutput) {
541
+ emitJson(true, value !== undefined ? value : result.result);
542
+ } else if (value !== undefined) {
543
+ console.log(JSON.stringify(value));
467
544
  } else if (result.result?.description) {
468
545
  console.log(result.result.description);
469
546
  } else {
470
547
  console.log(JSON.stringify(result));
471
548
  }
472
549
  } catch (e) {
550
+ if (jsonOutput) emitJson(false, null, e.message);
473
551
  log.fail(`Eval failed: ${e.message}`);
474
552
  process.exit(1);
475
553
  }
@@ -481,10 +559,17 @@ async function cmdClick(selector) {
481
559
  process.exit(1);
482
560
  }
483
561
 
484
- // Auto-connect if not connected
485
562
  if (!await ensureConnected()) {
486
563
  process.exit(1);
487
564
  }
565
+
566
+ try {
567
+ await assertCurrentUrlAllowed('click');
568
+ } catch (e) {
569
+ if (jsonOutput) emitJson(false, null, e.message);
570
+ log.fail(e.message);
571
+ process.exit(1);
572
+ }
488
573
 
489
574
  const js = `
490
575
  (() => {
@@ -502,11 +587,17 @@ async function cmdClick(selector) {
502
587
  });
503
588
 
504
589
  if (result.result?.value?.error) {
590
+ if (jsonOutput) emitJson(false, null, result.result.value.error);
505
591
  log.fail(result.result.value.error);
506
592
  process.exit(1);
507
593
  }
508
- log.ok(`Clicked: ${selector}`);
594
+ if (jsonOutput) {
595
+ emitJson(true, { selector, clicked: true });
596
+ } else {
597
+ log.ok(`Clicked: ${selector}`);
598
+ }
509
599
  } catch (e) {
600
+ if (jsonOutput) emitJson(false, null, e.message);
510
601
  log.fail(`Click failed: ${e.message}`);
511
602
  process.exit(1);
512
603
  }
@@ -979,7 +1070,7 @@ async function cmdWindow(args) {
979
1070
  }
980
1071
 
981
1072
  async function cmdDomains() {
982
- const domainKeys = Object.keys(DOMAINS);
1073
+ const domainKeys = Object.keys(DOMAINS).filter((k) => k !== 'meta');
983
1074
  if (domainKeys.length === 0) {
984
1075
  log.warn('No domains configured');
985
1076
  log.info('Add domains to ~/.glider/config/domains.json');
@@ -988,14 +1079,30 @@ async function cmdDomains() {
988
1079
  console.log(`${GREEN}${domainKeys.length}${NC} domain(s) configured:\n`);
989
1080
  for (const key of domainKeys) {
990
1081
  const d = DOMAINS[key];
991
- const type = d.script ? 'script' : 'url';
992
- const target = d.script || d.url || '';
1082
+ const shortcut = d.shortcut || {};
1083
+ const type = shortcut.type || (d.script ? 'script' : d.url ? 'url' : 'none');
1084
+ const target = shortcut.target || d.script || d.url || '';
993
1085
  console.log(` ${CYAN}${key}${NC} ${DIM}(${type})${NC}`);
1086
+ if (d.host) console.log(` ${DIM}host: ${d.host}${NC}`);
1087
+ if (d.warch) console.log(` ${DIM}warch: ${d.warch}${NC}`);
994
1088
  if (d.description) console.log(` ${d.description}`);
995
- console.log(` ${DIM}${target}${NC}`);
1089
+ if (target) console.log(` ${DIM}${target}${NC}`);
996
1090
  }
997
1091
  }
998
1092
 
1093
+ async function cmdResolve(input, opts = []) {
1094
+ if (!input) {
1095
+ log.fail('Usage: glider resolve <url|host> [--json]');
1096
+ process.exit(1);
1097
+ }
1098
+ const result = resolveDomain(input);
1099
+ if (opts.includes('--json') || jsonOutput) {
1100
+ console.log(JSON.stringify({ ok: true, ...result }));
1101
+ return;
1102
+ }
1103
+ console.log(JSON.stringify(result, null, 2));
1104
+ }
1105
+
999
1106
  async function cmdOpen(url) {
1000
1107
  if (!url) {
1001
1108
  log.fail('Usage: glider open <url>');
@@ -1055,7 +1162,6 @@ async function cmdTitle() {
1055
1162
  }
1056
1163
 
1057
1164
  async function cmdUrl() {
1058
- // Auto-connect if not connected
1059
1165
  if (!await ensureConnected()) {
1060
1166
  process.exit(1);
1061
1167
  }
@@ -1065,13 +1171,106 @@ async function cmdUrl() {
1065
1171
  method: 'Runtime.evaluate',
1066
1172
  params: { expression: 'window.location.href', returnByValue: true }
1067
1173
  });
1068
- console.log(result.result?.value || '');
1174
+ const url = result.result?.value || '';
1175
+ if (jsonOutput) emitJson(true, { url });
1176
+ else console.log(url);
1069
1177
  } catch (e) {
1178
+ if (jsonOutput) emitJson(false, null, e.message);
1070
1179
  log.fail(`URL extraction failed: ${e.message}`);
1071
1180
  process.exit(1);
1072
1181
  }
1073
1182
  }
1074
1183
 
1184
+ async function cmdSnapshot(opts = []) {
1185
+ let interactiveOnly = false;
1186
+ for (const o of opts) {
1187
+ if (o === '--interactive-only' || o === '-i') interactiveOnly = true;
1188
+ }
1189
+ if (!await ensureConnected()) process.exit(1);
1190
+ try {
1191
+ const result = await httpPost('/cdp', {
1192
+ method: 'Runtime.evaluate',
1193
+ params: {
1194
+ expression: buildSnapshotExpression(interactiveOnly),
1195
+ returnByValue: true,
1196
+ awaitPromise: true,
1197
+ },
1198
+ });
1199
+ const data = result?.result?.value;
1200
+ if (!data || data.error) {
1201
+ const err = data?.error || 'snapshot failed';
1202
+ if (jsonOutput) emitJson(false, null, err);
1203
+ log.fail(err);
1204
+ process.exit(1);
1205
+ }
1206
+ if (jsonOutput) {
1207
+ emitJson(true, data);
1208
+ } else {
1209
+ console.log(formatSnapshotText(data));
1210
+ }
1211
+ } catch (e) {
1212
+ if (jsonOutput) emitJson(false, null, e.message);
1213
+ log.fail(`Snapshot failed: ${e.message}`);
1214
+ process.exit(1);
1215
+ }
1216
+ }
1217
+
1218
+ async function cmdTargets() {
1219
+ const raw = await httpGet('/targets');
1220
+ const targets = (Array.isArray(raw) ? raw : []).map((t) => ({
1221
+ sessionId: t.sessionId,
1222
+ targetId: t.targetId,
1223
+ title: t.title || t.targetInfo?.title || '',
1224
+ url: t.url || t.targetInfo?.url || '',
1225
+ }));
1226
+ if (jsonOutput) {
1227
+ emitJson(true, { targets, activeSessionId });
1228
+ } else {
1229
+ if (!targets.length) {
1230
+ log.warn('No targets');
1231
+ return;
1232
+ }
1233
+ console.log(`${GREEN}${targets.length}${NC} target(s):\n`);
1234
+ for (const t of targets) {
1235
+ const mark = t.sessionId === activeSessionId ? `${GREEN}*${NC} ` : ' ';
1236
+ console.log(`${mark}${CYAN}${t.sessionId}${NC} ${t.title}`);
1237
+ console.log(` ${DIM}${t.url}${NC}`);
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ async function cmdUseSession(arg, opts = []) {
1243
+ let sessionId = arg;
1244
+ const urlIdx = opts.indexOf('--url');
1245
+ if (urlIdx >= 0 && opts[urlIdx + 1]) {
1246
+ const needle = opts[urlIdx + 1];
1247
+ const raw = await httpGet('/targets');
1248
+ const targets = Array.isArray(raw) ? raw : [];
1249
+ const hit = targets.find((t) => {
1250
+ const u = t.url || t.targetInfo?.url || '';
1251
+ return u.includes(needle);
1252
+ });
1253
+ if (!hit) {
1254
+ const msg = `no target matching --url ${needle}`;
1255
+ if (jsonOutput) emitJson(false, null, msg);
1256
+ log.fail(msg);
1257
+ process.exit(1);
1258
+ }
1259
+ sessionId = hit.sessionId;
1260
+ }
1261
+ if (!sessionId) {
1262
+ log.fail('Usage: glider use-session <sessionId> | glider use-session --url <host-fragment>');
1263
+ process.exit(1);
1264
+ }
1265
+ persistSession(sessionId);
1266
+ if (jsonOutput) {
1267
+ emitJson(true, { sessionId, persisted: SESSION_STORE });
1268
+ } else {
1269
+ log.ok(`Active session: ${sessionId}`);
1270
+ console.log(SESSION_STORE);
1271
+ }
1272
+ }
1273
+
1075
1274
  // Fetch URL using browser session (authenticated)
1076
1275
  async function cmdFetch(url, opts = []) {
1077
1276
  if (!url) {
@@ -1079,7 +1278,15 @@ async function cmdFetch(url, opts = []) {
1079
1278
  process.exit(1);
1080
1279
  }
1081
1280
 
1082
- log.info(`Fetching: ${url}`);
1281
+ try {
1282
+ assertUrlAllowed(url, allowedDomainList, 'fetch');
1283
+ } catch (e) {
1284
+ if (jsonOutput) emitJson(false, null, e.message);
1285
+ log.fail(e.message);
1286
+ process.exit(1);
1287
+ }
1288
+
1289
+ if (!jsonOutput) log.info(`Fetching: ${url}`);
1083
1290
 
1084
1291
  let outputFile = null;
1085
1292
  for (let i = 0; i < opts.length; i++) {
@@ -1107,13 +1314,16 @@ async function cmdFetch(url, opts = []) {
1107
1314
  const data = result?.result?.value;
1108
1315
  const output = typeof data === 'object' ? JSON.stringify(data, null, 2) : data;
1109
1316
 
1110
- if (outputFile) {
1317
+ if (jsonOutput) {
1318
+ emitJson(true, { url, data });
1319
+ } else if (outputFile) {
1111
1320
  fs.writeFileSync(outputFile, output);
1112
1321
  log.ok(`Saved to ${outputFile}`);
1113
1322
  } else {
1114
1323
  console.log(output);
1115
1324
  }
1116
1325
  } catch (e) {
1326
+ if (jsonOutput) emitJson(false, null, e.message);
1117
1327
  log.fail(`Fetch failed: ${e.message}`);
1118
1328
  process.exit(1);
1119
1329
  }
@@ -1422,7 +1632,7 @@ async function cmdRegistry(patternName, opts = []) {
1422
1632
  // Explore site (clicks around, captures network)
1423
1633
  async function cmdExplore(url, opts = []) {
1424
1634
  if (!url) {
1425
- log.fail('Usage: glider explore <url> [--depth N] [--output dir] [--har file]');
1635
+ log.fail('Usage: glider explore <url> [--depth N] [--output dir] [--har file] [--session-id id]');
1426
1636
  process.exit(1);
1427
1637
  }
1428
1638
 
@@ -1435,7 +1645,16 @@ async function cmdExplore(url, opts = []) {
1435
1645
  if (opts[i] === '--depth' || opts[i] === '-d') depth = parseInt(opts[++i], 10);
1436
1646
  else if (opts[i] === '--output' || opts[i] === '-o') outputDir = opts[++i];
1437
1647
  else if (opts[i] === '--har') harFile = opts[++i];
1438
- else if (opts[i] === '--session-id') sessionId = opts[++i];
1648
+ else if (opts[i] === '--session-id' || opts[i] === '--session') sessionId = opts[++i];
1649
+ }
1650
+ if (!sessionId && activeSessionId) sessionId = activeSessionId;
1651
+
1652
+ try {
1653
+ assertUrlAllowed(url, allowedDomainList, 'explore');
1654
+ } catch (e) {
1655
+ if (jsonOutput) emitJson(false, null, e.message);
1656
+ log.fail(e.message);
1657
+ process.exit(1);
1439
1658
  }
1440
1659
 
1441
1660
  log.info(`Exploring: ${url} (depth: ${depth})`);
@@ -1739,7 +1958,8 @@ function showHelp() {
1739
1958
  showBanner();
1740
1959
  console.log(`
1741
1960
  ${B5}USAGE${NC}
1742
- glider <command> [args]
1961
+ glider [--session <id>] [--json] [--allowed-domains 'host,*'] <command> [args]
1962
+ ${DIM}GLIDER_SESSION_ID=session-N${NC} ${DIM}pin tab for all /cdp commands${NC}
1743
1963
 
1744
1964
  ${B5}SETUP${NC}
1745
1965
  ${BW}install${NC} Install daemon ${DIM}(runs at login, auto-restarts)${NC}
@@ -1752,6 +1972,8 @@ ${B5}STATUS${NC}
1752
1972
  ${BW}browser${NC} Show browser config ${DIM}(name, path, processName, or use key)${NC}
1753
1973
  ${BW}use${NC} <key> Set browser by registry key ${DIM}(e.g. arc, brave, chrome)${NC}
1754
1974
  ${BW}test${NC} Run diagnostics
1975
+ ${BW}domains${NC} List ~/.glider domain shortcuts + warch paths
1976
+ ${BW}resolve${NC} <url> Resolve host to local warch intel ${DIM}(--json)${NC}
1755
1977
 
1756
1978
  ${B5}NAVIGATION${NC}
1757
1979
  ${BW}goto${NC} <url> Navigate to URL
@@ -1761,6 +1983,7 @@ ${B5}NAVIGATION${NC}
1761
1983
  ${BW}screenshot${NC} [path] Take screenshot
1762
1984
 
1763
1985
  ${B5}PAGE INFO${NC}
1986
+ ${BW}snapshot${NC} [opts] Page index for agents ${DIM}(--json, --interactive-only)${NC}
1764
1987
  ${BW}text${NC} Get page text
1765
1988
  ${BW}html${NC} [selector] Get HTML
1766
1989
  ${BW}title${NC} Get page title
@@ -1776,6 +1999,8 @@ ${B5}MULTI-WINDOW${NC}
1776
1999
  ${BW}window list${NC} List all windows/tabs
1777
2000
 
1778
2001
  ${B5}MULTI-TAB${NC}
2002
+ ${BW}targets${NC} List targets ${DIM}(sessionId, title, url)${NC}
2003
+ ${BW}use-session${NC} <id> Pin session ${DIM}(or --url host-fragment)${NC}
1779
2004
  ${BW}fetch${NC} <url> Fetch URL with browser session ${DIM}(auth)${NC}
1780
2005
  ${BW}spawn${NC} <urls...> Open multiple tabs
1781
2006
  ${BW}extract${NC} [opts] Extract from all tabs
@@ -1923,7 +2148,8 @@ async function cmdUpdate() {
1923
2148
 
1924
2149
  // Main
1925
2150
  async function main() {
1926
- const args = process.argv.slice(2);
2151
+ const args = parseGlobalFlags(process.argv.slice(2));
2152
+ loadPersistedSession();
1927
2153
  const cmd = args[0];
1928
2154
 
1929
2155
  if (!cmd || cmd === '--help' || cmd === '-h') {
@@ -1937,7 +2163,7 @@ async function main() {
1937
2163
  }
1938
2164
 
1939
2165
  // Ensure server is running for most commands
1940
- if (!['start', 'stop', 'help', '--help', '-h', 'update', 'version', '-v', '--version'].includes(cmd)) {
2166
+ if (!['start', 'stop', 'help', '--help', '-h', 'update', 'version', '-v', '--version', 'domains', 'resolve'].includes(cmd)) {
1941
2167
  if (!await checkServer()) {
1942
2168
  log.info('Server not running, starting...');
1943
2169
  await cmdStart();
@@ -1991,6 +2217,15 @@ async function main() {
1991
2217
  case 'tabs':
1992
2218
  await cmdTabs();
1993
2219
  break;
2220
+ case 'targets':
2221
+ await cmdTargets();
2222
+ break;
2223
+ case 'use-session':
2224
+ await cmdUseSession(args[1], args.slice(2));
2225
+ break;
2226
+ case 'snapshot':
2227
+ await cmdSnapshot(args.slice(1));
2228
+ break;
1994
2229
  case 'window':
1995
2230
  case 'win':
1996
2231
  await cmdWindow(args.slice(1));
@@ -1998,6 +2233,9 @@ async function main() {
1998
2233
  case 'domains':
1999
2234
  await cmdDomains();
2000
2235
  break;
2236
+ case 'resolve':
2237
+ await cmdResolve(args[1], args.slice(2));
2238
+ break;
2001
2239
  case 'goto':
2002
2240
  case 'navigate':
2003
2241
  await cmdGoto(args[1]);
@@ -2081,9 +2319,11 @@ async function main() {
2081
2319
  // Check if it's a domain command from config
2082
2320
  if (DOMAINS[cmd]) {
2083
2321
  const domain = DOMAINS[cmd];
2084
- if (domain.script) {
2085
- // Execute external script
2086
- const scriptPath = domain.script.replace(/^~/, os.homedir());
2322
+ const shortcut = domain.shortcut || {};
2323
+ const scriptPathRaw = shortcut.type === 'script' ? shortcut.target : domain.script;
2324
+ const urlRaw = shortcut.type === 'url' ? shortcut.target : domain.url;
2325
+ if (scriptPathRaw) {
2326
+ const scriptPath = scriptPathRaw.replace(/^~/, os.homedir()).replace(/\$HOME/g, os.homedir());
2087
2327
  if (fs.existsSync(scriptPath)) {
2088
2328
  const { execSync } = require('child_process');
2089
2329
  try {
@@ -2095,9 +2335,8 @@ async function main() {
2095
2335
  log.fail(`Domain script not found: ${scriptPath}`);
2096
2336
  process.exit(1);
2097
2337
  }
2098
- } else if (domain.url) {
2099
- // Navigate to domain URL
2100
- await cmdGoto(domain.url);
2338
+ } else if (urlRaw) {
2339
+ await cmdGoto(urlRaw);
2101
2340
  }
2102
2341
  break;
2103
2342
  }
package/docs/setup.json CHANGED
@@ -33,6 +33,8 @@
33
33
  },
34
34
  "config_paths": {
35
35
  "domains": "~/.glider/config/domains.json",
36
+ "domains_template": "$CURREGISTRY/glider/domains.template.json",
37
+ "resolve": "glider resolve <url> --json",
36
38
  "browser": "~/.glider/config/browser.json",
37
39
  "daemon_log": "~/.glider/daemon.log"
38
40
  },
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 };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { URL } = require('url');
7
+
8
+ const DOMAIN_CONFIG_PATHS = [
9
+ path.join(os.homedir(), '.glider', 'config', 'domains.json'),
10
+ path.join(os.homedir(), '.glider', 'domains.json'),
11
+ ];
12
+
13
+ function warchRoot() {
14
+ const base = process.env.CURREGISTRY || path.join(os.homedir(), '.cursor', 'registry');
15
+ return path.join(base, 'warch');
16
+ }
17
+
18
+ function loadDomainsIndex() {
19
+ for (const cfgPath of DOMAIN_CONFIG_PATHS) {
20
+ if (!fs.existsSync(cfgPath)) continue;
21
+ try {
22
+ return JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
23
+ } catch (_) { /* ignore */ }
24
+ }
25
+ return {};
26
+ }
27
+
28
+ function hostFromUrl(input) {
29
+ if (!input) return null;
30
+ try {
31
+ const u = input.includes('://') ? input : `https://${input}`;
32
+ return new URL(u).hostname || null;
33
+ } catch (_) {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function resolveDomain(input) {
39
+ const host = hostFromUrl(input);
40
+ const root = warchRoot();
41
+ const warchPath = host ? path.join(root, host) : null;
42
+ const domains = loadDomainsIndex();
43
+ let indexHit = null;
44
+ for (const [key, val] of Object.entries(domains)) {
45
+ if (key === 'meta' || !val || typeof val !== 'object') continue;
46
+ if (val.host === host || key === host) {
47
+ indexHit = key;
48
+ break;
49
+ }
50
+ }
51
+ const gliderJsonPath = warchPath && path.join(warchPath, 'glider.json');
52
+ const gotchasPath = warchPath && path.join(warchPath, 'gotchas.json');
53
+ const gliderJsonExists = !!(gliderJsonPath && fs.existsSync(gliderJsonPath));
54
+ const gotchasExists = !!(gotchasPath && fs.existsSync(gotchasPath));
55
+ let gliderJson = null;
56
+ if (gliderJsonExists) {
57
+ try {
58
+ gliderJson = JSON.parse(fs.readFileSync(gliderJsonPath, 'utf8'));
59
+ } catch (_) { /* ignore */ }
60
+ }
61
+ return {
62
+ host,
63
+ warch_path: warchPath,
64
+ glider_json_exists: gliderJsonExists,
65
+ gotchas_exists: gotchasExists,
66
+ domains_index_hit: indexHit,
67
+ capture_mode: gliderJson?.capture_mode ?? null,
68
+ wait_ms: gliderJson?.wait_ms ?? null,
69
+ };
70
+ }
71
+
72
+ module.exports = { resolveDomain, hostFromUrl, warchRoot, loadDomainsIndex };
@@ -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.8",
3
+ "version": "0.3.10",
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": {