@proagentstore/cli 0.1.17 → 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.
- package/dist/browser-runner/browser-actions.js +177 -0
- package/dist/browser-runner/browser-profile.js +78 -0
- package/dist/browser-runner/challenge.js +86 -0
- package/dist/browser-runner/errors.js +13 -0
- package/dist/browser-runner/human-mouse.js +71 -0
- package/dist/browser-runner/runner.js +568 -422
- package/dist/browser-runner/server.js +56 -0
- package/dist/browser-runner/store.js +24 -0
- package/dist/browser-runner/test-job-server.js +29 -2
- package/dist/index.js +68 -37
- package/package.json +1 -1
|
@@ -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
|
|
937
|
+
const cloudflaredPath = await resolveCloudflaredCommand(opts.cloudflared);
|
|
938
|
+
const openTunnel = () => new Promise((resolveTunnel, reject) => {
|
|
938
939
|
let output = "";
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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("
|
|
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;
|