@nuanu-ai/agentbrowse 0.2.29 → 0.2.30

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
@@ -165,6 +165,9 @@ overrides.
165
165
  Human-readable progress is written to `stderr`. Command results are written to
166
166
  `stdout`.
167
167
 
168
+ When `launch` detects a newer npm release, it prints the reminder to `stderr`
169
+ without changing the JSON result written to `stdout`.
170
+
168
171
  `observe()` and `extract()` responses expose where runtime work came from:
169
172
 
170
173
  - top-level `source` shows the winning execution source, for example `dom` or `stagehand`
@@ -20,6 +20,9 @@ type ObserveInventoryTarget = {
20
20
  framePath?: string[];
21
21
  frameUrl?: string;
22
22
  formSelector?: string;
23
+ pageSignature?: string;
24
+ goalInventoryType?: 'target' | 'scope';
25
+ goalSurfaceId?: string;
23
26
  };
24
27
  export declare function rerankDomTargetsForGoal<T extends ObserveInventoryTarget>(instruction: string, targets: ReadonlyArray<T>): Promise<T[]>;
25
28
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-observe.d.ts","sourceRoot":"","sources":["../../src/commands/semantic-observe.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,EAChB,MAAM,qBAAqB,CAAC;AAE7B,KAAK,sBAAsB,GAAG;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;IACnD,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACvC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AA6UF,wBAAsB,uBAAuB,CAAC,CAAC,SAAS,sBAAsB,EAC5E,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,EAAE,CAAC,CAkDd"}
1
+ {"version":3,"file":"semantic-observe.d.ts","sourceRoot":"","sources":["../../src/commands/semantic-observe.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,mBAAmB,EACnB,eAAe,EAChB,MAAM,qBAAqB,CAAC;AAE7B,KAAK,sBAAsB,GAAG;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;IACnD,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,cAAc,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACvC,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IACvC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AA+3BF,wBAAsB,uBAAuB,CAAC,CAAC,SAAS,sBAAsB,EAC5E,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,EAAE,CAAC,CA6Dd"}
@@ -9,6 +9,7 @@ const rerankSchema = z.object({
9
9
  .max(8),
10
10
  });
11
11
  const RERANK_CANDIDATE_LIMIT = 120;
12
+ const GOAL_RETRIEVAL_ENTITY_LIMIT = 64;
12
13
  const FORM_BUCKET_RESERVE_LIMIT = 48;
13
14
  const FORM_BUCKET_RESERVE_PER_BUCKET = 8;
14
15
  const SCOPE_BUCKET_RESERVE_LIMIT = 24;
@@ -27,12 +28,60 @@ const HIGH_SIGNAL_SCOPE_KINDS = new Set([
27
28
  'card',
28
29
  'form',
29
30
  ]);
31
+ const GOAL_TEXT_STOPWORDS = new Set([
32
+ 'a',
33
+ 'an',
34
+ 'and',
35
+ 'at',
36
+ 'for',
37
+ 'from',
38
+ 'in',
39
+ 'of',
40
+ 'on',
41
+ 'or',
42
+ 'the',
43
+ 'to',
44
+ 'with',
45
+ 'в',
46
+ 'для',
47
+ 'и',
48
+ 'или',
49
+ 'на',
50
+ 'найти',
51
+ ]);
30
52
  function isFieldLikeTarget(target) {
31
53
  const kind = (target.kind ?? '').trim().toLowerCase();
32
54
  const role = (target.role ?? '').trim().toLowerCase();
33
55
  return (['input', 'textarea', 'select', 'combobox'].includes(kind) ||
34
56
  ['textbox', 'combobox'].includes(role));
35
57
  }
58
+ function isScopeLikeCandidate(target) {
59
+ return ((target.goalInventoryType ?? '').trim().toLowerCase() === 'scope' ||
60
+ (target.capability ?? '').trim().toLowerCase() === 'scope');
61
+ }
62
+ function isActionLikeTargetCandidate(target) {
63
+ if (isScopeLikeCandidate(target) || isFieldLikeTarget(target)) {
64
+ return false;
65
+ }
66
+ const kind = (target.kind ?? '').trim().toLowerCase();
67
+ const role = (target.role ?? '').trim().toLowerCase();
68
+ return (target.allowedActions?.includes('click') ||
69
+ target.allowedActions?.includes('press') ||
70
+ kind === 'button' ||
71
+ role === 'button' ||
72
+ kind === 'link' ||
73
+ role === 'link');
74
+ }
75
+ function normalizeRetrievalText(value) {
76
+ const normalized = value?.replace(/\s+/g, ' ').trim().toLowerCase();
77
+ return normalized ? normalized : undefined;
78
+ }
79
+ function tokenizeRetrievalText(value) {
80
+ if (!value) {
81
+ return [];
82
+ }
83
+ return (value.match(/[\p{L}\p{N}]+/gu) ?? []).map((token) => token.toLowerCase());
84
+ }
36
85
  function semanticFormContextKey(context) {
37
86
  for (const node of [context?.landmark, context?.container, context?.group, context?.item]) {
38
87
  const kind = (node?.kind ?? '').trim().toLowerCase();
@@ -58,6 +107,17 @@ function candidateBucketKey(target, options = {}) {
58
107
  const surfaceKey = (options.preferFormBucket ? formKey : undefined) ?? target.surfaceRef ?? formKey ?? 'page-root';
59
108
  return `${frameKey}|${surfaceKey}`;
60
109
  }
110
+ function surfaceIdentityOf(target) {
111
+ const explicitSurfaceId = target.goalSurfaceId?.trim();
112
+ if (explicitSurfaceId) {
113
+ return explicitSurfaceId;
114
+ }
115
+ const surfaceRef = target.surfaceRef?.trim();
116
+ if (!surfaceRef) {
117
+ return undefined;
118
+ }
119
+ return surfaceRef.startsWith('scope:') ? surfaceRef.slice('scope:'.length) : surfaceRef;
120
+ }
61
121
  function isPrimaryFormControlTarget(target) {
62
122
  const kind = (target.kind ?? '').trim().toLowerCase();
63
123
  const role = (target.role ?? '').trim().toLowerCase();
@@ -102,6 +162,31 @@ function isHighSignalScopeCandidate(target) {
102
162
  const kind = (target.kind ?? '').trim().toLowerCase();
103
163
  return HIGH_SIGNAL_SCOPE_KINDS.has(kind);
104
164
  }
165
+ function contentItemBucketKey(target) {
166
+ if (isScopeLikeCandidate(target) ||
167
+ !isActionLikeTargetCandidate(target) ||
168
+ Boolean(formBucketKey(target)) ||
169
+ isPrimaryFormControlTarget(target)) {
170
+ return undefined;
171
+ }
172
+ const surfaceKind = (target.surfaceKind ?? '').trim().toLowerCase();
173
+ if (['form', 'dialog', 'listbox', 'menu', 'popover', 'dropdown', 'datepicker'].includes(surfaceKind)) {
174
+ return undefined;
175
+ }
176
+ const surfaceIdentity = surfaceIdentityOf(target);
177
+ if (surfaceIdentity) {
178
+ return `surface:${surfaceIdentity}`;
179
+ }
180
+ const itemKey = normalizeRetrievalText(target.context?.item?.label ?? target.context?.item?.text);
181
+ if (itemKey) {
182
+ return `item:${itemKey}`;
183
+ }
184
+ const containerKey = normalizeRetrievalText(target.context?.container?.label ?? target.context?.container?.text);
185
+ if (containerKey) {
186
+ return `container:${containerKey}`;
187
+ }
188
+ return undefined;
189
+ }
105
190
  function scopeCandidatePriority(target) {
106
191
  const kind = (target.kind ?? '').trim().toLowerCase();
107
192
  if (kind === 'dialog' || kind === 'listbox' || kind === 'menu') {
@@ -118,6 +203,343 @@ function scopeCandidatePriority(target) {
118
203
  }
119
204
  return 4;
120
205
  }
206
+ function representativeCandidateScore(target) {
207
+ const normalizedLabel = normalizeRetrievalText(target.label);
208
+ const kind = (target.kind ?? '').trim().toLowerCase();
209
+ const role = (target.role ?? '').trim().toLowerCase();
210
+ let score = (target.surfacePriority ?? 0) * 10;
211
+ if (isPrimaryFormControlTarget(target)) {
212
+ score += 1_500 - primaryFormTargetPriority(target) * 100;
213
+ }
214
+ else if (isFieldLikeTarget(target)) {
215
+ score += 1_250;
216
+ }
217
+ else if (isActionLikeTargetCandidate(target)) {
218
+ score += 1_000;
219
+ }
220
+ else if (isScopeLikeCandidate(target)) {
221
+ score += 700;
222
+ }
223
+ if (target.allowedActions?.includes('fill') || target.allowedActions?.includes('select')) {
224
+ score += 120;
225
+ }
226
+ if (target.allowedActions?.includes('click') || target.allowedActions?.includes('press')) {
227
+ score += 80;
228
+ }
229
+ if (target.acceptancePolicy === 'submit') {
230
+ score += 120;
231
+ }
232
+ if (target.acceptancePolicy === 'navigation') {
233
+ score += 60;
234
+ }
235
+ if (kind === 'link' || role === 'link') {
236
+ score += 40;
237
+ }
238
+ if (normalizedLabel) {
239
+ score += Math.min(normalizedLabel.length, 100);
240
+ if (normalizedLabel === 'button' || normalizedLabel === 'link') {
241
+ score -= 300;
242
+ }
243
+ if (normalizedLabel.includes('opens in new window')) {
244
+ score -= 120;
245
+ }
246
+ if (normalizedLabel.includes('save this item')) {
247
+ score -= 80;
248
+ }
249
+ }
250
+ return score;
251
+ }
252
+ function entityMemberPriority(entityKind, target) {
253
+ if (entityKind === 'form' && isPrimaryFormControlTarget(target)) {
254
+ return 5_000 - primaryFormTargetPriority(target) * 100;
255
+ }
256
+ if (entityKind === 'scope' && isScopeLikeCandidate(target)) {
257
+ return representativeCandidateScore(target) - 200;
258
+ }
259
+ return representativeCandidateScore(target);
260
+ }
261
+ function compareEntityMembers(entityKind, left, right) {
262
+ const scoreDelta = entityMemberPriority(entityKind, right.target) - entityMemberPriority(entityKind, left.target);
263
+ if (scoreDelta !== 0) {
264
+ return scoreDelta;
265
+ }
266
+ return left.index - right.index;
267
+ }
268
+ function pickEntityLabel(entityKind, representative) {
269
+ if (entityKind === 'form') {
270
+ return (representative.context?.landmark?.label ??
271
+ representative.context?.container?.label ??
272
+ representative.context?.group?.label ??
273
+ representative.surfaceLabel ??
274
+ representative.label);
275
+ }
276
+ return (representative.label ??
277
+ representative.context?.container?.label ??
278
+ representative.context?.item?.label ??
279
+ representative.surfaceLabel ??
280
+ representative.context?.group?.label ??
281
+ representative.context?.landmark?.label);
282
+ }
283
+ function collectRepresentativeLabels(targets) {
284
+ const labels = [];
285
+ for (const target of targets) {
286
+ const label = target.label?.replace(/\s+/g, ' ').trim();
287
+ if (!label || label === 'Button' || label === 'Link' || labels.includes(label)) {
288
+ continue;
289
+ }
290
+ labels.push(label);
291
+ if (labels.length >= 4) {
292
+ break;
293
+ }
294
+ }
295
+ return labels;
296
+ }
297
+ function buildEntitySearchText(entityLabel, representative, representativeLabels) {
298
+ return [
299
+ entityLabel,
300
+ representative.label,
301
+ ...representativeLabels,
302
+ representative.surfaceLabel,
303
+ representative.context?.item?.label,
304
+ representative.context?.item?.text,
305
+ representative.context?.group?.label,
306
+ representative.context?.group?.text,
307
+ representative.context?.container?.label,
308
+ representative.context?.container?.text,
309
+ representative.context?.landmark?.label,
310
+ representative.context?.landmark?.text,
311
+ representative.context?.hintText,
312
+ ]
313
+ .map((value) => normalizeRetrievalText(value))
314
+ .filter((value) => Boolean(value))
315
+ .join(' ');
316
+ }
317
+ function buildGoalRetrievalEntity(entityKind, entityKey, memberIndexes, targets) {
318
+ const orderedMembers = [...memberIndexes]
319
+ .map((index) => ({ index, target: targets[index] }))
320
+ .sort((left, right) => compareEntityMembers(entityKind, left, right));
321
+ const representative = orderedMembers[0].target;
322
+ const representativeLabels = collectRepresentativeLabels(orderedMembers.map((entry) => entry.target));
323
+ const label = pickEntityLabel(entityKind, representative);
324
+ return {
325
+ entityKind,
326
+ entityKey,
327
+ firstIndex: Math.min(...memberIndexes),
328
+ memberIndexes: orderedMembers.map((entry) => entry.index),
329
+ representative,
330
+ label,
331
+ kind: entityKind === 'form'
332
+ ? 'form'
333
+ : entityKind === 'item'
334
+ ? representative.surfaceKind ?? representative.kind
335
+ : representative.kind,
336
+ surfaceKind: representative.surfaceKind,
337
+ surfaceLabel: representative.surfaceLabel,
338
+ surfacePriority: representative.surfacePriority,
339
+ framePath: representative.framePath,
340
+ frameUrl: representative.frameUrl,
341
+ context: representative.context,
342
+ structure: representative.structure,
343
+ representativeLabels,
344
+ searchText: buildEntitySearchText(label, representative, representativeLabels),
345
+ };
346
+ }
347
+ function buildGoalRetrievalEntities(targets) {
348
+ const entities = [];
349
+ const groupedTargetIndexes = new Set();
350
+ const nonScopeTargetsBySurface = new Map();
351
+ for (const [index, target] of targets.entries()) {
352
+ if (isScopeLikeCandidate(target)) {
353
+ continue;
354
+ }
355
+ const surfaceIdentity = surfaceIdentityOf(target);
356
+ if (!surfaceIdentity) {
357
+ continue;
358
+ }
359
+ const linked = nonScopeTargetsBySurface.get(surfaceIdentity) ?? [];
360
+ linked.push(index);
361
+ nonScopeTargetsBySurface.set(surfaceIdentity, linked);
362
+ }
363
+ const formGroups = new Map();
364
+ for (const [index, target] of targets.entries()) {
365
+ if (isScopeLikeCandidate(target)) {
366
+ continue;
367
+ }
368
+ const bucketKey = formBucketKey(target);
369
+ if (!bucketKey) {
370
+ continue;
371
+ }
372
+ const members = formGroups.get(bucketKey) ?? [];
373
+ members.push(index);
374
+ formGroups.set(bucketKey, members);
375
+ }
376
+ for (const [key, memberIndexes] of formGroups.entries()) {
377
+ entities.push(buildGoalRetrievalEntity('form', `form:${key}`, memberIndexes, targets));
378
+ memberIndexes.forEach((index) => groupedTargetIndexes.add(index));
379
+ }
380
+ const itemGroups = new Map();
381
+ for (const [index, target] of targets.entries()) {
382
+ const bucketKey = contentItemBucketKey(target);
383
+ if (!bucketKey) {
384
+ continue;
385
+ }
386
+ const members = itemGroups.get(bucketKey) ?? [];
387
+ members.push(index);
388
+ itemGroups.set(bucketKey, members);
389
+ }
390
+ for (const [key, memberIndexes] of itemGroups.entries()) {
391
+ entities.push(buildGoalRetrievalEntity('item', `item:${key}`, memberIndexes, targets));
392
+ memberIndexes.forEach((index) => groupedTargetIndexes.add(index));
393
+ }
394
+ for (const [index, target] of targets.entries()) {
395
+ if (!isHighSignalScopeCandidate(target)) {
396
+ continue;
397
+ }
398
+ const kind = (target.kind ?? '').trim().toLowerCase();
399
+ const surfaceIdentity = surfaceIdentityOf(target);
400
+ if (!surfaceIdentity) {
401
+ continue;
402
+ }
403
+ if (kind === 'form') {
404
+ continue;
405
+ }
406
+ if (kind === 'card' && itemGroups.has(`surface:${surfaceIdentity}`)) {
407
+ continue;
408
+ }
409
+ const linkedTargets = nonScopeTargetsBySurface.get(surfaceIdentity) ?? [];
410
+ const memberIndexes = [index, ...linkedTargets];
411
+ entities.push(buildGoalRetrievalEntity('scope', `scope:${surfaceIdentity}`, memberIndexes, targets));
412
+ linkedTargets.forEach((targetIndex) => groupedTargetIndexes.add(targetIndex));
413
+ }
414
+ for (const [index, target] of targets.entries()) {
415
+ if (isScopeLikeCandidate(target) || groupedTargetIndexes.has(index)) {
416
+ continue;
417
+ }
418
+ if (!isActionLikeTargetCandidate(target) &&
419
+ !isFieldLikeTarget(target) &&
420
+ !isPrimaryFormControlTarget(target)) {
421
+ continue;
422
+ }
423
+ entities.push(buildGoalRetrievalEntity('standalone', `target:${index}`, [index], targets));
424
+ }
425
+ return entities.sort((left, right) => left.firstIndex - right.firstIndex);
426
+ }
427
+ function retrievalEntityPriority(entity) {
428
+ let score = (entity.surfacePriority ?? 0) * 10;
429
+ switch (entity.entityKind) {
430
+ case 'form':
431
+ score += 900;
432
+ break;
433
+ case 'item':
434
+ score += 820;
435
+ break;
436
+ case 'scope':
437
+ score += 720;
438
+ break;
439
+ case 'standalone':
440
+ score += 600;
441
+ break;
442
+ }
443
+ score += Math.min(entity.memberIndexes.length, 6) * 25;
444
+ score += representativeCandidateScore(entity.representative);
445
+ return score;
446
+ }
447
+ function scoreRetrievalEntityAgainstGoal(goal, entity) {
448
+ const normalizedGoal = normalizeRetrievalText(goal);
449
+ if (!normalizedGoal) {
450
+ return 0;
451
+ }
452
+ const entityText = entity.searchText;
453
+ if (!entityText) {
454
+ return 0;
455
+ }
456
+ let score = 0;
457
+ const goalTokens = tokenizeRetrievalText(normalizedGoal).filter((token) => token.length >= 2 && !GOAL_TEXT_STOPWORDS.has(token));
458
+ const entityTokens = new Set(tokenizeRetrievalText(entityText));
459
+ if (entityText.includes(normalizedGoal)) {
460
+ score += 220;
461
+ }
462
+ const entityLabel = normalizeRetrievalText(entity.label);
463
+ if (entityLabel) {
464
+ if (normalizedGoal.includes(entityLabel)) {
465
+ score += 180;
466
+ }
467
+ if (entityLabel.includes(normalizedGoal)) {
468
+ score += 140;
469
+ }
470
+ }
471
+ let matchedTokenCount = 0;
472
+ for (const token of goalTokens) {
473
+ if (entityTokens.has(token)) {
474
+ matchedTokenCount += 1;
475
+ score += 8 + Math.min(token.length, 12);
476
+ }
477
+ }
478
+ if (goalTokens.length > 0) {
479
+ const coverage = matchedTokenCount / goalTokens.length;
480
+ if (coverage === 1) {
481
+ score += 120;
482
+ }
483
+ else if (coverage >= 0.75) {
484
+ score += 70;
485
+ }
486
+ else if (coverage >= 0.5) {
487
+ score += 35;
488
+ }
489
+ }
490
+ return score;
491
+ }
492
+ function preselectRetrievalEntities(goal, entities) {
493
+ if (entities.length <= GOAL_RETRIEVAL_ENTITY_LIMIT) {
494
+ return [...entities];
495
+ }
496
+ const scored = entities.map((entity) => ({
497
+ entity,
498
+ lexicalScore: scoreRetrievalEntityAgainstGoal(goal, entity),
499
+ priority: retrievalEntityPriority(entity),
500
+ }));
501
+ const selected = [];
502
+ const seenKeys = new Set();
503
+ for (const entry of scored
504
+ .filter((candidate) => candidate.lexicalScore > 0)
505
+ .sort((left, right) => right.lexicalScore - left.lexicalScore ||
506
+ right.priority - left.priority ||
507
+ left.entity.firstIndex - right.entity.firstIndex)) {
508
+ if (seenKeys.has(entry.entity.entityKey)) {
509
+ continue;
510
+ }
511
+ seenKeys.add(entry.entity.entityKey);
512
+ selected.push(entry.entity);
513
+ if (selected.length >= GOAL_RETRIEVAL_ENTITY_LIMIT) {
514
+ return selected;
515
+ }
516
+ }
517
+ for (const entry of scored.sort((left, right) => right.priority - left.priority || left.entity.firstIndex - right.entity.firstIndex)) {
518
+ if (seenKeys.has(entry.entity.entityKey)) {
519
+ continue;
520
+ }
521
+ seenKeys.add(entry.entity.entityKey);
522
+ selected.push(entry.entity);
523
+ if (selected.length >= GOAL_RETRIEVAL_ENTITY_LIMIT) {
524
+ break;
525
+ }
526
+ }
527
+ return selected;
528
+ }
529
+ function expandRetrievalEntitiesToCandidates(targets, entities) {
530
+ const orderedIndexes = [];
531
+ const seenIndexes = new Set();
532
+ for (const entity of entities) {
533
+ for (const index of entity.memberIndexes) {
534
+ if (seenIndexes.has(index)) {
535
+ continue;
536
+ }
537
+ seenIndexes.add(index);
538
+ orderedIndexes.push(index);
539
+ }
540
+ }
541
+ return orderedIndexes.map((index) => targets[index]).filter(Boolean);
542
+ }
121
543
  function collectBucketedCandidateIndexes(targets, options) {
122
544
  const bucketEntries = new Map();
123
545
  for (const [index, target] of targets.entries()) {
@@ -305,9 +727,14 @@ export async function rerankDomTargetsForGoal(instruction, targets) {
305
727
  }
306
728
  const gateway = resolveAgentpayGatewayConfig();
307
729
  const client = new AgentpayStagehandLlmClient(gateway);
308
- const candidates = diversifyCandidates(targets);
730
+ const retrievalEntities = buildGoalRetrievalEntities(targets);
731
+ const retrievedCandidates = targets.length > RERANK_CANDIDATE_LIMIT
732
+ ? expandRetrievalEntitiesToCandidates(targets, preselectRetrievalEntities(instruction, retrievalEntities))
733
+ : [...targets];
734
+ const candidates = diversifyCandidates(retrievedCandidates.length > 0 ? retrievedCandidates : [...targets]);
309
735
  const prompt = [
310
736
  'You are choosing from already discovered visible actionable candidates on a webpage.',
737
+ 'Candidates already come from the most relevant retrieved page regions/items for this goal.',
311
738
  'Select only the candidate IDs that directly satisfy the user goal.',
312
739
  'Use container/landmark/layout/state information to disambiguate similar labels.',
313
740
  'Prefer candidates whose surrounding container clearly corroborates the goal.',
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA0QA,iBAAe,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgKhE;AAED,OAAO,EAAE,IAAI,EAAE,CAAC;AAEhB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAWzF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA0QA,iBAAe,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqKhE;AAED,OAAO,EAAE,IAAI,EAAE,CAAC;AAEhB,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAWzF"}
package/dist/index.js CHANGED
@@ -238,14 +238,21 @@ async function main(argv = process.argv) {
238
238
  }
239
239
  case 'launch': {
240
240
  const { launch } = await import('./commands/launch.js');
241
+ const { checkForLaunchUpdate } = await import('./update-check.js');
241
242
  const launchArgs = parseLaunchArgs(args);
242
- outputJSON(await launch(launchArgs.url, {
243
+ const updateNoticePromise = checkForLaunchUpdate().catch(() => null);
244
+ const launchResult = await launch(launchArgs.url, {
243
245
  compact: launchArgs.compact,
244
246
  profile: launchArgs.profile,
245
247
  headless: launchArgs.headless,
246
248
  proxy: launchArgs.proxy,
247
249
  noProxy: launchArgs.noProxy,
248
- }));
250
+ });
251
+ const updateNotice = await updateNoticePromise;
252
+ if (updateNotice) {
253
+ info(updateNotice.message);
254
+ }
255
+ outputJSON(launchResult);
249
256
  break;
250
257
  }
251
258
  case 'navigate': {
@@ -0,0 +1,14 @@
1
+ export type LaunchUpdateNotice = {
2
+ currentVersion: string;
3
+ latestVersion: string;
4
+ message: string;
5
+ };
6
+ export type LaunchUpdateCheckOptions = {
7
+ fetchImpl?: typeof fetch;
8
+ now?: Date;
9
+ timeoutMs?: number;
10
+ ttlMs?: number;
11
+ };
12
+ export declare function compareVersions(left: string, right: string): number;
13
+ export declare function checkForLaunchUpdate(options?: LaunchUpdateCheckOptions): Promise<LaunchUpdateNotice | null>;
14
+ //# sourceMappingURL=update-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../src/update-check.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CA8DnE;AAED,wBAAsB,oBAAoB,CACxC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA2BpC"}
@@ -0,0 +1,182 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getSolverDir } from './solver/config.js';
4
+ const PACKAGE_NAME = '@nuanu-ai/agentbrowse';
5
+ const PACKAGE_JSON_URL = new URL('../package.json', import.meta.url);
6
+ const REGISTRY_METADATA_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`;
7
+ const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1500;
8
+ const DEFAULT_UPDATE_TTL_MS = 12 * 60 * 60 * 1000;
9
+ export function compareVersions(left, right) {
10
+ const parsedLeft = parseVersion(left);
11
+ const parsedRight = parseVersion(right);
12
+ if (!parsedLeft || !parsedRight) {
13
+ return left.localeCompare(right, undefined, {
14
+ numeric: true,
15
+ sensitivity: 'base',
16
+ });
17
+ }
18
+ for (let i = 0; i < 3; i += 1) {
19
+ const diff = parsedLeft.core[i] - parsedRight.core[i];
20
+ if (diff !== 0) {
21
+ return diff;
22
+ }
23
+ }
24
+ if (!parsedLeft.prerelease && !parsedRight.prerelease) {
25
+ return 0;
26
+ }
27
+ if (!parsedLeft.prerelease) {
28
+ return 1;
29
+ }
30
+ if (!parsedRight.prerelease) {
31
+ return -1;
32
+ }
33
+ const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length);
34
+ for (let i = 0; i < length; i += 1) {
35
+ const leftPart = parsedLeft.prerelease[i];
36
+ const rightPart = parsedRight.prerelease[i];
37
+ if (leftPart === undefined) {
38
+ return -1;
39
+ }
40
+ if (rightPart === undefined) {
41
+ return 1;
42
+ }
43
+ if (typeof leftPart === 'number' && typeof rightPart === 'number') {
44
+ const diff = leftPart - rightPart;
45
+ if (diff !== 0) {
46
+ return diff;
47
+ }
48
+ continue;
49
+ }
50
+ if (typeof leftPart === 'number') {
51
+ return -1;
52
+ }
53
+ if (typeof rightPart === 'number') {
54
+ return 1;
55
+ }
56
+ const diff = leftPart.localeCompare(rightPart);
57
+ if (diff !== 0) {
58
+ return diff;
59
+ }
60
+ }
61
+ return 0;
62
+ }
63
+ export async function checkForLaunchUpdate(options = {}) {
64
+ const currentVersion = readCurrentPackageVersion();
65
+ const now = options.now ?? new Date();
66
+ const ttlMs = options.ttlMs ?? DEFAULT_UPDATE_TTL_MS;
67
+ const cachedState = readUpdateState();
68
+ const cachedNotice = buildLaunchUpdateNotice(currentVersion, cachedState?.latestVersion);
69
+ if (cachedState && !isCacheStale(cachedState.lastCheckedAt, now, ttlMs)) {
70
+ return cachedNotice;
71
+ }
72
+ const latestVersion = await fetchLatestVersion({
73
+ fetchImpl: options.fetchImpl,
74
+ timeoutMs: options.timeoutMs ?? DEFAULT_UPDATE_CHECK_TIMEOUT_MS,
75
+ }).catch(() => null);
76
+ if (!latestVersion) {
77
+ return cachedNotice;
78
+ }
79
+ writeUpdateState({
80
+ packageName: PACKAGE_NAME,
81
+ latestVersion,
82
+ lastCheckedAt: now.toISOString(),
83
+ });
84
+ return buildLaunchUpdateNotice(currentVersion, latestVersion);
85
+ }
86
+ function parseVersion(version) {
87
+ const match = version
88
+ .trim()
89
+ .match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+.*)?$/);
90
+ if (!match) {
91
+ return null;
92
+ }
93
+ const prerelease = match[4]
94
+ ? match[4].split('.').map((part) => (/^\d+$/.test(part) ? Number(part) : part))
95
+ : null;
96
+ return {
97
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
98
+ prerelease,
99
+ };
100
+ }
101
+ function buildLaunchUpdateNotice(currentVersion, latestVersion) {
102
+ if (!latestVersion || compareVersions(latestVersion, currentVersion) <= 0) {
103
+ return null;
104
+ }
105
+ return {
106
+ currentVersion,
107
+ latestVersion,
108
+ message: `A newer agentbrowse version is available: ${latestVersion} (current: ${currentVersion}). Update with: npm i -g ${PACKAGE_NAME}@latest`,
109
+ };
110
+ }
111
+ function readCurrentPackageVersion() {
112
+ const raw = JSON.parse(readFileSync(PACKAGE_JSON_URL, 'utf-8'));
113
+ if (!raw.version || typeof raw.version !== 'string') {
114
+ throw new Error('Package version is missing from package.json.');
115
+ }
116
+ return raw.version;
117
+ }
118
+ function readUpdateState() {
119
+ const updateStatePath = getUpdateStatePath();
120
+ if (!existsSync(updateStatePath)) {
121
+ return null;
122
+ }
123
+ try {
124
+ const raw = JSON.parse(readFileSync(updateStatePath, 'utf-8'));
125
+ if (raw.packageName !== PACKAGE_NAME ||
126
+ typeof raw.latestVersion !== 'string' ||
127
+ typeof raw.lastCheckedAt !== 'string') {
128
+ return null;
129
+ }
130
+ return {
131
+ packageName: raw.packageName,
132
+ latestVersion: raw.latestVersion,
133
+ lastCheckedAt: raw.lastCheckedAt,
134
+ };
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function writeUpdateState(state) {
141
+ const solverDir = getSolverDir();
142
+ mkdirSync(solverDir, { recursive: true });
143
+ writeFileSync(getUpdateStatePath(), JSON.stringify(state, null, 2) + '\n', 'utf-8');
144
+ }
145
+ function isCacheStale(lastCheckedAt, now, ttlMs) {
146
+ const lastCheckedAtMs = new Date(lastCheckedAt).getTime();
147
+ if (!Number.isFinite(lastCheckedAtMs)) {
148
+ return true;
149
+ }
150
+ return now.getTime() - lastCheckedAtMs >= ttlMs;
151
+ }
152
+ async function fetchLatestVersion(options) {
153
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
154
+ if (typeof fetchImpl !== 'function') {
155
+ throw new Error('Global fetch is not available.');
156
+ }
157
+ const controller = new AbortController();
158
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
159
+ try {
160
+ const response = await fetchImpl(REGISTRY_METADATA_URL, {
161
+ headers: {
162
+ Accept: 'application/json',
163
+ },
164
+ signal: controller.signal,
165
+ });
166
+ if (!response.ok) {
167
+ throw new Error(`Registry responded with ${response.status}.`);
168
+ }
169
+ const metadata = (await response.json());
170
+ const latestVersion = metadata['dist-tags']?.latest;
171
+ if (!latestVersion || typeof latestVersion !== 'string') {
172
+ throw new Error('Registry metadata does not include dist-tags.latest.');
173
+ }
174
+ return latestVersion;
175
+ }
176
+ finally {
177
+ clearTimeout(timeout);
178
+ }
179
+ }
180
+ function getUpdateStatePath() {
181
+ return join(getSolverDir(), 'update-state.json');
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuanu-ai/agentbrowse",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "type": "module",
5
5
  "description": "Browser automation CLI for AI agents: control a CDP browser, observe UI surfaces, act on refs, extract data, capture screenshots, complete protected fills, and solve captchas",
6
6
  "keywords": [