@proagentstore/cli 0.1.6 → 0.1.7

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.
Files changed (2) hide show
  1. package/dist/index.js +123 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -472,6 +472,7 @@ var publishCommand = new Command4("publish").description("Publish an agent to Pr
472
472
 
473
473
  // src/commands/runner.ts
474
474
  import { spawn as spawn2 } from "child_process";
475
+ import { randomUUID } from "crypto";
475
476
  import { existsSync as existsSync4 } from "fs";
476
477
  import { resolve as resolve4 } from "path";
477
478
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -507,6 +508,12 @@ function buildRunnerArgs(opts) {
507
508
  if (opts.headless) args.push("--headless");
508
509
  return args;
509
510
  }
511
+ function buildCloudflaredArgs(localUrl) {
512
+ return ["tunnel", "--url", localUrl];
513
+ }
514
+ function parseCloudflaredTunnelUrl(text) {
515
+ return text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i)?.[0] || null;
516
+ }
510
517
  function buildRuntimeRegistrationBody(opts, capabilities = []) {
511
518
  return {
512
519
  endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
@@ -533,7 +540,7 @@ function findWorkspaceRoot() {
533
540
  function bundledRunnerPath() {
534
541
  return fileURLToPath2(new URL("./browser-runner/index.js", import.meta.url));
535
542
  }
536
- function startRunnerForeground(opts) {
543
+ function runnerSpawnSpec(opts) {
537
544
  const root = findWorkspaceRoot();
538
545
  const localPackage = resolve4(root, "packages", "browser-runner", "src", "index.ts");
539
546
  const bundledPackage = bundledRunnerPath();
@@ -549,9 +556,13 @@ function startRunnerForeground(opts) {
549
556
  command = process.execPath;
550
557
  args = [bundledPackage, ...runnerArgs];
551
558
  }
559
+ return { command, args, cwd };
560
+ }
561
+ function startRunnerForeground(opts) {
562
+ const spec = runnerSpawnSpec(opts);
552
563
  return new Promise((resolvePromise, reject) => {
553
- const child = spawn2(command, args, {
554
- cwd,
564
+ const child = spawn2(spec.command, spec.args, {
565
+ cwd: spec.cwd,
555
566
  stdio: "inherit",
556
567
  shell: process.platform === "win32"
557
568
  });
@@ -562,6 +573,20 @@ function startRunnerForeground(opts) {
562
573
  });
563
574
  });
564
575
  }
576
+ async function waitForLocalRunner(opts, timeoutMs = 15e3) {
577
+ const started = Date.now();
578
+ let lastError;
579
+ while (Date.now() - started < timeoutMs) {
580
+ try {
581
+ await requestRunner("GET", "/health", opts);
582
+ return;
583
+ } catch (error) {
584
+ lastError = error;
585
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 500));
586
+ }
587
+ }
588
+ throw new Error(`runner did not become healthy: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
589
+ }
565
590
  async function requestRunner(method, path, opts, body) {
566
591
  const headers = {
567
592
  ...runnerRequestHeaders(opts)
@@ -621,6 +646,101 @@ function createRunnerCommand() {
621
646
  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) => {
622
647
  await startRunnerForeground(opts);
623
648
  });
649
+ command.command("connect <instanceId>").description("Start a local runner, 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", "cloudflared").option("--runner-version <version>", "Runner version").option("--skip-probe", "Skip PAGS runtime probe after registration").action(async (instanceId, opts) => {
650
+ const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
651
+ const host = clean(opts.host) || "127.0.0.1";
652
+ const port = clean(opts.port) || "49171";
653
+ const localUrl = `http://${host}:${port}`;
654
+ const runnerOpts = {
655
+ ...opts,
656
+ host,
657
+ port,
658
+ token: runnerToken,
659
+ instanceId
660
+ };
661
+ const spec = runnerSpawnSpec(runnerOpts);
662
+ const runner = spawn2(spec.command, spec.args, {
663
+ cwd: spec.cwd,
664
+ stdio: ["ignore", "pipe", "pipe"],
665
+ shell: process.platform === "win32"
666
+ });
667
+ runner.stdout?.on("data", (data) => process.stdout.write(data));
668
+ runner.stderr?.on("data", (data) => process.stderr.write(data));
669
+ runner.on("exit", (code) => {
670
+ if (code && code !== 0) writeError(`runner exited with code ${code}`);
671
+ });
672
+ let tunnel = null;
673
+ const shutdown = () => {
674
+ if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
675
+ if (!runner.killed) runner.kill("SIGTERM");
676
+ };
677
+ process.once("SIGINT", () => {
678
+ shutdown();
679
+ process.exit(0);
680
+ });
681
+ process.once("SIGTERM", () => {
682
+ shutdown();
683
+ process.exit(0);
684
+ });
685
+ try {
686
+ await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId });
687
+ writeLine(`Local runner healthy at ${localUrl}`);
688
+ const tunnelUrl = await new Promise((resolveTunnel, reject) => {
689
+ let output = "";
690
+ const cloudflared = clean(opts.cloudflared) || "cloudflared";
691
+ tunnel = spawn2(cloudflared, buildCloudflaredArgs(localUrl), {
692
+ stdio: ["ignore", "pipe", "pipe"],
693
+ shell: process.platform === "win32"
694
+ });
695
+ const onData = (data) => {
696
+ const text = data.toString("utf-8");
697
+ output += text;
698
+ process.stderr.write(text);
699
+ const parsed = parseCloudflaredTunnelUrl(output);
700
+ if (parsed) resolveTunnel(parsed);
701
+ };
702
+ tunnel.stdout?.on("data", onData);
703
+ tunnel.stderr?.on("data", onData);
704
+ tunnel.on("error", reject);
705
+ tunnel.on("exit", (code) => {
706
+ if (code && code !== 0) reject(new Error(`cloudflared exited with code ${code}`));
707
+ });
708
+ setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
709
+ });
710
+ writeLine(`Tunnel ready: ${tunnelUrl}`);
711
+ await waitForLocalRunner({ url: tunnelUrl, token: runnerToken, instanceId }, 3e4);
712
+ writeLine("Tunnel health check passed");
713
+ const capabilities = await requestRunner("GET", "/capabilities", {
714
+ url: tunnelUrl,
715
+ token: runnerToken,
716
+ instanceId
717
+ });
718
+ const result = await requestPags("POST", `/v1/instances/${apiPathSegment(instanceId)}/runtime`, opts, {
719
+ endpointUrl: tunnelUrl,
720
+ token: runnerToken,
721
+ placement: "local",
722
+ capabilities: Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [],
723
+ runnerVersion: clean(opts.runnerVersion) || ""
724
+ });
725
+ writeLine(JSON.stringify(result, null, 2));
726
+ if (!opts.skipProbe) {
727
+ const status = await requestPags(
728
+ "GET",
729
+ `/v1/instances/${apiPathSegment(instanceId)}/runtime/status`,
730
+ opts
731
+ );
732
+ writeLine(JSON.stringify(status, null, 2));
733
+ }
734
+ writeLine("Connected. Keep this process running while the agent uses your local browser.");
735
+ await new Promise((resolvePromise) => {
736
+ runner.on("exit", () => resolvePromise());
737
+ tunnel?.on("exit", () => resolvePromise());
738
+ });
739
+ } catch (error) {
740
+ shutdown();
741
+ throw error;
742
+ }
743
+ });
624
744
  command.command("status").description("Check local runner health and capabilities").option("--url <url>", "Runner URL").option("--token <token>", "Runner bearer token").option("--instance-id <id>", "PAGS instance id header").action(async (opts) => {
625
745
  const health = await requestRunner("GET", "/health", opts);
626
746
  const capabilities = await requestRunner("GET", "/capabilities", opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",