@proagentstore/cli 0.2.11 → 0.3.1

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.
@@ -377,7 +377,9 @@ export class LocalRunner {
377
377
  this.store.putTask(task);
378
378
  this.addTaskEvent(task, "task.completed", `Task completed: ${task.type}`, output);
379
379
  await this.endTakeover(taskId);
380
- return { submitted: true, output };
380
+ // This path only CLEARED a challenge — the form was NOT submitted (output.submitted
381
+ // is false). Report it accurately so the console doesn't show a false "submitted".
382
+ return { submitted: false, resumed: true, output };
381
383
  }
382
384
  /**
383
385
  * Always attach files via Playwright, never a native OS dialog: intercept any
@@ -584,6 +586,19 @@ export class LocalRunner {
584
586
  }
585
587
  });
586
588
  }
589
+ /** Close every page except the active one. ATS forms open popups/extra tabs that
590
+ * otherwise stay open in the persistent context until shutdown; over many
591
+ * applications they accumulate (memory/handles). Called when a task finishes. */
592
+ async closeStalePages() {
593
+ const ctx = this.browserContext;
594
+ if (!ctx)
595
+ return;
596
+ for (const p of ctx.pages()) {
597
+ if (p !== this.activePage && !p.isClosed()) {
598
+ await p.close().catch(() => undefined);
599
+ }
600
+ }
601
+ }
587
602
  /** The page the brain drives and the human takes over — created on demand. */
