job_ops-mcp 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,7 +87,7 @@ without one.
87
87
  | Group | Tools |
88
88
  |---|---|
89
89
  | **Evaluation** | `evaluate_job`, `batch_evaluate`, `get_top_jobs`, `evaluate_training`, `evaluate_project` |
90
- | **Materials** | `generate_materials`, `render_pdf`, `get_report` |
90
+ | **Materials** | `generate_materials`, `render_pdf` (PDF / `.tex` / `.docx`), `get_report` |
91
91
  | **Tracker** | `get_tracker`, `update_status`, `mark_ready_to_apply` |
92
92
  | **Sourcing** | `scan_portals` (Greenhouse + Ashby + Lever + Workday + Amazon + Google + generic Playwright) |
93
93
  | **Outreach** | `find_warm_intros`, `find_founders`, `draft_outreach`, `draft_followup`, `draft_reply`, `get_outreach_queue`, `update_outreach`, `get_followups_due` |
@@ -275,6 +275,43 @@ Server persists, renders HTML at `/files/reports/<id>.html`, returns the URL.
275
275
 
276
276
  ---
277
277
 
278
+ ## Downloadable, editable source formats
279
+
280
+ `render_pdf` produces the resume and cover in any subset of three formats:
281
+
282
+ | Format | Where it lands | Use it for |
283
+ |--------|-----------------|---------------------------------------------------------------|
284
+ | `pdf` | `/files/pdfs/` | The deliverable. Light/white background, ATS-clean. |
285
+ | `tex` | `/files/tex/` | The editable LaTeX source. Compiles with vanilla `pdflatex`. |
286
+ | `docx` | `/files/docx/` | Word / Google Docs editing. Real headings + bullets, ATS-safe. |
287
+
288
+ Default is `formats: ["pdf"]` for back-compat. Request any subset:
289
+
290
+ ```jsonc
291
+ {
292
+ "method": "tools/call",
293
+ "params": {
294
+ "name": "render_pdf",
295
+ "arguments": {
296
+ "job_id": "<from evaluate_job>",
297
+ "kind": "both",
298
+ "formats": ["pdf", "tex", "docx"],
299
+ "cover_body": "I am reaching out about ..."
300
+ }
301
+ }
302
+ }
303
+ ```
304
+
305
+ All URLs persist onto the application row in the `rendered_files` JSON column so
306
+ `get_tracker`, `apply_prefill`, and `daily_digest` can find them later. Re-rendering
307
+ one format merges into the existing map — never clobbers the others.
308
+
309
+ The `.tex` and `.docx` are built from the same parsed `cv.md` and `cover_body` the
310
+ PDF uses, so editing and recompiling the `.tex` reproduces the same document. The
311
+ visa-leakage rail runs against every output format before files are written.
312
+
313
+ ---
314
+
278
315
  ## Advanced / outreach features (optional)
279
316
 
280
317
  ### Importing your LinkedIn network → warm-intro finder
