diff --git a/.claude/skills/pupila-ai-review/SKILL.md b/.claude/skills/pupila-ai-review/SKILL.md index 93951df..829f177 100644 --- a/.claude/skills/pupila-ai-review/SKILL.md +++ b/.claude/skills/pupila-ai-review/SKILL.md @@ -2,10 +2,10 @@ name: pupila-ai-review description: How the AI per-job review pipeline works in this repo - generating verdicts via local LLM CLI, parsing markdown-fenced JSON, the candidate brief lever, and the AI Apply tailored-package flow. Use when modifying src/ai-review.ts, src/lib/ai-apply.ts, tuning verdict prompts, debugging review output, editing the candidate brief, or wiring a new LLM provider. metadata: - scope: pupila / job-hunt + scope: pupila --- -`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude` / `codex` / `gemini` / `opencode`, override `JOB_HUNT_LLM`). Uses the user's local subscription (e.g. Claude Max) — NOT an API key, so no per-token charges. +`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude` / `codex` / `gemini` / `opencode`, override `PUPILA_LLM`). Uses the user's local subscription (e.g. Claude Max) — NOT an API key, so no per-token charges. The launchd/cron review agent runs daily at 07:15 by default. Without an LLM CLI, run `scripts/install-launchd.sh --no-review` (or cron equivalent). @@ -54,7 +54,7 @@ Tests in `tests/ai-review-parse.test.ts` (9 cases) cover all the failure modes s ## The candidate brief -`config/candidate-brief.md` is the **only natural-language config in the repo**. Hand-edited, gitignored. Generated via `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). The CLI shells out to whichever local LLM CLI is installed (`claude` / `codex` / `gemini` / `opencode` — auto-detected; override via `JOB_HUNT_LLM=`). +`config/candidate-brief.md` is the **only natural-language config in the repo**. Hand-edited, gitignored. Generated via `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). The CLI shells out to whichever local LLM CLI is installed (`claude` / `codex` / `gemini` / `opencode` — auto-detected; override via `PUPILA_LLM=`). The brief is embedded **verbatim** in the review prompt — it's the main lever for tuning match/skip behaviour. To change verdicts at scale, edit the brief; to change individual scores, see the `pupila-filters` skill. diff --git a/.claude/skills/pupila-fetchers/SKILL.md b/.claude/skills/pupila-fetchers/SKILL.md index 0feddf7..31786dc 100644 --- a/.claude/skills/pupila-fetchers/SKILL.md +++ b/.claude/skills/pupila-fetchers/SKILL.md @@ -2,7 +2,7 @@ name: pupila-fetchers description: How to add a new job-source fetcher (ATS API, RSS, scraper) or extend tier-S slug lists in this repo. Use when adding a new job board, integrating a new ATS, scraping a new careers site, registering a new company under Ashby/Greenhouse/Lever, or diagnosing a fetcher that returned zero items. metadata: - scope: pupila / job-hunt + scope: pupila --- The pipeline ingests from 13 public sources (3 ATS APIs + RSS, JSON boards, HN, HTML scrapers, an Aave Next.js scraper, and `ashby-private` for orgs whose public posting-API is disabled). Adding a source means: a fetcher, a normalizer, a `Source` literal, a slot in the orchestrator, and dedup/render wiring. diff --git a/.claude/skills/pupila-filters/SKILL.md b/.claude/skills/pupila-filters/SKILL.md index 81d62ad..644fb58 100644 --- a/.claude/skills/pupila-filters/SKILL.md +++ b/.claude/skills/pupila-filters/SKILL.md @@ -2,7 +2,7 @@ name: pupila-filters description: How to tune job filter scoring, hard-drop rules, or debug why a specific job was kept/dropped via _signals. Use when adjusting weights in config/profile.json, adding a hard-exclude rule, tuning keyword lists, debugging fitScore, or interpreting the per-job _signals breakdown. metadata: - scope: pupila / job-hunt + scope: pupila --- All filter logic lives in `src/filters.ts`. Weights + keyword lists load from `config/profile.json` at runtime via `loadProfile()` (NOT a static import — the file is gitignored and auto-bootstrapped from `config/profile.default.json` on first run). Adjusting weights or keywords is a **non-code edit** to `profile.json`. diff --git a/AGENTS.md b/AGENTS.md index b183457..ec5d0d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Guidance for future Codex sessions working in this repo. The repo ships a **neutral template** in `config/profile.json`. After onboarding (CV upload → brief generation), `/api/profile-generate` shells out to the local LLM CLI to fill in the personal keyword lists + weights based on the brief. Re-runnable from Settings → Scoring profile → Regenerate. `config/slugs.json` ships with the full ~50-company tier-S list (all public ATS URLs — non-personal data, edit by hand to add/remove companies). -**First-run UX**: a forker generates their `config/candidate-brief.md` by running `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). That CLI shells out to whichever local LLM CLI is installed (`Codex`, `codex`, `gemini`, `opencode` — auto-detected, override via `JOB_HUNT_LLM=`). +**First-run UX**: a forker generates their `config/candidate-brief.md` by running `pnpm run setup-brief --file ~/cv.pdf` (or via the UI's Profile tab → drop a PDF/DOCX/MD CV). That CLI shells out to whichever local LLM CLI is installed (`Codex`, `codex`, `gemini`, `opencode` — auto-detected, override via `PUPILA_LLM=`). ## Stack @@ -279,7 +279,7 @@ The read happens **after** filter+dedup+sort but **before** `writeJson('data/job ## RSS feed -[`src/feed.ts`](./src/feed.ts) emits a hand-rolled RSS 2.0 XML to `data/feed.xml` containing the top 50 `newJobs` by `fitScore`. The XML is hand-built (not via fast-xml-parser) because we control the content shape — `escapeXml` covers the five entity classes. The feed metadata (title, description, link) is overridable via `JOB_HUNT_FEED_TITLE` / `JOB_HUNT_FEED_DESC` / `JOB_HUNT_FEED_LINK` env vars. To subscribe locally, point your RSS reader at the `file://` path of `data/feed.xml`. (No remote URL anymore — the project is local-first.) +[`src/feed.ts`](./src/feed.ts) emits a hand-rolled RSS 2.0 XML to `data/feed.xml` containing the top 50 `newJobs` by `fitScore`. The XML is hand-built (not via fast-xml-parser) because we control the content shape — `escapeXml` covers the five entity classes. The feed metadata (title, description, link) is overridable via `PUPILA_FEED_TITLE` / `PUPILA_FEED_DESC` / `PUPILA_FEED_LINK` env vars. To subscribe locally, point your RSS reader at the `file://` path of `data/feed.xml`. (No remote URL anymore — the project is local-first.) ## Salary parsing @@ -345,7 +345,7 @@ The HTML has `` as belt-and-suspe ## AI per-job review (`pnpm run ai-review`) -[`src/ai-review.ts`](./src/ai-review.ts) is a **local-only** companion to the daily pipeline that augments selected jobs with an LLM review. It shells out via `src/lib/llm.ts` (auto-detects `Codex` / `codex` / `gemini` / `opencode` on PATH, override with `JOB_HUNT_LLM=`). The CLI uses the user's local subscription (e.g. Codex Max) — **not** an API key, so there are no per-token charges. With the new local-first scheduling, the launchd/cron review agent runs this every day at 07:15 by default. If you don't have an LLM CLI installed, run `scripts/install-launchd.sh --no-review` (or the cron equivalent) to skip the review step. +[`src/ai-review.ts`](./src/ai-review.ts) is a **local-only** companion to the daily pipeline that augments selected jobs with an LLM review. It shells out via `src/lib/llm.ts` (auto-detects `Codex` / `codex` / `gemini` / `opencode` on PATH, override with `PUPILA_LLM=`). The CLI uses the user's local subscription (e.g. Codex Max) — **not** an API key, so there are no per-token charges. With the new local-first scheduling, the launchd/cron review agent runs this every day at 07:15 by default. If you don't have an LLM CLI installed, run `scripts/install-launchd.sh --no-review` (or the cron equivalent) to skip the review step. **Inputs:** - `data/jobs.json` — the slim list (committed) diff --git a/CLAUDE.md b/CLAUDE.md index 8d43f4b..5ab2627 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,8 +20,8 @@ Guidance for future Claude Code sessions working in this repo. **Slim by design* Cross-cutting invariants (apply repo-wide): - **`config/profile.json` is gitignored** — encodes personal scoring preferences. Auto-bootstraps from committed `config/profile.default.json` on first `pnpm run dev` / `pnpm run ui` via `bootstrapProfileIfMissing()` (idempotent — `COPYFILE_EXCL` no-ops on the steady state). Don't bypass; don't commit personalized weights. -- **Mandatory CV gate**: `pnpm run dev` checks for `config/candidate-brief.md` at startup and exits 1 if missing. Bypass with `JOB_HUNT_NO_BRIEF_CHECK=1` or `--no-brief-check`. -- **`config/candidate-brief.md` is the only natural-language config** (gitignored). Generated via `pnpm run setup-brief --file ~/cv.pdf` or via the UI's Profile tab (drop PDF/DOCX/MD CV). CLI shells out to `claude`/`codex`/`gemini`/`opencode` — auto-detected, override `JOB_HUNT_LLM=`. +- **Mandatory CV gate**: `pnpm run dev` checks for `config/candidate-brief.md` at startup and exits 1 if missing. Bypass with `PUPILA_NO_BRIEF_CHECK=1` or `--no-brief-check`. +- **`config/candidate-brief.md` is the only natural-language config** (gitignored). Generated via `pnpm run setup-brief --file ~/cv.pdf` or via the UI's Profile tab (drop PDF/DOCX/MD CV). CLI shells out to `claude`/`codex`/`gemini`/`opencode` — auto-detected, override `PUPILA_LLM=`. - **Local-first scheduling**: daily aggregation runs via `scripts/install-launchd.sh` (macOS) or `scripts/install-cron.sh` (Linux), not GitHub Actions cron. CI runs only on push/PR for gates. - **`data/applied.json` source of truth** for application tracking (UI writes via Vite middleware). Commit manually to persist across machines. **Don't filter applied jobs out of the main list** — user explicitly wants them visible. @@ -79,7 +79,7 @@ pnpm run mcp # MCP server over stdio `pnpm run dev` → `tsx src/index.ts` is the main pipeline (what launchd/cron runs). Steps: -1. **CV gate** — fail-fast if `config/candidate-brief.md` missing (bypass: `JOB_HUNT_NO_BRIEF_CHECK=1` or `--no-brief-check`). +1. **CV gate** — fail-fast if `config/candidate-brief.md` missing (bypass: `PUPILA_NO_BRIEF_CHECK=1` or `--no-brief-check`). 2. **Profile bootstrap** — `bootstrapProfileIfMissing()` copies `config/profile.default.json` → `profile.json` on first run. 3. **Fetch** — all 13 sources in parallel via `processFetcher()` + `Promise.all`. Each fetcher returns `{ items, errors }` and **never throws** (a rejection would kill the whole run). 4. **Normalize** — per-source `normalize()` → `Job[]`. Salary fields populated via `withSalary()` spread. @@ -134,7 +134,7 @@ Settings tab (eight panels), Jinder (swipe-to-apply queue), AI Apply (per-job ta ## AI per-job review -`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude`/`codex`/`gemini`/`opencode`, override `JOB_HUNT_LLM`). Uses the local subscription — **not** an API key, so no per-token charges. Output: `data/ai-reviews.json`. +`pnpm run ai-review` is a **local-only** companion that augments selected jobs with an LLM review via `src/lib/llm.ts` (auto-detects `claude`/`codex`/`gemini`/`opencode`, override `PUPILA_LLM`). Uses the local subscription — **not** an API key, so no per-token charges. Output: `data/ai-reviews.json`. Daily workflow: diff --git a/README.md b/README.md index 0621fbc..c075ef7 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ The repo ships with neutral defaults. To make it yours: ### 1. Clone or fork ```bash -gh repo fork FranRom/job-hunt --clone -cd job-hunt +gh repo fork FranRom/pupila --clone +cd pupila pnpm install ``` @@ -104,7 +104,7 @@ Or click "Fork" on GitHub, then `git clone `. ### 2. Generate your candidate brief (required) -The brief at `config/candidate-brief.md` is the natural-language description of who you are, what you want, and what to avoid. **This step is mandatory** — `pnpm run dev` will refuse to start until the file exists. (Bypass with `JOB_HUNT_NO_BRIEF_CHECK=1` if you genuinely want raw aggregation with no AI review.) +The brief at `config/candidate-brief.md` is the natural-language description of who you are, what you want, and what to avoid. **This step is mandatory** — `pnpm run dev` will refuse to start until the file exists. (Bypass with `PUPILA_NO_BRIEF_CHECK=1` if you genuinely want raw aggregation with no AI review.) **The file is gitignored** — it contains CV-derived personal information and should never be committed. setup-brief and the onboarding wizard write to the gitignored canonical path. @@ -125,7 +125,7 @@ Or open the UI and use the Profile tab: pnpm run ui # http://127.0.0.1:5173 → Profile tab → drop your CV ``` -The auto-detected provider order is `claude` → `codex` → `gemini` → `opencode` (whichever is on `PATH` first). Override with `JOB_HUNT_LLM=codex pnpm run setup-brief ...`. No API keys; uses your existing CLI subscription. +The auto-detected provider order is `claude` → `codex` → `gemini` → `opencode` (whichever is on `PATH` first). Override with `PUPILA_LLM=codex pnpm run setup-brief ...`. No API keys; uses your existing CLI subscription. > **The two personalization layers, briefly:** > - `config/profile.json` (committed defaults) controls **what gets fetched + scored** (weights, keyword lists, tier-S slugs). @@ -165,8 +165,8 @@ Two agents run on independent schedules so you can tune them separately: ./scripts/install-launchd.sh --review-time 09:00 ./scripts/install-launchd.sh --no-review # aggregator only (no LLM CLI) ./scripts/install-launchd.sh --uninstall # remove both -launchctl list | grep job-hunt # check status -launchctl start dev.${USER}.job-hunt.aggregate # trigger now +launchctl list | grep pupila # check status +launchctl start dev.${USER}.pupila.aggregate # trigger now ``` launchd's `StartCalendarInterval` catches up missed runs after wake — if your laptop was asleep at 7am, it runs once the lid opens. @@ -548,7 +548,7 @@ The Jobs filter bar adds two unified-skip / unified-queue controls: [`src/ai-review.ts`](./src/ai-review.ts) is an **optional, local-only** companion that adds an LLM "second opinion" to selected jobs. Each job gets a structured review — summary, what they want, what they offer, red flags, and a verdict (`strong-match | match | weak-match | skip`) — so you can scan the day's matches in seconds instead of reading every posting. -It shells out through [`src/lib/llm.ts`](./src/lib/llm.ts) to whichever local LLM CLI is available (`claude`, `codex`, `gemini`, or `opencode`; override with `JOB_HUNT_LLM=`). There are no project API keys and no per-token billing from this repo, but this cannot run in CI because the workflow runner is not authenticated as your local CLI user. Run it locally after the daily pipeline. +It shells out through [`src/lib/llm.ts`](./src/lib/llm.ts) to whichever local LLM CLI is available (`claude`, `codex`, `gemini`, or `opencode`; override with `PUPILA_LLM=`). There are no project API keys and no per-token billing from this repo, but this cannot run in CI because the workflow runner is not authenticated as your local CLI user. Run it locally after the daily pipeline. ### One-time setup @@ -733,7 +733,7 @@ Keyword arrays are joined with `|` and compiled into word-bounded, case-insensit Click to expand the full tree ``` -job-hunt/ +pupila/ ├── .github/ │ ├── workflows/ │ │ └── check.yml # PR/push: biome + typecheck + tests + build + audit @@ -831,7 +831,7 @@ pnpm run clean:onboarding # wipe only the onboarding state (preferences + bri pnpm run clean -- --all # full fresh-clone reset (see "Reset to a clean slate" below) ``` -> **Heads up:** `pnpm run dev` refuses to start unless `config/candidate-brief.md` exists — set up your candidate brief first via `pnpm run setup-brief` or the UI Profile tab. Bypass with `JOB_HUNT_NO_BRIEF_CHECK=1` (or `--no-brief-check`) for raw aggregation without AI review. +> **Heads up:** `pnpm run dev` refuses to start unless `config/candidate-brief.md` exists — set up your candidate brief first via `pnpm run setup-brief` or the UI Profile tab. Bypass with `PUPILA_NO_BRIEF_CHECK=1` (or `--no-brief-check`) for raw aggregation without AI review. The pre-commit hook runs `lint && typecheck` on every commit. To bypass it for an emergency commit: `SKIP_SIMPLE_GIT_HOOKS=1 git commit ...`. diff --git a/package.json b/package.json index 6b86f9c..f6d94dd 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "job-hunt", + "name": "pupila", "version": "0.1.0", "private": true, "type": "module", diff --git a/scripts/install-cron.sh b/scripts/install-cron.sh index 4ad06d8..585f93f 100755 --- a/scripts/install-cron.sh +++ b/scripts/install-cron.sh @@ -19,8 +19,8 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -TAG_AGG="# job-hunt:aggregate:${REPO_ROOT}" -TAG_REV="# job-hunt:review:${REPO_ROOT}" +TAG_AGG="# pupila:aggregate:${REPO_ROOT}" +TAG_REV="# pupila:review:${REPO_ROOT}" AGG_TIME="07:00" REV_TIME="07:15" @@ -72,14 +72,14 @@ if [[ -z "$PNPM" ]]; then exit 1 fi -# Read current crontab (no error if empty), strip any existing job-hunt +# Read current crontab (no error if empty), strip any existing pupila # entries for this repo so re-running is idempotent. CURRENT="$(crontab -l 2>/dev/null || true)" CLEANED="$(printf '%s\n' "$CURRENT" | grep -vF "$TAG_AGG" | grep -vF "$TAG_REV" || true)" if [[ "$UNINSTALL" == "1" ]]; then printf '%s\n' "$CLEANED" | crontab - - echo "✓ Removed job-hunt cron entries for $REPO_ROOT" + echo "✓ Removed pupila cron entries for $REPO_ROOT" exit 0 fi diff --git a/scripts/install-launchd.sh b/scripts/install-launchd.sh index 49f16e6..dbe3f69 100755 --- a/scripts/install-launchd.sh +++ b/scripts/install-launchd.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # Install two launchd agents on macOS: # -# 1. dev.${USER}.job-hunt.aggregate — runs `pnpm run dev` (fetch + filter +# 1. dev.${USER}.pupila.aggregate — runs `pnpm run dev` (fetch + filter # + score + write data/jobs.json + JOBS.md + feed.xml). No LLM needed. # -# 2. dev.${USER}.job-hunt.review — runs `pnpm run ai-review` (per-job +# 2. dev.${USER}.pupila.review — runs `pnpm run ai-review` (per-job # LLM verdict via your local CLI: claude / codex / gemini / opencode). # Skipped via --no-review for users without an LLM CLI installed. # @@ -20,7 +20,7 @@ set -euo pipefail -LABEL_BASE="dev.${USER}.job-hunt" +LABEL_BASE="dev.${USER}.pupila" LAUNCH_DIR="$HOME/Library/LaunchAgents" AGG_LABEL="${LABEL_BASE}.aggregate" REV_LABEL="${LABEL_BASE}.review" @@ -172,7 +172,7 @@ Trigger now: launchctl start ${REV_LABEL} Status: - launchctl list | grep job-hunt + launchctl list | grep pupila Uninstall: $0 --uninstall diff --git a/scripts/install-mcp.sh b/scripts/install-mcp.sh index d746473..ceb3234 100644 --- a/scripts/install-mcp.sh +++ b/scripts/install-mcp.sh @@ -4,13 +4,13 @@ # the client config(s). # # Usage: -# curl -sSf https://raw.githubusercontent.com/ogarciarevett/job-hunt/main/scripts/install-mcp.sh | bash +# curl -sSf https://raw.githubusercontent.com/FranRom/pupila/main/scripts/install-mcp.sh | bash # # or, if you've cloned the repo already: # bash scripts/install-mcp.sh # # Env overrides: # PUPILA_HOME - install location (default: $HOME/.pupila) -# PUPILA_REPO - git URL (default: https://github.com/ogarciarevett/job-hunt.git) +# PUPILA_REPO - git URL (default: https://github.com/FranRom/pupila.git) # PUPILA_REF - branch/tag/commit to checkout (default: main) # PUPILA_DRY_RUN - if set to 1, print intended actions and exit # @@ -25,7 +25,7 @@ set -euo pipefail PUPILA_HOME="${PUPILA_HOME:-$HOME/.pupila}" -PUPILA_REPO="${PUPILA_REPO:-https://github.com/ogarciarevett/job-hunt.git}" +PUPILA_REPO="${PUPILA_REPO:-https://github.com/FranRom/pupila.git}" PUPILA_REF="${PUPILA_REF:-main}" PUPILA_DRY_RUN="${PUPILA_DRY_RUN:-0}" diff --git a/scripts/uninstall-legacy-job-hunt.sh b/scripts/uninstall-legacy-job-hunt.sh new file mode 100755 index 0000000..d1c73cd --- /dev/null +++ b/scripts/uninstall-legacy-job-hunt.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# One-shot cleanup of legacy `job-hunt`-tagged launchd/cron entries left over +# from before the pupila rename. Safe to run multiple times. +# +# Removes ONLY entries whose tag/label matches: +# - dev.$USER.job-hunt.aggregate +# - dev.$USER.job-hunt.review +# - # job-hunt:aggregate: +# - # job-hunt:review: +# +# Anchored patterns — never bulk-deletes anything containing "job-hunt" as +# substring elsewhere in user crontab or LaunchAgents. + +set -euo pipefail + +REPO_ROOT="${REPO_ROOT:-$(pwd)}" +removed=0 + +uname_s="$(uname -s)" + +cleanup_launchd() { + local agg_label="dev.${USER}.job-hunt.aggregate" + local rev_label="dev.${USER}.job-hunt.review" + local agg_plist="$HOME/Library/LaunchAgents/${agg_label}.plist" + local rev_plist="$HOME/Library/LaunchAgents/${rev_label}.plist" + + for label in "$agg_label" "$rev_label"; do + if launchctl list | awk '{print $3}' | grep -Fxq "$label"; then + launchctl bootout "gui/$(id -u)" "$HOME/Library/LaunchAgents/${label}.plist" 2>/dev/null || \ + launchctl unload "$HOME/Library/LaunchAgents/${label}.plist" 2>/dev/null || true + echo "✓ Unloaded legacy launchd agent: $label" + removed=$((removed+1)) + fi + done + + for plist in "$agg_plist" "$rev_plist"; do + if [ -f "$plist" ]; then + rm -f "$plist" + echo "✓ Removed legacy plist: $plist" + removed=$((removed+1)) + fi + done +} + +cleanup_cron() { + local agg_tag="# job-hunt:aggregate:${REPO_ROOT}" + local rev_tag="# job-hunt:review:${REPO_ROOT}" + + if ! command -v crontab >/dev/null 2>&1; then + echo "ℹ︎ crontab not found; skipping cron cleanup" + return + fi + + local current + current="$(crontab -l 2>/dev/null || true)" + if [ -z "$current" ]; then + return + fi + + # Strip any cron line whose inline tag matches our agg/rev legacy tag. + local filtered + filtered="$(printf '%s\n' "$current" | grep -vF "$agg_tag" | grep -vF "$rev_tag" || true)" + + if [ "$current" != "$filtered" ]; then + printf '%s\n' "$filtered" | crontab - + echo "✓ Removed legacy cron entries for $REPO_ROOT" + removed=$((removed+1)) + fi +} + +case "$uname_s" in + Darwin) cleanup_launchd; cleanup_cron ;; + Linux) cleanup_cron ;; + *) echo "ℹ︎ Unsupported OS: $uname_s — only Darwin/Linux supported"; exit 0 ;; +esac + +if [ "$removed" -eq 0 ]; then + echo "Nothing to clean — no legacy job-hunt entries found." +else + echo "Done. Removed $removed legacy entries." +fi diff --git a/src/ai-review.ts b/src/ai-review.ts index 7bac1a1..b1f20c6 100644 --- a/src/ai-review.ts +++ b/src/ai-review.ts @@ -168,7 +168,7 @@ async function main(): Promise { const review: AiReview = { jobId: job.id, reviewedAt: new Date().toISOString(), - model: process.env.JOB_HUNT_LLM ?? 'claude', + model: process.env.PUPILA_LLM ?? 'claude', ...parsed, }; reviews[job.id] = review; diff --git a/src/feed.ts b/src/feed.ts index 9039d7c..11a3221 100644 --- a/src/feed.ts +++ b/src/feed.ts @@ -1,10 +1,10 @@ import type { Job } from './types.js'; -// Generic feed metadata. Forks can override via the JOB_HUNT_FEED_* env vars +// Generic feed metadata. Forks can override via the PUPILA_FEED_* env vars // without touching code (handy when self-hosting under a different repo URL). -const FEED_TITLE = process.env.JOB_HUNT_FEED_TITLE ?? 'pupila — new matches'; -const FEED_DESC = process.env.JOB_HUNT_FEED_DESC ?? 'Daily job matches new since the last run.'; -const FEED_LINK = process.env.JOB_HUNT_FEED_LINK ?? 'JOBS.md'; +const FEED_TITLE = process.env.PUPILA_FEED_TITLE ?? 'pupila — new matches'; +const FEED_DESC = process.env.PUPILA_FEED_DESC ?? 'Daily job matches new since the last run.'; +const FEED_LINK = process.env.PUPILA_FEED_LINK ?? 'JOBS.md'; function escapeXml(s: string): string { return s diff --git a/src/index.ts b/src/index.ts index edae16f..34fb3c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { fetchRemotive } from './fetchers/remotive.js'; import { fetchWeb3Career } from './fetchers/web3career.js'; import { fetchWeWorkRemotely } from './fetchers/weworkremotely.js'; import { BOILERPLATE_HEADERS_RE, createFilters, loadProfile } from './filters.js'; +import { detectLegacyEnvVars } from './legacy-env.js'; import { bootstrapProfileIfMissing } from './lib/profile-bootstrap.js'; import { normalizeAave, @@ -99,7 +100,7 @@ const PROFILE_PATH = 'config/profile.json'; function ensureCandidateBrief(): void { if (existsSync(BRIEF_PATH)) return; - if (process.env.JOB_HUNT_NO_BRIEF_CHECK === '1') return; + if (process.env.PUPILA_NO_BRIEF_CHECK === '1') return; if (process.argv.includes('--no-brief-check')) return; console.error(` ✗ ${BRIEF_PATH} not found. @@ -111,7 +112,7 @@ The aggregator expects you to set up your candidate profile first: pnpm run ui To skip this check (raw aggregation only, no AI review): - JOB_HUNT_NO_BRIEF_CHECK=1 pnpm run dev + PUPILA_NO_BRIEF_CHECK=1 pnpm run dev `); process.exit(1); } @@ -126,6 +127,18 @@ async function ensureProfile(): Promise { } async function main(): Promise { + const legacy = detectLegacyEnvVars(process.env); + if (legacy.length > 0) { + console.error('❌ Legacy JOB_HUNT_* environment variables detected:'); + for (const { old, replacement } of legacy) { + console.error(` ${old} → rename to ${replacement}`); + } + console.error('\nThe project was renamed from job-hunt to pupila.'); + console.error( + 'Update your shell config (e.g. ~/.zshrc) and re-source it, or unset the old names.', + ); + process.exit(1); + } ensureCandidateBrief(); await ensureProfile(); diff --git a/src/legacy-env.ts b/src/legacy-env.ts new file mode 100644 index 0000000..2e2ceb1 --- /dev/null +++ b/src/legacy-env.ts @@ -0,0 +1,19 @@ +const RENAMES: ReadonlyArray<[string, string]> = [ + ['JOB_HUNT_LLM', 'PUPILA_LLM'], + ['JOB_HUNT_LLM_FLAG', 'PUPILA_LLM_FLAG'], + ['JOB_HUNT_LLM_TIMEOUT_MS', 'PUPILA_LLM_TIMEOUT_MS'], + ['JOB_HUNT_CV_MAX_CHARS', 'PUPILA_CV_MAX_CHARS'], + ['JOB_HUNT_NO_BRIEF_CHECK', 'PUPILA_NO_BRIEF_CHECK'], + ['JOB_HUNT_FEED_TITLE', 'PUPILA_FEED_TITLE'], + ['JOB_HUNT_FEED_DESC', 'PUPILA_FEED_DESC'], + ['JOB_HUNT_FEED_LINK', 'PUPILA_FEED_LINK'], +]; + +export function detectLegacyEnvVars( + env: NodeJS.ProcessEnv, +): Array<{ old: string; replacement: string }> { + return RENAMES.filter(([oldName]) => env[oldName] !== undefined).map(([old, replacement]) => ({ + old, + replacement, + })); +} diff --git a/src/lib/ai-apply.ts b/src/lib/ai-apply.ts index d994eb4..8f3dcb9 100644 --- a/src/lib/ai-apply.ts +++ b/src/lib/ai-apply.ts @@ -20,8 +20,8 @@ import { detectLlmCli, type LlmProvider } from './llm.js'; // --------------------------------------------------------------------------- // How many chars of the parsed CV we send to the LLM. Configurable via -// JOB_HUNT_CV_MAX_CHARS for users hitting OOM kills on large CVs. -export const CV_MAX_CHARS = Number(process.env.JOB_HUNT_CV_MAX_CHARS ?? '12000'); +// PUPILA_CV_MAX_CHARS for users hitting OOM kills on large CVs. +export const CV_MAX_CHARS = Number(process.env.PUPILA_CV_MAX_CHARS ?? '12000'); // This file lives at src/lib/ai-apply.ts, so ../../ resolves to repo root. const DEFAULT_REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url)); diff --git a/src/lib/llm.ts b/src/lib/llm.ts index 777bbe4..51031da 100644 --- a/src/lib/llm.ts +++ b/src/lib/llm.ts @@ -4,11 +4,11 @@ // user's existing CLI subscription. // // Detection order: -// 1. JOB_HUNT_LLM env var (claude | codex | gemini | opencode) +// 1. PUPILA_LLM env var (claude | codex | gemini | opencode) // 2. First found on PATH in the order: claude > codex > gemini > opencode // -// Override the exact CLI invocation per provider via `JOB_HUNT_LLM_FLAG` -// (e.g. `JOB_HUNT_LLM_FLAG=--prompt`) if a CLI's flag syntax changes upstream. +// Override the exact CLI invocation per provider via `PUPILA_LLM_FLAG` +// (e.g. `PUPILA_LLM_FLAG=--prompt`) if a CLI's flag syntax changes upstream. // // Prompt delivery: we feed the prompt via STDIN, not argv. Three reasons: // 1. argv has a kernel-imposed size limit (ARG_MAX, ~1MB on macOS) and @@ -195,7 +195,7 @@ export async function availableProviders(): Promise } function buildSpec(provider: LlmProvider): ProviderSpec { - const flagOverride = process.env.JOB_HUNT_LLM_FLAG; + const flagOverride = process.env.PUPILA_LLM_FLAG; if (flagOverride) { return { args: [flagOverride] }; } @@ -203,21 +203,21 @@ function buildSpec(provider: LlmProvider): ProviderSpec { } /** - * Resolve the LLM CLI to use, either from `JOB_HUNT_LLM` env var or by + * Resolve the LLM CLI to use, either from `PUPILA_LLM` env var or by * detecting which one is installed. Throws with a helpful message if none * are available. */ export async function detectLlmCli(override?: LlmProvider): Promise { - const requested = override ?? process.env.JOB_HUNT_LLM; + const requested = override ?? process.env.PUPILA_LLM; if (requested) { if (!isSupportedProvider(requested)) { throw new Error( - `JOB_HUNT_LLM="${requested}" is not supported. Use one of: ${SUPPORTED_PROVIDERS.join(', ')}.`, + `PUPILA_LLM="${requested}" is not supported. Use one of: ${SUPPORTED_PROVIDERS.join(', ')}.`, ); } if (!(await commandExists(requested))) { throw new Error( - `JOB_HUNT_LLM="${requested}" was requested but the \`${requested}\` CLI is not on PATH.`, + `PUPILA_LLM="${requested}" was requested but the \`${requested}\` CLI is not on PATH.`, ); } return { provider: requested, cmd: requested, argTemplate: buildSpec(requested).args }; @@ -233,7 +233,7 @@ export async function detectLlmCli(override?: LlmProvider): Promise.`, + `${cmd} timed out after ${Math.round(RUN_TIMEOUT_MS / 1000)}s. Override with PUPILA_LLM_TIMEOUT_MS=.`, ), ); }, RUN_TIMEOUT_MS); @@ -397,9 +397,9 @@ export async function runLlm( lines.push(''); lines.push('OR use a different LLM CLI for this run:'); lines.push(''); - lines.push(' JOB_HUNT_LLM=codex pnpm run ui # if you have codex CLI'); - lines.push(' JOB_HUNT_LLM=gemini pnpm run ui # if you have gemini-cli'); - lines.push(' JOB_HUNT_LLM=opencode pnpm run ui # if you have opencode'); + lines.push(' PUPILA_LLM=codex pnpm run ui # if you have codex CLI'); + lines.push(' PUPILA_LLM=gemini pnpm run ui # if you have gemini-cli'); + lines.push(' PUPILA_LLM=opencode pnpm run ui # if you have opencode'); lines.push('========================================================================='); lines.push(''); } @@ -432,12 +432,12 @@ export async function runLlm( lines.push('Most likely cause: out-of-memory while processing your prompt.'); lines.push('Try (in order of effort):'); lines.push( - ` 1. Shrink the input. Lower JOB_HUNT_CV_MAX_CHARS (current default 12000) — try 6000 or 4000.`, + ` 1. Shrink the input. Lower PUPILA_CV_MAX_CHARS (current default 12000) — try 6000 or 4000.`, ); lines.push( ` 2. Close memory-heavy apps (other Node servers, browsers with many tabs, Docker).`, ); - lines.push(` 3. Switch provider for one run: JOB_HUNT_LLM=codex pnpm run ui`); + lines.push(` 3. Switch provider for one run: PUPILA_LLM=codex pnpm run ui`); lines.push( ` 4. Run the same prompt outside the dev server: cat /tmp/prompt.txt | ${invocation.cmd} ${invocation.argTemplate.join(' ')}`, ); @@ -462,7 +462,7 @@ export async function runLlm( lines.push(` Try running \`${invocation.cmd} --version\` directly in the same terminal.`); lines.push(` 3. macOS Memory Pressure Killer / sandbox kill. Check Console.app for entries`); lines.push(` with subsystem "com.apple.kernel" around the time of the kill.`); - lines.push(` 4. Switch provider: JOB_HUNT_LLM=codex pnpm run ui`); + lines.push(` 4. Switch provider: PUPILA_LLM=codex pnpm run ui`); } } diff --git a/src/render.ts b/src/render.ts index 8d55a2c..665d2f8 100644 --- a/src/render.ts +++ b/src/render.ts @@ -157,7 +157,7 @@ export function renderReadme( return `# Daily job matches -Auto-generated by the [job-hunt](./README.md) pipeline. Do not edit by hand — this file is overwritten on every run. +Auto-generated by the [pupila](./README.md) pipeline. Do not edit by hand — this file is overwritten on every run. > **Tip:** GitHub strips \`target="_blank"\` when rendering markdown, so apply links open in the same tab. Use **⌘+click** (Mac) / **Ctrl+click** (Win/Linux) / **middle-click** to open in a new tab. diff --git a/src/setup-brief.ts b/src/setup-brief.ts index 6596d79..70637b9 100644 --- a/src/setup-brief.ts +++ b/src/setup-brief.ts @@ -12,7 +12,7 @@ // cat cv.txt | pnpm run setup-brief # stdin // // Provider: auto-detects claude / codex / gemini / opencode on PATH (in that -// order). Override with JOB_HUNT_LLM=. +// order). Override with PUPILA_LLM=. import { existsSync } from 'node:fs'; import { copyFile } from 'node:fs/promises'; @@ -21,8 +21,8 @@ import { detectFormat, parseCvFile } from './lib/cv-parser.js'; import { detectLlmCli, runLlm } from './lib/llm.js'; // How many chars of the parsed CV we send to the LLM. Configurable via -// JOB_HUNT_CV_MAX_CHARS for users hitting OOM kills on large CVs. -const MAX_CV_CHARS = Number(process.env.JOB_HUNT_CV_MAX_CHARS ?? '12000'); +// PUPILA_CV_MAX_CHARS for users hitting OOM kills on large CVs. +const MAX_CV_CHARS = Number(process.env.PUPILA_CV_MAX_CHARS ?? '12000'); const CV_DEST_BASENAME = 'config/cv'; interface CliArgs { @@ -92,7 +92,7 @@ async function main(): Promise { console.log(' cat cv.txt | pnpm run setup-brief'); console.log(''); console.log('Provider: auto-detects claude/codex/gemini/opencode on PATH.'); - console.log('Override with JOB_HUNT_LLM=.'); + console.log('Override with PUPILA_LLM=.'); return; } diff --git a/src/utils.ts b/src/utils.ts index 0ab32fb..57a8322 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -237,7 +237,7 @@ export function uniq(items: T[]): T[] { } export const DEFAULT_USER_AGENT = - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 job-hunt-aggregator/0.1'; + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 pupila/0.1'; export const RSS_HEADERS: Record = { 'User-Agent': DEFAULT_USER_AGENT, diff --git a/tests/legacy-env-detector.test.ts b/tests/legacy-env-detector.test.ts new file mode 100644 index 0000000..4dc4345 --- /dev/null +++ b/tests/legacy-env-detector.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { detectLegacyEnvVars } from '../src/legacy-env.js'; + +describe('detectLegacyEnvVars', () => { + it('returns empty when no JOB_HUNT_* env vars set', () => { + expect(detectLegacyEnvVars({ HOME: '/x', PATH: '/y' })).toEqual([]); + }); + + it('detects all known legacy env vars', () => { + const env = { + JOB_HUNT_LLM: 'claude', + JOB_HUNT_NO_BRIEF_CHECK: '1', + JOB_HUNT_CV_MAX_CHARS: '20000', + UNRELATED: 'x', + }; + const result = detectLegacyEnvVars(env); + expect(result).toEqual( + expect.arrayContaining([ + { old: 'JOB_HUNT_LLM', replacement: 'PUPILA_LLM' }, + { old: 'JOB_HUNT_NO_BRIEF_CHECK', replacement: 'PUPILA_NO_BRIEF_CHECK' }, + { old: 'JOB_HUNT_CV_MAX_CHARS', replacement: 'PUPILA_CV_MAX_CHARS' }, + ]), + ); + expect(result).toHaveLength(3); + }); + + it('ignores unrelated env vars', () => { + expect(detectLegacyEnvVars({ JOBS_PATH: '/x' })).toEqual([]); + }); +}); diff --git a/ui/plugins/_shared.ts b/ui/plugins/_shared.ts index 06bcf93..8b0e71d 100644 --- a/ui/plugins/_shared.ts +++ b/ui/plugins/_shared.ts @@ -12,8 +12,8 @@ import { APPLIED_PATH, CV_BASENAME, PREFERENCES_PATH } from './_paths.ts'; export type { LlmProvider }; // How many chars of the parsed CV we send to the LLM. Configurable via -// JOB_HUNT_CV_MAX_CHARS for users hitting OOM kills on large CVs. -export const CV_MAX_CHARS = Number(process.env.JOB_HUNT_CV_MAX_CHARS ?? '12000'); +// PUPILA_CV_MAX_CHARS for users hitting OOM kills on large CVs. +export const CV_MAX_CHARS = Number(process.env.PUPILA_CV_MAX_CHARS ?? '12000'); // Wrap the const tuple in a Set so call sites can keep using `.has(x)` for // O(1) lookups in middleware request validation. The literal list lives in diff --git a/ui/plugins/aiApply.ts b/ui/plugins/aiApply.ts index f51b40b..8f24bd4 100644 --- a/ui/plugins/aiApply.ts +++ b/ui/plugins/aiApply.ts @@ -117,7 +117,7 @@ export function aiApplyApiPlugin(): Plugin { let inFlight = false; return { - name: 'job-hunt-ai-apply-api', + name: 'pupila-ai-apply-api', configureServer(server) { server.middlewares.use('/api/ai-apply-progress', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/applied.ts b/ui/plugins/applied.ts index a5a0e7b..56ff55e 100644 --- a/ui/plugins/applied.ts +++ b/ui/plugins/applied.ts @@ -10,7 +10,7 @@ import { export function appliedApiPlugin(): Plugin { return { - name: 'job-hunt-applied-api', + name: 'pupila-applied-api', configureServer(server) { server.middlewares.use('/api/applied', async (req, res) => { try { diff --git a/ui/plugins/applyQueue.ts b/ui/plugins/applyQueue.ts index 74b72b4..819abad 100644 --- a/ui/plugins/applyQueue.ts +++ b/ui/plugins/applyQueue.ts @@ -158,7 +158,7 @@ function pathnameOf(rawUrl: string | undefined): string { export function applyQueueApiPlugin(): Plugin { return { - name: 'job-hunt-apply-queue-api', + name: 'pupila-apply-queue-api', configureServer(server) { server.middlewares.use('/api/apply-queue', async (req, res) => { try { diff --git a/ui/plugins/brief.ts b/ui/plugins/brief.ts index 81c0c47..739e9b9 100644 --- a/ui/plugins/brief.ts +++ b/ui/plugins/brief.ts @@ -38,7 +38,7 @@ ${cvText.slice(0, CV_MAX_CHARS)}`; export function briefApiPlugin(): Plugin { return { - name: 'job-hunt-brief-api', + name: 'pupila-brief-api', configureServer(server) { server.middlewares.use('/api/brief', async (req, res) => { try { diff --git a/ui/plugins/clean.ts b/ui/plugins/clean.ts index 66ad37c..a0f0ed8 100644 --- a/ui/plugins/clean.ts +++ b/ui/plugins/clean.ts @@ -16,7 +16,7 @@ export function cleanApiPlugin(): Plugin { // Boolean lock claimed sync (the proc reference isn't needed elsewhere). let inFlight = false; return { - name: 'job-hunt-clean-api', + name: 'pupila-clean-api', configureServer(server) { server.middlewares.use('/api/clean', async (req, res) => { if (req.method !== 'POST') { diff --git a/ui/plugins/data.ts b/ui/plugins/data.ts index b8ff91a..a55e090 100644 --- a/ui/plugins/data.ts +++ b/ui/plugins/data.ts @@ -8,7 +8,7 @@ import { readJsonOrDefault } from './_shared.ts'; // gets `[]` / `{}` and renders the empty state cleanly. export function dataApiPlugin(): Plugin { return { - name: 'job-hunt-data-api', + name: 'pupila-data-api', configureServer(server) { server.middlewares.use('/api/jobs', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/diskUsage.ts b/ui/plugins/diskUsage.ts index e7bd238..5878c9b 100644 --- a/ui/plugins/diskUsage.ts +++ b/ui/plugins/diskUsage.ts @@ -49,7 +49,7 @@ async function walkBucket(absDir: string): Promise { export function diskUsageApiPlugin(): Plugin { return { - name: 'job-hunt-disk-usage-api', + name: 'pupila-disk-usage-api', configureServer(server) { server.middlewares.use('/api/disk-usage', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/env.ts b/ui/plugins/env.ts index 6741f60..9016ede 100644 --- a/ui/plugins/env.ts +++ b/ui/plugins/env.ts @@ -17,7 +17,7 @@ interface EnvInfo { export function envApiPlugin(): Plugin { return { - name: 'job-hunt-env-api', + name: 'pupila-env-api', configureServer(server) { server.middlewares.use('/api/env', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/fetchJobs.ts b/ui/plugins/fetchJobs.ts index 334f9b6..0d99ed1 100644 --- a/ui/plugins/fetchJobs.ts +++ b/ui/plugins/fetchJobs.ts @@ -102,7 +102,7 @@ export function fetchJobsApiPlugin(): Plugin { } return { - name: 'job-hunt-fetch-jobs-api', + name: 'pupila-fetch-jobs-api', configureServer(server) { server.middlewares.use('/api/fetch-jobs', async (req, res) => { try { @@ -124,7 +124,7 @@ export function fetchJobsApiPlugin(): Plugin { state.startedAt = new Date().toISOString(); // tsx is in node_modules/.bin and resolved via pnpm exec. Inherit - // env so JOB_HUNT_* / PATH propagate to the fetchers. + // env so PUPILA_* / PATH propagate to the fetchers. const proc = spawn('pnpm', ['exec', 'tsx', 'src/index.ts'], { cwd: REPO_ROOT, env: process.env, diff --git a/ui/plugins/jobBody.ts b/ui/plugins/jobBody.ts index 18706e9..f8b7688 100644 --- a/ui/plugins/jobBody.ts +++ b/ui/plugins/jobBody.ts @@ -23,7 +23,7 @@ function pathnameOf(rawUrl: string | undefined): string { export function jobBodyApiPlugin(): Plugin { return { - name: 'job-hunt-job-body-api', + name: 'pupila-job-body-api', configureServer(server) { server.middlewares.use('/api/job-body', async (req, res) => { try { diff --git a/ui/plugins/llmDetect.ts b/ui/plugins/llmDetect.ts index 6c2b8a3..61b93ca 100644 --- a/ui/plugins/llmDetect.ts +++ b/ui/plugins/llmDetect.ts @@ -5,7 +5,7 @@ import { availableProviders } from '../../src/lib/llm.js'; // onboarding wizard to ✓/✗ each provider option. export function llmDetectApiPlugin(): Plugin { return { - name: 'job-hunt-llm-detect-api', + name: 'pupila-llm-detect-api', configureServer(server) { server.middlewares.use('/api/llm-detect', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/llmTest.ts b/ui/plugins/llmTest.ts index 45965e2..9844841 100644 --- a/ui/plugins/llmTest.ts +++ b/ui/plugins/llmTest.ts @@ -9,7 +9,7 @@ interface LlmTestPostBody { // Tiny prompt to confirm the chosen LLM CLI works end-to-end. export function llmTestApiPlugin(): Plugin { return { - name: 'job-hunt-llm-test-api', + name: 'pupila-llm-test-api', configureServer(server) { server.middlewares.use('/api/llm-test', async (req, res) => { if (req.method !== 'POST') { diff --git a/ui/plugins/preferences.ts b/ui/plugins/preferences.ts index ea63b7d..ab31d68 100644 --- a/ui/plugins/preferences.ts +++ b/ui/plugins/preferences.ts @@ -12,7 +12,7 @@ import { // supported list (plus `auto`) and stamps `onboardedAt` if not already set. export function preferencesApiPlugin(): Plugin { return { - name: 'job-hunt-preferences-api', + name: 'pupila-preferences-api', configureServer(server) { server.middlewares.use('/api/preferences', async (req, res) => { try { diff --git a/ui/plugins/profile.ts b/ui/plugins/profile.ts index 5477e56..3c31e6a 100644 --- a/ui/plugins/profile.ts +++ b/ui/plugins/profile.ts @@ -28,7 +28,7 @@ interface ProfileGenerateBody { export function profileApiPlugin(): Plugin { let inFlight = false; return { - name: 'job-hunt-profile-api', + name: 'pupila-profile-api', async configureServer(server) { // Bootstrap config/profile.json from config/profile.default.json the // first time `pnpm run ui` runs against a fresh clone (or after the diff --git a/ui/plugins/runSummary.ts b/ui/plugins/runSummary.ts index f0eab9c..51ae0e7 100644 --- a/ui/plugins/runSummary.ts +++ b/ui/plugins/runSummary.ts @@ -21,7 +21,7 @@ interface RunSummary { // the pipeline. export function runSummaryApiPlugin(): Plugin { return { - name: 'job-hunt-run-summary-api', + name: 'pupila-run-summary-api', configureServer(server) { server.middlewares.use('/api/run-summary', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/scheduler.ts b/ui/plugins/scheduler.ts index b11cd88..ba9ff51 100644 --- a/ui/plugins/scheduler.ts +++ b/ui/plugins/scheduler.ts @@ -103,7 +103,7 @@ export function schedulerOpsApiPlugin(): Plugin { } return { - name: 'job-hunt-scheduler-ops-api', + name: 'pupila-scheduler-ops-api', configureServer(server) { server.middlewares.use('/api/scheduler-progress', async (req, res) => { if (req.method !== 'GET') { diff --git a/ui/plugins/schedulerStatus.ts b/ui/plugins/schedulerStatus.ts index 639bd3d..fd26940 100644 --- a/ui/plugins/schedulerStatus.ts +++ b/ui/plugins/schedulerStatus.ts @@ -19,7 +19,7 @@ interface SchedulerStatus { // Detect launchd/cron registration without modifying any system state. export function schedulerStatusApiPlugin(): Plugin { return { - name: 'job-hunt-scheduler-status-api', + name: 'pupila-scheduler-status-api', configureServer(server) { server.middlewares.use('/api/scheduler-status', async (req, res) => { if (req.method !== 'GET') { @@ -43,8 +43,8 @@ export function schedulerStatusApiPlugin(): Plugin { }; if (platform === 'darwin') { - const aggLabel = `dev.${user}.job-hunt.aggregate`; - const revLabel = `dev.${user}.job-hunt.review`; + const aggLabel = `dev.${user}.pupila.aggregate`; + const revLabel = `dev.${user}.pupila.review`; try { const { stdout } = await execAsync('launchctl list', { timeout: 4000 }); status.installed.aggregate = stdout.includes(aggLabel); @@ -63,8 +63,8 @@ export function schedulerStatusApiPlugin(): Plugin { } else if (platform === 'linux') { try { const { stdout } = await execAsync('crontab -l', { timeout: 4000 }); - status.installed.aggregate = stdout.includes(`# job-hunt:aggregate:${REPO_ROOT}`); - status.installed.review = stdout.includes(`# job-hunt:review:${REPO_ROOT}`); + status.installed.aggregate = stdout.includes(`# pupila:aggregate:${REPO_ROOT}`); + status.installed.review = stdout.includes(`# pupila:review:${REPO_ROOT}`); } catch { // no crontab → installed remains false }