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 +38 -1
- package/dist/core/render_docx.js +250 -0
- package/dist/core/render_docx.js.map +1 -0
- package/dist/core/render_tex.js +237 -0
- package/dist/core/render_tex.js.map +1 -0
- package/dist/mcp/tools/render_pdf.js +166 -32
- package/dist/mcp/tools/render_pdf.js.map +1 -1
- package/dist/migrations/003_rendered_files.sql +20 -0
- package/package.json +2 -1
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
|
|
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 {
|
|
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
|
|
9
|
-
description: '
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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 =
|
|
185
|
+
const advance = existing.status === 'materials_drafted' || existing.status === 'render_error';
|
|
47
186
|
db.prepare(`
|
|
48
187
|
UPDATE applications SET
|
|
49
|
-
resume_path
|
|
50
|
-
cover_path
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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(
|
|
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,
|
|
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":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
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.
|
|
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",
|