job-application-agent 3.4.2 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +23 -0
  2. package/installer/src/cli.mjs +8 -1
  3. package/installer/src/installer.mjs +8 -0
  4. package/job-application-agent/SKILL.md +32 -2
  5. package/job-application-agent/capabilities.json +3 -1
  6. package/job-application-agent/references/ACCOUNTING.md +104 -0
  7. package/job-application-agent/references/AUTONOMY.md +2 -0
  8. package/job-application-agent/references/CLOUD_STATE.md +2 -0
  9. package/job-application-agent/references/FREE_AI.md +39 -0
  10. package/job-application-agent/references/OUTREACH.md +210 -0
  11. package/job-application-agent/references/RUNS.md +35 -0
  12. package/job-application-agent/references/SCHEMAS.md +12 -2
  13. package/job-application-agent/references/agent-box/README.md +77 -0
  14. package/job-application-agent/references/agent-box/novnc.service.example +16 -0
  15. package/job-application-agent/scripts/application-accounting.mjs +246 -0
  16. package/job-application-agent/scripts/ats/answer-inject.mjs +203 -0
  17. package/job-application-agent/scripts/ats/submit-adapters.mjs +194 -0
  18. package/job-application-agent/scripts/attention-questions.mjs +111 -0
  19. package/job-application-agent/scripts/attention-resume-submit.mjs +450 -0
  20. package/job-application-agent/scripts/attention-runner-poll.mjs +328 -0
  21. package/job-application-agent/scripts/captcha-vendor.mjs +328 -0
  22. package/job-application-agent/scripts/cloud-state-client.mjs +62 -8
  23. package/job-application-agent/scripts/job-application.mjs +243 -27
  24. package/job-application-agent/scripts/novnc-display-guard.mjs +300 -0
  25. package/job-application-agent/scripts/outreach-cli.mjs +95 -0
  26. package/job-application-agent/scripts/outreach-domain.mjs +287 -0
  27. package/job-application-agent/scripts/outreach-store.mjs +72 -0
  28. package/job-application-agent/scripts/session-binding.mjs +474 -0
  29. package/job-application-agent/scripts/version.mjs +1 -1
  30. package/job-application-agent/tests/accounting-cli.test.mjs +86 -0
  31. package/job-application-agent/tests/accounting-cloud-client.test.mjs +183 -0
  32. package/job-application-agent/tests/accounting-retry-cli.test.mjs +96 -0
  33. package/job-application-agent/tests/accounting-source-race.test.mjs +109 -0
  34. package/job-application-agent/tests/answer-inject-captcha.test.mjs +169 -0
  35. package/job-application-agent/tests/application-accounting.test.mjs +315 -0
  36. package/job-application-agent/tests/attention-resume-submit.test.mjs +119 -0
  37. package/job-application-agent/tests/attention-runner-poll.test.mjs +135 -0
  38. package/job-application-agent/tests/fixtures/outreach.mjs +22 -0
  39. package/job-application-agent/tests/job-application.test.mjs +5 -0
  40. package/job-application-agent/tests/novnc-display-guard.test.mjs +50 -0
  41. package/job-application-agent/tests/outreach-cli.test.mjs +73 -0
  42. package/job-application-agent/tests/outreach.test.mjs +178 -0
  43. package/job-application-agent/tests/privacy-audit.test.mjs +2 -0
  44. package/job-application-agent/tests/review-cadence.test.mjs +66 -0
  45. package/job-application-agent/tests/session-binding.test.mjs +102 -0
  46. package/job-application-agent/tests/skill-contract.test.mjs +25 -0
  47. package/job-application-agent/tests/workflow-state.test.mjs +30 -3
  48. package/package.json +6 -3
