mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
* added contact skill * refactor(job-scraper): fold referral-contacts into Step 4.5, drop contacts cache
201 lines
8.8 KiB
Markdown
201 lines
8.8 KiB
Markdown
---
|
||
name: scrape
|
||
description: >
|
||
Finds new job postings matching your profile via installed portal-search CLIs
|
||
(LinkedIn, local job boards, and any skills added with /add-portal). Deduplicates
|
||
across runs. Triggers on: job scrape, find jobs, search jobs, new jobs, job search,
|
||
scrape jobs, /scrape
|
||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash(bun --version), Bash(bun run .agents/skills/*/cli/src/cli.ts *), WebFetch, WebSearch, Agent, AskUserQuestion
|
||
---
|
||
|
||
# Job Scraper
|
||
|
||
---
|
||
|
||
## How It Works
|
||
|
||
This skill searches job portals using the **installed portal-search CLIs** in
|
||
`.agents/skills/` (plus WebSearch as a fallback), using queries from your profile.
|
||
It deduplicates against previously seen jobs and the application tracker, and
|
||
presents new matches with a quick fit assessment.
|
||
|
||
## Invocation
|
||
|
||
The user triggers this skill by saying things like:
|
||
- "Find new jobs"
|
||
- "Scrape for jobs"
|
||
- "Any new positions?"
|
||
- "/scrape"
|
||
|
||
Optional arguments:
|
||
- A focus area, e.g. "/scrape data science" or "/scrape geophysics"
|
||
- "broad" to run all search categories, e.g. "/scrape broad"
|
||
|
||
---
|
||
|
||
## Execution Steps
|
||
|
||
### Step 0: Load State
|
||
|
||
1. Read `job_scraper/seen_jobs.json` (create if missing - start with `{"seen": {}}`)
|
||
2. Read `job_search_tracker.csv` to extract already-applied companies+roles
|
||
3. Read `search-queries.md` (this directory) for the search strategy
|
||
|
||
### Step 1: Search
|
||
|
||
Read `search-queries.md` (this directory) for the search strategy. By default, run the top 3 priority query categories. If the user said "broad", run all categories. If the user specified a focus area (e.g. "data science"), prioritize queries from that category.
|
||
|
||
**Use the installed CLI tools as the primary search mechanism.** Fall back to `WebSearch` only for portals that do not have a CLI skill, or if `bun` is unavailable on the system.
|
||
|
||
#### 1a. Check bun availability
|
||
|
||
```bash
|
||
bun --version
|
||
```
|
||
|
||
If this fails (bun not installed), skip to **1c (WebSearch fallback)** for all portals and note the fallback in the Step 5 output.
|
||
|
||
#### 1b. Run CLI tools (primary — run these in parallel where possible)
|
||
|
||
Discover all installed portal CLI skills by reading every `SKILL.md` found under `.agents/skills/*/SKILL.md`. Each file documents that portal's exact CLI flags and usage examples. **Use each portal's own documented interface — do not guess flags.** This approach automatically includes any new portals added via `/add-portal` without requiring changes to this file.
|
||
|
||
For each installed portal skill:
|
||
|
||
1. Read its `SKILL.md` to find the correct `bun run …` invocation and supported flags.
|
||
2. Translate the query terms from `search-queries.md` into that portal's flag format (e.g. `--key`, `--search-string`, `--query`, filter codes — whatever the portal's SKILL.md specifies).
|
||
3. Scope to the last 14 days using the portal's supported recency flag (`--jobage`, `--since <YYYY-MM-DD>`, `--order PublicationDate`, etc. — as documented per portal).
|
||
4. Cap results to ~20 per call using the portal's limit flag.
|
||
5. Use `--format json` for machine-readable output.
|
||
|
||
Run all portal CLI calls in parallel where possible using the Agent tool. Collect all `results` arrays into a single pool for Step 2, keeping each result tagged with its source portal skill (for Step 2 `detail` lookups).
|
||
|
||
If a CLI tool exits with a non-zero code, log the error message and continue — do not abort the whole search.
|
||
|
||
#### 1c. WebSearch fallback
|
||
|
||
Use `WebSearch` for:
|
||
- Portals listed in `search-queries.md` that do **not** have a corresponding directory under `.agents/skills/`
|
||
- Any portal whose CLI fails at runtime
|
||
- When bun is unavailable (Step 1a failed)
|
||
|
||
Use the site-specific query strings from `search-queries.md` directly as WebSearch queries for these portals.
|
||
|
||
### Step 2: Fetch & Parse
|
||
|
||
For each promising result from Step 1:
|
||
|
||
**From CLI results:** Search output already includes title, company, location, date,
|
||
and URL. For jobs worth a deeper look, fetch full detail with that portal's `detail`
|
||
command (see its SKILL.md — do not guess flags) to extract **key requirements**,
|
||
**application deadline**, and a brief description snippet.
|
||
|
||
**From WebSearch results:** Use `WebFetch` on the posting URL and extract the same
|
||
fields manually.
|
||
|
||
For every candidate:
|
||
- Skip if the URL or company+title combo already exists in `seen_jobs.json`
|
||
- Skip if the company+role already appears in `job_search_tracker.csv`
|
||
|
||
### Step 3: Quick Fit Assessment
|
||
|
||
For each new job, do a rapid fit check (NOT the full evaluation from `04-job-evaluation.md` - just a quick signal):
|
||
|
||
- **High match**: Role directly involves your core skills
|
||
- **Medium match**: Role is adjacent to your experience
|
||
- **Low match**: Role requires significant skills you lack
|
||
|
||
### Step 4: Deduplicate & Store
|
||
|
||
1. Add ALL fetched jobs (new and skipped) to `seen_jobs.json` with structure:
|
||
```json
|
||
{
|
||
"seen": {
|
||
"<url_or_company_title_key>": {
|
||
"title": "...",
|
||
"company": "...",
|
||
"url": "...",
|
||
"first_seen": "YYYY-MM-DD",
|
||
"fit": "high/medium/low",
|
||
"status": "new/skipped/evaluated/ranked/expired"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`/rank` extends this schema additively: ranked entries also carry `rank_score` (0–100 overall score), `rank_verdict` (fit band, e.g. "strong fit"), and `rank_date` (ISO date of ranking). The `status` field is set to `"ranked"`. Do not drop any of these fields when re-writing entries.
|
||
|
||
2. Only present jobs NOT already in the seen list or tracker.
|
||
|
||
### Step 4.5: Generate Referral Contact Links (High & Medium Fit Only)
|
||
|
||
For every job from this run with `fit` of **high** or **medium** (skip low-fit jobs),
|
||
build two LinkedIn people-search URLs so the user can find a recruiter or team member to
|
||
reach out to for a referral or a warm intro. This is deliberately a link-generation step,
|
||
not an automated lookup: no scraping, no third-party API, zero runtime dependencies or
|
||
credentials required.
|
||
|
||
**A. Recruiters / Talent Acquisition (the referral path)**
|
||
```
|
||
https://www.linkedin.com/search/results/people/?keywords=<url-encoded "<Company Name> recruiter">&origin=GLOBAL_SEARCH_HEADER
|
||
```
|
||
|
||
**B. Role/team peers (informational-outreach / warm-intro path)**
|
||
```
|
||
https://www.linkedin.com/search/results/people/?keywords=<url-encoded "<Company Name> <role keyword>">&origin=GLOBAL_SEARCH_HEADER
|
||
```
|
||
Use a short keyword drawn from the posting's title for `<role keyword>` - e.g. a posting
|
||
titled "AI Program Manager" becomes `"<Company Name> AI Program Manager"`.
|
||
|
||
Both links are for the user to open and browse themselves - never fetch or scrape the
|
||
LinkedIn people-search result pages programmatically. Never fabricate contacts or claim a
|
||
specific person was found; these are search links, not results.
|
||
|
||
### Step 5: Present Results
|
||
|
||
Present new jobs in a table sorted by fit (high first):
|
||
|
||
```
|
||
## New Job Matches - YYYY-MM-DD
|
||
|
||
Found X new positions (Y high, Z medium, W low match).
|
||
|
||
| # | Fit | Title | Company | Location | Deadline | URL |
|
||
|---|-----|-------|---------|----------|----------|-----|
|
||
| 1 | High | ... | ... | ... | ... | [Link](...) |
|
||
|
||
### High-Match Highlights
|
||
For each high-match job, add 2-3 bullet points:
|
||
- Why it matches your profile
|
||
- Key requirements to check
|
||
- Any red flags
|
||
|
||
### Contacts
|
||
For each high/medium-fit job from Step 4.5, add a short contacts block with the two
|
||
LinkedIn search links:
|
||
- Recruiters/TA search link, for the referral path
|
||
- Role/team-peer search link, for the warm-intro / informational-outreach path
|
||
```
|
||
|
||
After presenting, ask:
|
||
> "Want me to evaluate any of these in detail? Just give me the number(s)."
|
||
|
||
If the user picks a number, invoke the **job-application-assistant** skill workflow (fit evaluation first, then CV + cover letter if approved).
|
||
|
||
If the run found many new jobs (roughly 8+), also suggest `/rank` - it batch-scores all new postings against the full fit framework and returns a ranked shortlist, which beats eyeballing a long table. (`/rank` sets the `ranked` and `expired` status values in `seen_jobs.json`; treat both as already-seen for dedup purposes.)
|
||
|
||
### Step 6: Update Tracker (Optional)
|
||
|
||
If the user decides to apply to any job, add a row to `job_search_tracker.csv`.
|
||
|
||
---
|
||
|
||
## Important Rules
|
||
|
||
1. **Never fabricate job postings.** Only present jobs from actual CLI search/detail output or WebSearch/WebFetch results.
|
||
2. **Respect deduplication.** Always check seen_jobs.json AND job_search_tracker.csv before presenting.
|
||
3. **Focus on configured geographic area.** Skip jobs that require relocation or are clearly outside commute range.
|
||
4. **Only open positions.** Skip postings with expired deadlines or those marked as closed.
|
||
5. **Be efficient with detail fetches.** Don't run `detail` or WebFetch on every search hit — pre-filter by title/snippet, then fetch only promising matches.
|
||
6. **Parallel searches.** Run portal CLI searches in parallel; use WebSearch only for gaps the CLIs don't cover.
|
||
7. **No automated people lookups.** Referral contacts (Step 4.5) are LinkedIn search links only - never fetch or scrape LinkedIn people-search result pages programmatically.
|