cd /news/developer-tools/show-hn-a-local-first-job-search-comโ€ฆ ยท home โ€บ topics โ€บ developer-tools โ€บ article
[ARTICLE ยท art-26269] src=github.com โ†— pub= topic=developer-tools verified=true sentiment=โ†‘ positive

Show HN: A local-first job-search command center (no cloud, no telemetry)

Fighter90 released career-ops-ui v1.69.2, a local-first web dashboard for the open-source AI job-search pipeline career-ops. The tool runs entirely on the user's machine with no cloud accounts or telemetry, providing a CRM-style interface to search, evaluate, and track job applications using a six-dimension scoring rubric against the user's CV.

read28 min publishedJun 13, 2026

A clean, docs-style web interface for the

[career-ops]AI job-search pipeline. Search, evaluate, deep-dive, apply, and track every offer from a single browser tab โ€” instead of bouncing between Claude Code, terminals, and markdown files.

English | Espaรฑol | Portuguรชs (Brasil) | ํ•œ๊ตญ์–ด | ๆ—ฅๆœฌ่ชž | ะ ัƒััะบะธะน | ็ฎ€ไฝ“ไธญๆ–‡ | ็น้ซ”ไธญๆ–‡ | Franรงais

๐Ÿ†• Latest release โ€” v1.69.2

fix(test):A test (npm test

no longer overwrites your realconfig/profile.yml

/data/scan-history.tsv

.critical-fixes.test.mjs

) importedprompts.mjs

(โ†’paths.mjs

) at the top of the file, soPROJECT_ROOT

resolved to therealparent before the test setCAREER_OPS_ROOT

to a temp dir โ€” andPUT /api/profile

leaked an "Acceptance Test" fixture into your profile on every run. Now the carrier is loaded via dynamicimport()

after the env is set, andtests/test-root-isolation.test.mjs

guards the whole suite against the pattern. No production-code change.

Full suite1086/1086green ยท i18n + docs synced across all 9 locales.

career-ops is an open-source job-search system that runs as slash commands inside any AI coding CLI (Claude Code, Codex, OpenCode, Qwen CLI โ€” other Claude-compatible CLIs work too via the same slash-command surface). Model-agnostic. It evaluates each posting against your CV with a six-dimension 0.0โ€“5.0 rubric, generates tailored PDF rรฉsumรฉs, and tracks every application locally โ€” no cloud accounts, no telemetry, no auto-submit.

This repository (career-ops-ui) is a polished web interface on top. The CLI keeps owning form-fill (via Playwright MCP) and slash-command modes; the SPA gives you a CRM-style browser surface over the same cv.md

/ data/applications.md

/ reports/

files. Both share the same data.

Action thresholds by score (from career-ops.org/docs):

Score Next step
โ‰ฅ 4.5
/career-ops apply โ€” high fit, push immediately
4.0 โ€“ 4.4
apply, or /career-ops contacto for warm intro
3.5 โ€“ 3.9
/career-ops deep โ€” research first
< 3.5
skip unless you have a specific reason

Canonical guides at career-ops.org/docs:

Important โ€” career-ops-ui is a dashboardIt runson top of[.]santifer/career-ops

insidea career-ops project ascareer-ops/web-ui/

and reads yourcv.md

,config/

,data/

from the parent folder via../

. It doesnotwork standalone โ€” you need the parentcareer-ops

repo too. Don't clone it on its own and runinit

; use one of the two options below.

curl -fsSL https://raw.githubusercontent.com/Fighter90/career-ops-ui/main/bin/setup.sh | bash

Clones both repos, arranges the career-ops/web-ui/

layout, installs deps, runs the doctor, and starts the server at http://127.0.0.1:4317 โ€” then opens the dashboard.

If you already have career-ops configured and just want the dashboard, clone the UI inside it as web-ui

:

cd career-ops                                                   # โ† your existing career-ops project
git clone https://github.com/Fighter90/career-ops-ui.git web-ui
cd web-ui
npm install
npx career-ops-ui init        # interactive: pick LLM provider + paste its key โ†’ parent career-ops/.env

The nested web-ui/

layout is exactly what lets the UI resolve your ../cv.md

, ../config/

, ../data/

. Run npm link

once if you'd rather type the bare career-ops-ui <verb>

instead of npx career-ops-ui <verb>

.

career-ops-ui setup    # bootstrap: install deps โ†’ doctor โ†’ run (SKIP_START=1 to stop before run)
career-ops-ui init     # pick LLM provider + paste its key (interactive)
career-ops-ui doctor   # verify Node / project / keys / Playwright (exit 0 โ‡” all required green)
career-ops-ui run      # launch the server at http://127.0.0.1:4317
career-ops-ui open     # open + RAISE the dashboard tab in your browser
career-ops-ui help     # list every verb

Prefix with npx

(e.g. npx career-ops-ui run

) if you didn't npm link

. After setup

/run

the tab opens and is brought to the front automatically; set NO_OPEN=1

to disable auto-open (headless / CI).

init

is the provider wizard โ€” choose Claude / Claude Code (ANTHROPIC_API_KEY

), Gemini / Gemini CLI (GEMINI_API_KEY

), Codex / OpenCode CLI (OPENAI_API_KEY

), or Auto (Anthropic โ†’ Gemini fallback). Keys are typed with echo suppressed and written to the parent career-ops/.env

via the same validated path as the #/config

API-keys tab. Non-interactive form for CI:

career-ops-ui init --provider claude --anthropic-key sk-ant-โ€ฆ --yes
career-ops-ui init --provider gemini --gemini-key โ€ฆ          --yes
career-ops-ui init --provider auto   --openai-key sk-โ€ฆ       --yes

