The client¶
One executable, seven modes. It keeps your CV data on your machine and sends it, per request, to a server that holds the model API keys and the LaTeX toolchain. No Python, no API key and no LaTeX install of your own.
resumix [global options] MODE [mode options]
Global options work on either side of the mode name: resumix -v submit
posting.txt --out ~/applications and resumix submit -v … do the same
thing.
Which mode¶
flowchart TD
Q1{"What do you have?"}
Q1 -->|"a posting I am about to copy"| CLIP["<b>clipboard</b><br/>watch the clipboard"]
Q1 -->|"postings arriving as files"| WATCH["<b>watch</b><br/>watch a folder"]
Q1 -->|"one file, right now"| Q2{"do you want the<br/>folders, analysis<br/>and spreadsheet?"}
Q1 -->|"a CV I have edited"| REND["<b>render</b><br/>.json or .tex → PDF"]
Q1 -->|"a request id from a failure"| LOGS["<b>logs</b><br/>print the server's log"]
Q2 -->|"yes"| SUB["<b>submit</b>"]
Q2 -->|"no, just the CV"| RAW["<b>submit-raw</b>"]
| Mode | Asks before spending | Detects + analyses | Writes the folder tree | Spreadsheet | Cover letter |
|---|---|---|---|---|---|
clipboard |
yes (or --yes) |
yes | yes | yes | optional |
watch |
CHECK postings only (none with --yes) |
yes | yes | yes | optional |
submit |
yes (or --yes) |
yes | yes | yes | optional |
submit-raw |
no | no | no | no | no |
render |
no — costs no model call | no | no | no | no |
logs |
no | no | no | no | no |
The modes¶
clipboard — copy a posting, get a CV¶
resumix clipboard --out ~/applications
Watches the clipboard. When you copy something that looks like a posting, it
checks with the server, analyses it, prints the analysis and asks — every
posting, whatever its should_apply says:
Paste the posting URL to submit, y = submit without link, n/s = skip, q = quit
> https://jobs.example.com/12345
y, or the posting URL: its CV is queued and the clipboard is watched again at once. CVs are written in the background, one at a time, while you read the next posting; their lines are tagged with the job's folder name ([Acme_Corp_Head_of_IT] …) and held back while a question is on screen. The URL is stored inanalysis.jsonand goes to the spreadsheet.nors: it is filed underdiscarded/.q: clipboard mode stops. The posting stays inworking/, markedPENDINGin itsapproval_status.txt, and is asked about again at the next start — never decided by itsshould_apply.
Stopping. q also works while nothing is being asked: the CV being
written is finished, and the approved ones still queued wait in working/
for the next start. Ctrl-C stops at once.
Startup. Anything left in working/ by a previous run is offered back to
you first: resume, clean or quit. Resume sends each folder where its own
files say — the table under watch's Startup: approved ones go straight to
the CV writer, and the ones still waiting for an answer are asked about
before anything new is read.
With --yes nothing is asked: every posting is submitted, and so is every
leftover waiting for an answer.
On Linux the clipboard needs a helper — apt install xclip (X11) or
wl-clipboard (Wayland) — and a reachable DISPLAY/WAYLAND_DISPLAY. The
client probes it at startup and tells you if it cannot read it, instead of
waiting forever for a change that can never arrive. Over SSH, forward X11 or
use watch, which needs no clipboard at all.
watch — drop files into a folder¶
resumix watch --in ~/Downloads/postings --out ~/applications
Each posting goes through four stages, and the analysis decides the route:
should_apply YES gets its CV without a question, CHECK waits for you, NO is
put aside. The CVs are written in the background, one at a time, while you
read the next posting.
flowchart LR
IN["<b>1 · analysis</b><br/>--in"] -->|"YES"| GEN["<b>3 · waiting for the CV</b><br/>working/"]
IN -->|"CHECK"| ASK["<b>2 · waiting for you</b><br/>working/"]
IN -->|"NO"| DISC["discarded/"]
IN -->|"not a posting,<br/>or a call failed"| ERR["error/"]
ASK -->|"y"| GEN
ASK -->|"d"| DISC
ASK -->|"s / n: later"| ASK
GEN -->|"written"| CV["cv/"]
GEN -->|"a call failed"| ERR
What you can drop:
- A
.txtfile — the posting, read as UTF-8 text. It has to look like a posting (the local check, then the server's), otherwise it lands inerror/with a.logbeside it. Once its analysis is filed, the file is deleted from--in. Any other file — a PDF, a.docx— goes toerror/with nothing sent: paste its text into a.txt. - A folder holding
jd.txtand a validanalysis.json— one fromcv/,discarded/orerror/, for instance. Detection and analysis are skipped: it is copied intoworking/under its company and title, removed from--in, and goes where its own files say (the table under Startup). One that also holds its CV (cv_<name>_<job_title>.jsonor a.tex) is re-rendered — one LaTeX compile, no model call, no new spreadsheet row. A folder with nojd.txt, or whoseanalysis.jsondoes not validate, goes toerror/with alog.loginside saying why.
Names starting with . are ignored, and anything still being copied in is
left alone until it stops changing.
When a posting needs you (CHECK), you see its text, then the analysis, and answer:
y = write the CV (or paste the posting URL), d = discard, s/n = next, q = quit
> https://jobs.example.com/12345
y, or the posting URL: its CV is queued. The URL is stored inanalysis.jsonand goes to the spreadsheet.d: it is filed underdiscarded/.sorn: it is asked again after the others.
Your answer is kept in the folder as approval_status.txt — APPROVED or
DISCARDED (clipboard and submit also write PENDING while they wait for
yours). Anything typed before a posting was on screen is thrown away, so
a stray y never approves one you have not read, and what the inbox and the
CV writer print is held back until you have answered.
Stopping. q works at any time, also when nothing is waiting for you:
nothing new is started, the posting being analysed and the CV being written
are finished, then the watcher exits. Ctrl-C stops at once; the next start
picks up whatever was running, and a posting still in --in is read again.
--yes asks nothing: YES postings get their CV, NO postings are
discarded, and CHECK postings wait in working/ for a run without --yes.
Startup. Whatever an earlier run left in working/ goes where its own
files say, before anything new is read. The rules are tried in this order:
| The folder holds | It goes to |
|---|---|
no jd.txt, or no valid analysis.json |
error/ |
its CV: cv_<name>_<job_title>.json or a .tex |
re-rendered, then cv/ |
no approval_status.txt |
by should_apply: YES to the CV queue, CHECK to you, NO to discarded/ |
approval_status.txt saying APPROVED |
the CV queue |
approval_status.txt saying DISCARDED |
discarded/ |
approval_status.txt saying PENDING |
you: asked about again, whatever should_apply says |
approval_status.txt saying anything else |
error/ |
A folder dropped into --in follows the same table. To retry a job that
failed, move its folder from error/ back into --in; to have a CHECK
posting written without being asked, set its approval_status.txt to
APPROVED first. A loose file left in working/ by an older version goes to
error/: move it back into --in to have it read again.
--in defaults to ./incoming, created if missing. It may not be the same
folder as --out, and the watcher has to be allowed to delete from it.
submit — one file, once¶
resumix submit posting.txt --out ~/applications --yes
The same steps as clipboard, for a single file: analysed, asked about
(unless --yes), and the CV written right there — the command returns once
it is filed. No recovery prompt: a one-off run must not start by asking about
someone else's leftovers. The file you name is read, never moved — unlike
watch, which empties the folder it watches. q at the question leaves the
posting in working/ as PENDING, to be asked about at the next start of
clipboard or watch.
resumix submit posting.txt analysis.json --out ~/applications
With an analysis.json for the posting — from an earlier run's folder, say —
detection and analysis are skipped and it goes straight to the CV; you are
still asked first, unless --yes. Both are copied into the job folder as
jd.txt and analysis.json, so the analysis you passed is the one delivered
beside the CV. One that does not validate stops the command before anything is
sent or written.
submit-raw — just the CV, right here¶
resumix submit-raw posting.txt
resumix submit-raw posting.txt -o cv.pdf -o cv.json -o cv.tex
The CV and nothing else: no detection, no analysis, no question before spending, no cover letter, no spreadsheet, no folders. Files land in the directory you are standing in.
-o is repeatable and the suffix is the whole instruction — .pdf writes the
PDF, .json the document, .tex the LaTeX. A name you chose is overwritten
without asking, because a name you chose is a name you meant. With no -o you
get one PDF named after the posting (posting.txt → posting.pdf), and a
second run counts up to posting_1.pdf rather than replacing it.
render — compile an edited CV¶
resumix render ~/applications/cv/26-01-15/Acme_Head_of_IT/cv_jordan_rivera_head_of_it.json
resumix render cv_jordan_rivera_head_of_it.tex --output /tmp/preview.pdf
Give it the .json document to re-render from the content, or the .tex to
compile a hand-edit. The PDF lands next to the input unless --output says
otherwise. Needs no profile and no posting — just the server — and costs one
LaTeX compile and no model call. A failed render prints the server's log for
that request, which is where the LaTeX error is.
logs — what the server did¶
resumix logs b510f8dff047
Every failure names a request id; this is the other half of that. It prints
that request's server-side log — each model call with its duration and token
counts, and the error that ended it. The server keeps the last couple of
hundred requests, so ask reasonably soon; a pruned id gives 404.
version — which build is this¶
resumix version
Prints version x.y.z and exits. It reads no configuration and talks to no
server, so it answers even when resumix.toml is wrong. Every --verbose run
prints the same line at startup, which is what you want in a bug report.
The number comes from the package metadata bundled into the executable, so it is the version the release workflow built — there is no string in the source to forget to bump.
Postings you have seen before¶
Before anything is spent on a posting, every analysis.json under cv/,
discarded/ and working/ is read back. A posting is the same job when its URL matches, or
when its company and title match — case, accents and spacing ignored. A side
without a URL is compared by company and title only; one without a company or
title, by URL only. Old files are read as they are, never validated: one that
cannot be read is skipped.
The check runs as soon as a posting is analysed, before you are asked, in
every mode. A posting already applied to, discarded or still in flight prints
already applied, already discarded or already in progress with the
earlier folder, and is dropped: nothing is asked and nothing is written. The
mode carries on: the clipboard keeps waiting, submit exits. In watch, a
posting with a JOB_POSTING: <url> line is checked by that URL before
anything is sent, LinkedIn ids included: one already known is deleted from the
inbox with the same message, and costs no call at all.
A posting that is dropped leaves nothing behind: the earlier copy already
holds it. So to redo one, move the earlier folder out of cv/ or
discarded/ first — before you copy the posting again, and instead of
copying a folder you drop back into watch.
Options¶
Global¶
| Flag | What it does |
|---|---|
--server URL |
The resumix server. Default http://localhost:8080. |
--token TOKEN |
Bearer token, if the server requires one. |
--temperature F |
Sampling temperature override, passed to the server. |
--pages N |
Page limit the CV must fit. Default: the server's, which is 2. |
--config FILE |
Use this resumix.toml instead of searching for one. |
--data-dir DIR |
Look for your files here before the current folder. |
-d, --debug |
Fetch the server's log after every call and fold it into log.log. Without it, only failures are fetched. |
-v, --verbose |
Print version x.y.z at startup, then what the server reports about each finished step — tokens, thinking tokens, elapsed, and the reviewer's or the page check's own words — plus the request id and the /healthz attempts. |
Per mode¶
| Flag | Modes | What it does |
|---|---|---|
--out DIR |
clipboard, watch, submit | The output folder (working/, error/, discarded/, cv/). Default: the current folder. |
--in DIR |
watch | The folder to watch. Default: ./incoming, created if missing. |
--cover-letter no\|yes\|letter_only |
clipboard, watch, submit | Also write a cover letter, or write only one. Default no. |
--yes |
clipboard, watch, submit | Submit every valid posting without asking. Unattended runs spend tokens on their own. A posting already applied to, discarded or in flight is skipped, still without asking. In watch it only stops the questions: YES postings get their CV, NO postings are discarded, CHECK postings wait in working/ for a run without --yes. |
--no-xlsx |
clipboard, watch, submit | Do not record delivered CVs in the spreadsheet. |
--resume REQUEST_ID |
submit, submit-raw | Pick up a CV job already running on the server instead of submitting a new one: start from its /status. For a job whose answer never came back. |
-o FILE |
submit-raw | Write one output here: .pdf, .json or .tex. Repeatable. |
--output FILE |
render | Where to write the PDF. Default: beside the input. |
Exit code is 0 when nothing failed, 1 when a posting failed or the client
could not start (a missing file, an unreachable server, a bad config).
Configuration¶
Everything resolves in this order:
flowchart LR
CLI["command line"] --> ENV["environment"] --> TOML["resumix.toml"] --> FOUND["a file in the<br/>current folder"] --> SRV["the server's default"]
resumix.toml¶
Looked for as --config, then $RESUMIX_CONFIG, then resumix.toml in
the current folder.
server_url = "https://resumix.example.run.app"
token = "the-bearer-token" # only if the server requires one
cover_letter = "no" # no | yes | letter_only
# temperature = 0.4 # passed to the server
# pages = 2 # page limit the CV must fit
# timeout = 1800 # seconds to keep polling one CV job
# debug = true # same as -d on every run
# verbose = true # same as -v on every run
[files] # explicit paths win over discovery
# profile = "~/cv/candidate_profile.json"
# candidate_data = "~/cv/candidate_data.json"
# preferences = "~/cv/candidate_preferences.md"
# template = "~/cv/resume.tex.jinja"
# prompt_cv = "~/cv/sys_prompt_cv.txt"
# prompt_highlight = "~/cv/sys_prompt_highlight.txt"
# prompt_review = "~/cv/sys_prompt_review.txt"
# prompt_letter = "~/cv/sys_prompt_letter.txt"
A relative path under [files] is resolved against the config file's own
folder, and a path that does not exist is an error rather than a silent
fallback. The keys are short on purpose: TOML reads
candidate_profile.json = "x" as a dotted key, which is not what anyone
means.
Environment¶
| Variable | What it does |
|---|---|
RESUMIX_API_URL |
The server URL. Beaten by --server. |
RESUMIX_API_TOKEN |
The bearer token. Beaten by --token. |
RESUMIX_CONFIG |
Path to resumix.toml. Beaten by --config. |
Your files¶
Put them in the folder you run from (or in --data-dir) and they are picked
up by name:
| File | What it is | Required |
|---|---|---|
candidate_profile.json |
Everything you have ever done. The model tailors the CV from this — the more complete, the better. | yes |
candidate_data.json |
Name, email, phone, LinkedIn, languages, location, education. Printed by the template as-is; no model is shown it. | yes |
candidate_preferences.md |
What you want from a job, in prose. Scored against each posting. | for analysis |
resume.tex.jinja |
Your own LaTeX template. | no — the server's is used |
sys_prompt_cv.txt, sys_prompt_highlight.txt, sys_prompt_review.txt, sys_prompt_letter.txt |
Your own prompts. | no — same |
Every .png, .jpg and .jpeg in those folders is sent too, under its own
file name — and that name is the whole contract: the server writes each one
next to the .tex, so \includegraphics{photo.jpg} in your template is
satisfied by a photo.jpg sitting next to you. At most 10 per request.
candidate_signature.png is just one such image: the stock template includes
it, and the server has a blank one if you do not.
Start from the fictional set in the repository's examples/candidate/.
What you get¶
~/applications/
├── cv/
│ └── 26-01-15/
│ └── Acme_Corp_Head_of_IT/
│ ├── cv_jordan_rivera_head_of_it.pdf the CV
│ ├── cv_jordan_rivera_head_of_it.tex the LaTeX it was compiled from
│ ├── cv_jordan_rivera_head_of_it.json the content, for re-rendering
│ ├── candidate_signature.png the images your template includes
│ ├── analysis.json match score, salary, skills, gaps
│ ├── jd.txt the posting, whatever the file was called
│ ├── cover_letter.txt with --cover-letter
│ └── log.log every step, plus the server's on failure
├── discarded/26-01-15/… postings you said no to, analysis kept
├── error/26-01-15/… rejected or failed, with a .log beside it
├── working/ in flight, or waiting for you
└── applications.xlsx one row per delivered CV
File names come from your own candidate_data.json, not from the server.
To revise a CV: edit cv_jordan_rivera_head_of_it.json and run resumix render on it —
one LaTeX compile, no model calls. Editing the .tex and rendering that works
too. The spreadsheet's own header row is the schema: rename, reorder or add
columns in your copy and the rows follow.
log.log holds the client's steps, and the server's account of each request
on failure (or on every call with --debug):
2026-01-15T09:30:12 ✍ writing the CV (this takes minutes)...
--- server request b510f8dff047 ---
2026-01-15T09:30:53 INFO [cv.generate] [cv] qwen-max 41.2s | prompt=7104, completion=1832, thinking=903
2026-01-15T09:31:03 INFO [cv.review] [cv] qwen-max 9.8s | prompt=5210, completion=96
One line per model call is what it cost.
What leaves your machine¶
Per request: the posting text, your candidate_profile.json, your
candidate_preferences.md (for the analysis), your candidate_data.json and
every image in the folder. Whether the model provider keeps what it is shown
is between you and whoever runs the server — the same question as with any
hosted model.
No model is ever shown your candidate_data.json. It is sent because the
server compiles the CV to count its pages, and a page count taken with the
contact block missing is not the page count of the CV you will send; it goes
to the LaTeX template and nowhere else.
Nothing at all is sent for text that fails the local length and binary checks, so a copied password or a screenful of code never leaves the machine. The server holds a finished CV in a directory of its own until a couple of hundred newer requests have pushed it out, and keeps nothing else.
Troubleshooting¶
| What you see | What to do |
|---|---|
cannot reach the resumix server at … |
The server is down or the URL is wrong. curl <url>/healthz to check. |
did not come up after 3 attempts |
The server was unreachable on three tries two seconds apart — a cold container may need longer; try again. |
401 / a valid bearer token is required |
The server wants a token: set token in resumix.toml or pass --token. |
candidate_profile.json is required but was not found |
Put it in the current folder, or name it under [files]. |
no clipboard here: neither DISPLAY nor WAYLAND_DISPLAY… |
Install xclip or wl-clipboard, or use watch. |
429 / all job slots are busy |
The server is at capacity. Try again shortly. |
[clipboard] ✗ not a job description (412 chars, nothing sent) |
What you copied is a fragment. Nothing was sent anywhere. |
A posting you wanted lands in error/ |
Read the .log beside it. A wrong "not a job description" verdict usually means the copy grabbed only part of the page. |
A folder you dropped lands in error/ |
It needs jd.txt and a valid analysis.json. The log.log inside it says which was missing. |
| You need more than the error line | Re-run with -d, or resumix logs <request id> while the server still has it. |
working/ is not empty |
A previous run stopped mid-job, a CV was still queued when you pressed q, or a posting waits for your answer (CHECK, or PENDING). watch picks everything up by itself at startup (see its Startup table); clipboard offers to resume or clean. |
| The client died but the job was running | resumix submit posting.txt --resume <request_id> picks it back up. |