Metadata-Version: 2.4
Name: jobhunt-cli
Version: 1.0.0
Summary: Personal LinkedIn job discovery and tracking CLI
License-Expression: MIT
Project-URL: Homepage, https://github.com/yourusername/jobhunt
Project-URL: Issues, https://github.com/yourusername/jobhunt/issues
Keywords: linkedin,jobs,job-search,cli,career
Classifier: Programming Language :: Python :: 3
Classifier: Environment :: Console
Classifier: Topic :: Utilities
Classifier: Topic :: Office/Business
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: playwright>=1.43
Requires-Dist: click>=8.1
Requires-Dist: rich>=13.7
Requires-Dist: pyyaml>=6.0
Requires-Dist: openpyxl>=3.1
Requires-Dist: cryptography>=42
Provides-Extra: dev
Requires-Dist: pytest>=8.1; extra == "dev"
Requires-Dist: pytest-mock>=3.14; extra == "dev"
Dynamic: license-file

# jobhunt

Personal LinkedIn job discovery and tracking CLI. Playwright-based scraper with SQLite storage, multi-profile support, and aggregator filtering.

---

> **⚠️ LinkedIn ToS & Rate Limiting**
>
> This tool automates a real browser session on LinkedIn. Excessive or frequent use can trigger bot detection, CAPTCHA challenges, or temporary account restrictions.
>
> **Recommended usage:**
> - Run `discover` at most **once or twice per day** per profile
> - Avoid running `--all-profiles` across many profiles in rapid succession
> - If LinkedIn presents a CAPTCHA or checkpoint, stop and wait several hours before retrying
> - Using `--headless` with automation credentials increases detection risk — use sparingly

---

## Features

- **Automated discovery** — Playwright drives a real Chromium browser, extracts career-site URLs from each listing
- **Aggregator filtering** — two-pass filter skips job aggregators (Lensa, Dice, Wiraa, etc.) before visiting their pages
- **Multi-profile support** — separate search preferences for different job-search personas
- **Job lifecycle tracking** — `new → opened → applied / skipped`
- **Interactive review mode** — keyboard-driven triage: open in browser, skip, or move to next
- **Excel export** — one-command export with configurable output path
- **Headless mode** — run without a visible browser window using saved encrypted credentials
- **Proxy support** — route Playwright through an HTTP/SOCKS proxy

---

## Installation

**Prerequisites:** Python 3.10+, a LinkedIn account.

### From PyPI

```bash
pip install jobhunt-cli

# Install Playwright's Chromium browser
playwright install chromium

# Install bundled Claude Code skills (optional)
jobhunt skills install
```

### From source

```bash
git clone <repo-url>
cd jobhunt

python3 -m venv venv
source venv/bin/activate

pip install -e ".[dev]"
playwright install chromium
```

```bash
jobhunt --help
```

All user data is stored in `~/.config/jobhunt/`:
- `profiles/` — per-profile YAML files
- `jobhunt.db` — SQLite database
- `aggregators.yaml` — global aggregator blocklist
- `config.yaml` — default profile and export path settings
- `session/state.json` — saved LinkedIn browser session

---

## First Run

On the first command you run, jobhunt will automatically launch a setup wizard:

```bash
# Just run any command — the wizard starts automatically if no profile exists
jobhunt list
```

The wizard will ask for:
- A profile name (e.g. your first name)
- Your target job titles (one per line)
- Your search locations (one per line)

After setup, edit the profile to add your full details:
```bash
nano ~/.config/jobhunt/profiles/<your-name>.yaml
```

Then run discover — a browser window will open for you to log in manually:
```bash
jobhunt discover
```

---

## Commands

### Discovery

```bash
# Discover jobs for the active profile
jobhunt discover

# Run without a visible browser window (requires saved credentials — see Headless section)
jobhunt discover --headless

# Discover for every profile
jobhunt discover --all-profiles

# Override search parameters for this run only
jobhunt discover --location "Remote" --location "Austin, TX"
jobhunt discover --days 14
jobhunt discover --title "ML Engineer" --title "Backend Engineer"

# Narrow to a specific company
jobhunt discover --company Stripe --company Airbnb

# Quick test — 1 location, 1 page, 5 jobs (verify session and profile without a full run)
jobhunt discover --dry-run

# Verbose output (page navigations, SQL inserts)
jobhunt discover --verbose
```

