feat: optional resume-ops mode for high-quality tailoring#640
Conversation
|
Welcome to career-ops, @Rat-S! Thanks for your first PR. A few things to know:
We'll review your PR soon. Join our Discord if you have questions. |
📝 WalkthroughWalkthroughThis PR introduces a resume-ops workflow feature that enables high-quality resume tailoring against job descriptions. It adds service initialization, a CLI client for invoking tailoring, comprehensive documentation, supporting dependencies, and test fixtures to enable end-to-end resume generation and PDF output. ChangesResume-Ops Workflow Feature
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as resume-ops.mjs
participant API as Resume Ops API
participant Storage as Filesystem
User->>CLI: resume-ops --resume resume.json --jd job.txt --output output.pdf
CLI->>CLI: Parse args, load resume and JD
CLI->>API: POST /api/v1/tailor (resume + JD)
API-->>CLI: {task_id} or {pdf_base64}
alt async task
CLI->>API: GET /api/v1/tasks/{taskId} (poll)
API-->>CLI: {pdf_base64}
end
CLI->>Storage: Write PDF to output.pdf
CLI->>Storage: Write JSON to output.json
CLI-->>User: Success
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@modes/resume-ops.md`:
- Around line 25-27: Add a blank line immediately before the opening fenced code
block (```bash) and another blank line immediately after the closing ``` so the
snippet containing "node resume-ops.mjs --resume
resume-ops/.local/master-resume.json --jd \"{JD_TEXT}\" --output
output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf" is visually separated from
surrounding text and conforms to Markdown linting; update the surrounding lines
accordingly and ensure any subsequent note about passing --theme (if present)
remains on its own paragraph after the blank line.
In `@resume-ops.mjs`:
- Around line 4-15: The pollTask function interpolates taskId into the URL
without validation; validate and sanitize taskId before constructing the URL by
(1) enforcing an allowed pattern (e.g., UUID/hex or a strict /^[A-Za-z0-9-]+$/
regex) and throwing an error for invalid values, and (2) using
encodeURIComponent on the validated taskId when building the URL; update
pollTask to run this check at the top (refer to function pollTask and the taskId
parameter) so malicious strings like "../" or special characters are rejected or
safely encoded before fetch is called.
- Around line 45-88: Add request timeouts and a max-poll limit: wrap the Resume
Ops POST fetch in an AbortController with a configurable timeout (e.g., 10–30s)
and abort the request if exceeded, ensuring the error bubbles up to the existing
catch; likewise update the pollTask function to accept a maxAttempts and/or
totalTimeout and stop polling after that limit (or when the total timeout
elapses), returning a clear error if the task never completes. Reference the
fetch call in the main block (the POST to '/api/v1/tailor') and the
pollTask(task_id) function; ensure both paths clean up the AbortController
timers and throw a descriptive Error so the existing catch logs and exits.
- Around line 77-81: The code uses a case-sensitive string replace on
params.output to produce the JSON path; change this to use Node's path utilities
so extensions are handled case-insensitively and robustly: import/require 'path'
and compute jsonPath from path.parse(params.output) (e.g., const p =
path.parse(params.output); const jsonPath = path.join(p.dir, `${p.name}.json`));
then continue to call fs.writeFileSync(jsonPath, JSON.stringify(data.resume,
null, 2)) and log the jsonPath; reference symbols: params.output, data.resume,
fs.writeFileSync.
In `@start-resume-ops.mjs`:
- Around line 54-61: The replacement uses process.env[key] directly in
localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`) which
can corrupt the .env when values contain regex-replacement metacharacters;
change the replacement to use a safe replacer that injects the raw value (e.g.,
pass a function as the second argument or escape the replacement string) so the
matched line is replaced with `${key}=` + the literal environment value; update
the keysToSync loop to compute const value = process.env[key] and perform
localEnv = localEnv.replace(new RegExp(`${key}=.*`), () => `${key}=${value}`) to
ensure special characters in process.env[key] are not interpreted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5cbdeee1-5ea9-4293-9f3d-c05ab4efcf8a
📒 Files selected for processing (6)
.gitignoremodes/resume-ops.mdpackage.jsonresume-ops.mjsscratch/test-jd.txtstart-resume-ops.mjs
| ```bash | ||
| node resume-ops.mjs --resume resume-ops/.local/master-resume.json --jd "{JD_TEXT}" --output output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add blank lines around the fenced code block.
The code block should be surrounded by blank lines for proper Markdown rendering and consistency with linting rules.
📝 Proposed fix
3. **Execute Tailoring**:
- Determine the output PDF path: `output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf`.
- Use `config/profile.yml` to get `{candidate}` (normalized to kebab-case) and `{company}`.
- Run the helper script:
+
```bash
node resume-ops.mjs --resume resume-ops/.local/master-resume.json --jd "{JD_TEXT}" --output output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf
```
+
- If `config/profile.yml` has a `cv.theme` value, pass it via `--theme`.🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 25-25: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 27-27: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modes/resume-ops.md` around lines 25 - 27, Add a blank line immediately
before the opening fenced code block (```bash) and another blank line
immediately after the closing ``` so the snippet containing "node resume-ops.mjs
--resume resume-ops/.local/master-resume.json --jd \"{JD_TEXT}\" --output
output/cv-{candidate}-{company}-{YYYY-MM-DD}.pdf" is visually separated from
surrounding text and conforms to Markdown linting; update the surrounding lines
accordingly and ensure any subsequent note about passing --theme (if present)
remains on its own paragraph after the blank line.
| async function pollTask(taskId) { | ||
| const url = `http://127.0.0.1:8000/api/v1/tasks/${taskId}`; | ||
| while (true) { | ||
| const response = await fetch(url); | ||
| if (!response.ok) throw new Error(`Polling failed: ${response.status}`); | ||
| const data = await response.json(); | ||
| if (data.status === 'completed') return data; | ||
| if (data.status === 'failed') throw new Error(`Task failed: ${JSON.stringify(data.error)}`); | ||
| process.stdout.write('.'); | ||
| await new Promise(r => setTimeout(r, 2000)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Validate taskId format before URL construction.
Line 5 interpolates taskId directly into the URL without validation. If the Resume Ops API returns a malicious taskId (e.g., containing ../ or special characters), it could lead to unintended endpoint access. While the service is local, validating the format adds defense in depth.
🛡️ Proposed fix to validate taskId
async function pollTask(taskId) {
+ // Validate taskId format (alphanumeric, hyphens, underscores only)
+ if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) {
+ throw new Error(`Invalid task ID format: ${taskId}`);
+ }
const url = `http://127.0.0.1:8000/api/v1/tasks/${taskId}`;
while (true) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@resume-ops.mjs` around lines 4 - 15, The pollTask function interpolates
taskId into the URL without validation; validate and sanitize taskId before
constructing the URL by (1) enforcing an allowed pattern (e.g., UUID/hex or a
strict /^[A-Za-z0-9-]+$/ regex) and throwing an error for invalid values, and
(2) using encodeURIComponent on the validated taskId when building the URL;
update pollTask to run this check at the top (refer to function pollTask and the
taskId parameter) so malicious strings like "../" or special characters are
rejected or safely encoded before fetch is called.
| console.log('📡 Sending tailoring request to Resume Ops API...'); | ||
|
|
||
| try { | ||
| const response = await fetch('http://127.0.0.1:8000/api/v1/tailor', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| resume, | ||
| job_description: jd, | ||
| theme: params.theme || 'jsonresume-theme-stackoverflow' | ||
| }) | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const err = await response.text(); | ||
| throw new Error(`API Error: ${response.status} ${err}`); | ||
| } | ||
|
|
||
| let data = await response.json(); | ||
|
|
||
| if (data.task_id && !data.pdf_base64) { | ||
| console.log(`⏳ Task queued (ID: ${data.task_id}). Polling for completion...`); | ||
| data = await pollTask(data.task_id); | ||
| console.log('\n✅ Task completed.'); | ||
| } | ||
|
|
||
| if (data.pdf_base64) { | ||
| fs.mkdirSync(path.dirname(params.output), { recursive: true }); | ||
| fs.writeFileSync(params.output, Buffer.from(data.pdf_base64, 'base64')); | ||
| console.log(`📄 PDF successfully saved to ${params.output}`); | ||
|
|
||
| // Also save the tailored JSON if available | ||
| if (data.resume) { | ||
| const jsonPath = params.output.replace('.pdf', '.json'); | ||
| fs.writeFileSync(jsonPath, JSON.stringify(data.resume, null, 2)); | ||
| console.log(`📝 Tailored JSON saved to ${jsonPath}`); | ||
| } | ||
| } else { | ||
| throw new Error('No PDF content received from API'); | ||
| } | ||
| } catch (err) { | ||
| console.error(`❌ Error: ${err.message}`); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Consider adding request timeout for external API calls.
The fetch call to the Resume Ops API (line 48) and polling loop (lines 6-14) have no timeout configuration. If the service hangs or becomes unresponsive, the script will wait indefinitely, leading to poor user experience.
⏱️ Proposed fix to add timeout handling
console.log('📡 Sending tailoring request to Resume Ops API...');
try {
- const response = await fetch('http://127.0.0.1:8000/api/v1/tailor', {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
+ const response = await fetch('http://127.0.0.1:8000/api/v1/tailor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resume,
job_description: jd,
theme: params.theme || 'jsonresume-theme-stackoverflow'
- })
+ }),
+ signal: controller.signal
});
+ clearTimeout(timeoutId);
if (!response.ok) {Also add a maximum iteration limit to pollTask:
async function pollTask(taskId) {
if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) {
throw new Error(`Invalid task ID format: ${taskId}`);
}
const url = `http://127.0.0.1:8000/api/v1/tasks/${taskId}`;
+ let attempts = 0;
+ const MAX_ATTEMPTS = 60; // 2 minutes with 2s intervals
while (true) {
+ if (attempts++ >= MAX_ATTEMPTS) {
+ throw new Error('Polling timeout: task did not complete in time');
+ }
const response = await fetch(url);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@resume-ops.mjs` around lines 45 - 88, Add request timeouts and a max-poll
limit: wrap the Resume Ops POST fetch in an AbortController with a configurable
timeout (e.g., 10–30s) and abort the request if exceeded, ensuring the error
bubbles up to the existing catch; likewise update the pollTask function to
accept a maxAttempts and/or totalTimeout and stop polling after that limit (or
when the total timeout elapses), returning a clear error if the task never
completes. Reference the fetch call in the main block (the POST to
'/api/v1/tailor') and the pollTask(task_id) function; ensure both paths clean up
the AbortController timers and throw a descriptive Error so the existing catch
logs and exits.
| if (data.resume) { | ||
| const jsonPath = params.output.replace('.pdf', '.json'); | ||
| fs.writeFileSync(jsonPath, JSON.stringify(data.resume, null, 2)); | ||
| console.log(`📝 Tailored JSON saved to ${jsonPath}`); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use case-insensitive path handling for JSON output.
Line 78 uses .replace('.pdf', '.json') which is case-sensitive. If --output specifies a path with uppercase .PDF, the JSON file will be named incorrectly (e.g., resume.PDF.json instead of resume.json).
🔧 Proposed fix using proper path manipulation
// Also save the tailored JSON if available
if (data.resume) {
- const jsonPath = params.output.replace('.pdf', '.json');
+ const parsed = path.parse(params.output);
+ const jsonPath = path.join(parsed.dir, parsed.name + '.json');
fs.writeFileSync(jsonPath, JSON.stringify(data.resume, null, 2));
console.log(`📝 Tailored JSON saved to ${jsonPath}`);
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@resume-ops.mjs` around lines 77 - 81, The code uses a case-sensitive string
replace on params.output to produce the JSON path; change this to use Node's
path utilities so extensions are handled case-insensitively and robustly:
import/require 'path' and compute jsonPath from path.parse(params.output) (e.g.,
const p = path.parse(params.output); const jsonPath = path.join(p.dir,
`${p.name}.json`)); then continue to call fs.writeFileSync(jsonPath,
JSON.stringify(data.resume, null, 2)) and log the jsonPath; reference symbols:
params.output, data.resume, fs.writeFileSync.
| const keysToSync = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'OPENAI_BASE_URL']; | ||
| keysToSync.forEach(key => { | ||
| if (process.env[key]) { | ||
| console.log(`🔑 Syncing ${key} from environment...`); | ||
| // Replace the empty or placeholder value | ||
| localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Escape special characters when syncing API keys to .env.
Line 59 uses process.env[key] directly in a regex replacement string. If an API key contains characters with special meaning in regex replacements (e.g., $, \), they will be misinterpreted, potentially corrupting the .env file.
🔒 Proposed fix to escape replacement string
keysToSync.forEach(key => {
if (process.env[key]) {
console.log(`🔑 Syncing ${key} from environment...`);
- // Replace the empty or placeholder value
- localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`);
+ // Replace the empty or placeholder value (escape special regex chars in value)
+ const escapedValue = process.env[key].replace(/\$/g, '$$$$');
+ localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${escapedValue}`);
}
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const keysToSync = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'OPENAI_BASE_URL']; | |
| keysToSync.forEach(key => { | |
| if (process.env[key]) { | |
| console.log(`🔑 Syncing ${key} from environment...`); | |
| // Replace the empty or placeholder value | |
| localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${process.env[key]}`); | |
| } | |
| }); | |
| const keysToSync = ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GEMINI_API_KEY', 'OPENAI_BASE_URL']; | |
| keysToSync.forEach(key => { | |
| if (process.env[key]) { | |
| console.log(`🔑 Syncing ${key} from environment...`); | |
| // Replace the empty or placeholder value (escape special regex chars in value) | |
| const escapedValue = process.env[key].replace(/\$/g, '$$$$'); | |
| localEnv = localEnv.replace(new RegExp(`${key}=.*`), `${key}=${escapedValue}`); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@start-resume-ops.mjs` around lines 54 - 61, The replacement uses
process.env[key] directly in localEnv.replace(new RegExp(`${key}=.*`),
`${key}=${process.env[key]}`) which can corrupt the .env when values contain
regex-replacement metacharacters; change the replacement to use a safe replacer
that injects the raw value (e.g., pass a function as the second argument or
escape the replacement string) so the matched line is replaced with `${key}=` +
the literal environment value; update the keysToSync loop to compute const value
= process.env[key] and perform localEnv = localEnv.replace(new
RegExp(`${key}=.*`), () => `${key}=${value}`) to ensure special characters in
process.env[key] are not interpreted.
What does this PR do?
Introduces an optional
resume-opsmode that leverages an external high-quality tailoring service. It includes a robust startup script that automatically clones and configures the service, allowing users to opt-in for better resume quality without affecting the core agent-driven flow.Related issue
Fixes #635 / Closes #557
Type of change
Checklist
node test-all.mjsand all tests passSummary by CodeRabbit
New Features
Documentation
Chores
.gitignoreto exclude generated files and service directories