Wenlan
Wenlan is a source-backed AI knowledge base and LLM wiki for AI work: organize documents and decisions into pages you can find, inspect, and review.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
Loading the full README. The saved excerpt is shown below.
Paste a resume and a job posting; get back a rewritten resume aimed at that posting, a
reviewable list of every change, and an honest account of what the posting asks for that the
resume does not evidence.
The rule the whole app is built around: tailoring changes how true things are presented, never
what is true. The model may reframe, reorder, merge and re-emphasise existing content. It may not
invent an employer, a date, a degree, a tool, or a metric. Anything genuinely missing shows up under
"Genuine gaps" instead of quietly appearing in the resume.Read the full README ↗Paste a resume and a job posting; get back a rewritten resume aimed at that posting, a reviewable list of every change, and an honest account of what the posting asks for that the resume does not evidence.
The rule the whole app is built around: tailoring changes how true things are presented, never what is true. The model may reframe, reorder, merge and re-emphasise existing content. It may not invent an employer, a date, a degree, a tool, or a metric. Anything genuinely missing shows up under "Genuine gaps" instead of quietly appearing in the resume.
npm install
cp .env.example .env.local # then put a real key in it
npm run dev # http://localhost:3000
ANTHROPIC_API_KEY is read server-side only — the key never reaches the browser. Uploading a resume
and every export work without a key; only the analyse and tailor steps need one, and without it the
app says so plainly rather than failing obscurely.
npm run db:check # is DATABASE_URL reachable and the schema there?
npm test # unit tests (vitest)
npm run build # production build
npm run lint
npm run typecheck
Two model calls, each returning schema-validated JSON via the Anthropic SDK's structured outputs
(output_config.format + zodOutputFormat). They run at the same time: tailoring reads the
posting text directly rather than waiting for the structured analysis, so the slower call sets the
total time instead of the two adding up.
| Step | Route | Model call? | What it does |
|---|---|---|---|
| Extract | POST /api/extract | no | PDF/DOCX/TXT → plain text, entirely on the server |
| Fetch posting | POST /api/fetch-job | no | A posting's link → its readable text, for callers that want it separately |
| Tailor | POST /api/tailor | yes ×2 | Takes pasted text or a link, then analyses and tailors concurrently, streaming progress |
| Close gap | POST /api/close-gap | yes | Your account of some experience → placed into the resume |
| Answer | POST /api/answer | yes | An application question → an answer from the resume and posting, streamed |
Two more routes finish the job: POST /api/cover-letter drafts a letter from the tailored resume,
and POST /api/export renders PDF (via pdfkit), .docx (via docx), Markdown, or plain text —
neither export nor keyword coverage costs anything.
The posting can be pasted, or given as a URL. A link goes straight to tailoring: the server fetches the page and strips it to readable text as the first step of the same streamed request, rather than making you fetch, read it, and then ask.
Fetching a URL that someone else supplies, from inside the deployment, is the classic server-side
request forgery shape, so src/lib/fetchJob.ts refuses anything internal: non-http schemes,
localhost-style names, literal private addresses, and hosts that resolve to one. Redirects are
followed manually and re-checked at each hop, because a public URL redirecting to 169.254.169.254
is exactly how that guard gets walked around. Responses are capped at 2 MB and 12 seconds.
Plenty of job boards block automated visits or build the page in the browser. When that happens the error says so and tells you to paste instead, rather than failing obscurely.
Most of the wait is the model writing: the tailoring call emits the whole resume as JSON plus a change log, which for a two-page resume is roughly 2,500 output tokens. Output generation is the slow part of any model call, so three things matter:
/api/tailor streams. It returns newline-delimited JSON — progress lines while the model
writes, then one result line — so the page shows characters accumulating instead of a dead spinner,
and the connection never looks idle to a host that times those out.Roughly 60% of the tailoring output is duplicated text: every rewritten bullet appears once in the
resume and again as before/after in the change log. That is the price of a reviewable, revertible
edit list, and the obvious next optimisation if it needs to be faster still.
The default model is Haiku 4.5 ($1/$5 per million input/output tokens). This is bounded rewriting against a schema rather than open reasoning, so the cheap model does it well. Three other choices pull in the same direction:
effort parameter at all, and the app
omits it rather than sending one the model rejects.Set TAILOR_MODEL to trade money for better prose:
TAILOR_MODEL=claude-sonnet-5 # $2/$10
TAILOR_MODEL=claude-opus-5 # $5/$25, the strongest rewriting
The posting's keywords are deduplicated case-insensitively before anything uses them: the schema asks for a unique list, but asking is not enforcing, and a repeat rendered the same chip twice.
Keyword coverage is not the model's opinion — src/lib/keywords.ts does a literal whole-word
match of the posting's keywords against the resume text, before and after tailoring, because that is
what an applicant tracking system actually does. The chips show where each keyword was found.
Upload or paste once and it comes back on every later visit — the box is prefilled and a bar says which resume it is and when it was kept. Forget it clears both the box and the stored copy.
This is the beginning of the README. Read the rest at the original. Read the full README ↗