Conversation
- Implemented apply-auto.mjs: core automation workflow with browser steering - Added apply-computrabajo.mjs: Computrabajo-specific apply handler - Created auto-apply.mjs: CLI orchestrator with commands (scan, apply, sequence, status) - Implemented test-auto-apply.mjs: validation suite (25/25 tests passing) - Anti-automation measures: headless: false, navigator.webdriver override, stealth mode init - Computrabajo login flow: detects data-login-button-desktop, fills credentials - Multi-portal support: Computrabajo, LinkedIn, Lever, Greenhouse job boards - Form auto-fill using guessValue() based on field names (email, phone, names, etc.) - Success detection: checkmark SVG, status text, generic 'Application sent' messages - Application tracking: generates data/applications-log.md with per-job status reports - Current status: Computrabajo needs post-login selector refinement; other portals partially working"
…+ LinkedIn only - Rewrite apply-auto.mjs: one browser/session per platform, login once and apply to all pending jobs sequentially - Login CT flow: navigate to job URL, click Login header button, grab rfr href from Ingresar dropdown, fill Email + Password, click submit - After login, navigate back to each job URL and look for apply button - Filter pipeline.md to only process computrabajo.com and linkedin.com URLs; skip Lever, Greenhouse, and others silently - AI form-fill support via ANTHROPIC_API_KEY: unknown fields sent to Claude Haiku with CV context for intelligent answers - Add scan-bogota.mjs, scan-international.mjs, auto-scan.mjs scanners - Add apply-loop.mjs, apply-pipeline.mjs, apply-prepare.mjs helpers - Add diagnose-auto-apply.mjs and inspect-computrabajo*.mjs debug utilities - Update data/applications-log.md with latest run results (3 already-applied)
…phyton typo - Replace page.check() with force:true + JS iterate-all-radios fallback to ensure radio buttons are visually selected even with jQuery overlays - Add DOM verification after click (logs ✓ vs⚠️ not confirmed) - Add 'phyton' typo to CANDIDATE_KQ.si list and normalize in heuristic - Broaden salary siOpt regex with fallback chain for radio salary questions - Add text-field answers for 'expectativa salarial' and 'de acuerdo asignación' - Add typo normalization: phyton→python, javascrip→javascript, postgress→sql Run 5 results: 8 ✅ confirmed, 2⚠️ submitted-unverified, 0 ❌ failures"
…atch-all heuristic
…ot hardcoded string
…i+no, askAIRaw fallthrough, hasAI display, backend/frontend/dotnet text fields, power BI answer
…PHP web regex, discapacidad hardReject
…wift/kotlin/flutter text rules, automatizado regex
LinkedIn (apply-linkedin.mjs): - Login robusto con cadena de selectores + waitForSelector state:visible - Búsqueda con doble filtro de tiempo: 24h + 7d por keyword (28 búsquedas) - Paginación via scroll virtual con deduplicación por jobId - isDevJobTitle: regex /\.net\b|c#/i, 'senior ' al inicio de título - isYearsQ: devuelve '0' para Go, Golang, SRE, Rust, Scala, Elixir + stacks incompatibles - guessAnswerByLabel: detección isEn, respuestas bilingües EN/ES para todo el diccionario - Salary numérico: devuelve 2500000 (número limpio) en lugar de texto - fillAdditionalQuestions: prompts IA bilingües con perfil estructurado completo - checkJobCVMatch: verificación IA de descripción vs perfil del candidato - Auto-cierre al finalizar (sin waitForEvent close) - MAX_NEW_APPS=10, MAX_STEPS=25 Computrabajo (apply-auto.mjs): - hardReject: añadir 'programadora-de-turnos' y 'programadora-turnos' (variante femenino) Data: - data/li-applied.json: 11 aplicaciones LinkedIn acumuladas - data/applications-log.md: actualizado con aplicaciones CT de esta sesión
apply-auto.mjs:
- MAX_NEW_APPS: 10 → 5 (menos agresivo, evita ban CT)
- headless: configurable por CI=true o HEADLESS=true (requerido para GH Actions)
- saveReport: append por ejecución en vez de sobrescribir — cada run agrega
una sección '## Ejecución {timestamp}' al historial acumulado
.github/workflows/apply-auto.yml:
- Schedule: 5 ejecuciones/día COT (11,13,15,17,0 UTC)
- Reconstruye credentials.yml desde secrets CT_EMAIL + CT_PASSWORD
- Reconstruye profile.yml desde secret PROFILE_YML
- Escribe .env desde secrets ANTHROPIC/OPENAI/GEMINI API keys
- Copia data/pipeline-ci.md → data/pipeline.md antes de ejecutar
- Sube applications-log.md como artifact (siempre)
- Commit + push del log actualizado al repo al final
data/pipeline-ci.md:
- 15 búsquedas CT (backend/frontend/fullstack/node/react/etc.)
- Archivo tracked en git, sin datos personales
- Editar este archivo para agregar/quitar búsquedas del cron
📝 WalkthroughWalkthroughThis pull request introduces a complete automated job application system with scheduled execution, multi-platform support (Computrabajo and LinkedIn), configuration management, scanning/filtering capabilities, diagnostic utilities, and comprehensive documentation in English and Spanish. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CLI as auto-apply.mjs
participant Scanner as scan-*.mjs
participant Pipeline as data/pipeline.md
participant Apply as apply-auto.mjs
participant Browser as Playwright Browser
participant Job_Site as Computrabajo/LinkedIn
participant Logger as data/applications-log.md
User->>CLI: scan bogota
CLI->>Scanner: Execute scan command
Scanner->>Job_Site: Fetch job listings
Job_Site->>Scanner: Return results
Scanner->>Pipeline: Append new job URLs
User->>CLI: apply
CLI->>Pipeline: Read pending jobs
CLI->>Apply: Process each job URL
loop For each job
Apply->>Browser: Launch Chromium
Browser->>Job_Site: Navigate to job
Job_Site->>Browser: Load job page
Browser->>Browser: Detect form fields
Browser->>Browser: Fill form with profile data
Browser->>Browser: Submit application
Job_Site->>Browser: Return result page
Browser->>Browser: Detect success indicator
Apply->>Logger: Append result (status, timestamp)
end
Apply->>Pipeline: Mark job as completed [x]
User->>CLI: status
CLI->>Pipeline: Count pending/completed
CLI->>Logger: Parse application results
CLI->>User: Display summary
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 55
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
.gitignore (1)
20-43: 🧹 Nitpick | 🔵 TrivialLGTM, and consider extending the ignore list.
.envandconfig/credentials.ymlentries are correct. Given this PR also introducesdata/li-applied.jsonanddata/applications-log.mdas user activity logs, consider adding them here too so personal data is not committed on every CI cron run:# Personal data (user fills these) data/applications.md data/pipeline.md data/scan-history.tsv +data/li-applied.json +data/applications-log.mdAlso update
test-auto-apply.mjs(lines 220-241) to additionally assert.envis in.gitignore, matching the new# Secretssection.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore around lines 20 - 43, Add the two user log files to .gitignore so personal activity logs aren't committed: append "data/li-applied.json" and "data/applications-log.md" (place them near the "# Generated" or "# Secrets" sections for clarity). Then update the test in test-auto-apply.mjs (the assertion block around lines 220-241) to also assert that ".env" is present in the .gitignore contents (extend the existing assertions that check for config/credentials.yml to include ".env"). Ensure you reference the exact filenames data/li-applied.json and data/applications-log.md and the test file test-auto-apply.mjs when making the changes.
♻️ Duplicate comments (1)
temp-debug-computrabajo-all.mjs (1)
1-27: 🛠️ Refactor suggestion | 🟠 MajorSame concern as the other
temp-debug-computrabajo-*scripts: remove before merge or consolidate.This is a near-duplicate of
temp-debug-computrabajo-replica.mjsandtemp-debug-computrabajo-selector-issue.mjs— same UA, same selector list, samepage.$$eval(...).catch(() => 0)pattern, just iterating over multiple URLs. Three copy/paste variants of the same debug loop should not land onmain.Suggested action: delete all three
temp-debug-computrabajo-*.mjsfiles, or consolidate them into a single parameterizable helper (e.g.,scripts/debug/computrabajo-selectors.mjs <url>...) shared bydiagnose-auto-apply.mjs.Minor issues if kept:
- Hardcoded job URLs (line 5 even has a fragment with a specific offer id) will 404/redirect over time and make this script silently useless.
- No
try/finallyaroundbrowser.close().- Truncated UA (
AppleWebKit/537.36with no Chrome/Safari token) — see comment ontemp-debug-computrabajo-selector-issue.mjs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@temp-debug-computrabajo-all.mjs` around lines 1 - 27, This temp-debug script is a near-duplicate debug helper and should be removed or consolidated into a single reusable script; either delete temp-debug-computrabajo-all.mjs (and the other two duplicates) or refactor into a parameterizable helper (e.g., scripts/debug/computrabajo-selectors.mjs) that accepts URLs as args and is consumed by diagnose-auto-apply.mjs. If you choose to keep/merge it, replace the hardcoded urls array with CLI args, use a full realistic userAgent string for the browser.newPage call, wrap the browser lifecycle in try/finally to ensure browser.close() always runs, and centralize the selector list (sels) and the page.$$eval(...).catch(() => 0) pattern into a shared function to avoid copy/paste.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/apply-auto.yml:
- Around line 49-66: The workflow currently interpolates secrets directly inside
the run scripts (steps "Write credentials.yml", "Write profile.yml", "Write .env
(API keys IA)"), which can corrupt output or allow injection; instead, export
each secret into the step's env (e.g., CT_EMAIL, CT_PASSWORD, PROFILE_YML,
ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY) and change the run bodies to
serialize safely (for YAML use a short safe dumper, e.g., a one-liner
Python/Node script that loads values from os.environ and writes proper YAML, and
for .env build key=value lines only if the env var is non-empty using the same
script) so no secrets are directly expanded by the shell and all values are
properly escaped/quoted.
In `@apply-computrabajo.mjs`:
- Line 30: The chromium.launch call hardcodes headless: false which breaks CI;
change the chromium.launch invocation that creates "browser" so headless is
driven from an environment variable (e.g. process.env.HEADLESS or
process.env.CI) instead of a literal false: read the env var, coerce it to a
boolean (treat "0"/"false"/empty as false, otherwise true) and pass that value
as the headless option to chromium.launch so local interactive runs can be
non-headless while GitHub Actions/CI runs default to headless.
- Around line 20-27: Move the synchronous config reads out of module-evaluation
into runtime so missing files don't crash before arg validation: stop calling
readFileSync/yaml.load at top-level for PROFILE and CREDENTIALS; instead, inside
main() (or the entry function that validates process.argv and handles usage)
check for file existence with fs.existsSync (or wrap readFileSync in try/catch),
then read and yaml.load the files, logging a friendly error and process.exit(1)
if they are absent or invalid. Update any references to PROFILE/CREDENTIALS to
use the loaded variables from main() or pass them into functions that need them.
- Around line 115-144: The loop assumes field.getAttribute('name') and
PROFILE.candidate exist and that field.fill works for all input types; coerce
and guard both: getAttribute('name') may be null so convert to const name =
String(await field.getAttribute('name') || '') and only push/log non-empty
names, guard all PROFILE accesses (use PROFILE?.candidate?.... or short-circuit
to '') when mapping values, and branch on the input type (check the variable
type from field.getAttribute('type')) so you call field.fill only for
text/textarea/email/number types and handle/skip radio/checkbox/file
appropriately instead of silently swallowing errors from field.fill; update
references to name, type, PROFILE.candidate, field.fill, and filled to implement
these checks.
- Around line 216-221: The appendFileSync call that writes logLine can fail if
the data/ directory doesn't exist and errors are swallowed by the current exit
flow; before calling appendFileSync (near logLine and browser.close()) ensure
the directory exists by creating it (e.g., check/mkdirSync for 'data'), wrap the
append in a try/catch that logs any write error and rethrows so the outer flow
can fail, and remove the trailing main().catch(console.error) pattern so the
process exits non‑zero on errors (instead, let main throw or explicitly call
process.exit(1) when result.status === 'error' after main returns) — reference
symbols: logLine, appendFileSync, browser.close(), main().catch(console.error),
and result.status.
- Around line 86-88: The expression computing requiresLogin incorrectly uses
await on the synchronous Page.url() call; remove the unnecessary await before
page.url() so it reads page.url().includes('login') and keep the awaited page.$
calls as-is (references: requiresLogin variable, page.url(), page.$ and the form
selector 'form:has(input[type=password])') to preserve the intended
short-circuiting and correct async behavior.
- Around line 32-34: The call to context.newPage({ userAgent: ... }) is invalid
because BrowserContext.newPage accepts no args; move the userAgent option to the
context creation: create the context with browser.newContext({ userAgent:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) (or update the
existing context creation) and then call context.newPage() with no arguments;
remove the userAgent object from the context.newPage() call and ensure the
variable names (browser, context, page) match the existing code (e.g., where
context is created and where newPage() is invoked).
In `@apply-linkedin.mjs`:
- Around line 1378-1381: The top-level catch uses process.exit(1) which can
terminate the process before Playwright resources (created by createBrowser())
are closed; update the main() error handling so it does not call process.exit(1)
directly — either set process.exitCode = 1 and return, or explicitly attempt to
close the browser if it exists (call browser.close()) before exiting; locate the
main() invocation and its .catch(...) handler and change it to set
process.exitCode = 1 (or call await browser.close() if browser is accessible)
and let the process finish naturally so cleanup/fd flushing can occur.
- Around line 92-173: The code duplicates provider-selection, fetch, parse and
logging logic across askAI, askAIRaw and the individual provider functions
(askAIOpenAI / askAIGemini / askAILocal); extract per-provider caller functions
(e.g., callAnthropic, callOpenAI, callGemini, callLocal) that accept a single
options object ({ model, maxTokens, temperature, systemPrompt, userPrompt }) and
return the trimmed text or null, then implement a generic askChain(opts, tag)
that iterates providers [{ name, envKey, call }] checking env vars, calling each
provider (catching errors), logging with the tag and provider name, and
returning the first non-null result; replace the current logic in askAI and
askAIRaw to build the appropriate options (different maxTokens/prompt wrappers)
and delegate to askChain, keeping existing log format and fallback order.
- Around line 63-80: The createBrowser function currently hardcodes headless:
false which breaks CI; update createBrowser to set headless based on an
environment variable (e.g., process.env.HEADLESS or process.env.CI) and pass
that boolean into chromium.launch, and also ensure the verification branch that
calls page.waitForTimeout(60000) (the manual-verification block) short-circuits
when running headless/CI by returning false or throwing an appropriate error
instead of waiting 60s; reference the createBrowser function and the
verification branch that uses page.waitForTimeout to locate where to change the
behavior.
- Around line 663-858: The function guessAnswerByLabel contains hardcoded PII
and employer/education strings (e.g., "Cristian Camilo Montes Teheran", "Bogotá
DC, Colombia", phone 3143663821, "https://github.com/camilomont", SERVIMAX,
INTELIBPO, SENA 2024 and similar biographical literals); remove all hardcoded
personal data and instead read defaults from the provided profile object,
cvText, env or config/profile.yml (use profile.full_name, profile.location,
profile.phone, profile.email, profile.linkedin, profile.portfolio_url,
profile.salary_aspiration_cop, and extract employer/education snippets from
profile or cvText), and change employer/education-specific response texts to
generic templates that interpolate those profile/cv-derived values; apply the
same replacement pattern throughout guessAnswerByLabel (and other functions
referenced in the review such as main()) so no PII remains in the .mjs file.
- Around line 598-605: saveApplied currently calls
writeFileSync(LI_APPLIED_PATH, ...) which throws ENOENT if the containing data
directory doesn't exist; update saveApplied to ensure the parent directory of
LI_APPLIED_PATH exists before writing (e.g., use fs.mkdirSync(parentDir, {
recursive: true }) or equivalent) so the write succeeds on a clean
checkout—locate the saveApplied function and add a directory-creation step (use
path.dirname(LI_APPLIED_PATH) to get the parent) prior to calling writeFileSync;
keep loadApplied unchanged.
- Around line 17-22: Replace the CommonJS createRequire +
require('dotenv').config() workaround with the ESM-native import by removing
createRequire and the require call and adding a top-level import 'dotenv/config'
before any other imports; specifically remove the use of createRequire and the
require('dotenv').config() statement and ensure import 'dotenv/config' appears
as the very first import so env vars are loaded before modules like js-yaml or
playwright are imported.
In `@apply-loop.mjs`:
- Around line 54-57: The "Next run in N minutes" banner is printed after calling
runApply() causing it to interleave with child stdout; update the logic so when
!isOnce you either print the console.log banner before invoking runApply() or
move that banner emission into the child process's 'close' handler inside
runApply() so it only prints after the current run finishes; locate runApply,
the isOnce check and setInterval(intervalArg) usage and modify accordingly (or
add the banner emission to the child 'close' event) to ensure the banner appears
after each run finishes.
- Line 15: The current interval parsing (const intervalArg =
args.includes('--interval') ? parseInt(args[args.indexOf('--interval') + 1]) ||
5 : 5;) can call parseInt without a radix and will read undefined if
'--interval' is the last token, producing NaN that makes setInterval behave
incorrectly; update the parsing logic in apply-loop.mjs to (1) check that
args.indexOf('--interval')+1 is within bounds and that the next token is defined
before calling parseInt, (2) pass radix 10 to parseInt, (3) validate the parsed
value is a finite positive number and fall back to the default (5) while logging
or printing a warning when the provided token is missing or invalid, and (4) use
the validated numeric value when calling setInterval; reference the intervalArg
variable and the args array when locating where to change parsing and
validation.
- Around line 39-47: The close handler for proc uses process.exit(code) and only
checks code !== 0, but Node may call the 'close' event with code === null and a
non-null signal when the child was killed; this coerces to 0 and misreports
success. Update the proc.on('close', (code, signal) => { ... }) callback to
treat a non-null signal or null code as failure: log the signal when present
(alongside the exit code), change the error condition to if (code !== 0 ||
signal) and when calling process.exit use a numeric fallback (e.g., const
exitCode = typeof code === 'number' ? code : 1; process.exit(exitCode)). This
ensures proc.on('close'), the error log, and process.exit behave correctly for
signal-terminated children and --once mode.
In `@apply-pipeline.mjs`:
- Around line 76-95: runCommand(...) return values are ignored so failures in
scans or applies allow the script to continue; update the control flow in
apply-pipeline.mjs to check the boolean result of each runCommand call (the scan
calls for 'scan-bogota.mjs', 'scan-international.mjs', and 'scan.mjs' and the
apply call 'apply-auto.mjs'), and if any returns false immediately abort the
process (throw or call process.exit(nonZero)) before calling countPendingJobs()
or recording cycle history so a failed subprocess does not allow further steps
to run or produce a successful-looking cycle.
- Around line 140-145: The interval logic currently uses setInterval(main, ...)
which allows overlapping runs and accepts negative/zero values; change it to
validate the parsed minutes (ensure it's a positive integer, e.g., >=1) and
replace setInterval with a serialized loop that waits for each async run to
finish before scheduling the next (use an async self-rescheduling pattern via
setTimeout or an isRunning guard around main). Specifically, validate
process.argv parsing for '--interval' (from process.argv and parseInt), reject
or clamp invalid values, and implement serialization using a boolean flag (e.g.,
isRunning) or await main completion before scheduling the next execution so
writes to pipeline.md, pipeline-history.jsonl and scanner backups cannot
overlap.
In `@apply-prepare.mjs`:
- Around line 155-160: The loop currently calls guessValue(name, placeholder,
job.title) which causes guesses to use the job title; change it to extract the
actual field label text from the DOM and pass that to guessValue instead: for
each field, get its id (await field.getAttribute('id')) and try to find a
matching label via document.querySelector(`label[for="${id}"]`), or fall back to
field.closest('label') or the aria-label attribute, trim that label text, and
use it in place of job.title when calling guessValue(name, placeholder,
labelText); keep the existing fallbacks (name/placeholder) if no label is found
and still push the result into result.fields as before.
- Around line 179-184: The report header built in function buildReport currently
only includes title and date; update it to include the required header fields
"**URL:**" and "**Legitimacy:**" by adding those lines to the initial lines
array (the variable lines inside buildReport). If the values come from metadata,
extend buildReport signature to accept (results, url, legitimacy) or pull them
from an available metadata object and insert `**URL:** ${url}` and
`**Legitimacy:** ${legitimacy}` immediately after the date line so each
generated report includes those exact header fields.
In `@AUTO-APPLY-COMPLETE.md`:
- Around line 30-40: The file contains real credentials committed under the
top-level YAML keys computrabajo and linkedin; immediately rotate both account
passwords, replace the concrete values for the computrabajo and linkedin entries
with obvious placeholders (e.g. your-email@example.com / your-password)
consistent with other docs, and remove the sensitive values from history using a
history-rewrite tool (git filter-repo or BFG) and force-push, then invalidate
active sessions; after purging, ensure future secrets are stored out of repo
(environment variables/secret manager) and add any credential files to
.gitignore.
In `@AUTO-APPLY.md`:
- Around line 196-204: Replace the instruction that tells users to edit
data/applications.md directly with a directive to create a TSV in
batch/tracker-additions (use the naming pattern {num}-{company-slug}.tsv) and
then run node merge-tracker.mjs to merge and normalize entries; keep the step to
remove the URL from data/pipeline.md so apply-auto skips it and explicitly warn
that direct edits to data/applications.md bypass deduplication and status
normalization.
In `@auto-apply.mjs`:
- Around line 253-263: The loop function currently reads interval from args[1]
which breaks when callers use the flag form (e.g., "loop --interval 10") because
args[1] is "--interval" and parseInt returns NaN; update loop to accept both
styles by checking for a flag "--interval" in args (e.g., find
indexOf('--interval') and use the next arg) and fall back to the positional
args[1] if no flag is present, then parse to int with the same NaN -> default 5
behavior; modify the interval parsing logic inside function loop to handle both
the flag form and the positional form before calling spawnSync('node',
['apply-pipeline.mjs', '--interval', interval.toString()], ...).
- Around line 82-88: The current entries calculation can be negative when no
table rows exist; update the logic where entries is computed in auto-apply.mjs
(the readFileSync/content -> entries calculation) to first filter lines that
start with '|' but exclude separator rows (those composed only of pipes, dashes,
colons and whitespace), then compute the number of data rows by subtracting the
header line only (not both) and finally clamp the result to a minimum of 0
before logging; target the variable named entries and the surrounding block that
reads 'data/applications.md'.
- Around line 245-250: The --dry branch currently only logs but does not
propagate the flag to the child process, so spawnSync('node',
['apply-auto.mjs'], ...) should include '--dry' when args contains '--dry'
(modify the spawnSync call to build the argument array from args or append
'--dry' conditionally); also update apply-auto.mjs to read/process that --dry
flag (e.g., via its args parsing) so the child process actually simulates
instead of submitting. Use the existing symbols args and spawnSync and ensure
apply-auto.mjs honors the flag.
- Around line 341-404: The setup (and sequence) logic assumes config/ and data/
exist and misuse appendFileSync for bootstrapping files; ensure parent
directories exist (create config/ and data/ with mkdirSync(..., { recursive:
true }) before any file operation) and replace appendFileSync used to create new
template files (config/profile.yml, config/credentials.yml, data/pipeline.md)
with writeFileSync so you don't accidentally concatenate into partial files; for
reportPath and any other writes also ensure the containing directory exists
before calling appendFileSync/writeFileSync and only use appendFileSync when you
truly intend to append (leave existing behavior for report if appropriate).
In `@auto-scan.mjs`:
- Around line 8-18: The interval loop in auto-scan.mjs needs reliability fixes:
validate and parse the CLI arg with Number.parseInt(process.argv[2], 10) and
reject non-numeric or <=0 values (currently parseInt without radix silently
falls back), run the first scan immediately instead of waiting for setInterval,
prevent overlapping runs by tracking a running flag or queue and
skipping/spawning only when previous child has exited, attach child.on('error',
...) alongside child.on('close') to handle spawn failures, and install
SIGINT/SIGTERM handlers to cleanly kill any running child and clear the
interval; reference the variables and functions interval, setInterval,
spawn('node', ['scan.mjs']), child.on('close'), child.on('error') when making
these changes.
In `@COMIENZA-AQUI.md`:
- Around line 3-5: The file contains committed personal data (e.g., the
"**Para:** Cristian Camilo Montes" line, location/role like "**Bogotá, Colombia
| Full Stack / RPA Developer**", email addresses and plaintext
Computrabajo/LinkedIn passwords); remove all real names, locations, emails and
passwords and replace them with neutral placeholders (e.g., [REDACTED NAME],
[REDACTED EMAIL], [REDACTED PASSWORD]) in the document content,
rotate/invalidade the exposed credentials immediately, and then purge the
secrets from git history with a history-rewriting tool (git-filter-repo or BFG)
and force-push the cleaned branch to the remote; ensure commit messages or other
files do not retain the same secrets before merging.
In `@config/credentials.example.yml`:
- Around line 1-12: The file contains real credentials in the linkedin.email,
linkedin.password, computrabajo.email, and computrabajo.password entries;
immediately rotate those passwords (and the email account password if reused),
then purge the secrets from the repository history using git-filter-repo or BFG
on the branch. Replace the values in the example file with non-sensitive
placeholders (e.g., LINKEDIN_EMAIL_PLACEHOLDER, LINKEDIN_PASSWORD_PLACEHOLDER,
COMPUTRABAJO_EMAIL_PLACEHOLDER, COMPUTRABAJO_PASSWORD_PLACEHOLDER) and commit
that change, and update CI/workflows to reconstruct config/credentials.yml from
repository secrets (GitHub Actions secrets) rather than embedding real values.
Ensure the purged commits are force-pushed and inform reviewers of the rotation
and purge completion.
In `@cv.md`:
- Around line 50-62: The trailing block of cv.md is corrupted: remove or
consolidate the orphan lines ("Técnico en Sistemas, 2020", "Colegio Llano
Oriental (IED) – Bogotá", "Bachiller Académico, 2020") and delete the stray,
empty headers ("PROFESIONAL", "EDUCACIÓN", "MÍ", "HABILIDADES TÉCNICAS",
"INFORMACIÓN ADICIONAL") or convert them into proper section headings with
content; fix spacing/typos ("Ingles B1" → "Inglés B1"), correct the portfolio
URL by removing embedded spaces so it points to the valid site (replace
"https://mi -portaf olio-five-black.vercel.app" with the correct one), ensure
the GitHub URL is intact, and add a single trailing newline at EOF.
In `@data/apply-preparation-report.md`:
- Around line 1-24: Update each job section (e.g., the "Computrabajo —
Desarrollador Junior", "Computrabajo — Desarrollador", and "Computrabajo —
Desarrollador Full Stack" sections) to include a bold "URL:" and bold
"Legitimacy:" header line at the top of the section (replace the current "- URL:
…" bullet with a line starting with "**URL:** <url>" and add "**Legitimacy:**
<value>" immediately after), ensuring both fields appear in the mandated format
for every entry; additionally, omit or mark this autogenerated report as ignored
by adding data/apply-preparation-report.md to .gitignore so per-run outputs are
not committed.
In `@data/auto-apply-sequence-report.md`:
- Around line 1-10: The report file is missing the required header fields and
contains a near-verbatim duplicate run; add a top-level header containing
"**URL:** <repo-or-run-URL>" and "**Legitimacy:** <trusted|untrusted>" and then
remove the duplicated block (the repeated "## test-auto-apply" / "# Secuencia de
Auto-Apply" run) or split the second run into its own file (e.g. timestamped
filename) or merge it as a per-run subsection under the same report; locate the
content by the headers "# Secuencia de Auto-Apply" and "## test-auto-apply" to
perform the edit, scrub or confirm handling of the partial user emails found in
the runs (decide whether this file should be committed to the repo or added to
.gitignore/logs), and ensure the final file follows the guideline format with
both **URL:** and **Legitimacy:** present.
In `@data/li-applied.json`:
- Around line 1-68: The commit exposes personal LinkedIn application data in
data/li-applied.json and the cron workflow (.github/workflows/apply-auto.yml)
re-commits it; remove the sensitive file from the repo and prevent future leaks
by: add data/li-applied.json to .gitignore (alongside data/applications.md
etc.), replace the tracked file with either a .gitkeep or a template file
data/li-applied.example.json, purge the existing file from git history (e.g.,
via git filter-repo or BFG) so entries are removed from commits, and update the
apply-auto.yml workflow to stop committing user-owned data to main (store it
privately or skip committing). Ensure references in CI/workflows and any code
that reads data/li-applied.json are updated to use the new example or private
storage.
In `@data/pipeline-ci.md`:
- Around line 1-9: The top comment block (currently using leading '#' lines in
data/pipeline-ci.md) is rendering as Markdown headings and causing MD022/MD024
lint noise; replace that '#' prefixed comment block with an HTML comment block
(wrap the existing lines in <!-- ... -->) so the apply pipeline still ignores
non-checklist lines but GitHub preview and markdownlint won't render them as
headings—update the header block where the checklist format/description appears
to be commented and ensure the checklist lines beginning with '- [ ]' remain
outside the HTML comment so the parser continues to read them.
In `@diagnose-auto-apply.mjs`:
- Around line 158-161: The catch block handling Chromium launch currently calls
check('Chromium browser available', false, e.message) which already increments
the failed counter, then incorrectly does failed++ again; remove the redundant
failed++ so only check(...) updates failed, leaving the catch as: catch (e) {
check('Chromium browser available', false, e.message); } and keep the error
message passed through.
- Around line 132-144: The fetch calls in diagnose-auto-apply.mjs use the
unsupported timeout option and should use AbortSignal.timeout(5000) instead:
replace each fetch('https://...',{ timeout: 5000 }) with fetch('https://...', {
signal: AbortSignal.timeout(5000) }) and remove the .catch(e => null) wrapper so
that the try/catch can handle aborts; keep the same checks using
check('Computrabajo accessible', ...) and check('LinkedIn accessible', ...) but
ensure you handle a null/undefined response when fetch throws due to abort or
network error (e.g., treat as offline).
In `@inspect-computrabajo-2.mjs`:
- Around line 1-15: Collapse the multiple near-duplicate scripts into a single
CLI script (e.g. diagnose-computrabajo.mjs) that exposes flags like --selectors,
--html, --forms and drives the same logic currently in
inspect-computrabajo-2.mjs (browser/newPage/page.goto/page.content/$$eval) so we
don't keep duplicate files; ensure you wrap the Playwright flow in try/finally
around chromium.launch() and browser.close() so the browser is always closed if
any $$eval or other step throws; replace the { waitUntil: 'networkidle',
timeout: 60000 } + page.waitForTimeout(5000) pattern with a faster approach
using waitUntil: 'domcontentloaded' and explicit page.waitForSelector calls for
the key elements you need before doing page.content() or $$eval; and remove
hardcoded long timeouts in favor of a configurable timeout flag or sensible
shorter default.
In `@inspect-computrabajo.mjs`:
- Around line 1-12: The script hardcodes the URL and doesn't guard browser
cleanup; update the top-level async IIFE to accept the target URL from
process.argv[2] (with a sensible default or usage message) and wrap the
browser/page logic in try/finally so browser.close() always runs (or check
browser truthiness before closing). Locate the async anonymous function and
variables browser/page in the file (the chromium import, the IIFE, and calls to
page.goto, page.$$eval, and browser.close) and refactor to parse argv,
validate/print usage if missing, and ensure cleanup in finally; also consider
moving this into a reusable debug entry like scripts/debug/inspect.mjs if
consolidating similar tools.
In `@pdf_to_md.py`:
- Around line 1-22: Replace this Python script with a Node.js .mjs module:
remove the Python file and implement a CLI module (e.g., pdf_to_md.mjs) that
uses a supported JS PDF approach (Playwright or pdf-parse) instead of PyPDF2;
ensure you parse process.argv like the original, safely handle encrypted PDFs
(check and attempt to unlock or exit gracefully), guard against pages returning
null/undefined text (treat as empty string), avoid the noop replacement logic
(remove the redundant text.replace), wrap PDF parsing and file writes in
try/catch, accept an optional output path (default to cv.md) and write UTF-8
without blindly overwriting (create a clear overwrite behavior or prompt flag),
and update package.json dependencies (add pdf-parse or ensure Playwright is
present) so the runtime is reproducible. Reference the old symbols: pdf_file /
reader / page.extract_text() semantics when translating logic, and ensure the
new module follows project guidelines for .mjs, Playwright usage, and
YAML/config-driven behavior.
In `@QUICKSTART-ES.md`:
- Line 359: Fix the typo in the Spanish FAQ heading: replace the text "**P:
¿Néceesito Node.js instalado?**" with "**P: ¿Necesito Node.js instalado?**" in
QUICKSTART-ES.md so the user-facing heading reads "Necesito" instead of
"Néceesito".
In `@scan-bogota.mjs`:
- Around line 15-21: The current replacements on content (using fs.readFileSync
+ content.replace + fs.writeFileSync) inject location tokens inside existing
quoted terms and break YAML; instead parse portals.yml with js-yaml (use
yaml.load on fs.readFileSync), iterate cfg.portals and for entries where
portal.query matches /linkedin\.com\/jobs/ append ' "Colombia" "Bogotá"' to
portal.query (or add a separate location field) and then write back with
yaml.dump to preserve quoting; if you must keep regex, update the
content.replace patterns to append after the closing quote (not inside) by
targeting the full quoted token boundaries so you add tokens outside the
existing quotes rather than inserting into them.
- Around line 12-49: The script currently copies portals.yml to backup and
mutates it but does not guarantee restoration if execSync('node scan.mjs') or
the filtering steps fail; wrap the mutation/scan/filter sequence in a
try/finally so the restore (fs.copyFileSync(backup, 'portals.yml') and
fs.unlinkSync(backup)) always runs, and also check existence before creating the
initial backup (use fs.existsSync on 'portals.yml' and handle the missing file
case consistently with scan.mjs), ensuring any thrown errors are re-thrown after
finally so failures still surface.
In `@scan-international.mjs`:
- Around line 1-26: The script currently mutates portals.yml with fragile string
replacements and can leave it corrupted if execSync('node scan.mjs') throws; fix
by (1) adding an idempotent backup guard around fs.copyFileSync('portals.yml',
backup) so you don't overwrite an existing .bak (e.g., bail if backup exists or
create a uniquely named temp backup), (2) parse portals.yml as YAML (use a YAML
parser import) and programmatically remove or adjust specific entries (e.g.,
remove "Colombia", "Bogotá", "solicitud sencilla" from the relevant
arrays/fields rather than using content.replace), then write the modified YAML
back, and (3) wrap the execSync('node scan.mjs', {stdio:'inherit'}) call in a
try/finally block so the finally always restores the original file via
fs.copyFileSync(backup, 'portals.yml') and removes the backup with
fs.unlinkSync(backup) even if scan.mjs fails; also consider writing the modified
YAML to a temp file and atomically replacing portals.yml to minimize window of
corruption.
In `@SETUP-AUTO-APPLY.md`:
- Around line 345-393: The docs currently instruct users to edit apply-auto.mjs
(notably guessValue(), main(), and processJob() usage and the userDataDir
change), which risks losing user logic when the file auto-updates; instead
refactor apply-auto.mjs to be purely generic and read user customizations from
external YAML/extension data: move field mappings into a config key (e.g., a
mappings list that guessValue() reads at runtime), move credential rotation to a
credentials list (e.g., config/credentials-accountN.yml) that main() iterates
without hardcoded changes, expose a persistent-session flag and userDataDir path
in config rather than editing launch code, and expose job filter rules
(title/company patterns) in config that processJob() checks; update the docs to
instruct editing those YAML/extension files rather than modifying any *.mjs so
that apply-auto.mjs, modes/*, and other auto-updatable scripts remain untouched
by user-specific content.
- Around line 152-164: The cron example should not launch the forever-loop
command; update the Scenario C instructions to stop recommending `node
auto-apply.mjs loop 0` in cron and instead document a one-shot cron-safe command
or a process-manager approach: replace the crontab line with a one-time
invocation (e.g., the command that runs a single iteration or a dedicated "run
once" mode) or explicitly instruct readers to run `node auto-apply.mjs loop 5`
as a long-running foreground/service under a process manager (not via cron).
Ensure the doc references `auto-apply.mjs`, the `loop 0` token being removed
from cron examples, and the alternative `loop 5`/process-manager option.
In `@START-HERE-AUTO-APPLY.md`:
- Around line 56-63: The documentation references are inconsistent: several docs
and the AI summary point to a non-existent SETUP-AUTO-APPLY.md while the actual
entry file is START-HERE-AUTO-APPLY.md; fix by either renaming
START-HERE-AUTO-APPLY.md to SETUP-AUTO-APPLY.md or (recommended) update every
reference to SETUP-AUTO-APPLY.md (including entries in START-HERE-AUTO-APPLY.md,
AI summary, and any other docs that list it) to point to
START-HERE-AUTO-APPLY.md so links resolve correctly and remove
SETUP-AUTO-APPLY.md from lists if it doesn’t exist.
In `@temp-debug-computrabajo-selector-issue.mjs`:
- Around line 1-9: This temp-debug script
(temp-debug-computrabajo-selector-issue.mjs) should not be committed as-is —
either remove it or fold its logic into the long-lived diagnose-auto-apply.mjs
(or scripts/debug/) and add parameterization for URL/selectors instead of
hard-coding; while doing so, rename the local selector variables (sel1, sel2)
into descriptive names and ensure selector checking is reusable. Fix the
truncated userAgent used in newPage(...) to a full realistic UA string (include
Chrome/... Safari/...) to avoid fingerprinting, and wrap browser/page lifecycle
in try/finally (or try/catch/finally) around chromium.launch() /
browser.newPage() / page.goto() so browser.close() always runs to avoid leaking
Chromium processes. Ensure the refactor includes minimal docs or CLI args so the
check is maintainable if kept.
In `@temp-debug-computrabajo.mjs`:
- Around line 1-17: This temporary debug script should be removed or merged into
the maintained inspector (e.g., inspect-computrabajo.mjs); if you must keep it,
wrap Playwright usage in a try/finally so browser.close() always runs (protect
the browser variable and ensure page/buffer cleanup), and make the nodes mapping
robust by replacing e.textContent.trim() with a null-safe expression (e.g., use
optional chaining/defaults) when building nodes; also consider consolidating
duplicate selectors in the sels array and reusing existing inspector logic
instead of committing this scratch file.
In `@test-auto-apply.mjs`:
- Around line 108-118: The test currently only checks for presence of
credentials.computrabajo.email and credentials.linkedin.email; update the checks
to require both email and password (e.g., credentials.computrabajo.email &&
credentials.computrabajo.password and credentials.linkedin.email &&
credentials.linkedin.password) so the smoke test fails when passwords are
missing, and adjust the success messages to still mask the email (using the
existing split('@')[0]@... pattern) and the error messages (currently calling
error(...)) to indicate missing email or password for each portal; locate and
change the two conditional blocks that call success/error for Computrabajo and
LinkedIn in test-auto-apply.mjs.
- Around line 64-70: The loop currently uses bare returns on fatal setup errors
which bypass the final summary and process.exit; instead, when a required file
is missing or YAML is invalid (the branches that call error(...) and currently
return), set the shared failed flag to a positive value (e.g., failed =
Math.max(failed, 1) or failed++), break out of the loop so execution continues
to the final summary and the existing process.exit(failed > 0 ? 1 : 0) runs;
apply the same replacement for the other occurrences flagged (the branches
around lines 95-98 and 119-122) so existsSync / invalid-YAML error paths use
failed and break rather than returning immediately.
- Around line 142-155: The fetch calls are using a non-existent
RequestInit.timeout option; replace the timeout usage with an AbortSignal by
creating a signal via AbortSignal.timeout(5000) and pass it as the signal option
to fetch (e.g., const sig = AbortSignal.timeout(5000); const ctResp = await
fetch('https://co.computrabajo.com', { signal: sig });). Do the same for the
LinkedIn fetch (liResp) and remove the .catch(e => null) on the fetch calls so
the existing try/catch handles aborted/network errors; keep the existing
response checks (ctResp?.ok || ctResp?.status === 303 || ctResp?.status === 503)
unchanged.
In `@websearch-bogota.mjs`:
- Around line 26-35: The current search loop filters queries by checking q.query
for 'Bogotá' or 'Colombia' but then hardcodes the appended location string "- [
] ... | Bogotá, Colombia", causing Colombia-wide queries to be mislabeled;
update the filter and the line creation in the search loop (the queries array,
the for (const q of queries) loop, and the line variable creation where
searchGoogle is used) to derive location from the query object (use q.location
when present, fallback to parsing q.query for a city/country token if needed)
and only include queries whose intended scope matches the location you plan to
tag (or tighten the filter to q.location === 'Bogotá' when you want Bogotá-only
results); ensure the template uses the resolved location variable instead of the
hardcoded "Bogotá, Colombia".
- Around line 33-42: The loop currently uses appendFileSync to unconditionally
append pending lines (incrementing newOffers) which breaks scan.mjs's "insert
under ## Pendientes" contract, causes duplicates, fails if data/ is missing, and
can corrupt the pipe-separated line when q.name contains '|' or newlines;
replace the direct appendFileSync usage in websearch-bogota.mjs with a call to a
shared helper (factor the "insert under '## Pendientes' + dedupe against
existing URLs" logic out of scan.mjs into a new exported function that both
scripts import), ensure data/ exists using mkdirSync('data', { recursive: true
}) before any write, have the helper check the seen set (same logic scan.mjs
uses) before inserting, and sanitize q.name by stripping or escaping '|' and
newlines before composing the pending line (remove appendFileSync usage and only
increment newOffers when the helper actually inserts a new line).
- Around line 11-22: The searchGoogle() implementation and the pipeline-writing
logic are fragile: replace raw appendFileSync usage with a read-modify-write
that finds or creates the "## Pendientes" marker and inserts new entries inside
that section (don't write to EOF), add deduplication by implementing the
equivalent of loadSeenUrls() to check against existing pipeline.md and
scan-history.tsv before inserting, make searchGoogle() robust by handling Google
SERP shaping (prefer a real search API or direct site endpoints) and at minimum
add an AbortController-based timeout for fetch() and an inter-query delay in the
loop that calls searchGoogle(), ensure q.name is sanitized/escaped so pipeline
format cannot break (e.g., strip or escape "|" characters), and wrap main()
invocation with a top-level try/catch or .catch() so unhandled rejections exit
non-zero; refer to searchGoogle(), main(), and the pipeline write logic where
appendFileSync() is currently used.
---
Outside diff comments:
In @.gitignore:
- Around line 20-43: Add the two user log files to .gitignore so personal
activity logs aren't committed: append "data/li-applied.json" and
"data/applications-log.md" (place them near the "# Generated" or "# Secrets"
sections for clarity). Then update the test in test-auto-apply.mjs (the
assertion block around lines 220-241) to also assert that ".env" is present in
the .gitignore contents (extend the existing assertions that check for
config/credentials.yml to include ".env"). Ensure you reference the exact
filenames data/li-applied.json and data/applications-log.md and the test file
test-auto-apply.mjs when making the changes.
---
Duplicate comments:
In `@temp-debug-computrabajo-all.mjs`:
- Around line 1-27: This temp-debug script is a near-duplicate debug helper and
should be removed or consolidated into a single reusable script; either delete
temp-debug-computrabajo-all.mjs (and the other two duplicates) or refactor into
a parameterizable helper (e.g., scripts/debug/computrabajo-selectors.mjs) that
accepts URLs as args and is consumed by diagnose-auto-apply.mjs. If you choose
to keep/merge it, replace the hardcoded urls array with CLI args, use a full
realistic userAgent string for the browser.newPage call, wrap the browser
lifecycle in try/finally to ensure browser.close() always runs, and centralize
the selector list (sels) and the page.$$eval(...).catch(() => 0) pattern into a
shared function to avoid copy/paste.
🪄 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: 0db0290b-13f0-41c6-82c5-cefd75ecc572
⛔ Files ignored due to path filters (6)
debug-apply-auto.logis excluded by!**/*.logdebug-apply-auto2.logis excluded by!**/*.logdebug-apply-auto3.logis excluded by!**/*.logdebug-apply-auto4.logis excluded by!**/*.logdebug-apply-auto5.logis excluded by!**/*.loguploads/CamiloMontesCv .pdfis excluded by!**/*.pdf
📒 Files selected for processing (37)
.github/workflows/apply-auto.yml.gitignoreAUTO-APPLY-COMPLETE.mdAUTO-APPLY-IMPLEMENTATION.mdAUTO-APPLY.mdCOMIENZA-AQUI.mdQUICKSTART-ES.mdSETUP-AUTO-APPLY.mdSTART-HERE-AUTO-APPLY.mdapply-auto.mjsapply-computrabajo.mjsapply-linkedin.mjsapply-loop.mjsapply-pipeline.mjsapply-prepare.mjsauto-apply.mjsauto-scan.mjsconfig/credentials.example.ymlcv.mddata/applications-log.mddata/apply-preparation-report.mddata/auto-apply-sequence-report.mddata/li-applied.jsondata/pipeline-ci.mddiagnose-auto-apply.mjsinspect-computrabajo-2.mjsinspect-computrabajo.mjspackage.jsonpdf_to_md.pyscan-bogota.mjsscan-international.mjstemp-debug-computrabajo-all.mjstemp-debug-computrabajo-replica.mjstemp-debug-computrabajo-selector-issue.mjstemp-debug-computrabajo.mjstest-auto-apply.mjswebsearch-bogota.mjs
| - name: Write credentials.yml | ||
| env: | ||
| CT_EMAIL: ${{ secrets.CT_EMAIL }} | ||
| CT_PASSWORD: ${{ secrets.CT_PASSWORD }} | ||
| run: | | ||
| mkdir -p config | ||
| printf 'computrabajo:\n email: "%s"\n password: "%s"\nlinkedin:\n email: ""\n password: ""\n' \ | ||
| "$CT_EMAIL" "$CT_PASSWORD" > config/credentials.yml | ||
|
|
||
| - name: Write profile.yml | ||
| run: | | ||
| echo "${{ secrets.PROFILE_YML }}" > config/profile.yml | ||
|
|
||
| - name: Write .env (API keys IA) | ||
| run: | | ||
| [ -n "${{ secrets.ANTHROPIC_API_KEY }}" ] && echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> .env || true | ||
| [ -n "${{ secrets.OPENAI_API_KEY }}" ] && echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env || true | ||
| [ -n "${{ secrets.GEMINI_API_KEY }}" ] && echo "GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}" >> .env || true |
There was a problem hiding this comment.
Generate config files without shell-interpolating secrets.
Lines 55-66 can produce invalid YAML/.env when a secret contains quotes, newlines, or backslashes, and the direct ${{ secrets.* }} interpolation in the shell body increases injection risk. Pass secrets through env and serialize with a YAML/.env-safe writer instead.
Safer approach using environment variables and YAML serialization
- name: Write credentials.yml
env:
CT_EMAIL: ${{ secrets.CT_EMAIL }}
CT_PASSWORD: ${{ secrets.CT_PASSWORD }}
run: |
mkdir -p config
- printf 'computrabajo:\n email: "%s"\n password: "%s"\nlinkedin:\n email: ""\n password: ""\n' \
- "$CT_EMAIL" "$CT_PASSWORD" > config/credentials.yml
+ node --input-type=module <<'NODE'
+ import { writeFileSync } from 'node:fs';
+ import yaml from 'js-yaml';
+
+ writeFileSync('config/credentials.yml', yaml.dump({
+ computrabajo: {
+ email: process.env.CT_EMAIL || '',
+ password: process.env.CT_PASSWORD || '',
+ },
+ linkedin: {
+ email: process.env.LI_EMAIL || '',
+ password: process.env.LI_PASSWORD || '',
+ },
+ }), 'utf8');
+ NODE
- name: Write profile.yml
+ env:
+ PROFILE_YML: ${{ secrets.PROFILE_YML }}
run: |
- echo "${{ secrets.PROFILE_YML }}" > config/profile.yml
+ printf '%s\n' "$PROFILE_YML" > config/profile.yml
- name: Write .env (API keys IA)
+ env:
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
run: |
- [ -n "${{ secrets.ANTHROPIC_API_KEY }}" ] && echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}" >> .env || true
- [ -n "${{ secrets.OPENAI_API_KEY }}" ] && echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env || true
- [ -n "${{ secrets.GEMINI_API_KEY }}" ] && echo "GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}" >> .env || true
+ : > .env
+ [ -n "$ANTHROPIC_API_KEY" ] && printf 'ANTHROPIC_API_KEY=%s\n' "$ANTHROPIC_API_KEY" >> .env
+ [ -n "$OPENAI_API_KEY" ] && printf 'OPENAI_API_KEY=%s\n' "$OPENAI_API_KEY" >> .env
+ [ -n "$GEMINI_API_KEY" ] && printf 'GEMINI_API_KEY=%s\n' "$GEMINI_API_KEY" >> .env🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/apply-auto.yml around lines 49 - 66, The workflow
currently interpolates secrets directly inside the run scripts (steps "Write
credentials.yml", "Write profile.yml", "Write .env (API keys IA)"), which can
corrupt output or allow injection; instead, export each secret into the step's
env (e.g., CT_EMAIL, CT_PASSWORD, PROFILE_YML, ANTHROPIC_API_KEY,
OPENAI_API_KEY, GEMINI_API_KEY) and change the run bodies to serialize safely
(for YAML use a short safe dumper, e.g., a one-liner Python/Node script that
loads values from os.environ and writes proper YAML, and for .env build
key=value lines only if the env var is non-empty using the same script) so no
secrets are directly expanded by the shell and all values are properly
escaped/quoted.
| main().catch(e => { | ||
| console.error('[FATAL]', e); | ||
| process.exit(1); | ||
| }); |
There was a problem hiding this comment.
main handles credential errors via process.exit(1) but leaks the browser on async failures.
createBrowser() at Line 1266 opens a browser but if anything inside the try throws a Playwright error, it's caught by the outer .catch(...) at Line 1378 and the process exits without browser.close() (the finally at Line 1373 only runs if the error is thrown after createBrowser resolves — which it is here, so that's fine — BUT the process.exit(1) inside the catch terminates before any cleanup). Also, throwing process.exit(1) inside the top-level catch skips flushing of stdout/stderr. Prefer process.exitCode = 1 plus a natural return, or ensure browser.close() in the catch path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-linkedin.mjs` around lines 1378 - 1381, The top-level catch uses
process.exit(1) which can terminate the process before Playwright resources
(created by createBrowser()) are closed; update the main() error handling so it
does not call process.exit(1) directly — either set process.exitCode = 1 and
return, or explicitly attempt to close the browser if it exists (call
browser.close()) before exiting; locate the main() invocation and its
.catch(...) handler and change it to set process.exitCode = 1 (or call await
browser.close() if browser is accessible) and let the process finish naturally
so cleanup/fd flushing can occur.
| import { spawn } from 'child_process'; | ||
|
|
||
| const args = process.argv.slice(2); | ||
| const intervalArg = args.includes('--interval') ? parseInt(args[args.indexOf('--interval') + 1]) || 5 : 5; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor: argument parsing edge cases.
parseInt(args[args.indexOf('--interval') + 1]) has two small issues: no radix argument, and if --interval is the last token the index lookup reads undefined and falls through to 5 silently (no warning to the user). Also, setInterval with a non-numeric/NaN value would schedule immediately. Consider validating explicitly:
-const intervalArg = args.includes('--interval') ? parseInt(args[args.indexOf('--interval') + 1]) || 5 : 5;
+const idx = args.indexOf('--interval');
+const parsed = idx >= 0 ? Number.parseInt(args[idx + 1], 10) : NaN;
+const intervalArg = Number.isFinite(parsed) && parsed > 0 ? parsed : 5;📝 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 intervalArg = args.includes('--interval') ? parseInt(args[args.indexOf('--interval') + 1]) || 5 : 5; | |
| const idx = args.indexOf('--interval'); | |
| const parsed = idx >= 0 ? Number.parseInt(args[idx + 1], 10) : NaN; | |
| const intervalArg = Number.isFinite(parsed) && parsed > 0 ? parsed : 5; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-loop.mjs` at line 15, The current interval parsing (const intervalArg =
args.includes('--interval') ? parseInt(args[args.indexOf('--interval') + 1]) ||
5 : 5;) can call parseInt without a radix and will read undefined if
'--interval' is the last token, producing NaN that makes setInterval behave
incorrectly; update the parsing logic in apply-loop.mjs to (1) check that
args.indexOf('--interval')+1 is within bounds and that the next token is defined
before calling parseInt, (2) pass radix 10 to parseInt, (3) validate the parsed
value is a finite positive number and fall back to the default (5) while logging
or printing a warning when the provided token is missing or invalid, and (4) use
the validated numeric value when calling setInterval; reference the intervalArg
variable and the args array when locating where to change parsing and
validation.
| proc.on('close', (code) => { | ||
| if (code !== 0) { | ||
| console.error(`Error: apply-auto.mjs exited with code ${code}`); | ||
| } | ||
|
|
||
| if (isOnce) { | ||
| process.exit(code); | ||
| } | ||
| }); |
There was a problem hiding this comment.
process.exit(code) can receive null when the child is killed by a signal.
When the child terminates via a signal (e.g. SIGTERM), Node's close event fires with code === null and a non-null signal. Passing null to process.exit coerces to 0, so a signal-killed child will falsely report success in --once mode, and in loop mode the error branch on line 40 will not fire either.
🛡 Proposed fix
- proc.on('close', (code) => {
- if (code !== 0) {
- console.error(`Error: apply-auto.mjs exited with code ${code}`);
- }
-
- if (isOnce) {
- process.exit(code);
- }
- });
+ proc.on('close', (code, signal) => {
+ if (code !== 0 || signal) {
+ console.error(`Error: apply-auto.mjs exited with code=${code} signal=${signal}`);
+ }
+ if (isOnce) {
+ process.exit(code ?? (signal ? 1 : 0));
+ }
+ });📝 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.
| proc.on('close', (code) => { | |
| if (code !== 0) { | |
| console.error(`Error: apply-auto.mjs exited with code ${code}`); | |
| } | |
| if (isOnce) { | |
| process.exit(code); | |
| } | |
| }); | |
| proc.on('close', (code, signal) => { | |
| if (code !== 0 || signal) { | |
| console.error(`Error: apply-auto.mjs exited with code=${code} signal=${signal}`); | |
| } | |
| if (isOnce) { | |
| process.exit(code ?? (signal ? 1 : 0)); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-loop.mjs` around lines 39 - 47, The close handler for proc uses
process.exit(code) and only checks code !== 0, but Node may call the 'close'
event with code === null and a non-null signal when the child was killed; this
coerces to 0 and misreports success. Update the proc.on('close', (code, signal)
=> { ... }) callback to treat a non-null signal or null code as failure: log the
signal when present (alongside the exit code), change the error condition to if
(code !== 0 || signal) and when calling process.exit use a numeric fallback
(e.g., const exitCode = typeof code === 'number' ? code : 1;
process.exit(exitCode)). This ensures proc.on('close'), the error log, and
process.exit behave correctly for signal-terminated children and --once mode.
| if (!isOnce) { | ||
| setInterval(runApply, intervalArg * 60 * 1000); | ||
| console.log(`⏰ Next run in ${intervalArg} minutes...\n`); | ||
| } |
There was a problem hiding this comment.
Log message order is misleading.
runApply() is invoked synchronously on line 51, logs "Run #1", then line 56 logs "Next run in N minutes" — but that banner appears interleaved with the child's stdout and may print before the first run finishes. Consider moving the banner above runApply() or emitting it inside the close handler when !isOnce.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-loop.mjs` around lines 54 - 57, The "Next run in N minutes" banner is
printed after calling runApply() causing it to interleave with child stdout;
update the logic so when !isOnce you either print the console.log banner before
invoking runApply() or move that banner emission into the child process's
'close' handler inside runApply() so it only prints after the current run
finishes; locate runApply, the isOnce check and setInterval(intervalArg) usage
and modify accordingly (or add the banner emission to the child 'close' event)
to ensure the banner appears after each run finishes.
| if (credentials.computrabajo?.email) { | ||
| success(`Computrabajo: ${credentials.computrabajo.email.split('@')[0]}@...`); | ||
| } else { | ||
| error('Computrabajo credentials missing'); | ||
| } | ||
|
|
||
| if (credentials.linkedin?.email) { | ||
| success(`LinkedIn: ${credentials.linkedin.email.split('@')[0]}@...`); | ||
| } else { | ||
| error('LinkedIn credentials missing'); | ||
| } |
There was a problem hiding this comment.
Validate passwords, not just emails.
The apply flow requires both email and password for portal login, but this smoke test passes when only the email is present. Add password checks so CI/setup catches incomplete config/credentials.yml.
Add password assertions
if (credentials.computrabajo?.email) {
success(`Computrabajo: ${credentials.computrabajo.email.split('@')[0]}@...`);
} else {
- error('Computrabajo credentials missing');
+ error('Computrabajo email missing');
+ }
+
+ if (credentials.computrabajo?.password) {
+ success('Computrabajo password present');
+ } else {
+ error('Computrabajo password missing');
}
if (credentials.linkedin?.email) {
success(`LinkedIn: ${credentials.linkedin.email.split('@')[0]}@...`);
} else {
- error('LinkedIn credentials missing');
+ error('LinkedIn email missing');
+ }
+
+ if (credentials.linkedin?.password) {
+ success('LinkedIn password present');
+ } else {
+ error('LinkedIn password missing');
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test-auto-apply.mjs` around lines 108 - 118, The test currently only checks
for presence of credentials.computrabajo.email and credentials.linkedin.email;
update the checks to require both email and password (e.g.,
credentials.computrabajo.email && credentials.computrabajo.password and
credentials.linkedin.email && credentials.linkedin.password) so the smoke test
fails when passwords are missing, and adjust the success messages to still mask
the email (using the existing split('@')[0]@... pattern) and the error messages
(currently calling error(...)) to indicate missing email or password for each
portal; locate and change the two conditional blocks that call success/error for
Computrabajo and LinkedIn in test-auto-apply.mjs.
| try { | ||
| const ctResp = await fetch('https://co.computrabajo.com', { timeout: 5000 }).catch(e => null); | ||
| if (ctResp?.ok || ctResp?.status === 303 || ctResp?.status === 503) { | ||
| success('Computrabajo is reachable'); | ||
| } else { | ||
| error(`Computrabajo returned: ${ctResp?.status || 'no response'}`); | ||
| } | ||
| } catch (e) { | ||
| error(`Computrabajo unreachable: ${e.message}`); | ||
| } | ||
|
|
||
| try { | ||
| const liResp = await fetch('https://linkedin.com', { timeout: 5000 }).catch(e => null); | ||
| if (liResp?.ok || liResp?.status === 303 || liResp?.status === 503) { |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Node.js v22 native fetch support a RequestInit timeout option, or should AbortSignal.timeout() be used for request timeouts?
💡 Result:
No, Node.js v22 native fetch does not support a direct "timeout" option in RequestInit. Use AbortSignal.timeout for request timeouts. The native fetch in Node.js v22 (powered by undici) follows the web standard Fetch API specification. RequestInit accepts a "signal" option of type AbortSignal, but no direct "timeout" property. Attempting { timeout: number } will cause a TypeError, as "timeout" is not a key of RequestInit. AbortSignal.timeout(ms), available since Node.js v17.3.0, creates an AbortSignal that aborts after the specified milliseconds, throwing a TimeoutError DOMException. This is the standard, web-compatible way to implement request timeouts: try { const response = await fetch(url, { signal: AbortSignal.timeout(5000) }); } catch (err) { if (err.name === 'TimeoutError') { console.log('Request timed out after 5s'); } } This works for overall request timeouts (connect + headers + body). Note limitations: - Internal undici connect timeouts (default 10s) may trigger earlier for connection issues; AbortSignal does not override these (e.g., UND_ERR_CONNECT_TIMEOUT). - For fine-grained control (e.g., headersTimeout, bodyTimeout), use undici's Agent as dispatcher: { dispatcher: new Agent({ headersTimeout: 0, bodyTimeout: 0 }) }, but this requires importing undici. Official sources confirm no timeout in RequestInit and recommend AbortSignal for fetch.
Citations:
- 1: [Question] I'm on Node v18 using fetch as a built-in. How am I expected to configure the timeouts? nodejs/undici#1365
- 2:
fetch()in Node.js ignores connection timeout; no way to override 10s default nodejs/undici#4215 - 3: RequestInit - Type '"timeout"' is not assignable to type 'keyof RequestInit' DefinitelyTyped/DefinitelyTyped#59314
- 4: https://medium.com/deno-the-complete-reference/timeout-fetch-request-in-node-js-4231f33a9b95
- 5: https://web.nodejs.cn/en-US/docs/Web/API/AbortSignal/timeout_static
- 6: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static
- 7: https://nodejs.org/api/globals.html
- 8: https://nodejs.dev/en/learn/getting-started/fetch
- 9: https://github.com/nodejs/undici/
- 10: https://github.com/nodejs/undici?tab=readme-ov-file
- 11: https://www.npmjs.com/package/undici
🏁 Script executed:
git ls-files "test-auto-apply.mjs"Repository: santifer/career-ops
Length of output: 82
🏁 Script executed:
# Check if test-auto-apply.mjs exists and get line count
if [ -f "test-auto-apply.mjs" ]; then
wc -l test-auto-apply.mjs
fiRepository: santifer/career-ops
Length of output: 86
🏁 Script executed:
# Check Node.js version requirement in package.json
if [ -f "package.json" ]; then
cat package.json | jq '.engines' 2>/dev/null || grep -A2 '"engines"' package.json
fiRepository: santifer/career-ops
Length of output: 67
🏁 Script executed:
# View lines 140-160 from test-auto-apply.mjs
sed -n '140,160p' test-auto-apply.mjsRepository: santifer/career-ops
Length of output: 817
🏁 Script executed:
# Check package.json for engines field in plain text
cat package.json | head -50Repository: santifer/career-ops
Length of output: 1144
Use AbortSignal.timeout() for network request timeouts.
Native Node.js fetch does not support a timeout option in RequestInit. These fetch calls will not timeout as intended. Use AbortSignal.timeout(5000) instead, which is the standard web API approach supported since Node.js v17.3.0.
Apply abort-based fetch timeouts
- const ctResp = await fetch('https://co.computrabajo.com', { timeout: 5000 }).catch(e => null);
+ const ctResp = await fetch('https://co.computrabajo.com', {
+ signal: AbortSignal.timeout(5000),
+ }).catch(() => null);
@@
- const liResp = await fetch('https://linkedin.com', { timeout: 5000 }).catch(e => null);
+ const liResp = await fetch('https://linkedin.com', {
+ signal: AbortSignal.timeout(5000),
+ }).catch(() => null);📝 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.
| try { | |
| const ctResp = await fetch('https://co.computrabajo.com', { timeout: 5000 }).catch(e => null); | |
| if (ctResp?.ok || ctResp?.status === 303 || ctResp?.status === 503) { | |
| success('Computrabajo is reachable'); | |
| } else { | |
| error(`Computrabajo returned: ${ctResp?.status || 'no response'}`); | |
| } | |
| } catch (e) { | |
| error(`Computrabajo unreachable: ${e.message}`); | |
| } | |
| try { | |
| const liResp = await fetch('https://linkedin.com', { timeout: 5000 }).catch(e => null); | |
| if (liResp?.ok || liResp?.status === 303 || liResp?.status === 503) { | |
| try { | |
| const ctResp = await fetch('https://co.computrabajo.com', { | |
| signal: AbortSignal.timeout(5000), | |
| }).catch(() => null); | |
| if (ctResp?.ok || ctResp?.status === 303 || ctResp?.status === 503) { | |
| success('Computrabajo is reachable'); | |
| } else { | |
| error(`Computrabajo returned: ${ctResp?.status || 'no response'}`); | |
| } | |
| } catch (e) { | |
| error(`Computrabajo unreachable: ${e.message}`); | |
| } | |
| try { | |
| const liResp = await fetch('https://linkedin.com', { | |
| signal: AbortSignal.timeout(5000), | |
| }).catch(() => null); | |
| if (liResp?.ok || liResp?.status === 303 || liResp?.status === 503) { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test-auto-apply.mjs` around lines 142 - 155, The fetch calls are using a
non-existent RequestInit.timeout option; replace the timeout usage with an
AbortSignal by creating a signal via AbortSignal.timeout(5000) and pass it as
the signal option to fetch (e.g., const sig = AbortSignal.timeout(5000); const
ctResp = await fetch('https://co.computrabajo.com', { signal: sig });). Do the
same for the LinkedIn fetch (liResp) and remove the .catch(e => null) on the
fetch calls so the existing try/catch handles aborted/network errors; keep the
existing response checks (ctResp?.ok || ctResp?.status === 303 || ctResp?.status
=== 503) unchanged.
| async function searchGoogle(query) { | ||
| const url = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=10`; | ||
| const response = await fetch(url, { | ||
| headers: { | ||
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' | ||
| } | ||
| }); | ||
| const text = await response.text(); | ||
| // Simple regex to extract job links (basic, not perfect) | ||
| const links = [...text.matchAll(/https:\/\/[^"]*job[^"]*/g)].map(m => m[0]); | ||
| return [...new Set(links)].slice(0, 5); // Unique, limit 5 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -type f -name "websearch-bogota.mjs" -o -name "scan.mjs"Repository: santifer/career-ops
Length of output: 233
🏁 Script executed:
git ls-files | grep -E "(websearch-bogota|scan\.mjs|pipeline\.md)" | head -20Repository: santifer/career-ops
Length of output: 294
🏁 Script executed:
cat -n websearch-bogota.mjsRepository: santifer/career-ops
Length of output: 2036
🏁 Script executed:
cat -n scan.mjs | head -100Repository: santifer/career-ops
Length of output: 3529
🏁 Script executed:
cat -n scan.mjs | sed -n '100,250p'Repository: santifer/career-ops
Length of output: 5892
🏁 Script executed:
cat -n scan.mjs | grep -A 30 "appendFileSync.*pipeline"Repository: santifer/career-ops
Length of output: 45
🏁 Script executed:
head -50 data/pipeline.md 2>/dev/null || echo "File not found or empty"Repository: santifer/career-ops
Length of output: 86
🏁 Script executed:
cat -n scan.mjs | sed -n '250,350p'Repository: santifer/career-ops
Length of output: 4087
🏁 Script executed:
cat -n websearch-bogota.mjsRepository: santifer/career-ops
Length of output: 2036
🏁 Script executed:
ls -la data/ 2>/dev/null || echo "data/ directory does not exist"Repository: santifer/career-ops
Length of output: 611
🏁 Script executed:
cat -n websearch-bogota.mjs | tail -3Repository: santifer/career-ops
Length of output: 94
🏁 Script executed:
grep -n "main()" websearch-bogota.mjsRepository: santifer/career-ops
Length of output: 100
🏁 Script executed:
cat data/pipeline-ci.md 2>/dev/null | head -30Repository: santifer/career-ops
Length of output: 1530
🏁 Script executed:
grep -n "q\.name" websearch-bogota.mjsRepository: santifer/career-ops
Length of output: 223
🏁 Script executed:
wc -l scan.mjsRepository: santifer/career-ops
Length of output: 75
Appending to data/pipeline.md without respecting the ## Pendientes section structure will corrupt the file.
Line 36 uses appendFileSync() to write directly to EOF, bypassing the section boundary logic that scan.mjs implements (lines 188–217). Entries will land below subsequent sections like ## Procesadas, breaking parsing. Instead, adopt scan.mjs's approach: read the file, locate or create the ## Pendientes marker, insert entries within that section, then rewrite.
Missing deduplication against existing pipeline and history. The script has no loadSeenUrls() equivalent (scan.mjs lines 139–168). It will append duplicate URLs from scan-history.tsv and pipeline.md.
Google SERP scraping with a generic regex is unreliable and fragile.
- Google frequently returns a consent interstitial (especially from CI IPs) or a region-redirected page, not search results.
- Organic result links in the raw HTML are wrapped as
/url?q=<encoded>&..., not directhttps://…URLs, sohttps:\/\/[^"]*job[^"]*will primarily capture Google's own asset/tracking URLs, AMP links, and schema.org references, not real offers. - The
jobsubstring is also a very loose filter —support.google.com/.../job...andschema.org/JobPostingwill match. - Unauthenticated programmatic scraping of Google Search is against Google's Terms of Service; on CI this is also likely to trigger rate-limiting/CAPTCHA.
Additional issues:
- Line 13:
fetch()has no timeout; a single slow request hangs the entire scan. AddAbortControllerwithsetTimeout(compare scan.mjs lines 111–121). - Line 29–39: Loop has no inter-query delay, increasing block risk.
- Line 35:
q.nameis embedded raw in the pipeline format; if it contains|, the format breaks. - Line 47:
main()is called without a try-catch or.catch()handler. Unhandled rejections won't exit with non-zero status.
Prefer a proper search API (Google Programmable Search / Bing Web Search / SerpAPI) or call LinkedIn/Computrabajo/Indeed search pages directly with Playwright (which the rest of the repo already uses). At minimum, add per-request timeout, inter-query delay, dedup logic, and section-aware pipeline insertion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@websearch-bogota.mjs` around lines 11 - 22, The searchGoogle() implementation
and the pipeline-writing logic are fragile: replace raw appendFileSync usage
with a read-modify-write that finds or creates the "## Pendientes" marker and
inserts new entries inside that section (don't write to EOF), add deduplication
by implementing the equivalent of loadSeenUrls() to check against existing
pipeline.md and scan-history.tsv before inserting, make searchGoogle() robust by
handling Google SERP shaping (prefer a real search API or direct site endpoints)
and at minimum add an AbortController-based timeout for fetch() and an
inter-query delay in the loop that calls searchGoogle(), ensure q.name is
sanitized/escaped so pipeline format cannot break (e.g., strip or escape "|"
characters), and wrap main() invocation with a top-level try/catch or .catch()
so unhandled rejections exit non-zero; refer to searchGoogle(), main(), and the
pipeline write logic where appendFileSync() is currently used.
| const queries = portals.search_queries.filter(q => q.enabled && (q.query.includes('Bogotá') || q.query.includes('Colombia'))); | ||
|
|
||
| let newOffers = 0; | ||
| for (const q of queries) { | ||
| console.log(`Searching: ${q.query}`); | ||
| try { | ||
| const links = await searchGoogle(q.query); | ||
| for (const link of links) { | ||
| if (link.includes('linkedin') || link.includes('computrabajo') || link.includes('indeed')) { | ||
| const line = `- [ ] ${link} | ${q.name} | Job from search | Bogotá, Colombia\n`; |
There was a problem hiding this comment.
Hardcoded Bogotá, Colombia location and loose query filter.
The query filter keeps anything whose text contains Bogotá or Colombia, but every appended entry is labeled Bogotá, Colombia regardless of the actual query — a Colombia-wide search will be mislabeled as Bogotá, which will mislead location-based filters downstream in apply-pipeline.mjs / auto-apply.mjs. Consider deriving the location from q.location (or from the query string) instead of hardcoding it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@websearch-bogota.mjs` around lines 26 - 35, The current search loop filters
queries by checking q.query for 'Bogotá' or 'Colombia' but then hardcodes the
appended location string "- [ ] ... | Bogotá, Colombia", causing Colombia-wide
queries to be mislabeled; update the filter and the line creation in the search
loop (the queries array, the for (const q of queries) loop, and the line
variable creation where searchGoogle is used) to derive location from the query
object (use q.location when present, fallback to parsing q.query for a
city/country token if needed) and only include queries whose intended scope
matches the location you plan to tag (or tighten the filter to q.location ===
'Bogotá' when you want Bogotá-only results); ensure the template uses the
resolved location variable instead of the hardcoded "Bogotá, Colombia".
| for (const link of links) { | ||
| if (link.includes('linkedin') || link.includes('computrabajo') || link.includes('indeed')) { | ||
| const line = `- [ ] ${link} | ${q.name} | Job from search | Bogotá, Colombia\n`; | ||
| appendFileSync('data/pipeline.md', line); | ||
| newOffers++; | ||
| } | ||
| } | ||
| } catch (e) { | ||
| console.error(`Error searching ${q.name}: ${e.message}`); | ||
| } |
There was a problem hiding this comment.
Appends break the pipeline.md contract enforced by scan.mjs.
scan.mjs inserts new offers inside the ## Pendientes section (between that heading and the next ## section), so deduplication and downstream parsing rely on that structure. This script uses appendFileSync('data/pipeline.md', line) unconditionally, which will:
- Write lines at end-of-file — likely after later sections (e.g.,
## Aplicadas,## Rechazadas), polluting those sections and shifting already-applied offers' semantics. - Skip dedup:
scan.mjsbuilds aseenset from existing- [ ] <url>/- [x] <url>lines to avoid re-adding offers. This script re-appends the same URL on every run, so repeated scans will produce duplicate pending entries. - Fail hard if
data/does not exist (the coding guidelines require missing-data/to be handled gracefully). - Corrupt the line format if
q.namecontains|(the column separator) or a newline.
Please reuse the same "insert under ## Pendientes + dedup against existing URLs" logic that scan.mjs already implements (factor it into a shared helper and import it from both). Also ensure data/ exists via mkdirSync(..., { recursive: true }) before writing.
As per coding guidelines: "scripts handle missing data/ directories gracefully" (**/*.mjs).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@websearch-bogota.mjs` around lines 33 - 42, The loop currently uses
appendFileSync to unconditionally append pending lines (incrementing newOffers)
which breaks scan.mjs's "insert under ## Pendientes" contract, causes
duplicates, fails if data/ is missing, and can corrupt the pipe-separated line
when q.name contains '|' or newlines; replace the direct appendFileSync usage in
websearch-bogota.mjs with a call to a shared helper (factor the "insert under
'## Pendientes' + dedupe against existing URLs" logic out of scan.mjs into a new
exported function that both scripts import), ensure data/ exists using
mkdirSync('data', { recursive: true }) before any write, have the helper check
the seen set (same logic scan.mjs uses) before inserting, and sanitize q.name by
stripping or escaping '|' and newlines before composing the pending line (remove
appendFileSync usage and only increment newOffers when the helper actually
inserts a new line).
| const PROFILE = yaml.load(readFileSync('config/profile.yml', 'utf8')); | ||
| const CREDENTIALS = yaml.load(readFileSync('config/credentials.yml', 'utf8')); | ||
|
|
||
| const url = process.argv[2]; | ||
| if (!url) { | ||
| console.error('Usage: node apply-computrabajo.mjs <url>'); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
Top-level readFileSync/yaml.load crashes before argument validation and produces ugly traces.
If config/profile.yml or config/credentials.yml is missing, the script throws ENOENT at module-evaluation time — before the usage check at Line 24 — so node apply-computrabajo.mjs (without args) shows a stack trace instead of the intended Usage: message. Also, in CI the workflow reconstructs these files from secrets; any ordering mishap causes a non-actionable failure. Move loading into main() behind existsSync/try-catch and emit friendly errors.
🔧 Proposed fix
-const PROFILE = yaml.load(readFileSync('config/profile.yml', 'utf8'));
-const CREDENTIALS = yaml.load(readFileSync('config/credentials.yml', 'utf8'));
-
-const url = process.argv[2];
-if (!url) {
+const url = process.argv[2];
+if (!url) {
console.error('Usage: node apply-computrabajo.mjs <url>');
process.exit(1);
}
+for (const p of ['config/profile.yml', 'config/credentials.yml']) {
+ if (!existsSync(p)) {
+ console.error(`Error: missing ${p}`);
+ process.exit(1);
+ }
+}
+const PROFILE = yaml.load(readFileSync('config/profile.yml', 'utf8')) || {};
+const CREDENTIALS = yaml.load(readFileSync('config/credentials.yml', 'utf8')) || {};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-computrabajo.mjs` around lines 20 - 27, Move the synchronous config
reads out of module-evaluation into runtime so missing files don't crash before
arg validation: stop calling readFileSync/yaml.load at top-level for PROFILE and
CREDENTIALS; instead, inside main() (or the entry function that validates
process.argv and handles usage) check for file existence with fs.existsSync (or
wrap readFileSync in try/catch), then read and yaml.load the files, logging a
friendly error and process.exit(1) if they are absent or invalid. Update any
references to PROFILE/CREDENTIALS to use the loaded variables from main() or
pass them into functions that need them.
| } | ||
|
|
||
| async function main() { | ||
| const browser = await chromium.launch({ headless: false }); // Set to true for headless |
There was a problem hiding this comment.
headless: false is incompatible with CI execution.
The PR summary states headless should be configurable for CI, and .github/workflows/apply-auto.yml runs this under a headless runner. Hardcoding headless: false will cause Chromium to fail to launch (no display) in CI. Drive this from an env var (e.g. HEADLESS/CI) so it works both interactively and on GitHub Actions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-computrabajo.mjs` at line 30, The chromium.launch call hardcodes
headless: false which breaks CI; change the chromium.launch invocation that
creates "browser" so headless is driven from an environment variable (e.g.
process.env.HEADLESS or process.env.CI) instead of a literal false: read the env
var, coerce it to a boolean (treat "0"/"false"/empty as false, otherwise true)
and pass that value as the headless option to chromium.launch so local
interactive runs can be non-headless while GitHub Actions/CI runs default to
headless.
| const page = await context.newPage({ | ||
| userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Playwright BrowserContext.newPage() accept options argument userAgent
💡 Result:
No, BrowserContext.newPage does not accept a userAgent option in its arguments. The method signature is context.newPage with no options parameter, as shown in the official Playwright API documentation for BrowserContext.newPage. It creates a new page inheriting all settings from the parent BrowserContext, including userAgent if set during context creation via browser.newContext({ userAgent: '...' }). To use a custom userAgent: 1. Create the context with the option: const context = await browser.newContext({ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }); 2. Then create pages: const page = await context.newPage; This applies the userAgent to all pages in that context, affecting navigator.userAgent and request headers. Confirmed by Playwright tests showing context-level userAgent application to new pages, and official docs listing userAgent as a Browser.newContext option (deprecated in some contexts but still functional). Note: userAgent is marked as deprecated in Browser.newContext docs, but remains supported and tested in recent versions.
Citations:
- 1: https://playwright.dev/docs/api/class-browsercontext
- 2: https://github.com/microsoft/playwright/blob/main/tests/library/browsercontext-user-agent.spec.ts
- 3: https://playwright.dev/docs/next/api/class-browsercontext
- 4: https://playwright.dev/docs/api/class-browser?_highlight=newcontext
- 5: https://playwright.dev/docs/emulation
- 6: https://playwright.dev/docs/test-use-options
- 7: https://playwright.dev/docs/next/api/class-browser
- 8: https://playwright.dev/docs/api/class-browser
Invalid Playwright API: context.newPage() does not accept options.
BrowserContext.newPage() takes no arguments in Playwright — the { userAgent: ... } object here is silently ignored, so the anti-detection user agent is not actually applied. The userAgent option must be set on browser.newContext(...) instead.
🔧 Proposed fix
- const browser = await chromium.launch({ headless: false }); // Set to true for headless
- const context = await browser.newContext();
- const page = await context.newPage({
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
- });
+ const browser = await chromium.launch({
+ headless: process.env.HEADLESS !== 'false',
+ });
+ const context = await browser.newContext({
+ userAgent:
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+ });
+ const page = await context.newPage();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-computrabajo.mjs` around lines 32 - 34, The call to context.newPage({
userAgent: ... }) is invalid because BrowserContext.newPage accepts no args;
move the userAgent option to the context creation: create the context with
browser.newContext({ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36' }) (or update the existing context creation) and then call
context.newPage() with no arguments; remove the userAgent object from the
context.newPage() call and ensure the variable names (browser, context, page)
match the existing code (e.g., where context is created and where newPage() is
invoked).
| const requiresLogin = await page.url().includes('login') || | ||
| await page.$('input[name=email]') != null || | ||
| await page.$('form:has(input[type=password])') != null; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Playwright Page.url() return type synchronous
💡 Result:
In Playwright's Node.js API, Page.url is a synchronous method that returns a string representing the current URL of the page. It does not return a Promise and requires no 'await'. This is confirmed by multiple sources including Stack Overflow examples showing direct usage like const url = page.url; without await, official documentation listing it without 'await' prefix (unlike async methods like page.goto), Python docs where page.url is a property returning str (sync equivalent), and code snippets in GitHub issues demonstrating synchronous access. Some answers mistakenly use await, but authoritative usage shows it as synchronous string return.
Citations:
- 1: https://stackoverflow.com/questions/64308751/get-current-page-url-with-playwright-automation-tool
- 2: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/types/types.d.ts
- 3: [BUG] page.url() - doesn't return actual url microsoft/playwright#16072
- 4: how do i get the current url of a page? microsoft/playwright-python#1540
- 5: https://playwright.help/docs/next/api/class-page
Remove unnecessary await from synchronous page.url() call.
Page.url() is a synchronous method that returns a string, not a Promise. The await is unnecessary and misleading—it suggests the adjacent await page.$() calls might execute in parallel, when in fact they short-circuit sequentially via ||.
🔧 Proposed fix
- const requiresLogin = await page.url().includes('login') ||
- await page.$('input[name=email]') != null ||
- await page.$('form:has(input[type=password])') != null;
+ const requiresLogin =
+ page.url().includes('login') ||
+ (await page.$('input[name=email]')) != null ||
+ (await page.$('form:has(input[type=password])')) != null;📝 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 requiresLogin = await page.url().includes('login') || | |
| await page.$('input[name=email]') != null || | |
| await page.$('form:has(input[type=password])') != null; | |
| const requiresLogin = | |
| page.url().includes('login') || | |
| (await page.$('input[name=email]')) != null || | |
| (await page.$('form:has(input[type=password])')) != null; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-computrabajo.mjs` around lines 86 - 88, The expression computing
requiresLogin incorrectly uses await on the synchronous Page.url() call; remove
the unnecessary await before page.url() so it reads page.url().includes('login')
and keep the awaited page.$ calls as-is (references: requiresLogin variable,
page.url(), page.$ and the form selector 'form:has(input[type=password])') to
preserve the intended short-circuiting and correct async behavior.
| for (const field of formFields) { | ||
| const name = await field.getAttribute('name'); | ||
| const type = await field.getAttribute('type'); | ||
| const label = await field.evaluate(el => el.labels?.[0]?.innerText?.toLowerCase() || ''); | ||
|
|
||
| let value = ''; | ||
|
|
||
| // Map field name to profile data | ||
| if (/email/.test(name) || /email/.test(label)) { | ||
| value = PROFILE.candidate.email; | ||
| } else if (/nombre|full.?name|name/.test(name) || /nombre|full.?name/.test(label)) { | ||
| value = PROFILE.candidate.full_name; | ||
| } else if (/tel[eé]fono|celular|phone|mobile/.test(name) || /tel[eé]fono|celular|phone/.test(label)) { | ||
| value = PROFILE.candidate.phone; | ||
| } else if (/ciudad|location|city/.test(name) || /ciudad|ubicaci[oó]n|location/.test(label)) { | ||
| value = PROFILE.candidate.location; | ||
| } else if (/linkedin|perfil/.test(name) || /linkedin|perfil/.test(label)) { | ||
| value = PROFILE.candidate.linkedin || ''; | ||
| } else if (/portfolio|website|portafolio|sitio/.test(name) || /portfolio|website|portafolio/.test(label)) { | ||
| value = PROFILE.candidate.portfolio_url || ''; | ||
| } else if (/motivation|message|por.?qu[eé]|reason|coment/.test(name) || /motivation|message|por.?qu[eé]|reason|coment/.test(label)) { | ||
| value = `Estoy muy interesado en esta posición de Desarrollador porque alinea perfectamente con mis habilidades en tecnologías web modernas.`; | ||
| } | ||
|
|
||
| if (value) { | ||
| await field.fill(value).catch(() => {}); | ||
| filled.push(name); | ||
| console.log(` ✓ ${name} = ${value.substring(0, 30)}...`); | ||
| } | ||
| } |
There was a problem hiding this comment.
name may be null; regex match against null throws.
input[name] selectors guarantee the attribute exists, but getAttribute('name') can still return null for edge cases (attribute value "") and, more importantly, filled.push(name) plus the log on Line 142 will later surface null. Also there's no access check for PROFILE.candidate — a profile without candidate throws on Line 124. Coerce to string and guard profile access.
🔧 Proposed fix
- for (const field of formFields) {
- const name = await field.getAttribute('name');
- const type = await field.getAttribute('type');
- const label = await field.evaluate(el => el.labels?.[0]?.innerText?.toLowerCase() || '');
+ const candidate = PROFILE?.candidate || {};
+ for (const field of formFields) {
+ const name = (await field.getAttribute('name')) || '';
+ const type = (await field.getAttribute('type')) || '';
+ const label = await field.evaluate(el => el.labels?.[0]?.innerText?.toLowerCase() || '');
+ if (!name) continue;
@@
- if (/email/.test(name) || /email/.test(label)) {
- value = PROFILE.candidate.email;
+ if (/email/.test(name) || /email/.test(label)) {
+ value = candidate.email || '';Apply the same candidate.* guarding to every profile access in the chain.
Also note that field.fill(value) (Line 140) will reject for radio/checkbox/file inputs; the silent .catch(() => {}) hides real errors. Consider branching on type.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-computrabajo.mjs` around lines 115 - 144, The loop assumes
field.getAttribute('name') and PROFILE.candidate exist and that field.fill works
for all input types; coerce and guard both: getAttribute('name') may be null so
convert to const name = String(await field.getAttribute('name') || '') and only
push/log non-empty names, guard all PROFILE accesses (use
PROFILE?.candidate?.... or short-circuit to '') when mapping values, and branch
on the input type (check the variable type from field.getAttribute('type')) so
you call field.fill only for text/textarea/email/number types and handle/skip
radio/checkbox/file appropriately instead of silently swallowing errors from
field.fill; update references to name, type, PROFILE.candidate, field.fill, and
filled to implement these checks.
| function guessAnswerByLabel(label, profile, cvText) { | ||
| const q = label.toLowerCase(); | ||
| const cvName = profile?.full_name || 'Cristian Camilo Montes Teheran'; | ||
| const cvLocation = profile?.location || 'Bogotá DC, Colombia'; | ||
| const skills = 'JavaScript, TypeScript, Node.js, Angular, React, NestJS, MySQL, n8n'; | ||
| const salary = profile?.salary_aspiration_cop || '2500000'; | ||
|
|
||
| // Detectar si la pregunta está en inglés (heurística simple) | ||
| const isEn = /\b(how|what|do you|have you|years|please|describe|tell|experience with|are you|can you|your)\b/i.test(q); | ||
|
|
||
| // ── Datos de contacto ──────────────────────────────────────────────────── | ||
| if (/full.?name|nombre completo|nombres y apellidos/.test(q)) return cvName; | ||
| if (/first.?name|primer.?nombre/.test(q)) return cvName.split(' ')[0] || ''; | ||
| if (/last.?name|apellido/.test(q)) return cvName.split(' ').slice(1).join(' '); | ||
| if (/\bname\b|\bnombre\b/.test(q)) return cvName; | ||
| if (/email|correo/.test(q)) return profile?.email || ''; | ||
| if (/phone|tel[eé]fono|celular|mobile|whatsapp|n[uú]mero de contacto/.test(q)) return profile?.phone || ''; | ||
| if (/location|ciudad|city|municipio|reside|d[oó]nde vive|localidad|barrio|ubicaci[oó]n/.test(q)) return cvLocation; | ||
| if (/linkedin/.test(q)) return profile?.linkedin || ''; | ||
| if (/portfolio|website|sitio.?web|p[aá]gina.?web|url.?web/.test(q)) return profile?.portfolio_url || ''; | ||
| if (/github/.test(q)) return 'https://github.com/camilomont'; | ||
|
|
||
| // ── Salario ────────────────────────────────────────────────────────────── | ||
| if (/aspiraci[oó]n salarial|salario esperado|pretens|expectativa salarial|salary expectation|expected salary|desired salary/i.test(q)) | ||
| return isEn ? '2500000' : `Entre $2.000.000 y $3.000.000 COP mensuales.`; | ||
| if (/de acuerdo.*asignaci[oó]n|acuerdo.*salario|salario.*conforme|conforme.*salario|salary.*agree|agree.*salary/i.test(q)) | ||
| return isEn ? 'Yes, I agree with the offered salary.' : 'Sí, estoy de acuerdo con la asignación salarial ofrecida.'; | ||
| if (/[uú]ltimo salario|salario mensual|salario actual|current salary/i.test(q)) | ||
| return salary; // número puro — campo suele ser numérico | ||
| // Campo numérico puro de salario (LinkedIn a veces es solo un número) | ||
| if (/salario|sueldo|salary|compensation|remuner/i.test(q)) return salary; | ||
|
|
||
| // ── Disponibilidad / acuerdo ───────────────────────────────────────────── | ||
| if (/disponibilidad|horario|turno|availability|start date/i.test(q)) | ||
| return isEn ? 'Immediate availability, full-time.' : 'Disponibilidad inmediata para jornada completa.'; | ||
| if (/de acuerdo|acuerdo con (la |el |esta |este )|aceptas (la |el )|acepta (la |el )/i.test(q)) | ||
| return 'Sí, estoy de acuerdo con las condiciones de la oferta.'; | ||
| if (/condiciones solicitadas|condiciones del cargo|requisitos solicitados|cuentas con las condiciones/i.test(q)) | ||
| return 'Sí, cuento con las condiciones solicitadas y tengo disponibilidad inmediata.'; | ||
| if (/tipo de contrato|modalidad de contrato|contract type/i.test(q)) | ||
| return 'Contrato a término fijo.'; | ||
|
|
||
| // ── Educación ──────────────────────────────────────────────────────────── | ||
| if (/nivel acad[eé]mico|estudios|formaci[oó]n|t[ií]tulo|qu[eé] estudiaste|education level|highest.*education/i.test(q)) | ||
| return 'Tecnólogo en Análisis y Desarrollo de Software — SENA 2024. Formación complementaria (Uniagustiniana) y cursos en Platzi/Udemy.'; | ||
| if (/tecnol[oó]go|t[eé]cnico|estudiante|semestre|ingenier/i.test(q)) | ||
| return 'Sí, soy Tecnólogo en Análisis y Desarrollo de Software del SENA (2024) con formación adicional en Ingeniería de Sistemas.'; | ||
| if (/carrera profesional|carrera.*termin|carrera.*titulad/i.test(q)) | ||
| return 'Tecnólogo en Análisis y Desarrollo de Software — SENA 2024 (titulado).'; | ||
|
|
||
| // ── Idiomas ────────────────────────────────────────────────────────────── | ||
| if (/nivel de ingl[eé]s|english.*level|level.*english|english.*proficiency|idioma/i.test(q)) | ||
| return 'B1. Lectura técnica fluida en inglés, conversación básica.'; | ||
| if (/years.*english|english.*years|a[ñn]os.*ingl[eé]s/i.test(q)) return '2'; | ||
|
|
||
| // ── Stacks incompatibles — DEBEN ir ANTES del catch-all de años ────────── | ||
| // Para preguntas de "cuántos años / how many years" con stack incompatible → 0 | ||
| // Para preguntas de texto libre sobre stack incompatible → respuesta honesta (más abajo) | ||
| const isYearsQ = /a[ñn]os.*con\b|a[ñn]os.*de\b|years.*with\b|years.*of\b|how many years/i.test(q); | ||
| if (isYearsQ) { | ||
| if (/\.net\b|blazor|c#|csharp/i.test(q)) return '0'; | ||
| if (/\bjava\b/i.test(q) && !/javascript/i.test(q)) return '0'; | ||
| if (/\bphp\b|laravel|symfony/i.test(q)) return '0'; | ||
| if (/\bswift\b|\bxcode\b|\bios\b/i.test(q)) return '0'; | ||
| if (/\bkotlin\b|android.studio/i.test(q)) return '0'; | ||
| if (/\bflutter\b|\bdart\b/i.test(q)) return '0'; | ||
| if (/power.?bi\b|tableau|looker|qlik/i.test(q)) return '0'; | ||
| if (/\bgo\b|golang|go lang|site.?reliability|\bsre\b|rust\b|scala\b|elixir\b/i.test(q)) return '0'; | ||
| } | ||
|
|
||
| // ── Experiencia general ────────────────────────────────────────────────── | ||
| if (/a[ñn]os.*experiencia|years.*experience|experience.*years|tiempo.*experiencia|how many years/i.test(q)) | ||
| return '1'; | ||
| if (/experiencia|funciones|cargo|trayectoria/i.test(q)) | ||
| return isEn | ||
| ? `Over 1 year of software development experience. At SERVIMAX I built an institutional platform (Angular, Node.js, MySQL). At INTELIBPO I implemented RPA automation flows using n8n and NestJS.` | ||
| : `Más de 1 año de experiencia en desarrollo de software. En SERVIMAX desarrollé una página institucional con autenticación, APIs y pasarela de pago (Angular, Node.js, MySQL). En INTELIBPO implementé flujos RPA con n8n y Node.js/NestJS para automatización de cobranza.`; | ||
|
|
||
| // ── Motivación / carta ──────────────────────────────────────────────────── | ||
| if (/message|cover letter|carta de presentaci[oó]n|motivaci[oó]n|por qu[eé].*aplic|why.*apply|why.*interest|tell us.*yourself/i.test(q)) | ||
| return isEn | ||
| ? `I am very interested in this opportunity. My experience with ${skills} allows me to add value from day one. I have immediate availability and strong motivation to keep growing professionally.` | ||
| : `Estoy muy interesado en esta oportunidad. Mi experiencia en ${skills} me permite aportar valor desde el primer día y contribuir al crecimiento del equipo. Tengo disponibilidad inmediata.`; | ||
|
|
||
| // ── Stack JS / TypeScript ──────────────────────────────────────────────── | ||
| if (/javascript|js\b/i.test(q) && !/java\b/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, JavaScript/TypeScript is my primary language. Over 1 year in real projects with Node.js, Angular and React.' | ||
| : 'Sí, uso JavaScript/TypeScript como lenguaje principal. Tengo +1 año en proyectos reales con Node.js, Angular y React.'; | ||
| if (/typescript/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, I use TypeScript in all my backend (NestJS) and frontend (Angular) projects. Over 1 year of experience.' | ||
| : 'Sí, uso TypeScript en todos mis proyectos backend (NestJS) y frontend (Angular). +1 año de experiencia.'; | ||
| if (/a[ñn]os.*javascript|javascript.*a[ñn]os|years.*javascript|javascript.*years/i.test(q)) return '2'; | ||
| if (/a[ñn]os.*typescript|typescript.*a[ñn]os/i.test(q)) return '1'; | ||
|
|
||
| // ── Frontend ───────────────────────────────────────────────────────────── | ||
| if (/html|css/.test(q)) | ||
| return 'Sí, tengo experiencia sólida en HTML5 y CSS3. He desarrollado interfaces responsivas con Bootstrap y Tailwind.'; | ||
| if (/angular/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, I have Angular (v14+) experience. I used it at SERVIMAX to build a full institutional platform with authentication, API integration and payment gateway.' | ||
| : 'Sí, tengo experiencia en Angular (v14+). Lo usé en SERVIMAX para desarrollar una plataforma institucional completa con autenticación, consumo de APIs y pasarela de pago.'; | ||
| if (/a[ñn]os.*angular|angular.*a[ñn]os|years.*angular|angular.*years/i.test(q)) return '1'; | ||
| if (/react/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, I have React experience. I built components, hooks and REST API integrations in SERVIMAX and personal projects.' | ||
| : 'Sí, tengo experiencia en React. He desarrollado componentes, hooks y consumo de APIs REST en proyectos de SERVIMAX y personales.'; | ||
| if (/a[ñn]os.*react|react.*a[ñn]os|years.*react|react.*years/i.test(q)) return '1'; | ||
| if (/vue\.?js|vuejs/i.test(q)) | ||
| return isEn | ||
| ? 'I know Vue.js at a basic level. My main frontend stack is Angular and React.' | ||
| : 'Conozco Vue.js a nivel básico. Mi stack principal de frontend es Angular y React.'; | ||
|
|
||
| // ── Backend ────────────────────────────────────────────────────────────── | ||
| if (/node\.?js|nodejs/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, Node.js is my main backend runtime. I built REST APIs with Express and NestJS at SERVIMAX and INTELIBPO.' | ||
| : 'Sí, uso Node.js como runtime principal en el backend. He desarrollado APIs REST con Express y NestJS en SERVIMAX e INTELIBPO.'; | ||
| if (/a[ñn]os.*node|node.*a[ñn]os|years.*node|node.*years/i.test(q)) return '1'; | ||
| if (/nest\.?js/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, I use NestJS. I built auth modules and APIs at SERVIMAX, and RPA automation services at INTELIBPO using NestJS + TypeScript + MySQL.' | ||
| : 'Sí, uso NestJS. Construí módulos de autenticación y APIs en SERVIMAX, y servicios de automatización RPA en INTELIBPO con NestJS + TypeScript + MySQL.'; | ||
| if (/express\.?js|expressjs/i.test(q)) | ||
| return isEn | ||
| ? 'Yes, I have Express.js experience building REST APIs in production projects.' | ||
| : 'Sí, tengo experiencia con Express.js desarrollando APIs REST en proyectos de producción.'; | ||
| if (/crud/i.test(q)) | ||
| return 'Con NestJS y TypeORM: defino entity, creo el DTO, genero módulo/servicio/controller con CLI, implemento los 5 endpoints REST y agrego validaciones con class-validator. Tiempo: ~30-45 min.'; | ||
|
|
||
| // ── Bases de datos ──────────────────────────────────────────────────────── | ||
| if (/mysql/i.test(q)) | ||
| return 'Sí, tengo experiencia con MySQL. Lo he usado en SERVIMAX e INTELIBPO para diseñar esquemas, relaciones y consultas SQL complejas.'; | ||
| if (/mongodb|mongo/i.test(q)) | ||
| return 'Tengo conocimientos de MongoDB. Lo he usado en proyectos personales y como alternativa NoSQL para almacenamiento de documentos.'; | ||
| if (/postgresql|postgres/i.test(q)) | ||
| return 'Conozco PostgreSQL. Mi experiencia principal en bases de datos relacionales es con MySQL, pero manejo SQL de forma genérica.'; | ||
| if (/sql\b|base[s]? de datos|database/i.test(q)) | ||
| return 'Sí, tengo experiencia con SQL (MySQL). He diseñado esquemas, relaciones y consultas en proyectos de producción.'; | ||
| if (/a[ñn]os.*sql|sql.*a[ñn]os|years.*sql|sql.*years/i.test(q)) return '1'; | ||
|
|
||
| // ── Automatización / RPA ────────────────────────────────────────────────── | ||
| if (/n8n|automatizaci[oó]n|automatizad[oa]|rpa|bot\b/i.test(q)) | ||
| return 'Sí, tengo experiencia en n8n y automatización RPA. En INTELIBPO implementé flujos para recepción de archivos SFTP/correo/nube, extracción y transformación de datos.'; | ||
| if (/a[ñn]os.*rpa|rpa.*a[ñn]os|years.*rpa|rpa.*years/i.test(q)) return '1'; | ||
|
|
||
| // ── DevOps / herramientas ───────────────────────────────────────────────── | ||
| if (/docker|kubernetes|contenedor|container/i.test(q)) | ||
| return 'Tengo conocimientos básicos de Docker para entornos de desarrollo local (contenedores de bases de datos y servicios). Estoy profundizando.'; | ||
| if (/git|github|control de versiones|repositorio|versionamiento/i.test(q)) | ||
| return 'Sí, uso Git y GitHub en todos mis proyectos. Manejo branches, commits semánticos, pull requests, merge y resolución de conflictos. Repositorio: github.com/camilomont'; | ||
| if (/postman|api.*test|test.*api/i.test(q)) | ||
| return 'Sí, uso Postman regularmente para probar y documentar APIs REST.'; | ||
|
|
||
| // ── IA / herramientas modernas ──────────────────────────────────────────── | ||
| if (/herramientas.*ia|inteligencia artificial|copilot|chatgpt|llm|ia.*desarrollad/i.test(q)) | ||
| return 'Uso GitHub Copilot para autocompletado y revisión de código, y ChatGPT para depurar errores, generar boilerplate y documentar funciones.'; | ||
|
|
||
| // ── Stacks que NO manejo (respuestas honestas con disposición) ──────────── | ||
| if (/\.net\b|blazor|c#|csharp/i.test(q)) | ||
| return isEn | ||
| ? 'No direct experience with .NET/C#. My stack is JavaScript/TypeScript. Willing to learn.' | ||
| : 'No tengo experiencia directa con .NET/C#. Mi stack es JavaScript/TypeScript. Tengo disposición para aprender.'; | ||
| if (/java\b/i.test(q) && !/javascript/i.test(q)) | ||
| return isEn | ||
| ? 'No experience with Java. I work with TypeScript OOP and am willing to learn.' | ||
| : 'No tengo experiencia en Java. Manejo POO con TypeScript y estoy dispuesto a aprender.'; | ||
| if (/php|laravel|symfony/i.test(q)) | ||
| return isEn | ||
| ? 'No experience with PHP/Laravel. My main stack is JavaScript/TypeScript. Willing to learn.' | ||
| : 'No tengo experiencia en PHP/Laravel. Mi stack principal es JavaScript/TypeScript. Puedo aprender.'; | ||
| if (/swift|xcode|ios\b/i.test(q)) | ||
| return isEn | ||
| ? 'No experience with iOS/Swift. My focus is web development (Angular/React/Node.js).' | ||
| : 'No tengo experiencia en iOS/Swift. Mi enfoque es desarrollo web (Angular/React/Node.js).'; | ||
| if (/kotlin|android.studio/i.test(q)) | ||
| return isEn | ||
| ? 'No experience with native Android/Kotlin. My stack is web.' | ||
| : 'No tengo experiencia en Android nativo con Kotlin. Mi stack es web.'; | ||
| if (/flutter|dart/i.test(q)) | ||
| return isEn | ||
| ? 'No experience with Flutter/Dart, but strong foundations in JavaScript/TypeScript.' | ||
| : 'No tengo experiencia en Flutter/Dart, pero tengo bases sólidas en JavaScript/TypeScript.'; | ||
| if (/power.?bi|tableau|looker|qlik|inteligencia.*negocios/i.test(q)) | ||
| return isEn | ||
| ? 'No experience with BI tools. My stack is web development and SQL queries. Willing to learn.' | ||
| : 'No tengo experiencia con herramientas de BI/visualización. Mi stack es desarrollo web y SQL para consultas. Disposición para aprender.'; | ||
|
|
||
| // ── Escalas numéricas ───────────────────────────────────────────────────── | ||
| if (/escala.*[1-5]|califica.*nivel|puntúa|[0-9].*siendo.*[0-9]|de [0-9]+ a [0-9]+|[0-9]+ al [0-9]+|cuanto manejas|cuánto manejas/i.test(q)) | ||
| return '3'; | ||
|
|
||
| // ── Sin match — devuelve vacío, fallback a IA ───────────────────────────── | ||
| return ''; | ||
| } |
There was a problem hiding this comment.
Hardcoded user-specific data violates repo guideline for auto-updatable .mjs files.
This script — and the defaults in main() (Line 1237-1251), the GitHub URL on Line 683/Line 814, the phone 3143663821 on Line 652, the "Cristian Camilo Montes Teheran" / "Bogotá DC, Colombia" defaults on Line 665-666 and Line 894-897/Line 989, and the SERVIMAX/INTELIBPO/SENA biographical strings on Line 707-711, Line 737-739, Line 744-745, Line 785-786 — bakes a specific candidate's personal data into an auto-updatable system script.
Everything that varies per user must come from config/profile.yml, cv.md, or the env; guessAnswerByLabel should return generic templates or read from profile/cvText, never embed a name, phone, email, or employer history. Otherwise this file can't be safely shared, auto-updated, or used by anyone else without leaking PII and requiring manual edits.
🔧 Sketch of the fix (apply pattern throughout)
- const cvName = profile?.full_name || 'Cristian Camilo Montes Teheran';
- const cvLocation = profile?.location || 'Bogotá DC, Colombia';
- const skills = 'JavaScript, TypeScript, Node.js, Angular, React, NestJS, MySQL, n8n';
- const salary = profile?.salary_aspiration_cop || '2500000';
+ const cvName = profile?.full_name || '';
+ const cvLocation = profile?.location || '';
+ const skills = profile?.skills || '';
+ const salary = profile?.salary_aspiration_cop || '';
@@
- if (/github/.test(q)) return 'https://github.com/camilomont';
+ if (/github/.test(q)) return profile?.github || '';Apply equivalently to every hardcoded "SERVIMAX…", "INTELIBPO…", "SENA 2024", phone, and email literal — source them from profile / cvText or drop them.
As per coding guidelines: "Script files like update-system.mjs, verify-pipeline.mjs, generate-pdf.mjs, merge-tracker.mjs, and gemini-eval.mjs are auto-updatable system files — NEVER put user data in these files" and "Keep system-layer files … auto-updatable and free of user-specific content."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apply-linkedin.mjs` around lines 663 - 858, The function guessAnswerByLabel
contains hardcoded PII and employer/education strings (e.g., "Cristian Camilo
Montes Teheran", "Bogotá DC, Colombia", phone 3143663821,
"https://github.com/camilomont", SERVIMAX, INTELIBPO, SENA 2024 and similar
biographical literals); remove all hardcoded personal data and instead read
defaults from the provided profile object, cvText, env or config/profile.yml
(use profile.full_name, profile.location, profile.phone, profile.email,
profile.linkedin, profile.portfolio_url, profile.salary_aspiration_cop, and
extract employer/education snippets from profile or cvText), and change
employer/education-specific response texts to generic templates that interpolate
those profile/cv-derived values; apply the same replacement pattern throughout
guessAnswerByLabel (and other functions referenced in the review such as main())
so no PII remains in the .mjs file.
| // Application tracking | ||
| if (existsSync('data/applications.md')) { | ||
| const content = readFileSync('data/applications.md', 'utf8'); | ||
| const entries = content.split('\n').filter(line => line.startsWith('|')).length - 2; // -2 for header and divider | ||
| log(`Total applications: ${entries}`); | ||
| } | ||
|
|
There was a problem hiding this comment.
Row count can go negative if no markdown table is present.
entries = content.split('\n').filter(line => line.startsWith('|')).length - 2 produces -2 or -1 when data/applications.md has no pipe-rows (empty or non-table content), which surfaces as Total applications: -2. Clamp to zero and consider filtering out the separator row by excluding lines made only of | and -.
🔧 Proposed fix
- const entries = content.split('\n').filter(line => line.startsWith('|')).length - 2; // -2 for header and divider
- log(`Total applications: ${entries}`);
+ const rows = content.split('\n').filter(line => line.startsWith('|') && !/^\|\s*-+/.test(line));
+ const entries = Math.max(0, rows.length - 1); // -1 for header row
+ log(`Total applications: ${entries}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@auto-apply.mjs` around lines 82 - 88, The current entries calculation can be
negative when no table rows exist; update the logic where entries is computed in
auto-apply.mjs (the readFileSync/content -> entries calculation) to first filter
lines that start with '|' but exclude separator rows (those composed only of
pipes, dashes, colons and whitespace), then compute the number of data rows by
subtracting the header line only (not both) and finally clamp the result to a
minimum of 0 before logging; target the variable named entries and the
surrounding block that reads 'data/applications.md'.
| if (args.includes('--dry')) { | ||
| log('DRY RUN MODE - Not actually submitting', 'ℹ'); | ||
| } | ||
|
|
||
| spawnSync('node', ['apply-auto.mjs'], { stdio: 'inherit' }); | ||
| log('✅ Apply cycle complete'); |
There was a problem hiding this comment.
--dry flag is silently ignored.
The branch at Line 245 only prints a message but doesn't pass --dry to the spawned apply-auto.mjs, so apply --dry still submits real applications. This directly contradicts the help text ("Simulate without submitting") and is dangerous for users testing the pipeline.
🔧 Proposed fix
- if (args.includes('--dry')) {
- log('DRY RUN MODE - Not actually submitting', 'ℹ');
- }
-
- spawnSync('node', ['apply-auto.mjs'], { stdio: 'inherit' });
+ const dry = args.includes('--dry');
+ if (dry) {
+ log('DRY RUN MODE - Not actually submitting', 'ℹ');
+ }
+
+ const applyArgs = dry ? ['apply-auto.mjs', '--dry'] : ['apply-auto.mjs'];
+ spawnSync('node', applyArgs, { stdio: 'inherit' });You'll also need to make sure apply-auto.mjs honors the --dry flag.
📝 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.
| if (args.includes('--dry')) { | |
| log('DRY RUN MODE - Not actually submitting', 'ℹ'); | |
| } | |
| spawnSync('node', ['apply-auto.mjs'], { stdio: 'inherit' }); | |
| log('✅ Apply cycle complete'); | |
| const dry = args.includes('--dry'); | |
| if (dry) { | |
| log('DRY RUN MODE - Not actually submitting', 'ℹ'); | |
| } | |
| const applyArgs = dry ? ['apply-auto.mjs', '--dry'] : ['apply-auto.mjs']; | |
| spawnSync('node', applyArgs, { stdio: 'inherit' }); | |
| log('✅ Apply cycle complete'); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@auto-apply.mjs` around lines 245 - 250, The --dry branch currently only logs
but does not propagate the flag to the child process, so spawnSync('node',
['apply-auto.mjs'], ...) should include '--dry' when args contains '--dry'
(modify the spawnSync call to build the argument array from args or append
'--dry' conditionally); also update apply-auto.mjs to read/process that --dry
flag (e.g., via its args parsing) so the child process actually simulates
instead of submitting. Use the existing symbols args and spawnSync and ensure
apply-auto.mjs honors the flag.
| function loop() { | ||
| section('STARTING AUTO PIPELINE LOOP'); | ||
|
|
||
| const intervalArg = args[1] ? parseInt(args[1]) : 5; | ||
| const interval = isNaN(intervalArg) ? 5 : intervalArg; | ||
|
|
||
| log(`Running scan + apply every ${interval} minutes`); | ||
| log('Press Ctrl+C to stop', 'ℹ'); | ||
|
|
||
| spawnSync('node', ['apply-pipeline.mjs', '--interval', interval.toString()], { stdio: 'inherit' }); | ||
| } |
There was a problem hiding this comment.
loop interval parsing is inconsistent with help text.
Help at Line 122-123 and Line 158 documents loop 10 (positional) but users who try the more conventional loop --interval 10 (as shown in the file header at Line 16) will silently fall back to the default of 5 minutes because args[1] is '--interval', parsed as NaN. Pick one convention and parse accordingly, or accept both.
🔧 Proposed fix
- const intervalArg = args[1] ? parseInt(args[1]) : 5;
- const interval = isNaN(intervalArg) ? 5 : intervalArg;
+ // Accept either `loop 10` or `loop --interval 10`
+ const flagIdx = args.indexOf('--interval');
+ const raw = flagIdx !== -1 ? args[flagIdx + 1] : args[1];
+ const parsed = parseInt(raw, 10);
+ const interval = Number.isFinite(parsed) && parsed > 0 ? parsed : 5;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@auto-apply.mjs` around lines 253 - 263, The loop function currently reads
interval from args[1] which breaks when callers use the flag form (e.g., "loop
--interval 10") because args[1] is "--interval" and parseInt returns NaN; update
loop to accept both styles by checking for a flag "--interval" in args (e.g.,
find indexOf('--interval') and use the next arg) and fall back to the positional
args[1] if no flag is present, then parse to int with the same NaN -> default 5
behavior; modify the interval parsing logic inside function loop to handle both
the flag form and the positional form before calling spawnSync('node',
['apply-pipeline.mjs', '--interval', interval.toString()], ...).
| appendFileSync(reportPath, reportLines.join('\n') + '\n', 'utf8'); | ||
| log(`Reporte guardado en ${reportPath}`, '✓'); | ||
| } | ||
|
|
||
| function setup() { | ||
| section('INITIAL SETUP'); | ||
|
|
||
| log('Checking required files...'); | ||
|
|
||
| let missing = false; | ||
|
|
||
| // CV | ||
| if (!existsSync('cv.md')) { | ||
| log('cv.md not found', '✗'); | ||
| log('Create cv.md with your CV in markdown format', 'ℹ'); | ||
| missing = true; | ||
| } else { | ||
| log('cv.md found', '✓'); | ||
| } | ||
|
|
||
| // Profile | ||
| if (!existsSync('config/profile.yml')) { | ||
| log('config/profile.yml not found', '✗'); | ||
| if (existsSync('config/profile.example.yml')) { | ||
| log('Copying config/profile.example.yml → config/profile.yml', 'ℹ'); | ||
| const example = readFileSync('config/profile.example.yml', 'utf8'); | ||
| appendFileSync('config/profile.yml', example); | ||
| log('Profile created - edit it with your info', 'ℹ'); | ||
| } | ||
| missing = true; | ||
| } else { | ||
| log('config/profile.yml found', '✓'); | ||
| } | ||
|
|
||
| // Credentials | ||
| if (!existsSync('config/credentials.yml')) { | ||
| log('config/credentials.yml not found', '✗'); | ||
| if (existsSync('config/credentials.example.yml')) { | ||
| log('Copying config/credentials.example.yml → config/credentials.yml', 'ℹ'); | ||
| const example = readFileSync('config/credentials.example.yml', 'utf8'); | ||
| appendFileSync('config/credentials.yml', example); | ||
| log('Credentials initialized - add your login info', 'ℹ'); | ||
| } | ||
| missing = true; | ||
| } else { | ||
| log('config/credentials.yml found', '✓'); | ||
| } | ||
|
|
||
| // Pipeline | ||
| if (!existsSync('data/pipeline.md')) { | ||
| log('data/pipeline.md not found', '✗'); | ||
| appendFileSync('data/pipeline.md', `# Job Pipeline | ||
|
|
||
| ## Pendientes | ||
| (Add URLs here in format: - [ ] URL | Company | Role | Location) | ||
|
|
||
| ## Rechazadas | ||
|
|
||
| ## Aplicadas | ||
| `, 'utf8'); | ||
| log('Pipeline initialized', '✓'); | ||
| } else { | ||
| log('data/pipeline.md found', '✓'); | ||
| } |
There was a problem hiding this comment.
setup/sequence assume data/ and config/ directories exist, and misuse appendFileSync for initialization.
Two related issues:
appendFileSync('data/pipeline.md', ...)(Line 392),appendFileSync(reportPath, ...)(Line 341), and theconfig/profile.yml/config/credentials.ymlappends (Line 367, Line 381) will throwENOENTif the parent directory doesn't exist. The coding guidelines require scripts to handle missingdata/directories gracefully.- Using
appendFileSyncto materialize a file from an example template is semantically wrong — if the file already exists with partial content (e.g., user aborted mid-setup), it will concatenate rather than fail loudly. UsewriteFileSyncfor initial creation.
🔧 Proposed fix
-import { readFileSync, existsSync, appendFileSync } from 'fs';
+import { readFileSync, existsSync, appendFileSync, writeFileSync, mkdirSync } from 'fs';
+import { dirname } from 'path';
@@
- const example = readFileSync('config/profile.example.yml', 'utf8');
- appendFileSync('config/profile.yml', example);
+ mkdirSync(dirname('config/profile.yml'), { recursive: true });
+ writeFileSync('config/profile.yml', readFileSync('config/profile.example.yml', 'utf8'));
@@
- const example = readFileSync('config/credentials.example.yml', 'utf8');
- appendFileSync('config/credentials.yml', example);
+ mkdirSync(dirname('config/credentials.yml'), { recursive: true });
+ writeFileSync('config/credentials.yml', readFileSync('config/credentials.example.yml', 'utf8'));
@@
- appendFileSync('data/pipeline.md', `# Job Pipeline
+ mkdirSync('data', { recursive: true });
+ writeFileSync('data/pipeline.md', `# Job PipelineAnd for sequence():
+ mkdirSync('data', { recursive: true });
appendFileSync(reportPath, reportLines.join('\n') + '\n', 'utf8');As per coding guidelines: "Ensure scripts handle missing data/ directories gracefully."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@auto-apply.mjs` around lines 341 - 404, The setup (and sequence) logic
assumes config/ and data/ exist and misuse appendFileSync for bootstrapping
files; ensure parent directories exist (create config/ and data/ with
mkdirSync(..., { recursive: true }) before any file operation) and replace
appendFileSync used to create new template files (config/profile.yml,
config/credentials.yml, data/pipeline.md) with writeFileSync so you don't
accidentally concatenate into partial files; for reportPath and any other writes
also ensure the containing directory exists before calling
appendFileSync/writeFileSync and only use appendFileSync when you truly intend
to append (leave existing behavior for report if appropriate).
… URL found Discovered 1 genuinely new listing (not in 141 prior scans): - TikTok Security Software Engineer Project Intern (Product Security) lifeattiktok.com/search/7600536394771564853 | $45-60/hr | San Jose CA LLM/MCP server security + pentesting | adversarial LLM testing match Score: 4.0/5 | Report santifer#392 filed Also scanned: HN April 2026 retry, ElevenLabs (MBA-only skip), Sierra (Fall 2026 skip), Vapi (no intern), BCG (Summer 2027 cycle), SpeedyApply SWE/SimplifyJobs (all dup v7-v141), Greenhouse broad security (all dup). Urgent: Anthropic Fellows deadline APRIL 26 (5 days) — apply immediately. career-ops update available: v1.1.0 → v1.3.0 https://claude.ai/code/session_01HjJnWCr2CdZ7HF9buDgJsf
…ne entries Reports created for evaluations previously tracked in applications.md but missing report files: santifer#388 ByteDance Security Assurance (4.3/5), santifer#389 Verkada Security SWE New Grad (4.1/5), santifer#390 Rockstar Security Automation (4.0/5 — CLOSED deadline Apr 5), santifer#391 Netflix SWE Intern (4.2/5), santifer#392 TikTok Product Security Intern (4.0/5). v144 scan: 0 new qualifying ≥4.0. Found 2 genuinely new URLs: TikTok PGC-LLM Applications and AI Agents (~3.8/5) and DoorDash SWE Intern (~3.0/5) — both added to pipeline below threshold. Crusoe Product Security Applied AI Intern flagged as possible re-open (appears active on Simplify/ LinkedIn/Climate Draft but Ashby 503 — needs manual browser verify). Restored modes/_profile.md from template (was missing). career-ops update v1.1.0→v1.3.0 available (user to apply when ready). https://claude.ai/code/session_01VqxRjn4z1kgcFEsVwX1UAD
|
Thanks for contributing, @camilomont! Had to close this one — the PR includes personal data ( career-ops uses a Data Contract: Some of your scripts (Bogotá scanner, auto-apply flow) sound interesting as separate contributions — if you open them as focused PRs with placeholder data and a linked issue, happy to take a look. |
What does this PR do?
Related issue
Type of change
Checklist
node test-all.mjsand all tests passQuestions? Join the Discord for faster feedback.
Summary by CodeRabbit
New Features
Documentation
Configuration
Dependencies
dotenvfor environment variable management.