Captures placeholder from textareas in snapshot. When AI fallback is
used, includes placeholder as a format hint so the AI knows expected
format (dates, phone numbers, URLs, etc).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reads input placeholder in snapshot. If it contains MM/DD/YYYY pattern,
auto-fills with today's date in the correct format. Generic — works
regardless of the field label.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catches 'Date of Application', 'Available Start Date', and other
date fields that expect mm/dd/yyyy format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Start date fields now return mm/dd/yyyy format instead of "Immediately"
- Cancel any open sub-forms or date pickers before filling (handles
stacked popups with double-cancel check)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- getModalDebugInfo: one evaluate() for heading, buttons, errors (was N+2 calls)
- selectOptionFuzzy: batch-read option texts in one evaluate (was N calls)
- Tag elements with data-claw-idx during snapshot, query by attribute in fill()
(fixes fragile positional index matching for checkboxes/inputs)
- Log field counts per fill step for debugging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of 60+ sequential CDP round-trips per step (isVisible, getLabel,
inputValue, isRequired for each element), snapshot all form state in one
evaluate() call, do answer matching locally, then only make CDP calls to
fill/click elements that need action.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tested against Webflow (4311307116) and Scout Global (4378934058):
- Webflow: reaches Review page with all radios checked and checkbox group selected
- Scout Global: already applied successfully with select fix
Three fixes:
1. Fieldset radio answers validated against available options - prevents
experience pattern returning "7" when options are Yes/No
2. Checkbox groups no longer skipped when one checkbox already checked -
was preventing multi-select from working on retry
3. "authoriz" pattern matches both "authorized" and "authorization"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Built-in answerFor() could return a number (e.g. "7" for years experience)
when the select only has Yes/No options, causing selectOptionFuzzy to fail
silently. Now checks if the answer matches any available option before using
it, falling through to AI with actual options if not.
Also added "right to work" to work authorization pattern matching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Legend text was saved to answers.json with duplicated text and "Required"
suffix, causing pattern mismatches on future runs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn radio clicks via label can silently fail. Now verifies input:checked
after label click, falls back to clicking radio input directly by value match,
and tries <select> within fieldset as last resort.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn forms with "Select all that apply" questions use checkbox groups
inside fieldsets. The fieldset handler only clicked one label (radio behavior).
Now detects checkbox vs radio fieldsets and splits comma-separated AI answers
to click multiple labels. Also added "consent" to auto-check keyword list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- normalizeAnswers() handles both object {"q":"a"} and array [{pattern,answer}]
formats — prevents silent failures when answers.json format varies
- getLabel() now strips "Required" suffix before dedup, uses smarter
prefix-repeat detection instead of simple half-split
- telegram_answers.mjs also normalizes on load
- Cleaned existing answers.json on AWS: removed duplicated text in patterns,
fixed bad AI answer for "Current company", generalized Geotab-specific patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Every unknown required field now goes through AI before falling back to
Telegram. Claude sees the question + all saved answers + profile, and
either recognizes it as a variation of a saved answer or generates a new
one. AI answers are auto-saved to answers.json so the same question is
a free pattern match next time. Telegram is now last resort (no API key).
Flow: pattern match (free) → AI (smart) → auto-save → Telegram (human)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
When a required select has no answer, the unknown field now includes
the available options. Telegram notification shows them so the user
knows exactly what to reply with.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Non-EEO required selects with no answer were silently ignored, causing
infinite loops when LinkedIn blocked page advancement. Now reported as
unknown fields so the applier can exit with needs_answer status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Stuck detection after clicking Next: if heading+progress unchanged 2x,
exit with 'stuck' status instead of looping forever
- Select dropdowns: treat "Select an option" as unfilled (LinkedIn's
placeholder has a truthy value that was skipping the fill logic)
- Continue button fallback: detect draft "Continue" span via apply URL
pattern when Easy Apply button not found
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn renders Easy Apply modal inside shadow DOM. document.querySelector()
in evaluate() cannot pierce shadow DOM, but Playwright's page.$() can.
easy_apply.mjs:
- Replaced all frame.evaluate(document.querySelector) with ElementHandle ops
- findModalButton uses modal.$$() + btn.evaluate() instead of evaluateHandle
- getModalDebugInfo uses modal.$eval and modal.$$() for all queries
- dismissModal scans buttons via page.$$() instead of evaluateHandle
- Removed findModalFrame (no longer needed)
form_filler.mjs:
- getLabel() walks up ancestor DOM to find labels (LinkedIn doesn't use label[for])
- Deduplicates repeated label text ("Phone country codePhone country code")
- isRequired() walks ancestors to find labels with * or required indicators
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Text inputs matched to cover letter now report as unknown if required,
instead of silently leaving the field empty
- Submit click now verifies modal closed before reporting success;
returns 'incomplete' with actionable log if modal stays open
- selectAutocomplete scoped to container (modal) to avoid clicking
wrong dropdowns from the underlying page
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>
- form_filler.mjs: reject regex patterns over 200 chars to mitigate ReDoS
- notify.mjs: check res.ok before parsing Telegram API response
- README: update project structure with new lib/apply/ modules, session.mjs,
keywords.mjs; fix max_applications_per_run docs (no limit by default);
clarify ATS stub status in roadmap
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>