@proagentstore/cli 0.1.8 → 0.1.9

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.
@@ -11,7 +11,7 @@ const CAPABILITIES = [
11
11
  "file.upload",
12
12
  "human.approval",
13
13
  ];
14
- const APPROVAL_REQUIRED_TASKS = new Set(["browser.open", "job.apply_basic"]);
14
+ const APPROVAL_REQUIRED_TASKS = new Set(["browser.open", "job.apply_basic", "job.apply_authenticated"]);
15
15
  const require = createRequire(import.meta.url);
16
16
  export class RunnerInputError extends Error {
17
17
  status = 400;
@@ -34,7 +34,7 @@ export class LocalRunner {
34
34
  runtimePlane: "fags",
35
35
  runnerRole: "tool-executor",
36
36
  capabilities: CAPABILITIES,
37
- taskTypes: ["echo", "browser.open", "job.apply_basic"],
37
+ taskTypes: ["echo", "browser.open", "job.apply_basic", "job.apply_authenticated"],
38
38
  approvalRequiredFor: [...APPROVAL_REQUIRED_TASKS],
39
39
  };
40
40
  }
@@ -57,11 +57,8 @@ export class LocalRunner {
57
57
  updatedAt: now,
58
58
  };
59
59
  this.store.putTask(task);
60
- this.store.addEvent({
61
- taskId: task.id,
62
- type: "task.created",
63
- message: `Task created: ${task.type}`,
64
- data: { status: task.status },
60
+ this.addTaskEvent(task, "task.created", `Task created: ${task.type}`, {
61
+ status: task.status,
65
62
  });
66
63
  if (task.status === "queued")
67
64
  void this.runTask(task.id);
@@ -79,11 +76,7 @@ export class LocalRunner {
79
76
  approvedAt: task.updatedAt,
80
77
  };
81
78
  this.store.putTask(task);
82
- this.store.addEvent({
83
- taskId: task.id,
84
- type: "task.approved",
85
- message: `Task approved: ${task.type}`,
86
- });
79
+ this.addTaskEvent(task, "task.approved", `Task approved: ${task.type}`);
87
80
  await this.runTask(task.id);
88
81
  return this.requireTask(id);
89
82
  }
@@ -95,11 +88,7 @@ export class LocalRunner {
95
88
  task.updatedAt = new Date().toISOString();
96
89
  task.completedAt = task.updatedAt;
97
90
  this.store.putTask(task);
98
- this.store.addEvent({
99
- taskId: task.id,
100
- type: "task.cancelled",
101
- message: `Task cancelled: ${task.type}`,
102
- });
91
+ this.addTaskEvent(task, "task.cancelled", `Task cancelled: ${task.type}`);
103
92
  return task;
104
93
  }
105
94
  async close() {
@@ -113,11 +102,7 @@ export class LocalRunner {
113
102
  task.status = "running";
114
103
  task.updatedAt = new Date().toISOString();
115
104
  this.store.putTask(task);
116
- this.store.addEvent({
117
- taskId: task.id,
118
- type: "task.running",
119
- message: `Task running: ${task.type}`,
120
- });
105
+ this.addTaskEvent(task, "task.running", `Task running: ${task.type}`);
121
106
  try {
122
107
  const output = await this.execute(task);
123
108
  task.status = "completed";
@@ -125,12 +110,7 @@ export class LocalRunner {
125
110
  task.updatedAt = new Date().toISOString();
126
111
  task.completedAt = task.updatedAt;
127
112
  this.store.putTask(task);
128
- this.store.addEvent({
129
- taskId: task.id,
130
- type: "task.completed",
131
- message: `Task completed: ${task.type}`,
132
- data: output,
133
- });
113
+ this.addTaskEvent(task, "task.completed", `Task completed: ${task.type}`, output);
134
114
  }
135
115
  catch (error) {
136
116
  task.status = "failed";
@@ -138,13 +118,17 @@ export class LocalRunner {
138
118
  task.updatedAt = new Date().toISOString();
139
119
  task.completedAt = task.updatedAt;
140
120
  this.store.putTask(task);
141
- this.store.addEvent({
142
- taskId: task.id,
143
- type: "task.failed",
144
- message: task.error,
145
- });
121
+ this.addTaskEvent(task, "task.failed", task.error);
146
122
  }
147
123
  }
124
+ addTaskEvent(task, type, message, data) {
125
+ this.store.addEvent({
126
+ taskId: task.id,
127
+ type,
128
+ message,
129
+ data,
130
+ });
131
+ }
148
132
  async execute(task) {
149
133
  if (task.type === "echo") {
150
134
  return task.input;
@@ -156,32 +140,59 @@ export class LocalRunner {
156
140
  }
157
141
  const context = await this.getBrowserContext();
158
142
  const page = context.pages()[0] || (await context.newPage());
143
+ this.addTaskEvent(task, "browser.goto.started", "Opening browser page", { url });
159
144
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
160
- return {
145
+ const output = {
161
146
  url: page.url(),
162
147
  title: await page.title(),
163
148
  };
149
+ this.addTaskEvent(task, "browser.goto.completed", "Browser page loaded", output);
150
+ return output;
164
151
  }
165
152
  if (task.type === "job.apply_basic") {
166
- return this.applyToBasicJob(task.input);
153
+ return this.applyToBasicJob(task);
154
+ }
155
+ if (task.type === "job.apply_authenticated") {
156
+ return this.applyToAuthenticatedJob(task);
167
157
  }
168
158
  throw new Error(`Unknown task type: ${task.type}`);
169
159
  }
170
- async applyToBasicJob(input) {
171
- const job = normalizeJobApplicationInput(input);
160
+ async applyToBasicJob(task) {
161
+ const job = normalizeJobApplicationInput(task.input);
172
162
  const context = await this.getBrowserContext();
173
163
  const page = context.pages()[0] || (await context.newPage());
164
+ this.addTaskEvent(task, "browser.goto.started", "Opening job application page", {
165
+ url: job.url,
166
+ });
174
167
  await page.goto(job.url, { waitUntil: "domcontentloaded", timeout: 30_000 });
168
+ this.addTaskEvent(task, "browser.goto.completed", "Job application page loaded", {
169
+ url: page.url(),
170
+ title: await page.title(),
171
+ });
172
+ this.addTaskEvent(task, "job.form.fill.started", "Filling application form", {
173
+ candidate: {
174
+ fullName: job.candidate.fullName,
175
+ email: job.candidate.email,
176
+ },
177
+ resumeFile: basename(job.resumePath),
178
+ });
175
179
  const filledFields = await fillBasicApplicationForm(page, job);
180
+ this.addTaskEvent(task, "job.form.filled", "Application form fields completed", {
181
+ fieldsFilled: filledFields,
182
+ resumeFile: basename(job.resumePath),
183
+ });
176
184
  const beforeSubmitUrl = page.url();
177
185
  const navigation = page.waitForNavigation({
178
186
  waitUntil: "domcontentloaded",
179
187
  timeout: 15_000,
180
188
  }).catch(() => null);
189
+ this.addTaskEvent(task, "job.form.submit.started", "Submitting application form", {
190
+ url: beforeSubmitUrl,
191
+ });
181
192
  await submitApplicationForm(page);
182
193
  await navigation;
183
194
  await page.waitForLoadState("domcontentloaded", { timeout: 5_000 }).catch(() => undefined);
184
- return {
195
+ const output = {
185
196
  taskType: "job.apply_basic",
186
197
  submitted: true,
187
198
  beforeSubmitUrl,
@@ -191,6 +202,114 @@ export class LocalRunner {
191
202
  resumeFile: basename(job.resumePath),
192
203
  visibleText: (await page.locator("body").innerText({ timeout: 5_000 }).catch(() => "")).slice(0, 2_000),
193
204
  };
205
+ this.addTaskEvent(task, "job.form.submit.completed", "Application submission completed", output);
206
+ return output;
207
+ }
208
+ /**
209
+ * Apply to a job that requires login/registration first.
210
+ * Flow: navigate → detect login requirement → register or login → fill form → submit
211
+ */
212
+ async applyToAuthenticatedJob(task) {
213
+ const input = normalizeAuthenticatedJobInput(task.input);
214
+ const context = await this.getBrowserContext();
215
+ const page = context.pages()[0] || (await context.newPage());
216
+ // Step 1: Navigate to job page
217
+ this.addTaskEvent(task, "browser.goto.started", "Opening job page", { url: input.url });
218
+ await page.goto(input.url, { waitUntil: "domcontentloaded", timeout: 30_000 });
219
+ this.addTaskEvent(task, "browser.goto.completed", "Job page loaded", {
220
+ url: page.url(),
221
+ title: await page.title(),
222
+ });
223
+ // Step 2: Check if we need to authenticate
224
+ const needsAuth = await detectAuthRequirement(page);
225
+ if (needsAuth) {
226
+ this.addTaskEvent(task, "job.auth.required", "Authentication required", {
227
+ loginDetected: true,
228
+ });
229
+ // Navigate to login page
230
+ const loginUrl = input.loginUrl || await findLoginUrl(page, input.url);
231
+ if (!loginUrl)
232
+ throw new Error("Login required but no login URL found");
233
+ await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
234
+ if (input.registration && !input.accountExists) {
235
+ // Register first
236
+ this.addTaskEvent(task, "job.auth.register.started", "Registering new account");
237
+ const registerUrl = await findRegisterUrl(page, loginUrl);
238
+ if (registerUrl && registerUrl !== loginUrl) {
239
+ await page.goto(registerUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
240
+ }
241
+ await fillRegistrationForm(page, input.registration);
242
+ await submitForm(page);
243
+ await page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => undefined);
244
+ this.addTaskEvent(task, "job.auth.register.completed", "Registration submitted", {
245
+ url: page.url(),
246
+ });
247
+ }
248
+ else if (input.credentials) {
249
+ // Login with existing credentials
250
+ this.addTaskEvent(task, "job.auth.login.started", "Logging in");
251
+ await fillLoginForm(page, input.credentials);
252
+ await submitForm(page);
253
+ await page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => undefined);
254
+ this.addTaskEvent(task, "job.auth.login.completed", "Login submitted", {
255
+ url: page.url(),
256
+ });
257
+ }
258
+ else {
259
+ throw new Error("Login required but no credentials or registration details provided");
260
+ }
261
+ // Verify we're logged in (check for common indicators)
262
+ const stillNeedsAuth = await detectAuthRequirement(page);
263
+ if (stillNeedsAuth) {
264
+ // Try navigating back to the apply page — some sites redirect after login
265
+ const applyUrl = input.url.includes("/apply") ? input.url : `${input.url}`;
266
+ await page.goto(applyUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
267
+ const thirdCheck = await detectAuthRequirement(page);
268
+ if (thirdCheck) {
269
+ throw new Error("Authentication failed — still seeing login/register prompts after login attempt");
270
+ }
271
+ }
272
+ // Navigate to apply page (may have been redirected after login)
273
+ if (!page.url().includes("apply")) {
274
+ await page.goto(input.url, { waitUntil: "domcontentloaded", timeout: 30_000 });
275
+ }
276
+ }
277
+ // Step 3: Look for the apply form (might be on current page or linked)
278
+ const applyLink = await page.locator('a[href*="apply"], a:has-text("Apply")').first();
279
+ if (await applyLink.count() > 0) {
280
+ await applyLink.click();
281
+ await page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => undefined);
282
+ }
283
+ // Step 4: Fill the application form
284
+ this.addTaskEvent(task, "job.form.fill.started", "Filling application form");
285
+ const filledFields = await fillBasicApplicationForm(page, input);
286
+ this.addTaskEvent(task, "job.form.filled", "Form fields completed", {
287
+ fieldsFilled: filledFields,
288
+ resumeFile: basename(input.resumePath),
289
+ });
290
+ // Step 5: Submit
291
+ const beforeSubmitUrl = page.url();
292
+ const navigation = page.waitForNavigation({
293
+ waitUntil: "domcontentloaded",
294
+ timeout: 15_000,
295
+ }).catch(() => null);
296
+ this.addTaskEvent(task, "job.form.submit.started", "Submitting application");
297
+ await submitApplicationForm(page);
298
+ await navigation;
299
+ await page.waitForLoadState("domcontentloaded", { timeout: 5_000 }).catch(() => undefined);
300
+ const output = {
301
+ taskType: "job.apply_authenticated",
302
+ submitted: true,
303
+ authenticated: needsAuth,
304
+ beforeSubmitUrl,
305
+ finalUrl: page.url(),
306
+ title: await page.title(),
307
+ fieldsFilled: filledFields,
308
+ resumeFile: basename(input.resumePath),
309
+ visibleText: (await page.locator("body").innerText({ timeout: 5_000 }).catch(() => "")).slice(0, 2_000),
310
+ };
311
+ this.addTaskEvent(task, "job.form.submit.completed", "Application submitted", output);
312
+ return output;
194
313
  }
195
314
  requireTask(id) {
196
315
  const task = this.store.getTask(id);
@@ -406,3 +525,99 @@ function normalizeCreateTaskRequest(request) {
406
525
  export function fileUrl(path) {
407
526
  return pathToFileURL(path).toString();
408
527
  }
528
+ // ── Authenticated job application helpers ─────────────────────────────────
529
+ export function normalizeAuthenticatedJobInput(input) {
530
+ const base = normalizeJobApplicationInput(input);
531
+ const credentials = isRecord(input.credentials) ? input.credentials : undefined;
532
+ const registration = isRecord(input.registration) ? input.registration : undefined;
533
+ return {
534
+ ...base,
535
+ credentials: credentials ? {
536
+ email: stringValue(credentials.email),
537
+ password: stringValue(credentials.password),
538
+ } : undefined,
539
+ loginUrl: optionalString(input.loginUrl),
540
+ accountExists: input.accountExists === true,
541
+ registration: registration ? {
542
+ fullName: stringValue(registration.fullName),
543
+ email: stringValue(registration.email),
544
+ password: stringValue(registration.password),
545
+ } : undefined,
546
+ };
547
+ }
548
+ async function detectAuthRequirement(page) {
549
+ const body = await page.locator("body").innerHTML({ timeout: 5_000 }).catch(() => "");
550
+ const lower = body.toLowerCase();
551
+ // Check for login/register prompts
552
+ const hasLoginForm = await page.locator('input[type="password"], form[action*="login"], form[action*="signin"]').count() > 0;
553
+ const hasLoginText = /sign\s*in|log\s*in|create\s*(an\s*)?account|register\s*to\s*apply/i.test(lower);
554
+ const hasLoginLink = await page.locator('a[href*="login"], a[href*="signin"], a[href*="register"]').count() > 0;
555
+ return hasLoginForm || (hasLoginText && hasLoginLink);
556
+ }
557
+ async function findLoginUrl(page, baseUrl) {
558
+ const loginLink = page.locator('a[href*="login"], a[href*="signin"], a:has-text("Sign in"), a:has-text("Log in")').first();
559
+ if (await loginLink.count() > 0) {
560
+ const href = await loginLink.getAttribute("href");
561
+ if (href)
562
+ return new URL(href, baseUrl).toString();
563
+ }
564
+ return null;
565
+ }
566
+ async function findRegisterUrl(page, baseUrl) {
567
+ const regLink = page.locator('a[href*="register"], a[href*="signup"], a:has-text("Create account"), a:has-text("Sign up")').first();
568
+ if (await regLink.count() > 0) {
569
+ const href = await regLink.getAttribute("href");
570
+ if (href)
571
+ return new URL(href, baseUrl).toString();
572
+ }
573
+ return null;
574
+ }
575
+ async function fillLoginForm(page, credentials) {
576
+ await fillFirst(page, [
577
+ 'input[name="email"]',
578
+ 'input[type="email"]',
579
+ 'input[autocomplete="email"]',
580
+ 'input[autocomplete="username"]',
581
+ 'input[name="username"]',
582
+ ], credentials.email);
583
+ await fillFirst(page, [
584
+ 'input[name="password"]',
585
+ 'input[type="password"]',
586
+ 'input[autocomplete="current-password"]',
587
+ ], credentials.password);
588
+ }
589
+ async function fillRegistrationForm(page, registration) {
590
+ await fillFirst(page, [
591
+ 'input[name="fullName"]',
592
+ 'input[name="full_name"]',
593
+ 'input[name="name"]',
594
+ 'input[autocomplete="name"]',
595
+ ], registration.fullName);
596
+ await fillFirst(page, [
597
+ 'input[name="email"]',
598
+ 'input[type="email"]',
599
+ 'input[autocomplete="email"]',
600
+ ], registration.email);
601
+ await fillFirst(page, [
602
+ 'input[name="password"]',
603
+ 'input[type="password"]',
604
+ 'input[autocomplete="new-password"]',
605
+ ], registration.password);
606
+ }
607
+ async function submitForm(page) {
608
+ const submit = page.locator('button[type="submit"], input[type="submit"]').first();
609
+ if (await submit.count() > 0) {
610
+ await submit.click({ timeout: 10_000 });
611
+ return;
612
+ }
613
+ const form = page.locator("form").first();
614
+ if (await form.count() > 0) {
615
+ await form.evaluate((node) => {
616
+ const f = node;
617
+ if (typeof f.requestSubmit === "function")
618
+ f.requestSubmit();
619
+ else
620
+ f.submit();
621
+ });
622
+ }
623
+ }
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { startTestJobServerAuth } from "./test-job-server-auth.js";
3
+ function arg(name, fallback) {
4
+ const index = process.argv.indexOf(name);
5
+ if (index === -1)
6
+ return fallback;
7
+ return process.argv[index + 1] || fallback;
8
+ }
9
+ function flag(name) {
10
+ return process.argv.includes(name);
11
+ }
12
+ if (flag("--help") || flag("-h")) {
13
+ process.stdout.write(`ProAgentStore test job server (with auth)
14
+
15
+ Usage:
16
+ pags-test-job-server-auth [--port 49211]
17
+
18
+ Routes:
19
+ GET /register Registration form
20
+ POST /register Create account
21
+ GET /login Login form
22
+ POST /login Sign in
23
+ POST /logout Sign out
24
+ GET /jobs/software-engineer Job posting (public)
25
+ GET /apply Application form (requires auth)
26
+ POST /apply Submit application (requires auth)
27
+ GET /dashboard User's applications (requires auth)
28
+ GET /success/:id Submission confirmation (requires auth)
29
+ GET /submissions All submissions (JSON, no auth)
30
+ `);
31
+ process.exit(0);
32
+ }
33
+ const port = Number(arg("--port", process.env.PAGS_TEST_JOB_AUTH_PORT || "49211"));
34
+ const server = await startTestJobServerAuth(port);
35
+ process.stdout.write(`PAGS test job server (auth) listening at ${server.url}\n`);
36
+ process.stdout.write(`Job URL: ${server.jobUrl}\n`);
37
+ process.stdout.write(`Register: ${server.registerUrl}\n`);
38
+ process.stdout.write(`Login: ${server.loginUrl}\n`);
39
+ const shutdown = async () => {
40
+ await server.close();
41
+ process.exit(0);
42
+ };
43
+ process.on("SIGINT", () => void shutdown());
44
+ process.on("SIGTERM", () => void shutdown());