halo-agent 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/captcha.js CHANGED
@@ -16,10 +16,43 @@ const CAPSOLVER_API = 'https://api.capsolver.com';
16
16
  * Detect if a CAPTCHA is present anywhere on the page (including nested iframes).
17
17
  * Returns { detected, type, sitekey, pageUrl }
18
18
  */
19
+ // ATS domains that EMBED their form (and its reCAPTCHA) in an iframe on a
20
+ // company-branded careers page. When the captcha lives in one of these
21
+ // frames, the sitekey is registered to THIS domain — so CapSolver must be
22
+ // told the frame URL, not the wrapper page URL. (The Orion case:
23
+ // orioninnovation.com/careers embeds job-boards.greenhouse.io; sending the
24
+ // orioninnovation.com URL → "Invalid domain for site key".)
25
+ const EMBEDDED_ATS_HOSTS = /(greenhouse\.io|lever\.co|ashbyhq\.com|myworkdayjobs\.com|icims\.com|smartrecruiters\.com|jobvite\.com|workable\.com)/i;
26
+
27
+ /**
28
+ * Pick the URL to hand CapSolver for a given reCAPTCHA. The sitekey is
29
+ * domain-locked, so the URL must match the domain the widget actually runs
30
+ * on. If the form is embedded in a known-ATS iframe, that frame's URL wins
31
+ * over the top-level page URL.
32
+ */
33
+ function resolveCaptchaPageUrl(page) {
34
+ const topUrl = page.url();
35
+ // If the top URL is itself an ATS domain, use it directly.
36
+ if (EMBEDDED_ATS_HOSTS.test(topUrl)) return topUrl;
37
+ // Otherwise, look for an ATS iframe — its URL is where the sitekey lives.
38
+ for (const frame of page.frames()) {
39
+ if (frame === page.mainFrame()) continue;
40
+ const fUrl = frame.url();
41
+ if (fUrl && EMBEDDED_ATS_HOSTS.test(fUrl)) {
42
+ // Strip the recaptcha frame itself — we want the FORM's frame, not
43
+ // google's. recaptcha frames are on google.com so they won't match
44
+ // EMBEDDED_ATS_HOSTS anyway, but be defensive.
45
+ if (!/google\.com|gstatic\.com/i.test(fUrl)) return fUrl;
46
+ }
47
+ }
48
+ return topUrl;
49
+ }
50
+
19
51
  async function detectCaptcha(page) {
20
- // The pageUrl CapSolver wants is the top-level URL the user sees, not an
21
- // inner iframe URL. Anchor it here so every branch returns the same thing.
22
- const pageUrl = page.url();
52
+ // The URL CapSolver needs is the domain the sitekey is registered to —
53
+ // the embedded ATS frame's URL when the form is iframed into a branded
54
+ // careers page, otherwise the top-level URL.
55
+ const pageUrl = resolveCaptchaPageUrl(page);
23
56
 
24
57
  // First try the top-level frame via evaluate
25
58
  const topResult = await page.evaluate(() => {
package/orchestrator.js CHANGED
@@ -87,6 +87,11 @@ async function runJob(queueItem, chromeConn, config, reportStatus) {
87
87
 
88
88
  let page = null;
89
89
  let tempResumeFile = null;
90
+ // When the run ends in a terminal SUCCESS (DONE), we leave the tab open
91
+ // so the user sees the confirmation page — visual loop-closure. Only
92
+ // close the tab on errors/cleanup. Set true right before each successful
93
+ // DONE return/fall-through.
94
+ let leaveTabOpen = false;
90
95
 
91
96
  // Initialize cross-page context tracker
92
97
  const ctx = createFormContext();
@@ -246,6 +251,7 @@ async function runJob(queueItem, chromeConn, config, reportStatus) {
246
251
  const currentUrl = page.url();
247
252
  const looksLikeConfirmation = /thank|confirm|success|applied|submitted/i.test(currentUrl);
248
253
  if (looksLikeConfirmation) {
254
+ leaveTabOpen = true;
249
255
  await reportStatus('DONE', { fields_filled: cumulativeFilled });
250
256
  await clearCheckpoint(config, queueId);
251
257
  console.log(`[orchestrator] Detected confirmation page by URL: ${currentUrl}`);
@@ -388,6 +394,7 @@ async function runJob(queueItem, chromeConn, config, reportStatus) {
388
394
  if (vRes.ok) vVerdict = await vRes.json();
389
395
  } catch {}
390
396
  if (vVerdict.submitted === true) {
397
+ leaveTabOpen = true;
391
398
  await reportStatus('DONE', { confirmation_screenshot_r2_key: confirmKey || null, fields_filled: cumulativeFilled });
392
399
  await clearCheckpoint(config, queueId);
393
400
  console.log(`[orchestrator] Done via vision (verified): ${queueItem.company} - ${queueItem.title}`);
@@ -613,6 +620,7 @@ async function runJob(queueItem, chromeConn, config, reportStatus) {
613
620
  fields_filled: cumulativeFilled,
614
621
  });
615
622
  } else {
623
+ leaveTabOpen = true;
616
624
  await reportStatus('DONE', {
617
625
  confirmation_screenshot_r2_key: confirmKey || null,
618
626
  fields_filled: cumulativeFilled,
@@ -668,9 +676,16 @@ async function runJob(queueItem, chromeConn, config, reportStatus) {
668
676
  if (tempResumeFile) {
669
677
  try { fs.unlinkSync(tempResumeFile); } catch {}
670
678
  }
671
- // Close the tab (leave other tabs untouched)
672
- if (page) {
679
+ // Close the tab on failure/cleanup but on a successful submit, LEAVE
680
+ // it open so the user sees the confirmation page (visual loop-closure).
681
+ // The next job opens its own new tab, so leaving this one doesn't block
682
+ // anything; sequential processing is enforced by the poller (one job at
683
+ // a time). Stale success tabs accumulate, but that's the user's call to
684
+ // close — they asked for the satisfaction of seeing it land.
685
+ if (page && !leaveTabOpen) {
673
686
  try { await page.close(); } catch {}
687
+ } else if (page && leaveTabOpen) {
688
+ console.log('[orchestrator] Leaving confirmation tab open for your review.');
674
689
  }
675
690
  }
676
691
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "halo-agent",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "HALO local apply agent — auto-fills job applications using your real Chrome session",
5
5
  "main": "index.js",
6
6
  "bin": {