@proagentstore/cli 0.1.3 → 0.1.5

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.
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync } from "node:fs";
2
3
  import { createRequire } from "node:module";
3
- import { mkdirSync } from "node:fs";
4
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { RunnerStore } from "./store.js";
7
7
  const CAPABILITIES = [
@@ -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"]);
14
+ const APPROVAL_REQUIRED_TASKS = new Set(["browser.open", "job.apply_basic"]);
15
15
  const require = createRequire(import.meta.url);
16
16
  export class RunnerInputError extends Error {
17
17
  status = 400;
@@ -32,8 +32,8 @@ export class LocalRunner {
32
32
  brainPlacement: "pags",
33
33
  runnerRole: "tool-executor",
34
34
  capabilities: CAPABILITIES,
35
- taskTypes: ["echo", "browser.open"],
36
- approvalRequiredFor: ["browser.open"],
35
+ taskTypes: ["echo", "browser.open", "job.apply_basic"],
36
+ approvalRequiredFor: [...APPROVAL_REQUIRED_TASKS],
37
37
  };
38
38
  }
39
39
  createTask(request) {
@@ -160,8 +160,36 @@ export class LocalRunner {
160
160
  title: await page.title(),
161
161
  };
162
162
  }
163
+ if (task.type === "job.apply_basic") {
164
+ return this.applyToBasicJob(task.input);
165
+ }
163
166
  throw new Error(`Unknown task type: ${task.type}`);
164
167
  }
168
+ async applyToBasicJob(input) {
169
+ const job = normalizeJobApplicationInput(input);
170
+ const context = await this.getBrowserContext();
171
+ const page = context.pages()[0] || (await context.newPage());
172
+ await page.goto(job.url, { waitUntil: "domcontentloaded", timeout: 30_000 });
173
+ const filledFields = await fillBasicApplicationForm(page, job);
174
+ const beforeSubmitUrl = page.url();
175
+ const navigation = page.waitForNavigation({
176
+ waitUntil: "domcontentloaded",
177
+ timeout: 15_000,
178
+ }).catch(() => null);
179
+ await submitApplicationForm(page);
180
+ await navigation;
181
+ await page.waitForLoadState("domcontentloaded", { timeout: 5_000 }).catch(() => undefined);
182
+ return {
183
+ taskType: "job.apply_basic",
184
+ submitted: true,
185
+ beforeSubmitUrl,
186
+ finalUrl: page.url(),
187
+ title: await page.title(),
188
+ fieldsFilled: filledFields,
189
+ resumeFile: basename(job.resumePath),
190
+ visibleText: (await page.locator("body").innerText({ timeout: 5_000 }).catch(() => "")).slice(0, 2_000),
191
+ };
192
+ }
165
193
  requireTask(id) {
166
194
  const task = this.store.getTask(id);
167
195
  if (!task)
@@ -202,9 +230,164 @@ export class LocalRunner {
202
230
  }
203
231
  }
204
232
  }
