Put your best plume forward. A resume tailoring tool for technical roles — Program Management, Product, and IT / Infrastructure.
Rewrite rough technical resume bullets into high-impact statements — and turn every missing metric into a fill-in-the-blank field you type your real number into.
Built for three job families: Technical Program Management (TPM), Product Management (PdM), and
IT / Infrastructure / Security. Paste a responsibility or a weak bullet, pick a role family and a
voice, and get 3–5 rewritten bullets back. Anywhere a number belongs but you didn't supply one, the
app inserts a highlighted, editable slot like reducing MTTR by [ X% ] instead of inventing a
figure.
It has three tabs:
- Tailor — paste a job description plus your experience and get bullets re-aimed at that specific role, with a keyword coverage read showing what you cover and where the gaps are. See Tailoring to a job below.
- Enhance — the standalone resume bullet rewriter described above.
- Tracker — a job application tracker that follows the interview pipeline (Phone → Hiring Manager → On-site rounds → Offer), with notes and dates per round. It saves to your browser and exports to JSON or CSV. See The job tracker below.
Why the editable slot matters: pasting a resume prompt into a chatbot gives you dead text like
reducing MTTR by [X]%. Here, that placeholder is a live field — the prose stays locked, you fill the number, and copy gives you a finished bullet. The tool never fabricates metrics.
Browser (React, Vite) ──POST /api/enhance──▶ Express server ──▶ Your LLM provider
▲ (holds the key) Anthropic, OpenAI,
└──────────── bullets + slots ──────────┘ OpenRouter, Ollama, …
The browser never talks to the LLM provider directly and never sees your API key. All requests go through a thin Express proxy that holds the key server-side, builds the system prompt, calls your chosen model, and returns clean JSON. This is the one change that makes the tool safe to deploy publicly — a key shipped to the browser would be harvested within minutes.
Pick a backend with LLM_PROVIDER: anthropic (default, native
Messages API) or openai for any
OpenAI-compatible /chat/completions endpoint — OpenAI, OpenRouter, Groq, Together, DeepSeek,
Mistral, Google's OpenAI-compat layer, or a local model via Ollama / LM Studio / vLLM. See
Configuration.
Prerequisites
- Node.js 20 or newer
- An API key for the LLM provider you want to use. By default that's Anthropic — create one at https://console.anthropic.com/settings/keys. To use OpenAI, OpenRouter, Groq, etc., or a free local model via Ollama / LM Studio (no key at all), see Configuration.
Setup
git clone https://github.com/<your-username>/plume.git
cd plume
npm install
cp .env.example .env # on Windows (cmd): copy .env.example .env
# then open .env and paste your key (ANTHROPIC_API_KEY by default)Run in development
npm run devThis starts the Express API on http://localhost:3001 and the Vite dev server on
http://localhost:5173. Open the Vite URL — it proxies /api calls to Express automatically.
Run in production
npm run build # bundles the React app into dist/
npm start # Express serves dist/ and the API on one port (default 3001)All configuration is via environment variables (see .env.example):
| Variable | Default | Notes |
|---|---|---|
LLM_PROVIDER |
anthropic |
anthropic (native Messages API) or openai (any OpenAI-compatible endpoint). |
ANTHROPIC_API_KEY |
(required for anthropic) | Your Anthropic key. Never commit this. |
LLM_BASE_URL |
https://api.openai.com/v1 |
OpenAI-compatible base URL (no /chat/completions). Used when LLM_PROVIDER=openai. |
LLM_API_KEY |
(required for hosted) | Key for the OpenAI-compatible endpoint; optional for local models. OPENAI_API_KEY also works. |
MODEL |
claude-sonnet-4-6 * |
Model id. *Default applies to anthropic; required for openai (e.g. gpt-4o-mini). |
PORT |
3001 |
Port the Express server listens on. |
RATE_LIMIT_MAX |
30 |
Max API requests per IP per window. |
RATE_LIMIT_WINDOW_MS |
300000 |
Rate-limit window length in ms (default 5 min). |
TRUST_PROXY |
(off) | Set to 1 only when behind a proxy, so limits see real IPs. |
Examples — pick one provider's block for your .env:
# OpenAI
LLM_PROVIDER=openai
LLM_API_KEY=sk-...
MODEL=gpt-4o-mini
# OpenRouter (one key, hundreds of models)
LLM_PROVIDER=openai
LLM_BASE_URL=https://openrouter.ai/api/v1
LLM_API_KEY=sk-or-...
MODEL=anthropic/claude-3.5-sonnet
# Local model via Ollama — no API key, nothing leaves your machine
LLM_PROVIDER=openai
LLM_BASE_URL=http://localhost:11434/v1
MODEL=llama3.1Each enhancement or tailoring is a single API call with a small prompt and a short response — on the order of a thousand tokens total — so the per-call cost is a fraction of a cent on typical hosted models (e.g. Claude Sonnet 4.6, or GPT-4o mini). Cheaper or smaller models lower it further, and a local model via Ollama / LM Studio costs nothing per call. Check your provider's pricing page, since rates vary by model and change over time.
You pay your chosen provider directly for usage on your own key. There is no other cost to running this.
The app is a standard Node server after npm run build, so it runs anywhere that runs Node:
- Render / Railway / Fly.io / a VPS: set the build command to
npm install && npm run build, the start command tonpm start, and add your provider credentials as environment variables in the platform dashboard —ANTHROPIC_API_KEYfor the default Anthropic backend, orLLM_PROVIDER=openai+LLM_BASE_URL+LLM_API_KEY+MODELfor an OpenAI-compatible one. - Split frontend/backend: you can also host
dist/on any static host and runserver/as a standalone API — just point the frontend's fetch base at the API's URL and enable CORS.
Whatever you choose, set your API key as a platform secret, never in committed code.
Per-IP rate limiting is built in (configurable via RATE_LIMIT_MAX / RATE_LIMIT_WINDOW_MS); tune
it for your traffic before exposing a public instance, since each request spends real money on your
key. If you deploy behind a proxy or load balancer, also set TRUST_PROXY so limits apply per real
client IP.
The voice and rules of the rewrite live entirely in server/index.js — buildEnhancePrompt() and
buildTailorPrompt(), which share a BULLET_STYLE block and the toneLine / familyLine helpers.
Two things you'll likely want to tune:
- The verb lexicon and acronym density. The default "measured" voice avoids theatrical verbs;
"bold" leans punchier. Edit
toneLine()andBULLET_STYLEto taste. - The placeholder convention. The model wraps fill-in metrics in
{{double braces}}. The frontend (parseBulletinsrc/bullets.jsx) splits on that exact pattern to render the editable fields, so if you change the delimiter, change it in both places.
The Tailor tab is the difference between a generic resume and one aimed at the job in front of you. Paste the job description in one box and your current experience or bullets in the other, and it returns two things:
- Keyword coverage. It pulls the key skills and requirements out of the JD and splits them into what your experience already evidences (Covered) and what the JD wants but your input didn't show (Gaps), with a coverage percentage. The gaps list is the useful part — it tells you what to add if you have it, or what you'll be asked about that you can't claim.
- Tailored bullets. Your experience rewritten to lead with what this role cares about, using the
JD's own terminology — but only where you genuinely have the experience. Same editable
{{metric}}slots as the Enhance tab.
It will not invent qualifications. Tailoring here means re-emphasizing and rephrasing real experience, never fabricating skills or outcomes. A keyword only lands in "Covered" if your input actually supports it; everything else is honestly reported as a gap. That's deliberate — a resume that claims things you can't back up falls apart in the interview.
The Tracker tab is a self-contained application tracker — no API key, no server call, no account. It runs entirely in the browser.
- Pipeline-first. Each application moves through a status: Applied → Phone Screen → Hiring Manager → On-site → Offer (plus Rejected / Withdrawn). Inside each application you log any number of interview rounds, each with a type, date, and free-text notes — so multiple on-site rounds are just multiple rounds, not extra columns.
- At-a-glance. Summary counts at the top (tracked / active / interviewing / offers), a filter (Active / All / Offers / Archived), the next upcoming interview surfaced on each card, and a "stale" flag on anything active that hasn't moved in two weeks.
- Your data stays yours. Records are saved in the browser's local storage. Export to JSON (full backup, re-importable) or CSV at any time. Because it's browser-local, clearing site data wipes it — export regularly, and note it won't sync across devices.
The CSV export uses the same wide column layout a lot of people already track in, so it pastes back into Google Sheets cleanly:
| Spreadsheet column | Tracker field |
|---|---|
| Company | Company |
| Location | Location |
| Job Title | Job title |
| Pay Range | Pay range |
| JD | Job description link |
| Phone Interview Date/Notes | A round with type Phone |
| Hiring Manager Date/Notes | A round with type Hiring Manager |
| On-site Date/Notes (×N) | One round each with type On-site |
| Offer? | Status set to Offer |
There's no spreadsheet importer yet (it's a good first issue); for now you re-enter existing applications, then export keeps you in sync going forward.
plume/
├── server/
│ └── index.js Express proxy: /api/enhance, /api/tailor, rate limiting, static serving
├── src/
│ ├── App.jsx Tabbed shell (Tailor / Enhance / Tracker) + brand header
│ ├── Tailor.jsx Tailor bullets to a job description + coverage analysis
│ ├── Enhancer.jsx Standalone resume bullet rewriter
│ ├── Tracker.jsx Job application tracker (local storage, export)
│ ├── bullets.jsx Shared bullet rendering + editable metric slots + the formula note
│ ├── hooks.js Responsive + keyboard-shortcut hooks
│ ├── storage.js localStorage helpers for the tracker
│ ├── theme.js Shared color + type tokens
│ ├── main.jsx React entry point
│ └── index.css Minimal global styles
├── public/
│ ├── favicon.svg Feather mark (browser tab)
│ └── logo.png Feather mark (README / sharing)
├── index.html Vite HTML entry
├── vite.config.js Dev proxy: /api → Express
├── .env.example Copy to .env and fill in
└── package.json
Contributions are welcome. Please read CONTRIBUTING.md for the dev setup and conventions, and note that this project ships a Code of Conduct. Security issues should follow SECURITY.md rather than the public issue tracker.
MIT — do whatever you like, no warranty. This is an independent project and is not affiliated with or endorsed by Anthropic.