@@ -0,0 +1,194 @@
1
+ /**
2
+ * ATS-agnostic resume submit / confirmation helpers.
3
+ *
4
+ * Pure selector + classification data for the hosted resume→submit loop.
5
+ * Browser automation (Playwright/CDP) stays outside this module — the runner
6
+ * injects a page snapshot; these helpers decide the next action.
7
+ *
8
+ * Prefer submit after resume when the DOM is clear (golden-path submit bias).
9
+ * filled ≠ applied until a visible confirmation surface is observed.
10
+ */
11
+
12
+ /** Absolute blockers that must stay paused for a human. */
13
+ export const ABSOLUTE_BLOCKER_HINTS = Object.freeze([
14
+ { id: "captcha", patterns: [/recaptcha/i, /hcaptcha/i, /captcha/i, /cf-turnstile/i] },
15
+ { id: "authentication", patterns: [/sign in/i, /log in/i, /sso/i, /single sign-on/i, /verify your identity/i] },
16
+ { id: "mfa", patterns: [/two-factor/i, /2fa/i, /authenticator/i, /enter (the )?code/i, /one-time password/i] },
17
+ { id: "legal-attestation", patterns: [/i agree/i, /terms of use/i, /privacy policy/i, /ai policy/i, /acknowledge/i] },
18
+ { id: "judgment", patterns: [/why (do )?you (want to )?work/i, /cover letter/i, /in your own words/i] },
19
+ { id: "government-id", patterns: [/passport/i, /driver'?s license/i, /government (issued )?id/i] },
20
+ { id: "demographic", patterns: [/gender identity/i, /race\/ethnicity/i, /veteran status/i, /disability status/i] },
21
+ ]);
22
+
23
+ /**
24
+ * @typedef {{
25
+ * id: string,
26
+ * matchHosts?: RegExp[],
27
+ * submitSelectors: string[],
28
+ * confirmationSelectors: string[],
29
+ * confirmationUrlPatterns: RegExp[],
30
+ * blockerSelectors?: string[],
31
+ * }} AtsSubmitAdapter
32
+ */
33
+
34
+ /** @type {AtsSubmitAdapter} */
35
+ export const ASHBY_ADAPTER = Object.freeze({
36
+ id: "ashby",
37
+ matchHosts: [/ashbyhq\.com$/i, /jobs\.ashbyhq\.com$/i],
38
+ submitSelectors: [
39
+ 'button[type="submit"]',
40
+ 'button:has-text("Submit")',
41
+ 'button:has-text("Submit application")',
42
+ '[data-testid="submit-application"]',
43
+ 'input[type="submit"]',
44
+ ],
45
+ confirmationSelectors: [
46
+ "text=/thank you for applying/i",
47
+ "text=/application (has been )?submitted/i",
48
+ "text=/we.?ve received your application/i",
49
+ '[data-testid="application-submitted"]',
50
+ ".application-submitted",
51
+ ],
52
+ confirmationUrlPatterns: [
53
+ /\/application-submitted\b/i,
54
+ /[?&]submitted=1\b/i,
55
+ /\/confirmation\b/i,
56
+ ],
57
+ blockerSelectors: [
58
+ "iframe[src*='recaptcha']",
59
+ "iframe[src*='hcaptcha']",
60
+ ".g-recaptcha",
61
+ "[data-testid='captcha']",
62
+ ],
63
+ });
64
+
65
+ /** @type {AtsSubmitAdapter} */
66
+ export const GENERIC_ADAPTER = Object.freeze({
67
+ id: "generic",
68
+ submitSelectors: [
69
+ 'button[type="submit"]',
70
+ 'button:has-text("Submit")',
71
+ 'button:has-text("Submit application")',
72
+ 'input[type="submit"][value*="Submit" i]',
73
+ ],
74
+ confirmationSelectors: [
75
+ "text=/thank you for applying/i",
76
+ "text=/application (has been )?submitted/i",
77
+ "text=/we.?ve received your application/i",
78
+ "text=/successfully submitted/i",
79
+ ],
80
+ confirmationUrlPatterns: [
81
+ /\/thanks?\b/i,
82
+ /\/confirmation\b/i,
83
+ /\/application-submitted\b/i,
84
+ /[?&]submitted=1\b/i,
85
+ ],
86
+ blockerSelectors: [
87
+ "iframe[src*='recaptcha']",
88
+ "iframe[src*='hcaptcha']",
89
+ ".g-recaptcha",
90
+ ],
91
+ });
92
+
93
+ export const ATS_ADAPTERS = Object.freeze([ASHBY_ADAPTER, GENERIC_ADAPTER]);
94
+
95
+ /**
96
+ * @param {string} pageUrl
97
+ * @returns {AtsSubmitAdapter}
98
+ */
99
+ export function resolveAtsAdapter(pageUrl) {
100
+ let host = "";
101
+ try {
102
+ host = new URL(pageUrl).hostname;
103
+ } catch {
104
+ return GENERIC_ADAPTER;
105
+ }
106
+ for (const adapter of ATS_ADAPTERS) {
107
+ if (adapter.id === "generic") continue;
108
+ if (adapter.matchHosts?.some((re) => re.test(host))) return adapter;
109
+ }
110
+ return GENERIC_ADAPTER;
111
+ }
112
+
113
+ /**
114
+ * @param {{
115
+ * pageUrl?: string,
116
+ * pageText?: string,
117
+ * visibleTexts?: string[],
118
+ * matchedBlockerSelectors?: string[],
119
+ * }} snapshot
120
+ * @param {{ mode?: "pause" | "resume" }} [options]
121
+ * @returns {{ blocked: boolean, blockers: string[] }}
122
+ */
123
+ export function detectAbsoluteBlockers(snapshot = {}, options = {}) {
124
+ const mode = options.mode === "resume" ? "resume" : "pause";
125
+ const blockers = new Set();
126
+ const haystack = [
127
+ String(snapshot.pageText ?? ""),
128
+ ...(Array.isArray(snapshot.visibleTexts) ? snapshot.visibleTexts : []),
129
+ ].join("\n");
130
+
131
+ // On resume, prefer active challenge signals — static legal/"why us" copy often
132
+ // remains visible after the candidate already acted in the live panel.
133
+ const hints = mode === "resume"
134
+ ? ABSOLUTE_BLOCKER_HINTS.filter((hint) => ["captcha", "authentication", "mfa", "government-id"].includes(hint.id))
135
+ : ABSOLUTE_BLOCKER_HINTS;
136
+
137
+ for (const hint of hints) {
138
+ if (hint.patterns.some((re) => re.test(haystack))) blockers.add(hint.id);
139
+ }
140
+ if (Array.isArray(snapshot.matchedBlockerSelectors) && snapshot.matchedBlockerSelectors.length) {
141
+ blockers.add("captcha");
142
+ }
143
+ return { blocked: blockers.size > 0, blockers: [...blockers] };
144
+ }
145
+
146
+ /**
147
+ * @param {{
148
+ * pageUrl?: string,
149
+ * matchedConfirmationSelectors?: string[],
150
+ * pageText?: string,
151
+ * }} snapshot
152
+ * @param {AtsSubmitAdapter} [adapter]
153
+ */
154
+ export function detectConfirmation(snapshot = {}, adapter) {
155
+ const resolved = adapter ?? resolveAtsAdapter(snapshot.pageUrl ?? "");
156
+ const url = String(snapshot.pageUrl ?? "");
157
+ if (resolved.confirmationUrlPatterns.some((re) => re.test(url))) {
158
+ return { confirmed: true, reason: "url_pattern", adapterId: resolved.id };
159
+ }
160
+ if (Array.isArray(snapshot.matchedConfirmationSelectors) && snapshot.matchedConfirmationSelectors.length) {
161
+ return {
162
+ confirmed: true,
163
+ reason: "confirmation_selector",
164
+ adapterId: resolved.id,
165
+ matched: snapshot.matchedConfirmationSelectors,
166
+ };
167
+ }
168
+ const text = String(snapshot.pageText ?? "");
169
+ if (/thank you for applying|application (has been )?submitted|we.?ve received your application/i.test(text)) {
170
+ return { confirmed: true, reason: "confirmation_text", adapterId: resolved.id };
171
+ }
172
+ return { confirmed: false, reason: "not_found", adapterId: resolved.id };
173
+ }
174
+
175
+ /**
176
+ * Guidance payload for a browser tool (selectors only — no automation here).
177
+ * @param {string} pageUrl
178
+ */
179
+ export function buildSubmitProbePlan(pageUrl) {
180
+ const adapter = resolveAtsAdapter(pageUrl);
181
+ return {
182
+ adapterId: adapter.id,
183
+ submitSelectors: [...adapter.submitSelectors],
184
+ confirmationSelectors: [...adapter.confirmationSelectors],
185
+ blockerSelectors: [...(adapter.blockerSelectors ?? [])],
186
+ confirmationUrlPatterns: adapter.confirmationUrlPatterns.map((re) => re.source),
187
+ notes: [
188
+ "Focus the same filled ATS tab from session binding (never a cold jobs listing).",
189
+ "If absolute blockers remain, update attention honestly — do not submit.",
190
+ "If clear, click submit (submit bias). Wait for a visible confirmation surface.",
191
+ "Only then: cloud intent-confirm / ledger add. filled ≠ applied.",
192
+ ],
193
+ };
194
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pause-time attention question packaging + AI-policy detection (skill/runner).
3
+ * Mirrors site/lib/attention-questions.mjs for the published skill package.
4
+ */
5
+
6
+ export const AI_ASSISTANCE_DISCOURAGED_PATTERNS = Object.freeze([
7
+ /\bdo(?:n'?t| not)\s+use\s+ai\b/i,
8
+ /\bno\s+ai\s+assistance\b/i,
9
+ /\bwithout\s+ai\s+assistance\b/i,
10
+ /\bai[- ]generated\s+(?:answers?|content|responses?)\s+(?:are|is)\s+(?:not\s+)?(?:allowed|permitted|welcome)/i,
11
+ /\bplease\s+(?:write|answer)\s+in\s+your\s+own\s+(?:words|voice)\b/i,
12
+ /\b(?:write|answer)\s+in\s+your\s+own\s+(?:words|voice)\b/i,
13
+ /\bgenerative\s+ai\s+(?:is|are)\s+(?:not\s+)?(?:allowed|permitted)/i,
14
+ /\bchatgpt\b.*\b(?:not|never|don'?t)\b/i,
15
+ /\b(?:not|never|don'?t)\b.*\bchatgpt\b/i,
16
+ ]);
17
+
18
+ /**
19
+ * @param {unknown} text
20
+ * @returns {boolean}
21
+ */
22
+ export function detectAiAssistanceDiscouraged(text) {
23
+ const haystack = String(text ?? "");
24
+ if (!haystack.trim()) return false;
25
+ return AI_ASSISTANCE_DISCOURAGED_PATTERNS.some((re) => re.test(haystack));
26
+ }
27
+
28
+ /**
29
+ * @param {string} prompt
30
+ */
31
+ export function fingerprintPrompt(prompt) {
32
+ const normalized = String(prompt ?? "")
33
+ .toLowerCase()
34
+ .replace(/[^\p{L}\p{N}\s]+/gu, " ")
35
+ .replace(/\s+/g, " ")
36
+ .trim()
37
+ .slice(0, 240);
38
+ let hash = 2166136261;
39
+ for (let i = 0; i < normalized.length; i += 1) {
40
+ hash ^= normalized.charCodeAt(i);
41
+ hash = Math.imul(hash, 16777619);
42
+ }
43
+ return `q-${(hash >>> 0).toString(16).padStart(8, "0")}`;
44
+ }
45
+
46
+ const KINDS = new Set(["judgment", "why-us", "proud-project", "narrative", "other"]);
47
+
48
+ /**
49
+ * @param {unknown} raw
50
+ * @returns {{ id: string, prompt: string, kind: string, required: boolean }[]}
51
+ */
52
+ export function normalizeAttentionQuestions(raw) {
53
+ if (!Array.isArray(raw)) return [];
54
+ const out = [];
55
+ const seen = new Set();
56
+ for (const item of raw.slice(0, 8)) {
57
+ if (!item || typeof item !== "object") continue;
58
+ const prompt = String(item.prompt ?? item.text ?? item.label ?? "").trim().slice(0, 800);
59
+ if (!prompt) continue;
60
+ let id = String(item.id ?? "").trim().slice(0, 120);
61
+ if (!id) id = fingerprintPrompt(prompt);
62
+ if (seen.has(id)) continue;
63
+ seen.add(id);
64
+ const kindRaw = String(item.kind ?? "judgment").trim().toLowerCase().slice(0, 40);
65
+ out.push({
66
+ id,
67
+ prompt,
68
+ kind: KINDS.has(kindRaw) ? kindRaw : "judgment",
69
+ required: item.required !== false,
70
+ });
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * Extract narrative prompts from posting / form copy when the agent did not pass questions[].
77
+ * Best-effort packaging for judgment pauses.
78
+ * @param {string} pageText
79
+ * @param {{ limit?: number }} [options]
80
+ */
81
+ export function extractNarrativeQuestionsFromText(pageText, options = {}) {
82
+ const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(options.limit, 6)) : 4;
83
+ const text = String(pageText ?? "");
84
+ if (!text.trim()) return [];
85
+ const patterns = [
86
+ /why (?:do )?you (?:want to )?work(?: at| for| with)?[^\n?]{0,80}\?/gi,
87
+ /why (?:are you interested|this (?:role|company|team))[^\n?]{0,80}\?/gi,
88
+ /(?:tell us about|describe) (?:a )?project you(?:'re| are)? proud of[^\n?]{0,40}\?/gi,
89
+ /what (?:motivates|excites) you[^\n?]{0,60}\?/gi,
90
+ /in your own words[^\n?]{0,80}\?/gi,
91
+ ];
92
+ const found = [];
93
+ const seen = new Set();
94
+ for (const re of patterns) {
95
+ re.lastIndex = 0;
96
+ let match;
97
+ while ((match = re.exec(text)) && found.length < limit) {
98
+ const prompt = match[0].replace(/\s+/g, " ").trim();
99
+ const id = fingerprintPrompt(prompt);
100
+ if (seen.has(id)) continue;
101
+ seen.add(id);
102
+ found.push({
103
+ id,
104
+ prompt,
105
+ kind: /proud|project/i.test(prompt) ? "proud-project" : /why/i.test(prompt) ? "why-us" : "judgment",
106
+ required: true,
107
+ });
108
+ }
109
+ }
110
+ return found;
111
+ }