233
+ export function normalizeJobApplicationInput(input) {
234
+ const url = stringValue(input.url);
235
+ if (!url || !/^https?:\/\//.test(url)) {
236
+ throw new RunnerInputError("job.apply_basic requires an http(s) url");
237
+ }
238
+ const resumePath = resolve(stringValue(input.resumePath));
239
+ if (!resumePath || !existsSync(resumePath)) {
240
+ throw new RunnerInputError("job.apply_basic requires an existing resumePath");
241
+ }
242
+ const candidate = isRecord(input.candidate) ? input.candidate : {};
243
+ const fullName = stringValue(candidate.fullName);
244
+ const email = stringValue(candidate.email);
245
+ if (!fullName)
246
+ throw new RunnerInputError("candidate.fullName is required");
247
+ if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
248
+ throw new RunnerInputError("candidate.email must be a valid email address");
249
+ }
250
+ return {
251
+ url,
252
+ resumePath,
253
+ candidate: {
254
+ fullName,
255
+ email,
256
+ phone: optionalString(candidate.phone),
257
+ location: optionalString(candidate.location),
258
+ linkedin: optionalString(candidate.linkedin),
259
+ portfolio: optionalString(candidate.portfolio),
260
+ workAuthorization: optionalString(candidate.workAuthorization),
261
+ },
262
+ coverNote: optionalString(input.coverNote),
263
+ };
264
+ }
265
+ async function fillBasicApplicationForm(page, job) {
266
+ const filled = [];
267
+ const candidate = job.candidate;
268
+ const fillSpecs = [
269
+ ["fullName", candidate.fullName, [
270
+ 'input[name="fullName"]',
271
+ 'input[name="full_name"]',
272
+ 'input[name="name"]',
273
+ 'input[autocomplete="name"]',
274
+ ]],
275
+ ["email", candidate.email, [
276
+ 'input[name="email"]',
277
+ 'input[type="email"]',
278
+ 'input[autocomplete="email"]',
279
+ ]],
280
+ ["phone", candidate.phone, [
281
+ 'input[name="phone"]',
282
+ 'input[name="mobile"]',
283
+ 'input[autocomplete="tel"]',
284
+ ]],
285
+ ["location", candidate.location, [
286
+ 'input[name="location"]',
287
+ 'input[autocomplete="address-level2"]',
288
+ 'input[autocomplete="address-level1"]',
289
+ ]],
290
+ ["linkedin", candidate.linkedin, [
291
+ 'input[name="linkedin"]',
292
+ 'input[name="linkedIn"]',
293
+ 'input[placeholder*="LinkedIn" i]',
294
+ ]],
295
+ ["portfolio", candidate.portfolio, [
296
+ 'input[name="portfolio"]',
297
+ 'input[name="website"]',
298
+ 'input[placeholder*="Portfolio" i]',
299
+ 'input[placeholder*="Website" i]',
300
+ ]],
301
+ ["coverNote", job.coverNote, [
302
+ 'textarea[name="coverNote"]',
303
+ 'textarea[name="cover_letter"]',
304
+ 'textarea[name="coverLetter"]',
305
+ 'textarea[placeholder*="cover" i]',
306
+ 'textarea[placeholder*="message" i]',
307
+ ]],
308
+ ];
309
+ for (const [name, value, selectors] of fillSpecs) {
310
+ if (value && await fillFirst(page, selectors, value))
311
+ filled.push(name);
312
+ }
313
+ if (candidate.workAuthorization && await selectOrFillFirst(page, [
314
+ 'select[name="workAuthorization"]',
315
+ 'select[name="work_authorization"]',
316
+ 'select[name*="authorization" i]',
317
+ ], candidate.workAuthorization)) {
318
+ filled.push("workAuthorization");
319
+ }
320
+ const resumeInput = page.locator('input[type="file"][name*="resume" i], input[type="file"][name*="cv" i], input[type="file"]').first();
321
+ if (await resumeInput.count() === 0) {
322
+ throw new Error("No resume upload field found on the application form");
323
+ }
324
+ await resumeInput.setInputFiles(job.resumePath);
325
+ filled.push("resume");
326
+ return filled;
327
+ }
328
+ async function fillFirst(page, selectors, value) {
329
+ for (const selector of selectors) {
330
+ const locator = page.locator(selector).first();
331
+ if (await locator.count() === 0)
332
+ continue;
333
+ try {
334
+ await locator.fill(value, { timeout: 5_000 });
335
+ return true;
336
+ }
337
+ catch {
338
+ /* try next selector */
339
+ }
340
+ }
341
+ return false;
342
+ }
343
+ async function selectOrFillFirst(page, selectors, value) {
344
+ for (const selector of selectors) {
345
+ const locator = page.locator(selector).first();
346
+ if (await locator.count() === 0)
347
+ continue;
348
+ try {
349
+ await locator.selectOption({ label: value }, { timeout: 5_000 });
350
+ return true;
351
+ }
352
+ catch {
353
+ try {
354
+ await locator.fill(value, { timeout: 5_000 });
355
+ return true;
356
+ }
357
+ catch {
358
+ /* try next selector */
359
+ }
360
+ }
361
+ }
362
+ return false;
363
+ }
364
+ async function submitApplicationForm(page) {
365
+ const submit = page.locator('form button[type="submit"], form input[type="submit"], button:has-text("Submit"), input[type="submit"]').first();
366
+ if (await submit.count() > 0) {
367
+ await submit.click({ timeout: 10_000 });
368
+ return;
369
+ }
370
+ const form = page.locator("form").first();
371
+ if (await form.count() === 0)
372
+ throw new Error("No application form found to submit");
373
+ await form.evaluate((node) => {
374
+ const htmlForm = node;
375
+ if (typeof htmlForm.requestSubmit === "function")
376
+ htmlForm.requestSubmit();
377
+ else
378
+ htmlForm.submit();
379
+ });
380
+ }
205
381
  function isRecord(value) {
206
382
  return typeof value === "object" && value !== null && !Array.isArray(value);
207
383
  }
384
+ function stringValue(value) {
385
+ return String(value || "").trim();
386
+ }
387
+ function optionalString(value) {
388
+ const text = stringValue(value);
389
+ return text || undefined;
390
+ }
208
391
  function normalizeCreateTaskRequest(request) {
209
392
  if (!isRecord(request) || typeof request.type !== "string" || !request.type.trim()) {
210
393
  throw new RunnerInputError("task type required");
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createRequire } from "module";
5
- import { Command as Command5 } from "commander";
5
+ import { Command as Command6 } from "commander";
6
6
 
7
7
  // src/commands/check.ts
8
8
  import { existsSync, readFileSync } from "fs";
@@ -363,12 +363,36 @@ jobs:
363
363
  writeLine();
364
364
  });
365
365
 
