@proagentstore/cli 0.1.18 → 0.1.19

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.
@@ -84,6 +84,62 @@ async function route(runner, req, res) {
84
84
  const limit = clampLimit(url.searchParams.get("limit"), 100, 500);
85
85
  return json(res, 200, { events: runner.store.listEvents(limit) });
86
86
  }
87
+ // ── Brain-driven browser control (remote LLM acts on the live page) ─────
88
+ if (req.method === "POST" && path === "/browser/snapshot") {
89
+ const b = await readJson(req).catch(() => ({}));
90
+ return json(res, 200, await runner.browserSnapshot(b.taskId));
91
+ }
92
+ if (req.method === "POST" && path === "/browser/act") {
93
+ const body = await readJson(req);
94
+ return json(res, 200, await runner.browserAct(body, body.resumePath));
95
+ }
96
+ if (req.method === "POST" && path === "/browser/event") {
97
+ const b = await readJson(req);
98
+ return json(res, 200, runner.browserEvent(b.taskId, b.type, b.message, b.data));
99
+ }
100
+ if (req.method === "POST" && path === "/browser/handoff") {
101
+ const b = await readJson(req);
102
+ return json(res, 200, await runner.browserHandoff(b.taskId, b.label ?? b.challenge ?? "this step", b.reason ?? "challenge"));
103
+ }
104
+ if (req.method === "POST" && path === "/browser/handoff-status") {
105
+ const b = await readJson(req);
106
+ return json(res, 200, await runner.browserHandoffStatus(b.taskId));
107
+ }
108
+ if (req.method === "POST" && path === "/browser/resume") {
109
+ const b = await readJson(req);
110
+ return json(res, 200, await runner.browserResume(b.taskId));
111
+ }
112
+ if (req.method === "POST" && path === "/browser/input") {
113
+ const b = await readJson(req);
114
+ return json(res, 200, runner.browserSubmitInput(b.taskId, String(b.value ?? "")));
115
+ }
116
+ if (req.method === "POST" && path === "/browser/complete") {
117
+ const b = await readJson(req);
118
+ return json(res, 200, await runner.browserComplete(b.taskId, b.outcome, b.detail));
119
+ }
120
+ // ── Human takeover (remote view + control) ──────────────────────────────
121
+ if (req.method === "GET" && path === "/takeover") {
122
+ return json(res, 200, { takeovers: runner.listTakeovers() });
123
+ }
124
+ const frameMatch = path.match(/^\/takeover\/([^/]+)\/frame$/);
125
+ if (req.method === "GET" && frameMatch) {
126
+ return json(res, 200, await runner.takeoverFrame(frameMatch[1]));
127
+ }
128
+ const inputMatch = path.match(/^\/takeover\/([^/]+)\/input$/);
129
+ if (req.method === "POST" && inputMatch) {
130
+ const body = await readJson(req);
131
+ await runner.takeoverInput(inputMatch[1], body);
132
+ return json(res, 200, { ok: true });
133
+ }
134
+ const resumeMatch = path.match(/^\/takeover\/([^/]+)\/resume$/);
135
+ if (req.method === "POST" && resumeMatch) {
136
+ return json(res, 200, await runner.resumeTakeover(resumeMatch[1]));
137
+ }
138
+ const endMatch = path.match(/^\/takeover\/([^/]+)\/end$/);
139
+ if (req.method === "POST" && endMatch) {
140
+ await runner.endTakeover(endMatch[1]);
141
+ return json(res, 200, { ok: true });
142
+ }
87
143
  return json(res, 404, { error: "Not found" });
88
144
  }
