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.
- package/README.md +23 -0
- package/installer/src/cli.mjs +8 -1
- package/installer/src/installer.mjs +8 -0
- package/job-application-agent/SKILL.md +32 -2
- package/job-application-agent/capabilities.json +3 -1
- package/job-application-agent/references/ACCOUNTING.md +104 -0
- package/job-application-agent/references/AUTONOMY.md +2 -0
- package/job-application-agent/references/CLOUD_STATE.md +2 -0
- package/job-application-agent/references/FREE_AI.md +39 -0
- package/job-application-agent/references/OUTREACH.md +210 -0
- package/job-application-agent/references/RUNS.md +35 -0
- package/job-application-agent/references/SCHEMAS.md +12 -2
- package/job-application-agent/references/agent-box/README.md +77 -0
- package/job-application-agent/references/agent-box/novnc.service.example +16 -0
- package/job-application-agent/scripts/application-accounting.mjs +246 -0
- package/job-application-agent/scripts/ats/answer-inject.mjs +203 -0
- package/job-application-agent/scripts/ats/submit-adapters.mjs +194 -0
- package/job-application-agent/scripts/attention-questions.mjs +111 -0
- package/job-application-agent/scripts/attention-resume-submit.mjs +450 -0
- package/job-application-agent/scripts/attention-runner-poll.mjs +328 -0
- package/job-application-agent/scripts/captcha-vendor.mjs +328 -0
- package/job-application-agent/scripts/cloud-state-client.mjs +62 -8
- package/job-application-agent/scripts/job-application.mjs +243 -27
- package/job-application-agent/scripts/novnc-display-guard.mjs +300 -0
- package/job-application-agent/scripts/outreach-cli.mjs +95 -0
- package/job-application-agent/scripts/outreach-domain.mjs +287 -0
- package/job-application-agent/scripts/outreach-store.mjs +72 -0
- package/job-application-agent/scripts/session-binding.mjs +474 -0
- package/job-application-agent/scripts/version.mjs +1 -1
- package/job-application-agent/tests/accounting-cli.test.mjs +86 -0
- package/job-application-agent/tests/accounting-cloud-client.test.mjs +183 -0
- package/job-application-agent/tests/accounting-retry-cli.test.mjs +96 -0
- package/job-application-agent/tests/accounting-source-race.test.mjs +109 -0
- package/job-application-agent/tests/answer-inject-captcha.test.mjs +169 -0
- package/job-application-agent/tests/application-accounting.test.mjs +315 -0
- package/job-application-agent/tests/attention-resume-submit.test.mjs +119 -0
- package/job-application-agent/tests/attention-runner-poll.test.mjs +135 -0
- package/job-application-agent/tests/fixtures/outreach.mjs +22 -0
- package/job-application-agent/tests/job-application.test.mjs +5 -0
- package/job-application-agent/tests/novnc-display-guard.test.mjs +50 -0
- package/job-application-agent/tests/outreach-cli.test.mjs +73 -0
- package/job-application-agent/tests/outreach.test.mjs +178 -0
- package/job-application-agent/tests/privacy-audit.test.mjs +2 -0
- package/job-application-agent/tests/review-cadence.test.mjs +66 -0
- package/job-application-agent/tests/session-binding.test.mjs +102 -0
- package/job-application-agent/tests/skill-contract.test.mjs +25 -0
- package/job-application-agent/tests/workflow-state.test.mjs +30 -3
- package/package.json +6 -3
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Resume → submit loop guidance after attention-runner-poll exit 0.
|
|
4
|
+
*
|
|
5
|
+
* Pure decision engine over a page snapshot + session binding. Does not drive
|
|
6
|
+
* the browser itself — Antigravity / Playwright / CDP injects the snapshot.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* resume_requested → renew lease → load session binding → same-tab check →
|
|
10
|
+
* inject approved answers (P1.5) → re-inspect blockers →
|
|
11
|
+
* optional CAPTCHA vendor (default Off) → submit if clear →
|
|
12
|
+
* wait for visible confirmation → intent-confirm / ledger guidance
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/attention-resume-submit.mjs --attention-id attention-… --stdin
|
|
16
|
+
* # stdin: page snapshot JSON (see decideResumeSubmit)
|
|
17
|
+
*
|
|
18
|
+
* Exit:
|
|
19
|
+
* 0 submitted_confirmed (ledger guidance printed)
|
|
20
|
+
* 10 still_blocked (re-open / update attention)
|
|
21
|
+
* 11 tab_drift / binding missing
|
|
22
|
+
* 12 submit_clicked_awaiting_confirm (ambiguous — intent-sent, no retry)
|
|
23
|
+
* 13 ready_to_submit (DOM clear — agent should click submit then re-probe)
|
|
24
|
+
* 14 inject_answers (fill approved narrative textareas, then re-probe)
|
|
25
|
+
* 1 error
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import {
|
|
31
|
+
assertSameTab,
|
|
32
|
+
readSessionBindingFile,
|
|
33
|
+
sessionBindingPath,
|
|
34
|
+
FILL_DISPLAY,
|
|
35
|
+
FILL_VNC_PORT,
|
|
36
|
+
} from "./session-binding.mjs";
|
|
37
|
+
import {
|
|
38
|
+
buildSubmitProbePlan,
|
|
39
|
+
detectAbsoluteBlockers,
|
|
40
|
+
detectConfirmation,
|
|
41
|
+
resolveAtsAdapter,
|
|
42
|
+
} from "./ats/submit-adapters.mjs";
|
|
43
|
+
import {
|
|
44
|
+
buildAnswerInjectPlan,
|
|
45
|
+
shouldInjectAnswersBeforeSubmit,
|
|
46
|
+
} from "./ats/answer-inject.mjs";
|
|
47
|
+
import { tryCaptchaVendorAssist } from "./captcha-vendor.mjs";
|
|
48
|
+
|
|
49
|
+
export const RESUME_EXIT = Object.freeze({
|
|
50
|
+
SUBMITTED: 0,
|
|
51
|
+
ERROR: 1,
|
|
52
|
+
STILL_BLOCKED: 10,
|
|
53
|
+
TAB_OR_BINDING: 11,
|
|
54
|
+
AWAITING_CONFIRM: 12,
|
|
55
|
+
READY_TO_SUBMIT: 13,
|
|
56
|
+
INJECT_ANSWERS: 14,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {{
|
|
61
|
+
* pageUrl: string,
|
|
62
|
+
* pageTitle?: string,
|
|
63
|
+
* pageText?: string,
|
|
64
|
+
* pageHtml?: string,
|
|
65
|
+
* visibleTexts?: string[],
|
|
66
|
+
* submitEnabled?: boolean,
|
|
67
|
+
* submitPresent?: boolean,
|
|
68
|
+
* matchedBlockerSelectors?: string[],
|
|
69
|
+
* matchedConfirmationSelectors?: string[],
|
|
70
|
+
* submitAlreadyClicked?: boolean,
|
|
71
|
+
* leaseHeld?: boolean,
|
|
72
|
+
* answersInjected?: boolean,
|
|
73
|
+
* narrativeFields?: object[],
|
|
74
|
+
* sitekey?: string,
|
|
75
|
+
* challengeType?: string,
|
|
76
|
+
* }} ResumePageSnapshot
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {{
|
|
81
|
+
* binding: object | null,
|
|
82
|
+
* snapshot: ResumePageSnapshot,
|
|
83
|
+
* signal?: string,
|
|
84
|
+
* answers?: { questionId: string, text: string, source?: string, prompt?: string }[],
|
|
85
|
+
* captchaAssist?: object | null,
|
|
86
|
+
* }} input
|
|
87
|
+
*/
|
|
88
|
+
export function decideResumeSubmit(input) {
|
|
89
|
+
const snapshot = input.snapshot && typeof input.snapshot === "object" ? input.snapshot : null;
|
|
90
|
+
if (!snapshot || !String(snapshot.pageUrl ?? "").trim()) {
|
|
91
|
+
return {
|
|
92
|
+
action: "error",
|
|
93
|
+
exitCode: RESUME_EXIT.ERROR,
|
|
94
|
+
message: "pageUrl is required on the resume page snapshot.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (input.signal && input.signal !== "resume_requested") {
|
|
99
|
+
return {
|
|
100
|
+
action: "ignore_signal",
|
|
101
|
+
exitCode: RESUME_EXIT.ERROR,
|
|
102
|
+
message: `Expected resume_requested signal; got ${input.signal}.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (snapshot.leaseHeld === false) {
|
|
107
|
+
return {
|
|
108
|
+
action: "session_expired",
|
|
109
|
+
exitCode: RESUME_EXIT.TAB_OR_BINDING,
|
|
110
|
+
message: "Lease lost during attention wait. Open new attention session-expired; fail closed. Do not submit.",
|
|
111
|
+
next: [
|
|
112
|
+
"attention add --stdin with blocker site-error / requiredActions review-form (or session-expired note in evidence path)",
|
|
113
|
+
"Do not invent ledger success",
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const binding = input.binding;
|
|
119
|
+
if (!binding) {
|
|
120
|
+
return {
|
|
121
|
+
action: "binding_missing",
|
|
122
|
+
exitCode: RESUME_EXIT.TAB_OR_BINDING,
|
|
123
|
+
message: [
|
|
124
|
+
"Session binding missing for this attention id.",
|
|
125
|
+
"Cannot prove same-tab fill session. Refuse cold navigation.",
|
|
126
|
+
`Record binding at pause: display=${FILL_DISPLAY} vncPort=${FILL_VNC_PORT} browserProfilePath + jobUrl.`,
|
|
127
|
+
].join(" "),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tab = assertSameTab(binding.jobUrl, snapshot.pageUrl, binding.tabHint ?? {});
|
|
132
|
+
if (!tab.ok) {
|
|
133
|
+
return {
|
|
134
|
+
action: "tab_drift",
|
|
135
|
+
exitCode: RESUME_EXIT.TAB_OR_BINDING,
|
|
136
|
+
message: tab.message,
|
|
137
|
+
reason: tab.reason,
|
|
138
|
+
binding,
|
|
139
|
+
next: [
|
|
140
|
+
"Focus the paused filled ATS tab (same Chrome profile / CDP session)",
|
|
141
|
+
"Do not open a fresh jobs listing",
|
|
142
|
+
"Re-run resume probe once the bound tab is focused",
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const adapter = resolveAtsAdapter(snapshot.pageUrl);
|
|
148
|
+
const probe = buildSubmitProbePlan(snapshot.pageUrl);
|
|
149
|
+
const confirmation = detectConfirmation(snapshot, adapter);
|
|
150
|
+
if (confirmation.confirmed) {
|
|
151
|
+
return {
|
|
152
|
+
action: "submitted_confirmed",
|
|
153
|
+
exitCode: RESUME_EXIT.SUBMITTED,
|
|
154
|
+
message: "Visible ATS confirmation detected. Confirm intent / ledger only now. filled ≠ applied until this point.",
|
|
155
|
+
confirmation,
|
|
156
|
+
adapterId: adapter.id,
|
|
157
|
+
binding,
|
|
158
|
+
next: [
|
|
159
|
+
"cloud intent-confirm --stdin (with cloudIntentId + leaseId) OR ledger add with cloudIntentId + cloudLeaseId",
|
|
160
|
+
"attention resolve --stdin",
|
|
161
|
+
"Continue round or release lease per policy",
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const answers = Array.isArray(input.answers) ? input.answers : [];
|
|
167
|
+
const injectGate = shouldInjectAnswersBeforeSubmit({ answers });
|
|
168
|
+
if (injectGate.inject && !snapshot.answersInjected) {
|
|
169
|
+
const questions = Array.isArray(binding.questions) ? binding.questions : [];
|
|
170
|
+
const injectPlan = buildAnswerInjectPlan({
|
|
171
|
+
answers,
|
|
172
|
+
questions,
|
|
173
|
+
fields: snapshot.narrativeFields,
|
|
174
|
+
pageUrl: snapshot.pageUrl,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
action: "inject_answers",
|
|
178
|
+
exitCode: RESUME_EXIT.INJECT_ANSWERS,
|
|
179
|
+
message: "Approved attention answers ready — inject into matching textareas on the bound tab, then re-inspect.",
|
|
180
|
+
injectPlan,
|
|
181
|
+
adapterId: adapter.id,
|
|
182
|
+
binding,
|
|
183
|
+
next: [
|
|
184
|
+
"Fill mapped textareas from injectPlan.fills (Ashby textarea selectors preferred)",
|
|
185
|
+
"Re-run resume probe with answersInjected:true",
|
|
186
|
+
"Do not treat draft text as approved unless source is typed|draft_approved|bank",
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const blockers = detectAbsoluteBlockers(snapshot, { mode: "resume" });
|
|
192
|
+
if (blockers.blocked) {
|
|
193
|
+
const captchaAssist = input.captchaAssist ?? null;
|
|
194
|
+
return {
|
|
195
|
+
action: "still_blocked",
|
|
196
|
+
exitCode: RESUME_EXIT.STILL_BLOCKED,
|
|
197
|
+
message: `Absolute blockers still present: ${blockers.blockers.join(", ")}. Update attention honestly; stay paused.`,
|
|
198
|
+
blockers: blockers.blockers,
|
|
199
|
+
adapterId: adapter.id,
|
|
200
|
+
binding,
|
|
201
|
+
probe,
|
|
202
|
+
captchaAssist: blockers.blockers.includes("captcha") ? captchaAssist : undefined,
|
|
203
|
+
next: [
|
|
204
|
+
"attention add/update with remaining blockers + requiredActions",
|
|
205
|
+
"Keep same-tab session binding; renew lease while waiting",
|
|
206
|
+
"Do not submit",
|
|
207
|
+
captchaAssist && captchaAssist.reason === "vendor_off"
|
|
208
|
+
? "CAPTCHA_VENDOR=off — use live panel complete-captcha (default)"
|
|
209
|
+
: null,
|
|
210
|
+
].filter(Boolean),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (snapshot.submitAlreadyClicked) {
|
|
215
|
+
return {
|
|
216
|
+
action: "submit_ambiguous",
|
|
217
|
+
exitCode: RESUME_EXIT.AWAITING_CONFIRM,
|
|
218
|
+
message: "Submit was clicked but confirmation is not visible. Mark intent-sent / sent-unverified. Never retry until verified.",
|
|
219
|
+
adapterId: adapter.id,
|
|
220
|
+
binding,
|
|
221
|
+
probe,
|
|
222
|
+
next: [
|
|
223
|
+
"cloud intent-sent --stdin",
|
|
224
|
+
"Do not click submit again",
|
|
225
|
+
"Re-open attention if the candidate must check email/ATS manually",
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const submitPresent = snapshot.submitPresent !== false;
|
|
231
|
+
const submitEnabled = snapshot.submitEnabled !== false;
|
|
232
|
+
if (submitPresent && submitEnabled) {
|
|
233
|
+
return {
|
|
234
|
+
action: "ready_to_submit",
|
|
235
|
+
exitCode: RESUME_EXIT.READY_TO_SUBMIT,
|
|
236
|
+
message: "Re-inspect clear — submit if possible (golden-path bias). Click submit, then re-probe for confirmation.",
|
|
237
|
+
adapterId: adapter.id,
|
|
238
|
+
binding,
|
|
239
|
+
probe,
|
|
240
|
+
sameTab: tab,
|
|
241
|
+
next: [
|
|
242
|
+
`Click first enabled submit control matching: ${probe.submitSelectors.join(" | ")}`,
|
|
243
|
+
"Wait for confirmation selectors / URL patterns",
|
|
244
|
+
"Re-run this decision with matchedConfirmationSelectors or updated pageText",
|
|
245
|
+
"Only after visible confirm: intent-confirm / ledger add",
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
action: "submit_unavailable",
|
|
252
|
+
exitCode: RESUME_EXIT.STILL_BLOCKED,
|
|
253
|
+
message: "No enabled submit control after re-inspect. Stay paused / update attention — do not invent success.",
|
|
254
|
+
adapterId: adapter.id,
|
|
255
|
+
binding,
|
|
256
|
+
probe,
|
|
257
|
+
next: [
|
|
258
|
+
"Verify the live tab is still the filled application form",
|
|
259
|
+
"If fields were wiped, refill verified profile facts then re-probe",
|
|
260
|
+
"If a new absolute gate appeared, record attention honestly",
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Checklist printed for agents when poll exits 0 (even without a snapshot).
|
|
267
|
+
*/
|
|
268
|
+
export function resumeSubmitChecklist() {
|
|
269
|
+
return [
|
|
270
|
+
"1. cloud lease-renew (fail closed if lease lost → session-expired attention)",
|
|
271
|
+
`2. Load session binding (display=${FILL_DISPLAY}, vncPort=${FILL_VNC_PORT}, browserProfilePath, jobUrl)`,
|
|
272
|
+
"3. Focus the SAME filled ATS tab — never navigate to a cold jobs listing",
|
|
273
|
+
"4. Load approved answers from poll payload (answers[]) / signal",
|
|
274
|
+
"5. Inject answers into matching textareas (Ashby-first), then re-inspect",
|
|
275
|
+
"6. Re-inspect live DOM (blockers, required fields). Do not trust prior fill memory.",
|
|
276
|
+
"7. CAPTCHA_VENDOR=off by default — live panel for captcha; vendor assist only when explicitly enabled",
|
|
277
|
+
"8. If still blocked → update attention honestly; keep binding; do not submit",
|
|
278
|
+
"9. If clear → SUBMIT (no extra in-app confirm). Prefer Ashby/generic submit selectors.",
|
|
279
|
+
"10. Wait for visible confirmation surface (selectors / thank-you URL)",
|
|
280
|
+
"11. Only then: cloud intent-confirm / ledger add. filled ≠ applied.",
|
|
281
|
+
"12. If submit ambiguous → intent-sent; never retry until verified",
|
|
282
|
+
"13. attention resolve; continue round or release lease",
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function resolveStateDir(env = process.env) {
|
|
287
|
+
const trimmed = String(env.JOB_APPLICATION_AGENT_STATE_DIR ?? "").trim();
|
|
288
|
+
if (trimmed) return trimmed;
|
|
289
|
+
return join(String(env.HOME ?? "/tmp").trim() || "/tmp", ".job-application-agent");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @param {string[]} argv
|
|
294
|
+
*/
|
|
295
|
+
export function parseResumeSubmitArgs(argv) {
|
|
296
|
+
const args = {
|
|
297
|
+
attentionId: "",
|
|
298
|
+
stdin: false,
|
|
299
|
+
checklist: false,
|
|
300
|
+
help: false,
|
|
301
|
+
signal: "resume_requested",
|
|
302
|
+
};
|
|
303
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
304
|
+
const flag = argv[i];
|
|
305
|
+
const next = argv[i + 1];
|
|
306
|
+
if (flag === "--attention-id" || flag === "--id") {
|
|
307
|
+
args.attentionId = String(next ?? "").trim();
|
|
308
|
+
i += 1;
|
|
309
|
+
} else if (flag === "--signal") {
|
|
310
|
+
args.signal = String(next ?? "").trim();
|
|
311
|
+
i += 1;
|
|
312
|
+
} else if (flag === "--stdin") {
|
|
313
|
+
args.stdin = true;
|
|
314
|
+
} else if (flag === "--checklist") {
|
|
315
|
+
args.checklist = true;
|
|
316
|
+
} else if (flag === "--help" || flag === "-h") {
|
|
317
|
+
args.help = true;
|
|
318
|
+
} else if (flag.startsWith("-")) {
|
|
319
|
+
throw new Error(`Unknown flag: ${flag}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return args;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function readStdinJson() {
|
|
326
|
+
const chunks = [];
|
|
327
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
328
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
329
|
+
if (!text) return {};
|
|
330
|
+
return JSON.parse(text);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function printHelp() {
|
|
334
|
+
console.log(`Usage: node scripts/attention-resume-submit.mjs [options]
|
|
335
|
+
|
|
336
|
+
Options:
|
|
337
|
+
--attention-id <id> Load local session binding for same-tab checks
|
|
338
|
+
--stdin Page snapshot JSON on stdin (optional answers[])
|
|
339
|
+
--checklist Print resume→submit checklist and exit 0
|
|
340
|
+
--signal <name> Default resume_requested
|
|
341
|
+
|
|
342
|
+
Page snapshot fields:
|
|
343
|
+
pageUrl (required), pageTitle?, pageText?, visibleTexts?[],
|
|
344
|
+
submitEnabled?, submitPresent?, matchedBlockerSelectors?[],
|
|
345
|
+
matchedConfirmationSelectors?[], submitAlreadyClicked?, leaseHeld?,
|
|
346
|
+
answersInjected?, narrativeFields?[]
|
|
347
|
+
|
|
348
|
+
Exit codes:
|
|
349
|
+
0 submitted_confirmed
|
|
350
|
+
10 still_blocked / submit_unavailable
|
|
351
|
+
11 tab_drift / binding missing / lease lost
|
|
352
|
+
12 submit_ambiguous (awaiting confirm)
|
|
353
|
+
13 ready_to_submit
|
|
354
|
+
14 inject_answers (P1.5 — fill textareas then re-probe)
|
|
355
|
+
1 error
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function main(argv = process.argv.slice(2)) {
|
|
360
|
+
let args;
|
|
361
|
+
try {
|
|
362
|
+
args = parseResumeSubmitArgs(argv);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error(`[resume-submit] ${error instanceof Error ? error.message : error}`);
|
|
365
|
+
process.exitCode = RESUME_EXIT.ERROR;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (args.help) {
|
|
369
|
+
printHelp();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (args.checklist) {
|
|
373
|
+
for (const line of resumeSubmitChecklist()) console.log(line);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const input = args.stdin ? await readStdinJson() : {};
|
|
378
|
+
const attentionId = args.attentionId || String(input.attentionId ?? "").trim();
|
|
379
|
+
let binding = input.binding ?? null;
|
|
380
|
+
if (!binding && attentionId) {
|
|
381
|
+
const file = sessionBindingPath(resolveStateDir(), attentionId);
|
|
382
|
+
binding = await readSessionBindingFile(file);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const snapshot = input.snapshot ?? input;
|
|
386
|
+
const answers = Array.isArray(input.answers) ? input.answers : (snapshot.answers ?? []);
|
|
387
|
+
|
|
388
|
+
// Single CAPTCHA vendor call site — no-ops when CAPTCHA_VENDOR=off (default).
|
|
389
|
+
let captchaAssist = null;
|
|
390
|
+
const provisionalBlockers = detectAbsoluteBlockers({
|
|
391
|
+
pageUrl: snapshot.pageUrl,
|
|
392
|
+
pageText: snapshot.pageText,
|
|
393
|
+
pageHtml: snapshot.pageHtml,
|
|
394
|
+
visibleTexts: snapshot.visibleTexts,
|
|
395
|
+
matchedBlockerSelectors: snapshot.matchedBlockerSelectors,
|
|
396
|
+
}, { mode: "resume" });
|
|
397
|
+
if (provisionalBlockers.blockers.includes("captcha")) {
|
|
398
|
+
captchaAssist = await tryCaptchaVendorAssist({
|
|
399
|
+
snapshot: {
|
|
400
|
+
pageUrl: snapshot.pageUrl,
|
|
401
|
+
pageText: snapshot.pageText,
|
|
402
|
+
pageHtml: snapshot.pageHtml,
|
|
403
|
+
matchedBlockerSelectors: snapshot.matchedBlockerSelectors,
|
|
404
|
+
sitekey: snapshot.sitekey,
|
|
405
|
+
challengeType: snapshot.challengeType,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const decision = decideResumeSubmit({
|
|
411
|
+
binding,
|
|
412
|
+
snapshot: {
|
|
413
|
+
pageUrl: snapshot.pageUrl,
|
|
414
|
+
pageTitle: snapshot.pageTitle,
|
|
415
|
+
pageText: snapshot.pageText,
|
|
416
|
+
pageHtml: snapshot.pageHtml,
|
|
417
|
+
visibleTexts: snapshot.visibleTexts,
|
|
418
|
+
submitEnabled: snapshot.submitEnabled,
|
|
419
|
+
submitPresent: snapshot.submitPresent,
|
|
420
|
+
matchedBlockerSelectors: snapshot.matchedBlockerSelectors,
|
|
421
|
+
matchedConfirmationSelectors: snapshot.matchedConfirmationSelectors,
|
|
422
|
+
submitAlreadyClicked: snapshot.submitAlreadyClicked,
|
|
423
|
+
leaseHeld: snapshot.leaseHeld,
|
|
424
|
+
answersInjected: snapshot.answersInjected,
|
|
425
|
+
narrativeFields: snapshot.narrativeFields,
|
|
426
|
+
},
|
|
427
|
+
signal: args.signal,
|
|
428
|
+
answers,
|
|
429
|
+
captchaAssist,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
console.log(JSON.stringify({
|
|
433
|
+
ok: decision.exitCode === RESUME_EXIT.SUBMITTED
|
|
434
|
+
|| decision.exitCode === RESUME_EXIT.READY_TO_SUBMIT
|
|
435
|
+
|| decision.exitCode === RESUME_EXIT.INJECT_ANSWERS,
|
|
436
|
+
...decision,
|
|
437
|
+
checklist: resumeSubmitChecklist(),
|
|
438
|
+
}, null, 2));
|
|
439
|
+
process.exitCode = decision.exitCode;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const isDirect = import.meta.url === pathToFileURL(process.argv[1] ?? "").href
|
|
443
|
+
|| process.argv[1]?.endsWith("attention-resume-submit.mjs");
|
|
444
|
+
|
|
445
|
+
if (isDirect) {
|
|
446
|
+
main().catch((error) => {
|
|
447
|
+
console.error(`[resume-submit] ${error instanceof Error ? error.message : error}`);
|
|
448
|
+
process.exitCode = RESUME_EXIT.ERROR;
|
|
449
|
+
});
|
|
450
|
+
}
|