@proagentstore/cli 0.1.16 → 0.1.18
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 +12 -2
- package/dist/index.js +130 -20
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ export class LocalRunner {
|
|
|
40
40
|
}
|
|
41
41
|
createTask(request) {
|
|
42
42
|
const normalized = normalizeCreateTaskRequest(request);
|
|
43
|
+
validateTaskInput(normalized.type, normalized.input);
|
|
43
44
|
const now = new Date().toISOString();
|
|
44
45
|
const requiresApproval = normalized.requiresApproval || APPROVAL_REQUIRED_TASKS.has(normalized.type);
|
|
45
46
|
const task = {
|
|
@@ -354,11 +355,11 @@ export class LocalRunner {
|
|
|
354
355
|
export function normalizeJobApplicationInput(input) {
|
|
355
356
|
const url = stringValue(input.url);
|
|
356
357
|
if (!url || !/^https?:\/\//.test(url)) {
|
|
357
|
-
throw new RunnerInputError("job
|
|
358
|
+
throw new RunnerInputError("job application requires an http(s) url");
|
|
358
359
|
}
|
|
359
360
|
const resumePath = resolve(stringValue(input.resumePath));
|
|
360
361
|
if (!resumePath || !existsSync(resumePath)) {
|
|
361
|
-
throw new RunnerInputError("job
|
|
362
|
+
throw new RunnerInputError("job application requires an existing local resumePath");
|
|
362
363
|
}
|
|
363
364
|
const candidate = isRecord(input.candidate) ? input.candidate : {};
|
|
364
365
|
const fullName = stringValue(candidate.fullName);
|
|
@@ -522,6 +523,15 @@ function normalizeCreateTaskRequest(request) {
|
|
|
522
523
|
: "",
|
|
523
524
|
};
|
|
524
525
|
}
|
|
526
|
+
function validateTaskInput(type, input) {
|
|
527
|
+
if (type === "job.apply_basic") {
|
|
528
|
+
normalizeJobApplicationInput(input);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (type === "job.apply_authenticated") {
|
|
532
|
+
normalizeAuthenticatedJobInput(input);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
525
535
|
export function fileUrl(path) {
|
|
526
536
|
return pathToFileURL(path).toString();
|
|
527
537
|
}
|
package/dist/index.js
CHANGED
|
@@ -614,6 +614,9 @@ var publishCommand = new Command5("publish").description("Publish an agent to Pr
|
|
|
614
614
|
import { spawn as spawn2 } from "child_process";
|
|
615
615
|
import { randomUUID } from "crypto";
|
|
616
616
|
import { existsSync as existsSync5 } from "fs";
|
|
617
|
+
import { chmod, mkdir, writeFile } from "fs/promises";
|
|
618
|
+
import { gunzipSync } from "zlib";
|
|
619
|
+
import { arch, homedir as homedir2, platform } from "os";
|
|
617
620
|
import { resolve as resolve4 } from "path";
|
|
618
621
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
619
622
|
import { Command as Command6 } from "commander";
|
|
@@ -651,6 +654,112 @@ function buildRunnerArgs(opts) {
|
|
|
651
654
|
function buildCloudflaredArgs(localUrl) {
|
|
652
655
|
return ["tunnel", "--url", localUrl];
|
|
653
656
|
}
|
|
657
|
+
function cloudflaredAssetForPlatform(os = platform(), cpu = arch()) {
|
|
658
|
+
if (os === "darwin") {
|
|
659
|
+
if (cpu === "arm64") {
|
|
660
|
+
return {
|
|
661
|
+
asset: "cloudflared-darwin-arm64.tgz",
|
|
662
|
+
executableName: "cloudflared",
|
|
663
|
+
archive: true
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (cpu === "x64") {
|
|
667
|
+
return {
|
|
668
|
+
asset: "cloudflared-darwin-amd64.tgz",
|
|
669
|
+
executableName: "cloudflared",
|
|
670
|
+
archive: true
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (os === "linux") {
|
|
675
|
+
if (cpu === "x64") {
|
|
676
|
+
return {
|
|
677
|
+
asset: "cloudflared-linux-amd64",
|
|
678
|
+
executableName: "cloudflared",
|
|
679
|
+
archive: false
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (cpu === "arm64") {
|
|
683
|
+
return {
|
|
684
|
+
asset: "cloudflared-linux-arm64",
|
|
685
|
+
executableName: "cloudflared",
|
|
686
|
+
archive: false
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (os === "win32" && cpu === "x64") {
|
|
691
|
+
return {
|
|
692
|
+
asset: "cloudflared-windows-amd64.exe",
|
|
693
|
+
executableName: "cloudflared.exe",
|
|
694
|
+
archive: false
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Automatic cloudflared download is not supported on ${os}/${cpu}. Install cloudflared and pass --cloudflared <path>.`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
function cloudflaredDownloadUrl(asset) {
|
|
702
|
+
return `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`;
|
|
703
|
+
}
|
|
704
|
+
function extractCloudflaredBinary(asset, bytes) {
|
|
705
|
+
const buffer = Buffer.from(bytes);
|
|
706
|
+
if (!asset.archive) return buffer;
|
|
707
|
+
const tar = gunzipSync(buffer);
|
|
708
|
+
for (let offset = 0; offset + 512 <= tar.length; ) {
|
|
709
|
+
const header = tar.subarray(offset, offset + 512);
|
|
710
|
+
if (header.every((byte) => byte === 0)) break;
|
|
711
|
+
const rawName = header.subarray(0, 100).toString("utf8");
|
|
712
|
+
const name = rawName.slice(0, rawName.indexOf("\0") === -1 ? void 0 : rawName.indexOf("\0"));
|
|
713
|
+
const rawSize = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim();
|
|
714
|
+
const size = Number.parseInt(rawSize || "0", 8);
|
|
715
|
+
const contentStart = offset + 512;
|
|
716
|
+
const contentEnd = contentStart + size;
|
|
717
|
+
if (name.split("/").pop() === asset.executableName) {
|
|
718
|
+
return tar.subarray(contentStart, contentEnd);
|
|
719
|
+
}
|
|
720
|
+
offset = contentStart + Math.ceil(size / 512) * 512;
|
|
721
|
+
}
|
|
722
|
+
throw new Error(`Downloaded ${asset.asset} did not contain ${asset.executableName}`);
|
|
723
|
+
}
|
|
724
|
+
function cloudflaredCacheDir() {
|
|
725
|
+
return resolve4(homedir2(), ".config", "proagentstore", "bin");
|
|
726
|
+
}
|
|
727
|
+
function cachedCloudflaredPath(asset) {
|
|
728
|
+
const suffix = asset.asset.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
729
|
+
return resolve4(cloudflaredCacheDir(), `${asset.executableName}-${suffix}`);
|
|
730
|
+
}
|
|
731
|
+
async function commandAvailable(command) {
|
|
732
|
+
return new Promise((resolvePromise) => {
|
|
733
|
+
const child = spawn2(command, ["--version"], {
|
|
734
|
+
stdio: "ignore",
|
|
735
|
+
shell: process.platform === "win32"
|
|
736
|
+
});
|
|
737
|
+
child.on("error", () => resolvePromise(false));
|
|
738
|
+
child.on("exit", (code) => resolvePromise(code === 0));
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async function downloadCloudflared(asset) {
|
|
742
|
+
const target = cachedCloudflaredPath(asset);
|
|
743
|
+
if (existsSync5(target)) return target;
|
|
744
|
+
await mkdir(cloudflaredCacheDir(), { recursive: true });
|
|
745
|
+
const url = cloudflaredDownloadUrl(asset.asset);
|
|
746
|
+
writeLine(`cloudflared not found; downloading ${asset.asset}...`);
|
|
747
|
+
const res = await fetch(url);
|
|
748
|
+
if (!res.ok) {
|
|
749
|
+
throw new Error(`cloudflared download failed: ${res.status} ${res.statusText}`);
|
|
750
|
+
}
|
|
751
|
+
const binary = extractCloudflaredBinary(asset, new Uint8Array(await res.arrayBuffer()));
|
|
752
|
+
await writeFile(target, binary);
|
|
753
|
+
if (process.platform !== "win32") await chmod(target, 493);
|
|
754
|
+
writeLine(`cloudflared cached at ${target}`);
|
|
755
|
+
return target;
|
|
756
|
+
}
|
|
757
|
+
async function resolveCloudflaredCommand(requested) {
|
|
758
|
+
const explicit = clean(requested);
|
|
759
|
+
if (explicit && explicit !== "cloudflared") return explicit;
|
|
760
|
+
if (await commandAvailable("cloudflared")) return "cloudflared";
|
|
761
|
+
return downloadCloudflared(cloudflaredAssetForPlatform());
|
|
762
|
+
}
|
|
654
763
|
function parseCloudflaredTunnelUrl(text) {
|
|
655
764
|
return text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i)?.[0] || null;
|
|
656
765
|
}
|
|
@@ -786,7 +895,7 @@ function createRunnerCommand() {
|
|
|
786
895
|
command.command("start").description("Start the local FAGS browser runtime 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) => {
|
|
787
896
|
await startRunnerForeground(opts);
|
|
788
897
|
});
|
|
789
|
-
command.command("connect <instanceId>").description("Start the local FAGS runtime, open a Cloudflare quick tunnel, and register it with PAGS").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>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--cloudflared <path>", "cloudflared executable"
|
|
898
|
+
command.command("connect <instanceId>").description("Start the local FAGS runtime, open a Cloudflare quick tunnel, and register it with PAGS").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>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--cloudflared <path>", "cloudflared executable").option("--runner-version <version>", "Runner version").option("--skip-probe", "Skip PAGS FAGS-runtime probe after registration").action(async (instanceId, opts) => {
|
|
790
899
|
const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
|
|
791
900
|
const host = clean(opts.host) || "127.0.0.1";
|
|
792
901
|
const port = clean(opts.port) || "49171";
|
|
@@ -827,24 +936,25 @@ function createRunnerCommand() {
|
|
|
827
936
|
writeLine(`Local FAGS runtime healthy at ${localUrl}`);
|
|
828
937
|
const tunnelUrl = await new Promise((resolveTunnel, reject) => {
|
|
829
938
|
let output = "";
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
939
|
+
resolveCloudflaredCommand(opts.cloudflared).then((cloudflared) => {
|
|
940
|
+
tunnel = spawn2(cloudflared, buildCloudflaredArgs(localUrl), {
|
|
941
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
942
|
+
shell: process.platform === "win32"
|
|
943
|
+
});
|
|
944
|
+
const onData = (data) => {
|
|
945
|
+
const text = data.toString("utf-8");
|
|
946
|
+
output += text;
|
|
947
|
+
process.stderr.write(text);
|
|
948
|
+
const parsed = parseCloudflaredTunnelUrl(output);
|
|
949
|
+
if (parsed) resolveTunnel(parsed);
|
|
950
|
+
};
|
|
951
|
+
tunnel.stdout?.on("data", onData);
|
|
952
|
+
tunnel.stderr?.on("data", onData);
|
|
953
|
+
tunnel.on("error", reject);
|
|
954
|
+
tunnel.on("exit", (code) => {
|
|
955
|
+
if (code && code !== 0) reject(new Error(`cloudflared exited with code ${code}`));
|
|
956
|
+
});
|
|
957
|
+
}).catch(reject);
|
|
848
958
|
setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
|
|
849
959
|
});
|
|
850
960
|
writeLine(`Tunnel ready: ${tunnelUrl}`);
|
|
@@ -853,7 +963,7 @@ function createRunnerCommand() {
|
|
|
853
963
|
token: runnerToken,
|
|
854
964
|
instanceId
|
|
855
965
|
});
|
|
856
|
-
|
|
966
|
+
await requestPags("POST", `/v1/instances/${apiPathSegment(instanceId)}/runtime`, opts, {
|
|
857
967
|
endpointUrl: tunnelUrl,
|
|
858
968
|
token: runnerToken,
|
|
859
969
|
placement: "local",
|