diff --git a/README.md b/README.md new file mode 100644 index 0000000..698f287 --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# claw-apply + +Automated job search and application engine for LinkedIn and Wellfound. Searches for matching roles, applies automatically, and learns from every unknown question it encounters. + +Built for [OpenClaw](https://openclaw.dev) but runs standalone with Node.js. + +## What it does + +- **Searches** LinkedIn and Wellfound on a schedule with your configured keywords and filters +- **Applies** to matching jobs automatically via LinkedIn Easy Apply and Wellfound's native flow +- **Learns** — when it hits a question it can't answer, it messages you on Telegram, saves your reply, and never asks again +- **Deduplicates** across runs so you never apply to the same job twice +- **Retries** failed applications up to a configurable number of times before giving up + +## Quick start + +```bash +git clone https://github.com/MattJackson/claw-apply.git +cd claw-apply +npm install +``` + +### 1. Configure + +Copy the example configs and fill in your values: + +```bash +cp config/settings.example.json config/settings.json +cp config/profile.example.json config/profile.json +cp config/search_config.example.json config/search_config.json +``` + +| File | What to fill in | +|------|----------------| +| `profile.json` | Name, email, phone, resume path, work authorization, salary | +| `search_config.json` | Job titles, keywords, platforms, filters, exclusions | +| `settings.json` | Telegram bot token + user ID, Kernel profiles, proxy ID | + +### 2. Set up Kernel (stealth browsers) + +claw-apply uses [Kernel](https://kernel.sh) for stealth browser sessions that bypass bot detection. + +```bash +npm install -g @onkernel/cli + +# Create a residential proxy +kernel proxies create --type residential --country US --name "claw-apply-proxy" + +# Create authenticated browser profiles (follow prompts to log in) +kernel auth create --name "LinkedIn-YourName" +kernel auth create --name "WellFound-YourName" +``` + +Add the profile names and proxy ID to `config/settings.json`. + +### 3. Set up Telegram notifications + +1. Message [@BotFather](https://t.me/BotFather) on Telegram to create a bot +2. Copy the bot token to `settings.json` -> `notifications.bot_token` +3. Message [@userinfobot](https://t.me/userinfobot) to get your user ID +4. Add it to `settings.json` -> `notifications.telegram_user_id` + +### 4. Verify setup + +```bash +KERNEL_API_KEY=your_key node setup.mjs +``` + +This validates your config, tests LinkedIn and Wellfound logins, and sends a test Telegram message. + +### 5. Run + +```bash +# Search for jobs +KERNEL_API_KEY=your_key node job_searcher.mjs + +# Preview what's in the queue before applying +KERNEL_API_KEY=your_key node job_applier.mjs --preview + +# Apply to queued jobs +KERNEL_API_KEY=your_key node job_applier.mjs +``` + +For automated runs, set up cron or use OpenClaw's scheduler: + +``` +Search: 0 * * * * (hourly) +Apply: 0 */6 * * * (every 6 hours) +``` + +## How it works + +### Search flow + +1. Runs your configured keyword searches on LinkedIn and Wellfound +2. Paginates through results (LinkedIn) and infinite-scrolls (Wellfound) +3. Filters out excluded keywords and companies +4. Deduplicates against the existing queue by job ID and URL +5. Saves new jobs to `data/jobs_queue.json` with status `new` +6. Sends a Telegram summary + +### Apply flow + +1. Picks up all `new` and `needs_answer` jobs from the queue (up to `max_applications_per_run`) +2. Opens a stealth browser session per platform +3. For each job: + - **LinkedIn Easy Apply**: navigates to job, clicks Easy Apply, fills the multi-step modal, submits + - **Wellfound**: navigates to job, clicks Apply, fills the form, submits + - Detects and skips recruiter-only listings, external ATS jobs, and honeypot questions +4. On unknown required fields, messages you on Telegram and moves on +5. Failed jobs are retried on the next run (up to `max_retries`, default 2) +6. Sends a summary with counts: applied, failed, needs answer, skipped + +### Self-learning answers + +When the applier encounters a form question it doesn't know how to answer: + +1. Marks the job as `needs_answer` with the question text +2. Sends you a Telegram message with the question +3. You reply with the answer +4. The answer is saved to `config/answers.json` as a pattern match +5. Next run, it retries the job and fills in the answer automatically + +Patterns support regex: + +```json +[ + { "pattern": "quota attainment", "answer": "1.12" }, + { "pattern": "years.*enterprise", "answer": "5" }, + { "pattern": "1.*10.*scale", "answer": "9" } +] +``` + +## Configuration + +### Settings + +| Key | Default | Description | +|-----|---------|-------------| +| `max_applications_per_run` | `50` | Cap applications per run to avoid rate limits | +| `max_retries` | `2` | Times to retry a failed application before marking it permanently failed | +| `browser.provider` | `"kernel"` | `"kernel"` for stealth browsers, `"local"` for local Playwright | + +### Search filters + +| Filter | Type | Description | +|--------|------|-------------| +| `remote` | boolean | Remote jobs only | +| `posted_within_days` | number | Only jobs posted within N days | +| `easy_apply_only` | boolean | LinkedIn Easy Apply only | +| `exclude_keywords` | string[] | Skip jobs with these words in title or company | +| `first_run_days` | number | On first run, look back N days (default 90) | + +## Project structure + +``` +claw-apply/ +├── job_searcher.mjs Search agent +├── job_applier.mjs Apply agent +├── setup.mjs Setup wizard +├── lib/ +│ ├── constants.mjs Shared constants and defaults +│ ├── browser.mjs Kernel/Playwright browser factory +│ ├── form_filler.mjs Generic form filling with pattern matching +│ ├── linkedin.mjs LinkedIn search + Easy Apply +│ ├── wellfound.mjs Wellfound search + apply +│ ├── queue.mjs Job queue and config management +│ └── notify.mjs Telegram notifications with rate limiting +├── config/ +│ ├── *.example.json Templates (committed) +│ ├── profile.json Your info (gitignored) +│ ├── search_config.json Your searches (gitignored) +│ ├── answers.json Learned answers (gitignored) +│ └── settings.json Your settings (gitignored) +└── data/ + ├── jobs_queue.json Job queue (auto-managed) + └── applications_log.json Application history (auto-managed) +``` + +## Job statuses + +| Status | Meaning | +|--------|---------| +| `new` | Found, waiting to apply | +| `applied` | Successfully submitted | +| `needs_answer` | Blocked on unknown question, waiting for your reply | +| `failed` | Failed after max retries | +| `skipped` | Honeypot detected | +| `skipped_recruiter_only` | LinkedIn recruiter-only listing | +| `skipped_external_unsupported` | External ATS (Greenhouse, Lever — not yet supported) | +| `skipped_easy_apply_unsupported` | No Easy Apply button available | + +## Roadmap + +- [x] LinkedIn Easy Apply +- [x] Wellfound apply +- [x] Kernel stealth browsers + residential proxy +- [x] Self-learning answer bank +- [x] Retry logic for transient failures +- [x] Preview mode (`--preview`) +- [x] Configurable application caps and retry limits +- [ ] Indeed support +- [ ] External ATS support (Greenhouse, Lever) +- [ ] Job scoring and ranking +- [ ] Per-job cover letter generation via LLM + +## License + +MIT diff --git a/SKILL.md b/SKILL.md index fe73fb9..efaa0ea 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: claw-apply -description: Automated job search and application for LinkedIn and Wellfound. Searches for matching roles hourly, applies automatically every 6 hours using Playwright + Kernel stealth browsers. Handles LinkedIn Easy Apply and Wellfound applications. Asks you via Telegram when it hits a question it can't answer, saves your answer, and never asks again. Use when you want to automate your job search and application process. +description: Automated job search and application for LinkedIn and Wellfound. Searches for matching roles hourly, applies automatically every 6 hours using Playwright + Kernel stealth browsers. Handles LinkedIn Easy Apply multi-step modals and Wellfound applications. Self-learning — asks you via Telegram when it hits an unknown question, saves your answer, and never asks again. Retries failed applications automatically. Preview mode lets you review the queue before applying. --- # claw-apply @@ -9,7 +9,8 @@ Automated job search and application. Finds matching roles on LinkedIn and Wellf ## Requirements -- [Kernel.sh](https://kernel.sh) account (for stealth browsers + bot detection bypass) +- Node.js 18+ +- [Kernel](https://kernel.sh) account (stealth browsers + bot detection bypass) — or local Playwright - Kernel CLI: `npm install -g @onkernel/cli` - Kernel Managed Auth sessions for LinkedIn and Wellfound - Kernel residential proxy (US recommended) @@ -17,97 +18,95 @@ Automated job search and application. Finds matching roles on LinkedIn and Wellf ## Setup -### 1. Install dependencies +### 1. Install + ```bash +git clone https://github.com/MattJackson/claw-apply.git cd claw-apply npm install ``` -### 2. Create Kernel Managed Auth sessions +### 2. Create Kernel browser sessions + ```bash # Create residential proxy kernel proxies create --type residential --country US --name "claw-apply-proxy" # Create authenticated browser profiles -kernel auth create --name "LinkedIn-YourName" # Follow prompts to log in -kernel auth create --name "WellFound-YourName" # Follow prompts to log in +kernel auth create --name "LinkedIn-YourName" +kernel auth create --name "WellFound-YourName" ``` ### 3. Configure -Edit these files in `config/`: -- **`profile.json`** — your personal info, resume path, cover letter -- **`search_config.json`** — what jobs to search for (titles, keywords, filters) -- **`settings.json`** — Telegram bot token, Kernel profile names, proxy ID, mode A/B +Copy example configs and fill in your values: + +```bash +cp config/settings.example.json config/settings.json +cp config/profile.example.json config/profile.json +cp config/search_config.example.json config/search_config.json +``` + +- **`profile.json`** — name, email, phone, resume path, work authorization, salary +- **`search_config.json`** — keywords, platforms, filters, exclusions +- **`settings.json`** — Telegram bot token, Kernel profile names, proxy ID, run caps + +### 4. Verify -### 4. Run setup ```bash KERNEL_API_KEY=your_key node setup.mjs ``` -Verifies config, tests logins, sends a test Telegram message. +### 5. Run + +```bash +KERNEL_API_KEY=your_key node job_searcher.mjs # search now +KERNEL_API_KEY=your_key node job_applier.mjs --preview # preview queue +KERNEL_API_KEY=your_key node job_applier.mjs # apply now +``` + +### 6. Schedule (via OpenClaw or cron) -### 5. Register cron jobs (via OpenClaw) ``` Search: 0 * * * * (hourly) Apply: 0 */6 * * * (every 6 hours) ``` -## Running manually -```bash -KERNEL_API_KEY=your_key node job_searcher.mjs # search now -KERNEL_API_KEY=your_key node job_applier.mjs # apply now -``` - ## How it works -**JobSearcher** (hourly): -1. Searches LinkedIn + Wellfound with your configured keywords -2. Filters out excluded roles/companies -3. Adds new jobs to `data/jobs_queue.json` -4. Sends Telegram: "Found X new jobs" +**Search** — runs your keyword searches on LinkedIn and Wellfound, paginates/scrolls through results, filters exclusions, deduplicates, and queues new jobs. -**JobApplier** (every 6 hours): -1. Reads queue for `new` + `needs_answer` jobs -2. LinkedIn: navigates two-panel search view, clicks Easy Apply, fills form, submits -3. Wellfound: navigates to job, fills profile, submits -4. On unknown question → Telegrams you → saves answer → retries next run -5. Sends summary when done +**Apply** — picks up queued jobs, opens stealth browser sessions, fills forms using your profile + learned answers, and submits. Detects and skips honeypots, recruiter-only listings, and external ATS. Retries failed jobs automatically (default 2 retries). -## Mode A vs Mode B -Set in `config/settings.json` → `"mode": "A"` or `"B"` - -- **A**: Fully automatic. No intervention needed. -- **B**: Applier sends you the queue 30 min before running. You can flag jobs to skip before it fires. +**Learn** — on unknown questions, messages you on Telegram. You reply, the answer is saved to `answers.json` with regex pattern matching, and the job is retried next run. ## File structure + ``` claw-apply/ -├── job_searcher.mjs search agent -├── job_applier.mjs apply agent -├── setup.mjs setup wizard +├── job_searcher.mjs Search agent +├── job_applier.mjs Apply agent +├── setup.mjs Setup wizard ├── lib/ -│ ├── browser.mjs Kernel/Playwright factory -│ ├── form_filler.mjs generic form filling -│ ├── linkedin.mjs LinkedIn search + apply -│ ├── wellfound.mjs Wellfound search + apply -│ ├── queue.mjs queue management -│ └── notify.mjs Telegram notifications +│ ├── constants.mjs Shared constants +│ ├── browser.mjs Kernel/Playwright browser factory +│ ├── form_filler.mjs Form filling with pattern matching +│ ├── linkedin.mjs LinkedIn search + Easy Apply +│ ├── wellfound.mjs Wellfound search + apply +│ ├── queue.mjs Job queue + config management +│ └── notify.mjs Telegram notifications ├── config/ -│ ├── profile.json ← fill this in -│ ├── search_config.json ← fill this in -│ ├── answers.json ← auto-grows over time -│ └── settings.json ← fill this in +│ ├── *.example.json Templates (committed) +│ └── *.json Your config (gitignored) └── data/ - ├── jobs_queue.json auto-managed - └── applications_log.json auto-managed + ├── jobs_queue.json Job queue (auto-managed) + └── applications_log.json History (auto-managed) ``` -## answers.json — self-learning Q&A bank -When the applier hits a question it can't answer, it messages you on Telegram. -You reply. The answer is saved to `config/answers.json` and used forever after. +## answers.json — self-learning Q&A + +When the applier can't answer a question, it messages you. Your reply is saved and reused: -Pattern matching is regex-friendly: ```json [ { "pattern": "quota attainment", "answer": "1.12" }, @@ -115,3 +114,5 @@ Pattern matching is regex-friendly: { "pattern": "1.*10.*scale", "answer": "9" } ] ``` + +Patterns are matched case-insensitively and support regex. diff --git a/SPEC.md b/SPEC.md index e20ded0..a4bd4aa 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,36 +1,56 @@ -# claw-apply — Skill Spec v0.1 +# claw-apply — Technical Spec -Automated job search and application skill for OpenClaw. -Searches LinkedIn and Wellfound for matching roles, applies automatically using Playwright + Kernel stealth browsers. +Automated job search and application engine. Searches LinkedIn and Wellfound for matching roles, applies automatically using Playwright + Kernel stealth browsers, and self-learns from unknown questions. --- ## Architecture -### Two agents +### Two agents, shared queue **JobSearcher** (`job_searcher.mjs`) -- Runs on a schedule (default: hourly) -- Searches configured platforms with configured queries +- Runs on schedule (default: hourly) +- Searches configured platforms with configured keywords +- LinkedIn: paginates through up to 40 pages of results +- Wellfound: infinite-scrolls up to 10 times to load all results - Filters out excluded roles/companies -- Dedupes against existing queue +- Deduplicates by job ID and URL against existing queue - Writes new jobs to `jobs_queue.json` with status `new` -- Sends Telegram summary: "Found X new jobs" +- Sends Telegram summary **JobApplier** (`job_applier.mjs`) -- Runs on a schedule (default: every 6 hours) -- Reads `jobs_queue.json` for status `new` + `needs_answer` -- Attempts to apply to each job -- On success → status: `applied` -- On unknown question → messages user via Telegram, status: `needs_answer` -- On skip/fail → status: `skipped` or `failed` -- Sends Telegram summary when done +- Runs on schedule (default: every 6 hours) +- Reads queue for status `new` + `needs_answer` +- Respects `max_applications_per_run` cap +- LinkedIn: navigates directly to job URL, detects apply type (Easy Apply / external / recruiter-only), fills multi-step modal +- Wellfound: navigates to job, fills form, submits +- Detects honeypot questions and skips +- On unknown required fields: messages user via Telegram, marks `needs_answer` +- On error: retries up to `max_retries` (default 2) before marking `failed` +- Sends summary with granular skip reasons + +**Preview mode** (`--preview`): shows queued jobs without applying. + +### Shared modules + +| Module | Responsibility | +|--------|---------------| +| `lib/constants.mjs` | All timeouts, selectors, defaults — no magic numbers in code | +| `lib/browser.mjs` | Browser factory — Kernel stealth (default) with local Playwright fallback | +| `lib/form_filler.mjs` | Generic form filling — custom answers first, then built-in profile matching | +| `lib/queue.mjs` | Queue CRUD with in-memory caching, config file validation | +| `lib/notify.mjs` | Telegram Bot API with rate limiting (1.5s between sends) | +| `lib/linkedin.mjs` | LinkedIn search (paginated) + Easy Apply (multi-step modal) | +| `lib/wellfound.mjs` | Wellfound search (infinite scroll) + apply | --- -## Config Files (user sets up once) +## Config files + +All user config is gitignored. Example templates are committed. ### `profile.json` + ```json { "name": { "first": "Jane", "last": "Smith" }, @@ -51,81 +71,41 @@ Searches LinkedIn and Wellfound for matching roles, applies automatically using }, "willing_to_relocate": false, "desired_salary": 150000, - "cover_letter": "Your cover letter text here..." + "cover_letter": "Your cover letter text here." } ``` ### `search_config.json` + ```json { + "first_run_days": 90, "searches": [ { "name": "Founding GTM", "track": "gtm", - "keywords": [ - "founding account executive", - "first sales hire", - "first GTM hire", - "founding AE", - "head of sales startup remote" - ], + "keywords": ["founding account executive", "first sales hire"], "platforms": ["linkedin", "wellfound"], - "filters": { - "remote": true, - "posted_within_days": 2 - }, - "exclude_keywords": ["BDR", "SDR", "staffing", "insurance", "retail", "consumer", "recruiter"], - "salary_min": 130000 - }, - { - "name": "Enterprise AE", - "track": "ae", - "keywords": [ - "enterprise account executive SaaS remote", - "senior account executive technical SaaS remote" - ], - "platforms": ["linkedin"], "filters": { "remote": true, "posted_within_days": 2, - "easy_apply_only": true + "easy_apply_only": false }, - "exclude_keywords": ["BDR", "SDR", "SMB", "staffing"], - "salary_min": 150000 + "exclude_keywords": ["BDR", "SDR", "staffing", "insurance"] } ] } ``` -### `answers.json` -Flat array of pattern → answer mappings. Pattern is substring match (case-insensitive). First match wins. -```json -[ - { "pattern": "quota attainment", "answer": "1.12", "note": "FY24 $1.2M quota, hit $1.12M" }, - { "pattern": "sponsor", "answer": "No" }, - { "pattern": "authorized", "answer": "Yes" }, - { "pattern": "relocat", "answer": "No" }, - { "pattern": "years.*sales", "answer": "7" }, - { "pattern": "years.*enterprise", "answer": "5" }, - { "pattern": "years.*crm", "answer": "7" }, - { "pattern": "1.*10.*scale", "answer": "9" }, - { "pattern": "salary", "answer": "150000" }, - { "pattern": "start date", "answer": "Immediately" } -] -``` - ### `settings.json` + ```json { - "mode": "A", - "review_window_minutes": 30, - "schedules": { - "search": "0 * * * *", - "apply": "0 */6 * * *" - }, "max_applications_per_run": 50, + "max_retries": 2, "notifications": { - "telegram_user_id": "YOUR_TELEGRAM_ID" + "telegram_user_id": "YOUR_TELEGRAM_USER_ID", + "bot_token": "YOUR_TELEGRAM_BOT_TOKEN" }, "kernel": { "proxy_id": "YOUR_KERNEL_PROXY_ID", @@ -136,16 +116,29 @@ Flat array of pattern → answer mappings. Pattern is substring match (case-inse }, "browser": { "provider": "kernel", - "fallback": "local" + "playwright_path": null } } ``` +### `answers.json` + +Flat array of pattern-answer pairs. Patterns are matched case-insensitively and support regex. First match wins. + +```json +[ + { "pattern": "quota attainment", "answer": "1.12" }, + { "pattern": "years.*enterprise", "answer": "5" }, + { "pattern": "1.*10.*scale", "answer": "9" } +] +``` + --- -## Data Files (auto-managed) +## Data files (auto-managed) ### `jobs_queue.json` + ```json [ { @@ -158,6 +151,7 @@ Flat array of pattern → answer mappings. Pattern is substring match (case-inse "found_at": "2026-03-05T22:00:00Z", "status": "new", "status_updated_at": "2026-03-05T22:00:00Z", + "retry_count": 0, "pending_question": null, "applied_at": null, "notes": null @@ -165,83 +159,93 @@ Flat array of pattern → answer mappings. Pattern is substring match (case-inse ] ``` -**Statuses:** `new` → `applied` / `skipped` / `failed` / `needs_answer` +### Job statuses + +| Status | Meaning | Next action | +|--------|---------|-------------| +| `new` | Found, waiting to apply | Applier picks it up | +| `applied` | Successfully submitted | Done | +| `needs_answer` | Blocked on unknown question | Applier retries after user answers | +| `failed` | Failed after max retries | Manual review | +| `skipped` | Honeypot detected | Permanent skip | +| `skipped_recruiter_only` | LinkedIn recruiter-only | Permanent skip | +| `skipped_external_unsupported` | External ATS | Saved for future ATS support | +| `skipped_easy_apply_unsupported` | No Easy Apply button | Permanent skip | ### `applications_log.json` -Append-only history of every application attempt with outcome. + +Append-only history of every application attempt with outcome, timestamps, and metadata. --- -## Unknown Question Flow +## Unknown question flow -1. Applier hits a required field it can't answer -2. Marks job as `needs_answer`, stores the question text in `pending_question` -3. Sends Telegram: *"Applying to Senior AE @ Acme Corp and hit this question: 'What was your last quota attainment in $M?' — what should I answer?"* +1. Applier encounters a required field with no matching answer +2. Marks job as `needs_answer`, stores question in `pending_question` +3. Sends Telegram: "Applying to Senior AE @ Acme Corp — question: 'What was your quota attainment?' — what should I answer?" 4. Moves on to next job -5. User replies → answer saved to `answers.json` -6. Next applier run retries all `needs_answer` jobs +5. User replies with answer +6. Answer saved to `answers.json` as pattern match +7. Next applier run retries all `needs_answer` jobs --- -## Mode A vs Mode B +## Retry logic -**Mode A (fully automatic):** -Search → Queue → Apply. No intervention required. +When an application fails due to a transient error (timeout, network issue, page didn't load): -**Mode B (soft gate):** -Search → Queue → Telegram summary sent to user → 30 min window to reply with any job IDs to skip → Apply runs. - -Configured via `settings.json` → `mode: "A"` or `"B"` +1. `retry_count` is incremented on the job +2. Job status is reset to `new` so the next run picks it up +3. After `max_retries` (default 2) failures, job is marked `failed` permanently +4. Failed jobs are logged to `applications_log.json` with error details --- -## File Structure +## File structure ``` claw-apply/ -├── SKILL.md ← OpenClaw skill entry point -├── SPEC.md ← this file -├── job_searcher.mjs ← search agent -├── job_applier.mjs ← apply agent +├── README.md Documentation +├── SKILL.md OpenClaw skill manifest +├── SPEC.md This file +├── job_searcher.mjs Search agent +├── job_applier.mjs Apply agent +├── setup.mjs Setup wizard ├── lib/ -│ ├── browser.mjs ← Kernel/Playwright browser factory -│ ├── form_filler.mjs ← form filling logic -│ ├── linkedin.mjs ← LinkedIn search + apply -│ ├── wellfound.mjs ← Wellfound search + apply -│ └── notify.mjs ← Telegram notifications +│ ├── constants.mjs Shared constants and defaults +│ ├── browser.mjs Kernel/Playwright browser factory +│ ├── form_filler.mjs Form filling with pattern matching +│ ├── linkedin.mjs LinkedIn search + Easy Apply +│ ├── wellfound.mjs Wellfound search + apply +│ ├── queue.mjs Queue management + config validation +│ └── notify.mjs Telegram notifications + rate limiting ├── config/ -│ ├── profile.json ← user fills this -│ ├── search_config.json← user fills this -│ ├── answers.json ← auto-grows over time -│ └── settings.json ← user fills this +│ ├── *.example.json Templates (committed) +│ └── *.json User config (gitignored) └── data/ - ├── jobs_queue.json ← auto-managed - └── applications_log.json ← auto-managed + ├── jobs_queue.json Job queue (auto-managed) + └── applications_log.json Application history (auto-managed) ``` --- -## Setup (user steps) +## Roadmap -1. Install: `openclaw skill install claw-apply` -2. Configure Kernel Managed Auth for LinkedIn + Wellfound (or provide local Chrome) -3. Create a residential proxy in Kernel: `kernel proxies create --type residential --country US` -4. Fill in `config/profile.json`, `config/search_config.json`, `config/settings.json` -5. Run: `openclaw skill run claw-apply setup` — registers crons, verifies login, sends test notification -6. Done. Runs automatically. +### v1 (current) +- [x] LinkedIn Easy Apply (multi-step modal, pagination) +- [x] Wellfound apply (infinite scroll) +- [x] Kernel stealth browsers + residential proxy +- [x] Self-learning answer bank with regex patterns +- [x] Retry logic with configurable max retries +- [x] Preview mode (`--preview`) +- [x] Configurable application caps +- [x] Telegram notifications with rate limiting +- [x] Config validation with clear error messages +- [x] In-memory queue caching for performance +- [x] Constants extracted — no magic numbers in code ---- - -## v1 Scope - -- [x] LinkedIn Easy Apply -- [x] Wellfound apply -- [x] Kernel stealth browser + residential proxy -- [x] Mode A + Mode B -- [x] Unknown question → Telegram → answers.json flow -- [x] Deduplication -- [x] Hourly search / 6hr apply cron -- [ ] Indeed (v2) -- [ ] External ATS / Greenhouse / Lever (v2) -- [ ] Job scoring/ranking (v2) -- [ ] Cover letter generation per-job via LLM (v2) +### v2 (planned) +- [ ] Indeed support +- [ ] External ATS support (Greenhouse, Lever) +- [ ] Job scoring and ranking +- [ ] Per-job cover letter generation via LLM