588
603
  async getActivePage() {
589
604
  const context = await this.getBrowserContext();
@@ -716,28 +731,39 @@ export class LocalRunner {
716
731
  /** Whether the brain can resume: a solved captcha (auto), a human "Resume" for a stuck step, or a provided value. */
717
732
  async browserHandoffStatus(taskId) {
718
733
  const session = this.takeovers.get(taskId);
719
- const page = session?.page ?? (await this.getActivePage());
734
+ // No live takeover session (the runner restarted mid-handoff, or it expired): stay
735
+ // UNRESOLVED. The old code fell through and relaunched a blank page via
736
+ // getActivePage(), which detected no challenge and reported solved:true — so the
737
+ // brain resumed the application WITHOUT the human's captcha/value. A status poll
738
+ // must never relaunch the browser or claim a lost handoff is done.
739
+ if (!session)
740
+ return { solved: false, challenge: null };
741
+ const page = session.page;
720
742
  if (page.isClosed())
721
743
  return { solved: true, challenge: null };
722
744
  // A needs_input handoff resumes once the user supplies the value.
723
- if (session?.reason === "needs_input")
745
+ if (session.reason === "needs_input")
724
746
  return { solved: !!session.inputValue, challenge: null, value: session.inputValue };
725
747
  // A stuck handoff resumes only when the human explicitly clicks Resume —
726
748
  // there's nothing to auto-detect.
727
- if (session?.reason === "stuck")
749
+ if (session.reason === "stuck")
728
750
  return { solved: !!session.humanDone, challenge: null };
729
751
  // A challenge resumes when the token/widget clears OR the human clicks Done —
730
752
  // custom captchas (e.g. PageUp's "not a robot") have no detectable token, so
731
753
  // the human's explicit Done is the authority; never strand them.
732
754
  const challenge = await detectHumanChallenge(page);
733
- const solved = !challenge || (await challengeSolved(page)) || !!session?.humanDone;
755
+ const solved = !challenge || (await challengeSolved(page)) || !!session.humanDone;
734
756
  return { solved, challenge };
735
757
  }
736
758
  /** The user supplied the value the agent asked for (ask-and-hold). */
737
759
  browserSubmitInput(taskId, value) {
738
760
  const session = this.takeovers.get(taskId);
739
- if (session)
740
- session.inputValue = value;
761
+ // No live takeover (session expired / runner restarted): the value can't be
762
+ // delivered. Don't discard it silently AND resurrect the already-terminal task as
763
+ // "running" — report failure so the console can say "session expired, re-run".
764
+ if (!session)
765
+ return { ok: false };
766
+ session.inputValue = value;
741
767
  const task = this.store.getTask(taskId);
742
768
  if (task) {
743
769
  task.status = "running";
@@ -777,6 +803,7 @@ export class LocalRunner {
777
803
  this.store.putTask(task);
778
804
  this.addTaskEvent(task, success ? "task.completed" : "task.failed", detail || `Application ${outcome}`, { outcome, detail });
779
805
  }
806
+ await this.closeStalePages().catch(() => undefined);
780
807
  return { ok: true };
781
808
  }
782
809
  ensureChromiumInstalled() {
package/dist/index.js CHANGED
@@ -171,6 +171,7 @@ var initCommand = new Command2("init").description("Scaffold a new ProAgentStore
171
171
  category: "general",
172
172
  model: "@cf/meta/llama-3.2-3b-instruct",
173
173
  template,
174
+ capabilities: { surfaces: [], runtime: null, workflow: null },
174
175
  serverConfig: {
175
176
  durableObject: template === "worker",
176
177
  cron: template === "cron" ? "0 8 * * *" : void 0,
@@ -614,9 +615,7 @@ var publishCommand = new Command5("publish").description("Publish an agent to Pr
614
615
  import { spawn as spawn2 } from "child_process";
615
616
  import { randomUUID } from "crypto";
616
617
  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, hostname, platform } from "os";
618
+ import { hostname } from "os";
620
619
  import { resolve as resolve4 } from "path";
621
620
  import { createServer as createServer2 } from "net";
622
621
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -672,122 +671,6 @@ function buildRunnerArgs(opts) {
672
671
  if (opts.headless) args.push("--headless");
673
672
  return args;
674
673
  }
675
- function buildCloudflaredArgs(localUrl) {
676
- return ["tunnel", "--url", localUrl];
677
- }
678
- function cloudflaredAssetForPlatform(os = platform(), cpu = arch()) {
679
- if (os === "darwin") {
680
- if (cpu === "arm64") {
681
- return {
682
- asset: "cloudflared-darwin-arm64.tgz",
683
- executableName: "cloudflared",
684
- archive: true
685
- };
686
- }
687
- if (cpu === "x64") {
688
- return {
689
- asset: "cloudflared-darwin-amd64.tgz",
690
- executableName: "cloudflared",
691
- archive: true
692
- };
693
- }
694
- }
695
- if (os === "linux") {
696
- if (cpu === "x64") {
697
- return {
698
- asset: "cloudflared-linux-amd64",
699
- executableName: "cloudflared",
700
- archive: false
701
- };
702
- }
703
- if (cpu === "arm64") {
704
- return {
705
- asset: "cloudflared-linux-arm64",
706
- executableName: "cloudflared",
707
- archive: false
708
- };
709
- }
710
- }
711
- if (os === "win32" && cpu === "x64") {
712
- return {
713
- asset: "cloudflared-windows-amd64.exe",
714
- executableName: "cloudflared.exe",
715
- archive: false
716
- };
717
- }
718
- throw new Error(
719
- `Automatic cloudflared download is not supported on ${os}/${cpu}. Install cloudflared and pass --cloudflared <path>.`
720
- );
721
- }
722
- function cloudflaredDownloadUrl(asset) {
723
- return `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`;
724
- }
725
- function extractCloudflaredBinary(asset, bytes) {
726
- const buffer = Buffer.from(bytes);
727
- if (!asset.archive) return buffer;
728
- const tar = gunzipSync(buffer);
729
- for (let offset = 0; offset + 512 <= tar.length; ) {
730
- const header = tar.subarray(offset, offset + 512);
731
- if (header.every((byte) => byte === 0)) break;
732
- const rawName = header.subarray(0, 100).toString("utf8");
733
- const name = rawName.slice(0, rawName.indexOf("\0") === -1 ? void 0 : rawName.indexOf("\0"));
734
- const rawSize = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim();
735
- const size = Number.parseInt(rawSize || "0", 8);
736
- const contentStart = offset + 512;
737
- const contentEnd = contentStart + size;
738
- if (name.split("/").pop() === asset.executableName) {
739
- return tar.subarray(contentStart, contentEnd);
740
- }
741
- offset = contentStart + Math.ceil(size / 512) * 512;
742
- }
743
- throw new Error(`Downloaded ${asset.asset} did not contain ${asset.executableName}`);
744
- }
745
- function cloudflaredCacheDir() {
746
- return resolve4(homedir2(), ".config", "proagentstore", "bin");
747
- }
748
- function cachedCloudflaredPath(asset) {
749
- const suffix = asset.asset.replace(/[^a-zA-Z0-9._-]/g, "-");
750
- return resolve4(cloudflaredCacheDir(), `${asset.executableName}-${suffix}`);
751
- }
752
- async function commandAvailable(command) {
753
- return new Promise((resolvePromise) => {
754
- const child = spawn2(command, ["--version"], {
755
- stdio: "ignore",
756
- shell: process.platform === "win32"
757
- });
758
- child.on("error", () => resolvePromise(false));
759
- child.on("exit", (code) => resolvePromise(code === 0));
760
- });
761
- }
762
- async function downloadCloudflared(asset) {
763
- const target = cachedCloudflaredPath(asset);
764
- if (existsSync5(target)) return target;
765
- await mkdir(cloudflaredCacheDir(), { recursive: true });
766
- const url = cloudflaredDownloadUrl(asset.asset);
767
- writeLine(`cloudflared not found; downloading ${asset.asset}...`);
768
- const res = await fetch(url);
769
- if (!res.ok) {
770
- throw new Error(`cloudflared download failed: ${res.status} ${res.statusText}`);
771
- }
772
- const binary = extractCloudflaredBinary(asset, new Uint8Array(await res.arrayBuffer()));
773
- await writeFile(target, binary);
774
- if (process.platform !== "win32") await chmod(target, 493);
775
- writeLine(`cloudflared cached at ${target}`);
776
- return target;
777
- }
778
- async function resolveCloudflaredCommand(requested) {
779
- const explicit = clean(requested);
780
- if (explicit && explicit !== "cloudflared") return explicit;
781
- if (await commandAvailable("cloudflared")) return "cloudflared";
782
- return downloadCloudflared(cloudflaredAssetForPlatform());
783
- }
784
- function parseCloudflaredTunnelUrl(text) {
785
- const matches = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/gi) || [];
786
- for (const m of matches) {
787
- if (!/^https:\/\/api\.trycloudflare\.com/i.test(m)) return m;
788
- }
789
- return null;
790
- }
791
674
  function buildRuntimeRegistrationBody(opts, capabilities = []) {
792
675
  return {
793
676
  endpointUrl: clean(opts.endpointUrl) || opts.endpointUrl,
@@ -1043,18 +926,13 @@ function createRunnerCommand() {
1043
926
  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) => {
1044
927
  await startRunnerForeground(opts);
1045
928
  });
1046
- command.command("connect <instanceIds...>").description("Start ONE local runtime, open a Cloudflare quick tunnel, and register it for every given PAGS instance").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").option("--tunnel <mode>", "Tunnel mode: 'ws' (WebSocket relay, default), 'named' (stable hostname), or 'quick' (cloudflared)", "ws").option("--force", "Take over from another connected machine").action(async (instanceIds, opts) => {
929
+ command.command("connect <instanceIds...>").description("Start ONE local runtime, connect via WebSocket relay, and register it for every given PAGS instance").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("--runner-version <version>", "Runner version").option("--force", "Take over from another connected machine").action(async (instanceIds, opts) => {
1047
930
  const runnerToken = clean(opts.token) || clean(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID()}`;
1048
931
  const host = clean(opts.host) || "127.0.0.1";
1049
932
  const port = clean(opts.port) || String(await findFreePort2(49171));
1050
933
  const localUrl = `http://${host}:${port}`;
1051
934
  const primary = instanceIds[0];
1052
- const runnerOpts = {
1053
- ...opts,
1054
- host,
1055
- port,
1056
- token: runnerToken
1057
- };
935
+ const runnerOpts = { ...opts, host, port, token: runnerToken };
1058
936
  const spec = runnerSpawnSpec(runnerOpts);
1059
937
  const runner = spawn2(spec.command, spec.args, {
1060
938
  cwd: spec.cwd,
@@ -1066,9 +944,7 @@ function createRunnerCommand() {
1066
944
  runner.on("exit", (code) => {
1067
945
  if (code && code !== 0) writeError(`runner exited with code ${code}`);
1068
946
  });
1069
- let tunnel = null;
1070
947
  const shutdown = () => {
1071
- if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
1072
948
  if (!runner.killed) runner.kill("SIGTERM");
1073
949
  };
1074
950
  process.once("SIGINT", () => {
@@ -1082,193 +958,10 @@ function createRunnerCommand() {
1082
958
  try {
1083
959
  await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId: primary });
1084
960
  writeLine(`Local FAGS runtime healthy at ${localUrl}`);
1085
- if (opts.tunnel === "ws") {
1086
- await connectViaRelay(instanceIds, localUrl, runnerToken, opts, Boolean(opts.force));
1087
- await new Promise((resolvePromise) => {
1088
- runner.on("exit", () => resolvePromise());
1089
- });
1090
- return;
1091
- }
1092
- if (opts.tunnel === "named") {
1093
- writeLine("Provisioning named tunnel\u2026");
1094
- const provRes = await requestPags("POST", "/v1/tunnel/provision", opts, {});
1095
- const prov = provRes;
1096
- if (!prov.connectorToken) {
1097
- writeError(`Named tunnel provisioning failed: ${prov.error || "no token returned"}. Falling back to quick tunnel.`);
1098
- } else {
1099
- const tunnelUrl2 = prov.endpointUrl || `https://${prov.hostname}`;
1100
- writeLine(`Named tunnel: ${tunnelUrl2}`);
1101
- const cloudflaredPath2 = await resolveCloudflaredCommand(opts.cloudflared);
1102
- const tunnelProc = spawn2(cloudflaredPath2, ["tunnel", "run", "--token", prov.connectorToken], {
1103
- stdio: ["ignore", "pipe", "pipe"],
1104
- shell: process.platform === "win32"
1105
- });
1106
- tunnel = tunnelProc;
1107
- tunnelProc.stdout?.on("data", (d2) => {
1108
- const t = d2.toString("utf-8");
1109
- if (/registered/.test(t) || /CONNECTED/.test(t)) writeLine("Named tunnel connected \u2713");
1110
- });
1111
- tunnelProc.stderr?.on("data", (d2) => {
1112
- const t = d2.toString("utf-8");
1113
- if (/registered/.test(t) || /CONNECTED/.test(t)) writeLine("Named tunnel connected \u2713");
1114
- });
1115
- await new Promise((r) => setTimeout(r, 4e3));
1116
- const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: primary });
1117
- const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
1118
- for (const id of instanceIds) {
1119
- try {
1120
- await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
1121
- endpointUrl: tunnelUrl2,
1122
- token: runnerToken,
1123
- placement: "local",
1124
- capabilities: caps,
1125
- runnerVersion: clean(opts.runnerVersion) || "",
1126
- runnerNode: hostname()
1127
- });
1128
- } catch (error) {
1129
- writeError(`register ${id.slice(0, 8)}\u2026 failed (${error instanceof Error ? error.message : String(error)})`);
1130
- }
1131
- }
1132
- writeLine("Runtime registered with PAGS \u2713");
1133
- writeLine("");
1134
- writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
1135
- writeLine(" \u2705 CONNECTED \u2014 named tunnel (production)");
1136
- writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"}`);
1137
- writeLine(` Endpoint: ${tunnelUrl2}`);
1138
- writeLine(" Stable URL \u2014 no reconnect churn. Ctrl+C to disconnect.");
1139
- writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
1140
- let stopped2 = false;
1141
- void (async () => {
1142
- while (!stopped2) {
1143
- await new Promise((r) => setTimeout(r, 3e4));
1144
- if (stopped2) break;
1145
- for (const id of instanceIds) {
1146
- await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).catch(() => void 0);
1147
- }
1148
- }
1149
- })();
1150
- await new Promise((resolvePromise) => {
1151
- runner.on("exit", () => resolvePromise());
1152
- });
1153
- stopped2 = true;
1154
- return;
1155
- }
1156
- }
1157
- const cloudflaredPath = await resolveCloudflaredCommand(opts.cloudflared);
1158
- const openTunnel = () => new Promise((resolveTunnel, reject) => {
1159
- let output = "";
1160
- const proc = spawn2(cloudflaredPath, buildCloudflaredArgs(localUrl), {
1161
- stdio: ["ignore", "pipe", "pipe"],
1162
- shell: process.platform === "win32"
1163
- });
1164
- const onData = (data) => {
1165
- output += data.toString("utf-8");
1166
- const parsed = parseCloudflaredTunnelUrl(output);
1167
- if (parsed) resolveTunnel({ url: parsed, proc });
1168
- };
1169
- proc.stdout?.on("data", onData);
1170
- proc.stderr?.on("data", onData);
1171
- proc.on("error", reject);
1172
- setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
1173
- });
1174
- const registerRuntime = async (url) => {
1175
- const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId: primary });
1176
- const caps = Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [];
1177
- for (const id of instanceIds) {
1178
- try {
1179
- await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime`, opts, {
1180
- endpointUrl: url,
1181
- token: runnerToken,
1182
- placement: "local",
1183
- capabilities: caps,
1184
- runnerVersion: clean(opts.runnerVersion) || "",
1185
- runnerNode: hostname()
1186
- });
1187
- } catch (error) {
1188
- writeError(`register ${id.slice(0, 8)}\u2026 failed (${error instanceof Error ? error.message : String(error)}); will retry`);
1189
- }
1190
- }
1191
- };
1192
- const first = await openTunnel();
1193
- tunnel = first.proc;
1194
- let tunnelUrl = first.url;
1195
- await registerRuntime(tunnelUrl).catch((error) => {
1196
- writeError(`registration will retry (${error instanceof Error ? error.message : String(error)})`);
1197
- });
1198
- writeLine("Runtime registered with PAGS \u2713");
1199
- writeLine("");
1200
- writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
1201
- writeLine(" \u2705 CONNECTED \u2014 runner is live");
1202
- writeLine(` Agents: ${instanceIds.length} instance${instanceIds.length === 1 ? "" : "s"} served by this runner`);
1203
- writeLine(` Tunnel: ${tunnelUrl}`);
1204
- writeLine(" Auto-reconnect is on \u2014 keep this terminal open. Ctrl+C to disconnect.");
1205
- writeLine("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
1206
- let stopped = false;
1207
- let consecutiveFailures = 0;
1208
- let respawnBackoffMs = 3e4;
1209
- const PROBE_FAIL_THRESHOLD = 3;
1210
- const MAX_BACKOFF_MS = 5 * 6e4;
1211
- const probeTunnel = async (url) => {
1212
- try {
1213
- const res = await fetch(`${url.replace(/\/$/, "")}/health`, {
1214
- headers: { Authorization: `Bearer ${runnerToken}`, "X-PAGS-Instance-Id": primary },
1215
- signal: AbortSignal.timeout(1e4)
1216
- });
1217
- return res.ok;
1218
- } catch {
1219
- return false;
1220
- }
1221
- };
1222
- const heartbeatAll = async () => {
1223
- let allOk = true;
1224
- for (const id of instanceIds) {
1225
- const ok = await requestPags("POST", `/v1/instances/${apiPathSegment(id)}/runtime/heartbeat`, opts, {}).then(() => true).catch(() => false);
1226
- if (!ok) allOk = false;
1227
- }
1228
- return allOk;
1229
- };
1230
- void (async () => {
1231
- while (!stopped) {
1232
- await new Promise((r) => setTimeout(r, 3e4));
1233
- if (stopped) break;
1234
- if (await probeTunnel(tunnelUrl)) {
1235
- consecutiveFailures = 0;
1236
- respawnBackoffMs = 3e4;
1237
- const beat = await heartbeatAll();
1238
- if (!beat) {
1239
- await registerRuntime(tunnelUrl).then(() => writeLine("\u2705 Re-registered with PAGS")).catch(() => void 0);
1240
- }
1241
- continue;
1242
- }
1243
- consecutiveFailures++;
1244
- if (consecutiveFailures < PROBE_FAIL_THRESHOLD) {
1245
- writeLine(`\u26A0 Tunnel probe failed (${consecutiveFailures}/${PROBE_FAIL_THRESHOLD}) \u2014 will retry`);
1246
- continue;
1247
- }
1248
- writeLine("\u26A0 Tunnel unreachable \u2014 reconnecting\u2026");
1249
- consecutiveFailures = 0;
1250
- try {
1251
- if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
1252
- } catch {
1253
- }
1254
- try {
1255
- const next = await openTunnel();
1256
- tunnel = next.proc;
1257
- tunnelUrl = next.url;
1258
- await registerRuntime(tunnelUrl);
1259
- respawnBackoffMs = 3e4;
1260
- writeLine(`\u2705 Reconnected: ${tunnelUrl}`);
1261
- } catch (error) {
1262
- writeError(`reconnect failed (${error instanceof Error ? error.message : String(error)}); retrying in ${Math.round(respawnBackoffMs / 1e3)}s`);
1263
- await new Promise((r) => setTimeout(r, respawnBackoffMs));
1264
- respawnBackoffMs = Math.min(respawnBackoffMs * 2, MAX_BACKOFF_MS);
1265
- }
1266
- }
1267
- })();
961
+ await connectViaRelay(instanceIds, localUrl, runnerToken, opts, Boolean(opts.force));
1268
962
  await new Promise((resolvePromise) => {
1269
963
  runner.on("exit", () => resolvePromise());
1270
964
  });
1271
- stopped = true;
1272
965
  } catch (error) {
1273
966
  shutdown();
1274
967
  throw error;
@@ -1498,8 +1191,7 @@ async function stopRunnerProcesses() {
1498
1191
  const patterns = [
1499
1192
  "dist/browser-runner/index.js",
1500
1193
  "browser-runner/src/index",
1501
- "runner connect",
1502
- "tunnel --url http://127.0.0.1"
1194
+ "runner connect"
1503
1195
  ];
1504
1196
  let stopped = false;
1505
1197
  for (const p of patterns) {
@@ -1511,7 +1203,7 @@ async function stopRunnerProcesses() {
1511
1203
  }
1512
1204
  return stopped;
1513
1205
  }
1514
- var upCommand = new Command7("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--tunnel <mode>", "Tunnel mode: 'ws' (WebSocket relay, default), 'named' (stable hostname), or 'quick' (cloudflared)", "ws").option("--force", "Take over from another connected machine").action(async (opts) => {
1206
+ var upCommand = new Command7("up").description("Start the browser runner for all your agent instances").option("--headless", "Run browser in headless mode").option("--instance <id>", "Connect to a specific instance only").option("--force", "Take over from another connected machine").action(async (opts) => {
1515
1207
  const session = requireSession();
1516
1208
  const state = {
1517
1209
  user: session.user.login,
@@ -1558,8 +1250,6 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1558
1250
  const cliPath = process.argv[1];
1559
1251
  const args = [cliPath, "runner", "connect", ...instances.map((i) => i.id)];
1560
1252
  if (opts.headless) args.push("--headless");
1561
- if (opts.tunnel === "named") args.push("--tunnel", "named");
1562
- if (opts.tunnel === "ws") args.push("--tunnel", "ws");
1563
1253
  if (opts.force) args.push("--force");
1564
1254
  const child = spawn3(process.execPath, args, {
1565
1255
  stdio: ["ignore", "pipe", "pipe"],
@@ -1573,14 +1263,6 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1573
1263
  if (!trimmed) continue;
1574
1264
  logs.push(trimmed);
1575
1265
  if (logs.length > 200) logs.shift();
1576
- const tunnelMatch = trimmed.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
1577
- if (tunnelMatch && !/^https:\/\/api\.trycloudflare\.com/i.test(tunnelMatch[0])) {
1578
- state.tunnel = "online";
1579
- state.tunnelUrl = tunnelMatch[0];
1580
- state.lastEvent = "Tunnel created";
1581
- printStatus(state);
1582
- continue;
1583
- }
1584
1266
  if (trimmed.includes("Relay connected:")) {
1585
1267
  state.tunnel = "online";
1586
1268
  state.tunnelUrl = "WebSocket relay";
@@ -1588,7 +1270,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1588
1270
  printStatus(state);
1589
1271
  continue;
1590
1272
  }
1591
- if (trimmed.includes("WebSocket relay (no tunnel)")) {
1273
+ if (trimmed.includes("WebSocket relay")) {
1592
1274
  state.tunnel = "online";
1593
1275
  state.tunnelUrl = "WebSocket relay";
1594
1276
  state.registration = "registered";
@@ -1608,13 +1290,18 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1608
1290
  printStatus(state);
1609
1291
  continue;
1610
1292
  }
1293
+ if (trimmed.includes("Another machine")) {
1294
+ state.registration = "failed";
1295
+ state.lastEvent = trimmed.slice(0, 80);
1296
+ printStatus(state);
1297
+ continue;
1298
+ }
1611
1299
  if (trimmed.includes("fetch failed")) {
1612
1300
  state.registration = "failed";
1613
- state.lastEvent = "PAGS registration failed (will retry on next task)";
1301
+ state.lastEvent = "PAGS registration failed";
1614
1302
  printStatus(state);
1615
1303
  continue;
1616
1304
  }
1617
- if (trimmed.includes("INF ")) continue;
1618
1305
  if (/error|Error|EADDRINUSE|ECONNREFUSED|failed/i.test(trimmed)) {
1619
1306
  state.lastEvent = trimmed.slice(0, 80);
1620
1307
  printStatus(state);
@@ -1629,7 +1316,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1629
1316
  if (code && code !== 0) {
1630
1317
  state.runner = "error";
1631
1318
  state.lastEvent = `Runner exited (code ${code})`;
1632
- const recent = logs.slice(-5).filter((l) => !l.includes("INF "));
1319
+ const recent = logs.slice(-5);
1633
1320
  if (recent.length) state.lastEvent += ": " + recent[recent.length - 1].slice(0, 60);
1634
1321
  printStatus(state);
1635
1322
  }
@@ -1663,14 +1350,9 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1663
1350
  if (key === "r") {
1664
1351
  if (childDead) {
1665
1352
  writeLine(" Restarting runner...");
1666
- state.runner = "starting";
1667
- state.tunnel = "offline";
1668
- state.tunnelUrl = "";
1669
- state.registration = "pending";
1670
- state.lastEvent = "Restarting...";
1671
1353
  const { execSync } = await import("child_process");
1672
1354
  try {
1673
- execSync(`${process.execPath} ${process.argv[1]} up${opts.headless ? " --headless" : ""}`, {
1355
+ execSync(`${process.execPath} ${process.argv[1]} up${opts.headless ? " --headless" : ""}${opts.force ? " --force" : ""}`, {
1674
1356
  stdio: "inherit",
1675
1357
  env: process.env
1676
1358
  });
@@ -1682,7 +1364,7 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1682
1364
  }
1683
1365
  }
1684
1366
  });
1685
- var downCommand = new Command7("down").description("Stop the browser runner and disconnect (cleans up any stuck processes)").action(async () => {
1367
+ var downCommand = new Command7("down").description("Stop the browser runner and disconnect").action(async () => {
1686
1368
  clearScreen();
1687
1369
  printLogo(CLI_VERSION2);
1688
1370
  if (process.platform === "win32") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.2.11",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",