halo-agent 2.4.5 → 2.4.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "2.4.5",
3
+ "version": "2.4.6",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -292,6 +292,25 @@ async function enrichFromDom(page, axFields) {
292
292
  const ownerFrame = window.frameElement;
293
293
  const iframeSrc = ownerFrame ? ownerFrame.src || ownerFrame.name || '' : '';
294
294
 
295
+ // Disjoint label-for recovery. When the input has no matching
296
+ // <label for=id> partner (e.g. Ashby puts data-field-path on a
297
+ // wrapper <div> while the input itself has no id), Chrome's AX
298
+ // algorithm bottoms out at placeholder. We pre-compute a label
299
+ // from a known ATS field-group wrapper here so pickLabel() can
300
+ // prefer it when rawName turns out to be the placeholder.
301
+ let recoveredLabel = '';
302
+ try {
303
+ const wrapperSel = '[data-field-path], [class*="_fieldEntry_"], .ashby-application-form-field-entry, .application-question, [data-question-id]';
304
+ const wrapper = el.closest && el.closest(wrapperSel);
305
+ if (wrapper) {
306
+ const lbl = wrapper.querySelector('label');
307
+ if (lbl) {
308
+ const t = safeText(lbl.textContent || '').replace(/\s*\*\s*$/, '').trim();
309
+ if (t && t.length < 200) recoveredLabel = t;
310
+ }
311
+ }
312
+ } catch {}
313
+
295
314
  return {
296
315
  mmid,
297
316
  tag,
@@ -301,6 +320,7 @@ async function enrichFromDom(page, axFields) {
301
320
  id: el.id || '',
302
321
  ariaLabel: el.getAttribute('aria-label') || '',
303
322
  placeholder: el.placeholder || '',
323
+ recoveredLabel,
304
324
  selectorHint,
305
325
  currentValue,
306
326
  options,
@@ -349,7 +369,20 @@ function pickLabel(f) {
349
369
  // Priority: AX name → aria-label → placeholder → text content → field id.
350
370
  // (AX name already merges <label for>, aria-labelledby, fieldset/legend
351
371
  // walks — we get them all for free.)
352
- return (f.rawName || f.ariaLabel || f.placeholder || f.textContent || f.id || f.name || '').trim();
372
+ //
373
+ // Disjoint label-for recovery: when Chrome's AX algorithm bottoms out at
374
+ // the placeholder (because <label for=X> points at a wrapper id and the
375
+ // input itself has no id — Ashby's Location pattern, also any
376
+ // CSS-modules form library), rawName === placeholder. In that case
377
+ // prefer the label we walked from the field-group wrapper.
378
+ const rawName = (f.rawName || '').trim();
379
+ const placeholder = (f.placeholder || '').trim();
380
+ const recovered = (f.recoveredLabel || '').trim();
381
+ if (rawName && placeholder && rawName.toLowerCase() === placeholder.toLowerCase() && recovered) {
382
+ console.log(`[scanAx] recovered label "${recovered}" for placeholder-only field (was: "${rawName}")`);
383
+ return recovered;
384
+ }
385
+ return (rawName || f.ariaLabel || recovered || placeholder || f.textContent || f.id || f.name || '').trim();
353
386
  }
354
387
 
355
388
  function pickDescription(f) {
@@ -394,6 +427,19 @@ async function scanFrameDom(frame, frameUrl) {
394
427
  const t = alb.split(/\s+/).map((id) => document.getElementById(id)).filter(Boolean).map((e) => safeText(e.textContent)).join(' ').trim();
395
428
  if (t) return t;
396
429
  }
430
+ // Disjoint label-for recovery via ATS field-group wrapper.
431
+ // Mirrors the recoveredLabel walk in the main-frame DOM evaluator.
432
+ try {
433
+ const wrapperSel = '[data-field-path], [class*="_fieldEntry_"], .ashby-application-form-field-entry, .application-question, [data-question-id]';
434
+ const wrapper = el.closest && el.closest(wrapperSel);
435
+ if (wrapper) {
436
+ const lbl = wrapper.querySelector('label');
437
+ if (lbl && !lbl.contains(el)) {
438
+ const t = safeText(lbl.textContent).replace(/\*$/, '').trim();
439
+ if (t && t.length < 200) return t;
440
+ }
441
+ }
442
+ } catch {}
397
443
  let p = el.parentElement, hops = 0;
398
444
  while (p && hops < 8) {
399
445
  const lbl = p.querySelector('label, legend, h2, h3, h4');