Discovery summary: `3 new · 1 easy apply · 12 skipped (8 aggregators filtered)`

---

### Listing Jobs

```bash
# Show new and opened jobs (default)
jobhunt list

# Show all jobs including applied and skipped
jobhunt list --status all

# Show jobs for a specific profile
jobhunt --profile alice list
```

---

### Reviewing Jobs

```bash
# Interactive one-by-one review
jobhunt review
# Keys: [o]pen in browser   [s]kip   [n]ext   [q]uit
```

---

### Acting on Individual Jobs

```bash
# Open the career URL in your browser and mark the job as 'opened'
jobhunt open 42

# Mark a job as skipped (hidden from list and export)
jobhunt skip 42
```

---

### Exporting

```bash
# Export to ~/job_tracker.xlsx (default)
jobhunt export

# Export to a specific path
jobhunt export --output ~/Documents/jobs_march.xlsx

# Set a persistent default export path in ~/.config/jobhunt/config.yaml:
#   export_path: ~/Documents/jobs.xlsx
# Then just run:
jobhunt export
```

Skipped jobs are excluded. All other statuses (new, opened, applied) are included.

---

### Dashboard

```bash
# Show job counts by status for the active profile
jobhunt dashboard

# Show counts for a specific profile
jobhunt --profile alice dashboard
```

---

### Flushing the Database

```bash
# Delete all jobs for the active profile (prompts for confirmation)
jobhunt flush

# Delete jobs for a specific profile
jobhunt --profile alice flush

# Wipe all jobs across every profile
jobhunt flush --all-profiles
```

---

### Profiles

```bash
# List all profiles (* marks the default)
jobhunt profile list

# Create a new profile
jobhunt profile create alice

# Switch the default profile
jobhunt profile set-default alice

# Edit a profile in $EDITOR (defaults to nano)
jobhunt profile edit
jobhunt profile edit alice

# Delete a profile
jobhunt profile delete alice

# Save LinkedIn credentials for headless login (password is encrypted at rest)
jobhunt profile set-credentials

# Remove saved credentials
jobhunt profile clear-credentials

# Run any command as a specific profile
jobhunt --profile alice discover
jobhunt --profile alice list
```

**Profile resolution order:**
1. `--profile` flag
2. `default_profile` in `~/.config/jobhunt/config.yaml`
3. Error if no default is set but profiles exist — run `jobhunt profile set-default <name>`
4. First-run wizard if no profiles exist at all

#### Profile YAML structure

```yaml
personal:
  name: "Your Name"
  email: "you@example.com"
  phone: "+1-555-000-0000"
  linkedin_url: "https://linkedin.com/in/yourhandle"
  github_url: "https://github.com/yourhandle"
  location: "Austin, TX"
  linkedin_email: "you@example.com"       # optional: for headless auto-login
  linkedin_password_enc: "gAAA..."        # set via: jobhunt profile set-credentials

preferences:
  target_titles:
    - "Software Engineer"
    - "Backend Engineer"
  locations:
    - "Austin, TX"
    - "Remote"
  max_days_posted: 7
  min_salary: 140000        # set to null to disable
  blocked_domains: []       # profile-specific domain blocklist additions
  blocked_companies: []     # profile-specific company blocklist additions

work_history:
  - company: "Acme Corp"
    title: "Senior Engineer"
    start: "2021-06"
    end: null
    description: "..."
    tech: [Python, AWS]

skills: [Python, Java, AWS]
education:
  - degree: "B.S. Computer Science"
    school: "University of Texas at Austin"
    year: 2019
certifications:
  - "AWS Solutions Architect Associate"
resume_path: "/path/to/Resume.docx"
```

---

### Claude Code Skills

