@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.
- package/dist/browser-runner/runner.js +188 -5
- package/dist/index.js +33 -8
- package/package.json +3 -2
|
@@ -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 {
|
|
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: [
|
|
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
|
|
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
|
|
371
|
-
var publishCommand = new
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "CLI for creating and
|
|
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": {
|