lobster-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -455,6 +455,161 @@ var SNAPSHOT_SCRIPT = `
455
455
  })()
456
456
  `;
457
457
 
458
+ // src/browser/dom/compact-snapshot.ts
459
+ var COMPACT_SNAPSHOT_SCRIPT = `
460
+ (() => {
461
+ const TOKEN_BUDGET = 800;
462
+ const CHARS_PER_TOKEN = 4;
463
+
464
+ const INTERACTIVE_TAGS = new Set([
465
+ 'a','button','input','select','textarea','details','summary','label',
466
+ ]);
467
+ const INTERACTIVE_ROLES = new Set([
468
+ 'button','link','textbox','checkbox','radio','combobox','listbox',
469
+ 'menu','menuitem','tab','switch','slider','searchbox','spinbutton',
470
+ 'option','menuitemcheckbox','menuitemradio','treeitem',
471
+ ]);
472
+ const LANDMARK_TAGS = new Map([
473
+ ['nav', 'Navigation'],
474
+ ['main', 'Main Content'],
475
+ ['header', 'Header'],
476
+ ['footer', 'Footer'],
477
+ ['aside', 'Sidebar'],
478
+ ['form', 'Form'],
479
+ ]);
480
+ const LANDMARK_ROLES = new Map([
481
+ ['navigation', 'Navigation'],
482
+ ['main', 'Main Content'],
483
+ ['banner', 'Header'],
484
+ ['contentinfo', 'Footer'],
485
+ ['complementary', 'Sidebar'],
486
+ ['search', 'Search'],
487
+ ['dialog', 'Dialog'],
488
+ ]);
489
+
490
+ function isVisible(el) {
491
+ if (el.offsetWidth === 0 && el.offsetHeight === 0 && el.tagName !== 'INPUT') return false;
492
+ const s = getComputedStyle(el);
493
+ return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
494
+ }
495
+
496
+ function isInteractive(el) {
497
+ const tag = el.tagName.toLowerCase();
498
+ if (INTERACTIVE_TAGS.has(tag)) {
499
+ if (el.disabled) return false;
500
+ if (tag === 'input' && el.type === 'hidden') return false;
501
+ return true;
502
+ }
503
+ const role = el.getAttribute('role');
504
+ if (role && INTERACTIVE_ROLES.has(role)) return true;
505
+ if (el.contentEditable === 'true') return true;
506
+ if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) return true;
507
+ return false;
508
+ }
509
+
510
+ function getRole(el) {
511
+ const role = el.getAttribute('role');
512
+ if (role) return role;
513
+ const tag = el.tagName.toLowerCase();
514
+ if (tag === 'a') return 'link';
515
+ if (tag === 'button' || tag === 'summary') return 'button';
516
+ if (tag === 'input') return el.type || 'text';
517
+ if (tag === 'select') return 'select';
518
+ if (tag === 'textarea') return 'textarea';
519
+ if (tag === 'label') return 'label';
520
+ return tag;
521
+ }
522
+
523
+ function getName(el) {
524
+ return (
525
+ el.getAttribute('aria-label') ||
526
+ el.getAttribute('alt') ||
527
+ el.getAttribute('title') ||
528
+ el.getAttribute('placeholder') ||
529
+ (el.tagName === 'INPUT' && (el.type === 'submit' || el.type === 'button') ? el.value : '') ||
530
+ (el.id ? document.querySelector('label[for="' + el.id + '"]')?.textContent?.trim() : '') ||
531
+ (el.children.length <= 2 ? el.textContent?.trim() : '') ||
532
+ ''
533
+ ).slice(0, 60);
534
+ }
535
+
536
+ function getValue(el) {
537
+ const tag = el.tagName.toLowerCase();
538
+ if (tag === 'input') {
539
+ const type = el.type || 'text';
540
+ if (type === 'checkbox' || type === 'radio') return el.checked ? 'checked' : 'unchecked';
541
+ if (type === 'password') return el.value ? '****' : '';
542
+ return el.value ? el.value.slice(0, 30) : '';
543
+ }
544
+ if (tag === 'textarea') return el.value ? el.value.slice(0, 30) : '';
545
+ if (tag === 'select' && el.selectedOptions?.length) return el.selectedOptions[0].text.slice(0, 30);
546
+ return '';
547
+ }
548
+
549
+ // Collect elements
550
+ let idx = 0;
551
+ let charsUsed = 0;
552
+ const lines = [];
553
+ let lastLandmark = '';
554
+
555
+ // Page header
556
+ const scrollY = window.scrollY;
557
+ const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
558
+ const scrollPct = scrollMax > 0 ? Math.round((scrollY / scrollMax) * 100) : 0;
559
+ const header = 'url: ' + location.href + ' | scroll: ' + scrollPct + '%';
560
+ lines.push(header);
561
+ charsUsed += header.length;
562
+
563
+ // Walk DOM
564
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
565
+ let node;
566
+ while ((node = walker.nextNode())) {
567
+ if (!isVisible(node)) continue;
568
+
569
+ const tag = node.tagName.toLowerCase();
570
+ if (['script','style','noscript','svg','path','meta','link','head','template'].includes(tag)) continue;
571
+
572
+ // Check for landmark
573
+ const role = node.getAttribute('role');
574
+ const landmark = LANDMARK_TAGS.get(tag) || (role ? LANDMARK_ROLES.get(role) : null);
575
+ if (landmark && landmark !== lastLandmark) {
576
+ const sectionLine = '--- ' + landmark + ' ---';
577
+ if (charsUsed + sectionLine.length > TOKEN_BUDGET * CHARS_PER_TOKEN) break;
578
+ lines.push(sectionLine);
579
+ charsUsed += sectionLine.length;
580
+ lastLandmark = landmark;
581
+ }
582
+
583
+ // Only emit interactive elements
584
+ if (!isInteractive(node)) continue;
585
+
586
+ const elRole = getRole(node);
587
+ const name = getName(node);
588
+ const value = getValue(node);
589
+
590
+ // Build compact line
591
+ let line = '[' + idx + '] ' + elRole;
592
+ if (name) line += ' "' + name.replace(/"/g, "'") + '"';
593
+ if (value) line += ' val="' + value.replace(/"/g, "'") + '"';
594
+
595
+ // Check token budget
596
+ if (charsUsed + line.length > TOKEN_BUDGET * CHARS_PER_TOKEN) {
597
+ lines.push('... (' + (document.querySelectorAll('a,button,input,select,textarea,[role]').length - idx) + ' more elements)');
598
+ break;
599
+ }
600
+
601
+ // Annotate element with ref for clicking
602
+ try { node.dataset.ref = String(idx); } catch {}
603
+
604
+ lines.push(line);
605
+ charsUsed += line.length;
606
+ idx++;
607
+ }
608
+
609
+ return lines.join('\\n');
610
+ })()
611
+ `;
612
+
458
613
  // src/browser/dom/semantic-tree.ts
