lazy-apply 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/runner.js +86 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
6
6
|
import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
|
-
var VERSION = true ? "0.1.
|
|
9
|
+
var VERSION = true ? "0.1.3" : "dev";
|
|
10
10
|
var DEFAULT_APP = "https://lazyapply.online";
|
|
11
11
|
var HOME = join(homedir(), ".lazy-apply");
|
|
12
12
|
var CONFIG = join(HOME, "config.json");
|
package/dist/runner.js
CHANGED
|
@@ -144,7 +144,33 @@ var pick = (opts, ...prefer) => {
|
|
|
144
144
|
};
|
|
145
145
|
var or = (v, fallback) => v && v.trim() || fallback;
|
|
146
146
|
var links = (s) => [s.website, s.linkedin, s.github].filter(Boolean).join(" | ");
|
|
147
|
+
var yesNo = (o, yes) => o?.length ? pick(o, yes ? /^yes\b/i : /^no\b/i) : yes ? "Yes" : "No";
|
|
148
|
+
var countriesIn = (text) => {
|
|
149
|
+
const out = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const part of text.split(/,|\bor\b|\band\b|\//i)) {
|
|
151
|
+
const c = countryOf(part) ?? (/\bpoland\b/i.test(part) ? "Poland" : void 0);
|
|
152
|
+
if (c) out.add(c);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
147
156
|
var RULES = [
|
|
157
|
+
// Screening questions that sound like other rules but are not: answered before the broad rules below see them.
|
|
158
|
+
{ test: /employment agreements?|post[- ]employment restrictions?|non[- ]?compete|non[- ]?solicit|restrictive covenants?/i, answer: (_s, o) => yesNo(o, false) },
|
|
159
|
+
{ test: /(previously|ever|before) (worked|been employed|consulted|interned)( at| for| with)|former (employee|contractor) (of|at)/i, answer: (_s, o) => yesNo(o, false) },
|
|
160
|
+
// "...sponsorship to remain in your current location": about where the person lives, not where the job is.
|
|
161
|
+
{ test: /sponsor[^?]*(current(ly)? (location|country)|where you (currently )?(live|reside|are based))/i, answer: (s, o) => {
|
|
162
|
+
const home = countryOf(s.location);
|
|
163
|
+
if (!home) return void 0;
|
|
164
|
+
const ok = s.workAuthorizedCountries.split(",").some((c) => c.trim().toLowerCase() === home.toLowerCase());
|
|
165
|
+
return yesNo(o, !ok);
|
|
166
|
+
} },
|
|
167
|
+
// "Are you currently located in either Canada, UK or Poland?"
|
|
168
|
+
{ test: /(currently |presently )?(locat(ed|ion)|living|based|residing|reside) in (either )?[A-Z][^?]*\b(or|and)\b/i, answer: (s, o, _c, label) => {
|
|
169
|
+
const home = countryOf(s.location);
|
|
170
|
+
if (!home || !label) return void 0;
|
|
171
|
+
const listed = countriesIn(label.replace(/^.*?\b(?:in|either)\b/i, ""));
|
|
172
|
+
return listed.size ? yesNo(o, listed.has(home)) : void 0;
|
|
173
|
+
} },
|
|
148
174
|
{ test: /^first\s*name/i, answer: (s) => or(s.firstName) },
|
|
149
175
|
{ test: /^last\s*name|surname|family name/i, answer: (s) => or(s.lastName) },
|
|
150
176
|
{ test: /^(full |legal )?name$/i, answer: (s) => or(`${s.firstName} ${s.lastName}`.trim()) },
|
|
@@ -254,6 +280,25 @@ function resumeFileName(...nameParts) {
|
|
|
254
280
|
// src/lib/apply/discover.ts
|
|
255
281
|
function discoverFields() {
|
|
256
282
|
const clean = (s) => (s || "").replace(/\s+/g, " ").replace(/[✱*]/g, "").trim();
|
|
283
|
+
const NOISE = "input, select, textarea, button, svg, script, style, [class*=dropdown], [role=listbox], [role=option], [aria-hidden=true]";
|
|
284
|
+
const textOf = (el) => {
|
|
285
|
+
if (!el) return "";
|
|
286
|
+
let out2 = "";
|
|
287
|
+
const walk = (n) => {
|
|
288
|
+
for (const c of Array.from(n.childNodes)) {
|
|
289
|
+
if (c.nodeType === 3) out2 += c.textContent;
|
|
290
|
+
else if (c.nodeType === 1) {
|
|
291
|
+
const e = c;
|
|
292
|
+
if (e.matches(NOISE)) continue;
|
|
293
|
+
const cs = getComputedStyle(e);
|
|
294
|
+
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
|
295
|
+
walk(e);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
walk(el);
|
|
300
|
+
return out2;
|
|
301
|
+
};
|
|
257
302
|
const optionLabel = (el) => clean(el.id && document.querySelector(`label[for="${el.id}"]`)?.textContent || el.closest("label")?.textContent);
|
|
258
303
|
const out = [];
|
|
259
304
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -269,7 +314,7 @@ function discoverFields() {
|
|
|
269
314
|
let gi = 0;
|
|
270
315
|
for (const [box, buttons] of buttonGroups) {
|
|
271
316
|
if (buttons.length < 2 || buttons.length > 8) continue;
|
|
272
|
-
const label = clean(box.querySelector("label, legend, [class*=question-title], [class*=title], [class*=label]")
|
|
317
|
+
const label = clean(textOf(box.querySelector("label, legend, [class*=question-title], [class*=title], [class*=label]")));
|
|
273
318
|
if (!label) continue;
|
|
274
319
|
const key = `aa-btn-${gi++}`;
|
|
275
320
|
box.setAttribute("data-aa-group", key);
|
|
@@ -289,7 +334,7 @@ function discoverFields() {
|
|
|
289
334
|
const controls = Array.from(node.querySelectorAll("input, textarea, select")).filter((i) => !["hidden", "submit", "button"].includes(i.type));
|
|
290
335
|
if (!controls.every((i) => (i.type === "radio" || i.type === "checkbox") && i.name === input.name)) break;
|
|
291
336
|
const cand = node.querySelector("legend, h3, h4, h5, h6, [class*=question], [class*=Label], [class*=label]:not(label), .application-label");
|
|
292
|
-
const t = clean(cand
|
|
337
|
+
const t = clean(textOf(cand));
|
|
293
338
|
if (t && t !== opt) question = t;
|
|
294
339
|
node = node.parentElement;
|
|
295
340
|
}
|
|
@@ -310,16 +355,16 @@ function discoverFields() {
|
|
|
310
355
|
if (seen.has(selector)) return;
|
|
311
356
|
seen.add(selector);
|
|
312
357
|
let label = "";
|
|
313
|
-
if (el.id) label = document.querySelector(`label[for="${el.id}"]`)
|
|
314
|
-
if (!label) label = el.closest("label")
|
|
358
|
+
if (el.id) label = textOf(document.querySelector(`label[for="${el.id}"]`));
|
|
359
|
+
if (!label.trim()) label = textOf(el.closest("label"));
|
|
315
360
|
if (!label) {
|
|
316
361
|
let node = el.parentElement;
|
|
317
362
|
while (node && node !== document.body) {
|
|
318
363
|
const controls = node.querySelectorAll("input:not([type=hidden]):not([type=submit]):not([type=button]), textarea, select").length;
|
|
319
364
|
if (controls > 1) break;
|
|
320
365
|
const lab = node.querySelector(".application-label, label, legend, [class*=question-title], [class*=label]:not(input)");
|
|
321
|
-
if (lab
|
|
322
|
-
label = lab
|
|
366
|
+
if (textOf(lab).trim()) {
|
|
367
|
+
label = textOf(lab);
|
|
323
368
|
break;
|
|
324
369
|
}
|
|
325
370
|
node = node.parentElement;
|
|
@@ -349,24 +394,45 @@ async function discoverLiveFields(page) {
|
|
|
349
394
|
}
|
|
350
395
|
function findEmptyRequired() {
|
|
351
396
|
const clean = (s) => (s || "").replace(/\s+/g, " ").replace(/[✱*]/g, "").trim();
|
|
397
|
+
const NOISE = "input, select, textarea, button, svg, script, style, [class*=dropdown], [role=listbox], [role=option], [aria-hidden=true]";
|
|
398
|
+
const textOf = (el) => {
|
|
399
|
+
if (!el) return "";
|
|
400
|
+
let out2 = "";
|
|
401
|
+
const walk = (n) => {
|
|
402
|
+
for (const c of Array.from(n.childNodes)) {
|
|
403
|
+
if (c.nodeType === 3) out2 += c.textContent;
|
|
404
|
+
else if (c.nodeType === 1) {
|
|
405
|
+
const e = c;
|
|
406
|
+
if (e.matches(NOISE)) continue;
|
|
407
|
+
const cs = getComputedStyle(e);
|
|
408
|
+
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
|
409
|
+
walk(e);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
walk(el);
|
|
414
|
+
return out2;
|
|
415
|
+
};
|
|
352
416
|
const seen = /* @__PURE__ */ new Set();
|
|
353
417
|
const out = [];
|
|
354
418
|
const labels = Array.from(document.querySelectorAll("label, legend, [class*=question-title]"));
|
|
355
419
|
for (const lab of labels) {
|
|
356
|
-
const raw = lab
|
|
420
|
+
const raw = textOf(lab);
|
|
357
421
|
const ctl = lab.control;
|
|
358
422
|
const box = lab.closest("[data-field-entry-id], fieldset, [class*=field-entry], [class*=question], [class*=field], li") || lab.parentElement;
|
|
359
423
|
if (!box) continue;
|
|
360
424
|
const required = /\*|✱/.test(raw) || /required/i.test(lab.className) || !!(ctl && (ctl.hasAttribute("required") || ctl.getAttribute("aria-required") === "true")) || !!box.querySelector("[required], [aria-required=true]");
|
|
361
425
|
if (!required) continue;
|
|
362
|
-
const
|
|
426
|
+
const combo = !!ctl && (ctl.getAttribute("role") === "combobox" || !!ctl.closest("[class*=select__], [class*=-container]"));
|
|
427
|
+
const scope = ctl && !combo && !/radio|checkbox/.test(ctl.type || "") ? ctl.parentElement || box : box;
|
|
363
428
|
const texts = Array.from(scope.querySelectorAll("input:not([type=hidden]):not([type=radio]):not([type=checkbox]):not([type=file]):not([type=submit]), textarea, select"));
|
|
364
429
|
const files = Array.from(scope.querySelectorAll("input[type=file]"));
|
|
365
430
|
const checks = Array.from(scope.querySelectorAll("input[type=radio], input[type=checkbox]"));
|
|
366
431
|
const buttons = Array.from(scope.querySelectorAll("button, [role=radio], [role=checkbox]")).filter((b) => b.type !== "submit");
|
|
367
432
|
if (!texts.length && !files.length && !checks.length && !buttons.length) continue;
|
|
368
433
|
const scopeText = scope.textContent || "";
|
|
369
|
-
const
|
|
434
|
+
const chosen = !!Array.from(scope.querySelectorAll("[class*=single-value], [class*=multi-value], [class*=singleValue], [class*=multiValue]")).some((e) => (e.textContent || "").trim()) || !!scope.querySelector("[class*=value-container--has-value]") || Array.from(scope.querySelectorAll("input[type=hidden]")).some((h) => h.value.trim() && /question|answer|select|value/i.test(h.name || h.id || ""));
|
|
435
|
+
const filled = chosen || texts.some((t) => t.value?.trim()) || files.some((f) => (f.files?.length || 0) > 0) || files.length > 0 && /\.(pdf|docx?|rtf|txt)\b/i.test(scopeText) || checks.some((c) => c.checked) || buttons.some((b) => b.getAttribute("aria-pressed") === "true" || b.getAttribute("aria-checked") === "true" || /selected|active|checked/i.test(b.className) && !/unselected/i.test(b.className));
|
|
370
436
|
const label = clean(raw).replace(/(ATTACH RESUME|No location found|Couldn't auto-read|Analyzing resume|Loading).*$/i, "").trim();
|
|
371
437
|
if (!filled && label && !seen.has(label)) {
|
|
372
438
|
seen.add(label);
|
|
@@ -449,6 +515,14 @@ var hostBackoffUntil = /* @__PURE__ */ new Map();
|
|
|
449
515
|
var SUBMIT_GAP_MS = 9e4;
|
|
450
516
|
var SPAM_BACKOFF_MS = 6 * 6e4;
|
|
451
517
|
var jitter = (min, max) => min + Math.random() * (max - min);
|
|
518
|
+
async function typeAnswer(el, value) {
|
|
519
|
+
if (value.length <= 120) {
|
|
520
|
+
await el.pressSequentially(value, { delay: jitter(35, 80), timeout: 0 });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
await el.fill(value.slice(0, -1));
|
|
524
|
+
await el.pressSequentially(value.slice(-1), { delay: 40 });
|
|
525
|
+
}
|
|
452
526
|
var LOCK = join(tmpdir(), `auto-apply-runner-${PROFILE_DIR}-${APP_URL.replace(/[^a-z0-9]+/gi, "_")}.lock`);
|
|
453
527
|
if (existsSync(LOCK)) {
|
|
454
528
|
const pid = Number(readSync(LOCK, "utf8"));
|
|
@@ -535,7 +609,7 @@ async function fillGreenhouse(page, app, notes) {
|
|
|
535
609
|
await el.scrollIntoViewIfNeeded();
|
|
536
610
|
await el.click();
|
|
537
611
|
await el.fill("");
|
|
538
|
-
await el
|
|
612
|
+
await typeAnswer(el, value);
|
|
539
613
|
return true;
|
|
540
614
|
};
|
|
541
615
|
const norm = (t) => t.replace(/[\u2018\u2019]/g, "'").replace(/[\u201c\u201d]/g, '"').replace(/\s+/g, " ").trim().toLowerCase();
|
|
@@ -988,7 +1062,7 @@ async function discoverAndFillGeneric(page, app, notes) {
|
|
|
988
1062
|
await el.fill("");
|
|
989
1063
|
const isPlace = /location|city|where (are|do) you/i.test(f.label);
|
|
990
1064
|
const typed = isPlace ? value.split(",")[0].trim() : value;
|
|
991
|
-
await el
|
|
1065
|
+
await typeAnswer(el, typed);
|
|
992
1066
|
const picked = await pickSuggestion(page, typed, isPlace ? 8e3 : 1300);
|
|
993
1067
|
if (picked) notes.push(`${f.label}: chose "${picked}" from the suggestions`);
|
|
994
1068
|
else if (isPlace && await el.inputValue() !== typed) notes.push(`${f.label}: the field did not keep "${typed}"; check it in the window`);
|
|
@@ -1003,7 +1077,7 @@ async function discoverAndFillGeneric(page, app, notes) {
|
|
|
1003
1077
|
}
|
|
1004
1078
|
await fillAshbyEducation(page, notes);
|
|
1005
1079
|
const eduLabel = /search schools|^degree$|field of study|still (a )?student/i;
|
|
1006
|
-
const stillOpen = unanswered.filter((u) => !eduLabel.test(u.label));
|
|
1080
|
+
const stillOpen = unanswered.filter((u) => !eduLabel.test(u.label) && u.required);
|
|
1007
1081
|
if (stillOpen.length) {
|
|
1008
1082
|
notes.push(`${stillOpen.length} field(s) need your answer: ${stillOpen.map((u) => u.label).join("; ")}`);
|
|
1009
1083
|
await report(app.id, { questions: stillOpen });
|
package/package.json
CHANGED