366
+ // src/commands/mcp.ts
367
+ import { spawn } from "child_process";
368
+ import { Command as Command3 } from "commander";
369
+ var DEFAULT_MCP_URL = "https://mcp.proagentstore.online/mcp";
370
+ function buildMcpRemoteArgs(opts, extraArgs = []) {
371
+ return ["-y", "mcp-remote", opts.url || DEFAULT_MCP_URL, ...extraArgs];
372
+ }
373
+ async function runMcpProxy(opts, extraArgs = []) {
374
+ const child = spawn("npx", buildMcpRemoteArgs(opts, extraArgs), {
375
+ stdio: "inherit",
376
+ env: process.env
377
+ });
378
+ await new Promise((resolve5, reject) => {
379
+ child.on("error", reject);
380
+ child.on("close", (code) => {
381
+ if (code && code !== 0) reject(new Error(`mcp proxy exited with code ${code}`));
382
+ else resolve5();
383
+ });
384
+ });
385
+ }
386
+ var mcpCommand = new Command3("mcp").description("Run a local stdio proxy for the official ProAgentStore MCP server").option("--url <url>", "Remote MCP endpoint", DEFAULT_MCP_URL).argument("[args...]", "Extra arguments passed to mcp-remote").action(async (args, opts) => {
387
+ await runMcpProxy(opts, args);
388
+ });
389
+
366
390
  // src/commands/publish.ts
367
391
  import { execFileSync } from "child_process";
368
392
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
369
393
  import { join as join3, resolve as resolve3 } from "path";
370
- import { Command as Command3 } from "commander";
371
- var publishCommand = new Command3("publish").description("Publish an agent to ProAgentStore").option("-d, --dir <path>", "Agent directory", ".").action(async (opts) => {
394
+ import { Command as Command4 } from "commander";
395
+ var publishCommand = new Command4("publish").description("Publish an agent to ProAgentStore").option("-d, --dir <path>", "Agent directory", ".").action(async (opts) => {
372
396
  const dir = resolve3(opts.dir);
373
397
  const manifestPath = join3(dir, "agent.json");
374
398
  if (!existsSync3(manifestPath)) {
@@ -447,11 +471,11 @@ var publishCommand = new Command3("publish").description("Publish an agent to Pr
447
471
  });
448
472
 
449
473
  // src/commands/runner.ts
450
- import { spawn } from "child_process";
474
+ import { spawn as spawn2 } from "child_process";
451
475
  import { existsSync as existsSync4 } from "fs";
452
476
  import { resolve as resolve4 } from "path";
453
477
  import { fileURLToPath as fileURLToPath2 } from "url";
454
- import { Command as Command4 } from "commander";
478
+ import { Command as Command5 } from "commander";
455
479
  var runnerCommand = createRunnerCommand();
456
480
  function runnerBaseUrl(url) {
457
481
  return (clean(url) || clean(process.env.PAGS_RUNNER_URL) || "http://127.0.0.1:49171").replace(/\/$/, "");
@@ -526,7 +550,7 @@ function startRunnerForeground(opts) {
526
550
  args = [bundledPackage, ...runnerArgs];
527
551
  }
528
552
  return new Promise((resolvePromise, reject) => {
529
- const child = spawn(command, args, {
553
+ const child = spawn2(command, args, {
530
554
  cwd,
531
555
  stdio: "inherit",
532
556
  shell: process.platform === "win32"
@@ -591,7 +615,7 @@ function collectCapability(value, previous = []) {
591
615
  return [...previous, value];
592
616
  }
593
617
  function createRunnerCommand() {
594
- const command = new Command4("runner").description(
618
+ const command = new Command5("runner").description(
595
619
  "Manage a local ProAgentStore browser runner"
596
620
  );
597
621
  command.command("start").description("Start the local browser runner in the foreground").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Require this bearer token").option("--instance-id <id>", "Bind runner requests to a PAGS instance id").option("--headless", "Run Playwright headless").action(async (opts) => {
@@ -709,7 +733,7 @@ function createRunnerCommand() {
709
733
  // src/index.ts
710
734
  var require2 = createRequire(import.meta.url);
711
735
  var { version } = require2("../package.json");
712
- var program = new Command5();
736
+ var program = new Command6();
713
737
  program.name("pags").description(
714
738
  "ProAgentStore CLI \u2014 create and publish server-powered AI agents"
715
739
  ).version(version);
@@ -717,6 +741,7 @@ program.addCommand(initCommand);
717
741
  program.addCommand(checkCommand);
718
742
  program.addCommand(publishCommand);
719
743
  program.addCommand(runnerCommand);
744
+ program.addCommand(mcpCommand);
720
745
  try {
721
746
  await program.parseAsync();
722
747
  } catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.1.3",
4
- "description": "CLI for creating and publishing ProAgentStore agents",
3
+ "version": "0.1.5",
4
+ "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "pags",
19
19
  "cli",
20
20
  "agents",
21
+ "mcp",
21
22
  "browser-runner"
22
23
  ],
23
24
  "publishConfig": {