459
614
  var SEMANTIC_TREE_SCRIPT = `
460
615
  (() => {
@@ -980,6 +1135,64 @@ var FORM_STATE_SCRIPT = `
980
1135
  })()
981
1136
  `;
982
1137
 
1138
+ // src/browser/dom/interactive.ts
1139
+ var INTERACTIVE_ELEMENTS_SCRIPT = `
1140
+ (() => {
1141
+ const results = [];
1142
+
1143
+ function classify(el) {
1144
+ const tag = el.tagName.toLowerCase();
1145
+ const role = el.getAttribute('role');
1146
+ const types = [];
1147
+
1148
+ // Native interactive
1149
+ if (['a', 'button', 'input', 'select', 'textarea', 'details', 'summary'].includes(tag)) {
1150
+ types.push('native');
1151
+ }
1152
+
1153
+ // ARIA role interactive
1154
+ if (role && ['button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'tab', 'switch', 'menuitem', 'slider'].includes(role)) {
1155
+ types.push('aria');
1156
+ }
1157
+
1158
+ // Contenteditable
1159
+ if (el.contentEditable === 'true') types.push('contenteditable');
1160
+
1161
+ // Focusable
1162
+ if (el.tabIndex >= 0 && el.getAttribute('tabindex') !== null) types.push('focusable');
1163
+
1164
+ // Has click listener (approximate)
1165
+ if (el.onclick) types.push('listener');
1166
+
1167
+ return types;
1168
+ }
1169
+
1170
+ let idx = 0;
1171
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
1172
+ let node;
1173
+ while (node = walker.nextNode()) {
1174
+ const types = classify(node);
1175
+ if (types.length === 0) continue;
1176
+
1177
+ const style = getComputedStyle(node);
1178
+ if (style.display === 'none' || style.visibility === 'hidden') continue;
1179
+
1180
+ const rect = node.getBoundingClientRect();
1181
+ results.push({
1182
+ index: idx++,
1183
+ tag: node.tagName.toLowerCase(),
1184
+ role: node.getAttribute('role') || '',
1185
+ text: (node.textContent || '').trim().slice(0, 100),
1186
+ types,
1187
+ ariaLabel: node.getAttribute('aria-label') || '',
1188
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1189
+ });
1190
+ }
1191
+
1192
+ return results;
1193
+ })()
1194
+ `;
1195
+
983
1196
  // src/browser/interceptor.ts
984
1197
  function buildInterceptorScript(pattern) {
985
1198
  return `
@@ -1036,6 +1249,155 @@ var GET_INTERCEPTED_SCRIPT = `
1036
1249
  })()
1037
1250
  `;
1038
1251
 
1252
+ // src/browser/semantic-find.ts
1253
+ var SYNONYMS = {
1254
+ btn: ["button"],
1255
+ button: ["btn", "submit", "click"],
1256
+ submit: ["go", "send", "ok", "confirm", "done", "button"],
1257
+ search: ["find", "lookup", "query", "filter"],
1258
+ login: ["signin", "sign-in", "log-in", "authenticate"],
1259
+ signup: ["register", "create-account", "sign-up", "join"],
1260
+ logout: ["signout", "sign-out", "log-out"],
1261
+ close: ["dismiss", "x", "cancel", "exit"],
1262
+ menu: ["nav", "navigation", "hamburger", "sidebar"],
1263
+ nav: ["navigation", "menu", "navbar"],
1264
+ input: ["field", "textbox", "text", "entry"],
1265
+ email: ["mail", "e-mail"],
1266
+ password: ["pass", "pwd", "secret"],
1267
+ next: ["continue", "forward", "proceed"],
1268
+ back: ["previous", "return", "go-back"],
1269
+ save: ["store", "keep", "persist"],
1270
+ delete: ["remove", "trash", "discard", "destroy"],
1271
+ edit: ["modify", "change", "update"],
1272
+ add: ["create", "new", "plus", "insert"],
1273
+ settings: ["preferences", "config", "options", "gear"],
1274
+ profile: ["account", "user", "avatar"],
1275
+ home: ["main", "dashboard", "start"],
1276
+ link: ["anchor", "href", "url"],
1277
+ select: ["dropdown", "combo", "picker", "choose"],
1278
+ checkbox: ["check", "toggle", "tick"],
1279
+ upload: ["attach", "file", "browse"],
1280
+ download: ["save", "export"]
1281
+ };
1282
+ var ROLE_KEYWORDS = /* @__PURE__ */ new Set([
1283
+ "button",
1284
+ "link",
1285
+ "input",
1286
+ "textbox",
1287
+ "checkbox",
1288
+ "radio",
1289
+ "select",
1290
+ "dropdown",
1291
+ "tab",
1292
+ "menu",
1293
+ "menuitem",
1294
+ "switch",
1295
+ "slider",
1296
+ "combobox",
1297
+ "searchbox",
1298
+ "option"
1299
+ ]);
1300
+ function tokenize(text) {
1301
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/[\s-]+/).filter((t) => t.length > 0);
1302
+ }
1303
+ function expandSynonyms(tokens) {
1304
+ const expanded = new Set(tokens);
1305
+ for (const token of tokens) {
1306
+ const syns = SYNONYMS[token];
1307
+ if (syns) {
1308
+ for (const syn of syns) expanded.add(syn);
1309
+ }
1310
+ }
1311
+ return expanded;
1312
+ }
1313
+ function freqMap(tokens) {
1314
+ const map = /* @__PURE__ */ new Map();
1315
+ for (const t of tokens) {
1316
+ map.set(t, (map.get(t) || 0) + 1);
1317
+ }
1318
+ return map;
1319
+ }
1320
+ function jaccardScore(queryTokens, descTokens) {
1321
+ const qFreq = freqMap(queryTokens);
1322
+ const dFreq = freqMap(descTokens);
1323
+ let intersection = 0;
1324
+ let union = 0;
1325
+ const allTokens = /* @__PURE__ */ new Set([...qFreq.keys(), ...dFreq.keys()]);
1326
+ for (const token of allTokens) {
1327
+ const qCount = qFreq.get(token) || 0;
1328
+ const dCount = dFreq.get(token) || 0;
1329
+ intersection += Math.min(qCount, dCount);
1330
+ union += Math.max(qCount, dCount);
1331
+ }
1332
+ return union === 0 ? 0 : intersection / union;
1333
+ }
1334
+ function prefixScore(queryTokens, descTokens) {
1335
+ if (queryTokens.length === 0 || descTokens.length === 0) return 0;
1336
+ let matches = 0;
1337
+ for (const qt of queryTokens) {
1338
+ if (qt.length < 3) continue;
1339
+ for (const dt of descTokens) {
1340
+ if (dt.startsWith(qt) || qt.startsWith(dt)) {
1341
+ matches += 0.5;
1342
+ break;
1343
+ }
1344
+ }
1345
+ }
1346
+ return Math.min(matches / queryTokens.length, 0.3);
1347
+ }
1348
+ function roleBoost(queryTokens, elementRole) {
1349
+ const roleLower = elementRole.toLowerCase();
1350
+ for (const qt of queryTokens) {
1351
+ if (ROLE_KEYWORDS.has(qt) && roleLower.includes(qt)) {
1352
+ return 0.2;
1353
+ }
1354
+ }
1355
+ return 0;
1356
+ }
1357
+ function scoreElement(queryTokens, queryExpanded, element) {
1358
+ const descParts = [
1359
+ element.text,
1360
+ element.role,
1361
+ element.tag,
1362
+ element.ariaLabel
1363
+ ].filter(Boolean);
1364
+ const descText = descParts.join(" ");
1365
+ const descTokens = tokenize(descText);
1366
+ if (descTokens.length === 0) return 0;
1367
+ const descExpanded = expandSynonyms(descTokens);
1368
+ const expandedQueryTokens = [...queryExpanded];
1369
+ const expandedDescTokens = [...descExpanded];
1370
+ const jaccard = jaccardScore(expandedQueryTokens, expandedDescTokens);
1371
+ const prefix = prefixScore(queryTokens, descTokens);
1372
+ const role = roleBoost(queryTokens, element.role || element.tag);
1373
+ const queryStr = queryTokens.join(" ");
1374
+ const descStr = descTokens.join(" ");
1375
+ const exactBonus = descStr.includes(queryStr) ? 0.3 : 0;
1376
+ return Math.min(jaccard + prefix + role + exactBonus, 1);
1377
+ }
1378
+ function semanticFind(elements, query, options) {
1379
+ const maxResults = options?.maxResults ?? 5;
1380
+ const minScore = options?.minScore ?? 0.3;
1381
+ const queryTokens = tokenize(query);
1382
+ if (queryTokens.length === 0) return [];
1383
+ const queryExpanded = expandSynonyms(queryTokens);
1384
+ const scored = [];
1385
+ for (const el of elements) {
1386
+ const score = scoreElement(queryTokens, queryExpanded, el);
1387
+ if (score >= minScore) {
1388
+ scored.push({
1389
+ ref: el.index,
1390
+ score: Math.round(score * 100) / 100,
1391
+ text: (el.text || el.ariaLabel || "").slice(0, 60),
1392
+ role: el.role || el.tag,
1393
+ tag: el.tag
1394
+ });
1395
+ }
1396
+ }
1397
+ scored.sort((a, b) => b.score - a.score);
1398
+ return scored.slice(0, maxResults);
1399
+ }
1400
+
1039
1401
  // src/browser/page-adapter.ts
1040
1402
  var PuppeteerPage = class {
1041
1403
  page;
@@ -1063,7 +1425,10 @@ var PuppeteerPage = class {
1063
1425
  async evaluate(js) {
1064
1426
  return this.page.evaluate(js);
1065
1427
  }
1066
- async snapshot(_opts) {
1428
+ async snapshot(opts) {
1429
+ if (opts?.compact) {
1430
+ return this.page.evaluate(COMPACT_SNAPSHOT_SCRIPT);
1431
+ }
1067
1432
  return this.page.evaluate(SNAPSHOT_SCRIPT);
1068
1433
  }
1069
1434
  async semanticTree(_opts) {
@@ -1335,6 +1700,10 @@ var PuppeteerPage = class {
1335
1700
  active: p === this.page
1336
1701
  }));
1337
1702
  }
1703
+ async find(query, options) {
1704
+ const elements = await this.page.evaluate(INTERACTIVE_ELEMENTS_SCRIPT);
1705
+ return semanticFind(elements, query, options);
1706
+ }
1338
1707
  async close() {
1339
1708
  await this.page.close();
1340
1709
  }