89
145
  function authorize(req, config) {
@@ -33,6 +33,30 @@ export class RunnerStore {
33
33
  getTask(id) {
34
34
  return this.read().tasks.find((task) => task.id === id) || null;
35
35
  }
36
+ /**
37
+ * On process startup, any persisted task still in an in-flight state
38
+ * (needs_human / running) is orphaned — its live browser page and takeover
39
+ * session died with the previous process and can't be resumed. Fail them so
40
+ * they don't linger as stale "Needs you" cards. Returns the count expired.
41
+ */
42
+ expireInFlightTasks() {
43
+ const data = this.read();
44
+ const now = new Date().toISOString();
45
+ let expired = 0;
46
+ for (const task of data.tasks) {
47
+ if (task.status === "needs_human" || task.status === "running") {
48
+ task.status = "failed";
49
+ task.error =
50
+ "Runner restarted — this paused task was orphaned (its browser session is gone). Re-run it to try again.";
51
+ task.updatedAt = now;
52
+ task.completedAt = now;
53
+ expired += 1;
54
+ }
55
+ }
56
+ if (expired > 0)
57
+ this.write(data);
58
+ return expired;
59
+ }
36
60
  putTask(task) {
37
61
  const data = this.read();
38
62
  const index = data.tasks.findIndex((row) => row.id === task.id);
@@ -32,7 +32,12 @@ async function route(req, res, submissions) {
32
32
  return;
33
33
  }
34
34
  if (req.method === "GET" && url.pathname === "/jobs/software-engineer") {
35
- html(res, 200, jobPage());
35
+ html(res, 200, jobPage({
36
+ challenge: url.searchParams.get("challenge") === "1",
37
+ hcaptcha: url.searchParams.get("hcaptcha") === "1",
38
+ invisibleRecaptcha: url.searchParams.get("invisible_recaptcha") === "1",
39
+ recaptcha: url.searchParams.get("recaptcha") === "1",
40
+ }));
36
41
  return;
37
42
  }
38
43
  if (req.method === "POST" && url.pathname === "/apply") {
@@ -57,7 +62,28 @@ async function route(req, res, submissions) {
57
62
  }
58
63
  html(res, 404, "<h1>Not found</h1>");
59
64
  }
60
- function jobPage() {
65
+ function jobPage(opts = {}) {
66
+ let challenge = "";
67
+ if (opts.hcaptcha) {
68
+ // Real hCaptcha widget using hCaptcha's official always-passes TEST sitekey
69
+ // — lets a human actually solve a challenge end-to-end with zero real-world impact.
70
+ challenge =
71
+ '<div class="h-captcha" data-sitekey="10000000-ffff-ffff-ffff-000000000001"></div>\n <script src="https://js.hcaptcha.com/1/api.js" async defer></script>';
72
+ }
73
+ else if (opts.invisibleRecaptcha) {
74
+ // The invisible v3 badge — present on countless pages, needs NO human. Must
75
+ // NOT be treated as a challenge (the false positive that bailed to the user).
76
+ challenge =
77
+ '<div class="grecaptcha-badge"><iframe title="reCAPTCHA" src="https://example.test/recaptcha/api2/anchor?size=invisible&k=x" style="width:256px;height:60px;display:block"></iframe></div>';
78
+ }
79
+ else if (opts.recaptcha) {
80
+ // A VISIBLE "I'm not a robot" checkbox — a real human challenge.
81
+ challenge =
82
+ '<div class="g-recaptcha"><iframe title="reCAPTCHA" src="https://example.test/recaptcha/api2/anchor?k=x" style="width:304px;height:78px;display:block"></iframe></div>';
83
+ }
84
+ else if (opts.challenge) {
85
+ challenge = '<div class="cf-turnstile" data-sitekey="test">Verify you are human</div>';
86
+ }
61
87
  return page("Senior Software Engineer", `
62
88
  <main>
63
89
  <section class="job">
@@ -82,6 +108,7 @@ function jobPage() {
82
108
  </label>
83
109
  <label>Resume <input name="resume" type="file" accept=".pdf,.txt,.doc,.docx" required /></label>
84
110
  <label>Cover note <textarea name="coverNote" rows="5" required></textarea></label>
111
+ ${challenge}
85
112
  <button type="submit">Submit application</button>
86
113
  </form>
87
114
  </main>
package/dist/index.js CHANGED
@@ -628,7 +628,7 @@ function pagsApiBase(url) {
628
628
  return (clean(url) || clean(process.env.PAGS_API_BASE) || "https://api.proagentstore.online").replace(/\/$/, "");
629
629
  }
630
630
  function pagsHeaders(token) {
631
- const resolved = clean(token) || clean(process.env.PAGS_TOKEN);
631
+ const resolved = clean(token) || clean(process.env.PAGS_TOKEN) || clean(loadSession()?.token);
632
632
  return resolved ? { Authorization: `Bearer ${resolved}` } : {};
633
633
  }
634
634
  function runnerRequestHeaders(opts) {
@@ -934,55 +934,86 @@ function createRunnerCommand() {
934
934
  try {
935
935
  await waitForLocalRunner({ url: localUrl, token: runnerToken, instanceId });
936
936
  writeLine(`Local FAGS runtime healthy at ${localUrl}`);
937
- const tunnelUrl = await new Promise((resolveTunnel, reject) => {
937
+ const cloudflaredPath = await resolveCloudflaredCommand(opts.cloudflared);
938
+ const openTunnel = () => new Promise((resolveTunnel, reject) => {
938
939
  let output = "";
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);
940
+ const proc = spawn2(cloudflaredPath, buildCloudflaredArgs(localUrl), {
941
+ stdio: ["ignore", "pipe", "pipe"],
942
+ shell: process.platform === "win32"
943
+ });
944
+ const onData = (data) => {
945
+ output += data.toString("utf-8");
946
+ const parsed = parseCloudflaredTunnelUrl(output);
947
+ if (parsed) resolveTunnel({ url: parsed, proc });
948
+ };
949
+ proc.stdout?.on("data", onData);
950
+ proc.stderr?.on("data", onData);
951
+ proc.on("error", reject);
958
952
  setTimeout(() => reject(new Error("timed out waiting for cloudflared tunnel URL")), 45e3).unref();
959
953
  });
960
- writeLine(`Tunnel ready: ${tunnelUrl}`);
961
- const capabilities = await requestRunner("GET", "/capabilities", {
962
- url: localUrl,
963
- token: runnerToken,
964
- instanceId
965
- });
966
- await requestPags("POST", `/v1/instances/${apiPathSegment(instanceId)}/runtime`, opts, {
967
- endpointUrl: tunnelUrl,
968
- token: runnerToken,
969
- placement: "local",
970
- capabilities: Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [],
971
- runnerVersion: clean(opts.runnerVersion) || ""
972
- });
954
+ const registerRuntime = async (url) => {
955
+ const capabilities = await requestRunner("GET", "/capabilities", { url: localUrl, token: runnerToken, instanceId });
956
+ await requestPags("POST", `/v1/instances/${apiPathSegment(instanceId)}/runtime`, opts, {
957
+ endpointUrl: url,
958
+ token: runnerToken,
959
+ placement: "local",
960
+ capabilities: Array.isArray(capabilities.capabilities) ? capabilities.capabilities.filter((item) => typeof item === "string") : [],
961
+ runnerVersion: clean(opts.runnerVersion) || ""
962
+ });
963
+ };
964
+ const first = await openTunnel();
965
+ tunnel = first.proc;
966
+ let tunnelUrl = first.url;
967
+ await registerRuntime(tunnelUrl);
973
968
  writeLine("Runtime registered with PAGS \u2713");
974
969
  writeLine("");
975
970
  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");
976
971
  writeLine(" \u2705 CONNECTED \u2014 Browser runner is live");
977
972
  writeLine(` Instance: ${instanceId.slice(0, 8)}...`);
978
973
  writeLine(` Tunnel: ${tunnelUrl}`);
979
- writeLine(" The agent can now submit applications via browser.");
980
- writeLine(" Keep this terminal open. Press Ctrl+C to disconnect.");
974
+ writeLine(" Auto-reconnect is on \u2014 keep this terminal open. Ctrl+C to disconnect.");
981
975
  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");
976
+ let stopped = false;
977
+ const heartbeatPath = `/v1/instances/${apiPathSegment(instanceId)}/runtime/heartbeat`;
978
+ const probeTunnel = async (url) => {
979
+ try {
980
+ const res = await fetch(`${url.replace(/\/$/, "")}/health`, {
981
+ headers: { Authorization: `Bearer ${runnerToken}`, "X-PAGS-Instance-Id": instanceId },
982
+ signal: AbortSignal.timeout(8e3)
983
+ });
984
+ return res.ok;
985
+ } catch {
986
+ return false;
987
+ }
988
+ };
989
+ void (async () => {
990
+ while (!stopped) {
991
+ await new Promise((r) => setTimeout(r, 3e4));
992
+ if (stopped) break;
993
+ if (await probeTunnel(tunnelUrl)) {
994
+ await requestPags("POST", heartbeatPath, opts, {}).catch(() => void 0);
995
+ continue;
996
+ }
997
+ writeLine("\u26A0 Tunnel unreachable \u2014 reconnecting\u2026");
998
+ try {
999
+ if (tunnel && !tunnel.killed) tunnel.kill("SIGTERM");
1000
+ } catch {
1001
+ }
1002
+ try {
1003
+ const next = await openTunnel();
1004
+ tunnel = next.proc;
1005
+ tunnelUrl = next.url;
1006
+ await registerRuntime(tunnelUrl);
1007
+ writeLine(`\u2705 Reconnected: ${tunnelUrl}`);
1008
+ } catch (error) {
1009
+ writeError(`reconnect failed (${error instanceof Error ? error.message : String(error)}); retrying in 30s`);
1010
+ }
1011
+ }
1012
+ })();
982
1013
  await new Promise((resolvePromise) => {
983
1014
  runner.on("exit", () => resolvePromise());
984
- tunnel?.on("exit", () => resolvePromise());
985
1015
  });
1016
+ stopped = true;
986
1017
  } catch (error) {
987
1018
  shutdown();
988
1019
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",