Some LinkedIn listings have image-only upload fields (JPG/PNG).
Don't attempt to upload a PDF resume to these inputs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- answerFor returning '' (intentionally blank) was treated as falsy,
falling through to AI which fabricated "123 Main Street". Now
empty string skips the field without triggering AI or reporting unknown.
- status.mjs was printing to stdout AND sending via Telegram, causing
OpenClaw to relay a duplicate plain-text copy. Now only prints to
stdout when Telegram isn't configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn validates number fields even when not marked required in the
DOM. Previously these were skipped (no AI call, no answer). Now number
fields always trigger AI fallback and are reported as unknown if empty.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LinkedIn renders label text twice in nested spans, producing
"Question? Question?" instead of "Question?". The old dedup only
caught exact concatenation (ABCABC); now also handles space-separated
duplicates by comparing left/right halves at the midpoint space.
Applied to all 4 copies: extractLabel, _extractLabel, normalizeLegend,
_normalizeLegend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add street/address pattern to answerFor() — returns profile address or empty string
- Update AI prompt: return "UNKNOWN" instead of guessing facts
- Handle UNKNOWN response by treating it as no answer (triggers Telegram ask)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Number inputs (type="number") were getting answers like "5 years"
instead of "5", causing LinkedIn validation errors. Now:
- AI gets "(must be a number, no text or units)" hint
- Answers are stripped to digits before filling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Profile name is returned by ensureAuth() from the auth connection
(looked up by domain). No more storing profile names in settings.json
for the applier flow. createBrowser() still supports legacy platform
keys as fallback for searcher/setup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Connection IDs change when re-authing. Instead of storing IDs in
settings.json (which go stale), look up connections by domain at
runtime via kernel.auth.connections.list({ domain }). This keeps
the applier in sync regardless of connection recreation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite session.mjs: check connection status via SDK before creating
browser. If NEEDS_AUTH + can_reauth, auto re-auth with stored creds.
If can't re-auth, send Telegram alert and skip platform.
- Wire ensureAuth() into job_applier.mjs before createBrowser()
- Jobs are returned to queue (not failed) when auth is down
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
LinkedIn sometimes opens an Add Education/Experience sub-form with
Save/Cancel buttons that blocks the main Next button. Detect and
cancel these before attempting to fill the step.
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>
The dated model ID claude-sonnet-4-6-20251101 doesn't exist, causing
keyword generation to fall back to static keywords.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Telegram Markdown treats lines starting with spaces as preformatted code
blocks. Replaced leading spaces with bullet points and arrows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Status: only show non-zero queue counts, compact breakdown line,
remove emoji clutter
Apply summary: remove misleading green check on header, show applied/total
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>
Search: show per-track breakdown (found/added per track name)
Filter: show top 5 scoring jobs with score, title, company and cost
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each job now shows title @ company with a clickable link, grouped by
status category. Only non-empty categories are shown.
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>
Distinguishes closed listings from missing apply buttons. Shows in summary
as a separate line item.
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>
LinkedIn's apply URL pattern is {jobUrl}/apply/?openSDUIApplyFlow=true
which opens the modal directly. This eliminates:
- Button finding with waitForSelector (flaky on slow loads)
- Click retry logic
- "Continue" link fallback for draft applications
- Shadow DOM piercing for button detection
Tested: modal opens reliably, meta readable from background page,
form + progress bar present, 3.4s total navigation time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
domcontentloaded fires before JS renders the Easy Apply button,
causing flaky "No Easy Apply button found" failures on valid listings.
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>
- All entry points with log tee now call logStream.end() + process.exit()
(log stream kept event loop alive, blocking next cron run)
- easy_apply: detect "no longer accepting applications" and similar closed
listing text before reporting as unsupported
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused APPLY_PRIORITY array (replaced by score-based sort)
- Fix run timeout only breaking inner loop — now breaks outer platform loop too
- Remove dead lastProgress variable in easy_apply step loop
- Add stdout/stderr log tee to job_searcher, job_filter, telegram_poller
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Default case in handleResult now increments skipped_other
- Summary only shows non-zero categories (cleaner output)
- Applied count always shown
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>
Log lines now explicitly say STOPPING with reason when the modal is
dismissed due to unknown questions or honeypots. handleResult logs
the full flow: paused → generating AI answer → sent to Telegram →
will retry after reply. Prevents claw from misreading as a hang/timeout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New lib/telegram_answers.mjs: shared module that polls Telegram getUpdates,
matches replies to needs_answer jobs, saves to answers.json, flips job to new
- telegram_poller.mjs: lightweight cron script (every minute via OpenClaw)
- Applier also processes replies at start of each run as safety net
- sendTelegram now returns message_id, stored on job for reply matching
- User replies "ACCEPT" to use AI answer, or types their own
- Answers persist in answers.json and apply to ALL future jobs
- Also includes: selectOptionFuzzy, multiple dialog handling, browser
recovery, answers reload, per-job timeout bump to 10min
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>
Replace fixed 2.5s sleep + single check with event-driven waitForSelector
(state: 'detached', timeout: 8s). If modal persists, check for success
indicators (success text, Submit button gone) before marking incomplete.
Fixes false 'incomplete' status when LinkedIn shows post-submit dialog.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Heading is always "Apply to <Company>" on every step — comparing it
caused false stuck detection on step 2. Now:
- Progress bar selector finds <progress> elements (no explicit role)
- Stuck detection re-reads progress AFTER clicking Next to see if it changed
- Threshold raised to 3 same-progress clicks before exiting
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>
page.$() and frame.$() can't find the <a> in LinkedIn's shadow DOM,
but frame.evaluateHandle with document.querySelectorAll can.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>