Or set it by hand: echo "ANTHROPIC_API_KEY=sk-ant-โ€ฆ" >> career-ops/.env

. The provider sets LLM_PROVIDER

(auto

| claude

| gemini

); change it any time from ** #/config โ†’ API keys** without restarting.

If career-ops-ui init

fails or the command isn't found (common right after a git pull

):

cd career-ops/web-ui
npm install
npx career-ops-ui init        # npx runs the local bin even without `npm link`

Make sure:

  • You're running it from insideโ€” not from a standalonecareer-ops/web-ui/

career-ops-ui/

clone. - The parent and containscareer-ops/

folder existscv.md

andconfig/

. If you cloned career-ops-ui on its own, move it (or re-clone) so it sits atcareer-ops/web-ui/

โ€” or just run Option 1's curl, which arranges the layout for you. career-ops-ui doctor

(ornpx career-ops-ui doctor

) prints exactly what's missing.

career-ops is a powerful Claude-Code-driven job-search system: paste a JD โ†’ get a 0-5 fit score, an ATS-optimized PDF, and a tracker entry. It works great inside Claude Code, but the data lives across cv.md

, data/applications.md

, reports/*.md

, data/pipeline.md

, portals.yml

, config/profile.yml

โ€” easy to lose, hard to skim.

career-ops-ui

puts a polished UI on top:

Auto-pipelineโ€” paste one job URL on#/auto

, click once: validate โ†’ fetch โ†’ evaluate โ†’ save report โ†’ add to tracker, with a live a11y stepper and artifact deep-links.Browse the tracker, reports, and pipeline like a CRM.Trigger scans (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workdayand hh.ru / Habr Career / Trudvsem / GetMatch / GeekJob) and watch live SSE logs.Evaluate a JD live via Anthropic (preferred) or Gemini, or get a copy-paste prompt for Claude Code if no API key is set.Deep research companies live via Anthropic SDK with cv / profile / mode files inlined automatically.Editcv.md

with side-by-side markdown preview and server-side XSS sanitization.Maintain the system: doctor, verify, normalize, dedup, merge โ€” one click each.Multi-CLI: drives identically from Claude Code, Codex, Cursor, Aider, or Gemini CLI โ€”CLAUDE.md

/AGENTS.md

/GEMINI.md

shims point to a single source of truth.

It's pure additions: nothing inside career-ops/

changes. All your customizations stay yours.

git clone https://github.com/santifer/career-ops.git
cd career-ops

Follow career-ops onboarding so cv.md

, config/profile.yml

, and portals.yml

exist.

git clone https://github.com/Fighter90/career-ops-ui.git web-ui

Your tree now looks like:

career-ops/
โ”œโ”€ cv.md
โ”œโ”€ portals.yml
โ”œโ”€ config/
โ”œโ”€ data/
โ”œโ”€ modes/
โ”œโ”€ reports/
โ”œโ”€ scan.mjs โ€ฆ doctor.mjs โ€ฆ (etc)
โ””โ”€ web-ui/                 โ† this repo
   โ”œโ”€ bin/start.sh
   โ”œโ”€ package.json
   โ”œโ”€ server/
   โ”œโ”€ public/
   โ””โ”€ tests/
bash web-ui/bin/start.sh

The script:

  • Checks Node โ‰ฅ 18. npm install

(only on first run, three deps โ€” Express + js-yaml + multer).- Starts the Express server on 127.0.0.1:4317

. - Opens http://127.0.0.1:4317/in your default browser.

Custom port / host:

PORT=8080 bash web-ui/bin/start.sh
HOST=0.0.0.0 PORT=4317 bash web-ui/bin/start.sh   # expose on LAN

If you cloned the repo somewhere else (not as career-ops/web-ui

), point at career-ops via env:

CAREER_OPS_ROOT=/path/to/career-ops bash bin/start.sh

career-ops/data/pipeline.md

ships with two QA fixture URLs (example.com/qa-fixture-*

) so the test suite can run hermetically. On a fresh clone you'll see Pipeline showing 2 pending

โ€” those are not real jobs. Purge them before your first scan:

make clean-test-fixtures        # removes pipeline.md fixture rows + qa-fixture-* applications.md rows
npm start

Open http://127.0.0.1:4317. Pipeline counter should now read 0 pending

. The Makefile is idempotent โ€” re-running it on a clean tree is a no-op.

Node.js | โ‰ฅ 18 (uses native fetch , node:test ) | career-ops | Cloned and onboarded โ€” see above | Optional | GEMINI_API_KEY in .env of the parent project (free-tier model gemini-2.0-flash ) for one-click JD evaluation. Otherwise the UI returns a copy-paste prompt for Claude. | Optional | Run from a Russian IP / VPN if hh.ru returns 403. Habr Career works from any IP regardless. | Optional | Playwright (already a transitive dep of career-ops) for the e2e test suite. |

Page What it does
Dashboard
Aggregated counts (apps / pipeline / reports), avg score, status breakdown, latest 5 apps + latest report.
Scan
๐ŸŒ Single Scan button โ€” runs every enabled source in one go (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday for EN, hh.ru + Habr Career + Trudvsem + GetMatch + GeekJob for RU). Live SSE log streaming + clickable results table with location / Remote-Hybrid badge / relocation flag / salary / source filters and dynamic stack / level / keyword chips. Active-Companies card lists every tracked board with its API health.
Pipeline
CRUD on data/pipeline.md . Server-side preview proxy (SSRF-safe, per-hop redirect validation, 8 KB body cap). Jump straight from a URL to evaluate.
Evaluate
Paste JD โ†’ Anthropic-first (preferred when both keys present), then Gemini, then manual prompt fallback. Anthropic path inlines cv / profile / _shared.md / oferta.md automatically (REVIEW-A1). Save JD to jds/ optional.
Deep research
Same fallback chain as Evaluate. Live Anthropic returns ~10-30 KB of grounded markdown saved to interview-prep/<company>-<role>.md .
Modes
7 generic mode pages (/#/project , /#/training , /#/followup , /#/batch , /#/contacto , /#/interview-prep , /#/patterns ) with the same Anthropic / Gemini / manual fallback.
Apply helper
Generates a submission checklist; the actual Playwright form-fill stays in /career-ops apply inside Claude Code.
Tracker
Filterable table over data/applications.md (status, score, free-text). One-click normalize-statuses.mjs / dedup-tracker.mjs / merge-tracker.mjs . Pipe + newline escapes are GFM-compliant โ€” names like `"Acme Co"` round-trip losslessly.
Reports
Browse and read every report under reports/ with parsed header (Score / Legitimacy / URL).
CV
Live markdown editor for cv.md with side-by-side preview + one-click cv-sync-check.mjs + ๐Ÿ“ Upload CV. Server-side XSS strip on save (<script> , javascript: , on*= handlers).
Profile
Read-only view of config/profile.yml + archetypes โ€” UI-friendly summary.
App settings
In-UI editor for parent .env keys: ANTHROPIC_API_KEY , GEMINI_API_KEY , model overrides, port / host. Secrets masked on read.
Health
All setup checks in OK / OPTIONAL / FAIL badges + buttons to run doctor.mjs and verify-pipeline.mjs .
Help
In-app Markdown user guide (/#/help ), localized for all 9 supported languages (en / es / fr / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW).
Activity log
Audit trail of every state-changing request (writes, runs, scans). Secrets redacted.
Notifications ๐Ÿ”” (v1.58.34 / v1.58.35)
Top-bar bell with red unread badge. Click to slide-in a drawer listing the last 50 toasts (per tab, per session) โ€” Success / Error / Info-progress, each with a localized timestamp, the human message, and any (METHOD /path ยท HTTP NNN) postfix tucked into a <details> . Help ยง18 documents every category. Drawer opens only on bell click (or keyboard Enter / Space); closes via ร—, Esc, or re-clicking the bell.

Global keyboard shortcuts:

Ctrl+K

/Cmd+K

โ€” focus the global search. The footer hint adapts per platform (โŒ˜K on macOS/iOS, Ctrl+K elsewhere) with the localized verb (v1.58.20).- Pasting a URL into global search auto-adds it to the pipeline. Esc

โ€” close any open modalor the notifications drawer (v1.58.34).

Zero-token portal scanning that actually returns vacancies. One ๐ŸŒ Scan button in the UI runs every configured source in a single sweep:

Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workdayโ€” public boards-api for every company inportals.yml::tracked_companies

with a recognizable ATS pattern. Bundled list covers Stripe, GitLab, Vercel, Cloudflare, Datadog, Discord, Elastic, Grafana Labs, CockroachDB, Fastly, Twilio, Coinbase, Reddit, Robinhood, Affirm, Lyft, Linear, Supabase, PostHog, Ramp, Modal Labs, Railway, Browserbase, JetBrains โ€” extend or trim freely.RSS boardsโ€” any job board that exposes an RSS/Atom feed (LaraJobs, WeWorkRemotely, RemoteOK, golangprojects, โ€ฆ). Addprovider: rss

  • the feed URL toportals.yml

โ€” no code changes required.hh.ruโ€” HTML scrape ofhh.ru/search/vacancy

. Works from any IP, no key, no proxy. (The JSON APIapi.hh.ru

is not used โ€” it now 403s every programmatic client regardless of IP/User-Agent; the website serves full results to any browser-like client, so we scrape that, the same way Habr Career is scraped.)Habr Careerโ€” HTML scrape ofcareer.habr.com/vacancies

. Works from any IP, no auth.

Point the scanner at any RSS-based job board by adding an entry with provider: rss

and an rss:

(or feed_url:

) key to portals.yml

:

tracked_companies:
  - name: LaraJobs
    provider: rss
    rss: https://larajobs.com/feed
    enabled: true
  - name: WeWorkRemotely
    provider: rss
    rss: https://weworkremotely.com/remote-jobs.rss
    enabled: true

The adapter parses <item>

blocks using a tiny regex-based parser (no XML library needed). It extracts title

, link

(โ†’ url

), pubDate

(โ†’ date

), and description

(โ†’ snippet

, HTML stripped). Remote status is inferred from /remote|anywhere/i

in the title or description; company name is pulled from dc:creator

, a "Company โ€” Role" title pattern, or the feed hostname as a fallback. The same normalize โ†’ filter โ†’ dedup โ†’ pipeline-append flow applies as for ATS adapters.

All sources go through the same pipeline: normalize โ†’ filter (title_filter.positive

/ title_filter.negative

) โ†’ dedup against data/scan-history.tsv

  • data/pipeline.md

  • data/applications.md

โ†’ append to data/pipeline.md

โ†’ save full result set to data/last-scan.json

for the UI's filterable table.

Configure via portals.yml

:

title_filter:
  positive: [backend, engineer, senior, tech lead, golang, php]
  negative: [junior, intern, frontend, ios, android]
tracked_companies:
  - { name: Stripe, enabled: true, careers_url: https://job-boards.greenhouse.io/stripe }
  - { name: Linear, enabled: true, careers_url: https://jobs.ashbyhq.com/linear }
russian_portals:
  sources: ["hh", "habr"]   # one or both
  area: 113                  # 1=Moscow, 2=SPb, 113=Russia, 1001=remote
  per_page: 50
  only_remote: false
  queries: ["Senior PHP", "Senior Go", "Tech Lead"]

All sources flow through a single SSE endpoint: /api/stream/scan?source=ats|regional|both

. The ๐ŸŒ Scan UI button calls source=both

so every adapter (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday + hh.ru + Habr Career + Trudvsem + GetMatch + GeekJob) runs in one connection. Honors AbortSignal

on client disconnect โ€” no orphan fetches.

career-ops-ui/
โ”œโ”€ CLAUDE.md                 # project-level agent instructions (canonical)
โ”œโ”€ AGENTS.md                 # Codex / Aider / generic CLI shim โ†’ CLAUDE.md
โ”œโ”€ GEMINI.md                 # Gemini CLI shim โ†’ CLAUDE.md
โ”œโ”€ .aiignore                 # exclusion list for AI tools
โ”œโ”€ .claude/                  # Claude Code agent config
โ”‚  โ”œโ”€ agents/                # 3 project-specific subagents (route, view, test isolation)
โ”‚  โ””โ”€ commands/               # slash-command stubs
โ”œโ”€ bin/start.sh              # one-shot launcher (Node check โ†’ npm install โ†’ server โ†’ open browser)
โ”œโ”€ package.json              # 2 runtime deps: express, js-yaml
โ”œโ”€ server/
โ”‚  โ”œโ”€ index.mjs              # ~130 LOC orchestrator: middleware + 12 register<Topic>Routes(app) calls + SPA catch-all
โ”‚  โ””โ”€ lib/
โ”‚     โ”œโ”€ paths.mjs           # absolute paths to career-ops files (CAREER_OPS_ROOT aware)
โ”‚     โ”œโ”€ parsers.mjs         # markdown / pipeline / report parsers (GFM-compliant pipe escapes)
โ”‚     โ”œโ”€ runner.mjs          # runNodeScript() + streamNodeScript() with SIGTERMโ†’SIGKILL escalation + 30 min cap
โ”‚     โ”œโ”€ security.mjs        # isValidJobUrl, stripDangerousMarkdown, sanitizeJobDescription, isPubliclyExposed
โ”‚     โ”œโ”€ prompts.mjs         # bundleProjectContext, buildEvaluationPrompt, buildDeepPrompt, buildModePrompt
โ”‚     โ”œโ”€ store.mjs           # safeReadApps/Pipeline/Reports, checkProfileCustomized, ensureRussianPortalsDefaults
โ”‚     โ”œโ”€ anthropic.mjs       # minimal Anthropic SDK adapter (runAnthropic, hasAnthropicKey, hasGeminiKey)
โ”‚     โ”œโ”€ env-config.mjs      # .env round-trip with secret masking + validation
โ”‚     โ”œโ”€ activity-log.mjs    # JSONL audit trail middleware (secrets redacted)
โ”‚     โ”œโ”€ dotenv.mjs          # tiny dotenv 
โ”‚     โ”œโ”€ en-scanner.mjs      # in-process Greenhouse/Ashby/Lever orchestrator (AbortSignal aware)
โ”‚     โ”œโ”€ ru-scanner.mjs      # in-process hh.ru + Habr orchestrator (AbortSignal aware)
โ”‚     โ”œโ”€ sources/
โ”‚     โ”‚  โ”œโ”€ greenhouse.mjs   # boards-api.greenhouse.io client
โ”‚     โ”‚  โ”œโ”€ ashby.mjs        # api.ashbyhq.com client
โ”‚     โ”‚  โ”œโ”€ lever.mjs        # api.lever.co client
โ”‚     โ”‚  โ”œโ”€ hh.mjs           # hh.ru/search/vacancy HTML scraper (paginated, UA-aware)
โ”‚     โ”‚  โ””โ”€ habr.mjs         # career.habr.com HTML parser (no cheerio, regex only)
โ”‚     โ””โ”€ routes/             # 12 route modules โ€” one per topic (P-2)
โ”‚        โ”œโ”€ activity.mjs     # /api/activity
โ”‚        โ”œโ”€ config.mjs       # /api/config (parent .env round-trip)
โ”‚        โ”œโ”€ content.mjs      # /api/cv, /api/profile, /api/portals, /api/modes
โ”‚        โ”œโ”€ health.mjs       # /api/health, /api/dashboard
โ”‚        โ”œโ”€ help.mjs         # /api/help/:lang
โ”‚        โ”œโ”€ jds.mjs          # /api/jds CRUD
โ”‚        โ”œโ”€ llm.mjs          # /api/evaluate, /api/deep, /api/mode/:slug, /api/apply-helper, /api/interview-prep*
โ”‚        โ”œโ”€ pipeline.mjs     # /api/pipeline + SSRF-safe preview proxy
โ”‚        โ”œโ”€ reports.mjs      # /api/reports
โ”‚        โ”œโ”€ runners.mjs      # /api/run/* + /api/stream/{scan,liveness,pdf} + /api/output/pdfs
โ”‚        โ”œโ”€ scan.mjs         # /api/stream/scan-{ru,en} + /api/scan-results
โ”‚        โ””โ”€ tracker.mjs      # /api/tracker
โ”œโ”€ public/                   # static SPA โ€” no build step
โ”‚  โ”œโ”€ index.html
โ”‚  โ”œโ”€ css/app.css            # design tokens (docs-style palette)
โ”‚  โ””โ”€ js/
โ”‚     โ”œโ”€ api.js              # fetch wrapper + connection-banner state + UI helpers + safe markdown renderer
โ”‚     โ”œโ”€ router.js           # hash-based router with 404 fallback + alias support
โ”‚     โ”œโ”€ app.js              # boot + global keyboard handlers + mobile sidebar drawer
โ”‚     โ”œโ”€ lib/{i18n,skills}.js
โ”‚     โ””โ”€ views/              # one file per page (dashboard, scan, pipeline, evaluate, deep, apply, tracker, reports, cv, settings, health, config, help, activity, mode-page)
โ”œโ”€ docs/                     # public reference: architecture, API, data-flows, SDD, conventions, reviews
โ”‚  โ”œโ”€ PROJECT.md             # what / why / for-whom
โ”‚  โ”œโ”€ ROADMAP.md             # current milestone + completed history
โ”‚  โ”œโ”€ PRODUCTION-READINESS.md # honest deployment-gate assessment
โ”‚  โ”œโ”€ sdd/{SDD-GUIDE,CONVENTIONS}.md
โ”‚  โ”œโ”€ architecture/{OVERVIEW,SERVER,FRONTEND,API,DATA-FLOWS}.md
โ”‚  โ””โ”€ reviews/REVIEW-*.md
โ””โ”€ tests/                    # 1000 unit + 70 Playwright + 23/23 e2e:full + 20 e2e:smoke (baseline @ v1.60.0)
   โ”œโ”€ parsers.test.mjs       # markdown / pipeline / report parsers (pure functions)
   โ”œโ”€ api.test.mjs           # every endpoint, ephemeral server, no network
   โ”œโ”€ {ru,en}-scanner.test.mjs   # mocked fetch
   โ”œโ”€ pipeline-preview.test.mjs   # per-hop redirect validation (REVIEW-B1)
   โ”œโ”€ anthropic.test.mjs     # SDK adapter + log-guard test (REVIEW-B4)
   โ”œโ”€ url-validation.test.mjs    # SSRF reject sweep (FIX-M3 + M6 + M7)
   โ”œโ”€ cv-xss.test.mjs        # stripDangerousMarkdown round-trip
   โ”œโ”€ jd-sanitize.test.mjs   # sanitizeJobDescription
   โ”œโ”€ help.test.mjs / help-ui.test.mjs    # i18n parity across all 8 locales
   โ”œโ”€ playwright-smoke.mjs   # 12 browser flows (CV save, tracker, pipeline, evaluate, config, etc.)
   โ””โ”€ e2e{,-comprehensive}.mjs   # full Playwright walkthrough

Vanilla HTML/CSS/JS keeps the surface area tiny: one npm install

of two deps and you're running. No Webpack, no Vite, no node_modules

of doom. The whole UI is < 30 KB minified. If you want hot-reload during development, npm run dev

uses Node's built-in --watch

.

Non-trivial changes go through the GSD pipeline (gsd-*

skills from superpowers@claude-plugins-official

):

discuss โ†’ spec โ†’ plan โ†’ execute โ†’ verify โ†’ review

Public reference: docs/sdd/SDD-GUIDE.md. All planning artifacts live under

.planning/

(gitignored). The docs/

tree is the long-lived public contract.All endpoints under /api/*

. JSON in / JSON out unless noted.

Method Path Response
GET /api/health
{ ok, warnings, version, parentVersion, checks: [{name, ok, required, value?}] }
GET /api/dashboard
{ counts, avgScore, byStatus, recent, pipeline, lastReport }
GET /api/status/providers
{ activeProvider, activeModel, keysConfigured } โ€” LLM readiness for the onboarding banner + โšก cost hint (v1.55.3); includes openrouter (v1.57.0)
GET /api/openrouter/models
{ models:[{id,name,context_length}], fallback, cached } โ€” OpenRouter catalogue proxy for the #/config model dropdown (v1.57.0)
GET /api/activity?limit&type
tail of data/activity.jsonl audit trail
GET /api/help/:lang
localized in-app user guide (fallback: en.md )
Method Path Purpose
GET /api/config
known env keys with secrets masked
POST /api/config
validate + write parent .env ; applies to process.env in-place
Method Path Purpose
GET /api/tracker
{ rows: [parsed applications.md] }
POST /api/tracker
body { company, role, score?, status?, url?, notes?, date? } โ€” dedup-aware (case-insensitive on company + role)
GET /api/pipeline
{ urls: [...] }
POST /api/pipeline
body { url } โ†’ adds to data/pipeline.md with dedup + isValidJobUrl
GET /api/pipeline/preview?url=โ€ฆ
server-side fetch proxy (per-hop SSRF check, โ‰ค3 redirects, 8 KB cap)
DELETE /api/pipeline?url=โ€ฆ
removes a URL
GET /api/reports
parsed list of reports/*.md
GET /api/reports/:slug
full markdown + parsed header
GET /api/jds
list of saved JD files
GET /api/jds/:name
text/plain โ€” raw JD
POST /api/jds
body { text, slug? } โ†’ saves to jds/
DELETE /api/jds/:name
unlink (.txt suffix required)
GET /api/cv
{ markdown }
PUT /api/cv
body { markdown } โ†’ writes cv.md (XSS-stripped, โ‰ค1 MB)
GET /api/profile
{ profile: yaml-parsed, raw: text }
GET /api/portals
{ portals: yaml-parsed, raw: text }
GET /api/modes
list of mode files
GET /api/modes/:name
text/plain โ€” raw mode prompt
GET /api/output/pdfs
list of generated PDFs
GET /api/output/pdfs/:name
download (Content-Disposition: attachment )
GET /api/interview-prep
list of saved deep-research files
GET /api/interview-prep/:name
{ name, markdown }
DELETE /api/interview-prep/:name
unlink (.md suffix required)
Method Path Wraps
POST /api/run/doctor
node doctor.mjs
POST /api/run/verify
node verify-pipeline.mjs
POST /api/run/normalize
node normalize-statuses.mjs
POST /api/run/dedup
node dedup-tracker.mjs
POST /api/run/merge
node merge-tracker.mjs
POST /api/run/sync-check
node cv-sync-check.mjs

All buffered runs cap at 60 s; SIGTERM โ†’ SIGKILL escalation after a 5 s grace period.

Method Path Streams
GET /api/stream/scan
legacy node scan.mjs (subprocess)
GET `/api/stream/scan?source=ats regional
consolidated in-process scanner SSE โ€” query: dryRun=1 , company=โ€ฆ (ATS only).
GET /api/stream/liveness
node check-liveness.mjs
GET /api/stream/pdf
node generate-pdf.mjs

SSE event types:

event: start    data: { script, args?, writeFiles? }
event: log      data: { stream: "stdout"|"stderr", line: string }
event: done     data: { code, counts?, errors? }
event: error    data: { message }
Method Path Purpose
POST /api/evaluate
body { jd, save? } โ†’ JD evaluation (Aโ€“G sections per oferta.md )
POST /api/evaluate/test-gemini
smoke check GEMINI_API_KEY
POST /api/evaluate/test-anthropic
smoke check ANTHROPIC_API_KEY
POST /api/deep
body { company, role?, run? } โ†’ deep-research prompt or live grounded markdown
POST /api/mode/:slug
generic mode runner; allowlist: batch , contacto , followup , interview-prep , patterns , project , training
POST /api/apply-helper
body { url, jd? } โ†’ application checklist
GET /api/scan-results
{ en: {when, fresh[], filtered[], errors[]}, ru: { ... } } โ€” last scan
GET /api/scan/regional/config
effective regional-scanner config (queries, negatives, sources).

When run: true

is set on /api/deep

or /api/mode/:slug

, the server prefers Anthropic (when both keys present), inlines cv.md

  • config/profile.yml

  • modes/_shared.md

  • the relevant mode template into a <project_context>

block, and returns the model's grounded markdown directly. Soft cap: 200 KB on the assembled prompt โ€” overflow returns 413.

npm test                       # 1000 unit/integration tests
npm run test:e2e               # 20 smoke e2e (boots own server)
npm run test:e2e:full          # 23 comprehensive e2e
npm run test:e2e:browser       # 70 Playwright browser (smoke + full-cycle + forms + locale-sweep)
npm run test:coverage          # same as `npm test` plus V8 coverage
Suite Tests What
node --test tests/*.test.mjs (unit + integration)
1000
Every endpoint, ephemeral server, no network. 110 files: parsers, scanners (mocked), runners, anthropic/openai, security headers, XSS, JD sanitize, URL validation, i18n parity, + the v1.55โ†’v1.56 UX-fix suites.
tests/e2e.mjs (smoke)
20 Playwright headless: every route renders, basic flows.
tests/e2e-comprehensive.mjs
23 Full Playwright walkthrough: 11 routes + 12 functional flows.
npm run test:e2e:browser (playwright-smoke + playwright-full-cycle + playwright-forms + playwright-locale-sweep )
70
Browser-driven: dashboard render, navigation, language switch, 404, health, tracker round-trip, pipeline add + invalid-URL sweep, reports, evaluate manual fallback, config keys masked, CV PUT XSS strip, pipeline preview 400, auto-pipeline SSE.
Total
1113
0 fails, 0 flakes

Coverage: ~95.7% line / ~87% branch via --experimental-test-coverage

.

Parsers are pure functions (no I/O) โ€” tested against real data fragments from applications.md

, pipeline.md

, and reports/*.md

. API tests boot the Express app on an ephemeral port and exercise every endpoint end-to-end. Scanner tests mock fetch

so they pass even if hh.ru blocks your IP. The Playwright browser smoke runs against the in-process server and resolves Playwright via the parent project's node_modules

โ€” no new dependency in web-ui/

.

CI runs the unit + e2e + Playwright matrix on every push to main

against Node 18 / 20 / 22.

Environment variables (read at server start, all optional except where noted):

Var Default Purpose
PORT
4317
Express bind port
HOST
127.0.0.1
Express bind host. CSP attaches when non-loopback; auth gate planned for v2.0.0.
CAREER_OPS_ROOT
.. from script
Where to find cv.md , data/ , portals.yml , modes/ , etc.
ANTHROPIC_API_KEY
unset Enables /api/evaluate , /api/deep , /api/mode/:slug live mode (preferred when both keys set).
ANTHROPIC_MODEL
claude-sonnet-4-6
Override Anthropic model.
GEMINI_API_KEY
unset Forwarded to gemini-eval.mjs and used as fallback for /api/evaluate .
GEMINI_MODEL
gemini-2.0-flash
Override Gemini model.
OPENAI_API_KEY
unset Headless live-eval (3rd in the auto order) + parent Codex/OpenAI CLI flow.
OPENAI_MODEL
gpt-5-codex
Override OpenAI model.
QWEN_API_KEY
unset Headless live-eval via DashScope OpenAI-compatible (4th in the auto order).
QWEN_MODEL
qwen-max
Override Qwen model.
OPENROUTER_API_KEY
unset Headless live-eval via OpenRouter โ€” one key, 300+ models (5th / last in auto ).
OPENROUTER_MODEL
openrouter/auto
vendor/model id. Catalogue loaded live from GET /api/openrouter/models .

portals.yml

extension recognized by this UI (add to your existing file in the parent project):

russian_portals:
  sources: ["hh", "habr"]
  area: 113          # hh.ru area id
  per_page: 50
  only_remote: false
  queries: ["Senior PHP", "ะขะธะผะปะธะด Go", ...]

You can also extend any company entry with an explicit api:

URL. See docs/portals-examples.md (in this repo) for ready-to-paste blocks for 24 verified companies.

  • Server binds to 127.0.0.1

by default โ€” never exposed to the internet without explicitHOST=0.0.0.0

. Path sanitization (v1.21.0): every:name

/:slug

route param goes throughsanitizePathName()

inserver/lib/security.mjs

โ€” strips non-[\w-.]

, drops leading dot-runs, collapses internal dot-runs, caps at 200 chars, empty โ†’ 400. Replaces 10 duplicated regex copies that previously kept..pdf

/....md

through.DNS-rebind defense (v1.21.0):/api/pipeline/preview

and/api/auto-pipeline

route throughserver/lib/safe-fetch.mjs::safeGet

โ€” one DNS lookup, pinned TCP connection, SNI/Host targeted at the original hostname. No second lookup, no TOCTOU window.Concurrent-write mutex (v1.21.0):tracker.mjs

,pipeline.mjs

(POST + DELETE), andauto-pipeline.mjs

's tracker step wrap read-modify-write inwithFileLock(path, fn)

fromserver/lib/file-lock.mjs

. Concurrent POSTs no longer drop rows.LLM rate-limit (v1.21.0):/api/evaluate

,/api/deep

,/api/mode/:slug

,/api/auto-pipeline

wearllmRateLimit

fromserver/lib/rate-limit.mjs

.No-op on loopback; 10 req/min/IP onHOST=0.0.0.0

. Configurable viaLLM_RATE_LIMIT="N/Ws"

. 429 +Retry-After

.CV XSS strip (v1.22.0 hardening):stripDangerousMarkdown

is now entity-aware โ€” decodes<

,>

,&#NN;

,&#xHH;

before regex strip so<script>

andjavascript:

payloads can't bypass.- Subprocess invocations use spawn

with arg arrays โ€”no shell interpolation, ever.bash

runner uses--noprofile --norc

to ignore~/.bashrc

. - Streaming endpoints kill the child process on client disconnect (no orphaned scanners).

  • Write endpoints touch only known career-ops paths: data/

,jds/

,cv.md

,config/

,portals.yml

,output/

,reports/

,interview-prep/

,modes/_profile.md

. Never anywhere else. - The connection banner pings /api/health

with exponential backoff (3 s โ†’ 6 s โ†’ 12 s โ†’ 24 s โ†’ 60 s) while disconnected and auto-clears on recovery (v1.22.0 M-6).

The fully LLM-driven modes (oferta

, deep

, contacto

, apply

, batch

, patterns

, followup

) need an LLM to actually run. The web UI resolves a provider from the auto

order Anthropic โ†’ Gemini โ†’ OpenAI โ†’ Qwen โ†’ OpenRouter (or whatever LLM_PROVIDER

pins):

Anthropic (preferred)โ€” setANTHROPIC_API_KEY

in the parent project's.env

. Routes throughrunAnthropic

withcv.md

/config/profile.yml

/modes/_shared.md

/ mode template inlined automatically (REVIEW-A1). Verified live in v1.8.0+ withclaude-sonnet-4-6

returning 26 KB of grounded markdown for a deep-research call.as fallback โ€” works out of the box when onlygemini-eval.mjs

GEMINI_API_KEY

is set.OpenAI / Qwen / OpenRouterโ€” zero-dep OpenAI-compatible clients (the_tailProvider()

path).OpenRouter(v1.57.0) is the most flexible: oneOPENROUTER_API_KEY

fronts 300+ models from every major lab, and the#/config

model dropdown is populated live fromGET /api/openrouter/models

(server-side proxy, CSP-safe, curated offline fallback).Copy-paste promptโ€” when no key is set, the UI generates a ready prompt formatted for Claude Code / ChatGPT / Gemini Web.

The existing /career-ops apply

Playwright form-fill flow inside Claude Code remains the only way to truly auto-fill application forms โ€” the UI's Apply helper generates a checklist instead.

For the production-readiness assessment (deployment gates, risk register, deferred work), see docs/PRODUCTION-READINESS.md. TL;DR: ready for single-tenant loopback; LAN exposure waits on the v2.0 P-12 auth gate.

The UI ships 9 locales โ€” en

, es

, fr

, pt-BR

, ko

, ja

, ru

, zh-CN

, zh-TW

. Since v1.60.0 (I18N-SPLIT) translations live one file per locale under public/js/lib/locales/ โ€”

i18n-dict.<lang>.js

, each a flat key โ†’ string

table โ€” plus a shared i18n-dict.aliases.js

. assembles them into

i18n-dict.js

window.__I18N_DICT

; resolves

i18n.js

t('key', 'fallback')

. No build step, no runtime fetch โ€” a translator edits a single language file in isolation.Add or change a string:

// public/js/lib/locales/i18n-dict.en.js   โ†’   'scan.newButton': 'Run scan',
// public/js/lib/locales/i18n-dict.es.js   โ†’   'scan.newButton': 'Ejecutar bรบsqueda',
// โ€ฆadd the same key to all 9 locale files (parity is gated)

Then use it via data-i18n="scan.newButton"

in markup or t('scan.newButton')

in JS, and run npm test

. To add a brand-new language, register it in i18n.js

(LANGS

  • detect()

), the assembler, index.html

, and the locale-enumerating tooling.

๐Ÿ“– Full guide: docs/LOCALIZATION.md โ€” the per-locale layout, the

@alias

mechanism, adding a new locale step-by-step, and every i18n CI gate.Issues and PRs welcome. House rules:

  • Run npm test

before pushing โ€”284 checks green is the bar (plus 12 Playwright if you touch UI). - Non-trivial changes go through the GSD pipeline. See .docs/sdd/SDD-GUIDE.md

  • Don't modify anything in the parent career-ops/

project from inside this repo. The whole point is that this is a non-invasive overlay. Hard rules in.CLAUDE.md

  • Conventional commits: feat

,fix

,refactor

,docs

,test

,chore

,perf

,ci

. Optional scope:feat(scan):

. Breaking change:feat!:

. - Tests must be CI-isolated โ€” bootstrap fixtures via mkdtempSync

orCAREER_OPS_ROOT=$(mktemp -d)

.

Driving the repo from a non-Claude CLI (Codex, Aider, Cursor, Gemini)? Read AGENTS.md or

โ€” both shim to the canonical

GEMINI.md

CLAUDE.md

.After the one-command install you have two empty git clones, scaffolded with starter cv.md

, config/profile.yml

, portals.yml

, data/applications.md

, and data/pipeline.md

files containing EDIT ME markers. The Health page should already be all-green on first launch. Replace the placeholders with your real data:

You have three options:

Option A โ€” paste an existing resume: opencareer-ops/cv.md

, replace the EDIT-ME placeholders with your real resume in clean markdown (sections: Summary, Experience, Projects, Education, Skills). The simpler the better โ€”career-ops

reads it as plain text.Option B โ€” upload from the UI: clickCV in the sidebar โ†’๐Ÿ“ Upload CVโ†’ pick your.md

/.txt

file โ†’ review the preview โ†’ click๐Ÿ’พ Save.** Option C โ€” give your LinkedIn URL to Claude Code:**open Claude Code incareer-ops/

, run/career-ops

, paste your LinkedIn URL, and ask*"extract my CV from this and write it to cv.md"*.

Make every metric specific (e.g. "reduced p99 latency by 38%" not "improved performance"). The evaluation pipeline reads metrics straight from this file.

$EDITOR career-ops/config/profile.yml

Replace the placeholders for full name, email, location, LinkedIn, target roles, archetypes, salary target. The archetypes are the most important field โ€” they're how every JD is matched against you.

$EDITOR career-ops/portals.yml

Set title_filter.positive

(e.g. "PHP"

, "Go"

, "Backend"

, "Senior"

) and title_filter.negative

(e.g. "Junior"

, "Java"

, "iOS"

) to your stack and seniority. The bundled tracked_companies

list already includes 3 verified Greenhouse / Ashby boards (GitLab, Vercel, Linear). For 24+ more ready-to-paste blocks, see docs/portals-examples.md.

If you want hh.ru / Habr Career scanning, edit the russian_portals:

block that the setup script created โ€” add your search queries (e.g. "Senior PHP"

, "ะขะธะผะปะธะด Go"

).

The UI prefers Anthropic over Gemini when both are present. Either or neither works โ€” without a key, Evaluate returns a copy-paste prompt for Claude Code instead.

echo "ANTHROPIC_API_KEY=sk-ant-..." >> career-ops/.env
echo "GEMINI_API_KEY=AIza..." >> career-ops/.env

Or set them via the App settings page in the UI (/#/config

) โ€” same file, masked-on-read, applied to process.env

immediately.

Refresh the Health page โ€” every required check should be green. Then:

  • Click ๐ŸŒ Scanโ†’ wait ~5 seconds โ†’ Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday + hh.ru / Habr Career are scanned, vacancies appear in the table below. - Click any title โ†’ the original posting opens in a new tab.
  • Filter by stack chips (PHP / Go / Backend / Senior) until you see something promising.
  • Copy the URL โ†’ paste it into Pipelineโ†’ click** Evaluate**to score it 0-5 live (Anthropic / Gemini) or get a manual prompt. - Reports land in reports/

, tracker indata/applications.md

, live deep-research ininterview-prep/

. All visible in the UI.

Translations of this guide live in each language-specific README:

[Espaรฑol]ยท[Franรงais]ยท[Portuguรชs (Brasil)]ยท[ํ•œ๊ตญ์–ด]ยท[ๆ—ฅๆœฌ่ชž]ยท[ะ ัƒััะบะธะน]ยท[็ฎ€ไฝ“ไธญๆ–‡]ยท[็น้ซ”ไธญๆ–‡]

MIT. See LICENSE.

Built on top of career-ops by santifer. Thanks for the brilliant pipeline.

Thanks to everyone who helps build career-ops-ui. The project is maintained by Fighter90 and improved by community contributions โ€” see the full list on the contributors graph.

โ”€โ”€ more in #developer-tools 4 stories ยท sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain โ€” perfect for shipping the agent you just read about.

$git push zahid main
โ†’ Live at https://your-agent.zahid.host โœ“
Get free account โ†’ Pricing
from โ‚ฌ0/mo ยท no card required
LIVE [news/show-hn-a-local-firsโ€ฆ] indexed:0 read:28min 2026-06-13 ยท โ€”