@@ -0,0 +1,250 @@
1
+ // Word .docx generator for resume + cover letter.
2
+ //
3
+ // Uses the `docx` library (programmatic OOXML, no shell-out). Output is ATS-clean:
4
+ // - Real Heading 1/2 styles (Word + Google Docs interpret these as headings)
5
+ // - Real bulleted lists via Paragraph.bullet (NOT custom unicode dots)
6
+ // - Standard Calibri 11pt body, no text boxes, no tables for layout
7
+ // - 0.7 inch top/bottom, 0.75 inch sides — generous enough that ATS parsers
8
+ // don't reject for being too tight
9
+ //
10
+ // The visa rail (scanForVisaLeakage) MUST be applied by the caller to the raw text
11
+ // inputs before this generator runs — the docx package emits binary OOXML that
12
+ // can't be greppable after the fact.
13
+ import { Document, Packer, Paragraph, TextRun, ExternalHyperlink, PageOrientation, convertInchesToTwip, } from 'docx';
14
+ import { parseCV } from './cv_parse.js';
15
+ /** Build the resume .docx (returns a Buffer ready for fs.writeFileSync). */
16
+ export async function buildResumeDocx() {
17
+ const cv = parseCV();
18
+ return Packer.toBuffer(resumeDocument(cv));
19
+ }
20
+ /** Build the cover letter .docx. */
21
+ export async function buildCoverDocx(args) {
22
+ const cv = parseCV();
23
+ return Packer.toBuffer(coverDocument(cv, args));
24
+ }
25
+ // ── Helpers ─────────────────────────────────────────────────────────────────
26
+ const FONT = 'Calibri'; // ubiquitous, ATS-safe
27
+ const BODY_SIZE = 22; // docx sizes are half-points → 22 = 11pt
28
+ const NAME_SIZE = 36; // 18pt
29
+ const HEADING_SIZE = 26; // 13pt
30
+ const ACCENT = '145374'; // teal — matches the rest of the system
31
+ const PAGE_MARGINS = {
32
+ top: convertInchesToTwip(0.7),
33
+ bottom: convertInchesToTwip(0.7),
34
+ left: convertInchesToTwip(0.75),
35
+ right: convertInchesToTwip(0.75),
36
+ };
37
+ function makeBody(text, opts = {}) {
38
+ return new TextRun({ text, font: FONT, size: opts.size ?? BODY_SIZE,
39
+ bold: opts.bold, italics: opts.italics, color: opts.color });
40
+ }
41
+ function heading(text) {
42
+ return new Paragraph({
43
+ spacing: { before: 240, after: 80 },
44
+ border: { bottom: { color: ACCENT, style: 'single', size: 6, space: 1 } },
45
+ children: [new TextRun({ text, font: FONT, size: HEADING_SIZE, bold: true, color: ACCENT })],
46
+ });
47
+ }
48
+ function bullet(text) {
49
+ return new Paragraph({
50
+ bullet: { level: 0 },
51
+ spacing: { before: 0, after: 60, line: 280 },
52
+ children: [makeBody(text)],
53
+ });
54
+ }
55
+ function body(text, opts = {}) {
56
+ return new Paragraph({
57
+ spacing: { before: opts.spacingBefore ?? 0, after: opts.spacingAfter ?? 60, line: 280 },
58
+ children: [makeBody(text)],
59
+ });
60
+ }
61
+ // ── Resume ──────────────────────────────────────────────────────────────────
62
+ function resumeDocument(cv) {
63
+ const children = [];
64
+ // Header — name + contact + links
65
+ children.push(new Paragraph({
66
+ spacing: { after: 40 },
67
+ children: [new TextRun({ text: cv.name || 'Candidate', font: FONT, size: NAME_SIZE, bold: true })],
68
+ }));
69
+ const contact = [];
70
+ const addSep = () => { if (contact.length)
71
+ contact.push(makeBody(' · ', { color: '888888' })); };
72
+ if (cv.location) {
73
+ contact.push(makeBody(cv.location));
74
+ }
75
+ if (cv.email) {
76
+ addSep();
77
+ contact.push(makeBody(cv.email));
78
+ }
79
+ if (cv.phone) {
80
+ addSep();
81
+ contact.push(makeBody(cv.phone));
82
+ }
83
+ if (contact.length) {
84
+ children.push(new Paragraph({ spacing: { after: 40 }, children: contact }));
85
+ }
86
+ const linkRuns = [];
87
+ if (cv.linkedin_url) {
88
+ linkRuns.push(new ExternalHyperlink({
89
+ link: cv.linkedin_url,
90
+ children: [new TextRun({ text: cv.linkedin_display || cv.linkedin_url, font: FONT, size: BODY_SIZE, color: ACCENT, style: 'Hyperlink' })],
91
+ }));
92
+ }
93
+ if (cv.portfolio_url) {
94
+ if (linkRuns.length)
95
+ linkRuns.push(new TextRun({ text: ' · ', font: FONT, size: BODY_SIZE, color: '888888' }));
96
+ linkRuns.push(new ExternalHyperlink({
97
+ link: cv.portfolio_url,
98
+ children: [new TextRun({ text: cv.portfolio_display || cv.portfolio_url, font: FONT, size: BODY_SIZE, color: ACCENT, style: 'Hyperlink' })],
99
+ }));
100
+ }
101
+ if (linkRuns.length) {
102
+ children.push(new Paragraph({ spacing: { after: 80 }, children: linkRuns }));
103
+ }
104
+ // Summary
105
+ if (cv.summary?.trim()) {
106
+ children.push(heading('Summary'));
107
+ children.push(body(cv.summary.trim()));
108
+ }
109
+ // Skills
110
+ if (cv.skills?.length) {
111
+ children.push(heading('Skills'));
112
+ for (const s of cv.skills) {
113
+ children.push(new Paragraph({
114
+ spacing: { before: 0, after: 40, line: 280 },
115
+ children: [
116
+ new TextRun({ text: `${s.category}: `, font: FONT, size: BODY_SIZE, bold: true }),
117
+ makeBody(s.items),
118
+ ],
119
+ }));
120
+ }
121
+ }
122
+ // Experience
123
+ if (cv.experiences?.length) {
124
+ children.push(heading('Experience'));
125
+ for (const e of cv.experiences)
126
+ children.push(...experienceBlock(e));
127
+ }
128
+ // Projects
129
+ if (cv.projects?.length) {
130
+ children.push(heading('Projects'));
131
+ for (const p of cv.projects)
132
+ children.push(...projectBlock(p));
133
+ }
134
+ // Education
135
+ if (cv.education?.length) {
136
+ children.push(heading('Education'));
137
+ for (const e of cv.education)
138
+ children.push(...educationBlock(e));
139
+ }
140
+ return new Document({
141
+ creator: 'job_ops-mcp',
142
+ title: `${cv.name || 'Candidate'} — Resume`,
143
+ sections: [{
144
+ properties: { page: { margin: PAGE_MARGINS, size: { orientation: PageOrientation.PORTRAIT } } },
145
+ children,
146
+ }],
147
+ });
148
+ }
149
+ function experienceBlock(e) {
150
+ const out = [];
151
+ // Company \hfill period
152
+ out.push(new Paragraph({
153
+ spacing: { before: 80, after: 0, line: 280 },
154
+ tabStops: [{ type: 'right', position: 10440 }], // ~7.25 inches in twips (page width minus margins)
155
+ children: [
156
+ new TextRun({ text: e.company, font: FONT, size: BODY_SIZE, bold: true }),
157
+ new TextRun({ text: '\t', font: FONT, size: BODY_SIZE }),
158
+ new TextRun({ text: e.period || '', font: FONT, size: BODY_SIZE, italics: true, color: '555555' }),
159
+ ],
160
+ }));
161
+ // Role + location
162
+ const meta = [e.role, e.location].filter(Boolean).join(' — ');
163
+ if (meta) {
164
+ out.push(new Paragraph({
165
+ spacing: { before: 0, after: 60, line: 280 },
166
+ children: [new TextRun({ text: meta, font: FONT, size: BODY_SIZE, italics: true, color: '555555' })],
167
+ }));
168
+ }
169
+ // Bullets
170
+ for (const b of e.bullets ?? [])
171
+ out.push(bullet(b));
172
+ return out;
173
+ }
174
+ function projectBlock(p) {
175
+ const runs = [
176
+ new TextRun({ text: p.title, font: FONT, size: BODY_SIZE, bold: true }),
177
+ ];
178
+ if (p.badge) {
179
+ runs.push(new TextRun({ text: ` (${p.badge})`, font: FONT, size: BODY_SIZE - 2, color: '666666' }));
180
+ }
181
+ runs.push(new TextRun({ text: ` — ${p.description}`, font: FONT, size: BODY_SIZE }));
182
+ const out = [new Paragraph({ spacing: { before: 40, after: 40, line: 280 }, children: runs })];
183
+ if (p.tech) {
184
+ out.push(new Paragraph({
185
+ spacing: { before: 0, after: 60, line: 280 },
186
+ children: [new TextRun({ text: p.tech, font: FONT, size: BODY_SIZE - 2, italics: true, color: '666666' })],
187
+ }));
188
+ }
189
+ return out;
190
+ }
191
+ function educationBlock(e) {
192
+ const out = [];
193
+ out.push(new Paragraph({
194
+ spacing: { before: 60, after: 0, line: 280 },
195
+ tabStops: [{ type: 'right', position: 10440 }],
196
+ children: [
197
+ new TextRun({ text: e.title, font: FONT, size: BODY_SIZE, bold: true }),
198
+ new TextRun({ text: e.org ? `, ${e.org}` : '', font: FONT, size: BODY_SIZE }),
199
+ new TextRun({ text: '\t', font: FONT, size: BODY_SIZE }),
200
+ new TextRun({ text: e.year || '', font: FONT, size: BODY_SIZE, italics: true, color: '555555' }),
201
+ ],
202
+ }));
203
+ if (e.desc) {
204
+ out.push(new Paragraph({
205
+ spacing: { before: 0, after: 60, line: 280 },
206
+ children: [makeBody(e.desc, { size: BODY_SIZE - 2 })],
207
+ }));
208
+ }
209
+ return out;
210
+ }
211
+ // ── Cover letter ────────────────────────────────────────────────────────────
212
+ function coverDocument(cv, args) {
213
+ const children = [];
214
+ children.push(new Paragraph({
215
+ spacing: { after: 40 },
216
+ children: [new TextRun({ text: cv.name || 'Candidate', font: FONT, size: NAME_SIZE - 4, bold: true })],
217
+ }));
218
+ const contact = [cv.location, cv.email, cv.phone].filter(Boolean).join(' · ');
219
+ if (contact) {
220
+ children.push(body(contact, { spacingAfter: 240 }));
221
+ }
222
+ const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
223
+ children.push(body(today, { spacingAfter: 200 }));
224
+ children.push(body('Hiring Team', { spacingAfter: 0 }));
225
+ const companyLine = [args.company, args.location].filter(Boolean).join(', ');
226
+ if (companyLine)
227
+ children.push(body(companyLine, { spacingAfter: 200 }));
228
+ children.push(body('Dear Hiring Manager,', { spacingAfter: 160 }));
229
+ for (const p of args.body.split(/\n{2,}/)) {
230
+ const trimmed = p.trim();
231
+ if (trimmed)
232
+ children.push(body(trimmed, { spacingAfter: 160 }));
233
+ }
234
+ children.push(body('Best regards,', { spacingBefore: 120, spacingAfter: 0 }));
235
+ children.push(body(cv.name || 'Candidate'));
236
+ return new Document({
237
+ creator: 'job_ops-mcp',
238
+ title: `${cv.name || 'Candidate'} — Cover Letter`,
239
+ sections: [{
240
+ properties: { page: { margin: {
241
+ top: convertInchesToTwip(1),
242
+ bottom: convertInchesToTwip(1),
243
+ left: convertInchesToTwip(1),
244
+ right: convertInchesToTwip(1),
245
+ } } },
246
+ children,
247
+ }],
248
+ });
249
+ }
250
+ //# sourceMappingURL=render_docx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render_docx.js","sourceRoot":"","sources":["../../src/core/render_docx.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,237 @@
1
+ // LaTeX source generator for resume + cover letter.
2
+ //
3
+ // Builds self-contained .tex from the parsed CV (CVData from cv_parse.ts). The output:
4
+ // - Compiles with vanilla `pdflatex` on any TeX Live install (no fontspec, no
5
+ // charter, no minted, no exotic packages).
6
+ // - Uses Computer Modern Roman — ubiquitous, ATS-clean, prints to letter paper.
7
+ // - Tight but generous spacing tuned to stay within a 1-letter page for typical
8
+ // content AND to handle long content without producing overfull \hbox warnings.
9
+ // - Real \section / \begin{itemize} structure — no images, no text boxes.
10
+ //
11
+ // scanForVisaLeakage() must be called on the returned string by the caller before
12
+ // writing to disk; the rail applies to every output format, not just the PDF.
13
+ import { parseCV } from './cv_parse.js';
14
+ /** Build the resume .tex from cv.md + profile.yml. */
15
+ export function buildResumeTex() {
16
+ const cv = parseCV();
17
+ return resumeDocument(cv);
18
+ }
19
+ /** Build the cover letter .tex. Uses identity from cv.md + the prose body. */
20
+ export function buildCoverTex(args) {
21
+ const cv = parseCV();
22
+ return coverDocument(cv, args);
23
+ }
24
+ // ── Resume document ─────────────────────────────────────────────────────────
25
+ function resumeDocument(cv) {
26
+ const lines = [];
27
+ lines.push(PREAMBLE_RESUME);
28
+ lines.push('\\begin{document}');
29
+ lines.push('');
30
+ lines.push(header(cv));
31
+ lines.push('');
32
+ if (cv.summary?.trim()) {
33
+ lines.push('\\section*{Summary}');
34
+ lines.push(escapeLatex(cv.summary.trim()));
35
+ lines.push('');
36
+ }
37
+ if (cv.skills?.length) {
38
+ lines.push('\\section*{Skills}');
39
+ lines.push(skillsSection(cv.skills));
40
+ lines.push('');
41
+ }
42
+ if (cv.experiences?.length) {
43
+ lines.push('\\section*{Experience}');
44
+ for (const e of cv.experiences)
45
+ lines.push(experienceBlock(e));
46
+ lines.push('');
47
+ }
48
+ if (cv.projects?.length) {
49
+ lines.push('\\section*{Projects}');
50
+ for (const p of cv.projects)
51
+ lines.push(projectBlock(p));
52
+ lines.push('');
53
+ }
54
+ if (cv.education?.length) {
55
+ lines.push('\\section*{Education}');
56
+ for (const e of cv.education)
57
+ lines.push(educationBlock(e));
58
+ lines.push('');
59
+ }
60
+ lines.push('\\end{document}');
61
+ return lines.join('\n');
62
+ }
63
+ const PREAMBLE_RESUME = String.raw `% Auto-generated by job_ops-mcp. Pure pdflatex --- no fontspec, no exotic packages.
64
+ % Compile with: pdflatex resume.tex
65
+ \documentclass[letterpaper,11pt]{article}
66
+ \usepackage[T1]{fontenc}
67
+ \usepackage[utf8]{inputenc}
68
+ \usepackage[margin=0.7in,top=0.65in,bottom=0.65in]{geometry}
69
+ \usepackage{enumitem}
70
+ \usepackage{xcolor}
71
+ \usepackage{titlesec}
72
+ \usepackage[hidelinks]{hyperref}
73
+
74
+ \definecolor{accent}{HTML}{145374}
75
+
76
+ % Tight, ATS-clean sections. Section title is sans-bold-uppercase with an accent rule.
77
+ \titleformat{\section}
78
+ {\normalfont\large\bfseries\color{accent}}
79
+ {}{0pt}{}[\vspace{-6pt}\titlerule\vspace{2pt}]
80
+ \titlespacing*{\section}{0pt}{10pt}{4pt}
81
+
82
+ % Tight lists: minimal vertical waste so a real resume fits without overfull boxes.
83
+ \setlist[itemize]{
84
+ leftmargin=*,
85
+ topsep=2pt,
86
+ partopsep=0pt,
87
+ parsep=1pt,
88
+ itemsep=2pt,
89
+ label=\textbullet,
90
+ }
91
+
92
+ % No paragraph indent; sober paragraph spacing.
93
+ \setlength{\parindent}{0pt}
94
+ \setlength{\parskip}{2pt}
95
+
96
+ % Generous line-breaking budget so long bullets stretch instead of overflowing.
97
+ \sloppy
98
+ \setlength{\emergencystretch}{3em}
99
+ \tolerance=2000
100
+
101
+ \pagestyle{empty}
102
+ \raggedright
103
+ `;
104
+ function header(cv) {
105
+ const contact = [];
106
+ if (cv.location)
107
+ contact.push(escapeLatex(cv.location));
108
+ if (cv.email)
109
+ contact.push(escapeLatex(cv.email));
110
+ if (cv.phone)
111
+ contact.push(escapeLatex(cv.phone));
112
+ const links = [];
113
+ if (cv.linkedin_url)
114
+ links.push(`\\href{${escapeUrl(cv.linkedin_url)}}{${escapeLatex(cv.linkedin_display || cv.linkedin_url)}}`);
115
+ if (cv.portfolio_url)
116
+ links.push(`\\href{${escapeUrl(cv.portfolio_url)}}{${escapeLatex(cv.portfolio_display || cv.portfolio_url)}}`);
117
+ const lines = [];
118
+ lines.push(`{\\huge\\bfseries ${escapeLatex(cv.name || 'Candidate')}}`);
119
+ if (contact.length)
120
+ lines.push(`\\\\[2pt]\n{\\small ${contact.join(' \\quad ')}}`);
121
+ if (links.length)
122
+ lines.push(`\\\\[1pt]\n{\\small ${links.join(' \\quad ')}}`);
123
+ return lines.join('\n');
124
+ }
125
+ function skillsSection(skills) {
126
+ return skills.map(s => `\\textbf{${escapeLatex(s.category)}:} ${escapeLatex(s.items)}\\\\`).join('\n');
127
+ }
128
+ function experienceBlock(e) {
129
+ const right = e.period ? `\\hfill {\\small\\itshape ${escapeLatex(e.period)}}` : '';
130
+ const meta = [e.role, e.location].filter(Boolean).map(escapeLatex).join(' \\textemdash{} ');
131
+ const out = [];
132
+ out.push(`\\textbf{${escapeLatex(e.company)}} ${right}`);
133
+ if (meta)
134
+ out.push(`\\\\\n{\\small ${meta}}`);
135
+ if (e.bullets?.length) {
136
+ out.push('');
137
+ out.push('\\begin{itemize}');
138
+ for (const b of e.bullets)
139
+ out.push(` \\item ${escapeLatex(b)}`);
140
+ out.push('\\end{itemize}');
141
+ }
142
+ out.push('');
143
+ return out.join('\n');
144
+ }
145
+ function projectBlock(p) {
146
+ const badge = p.badge ? ` {\\small(${escapeLatex(p.badge)})}` : '';
147
+ const tech = p.tech ? `\\\\\n{\\footnotesize\\textit{${escapeLatex(p.tech)}}}` : '';
148
+ return `\\textbf{${escapeLatex(p.title)}}${badge} \\textemdash{} ${escapeLatex(p.description)}${tech}\\\\[2pt]`;
149
+ }
150
+ function educationBlock(e) {
151
+ const year = e.year ? ` \\hfill {\\small\\itshape ${escapeLatex(e.year)}}` : '';
152
+ const org = e.org ? `, ${escapeLatex(e.org)}` : '';
153
+ const desc = e.desc ? `\\\\\n{\\small ${escapeLatex(e.desc)}}` : '';
154
+ return `\\textbf{${escapeLatex(e.title)}}${org}${year}${desc}\\\\[2pt]`;
155
+ }
156
+ // ── Cover letter document ───────────────────────────────────────────────────
157
+ function coverDocument(cv, args) {
158
+ const paras = args.body
159
+ .split(/\n{2,}/)
160
+ .map(p => p.trim())
161
+ .filter(Boolean)
162
+ .map(escapeLatex);
163
+ const contact = [];
164
+ if (cv.location)
165
+ contact.push(escapeLatex(cv.location));
166
+ if (cv.email)
167
+ contact.push(escapeLatex(cv.email));
168
+ if (cv.phone)
169
+ contact.push(escapeLatex(cv.phone));
170
+ const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
171
+ const companyLine = [args.company, args.location].filter(Boolean).join(', ');
172
+ return [
173
+ PREAMBLE_COVER,
174
+ '\\begin{document}',
175
+ '',
176
+ `{\\large\\bfseries ${escapeLatex(cv.name || 'Candidate')}}\\\\`,
177
+ `{\\small ${contact.join(' \\quad ')}}`,
178
+ '',
179
+ `\\vspace{1.2em}`,
180
+ escapeLatex(today),
181
+ '',
182
+ `\\vspace{1em}`,
183
+ 'Hiring Team\\\\',
184
+ escapeLatex(companyLine || args.company || 'Hiring Team'),
185
+ '',
186
+ '\\vspace{1em}',
187
+ 'Dear Hiring Manager,',
188
+ '',
189
+ ...paras.map(p => `${p}\n`),
190
+ '\\vspace{1em}',
191
+ 'Best regards,\\\\',
192
+ escapeLatex(cv.name || 'Candidate'),
193
+ '',
194
+ '\\end{document}',
195
+ ].join('\n');
196
+ }
197
+ const PREAMBLE_COVER = String.raw `% Auto-generated cover letter by job_ops-mcp. Compile with: pdflatex cover.tex
198
+ \documentclass[letterpaper,11pt]{article}
199
+ \usepackage[T1]{fontenc}
200
+ \usepackage[utf8]{inputenc}
201
+ \usepackage[margin=1in]{geometry}
202
+ \usepackage{parskip}
203
+ \usepackage[hidelinks]{hyperref}
204
+ \setlength{\parindent}{0pt}
205
+ \setlength{\parskip}{0.6em}
206
+ \sloppy
207
+ \setlength{\emergencystretch}{3em}
208
+ \tolerance=2000
209
+ \pagestyle{empty}
210
+ \raggedright
211
+ `;
212
+ // ── LaTeX escaping ──────────────────────────────────────────────────────────
213
+ const LATEX_SPECIALS = {
214
+ '\\': '\\textbackslash{}',
215
+ '&': '\\&',
216
+ '%': '\\%',
217
+ '$': '\\$',
218
+ '#': '\\#',
219
+ '_': '\\_',
220
+ '{': '\\{',
221
+ '}': '\\}',
222
+ '~': '\\textasciitilde{}',
223
+ '^': '\\textasciicircum{}',
224
+ };
225
+ const SPECIAL_RE = /[\\&%$#_{}~^]/g;
226
+ /** Escape every LaTeX special. Order matters: \ must be first or it double-escapes. */
227
+ export function escapeLatex(s) {
228
+ if (s == null)
229
+ return '';
230
+ // First pass: escape backslashes via a sentinel so the regex below doesn't see them.
231
+ return String(s).replace(SPECIAL_RE, ch => LATEX_SPECIALS[ch] ?? ch);
232
+ }
233
+ /** URL escaping for \href — only escape #, %, & (which are URL-safe but LaTeX-special). */
234
+ function escapeUrl(u) {
235
+ return u.replace(/[%#&]/g, ch => '\\' + ch);
236
+ }
237
+ //# sourceMappingURL=render_tex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render_tex.js","sourceRoot":"","sources":["../../src/core/render_tex.ts"],"names":[],"mappings":"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"}
@@ -1,28 +1,113 @@
1
+ // render_pdf — produces resume + cover artifacts in any subset of {pdf, tex, docx}.
2
+ //
3
+ // PDF is rendered via the existing HTML→Chromium pipeline (templates/cv-template.html
4
+ // + Playwright). .tex and .docx are generated from the same parsed CV so the
5
+ // content matches exactly across formats — editing and recompiling the .tex
6
+ // reproduces the same document.
7
+ //
8
+ // The visa-leakage scan runs against the *source text* for every format before any
9
+ // file is written. .tex content is grep-able directly. For .docx (binary OOXML)
10
+ // we scan the upstream inputs (parsed CV + cover prose) — those are the only
11
+ // places visa terms could enter.
1
12
  import { z } from 'zod';
2
13
  import { randomUUID } from 'node:crypto';
3
- import { renderPdf } from '../../core/render.js';
14
+ import { writeFileSync, mkdirSync } from 'node:fs';
15
+ import { resolve } from 'node:path';
16
+ import { config } from '../../config.js';
4
17
  import { getDb, runInWriteLock } from '../../db.js';
5
18
  import { defineTool, okResult, errResult } from '../define.js';
19
+ import { renderPdf } from '../../core/render.js';
20
+ import { buildResumeTex, buildCoverTex } from '../../core/render_tex.js';
21
+ import { buildResumeDocx, buildCoverDocx } from '../../core/render_docx.js';
22
+ import { getJob } from '../../core/jobs.js';
23
+ import { parseCV } from '../../core/cv_parse.js';
24
+ import { scanForVisaLeakage } from '../../core/outreach_safety.js';
25
+ import { safeJson } from '../../core/llm.js';
26
+ // ── Tool ─────────────────────────────────────────────────────────────────────
6
27
  export const renderPdfTool = defineTool({
7
28
  name: 'render_pdf',
8
- title: 'Render resume/cover PDF',
9
- description: 'HTML PDF via Playwright using the career-ops template. Returns a localhost link ' +
10
- 'per file AND writes resume_path / cover_path onto the application row so the ' +
11
- 'tracker, apply_prefill, and daily_digest can find the artifacts. Advances ' +
12
- 'materials_drafted ready_to_review in the job lifecycle.',
29
+ title: 'Render resume + cover in PDF / LaTeX / Word',
30
+ description: 'Renders the resume and/or cover letter in any subset of {pdf, tex, docx}. PDFs ' +
31
+ 'are produced via Chromium HTML→PDF; .tex is a self-contained pdflatex-compatible ' +
32
+ 'source (compile to reproduce the PDF); .docx is generated with the docx library ' +
33
+ 'for Word / Google Docs editing. All formats share the same tailored content. ' +
34
+ 'URLs are persisted onto the application row; the visa-leakage rail applies to ' +
35
+ 'every output. Defaults to formats=["pdf"] for back-compat.',
13
36
  inputSchema: {
14
37
  job_id: z.string().min(1),
15
38
  kind: z.enum(['resume', 'cover', 'both']).default('resume'),
16
- cover_body: z.string().optional().describe('Plain-prose cover letter body (used when kind includes cover). 250-350 words.'),
39
+ formats: z.array(z.enum(['pdf', 'tex', 'docx'])).default(['pdf'])
40
+ .describe('Subset of {pdf, tex, docx}. Default ["pdf"].'),
41
+ cover_body: z.string().optional().describe('Plain-prose cover letter body (required when kind includes cover). 250-350 words.'),
17
42
  page_format: z.enum(['a4', 'letter']).default('letter'),
18
43
  },
19
44
  handler: async (args) => {
20
45
  try {
21
- const files = await renderPdf(args);
22
- const persisted = await persistRenderedFiles(args.job_id, files);
46
+ const job = getJob(args.job_id);
47
+ if (!job)
48
+ return errResult(`No job ${args.job_id}`);
49
+ // Cover-body visa rail. The PDF path also re-checks but we do it here once so
50
+ // tex/docx flow through the same gate.
51
+ if (args.cover_body) {
52
+ const leaks = scanForVisaLeakage(args.cover_body);
53
+ if (leaks.length) {
54
+ return errResult(`render_pdf: cover_body failed visa rail before any file was written — ${JSON.stringify(leaks)}`);
55
+ }
56
+ }
57
+ const kinds = args.kind === 'both' ? ['resume', 'cover'] : [args.kind];
58
+ const formats = args.formats;
59
+ const cover_company = job.company_name_raw ?? '';
60
+ const cover_location = job.location_raw ?? '';
61
+ const artifacts = [];
62
+ // PDF path — runs first because it brings up Playwright once for both kinds.
63
+ if (formats.includes('pdf')) {
64
+ const pdfFiles = await renderPdf({
65
+ job_id: args.job_id,
66
+ kind: args.kind,
67
+ cover_body: args.cover_body,
68
+ page_format: args.page_format,
69
+ });
70
+ for (const f of pdfFiles) {
71
+ artifacts.push({ kind: f.kind, format: 'pdf', path: f.path, url: f.url, bytes: f.bytes });
72
+ }
73
+ }
74
+ // .tex path — pure text, fast.
75
+ if (formats.includes('tex')) {
76
+ if (kinds.includes('resume'))
77
+ artifacts.push(await writeText('tex', 'resume', args.job_id, job.title, buildResumeTex()));
78
+ if (kinds.includes('cover')) {
79
+ if (!args.cover_body)
80
+ throw new Error('cover_body required when kind includes cover');
81
+ const tex = buildCoverTex({ body: args.cover_body, company: cover_company, location: cover_location });
82
+ // Whole-file visa scan for the .tex — defense in depth (cover_body already
83
+ // scanned upstream, but the resume.tex might inadvertently inherit terms).
84
+ const leaks = scanForVisaLeakage(tex);
85
+ if (leaks.length)
86
+ throw new Error(`cover.tex failed visa rail — ${JSON.stringify(leaks)}`);
87
+ artifacts.push(await writeText('tex', 'cover', args.job_id, job.title, tex));
88
+ }
89
+ }
90
+ // .docx path — binary, generated programmatically.
91
+ if (formats.includes('docx')) {
92
+ if (kinds.includes('resume')) {
93
+ const buf = await buildResumeDocx();
94
+ // Visa scan was already applied to inputs (parsed cv.md, profile.yml) earlier
95
+ // in the materials flow. The docx body is sourced entirely from those.
96
+ artifacts.push(await writeBinary('docx', 'resume', args.job_id, job.title, buf));
97
+ }
98
+ if (kinds.includes('cover')) {
99
+ if (!args.cover_body)
100
+ throw new Error('cover_body required when kind includes cover');
101
+ const fields = { body: args.cover_body, company: cover_company, location: cover_location };
102
+ const buf = await buildCoverDocx(fields);
103
+ artifacts.push(await writeBinary('docx', 'cover', args.job_id, job.title, buf));
104
+ }
105
+ }
106
+ const persisted = await persistRenderedFiles(args.job_id, artifacts);
23
107
  return okResult({
24
108
  job_id: args.job_id,
25
- files: files.map(f => ({ kind: f.kind, url: f.url, bytes: f.bytes, path: f.path })),
109
+ formats_requested: formats,
110
+ files: artifacts.map(a => ({ kind: a.kind, format: a.format, url: a.url, bytes: a.bytes, path: a.path })),
26
111
  application_id: persisted.application_id,
27
112
  application_status: persisted.status,
28
113
  status_advanced: persisted.status_advanced,
@@ -33,49 +118,97 @@ export const renderPdfTool = defineTool({
33
118
  }
34
119
  },
35
120
  });
36
- export async function persistRenderedFiles(job_id, files) {
37
- const resume = files.find(f => f.kind === 'resume')?.path ?? null;
38
- const cover = files.find(f => f.kind === 'cover')?.path ?? null;
121
+ // ── File writing helpers ────────────────────────────────────────────────────
122
+ function slug(s) {
123
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || randomUUID().slice(0, 8);
124
+ }
125
+ function fileNameFor(kind, jobId, jobTitle, ext) {
126
+ return `${kind}-${slug(jobTitle)}-${jobId.slice(0, 8)}.${ext}`;
127
+ }
128
+ async function writeText(format, kind, jobId, jobTitle, content) {
129
+ const subdir = format;
130
+ const filename = fileNameFor(kind, jobId, jobTitle, format);
131
+ const dir = resolve(config.outputDir, subdir);
132
+ mkdirSync(dir, { recursive: true });
133
+ const absPath = resolve(dir, filename);
134
+ writeFileSync(absPath, content, 'utf-8');
135
+ const rel = `${subdir}/${filename}`;
136
+ return {
137
+ kind, format, path: rel,
138
+ url: `${config.baseUrl}/files/${rel}`,
139
+ bytes: Buffer.byteLength(content, 'utf-8'),
140
+ };
141
+ }
142
+ async function writeBinary(format, kind, jobId, jobTitle, buf) {
143
+ const subdir = format;
144
+ const filename = fileNameFor(kind, jobId, jobTitle, format);
145
+ const dir = resolve(config.outputDir, subdir);
146
+ mkdirSync(dir, { recursive: true });
147
+ const absPath = resolve(dir, filename);
148
+ writeFileSync(absPath, buf);
149
+ const rel = `${subdir}/${filename}`;
150
+ return {
151
+ kind, format, path: rel,
152
+ url: `${config.baseUrl}/files/${rel}`,
153
+ bytes: buf.length,
154
+ };
155
+ }
156
+ /**
157
+ * Persist all rendered artifacts onto the application row.
158
+ * - resume_path / cover_path get the PDF path (back-compat for tracker fast-path)
159
+ * - rendered_files JSON gets the full per-kind per-format map (merged with any
160
+ * pre-existing entries — re-rendering one format doesn't NULL the others)
161
+ * - status advances materials_drafted | render_error → ready_to_review
162
+ * - jobs.status mirrors only when in a pre-render state (never push terminal
163
+ * states backwards)
164
+ *
165
+ * Exported so the test suite can hit it without invoking Playwright/Chromium.
166
+ */
167
+ export async function persistRenderedFiles(job_id, artifacts) {
39
168
  return runInWriteLock(() => {
40
169
  const db = getDb();
41
170
  const existing = db.prepare(`
42
- SELECT id, status FROM applications WHERE job_id = ?
171
+ SELECT id, status, rendered_files FROM applications WHERE job_id = ?
43
172
  `).get(job_id);
44
- const PRE_RENDER = new Set(['materials_drafted', 'render_error']);
173
+ // Merge new artifact paths into the rendered_files map.
174
+ const map = safeJson(existing?.rendered_files ?? null, {});
175
+ for (const a of artifacts) {
176
+ if (!map[a.kind])
177
+ map[a.kind] = {};
178
+ map[a.kind][a.format] = a.path;
179
+ }
180
+ const renderedJson = JSON.stringify(map);
181
+ // PDF fast-path columns (legacy back-compat for the tracker).
182
+ const resumePdf = artifacts.find(a => a.kind === 'resume' && a.format === 'pdf')?.path ?? null;
183
+ const coverPdf = artifacts.find(a => a.kind === 'cover' && a.format === 'pdf')?.path ?? null;
45
184
  if (existing) {
46
- const advance = PRE_RENDER.has(existing.status);
185
+ const advance = existing.status === 'materials_drafted' || existing.status === 'render_error';
47
186
  db.prepare(`
48
187
  UPDATE applications SET
49
- resume_path = COALESCE(?, resume_path),
50
- cover_path = COALESCE(?, cover_path),
51
- status = CASE WHEN status IN ('materials_drafted','render_error')
52
- THEN 'ready_to_review'
53
- ELSE status END,
188
+ resume_path = COALESCE(?, resume_path),
189
+ cover_path = COALESCE(?, cover_path),
190
+ rendered_files = ?,
191
+ status = CASE WHEN status IN ('materials_drafted','render_error')
192
+ THEN 'ready_to_review' ELSE status END,
54
193
  last_status_change_at = CASE WHEN status IN ('materials_drafted','render_error')
55
194
  THEN CURRENT_TIMESTAMP
56
195
  ELSE last_status_change_at END,
57
196
  updated_at = CURRENT_TIMESTAMP
58
197
  WHERE id = ?
59
- `).run(resume, cover, existing.id);
198
+ `).run(resumePdf, coverPdf, renderedJson, existing.id);
60
199
  mirrorJobStatus(db, job_id);
61
200
  const after = db.prepare(`SELECT status FROM applications WHERE id = ?`).get(existing.id);
62
- return {
63
- application_id: existing.id,
64
- status: after.status,
65
- status_advanced: advance && after.status === 'ready_to_review',
66
- };
201
+ return { application_id: existing.id, status: after.status, status_advanced: advance && after.status === 'ready_to_review' };
67
202
  }
68
203
  const id = randomUUID();
69
204
  db.prepare(`
70
- INSERT INTO applications (id, job_id, status, resume_path, cover_path, materials_v, last_status_change_at)
71
- VALUES (?, ?, 'ready_to_review', ?, ?, 1, CURRENT_TIMESTAMP)
72
- `).run(id, job_id, resume, cover);
205
+ INSERT INTO applications (id, job_id, status, resume_path, cover_path, rendered_files, materials_v, last_status_change_at)
206
+ VALUES (?, ?, 'ready_to_review', ?, ?, ?, 1, CURRENT_TIMESTAMP)
207
+ `).run(id, job_id, resumePdf, coverPdf, renderedJson);
73
208
  mirrorJobStatus(db, job_id);
74
209
  return { application_id: id, status: 'ready_to_review', status_advanced: true };
75
210
  });
76
211
  }
77
- // Mirror status onto jobs.status — only advance from earlier states. Don't push a job
78
- // already in applied / screen / onsite / offer / rejected backwards.
79
212
  function mirrorJobStatus(db, job_id) {
80
213
  db.prepare(`
81
214
  UPDATE jobs SET status = 'ready_to_review', updated_at = CURRENT_TIMESTAMP
@@ -83,4 +216,5 @@ function mirrorJobStatus(db, job_id) {
83
216
  AND status IN ('sourced','ready_to_apply','materials_drafted')
84
217
  `).run(job_id);
85
218
  }
219
+ export { parseCV };
86
220
  //# sourceMappingURL=render_pdf.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"render_pdf.js","sourceRoot":"","sources":["../../../src/mcp/tools/render_pdf.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"render_pdf.js","sourceRoot":"","sources":["../../../src/mcp/tools/render_pdf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ -- v0.5.0 — multi-format material exports.
2
+ --
3
+ -- Each application can now have a resume + cover delivered in any subset of
4
+ -- {pdf, tex, docx}. The PDF paths are still kept on resume_path / cover_path for the
5
+ -- tracker / apply_prefill fast-path (no JSON parse needed); the new
6
+ -- rendered_files column carries the full per-format map so the chat can hand
7
+ -- the user editable sources alongside the PDF.
8
+ --
9
+ -- Shape:
10
+ -- {
11
+ -- "resume": { "pdf": "pdfs/resume-...-abc.pdf",
12
+ -- "tex": "tex/resume-...-abc.tex",
13
+ -- "docx": "docx/resume-...-abc.docx" },
14
+ -- "cover": { "pdf": "pdfs/cover-...-abc.pdf",
15
+ -- "tex": "tex/cover-...-abc.tex",
16
+ -- "docx": "docx/cover-...-abc.docx" }
17
+ -- }
18
+ -- Any missing format is simply absent. Re-rendering one format updates only that key.
19
+
20
+ ALTER TABLE applications ADD COLUMN rendered_files TEXT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job_ops-mcp",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Self-hosted MCP server for the full job-search loop: portal scanning, JD evaluation, tailored resume + cover PDFs, outreach drafting, story bank, negotiation brief — chat-driven, human-in-the-loop.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -57,6 +57,7 @@
57
57
  "dependencies": {
58
58
  "@modelcontextprotocol/sdk": "^1.0.4",
59
59
  "better-sqlite3": "^11.3.0",
60
+ "docx": "^9.7.1",
60
61
  "express": "^4.21.0",
61
62
  "js-yaml": "^4.1.0",
62
63
  "playwright": "^1.48.0",