Commit Graph

34 Commits

Author SHA1 Message Date
38dabbd6dd Fix address hallucination and duplicate status messages
- 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>
2026-03-06 16:47:18 -08:00
84a98e7839 Treat number fields as required even if DOM doesn't mark them
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>
2026-03-06 16:43:39 -08:00
aefd1f2023 Fix label dedup for space-separated duplicates
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>
2026-03-06 16:28:51 -08:00
05739a455b Fix AI hallucinating street addresses and other facts
- 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>
2026-03-06 16:25:32 -08:00
fd30646d8a Fix number field validation: strip text from numeric answers
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>
2026-03-06 16:21:37 -08:00
6d69443f51 Pass placeholder format hints to AI for inputs and textareas
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>
2026-03-06 15:12:35 -08:00
3bf1786895 Detect date format from placeholder (MM/DD/YYYY) instead of label matching
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>
2026-03-06 15:05:41 -08:00
7d81d59cb5 Broaden date field detection — match any label containing 'date'
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>
2026-03-06 15:04:10 -08:00
fdb0224226 Fix date inputs and dismiss date picker/sub-form popups
- 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>
2026-03-06 15:02:35 -08:00
d0a40e4654 Batch CDP calls: single-evaluate debug info, data-claw-idx tagging, field count logging
- 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>
2026-03-06 14:50:47 -08:00
a956b98941 Stop auto-checking 'top choice' and 'interested' checkboxes
Only auto-check confirm/agree/consent checkboxes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:46:03 -08:00
23eb5284fa Fix snapshot evaluate: scope queries to container element, handle Page fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:42:41 -08:00
aeb41b42c2 Optimize form_filler: batch DOM reads into single evaluate() call
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>
2026-03-06 14:39:18 -08:00
b0a2eb3746 Fix fieldset radios: validate answers against options, fix checkbox skip, fix authorization match
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>
2026-03-06 14:05:02 -08:00
053c0b1242 Fix select answer mismatch: validate answer against available options
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>
2026-03-06 13:44:49 -08:00
615899e9a9 Normalize fieldset legend text same as getLabel (dedup, strip Required)
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>
2026-03-06 13:27:45 -08:00
97aa8472aa Fix radio selection: verify click worked, fallback to input/select
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>
2026-03-06 13:16:30 -08:00
3e367687b2 Fix checkbox group (multi-select) handling in form filler
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>
2026-03-06 12:43:58 -08:00
c5ebdc9362 Fix answer saving: normalize answers format, improve label dedup
- 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>
2026-03-06 12:24:10 -08:00
7c9de1af4a AI fallback for unknown form fields: ask Claude before Telegram
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>
2026-03-06 11:52:39 -08:00
14cf9a12c1 Reliability improvements: click retry, resume selection, answer loop, browser recovery
- 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>
2026-03-06 11:30:09 -08:00
00bcfaca53 feat: include select options in unknown field reports and Telegram messages
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>
2026-03-06 11:06:38 -08:00
2c04504ad8 fix: report unfilled required selects as unknown fields
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>
2026-03-06 11:05:54 -08:00
4c85a88902 fix: stuck loop on unfilled selects, Continue button detection
- 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>
2026-03-06 10:51:26 -08:00
3c46de1358 fix: shadow DOM support — LinkedIn modal is inside shadow root
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>
2026-03-06 10:25:51 -08:00
e6fb380e1c fix: cover letter silent drop, submit verification, autocomplete scoping
- 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>
2026-03-06 10:11:44 -08:00
e62756c6ca fix: robustness improvements — atomic writes, timeouts, shell injection, validation errors
- Atomic JSON writes (write-to-tmp + rename) prevent queue/log corruption
- Per-job (3min) and overall run (45min) timeouts prevent hangs
- execFileSync in ai_answer.mjs prevents shell injection with resume paths
- Validation error detection after form fill in Easy Apply modal
- Config-driven enabled_apply_types (from settings.json)
- isRequired() detects required/aria-required/label * patterns
- getLabel() strips trailing * from required field labels
- Actionable logging on failures ("Action: ..." messages)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:01:53 -08:00
a7ce119bde fix: resilient modal button detection and form filler robustness
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>
2026-03-06 09:54:44 -08:00
b79e9d0b9c feat: city autocomplete dropdown, phone country code, checkboxes, EEO selects, location/heard-about answers 2026-03-06 17:39:05 +00:00
b1528ac0ad refactor: extract magic numbers to constants, fix audit issues
- Centralize all magic numbers/strings in lib/constants.mjs
- Fix double-replaced import names in filter.mjs
- Consolidate duplicate fs imports in job_applier/job_searcher
- Remove empty JSDoc block in job_searcher
- Update keywords.mjs model from claude-3-haiku to claude-haiku-4-5
- Extract Anthropic API URLs to constants
- Convert :has-text() selectors to page.locator() API
- Fix SIGTERM handler conflict — move partial-run notification into lock.onShutdown
- Remove unused exports (LOCAL_USER_AGENT, DEFAULT_REVIEW_WINDOW_MINUTES)
- Fix variable shadowing (b -> v) in job_filter reduce callback
- Replace SKILL.md PM2 references with system cron

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:45:17 -08:00
ed908c91af fix: audit bugs + better error logging for searcher debugging
Bugs fixed:
- form_filler.mjs: add missing await on el.evaluate() in getLabel()
- analyze_ats.mjs: check job.apply_type instead of non-existent job.easy_apply
- status.mjs: fix typo "that run" → "in that run"
- README: add missing lock.mjs to project structure

Logging improvements:
- job_searcher.mjs: log browser creation, login steps, stack traces on error
- linkedin.mjs/wellfound.mjs: catch and log navigation failures per keyword
- browser.mjs: descriptive errors for Kernel and CDP connection failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:28:23 -08:00
ec68e621b8 fix: audit cleanup — ReDoS guard, Telegram validation, README accuracy
- 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>
2026-03-05 17:20:32 -08:00
47513e8cec fix: security/bug fixes, extract constants, remove magic values
- 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>
2026-03-05 16:01:42 -08:00
52a56f59f6 feat: claw-apply v0.1 — full implementation
- job_searcher.mjs: LinkedIn + Wellfound search, queue population
- job_applier.mjs: Easy Apply + Wellfound apply, Mode A/B
- lib/form_filler.mjs: config-driven form filling, custom answers.json
- lib/linkedin.mjs: two-panel Easy Apply flow
- lib/wellfound.mjs: Wellfound search + apply
- lib/browser.mjs: Kernel stealth browser factory with local fallback
- lib/queue.mjs: jobs_queue.json management
- lib/notify.mjs: Telegram notifications
- setup.mjs: setup wizard with login verification
- Config templates: profile, search_config, answers, settings
- SKILL.md: OpenClaw skill definition
2026-03-05 23:24:09 +00:00