- Easy Apply click: click found element directly, retry with force if modal doesn't open
- Resume: select first radio if none checked, fall back to file upload
- AI answers: inject stored answers into formFiller on needs_answer retry
- Answers persistence: reload answers.json before each job for Telegram replies
- Browser recovery: detect dead page, create fresh browser session
- Multiple dialogs: findApplyModal() tags the right dialog among cookie banners etc.
- Select matching: case-insensitive fuzzy match with substring fallback
- dismissModal: scope Discard scan to dialog elements only
- Label dedup: normalize whitespace, fix odd-length edge case
- no_modal status: explicit handleResult case
- Per-job timeout: 10 minutes (was 3)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- dismissModal now always waits for Discard confirmation after closing
(previously returned early after Dismiss click, leaving draft saved)
- LINKEDIN_APPLY_BUTTON_SELECTOR matches both "Easy Apply" and
"Continue applying" (shown when a previous draft exists)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
easy_apply.mjs:
- findModalButton() uses 3-strategy detection: aria-label exact/substring,
then exact button text match — survives LinkedIn aria-label changes
- Check order fixed: Next → Review → Submit (submit only when no forward nav)
- All queries scoped to modal + :not([disabled])
- dismissModal() with fallback chain: Dismiss → Close/X → Escape → Discard
- Uses innerText for button text (ignores hidden children)
form_filler.mjs:
- All queries scoped to container (modal when present, page otherwise)
- Radio labels use $$('label') + textContent instead of broken :has-text()
- Autocomplete uses waitForSelector instead of blind 800ms sleep
- EEO selects iterate options directly (selectOption doesn't accept regex)
- Country code check ordered before country to prevent fragile match order
constants.mjs:
- Add AUTOCOMPLETE_WAIT, AUTOCOMPLETE_TIMEOUT
- Remove unused button selectors (now handled inline by findModalButton)
ai_answer.mjs + keywords.mjs:
- Use ANTHROPIC_API_URL constant, claude-sonnet-4-6 model
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add loadConfig() helper with clear errors for missing/malformed JSON
- Replace raw JSON.parse(readFileSync(...)) in both entry points
- Track retry_count on jobs; re-queue as 'new' up to max_retries (default 2)
- Add max_retries and DEFAULT_MAX_RETRIES constant
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove random suffix from Wellfound job IDs (broke dedup)
- Add null coalescing to all profile field returns in form_filler
- Fix honeypot case referencing nonexistent results.skipped counter
- Remove unused makeJobId import from linkedin.mjs
- Navigate directly to job URL instead of search+click in linkedin apply
- Add Telegram notification rate limiting (1.5s between sends)
- Replace Mode B blocking sleep with --preview flag
- Add max_applications_per_run enforcement
- Remove tracked search_config.json (keep .example.json only)
- Add search_config.json to .gitignore, fix duplicate node_modules entry
- Extract all magic numbers/strings to lib/constants.mjs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>