Skip to content

About

Search LinkedIn job postings and get an LLM-scored shortlist against your own resume — runs entirely on your machine.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Job Search Agent

Upload your resume, describe what you're looking for, and get an LLM-scored APPLY/MAYBE/SKIP shortlist of real job postings — each with a link and a short reason. Runs entirely on your machine; nothing leaves it except the scoring calls you make yourself to OpenRouter.

Demo: search page, run history, and a scored run's results

Quick start

git clone <this-repo-url>
cd job-search-agent
npm install          # also downloads a Chromium binary for Playwright
cp .env.example .env
npx prisma migrate dev
npm run dev

Open http://localhost:3000, then on the Setup page: upload your resume (PDF), describe the role/companies/location you want, and paste an OpenRouter API key — free at openrouter.ai/keys, no card needed. Leave the model field blank — the app automatically picks a currently free model for you (see How it works). Then go to Search, pick keywords/location/date-window (or paste a LinkedIn search URL directly), and click Run search.

How it works

flowchart LR
    A["Setup<br/>resume + criteria + API key"] --> B["Search<br/>params or URL"]
    B --> C["Scrape LinkedIn<br/>logged-out, Playwright"]
    C --> D[("SQLite<br/>dedup + store")]
    D --> E["Score in background<br/>OpenRouter free model"]
    E --> F["Runs<br/>Apply / Maybe / Skip"]
    G["Schedule<br/>hourly / daily / weekly"] -. triggers .-> B
Loading

The app scrapes job postings from a source — currently LinkedIn's public job search, used logged-out/anonymously (it's the only source implemented so far, not a permanent design choice — see src/server/scraping/). A single run typically covers up to ~50-60 postings — LinkedIn's practical limit for anonymous browsing.

Clicking Run search returns as soon as LinkedIn has been scraped; scoring each posting against your resume happens in the background afterward, and the run page fills in Apply / Maybe / Skip verdicts as they land (auto-refreshing, no need to keep the tab open). Free-tier models sit behind a shared, often-congested rate-limit pool, so a full batch can take anywhere from under a minute to several minutes — feel free to navigate away and check Runs later. Anything that doesn't get scored is retried automatically the next time a search surfaces it.

Model selection works the same way: if you leave the model field blank, the app calls OpenRouter's own /models endpoint, filters for models that are currently free and support structured JSON output, and picks from those (src/server/llm/openRouterModels.ts) — instead of hardcoding a slug that could get deprecated or moved to paid-only later.

Scheduling

The Schedule page lets the app re-run your default search (target role + location from Setup) on its own, every hour/day/week — no need to click Run search yourself each time. It's built into the app itself (an internal timer, checked every minute — see src/server/scheduler/), so it only fires while the server is actually running (npm run dev/npm start); nothing happens while your machine is off or the process is stopped. There's one schedule for now, reusing your Setup defaults — not per-search schedules.

Responsible use

This app only reads what a logged-out browser would see — it never signs in, never bypasses a login/CAPTCHA wall, and never applies or contacts anyone on your behalf. It's built for personal, low-volume use (one person searching for their own next role), not bulk data collection. Scraping LinkedIn is against its Terms of Service even when done politely and logged-out — running this is at your own risk. If you fork this, keep it personal-scale: don't remove the scroll delay in src/server/scraping/linkedin.ts, don't run it on a tight schedule or at high concurrency.

Privacy

Your resume, criteria, and API key are stored only in your local SQLite database (prisma/dev.db, gitignored) — in plaintext, which is fine for a single-user local tool but not a public/multi-tenant deployment as-is.

Troubleshooting

  • Playwright/browser errors: npx playwright install chromium.
  • Database errors: re-run npx prisma migrate dev.
  • "Scoring failed": check your OpenRouter key on Setup. If you set a specific model yourself and it errors as unavailable/deprecated, clear the field to go back to automatic free-model selection. Either way, just run a search again — any posting that hasn't been successfully scored yet gets retried whenever it resurfaces.
  • Scanned/image PDFs: only text-layer extraction is supported, no OCR.
  • Zero results: LinkedIn's markup can drift; selectors live in src/server/scraping/linkedin.ts.

License

MIT — use it, fork it, change it.

About

Search LinkedIn job postings and get an LLM-scored shortlist against your own resume — runs entirely on your machine.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages