job_ops-mcp 0.4.3 → 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/apply_prefill.js +165 -60
- package/dist/mcp/tools/apply_prefill.js.map +1 -1
- 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":"AAAA,kDAAkD;AAClD,EAAE;AACF,mFAAmF;AACnF,+EAA+E;AAC/E,yEAAyE;AACzE,sEAAsE;AACtE,8EAA8E;AAC9E,uCAAuC;AACvC,EAAE;AACF,mFAAmF;AACnF,+EAA+E;AAC/E,qCAAqC;AAErC,OAAO,EACL,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAA+B,iBAAiB,EACpF,eAAe,EAAE,mBAAmB,GACrC,MAAM,MAAM,CAAC;AAEd,OAAO,EAAE,OAAO,EAA8F,MAAM,eAAe,CAAC;AAUpI,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC;IACrB,OAAO,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,oCAAoC;AACpC,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAqB;IACxD,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC;IACrB,OAAO,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,+EAA+E;AAE/E,MAAM,IAAI,GAAG,SAAS,CAAC,CAAY,uBAAuB;AAC1D,MAAM,SAAS,GAAG,EAAE,CAAC,CAAc,yCAAyC;AAC5E,MAAM,SAAS,GAAG,EAAE,CAAC,CAAc,OAAO;AAC1C,MAAM,YAAY,GAAG,EAAE,CAAC,CAAW,OAAO;AAC1C,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAW,wCAAwC;AAE3E,MAAM,YAAY,GAAG;IACnB,GAAG,EAAK,mBAAmB,CAAC,GAAG,CAAC;IAChC,MAAM,EAAE,mBAAmB,CAAC,GAAG,CAAC;IAChC,IAAI,EAAI,mBAAmB,CAAC,IAAI,CAAC;IACjC,KAAK,EAAG,mBAAmB,CAAC,IAAI,CAAC;CAClC,CAAC;AAEF,SAAS,QAAQ,CAAC,IAAY,EAAE,OAA6E,EAAE;IAC7G,OAAO,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS;QAC9C,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AACpF,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,IAAI,SAAS,CAAC;QACnB,OAAO,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE;QACnC,MAAM,EAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1E,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;KAC7F,CAAC,CAAC;AACL,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,OAAO,IAAI,SAAS,CAAC;QACnB,MAAM,EAAG,EAAE,KAAK,EAAE,CAAC,EAAE;QACrB,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;QAC5C,QAAQ,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED,SAAS,IAAI,CAAC,IAAY,EAAE,OAA0D,EAAE;IACtF,OAAO,IAAI,SAAS,CAAC;QACnB,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,aAAa,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;QACvF,QAAQ,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAED,+EAA+E;AAE/E,SAAS,cAAc,CAAC,EAAU;IAChC,MAAM,QAAQ,GAAgB,EAAE,CAAC;IAEjC,kCAAkC;IAClC,QAAQ,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;QAC1B,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QACtB,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;KACnG,CAAC,CAAC,CAAC;IACJ,MAAM,OAAO,GAAc,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,MAAM;QAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnG,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAAC,CAAC;IACzD,IAAI,EAAE,CAAC,KAAK,EAAK,CAAC;QAAC,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAAC,CAAC;IAChE,IAAI,EAAE,CAAC,KAAK,EAAK,CAAC;QAAC,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAAC,CAAC;IAChE,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,QAAQ,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,QAAQ,GAAoC,EAAE,CAAC;IACrD,IAAI,EAAE,CAAC,YAAY,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC;YAClC,IAAI,EAAE,EAAE,CAAC,YAAY;YACrB,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,gBAAgB,IAAI,EAAE,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;SAC1I,CAAC,CAAC,CAAC;IACN,CAAC;IACD,IAAI,EAAE,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,QAAQ,CAAC,MAAM;YAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QACjH,QAAQ,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC;YAClC,IAAI,EAAE,EAAE,CAAC,aAAa;YACtB,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,iBAAiB,IAAI,EAAE,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;SAC5I,CAAC,CAAC,CAAC;IACN,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAe,EAAE,CAAC,CAAC,CAAC;IACtF,CAAC;IAED,UAAU;IACV,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,SAAS;IACT,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;QACjC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;gBAC1B,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;gBAC5C,QAAQ,EAAE;oBACR,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,QAAQ,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;oBACjF,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;iBAClB;aACF,CAAC,CAAC,CAAC;QACN,CAAC;IACH,CAAC;IAED,aAAa;IACb,IAAI,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;QACrC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,WAAW;IACX,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;QACnC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IAED,YAAY;IACZ,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QACpC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,IAAI,QAAQ,CAAC;QAClB,OAAO,EAAE,aAAa;QACtB,KAAK,EAAI,GAAG,EAAE,CAAC,IAAI,IAAI,WAAW,WAAW;QAC7C,QAAQ,EAAE,CAAC;gBACT,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,eAAe,CAAC,QAAQ,EAAE,EAAE,EAAE;gBAC/F,QAAQ;aACT,CAAC;KACH,CAAC,CAAC;AACL,CAAC;AAED,SAAS,eAAe,CAAC,CAAiB;IACxC,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,wBAAwB;IACxB,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;QACrB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE;QAC5C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAI,mDAAmD;QACrG,QAAQ,EAAE;YACR,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACzE,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YACxD,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SACnG;KACF,CAAC,CAAC,CAAC;IACJ,kBAAkB;IAClB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9D,IAAI,IAAI,EAAE,CAAC;QACT,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;YACrB,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;YAC5C,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;SACrG,CAAC,CAAC,CAAC;IACN,CAAC;IACD,UAAU;IACV,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,EAAE;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CAAC,CAAc;IAClC,MAAM,IAAI,GAAc;QACtB,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;KACxE,CAAC;IACF,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;QACZ,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,KAAK,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IACtG,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IACrF,MAAM,GAAG,GAAG,CAAC,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/F,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACX,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;YACrB,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;YAC5C,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;SAC3G,CAAC,CAAC,CAAC;IACN,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,cAAc,CAAC,CAAgB;IACtC,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;QACrB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE;QAC5C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QAC9C,QAAQ,EAAE;YACR,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACvE,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC7E,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YACxD,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SACjG;KACF,CAAC,CAAC,CAAC;IACJ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACX,GAAG,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;YACrB,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;YAC5C,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,GAAG,CAAC,EAAE,CAAC,CAAC;SACtD,CAAC,CAAC,CAAC;IACN,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,+EAA+E;AAE/E,SAAS,aAAa,CAAC,EAAU,EAAE,IAAqB;IACtD,MAAM,QAAQ,GAAgB,EAAE,CAAC;IAEjC,QAAQ,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;QAC1B,OAAO,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QACtB,QAAQ,EAAE,CAAC,IAAI,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;KACvG,CAAC,CAAC,CAAC;IACJ,MAAM,OAAO,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAChF,IAAI,OAAO,EAAE,CAAC;QACZ,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IACzG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAElD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7E,IAAI,WAAW;QAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAEzE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAEnE,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1C,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,OAAO;YAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,WAAW,CAAC,CAAC,CAAC;IAE5C,OAAO,IAAI,QAAQ,CAAC;QAClB,OAAO,EAAE,aAAa;QACtB,KAAK,EAAI,GAAG,EAAE,CAAC,IAAI,IAAI,WAAW,iBAAiB;QACnD,QAAQ,EAAE,CAAC;gBACT,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE;4BAC5B,GAAG,EAAK,mBAAmB,CAAC,CAAC,CAAC;4BAC9B,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC;4BAC9B,IAAI,EAAI,mBAAmB,CAAC,CAAC,CAAC;4BAC9B,KAAK,EAAG,mBAAmB,CAAC,CAAC,CAAC;yBAC/B,EAAC,EAAC;gBACH,QAAQ;aACT,CAAC;KACH,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -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,6 +1,30 @@
|
|
|
1
1
|
// apply_prefill — Playwright opens the application form, reads visible fields, drafts
|
|
2
|
-
// values
|
|
3
|
-
// preview. NEVER auto-submits.
|
|
2
|
+
// values for the few fields we can confidently identify from structured profile data,
|
|
3
|
+
// takes a screenshot, returns a preview. NEVER auto-submits.
|
|
4
|
+
//
|
|
5
|
+
// Mapping policy (the heart of this file):
|
|
6
|
+
//
|
|
7
|
+
// We maintain a SMALL ALLOWLIST of field kinds we'll fill from structured profile data:
|
|
8
|
+
// first_name, last_name, preferred_name, full_name, email, phone,
|
|
9
|
+
// linkedin, github, portfolio_url, city.
|
|
10
|
+
//
|
|
11
|
+
// A field is classified into a kind ONLY when it has a strong, well-defined signal:
|
|
12
|
+
// - autocomplete attribute (e.g. autocomplete="email")
|
|
13
|
+
// - input type attribute (e.g. type="email", type="tel")
|
|
14
|
+
// - exact `name` attribute match (e.g. name="email", name="first_name")
|
|
15
|
+
// - exact / whole-word label match (e.g. label === "Email" or "First Name *")
|
|
16
|
+
//
|
|
17
|
+
// Anything else — free-text questions ("Why this company?", "Where did you hear",
|
|
18
|
+
// "Tell us about a project"), dropdowns, custom EEO/demographic fields, ambiguous
|
|
19
|
+
// labels — is left BLANK with source='user_must_provide'. We NEVER fall back to
|
|
20
|
+
// dumping tagline / cover-letter / summary prose into unmapped fields. Resume + cover
|
|
21
|
+
// are returned as localhost links for the user to download and upload manually.
|
|
22
|
+
//
|
|
23
|
+
// Visa / work-auth / citizenship / country-of-residence fields are explicitly
|
|
24
|
+
// blocked regardless of any other match.
|
|
25
|
+
//
|
|
26
|
+
// The default for an unrecognised field is ALWAYS blank/user_must_provide. We err
|
|
27
|
+
// toward under-filling so a wrong value never lands in a free-text answer.
|
|
4
28
|
import { z } from 'zod';
|
|
5
29
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
30
|
import { resolve } from 'node:path';
|
|
@@ -8,17 +32,22 @@ import { randomUUID } from 'node:crypto';
|
|
|
8
32
|
import { config } from '../../config.js';
|
|
9
33
|
import { getDb } from '../../db.js';
|
|
10
34
|
import { defineTool, okResult, errResult } from '../define.js';
|
|
11
|
-
import {
|
|
35
|
+
import { loadProjectFiles } from '../../core/profile.js';
|
|
12
36
|
import { getJobWithCompany } from '../../core/jobs.js';
|
|
13
37
|
import { getSharedBrowser } from '../../core/browser.js';
|
|
14
|
-
import { safeJson } from '../../core/llm.js';
|
|
15
38
|
const PREVIEW_SUBDIR = 'previews';
|
|
39
|
+
// ── Tool ─────────────────────────────────────────────────────────────────────
|
|
16
40
|
export const applyPrefillTool = defineTool({
|
|
17
41
|
name: 'apply_prefill',
|
|
18
42
|
title: 'Apply prefill (preview only — never submits)',
|
|
19
|
-
description: 'Opens the application URL in Playwright,
|
|
20
|
-
'
|
|
21
|
-
'
|
|
43
|
+
description: 'Opens the application URL in Playwright, classifies form fields by strong signals ' +
|
|
44
|
+
'(autocomplete, input type, exact name, whole-word label), and fills ONLY a small ' +
|
|
45
|
+
'allowlist of identity fields (name parts, email, phone, LinkedIn, GitHub, ' +
|
|
46
|
+
'portfolio, city) from your profile. Any free-text question, dropdown, or ' +
|
|
47
|
+
'unrecognised field is left BLANK with source=user_must_provide — never auto-filled ' +
|
|
48
|
+
'with tagline / cover-letter / summary text. Visa / work-auth fields are explicitly ' +
|
|
49
|
+
'blocked. Returns a preview + screenshot + your rendered resume/cover URLs for you ' +
|
|
50
|
+
'to download + upload manually. NEVER submits.',
|
|
22
51
|
inputSchema: {
|
|
23
52
|
job_id: z.string().min(1),
|
|
24
53
|
url: z.string().url().optional().describe('Override the job source_url (e.g. a different ATS apply URL).'),
|
|
@@ -32,18 +61,14 @@ export const applyPrefillTool = defineTool({
|
|
|
32
61
|
return errResult(`No usable application URL for job ${args.job_id}`);
|
|
33
62
|
const app = getDb().prepare(`SELECT * FROM applications WHERE job_id = ?`).get(args.job_id);
|
|
34
63
|
const { profile } = loadProjectFiles();
|
|
35
|
-
const packet = getActiveCareerPacket()?.content ?? '';
|
|
36
|
-
// Build a lookup of values we can confidently draft.
|
|
37
64
|
const identity = (profile?.candidate ?? {});
|
|
38
|
-
const tailored = safeJson(app?.tailored_bullets, null);
|
|
39
|
-
const coverDraft = app?.cover_letter_draft ?? null;
|
|
40
65
|
try {
|
|
41
66
|
const browser = await getSharedBrowser();
|
|
42
67
|
const page = await browser.newPage();
|
|
43
68
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
44
69
|
await page.waitForTimeout(1_500);
|
|
45
|
-
// Detect form inputs.
|
|
46
|
-
//
|
|
70
|
+
// Detect form inputs. Skip file/password/hidden/submit/button. Gather every
|
|
71
|
+
// signal classification needs: autocomplete, name, type, plus the label.
|
|
47
72
|
const detected = await page.$$eval('input:not([type="hidden"]):not([type="file"]):not([type="password"]):not([type="submit"]):not([type="button"]), textarea, select', (els) => {
|
|
48
73
|
const lookupLabel = (el) => {
|
|
49
74
|
if (el.getAttribute('aria-label'))
|
|
@@ -71,8 +96,11 @@ export const applyPrefillTool = defineTool({
|
|
|
71
96
|
return els.slice(0, 60).map((el) => ({
|
|
72
97
|
selector: sel(el),
|
|
73
98
|
label: lookupLabel(el),
|
|
99
|
+
name: el.getAttribute('name') ?? '',
|
|
100
|
+
autocomplete: el.getAttribute('autocomplete') ?? '',
|
|
74
101
|
type: (el.getAttribute('type') || el.tagName).toLowerCase(),
|
|
75
102
|
required: !!el.required || el.getAttribute('aria-required') === 'true',
|
|
103
|
+
tag: el.tagName.toLowerCase(),
|
|
76
104
|
}));
|
|
77
105
|
});
|
|
78
106
|
// Screenshot
|
|
@@ -80,21 +108,24 @@ export const applyPrefillTool = defineTool({
|
|
|
80
108
|
const shotPath = `${PREVIEW_SUBDIR}/apply-${args.job_id.slice(0, 8)}-${Date.now()}.png`;
|
|
81
109
|
const absShot = resolve(config.outputDir, shotPath);
|
|
82
110
|
await page.screenshot({ path: absShot, fullPage: true });
|
|
83
|
-
const fields = detected.map(
|
|
111
|
+
const fields = detected.map(d => draftValue(d, identity));
|
|
84
112
|
const previewId = randomUUID();
|
|
113
|
+
const filled = fields.filter(f => f.source === 'profile').length;
|
|
85
114
|
const previewJson = {
|
|
86
115
|
preview_id: previewId,
|
|
87
116
|
job_id: args.job_id,
|
|
88
117
|
url,
|
|
89
118
|
company: job.company_name,
|
|
90
119
|
title: job.title,
|
|
91
|
-
warning: 'preview only — this tool never submits. Upload resume/cover manually using the localhost links below.',
|
|
120
|
+
warning: 'preview only — this tool never submits. Upload resume/cover manually using the localhost links below. Any blank field is intentionally left for you to answer.',
|
|
121
|
+
fields_total: fields.length,
|
|
122
|
+
fields_auto_filled: filled,
|
|
123
|
+
fields_user_must_provide: fields.length - filled,
|
|
92
124
|
fields,
|
|
93
125
|
resume_url: app?.resume_path ? `${config.baseUrl}/files/${app.resume_path}` : null,
|
|
94
126
|
cover_url: app?.cover_path ? `${config.baseUrl}/files/${app.cover_path}` : null,
|
|
95
127
|
screenshot_url: `${config.baseUrl}/files/${shotPath}`,
|
|
96
128
|
};
|
|
97
|
-
// Persist a JSON copy alongside the screenshot for the chat to re-read.
|
|
98
129
|
writeFileSync(absShot.replace(/\.png$/, '.json'), JSON.stringify(previewJson, null, 2), 'utf-8');
|
|
99
130
|
await page.close();
|
|
100
131
|
return okResult(previewJson);
|
|
@@ -105,63 +136,137 @@ export const applyPrefillTool = defineTool({
|
|
|
105
136
|
// Shared browser stays alive for the next caller — closed at server shutdown.
|
|
106
137
|
},
|
|
107
138
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
// ── Classification (pure — exported for tests) ───────────────────────────────
|
|
140
|
+
const VISA_PATTERN = /\b(visa|sponsor(ship|ed|s)?|work[\s_-]*(auth|permit|authoriz|authoris)|legally\s*authorized|citizen(ship)?|h-?1-?b|opt\b|stem\s*opt|ead\b|nationality|country[\s_-]*of[\s_-]*(residence|origin|citizenship))\b/i;
|
|
141
|
+
/** Classify a detected field by strong signals only. Returns kind=null when unsure. */
|
|
142
|
+
export function classifyField(d) {
|
|
143
|
+
const label = (d.label ?? '').toLowerCase().trim();
|
|
144
|
+
const name = (d.name ?? '').toLowerCase().trim();
|
|
145
|
+
const ac = (d.autocomplete ?? '').toLowerCase().trim();
|
|
146
|
+
const type = (d.type ?? '').toLowerCase().trim();
|
|
147
|
+
// Visa / work-auth — explicit block, runs FIRST so no other rule can override.
|
|
148
|
+
if (VISA_PATTERN.test(label) || VISA_PATTERN.test(name)) {
|
|
149
|
+
return { kind: null, reason: 'visa/work-auth field — never auto-fill' };
|
|
150
|
+
}
|
|
151
|
+
// Free-text answer surfaces (textarea, large free-form questions) are NEVER mapped,
|
|
152
|
+
// even if the label happens to contain a kind-keyword as a substring. The point of
|
|
153
|
+
// these fields is to elicit a candidate-written answer.
|
|
154
|
+
if (d.tag === 'textarea') {
|
|
155
|
+
return { kind: null, reason: 'textarea / free-text — never auto-fill' };
|
|
156
|
+
}
|
|
157
|
+
if (d.tag === 'select') {
|
|
158
|
+
return { kind: null, reason: 'select / dropdown — never auto-fill' };
|
|
116
159
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
// ── Email
|
|
161
|
+
if (type === 'email' || ac === 'email'
|
|
162
|
+
|| /^(email|email_address|emailaddress|e_mail|emailid)$/.test(name)
|
|
163
|
+
|| /^(email|e[\s\-]?mail)( address)?\*?$/.test(label)) {
|
|
164
|
+
return { kind: 'email', reason: 'strong email signal' };
|
|
120
165
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
166
|
+
// ── Phone
|
|
167
|
+
if (type === 'tel' || ac === 'tel'
|
|
168
|
+
|| /^(phone|telephone|mobile|tel|phone_number|mobile_number|cellphone)$/.test(name)
|
|
169
|
+
|| /^(phone|telephone|mobile|cell)( number)?\*?$/.test(label)) {
|
|
170
|
+
return { kind: 'phone', reason: 'strong phone signal' };
|
|
124
171
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
172
|
+
// ── First name
|
|
173
|
+
if (ac === 'given-name'
|
|
174
|
+
|| /^(first|first_name|firstname|givenname|given_name|fname)$/.test(name)
|
|
175
|
+
|| /^(first name|given name|first)\*?$/.test(label)) {
|
|
176
|
+
return { kind: 'first_name', reason: 'strong first-name signal' };
|
|
128
177
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
178
|
+
// ── Last name
|
|
179
|
+
if (ac === 'family-name'
|
|
180
|
+
|| /^(last|last_name|lastname|familyname|family_name|surname|lname)$/.test(name)
|
|
181
|
+
|| /^(last name|family name|surname|last)\*?$/.test(label)) {
|
|
182
|
+
return { kind: 'last_name', reason: 'strong last-name signal' };
|
|
132
183
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
184
|
+
// ── Preferred name / nickname
|
|
185
|
+
if (/^(preferred_name|preferredname|nickname|known_as)$/.test(name)
|
|
186
|
+
|| /^(preferred name|nickname|what should we call you|known as)\*?$/.test(label)) {
|
|
187
|
+
return { kind: 'preferred_name', reason: 'preferred-name signal' };
|
|
136
188
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
// ── Full name (kept narrow — "company name", "manager name" must NOT match)
|
|
190
|
+
if (ac === 'name'
|
|
191
|
+
|| /^(name|full_name|fullname|your_name|legal_name)$/.test(name)
|
|
192
|
+
|| /^(full name|your name|legal name|name)\*?$/.test(label)) {
|
|
193
|
+
return { kind: 'full_name', reason: 'strong full-name signal' };
|
|
140
194
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
195
|
+
// ── LinkedIn
|
|
196
|
+
if (/(^|[_\b])linkedin($|[_\b])/.test(name)
|
|
197
|
+
|| /\blinkedin( profile| url)?\*?$/.test(label)) {
|
|
198
|
+
return { kind: 'linkedin', reason: 'linkedin signal' };
|
|
144
199
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
200
|
+
// ── GitHub
|
|
201
|
+
if (/(^|[_\b])github($|[_\b])/.test(name)
|
|
202
|
+
|| /\bgithub( profile| url)?\*?$/.test(label)) {
|
|
203
|
+
return { kind: 'github', reason: 'github signal' };
|
|
148
204
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
205
|
+
// ── Portfolio / personal website (only when whole-word, not generic "url")
|
|
206
|
+
if (/^(website|portfolio|personal_site|personal_website|website_url|portfolio_url|portfolio_link)$/.test(name)
|
|
207
|
+
|| /^(website|portfolio|personal website|personal site)\*?$/.test(label)) {
|
|
208
|
+
return { kind: 'portfolio_url', reason: 'portfolio signal' };
|
|
152
209
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
210
|
+
// ── City / location (strict whole-word; "where are you LOCATED" is too ambiguous)
|
|
211
|
+
if (ac === 'address-level2'
|
|
212
|
+
|| /^(city|current_city|location)$/.test(name)
|
|
213
|
+
|| /^(city|current city|location)\*?$/.test(label)) {
|
|
214
|
+
return { kind: 'city', reason: 'city signal' };
|
|
156
215
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
return { kind: null, reason: 'no confident match — left for user' };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Map a classified field to a draft value from the candidate's profile. Pure function,
|
|
220
|
+
* exported for tests. Returns a DetectedField with source='user_must_provide' for any
|
|
221
|
+
* kind=null OR when the profile lacks the data (e.g. no linkedin in profile.yml).
|
|
222
|
+
*/
|
|
223
|
+
export function draftValue(d, identity) {
|
|
224
|
+
const classification = classifyField(d);
|
|
225
|
+
let value = '';
|
|
226
|
+
switch (classification.kind) {
|
|
227
|
+
case 'email':
|
|
228
|
+
value = identity.email ?? '';
|
|
229
|
+
break;
|
|
230
|
+
case 'phone':
|
|
231
|
+
value = identity.phone ?? '';
|
|
232
|
+
break;
|
|
233
|
+
case 'first_name':
|
|
234
|
+
value = (identity.full_name ?? '').trim().split(/\s+/)[0] ?? '';
|
|
235
|
+
break;
|
|
236
|
+
case 'last_name':
|
|
237
|
+
value = (identity.full_name ?? '').trim().split(/\s+/).slice(1).join(' ').trim();
|
|
238
|
+
break;
|
|
239
|
+
case 'preferred_name':
|
|
240
|
+
value = (identity.full_name ?? '').trim().split(/\s+/)[0] ?? '';
|
|
241
|
+
break;
|
|
242
|
+
case 'full_name':
|
|
243
|
+
value = identity.full_name ?? '';
|
|
244
|
+
break;
|
|
245
|
+
case 'linkedin':
|
|
246
|
+
value = identity.linkedin ?? '';
|
|
247
|
+
break;
|
|
248
|
+
case 'github':
|
|
249
|
+
value = identity.github ?? '';
|
|
250
|
+
break;
|
|
251
|
+
case 'portfolio_url':
|
|
252
|
+
value = identity.portfolio_url ?? identity.github ?? '';
|
|
253
|
+
break;
|
|
254
|
+
case 'city':
|
|
255
|
+
value = identity.location ?? '';
|
|
256
|
+
break;
|
|
257
|
+
case null:
|
|
258
|
+
value = '';
|
|
259
|
+
break;
|
|
161
260
|
}
|
|
261
|
+
const source = (classification.kind && value) ? 'profile' : 'user_must_provide';
|
|
162
262
|
return {
|
|
163
|
-
selector: d.selector,
|
|
164
|
-
|
|
263
|
+
selector: d.selector,
|
|
264
|
+
label: d.label,
|
|
265
|
+
type: d.type,
|
|
266
|
+
required: !!d.required,
|
|
267
|
+
draft_value: value,
|
|
268
|
+
source,
|
|
269
|
+
classification,
|
|
165
270
|
};
|
|
166
271
|
}
|
|
167
272
|
//# sourceMappingURL=apply_prefill.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply_prefill.js","sourceRoot":"","sources":["../../../src/mcp/tools/apply_prefill.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,oFAAoF;AACpF,gFAAgF;
|
|
1
|
+
{"version":3,"file":"apply_prefill.js","sourceRoot":"","sources":["../../../src/mcp/tools/apply_prefill.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",
|