jobhunt ships bundled [Claude Code](https://claude.ai/code) skills for AI-assisted job hunting workflows.

```bash
# Install skills to ~/.claude/skills/
jobhunt skills install

# List available skills
jobhunt skills list

# Overwrite existing skill files
jobhunt skills install --force
```

Available skills: `jobhunt-discover`, `jobhunt-profile-creator`, `jobhunt-search-tuner`, `jobhunt-triage`, `jobhunt-track-application`, `identify-aggregators`.

---

### Aggregator Blocklist

Two-pass filter runs during `discover`:

- **Pass 1** (before page visit): company name checked against the blocklist
- **Pass 2** (after career URL extracted): career-site domain checked

```bash
# View the current blocklist
jobhunt aggregator list

# Global blocklist (affects all profiles)
jobhunt aggregator block-domain spamjobs.io
jobhunt aggregator unblock-domain spamjobs.io
jobhunt aggregator block-company "Jobs Via Dice"
jobhunt aggregator unblock-company "Jobs Via Dice"

# Profile-specific additions
jobhunt --profile alice aggregator block-domain foo.com
jobhunt --profile alice aggregator block-company "Some Staffing Co"
```

---

## Headless Mode

Run `discover` without a visible browser window — useful for scheduled/automated runs.

Requires saved credentials first:

```bash
# Save your LinkedIn email and password (password is Fernet-encrypted at rest)
jobhunt profile set-credentials

# Now run headless
jobhunt discover --headless
```

The encryption key lives at `~/.config/jobhunt/.key` (mode 600). If the key file is deleted, saved passwords are unrecoverable — re-run `set-credentials`.

> **⚠️** Headless mode is more likely to trigger LinkedIn bot detection than a normal browser session. Use sparingly.

---

## Proxy Support

Route Playwright through a proxy by adding a `proxy:` key to `~/.config/jobhunt/config.yaml`:

```yaml
default_profile: alice
proxy:
  server: "http://proxyhost:8080"
  username: "user"      # optional
  password: "pass"      # optional
```

Supports `http://`, `https://`, `socks4://`, and `socks5://` servers. Remove the `proxy:` key to disable.

---

## Job Statuses

| Status | Meaning |
|--------|---------|
| `new` | Discovered, not yet opened |
| `opened` | Career URL opened via `jobhunt open <id>` |
| `applied` | Applied — set manually |
| `skipped` | Dismissed — excluded from `list` and `export` |

To mark a job as applied:
```bash
sqlite3 ~/.config/jobhunt/jobhunt.db "UPDATE jobs SET status='applied' WHERE id=42;"
```

---

## config.yaml

`~/.config/jobhunt/config.yaml` controls global settings:

```yaml
default_profile: alice
export_path: ~/Documents/jobs.xlsx   # optional: persistent default export path
proxy:                               # optional: Playwright proxy
  server: "http://proxyhost:8080"
```

---

## File Structure

```
jobhunt/                              ← project root
├── pyproject.toml                    ← package definition + entry point
├── requirements.txt
├── src/jobhunt/
│   ├── cli.py                        ← Click commands (entry point)
│   ├── scraper.py                    ← Playwright scraper + session management
│   ├── scraper_utils.py              ← Pure parsing helpers
│   ├── filters.py                    ← Aggregator detection
│   ├── db.py                         ← SQLite layer
│   ├── exporter.py                   ← Excel export
│   ├── crypto.py                     ← Fernet encryption for credentials
│   ├── paths.py                      ← CONFIG_DIR constant
│   └── defaults/aggregators.yaml    ← Bundled default blocklist
└── tests/

~/.config/jobhunt/                    ← all runtime data
├── config.yaml
├── aggregators.yaml
├── .key                              ← Fernet encryption key (mode 600)
├── profiles/
│   └── <name>.yaml
├── jobhunt.db
└── session/state.json
```

---

## Running Tests

```bash
source venv/bin/activate
pytest tests/ -v
# 113 tests — no live LinkedIn connection required
```

---

## Troubleshooting

| Problem | Fix |
|---------|-----|
| Bot detection / CAPTCHA | Wait several hours; reduce run frequency |
| `open <id>` opens nothing | Easy Apply only job — no external career URL exists |
| Profile not found | Run `jobhunt profile list` to see available profiles |
| Session expired mid-run | Tool detects and prompts for manual re-login |
| Key file deleted | Re-run `jobhunt profile set-credentials` |
| openpyxl deprecation warnings in tests | Cosmetic; safe to ignore |
| Setup wizard cancelled | Re-run any command to restart it |
