job-application-agent 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/job-application-agent/SKILL.md +2 -1
- package/job-application-agent/references/BROWSER_UPLOADS.md +19 -0
- package/job-application-agent/scripts/job-application.mjs +15 -2
- package/job-application-agent/tests/job-application.test.mjs +14 -0
- package/job-application-agent/tests/skill-contract.test.mjs +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ Updates are staged and validated before replacement. The immediately previous sk
|
|
|
62
62
|
| 🔎 Discover | 🎯 Qualify | 📝 Apply |
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| Finds active roles on direct career pages and major ATS platforms. | Scores seniority, skills, location, eligibility, work mode, and compensation. | Fills forms using only verified profile and résumé facts. |
|
|
65
|
-
| Resolves social and aggregator leads to direct employer pages. | Skips closed, duplicated, ineligible, and weak-fit opportunities. | Uploads one canonical résumé and drafts truthful short answers. |
|
|
65
|
+
| Resolves social and aggregator leads to direct employer pages. | Skips closed, duplicated, ineligible, and weak-fit opportunities. | Uploads one canonical résumé directly by path when the browser supports it, and drafts truthful short answers. |
|
|
66
66
|
|
|
67
67
|
| 🔐 Protect | 📚 Track | 📈 Improve |
|
|
68
68
|
|---|---|---|
|
|
@@ -52,7 +52,7 @@ Do not lower seniority, compensation, location, work mode, or evidence threshold
|
|
|
52
52
|
4. Keep authentication in the existing browser session. Never inspect cookies, local storage, passwords, or session files.
|
|
53
53
|
5. Fill only explicit profile fields, candidate-provided answers, or facts verified in the canonical resume.
|
|
54
54
|
6. Follow [references/APPLICATION_GUIDANCE.md](references/APPLICATION_GUIDANCE.md) for narrative answers.
|
|
55
|
-
7. Upload only the canonical resume unless the candidate explicitly provides another attachment.
|
|
55
|
+
7. Upload only the canonical resume unless the candidate explicitly provides another attachment. Resolve its absolute path with `resume path`, then follow [references/BROWSER_UPLOADS.md](references/BROWSER_UPLOADS.md). Use the browser's privileged path-based upload capability first; treat a visible native file picker as a fallback.
|
|
56
56
|
8. Do not answer demographic questions. Stop for login/SSO/MFA, CAPTCHA, legal attestations, unclear authorization or compensation, sensitive identifiers, and judgment-only questions.
|
|
57
57
|
9. Verify every required field, answer, attachment, and disclosure. Submit only when the current request and confirmation policy authorize it.
|
|
58
58
|
10. Record `submitted` only after visible success confirmation. Record no submission when confirmation is missing or ambiguous.
|
|
@@ -78,6 +78,7 @@ node scripts/job-application.mjs profile migrate --stdin
|
|
|
78
78
|
node scripts/job-application.mjs profile check
|
|
79
79
|
node scripts/job-application.mjs profile field <allowed-field>
|
|
80
80
|
node scripts/job-application.mjs resume import <google-doc-url-or-local-pdf>
|
|
81
|
+
node scripts/job-application.mjs resume path
|
|
81
82
|
node scripts/job-application.mjs score --stdin
|
|
82
83
|
node scripts/job-application.mjs ledger check --stdin
|
|
83
84
|
node scripts/job-application.mjs ledger add --stdin
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Browser uploads
|
|
2
|
+
|
|
3
|
+
Use this procedure whenever an application requires the canonical resume or another candidate-authorized attachment.
|
|
4
|
+
|
|
5
|
+
1. Run `node scripts/job-application.mjs resume path` and use the returned absolute path. Stop if the command reports that no canonical resume is imported.
|
|
6
|
+
2. Read the selected browser tool's upload documentation. Prefer its privileged path-based upload capability over native UI automation.
|
|
7
|
+
3. When the browser exposes a file chooser, start waiting for the chooser before clicking the actual `input[type="file"]` or its associated upload control, then set the absolute path directly. For example:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const chooserPromise = tab.playwright.waitForEvent("filechooser", { timeoutMs: 10000 });
|
|
11
|
+
await tab.playwright.locator('input[type="file"]').click();
|
|
12
|
+
const chooser = await chooserPromise;
|
|
13
|
+
await chooser.setFiles([resumePath]);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
4. Use an equivalent documented primitive such as `setInputFiles` when that is what the selected browser provides. Do not assign `input.value`, synthesize a `DataTransfer`, inject file bytes through page JavaScript, or inspect browser session storage.
|
|
17
|
+
5. Use a native file picker only as a fallback after the privileged upload path is unavailable or fails and the browser-specific troubleshooting has been exhausted. Never make Finder, Explorer, or another visible picker the default flow.
|
|
18
|
+
6. Wait for the ATS to finish uploading or parsing. Verify the displayed filename, attachment success state, and any fields repopulated from the resume. Restore verified fields that parsing cleared or changed before continuing.
|
|
19
|
+
7. Keep the local path, filename, file bytes, and resume metadata out of telemetry, application answers, logs, and third-party messages.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { appendFile, chmod, mkdir, open, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { appendFile, chmod, mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { platform } from 'node:os';
|
|
6
6
|
import { basename, join, resolve } from 'node:path';
|
|
7
7
|
|
|
@@ -649,6 +649,18 @@ async function importResume(source) {
|
|
|
649
649
|
return { path: target, sha256: metadata.sha256, bytes: metadata.bytes };
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
async function canonicalResumePath() {
|
|
653
|
+
const target = join(await ensureStateDir(), 'resume.pdf');
|
|
654
|
+
try {
|
|
655
|
+
const details = await stat(target);
|
|
656
|
+
if (!details.isFile()) throw new Error('Canonical resume path is not a file. Import the resume again.');
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error.code === 'ENOENT') throw new Error('Canonical resume is not imported. Run resume import first.');
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
return { path: target };
|
|
662
|
+
}
|
|
663
|
+
|
|
652
664
|
function duplicateResult(entries, candidate) {
|
|
653
665
|
const candidateCompany = normalizedText(candidate.company);
|
|
654
666
|
const candidateRole = normalizedText(candidate.role);
|
|
@@ -806,6 +818,7 @@ async function executeCommand([area, action, value], telemetry, session) {
|
|
|
806
818
|
if (![...STRING_PROFILE_FIELDS, ...ARRAY_PROFILE_FIELDS, ...NUMBER_PROFILE_FIELDS, ...OBJECT_PROFILE_FIELDS].includes(value)) throw new Error('Profile field is not allowed.');
|
|
807
819
|
result = { [value]: storedProfile()[value] ?? null };
|
|
808
820
|
} else if (area === 'resume' && action === 'import' && value) result = await importResume(value);
|
|
821
|
+
else if (area === 'resume' && action === 'path' && value == null) result = await canonicalResumePath();
|
|
809
822
|
else if (area === 'score' && action === '--stdin') {
|
|
810
823
|
const job = await jsonStdin();
|
|
811
824
|
result = scoreJob(job, job.target ?? storedProfile());
|
|
@@ -828,7 +841,7 @@ async function executeCommand([area, action, value], telemetry, session) {
|
|
|
828
841
|
domainEvents.push(reviewTelemetry(result));
|
|
829
842
|
} else if (area === 'ledger' && action === 'review-ack' && value === '--stdin') {
|
|
830
843
|
result = await ledgerReviewAcknowledge(await jsonStdin());
|
|
831
|
-
} else throw new Error('Usage: profile set|migrate --stdin; profile check|field <name>; resume import <url-or-pdf
|
|
844
|
+
} else throw new Error('Usage: profile set|migrate --stdin; profile check|field <name>; resume import <url-or-pdf>|path; score --stdin; ledger check|add|outcome|review-ack --stdin; ledger review; telemetry status|enable|disable|reset|preview --stdin|record --stdin');
|
|
832
845
|
for (const event of domainEvents) await telemetry.record(event, session);
|
|
833
846
|
return result;
|
|
834
847
|
}
|
|
@@ -65,6 +65,20 @@ test('validates a candidate-defined target profile', () => {
|
|
|
65
65
|
assert.throws(() => validateProfile({ ...target, submissionMode: 'always' }), /review-each/);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
test('returns the canonical resume path for direct browser uploads', async (t) => {
|
|
69
|
+
const directory = await mkdtemp(join(tmpdir(), 'public-job-agent-resume-path-'));
|
|
70
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
71
|
+
const script = new URL('../scripts/job-application.mjs', import.meta.url).pathname;
|
|
72
|
+
const resume = join(directory, 'resume.pdf');
|
|
73
|
+
await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
|
|
74
|
+
await writeFile(resume, '%PDF-1.7\ncanonical resume fixture');
|
|
75
|
+
const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
|
|
76
|
+
|
|
77
|
+
const result = JSON.parse(execFileSync(process.execPath, [script, 'resume', 'path'], { env, encoding: 'utf8' }));
|
|
78
|
+
|
|
79
|
+
assert.deepEqual(result, { path: resume });
|
|
80
|
+
});
|
|
81
|
+
|
|
68
82
|
test('migrates a legacy profile without discarding identity or salary preference', () => {
|
|
69
83
|
const legacy = {
|
|
70
84
|
name: 'Test Candidate', email: 'candidate@example.com', phone: '+1 555 0100',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
|
|
5
|
+
test('requires direct path resume upload before native picker fallback', async () => {
|
|
6
|
+
const skill = await readFile(new URL('../SKILL.md', import.meta.url), 'utf8');
|
|
7
|
+
const guidance = await readFile(new URL('../references/BROWSER_UPLOADS.md', import.meta.url), 'utf8');
|
|
8
|
+
|
|
9
|
+
assert.match(skill, /resume path/);
|
|
10
|
+
assert.match(skill, /BROWSER_UPLOADS\.md/);
|
|
11
|
+
assert.match(guidance, /absolute path/i);
|
|
12
|
+
assert.match(guidance, /file chooser/i);
|
|
13
|
+
assert.match(guidance, /setFiles/);
|
|
14
|
+
assert.match(guidance, /native (file )?picker.*fallback/i);
|
|
15
|
+
assert.match(guidance, /verify.*filename/i);
|
|
16
|
+
});
|
package/package.json
CHANGED