@proagentstore/cli 0.4.144 → 0.4.146
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskedPageScreenshot } from "./local-browser/replay.js";
|
|
1
2
|
/**
|
|
2
3
|
* Detect an anti-bot human challenge on the page (reCAPTCHA, hCaptcha,
|
|
3
4
|
* Cloudflare Turnstile, generic captcha). These can't be solved by the model,
|
|
@@ -74,13 +75,13 @@ export async function challengeSolved(page) {
|
|
|
74
75
|
})
|
|
75
76
|
.catch(() => false);
|
|
76
77
|
}
|
|
77
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Capture a downscaled JPEG screenshot as a data URL for the human-takeover UI — the STILL stored with a
|
|
80
|
+
* handoff. Form controls are blurred first and no still is kept when that cannot be confirmed (#1067): a
|
|
81
|
+
* handoff can be raised mid-form, and the person acts through the live takeover view (which is not
|
|
82
|
+
* blurred — it is the control surface), never through this preview.
|
|
83
|
+
*/
|
|
78
84
|
export async function captureScreenshotDataUrl(page) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return `data:image/jpeg;base64,${buf.toString("base64")}`;
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
85
|
+
const shot = await maskedPageScreenshot(page, { quality: 55, timeout: 8_000 });
|
|
86
|
+
return shot ? `data:image/jpeg;base64,${shot}` : undefined;
|
|
86
87
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { globMatches, isGlob } from "./path-glob.js";
|
|
1
2
|
import { execFileSync } from "node:child_process";
|
|
2
3
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
4
|
import { relative, resolve, sep } from "node:path";
|
|
@@ -331,9 +332,12 @@ export function repoSearch(workDir, opts) {
|
|
|
331
332
|
const lines = out.split("\n").filter((l) => l !== "");
|
|
332
333
|
if (mode === "path") {
|
|
333
334
|
// The pattern never reaches git here — the filter is a plain case-insensitive substring
|
|
334
|
-
// over the path, which is what "find the file called X" actually means.
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
// over the path, which is what "find the file called X" actually means. A `glob` (#1070) is
|
|
336
|
+
// matched instead, over the WHOLE list, so `total` counts real matches, not the literal's.
|
|
337
|
+
const glob = (opts.glob ?? "").trim();
|
|
338
|
+
const useGlob = glob !== "" && isGlob(glob);
|
|
339
|
+
const all = lines.filter((p) => (useGlob ? globMatches(glob, p) : p.toLowerCase().includes(pattern.toLowerCase()))).map((p) => ({ path: p }));
|
|
340
|
+
return { mode, pattern, matches: all.slice(0, limit), shown: Math.min(all.length, limit), total: all.length, truncated: all.length > limit, ...(useGlob ? { glob: true } : {}) };
|
|
337
341
|
}
|
|
338
342
|
const all = lines.map(parseGrepLine).filter((m) => m !== null);
|
|
339
343
|
const perFile = new Map();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob patterns over repo-relative paths, for `repo_find` (#1070).
|
|
3
|
+
*
|
|
4
|
+
* `repo_find` matched its pattern as a plain substring, so `*.test.js` returned NOTHING — no path
|
|
5
|
+
* contains an asterisk — and an agent concluded a repository with ~90 test files had none. Silent false
|
|
6
|
+
* negatives are the failure; this is the matcher that ends them.
|
|
7
|
+
*
|
|
8
|
+
* Semantics (gitignore/ripgrep-like, case-insensitive like the substring search it extends):
|
|
9
|
+
* `*` any run of characters within ONE path segment (never a `/`)
|
|
10
|
+
* `**` any run across segments; `**` followed by `/` also matches zero folders
|
|
11
|
+
* `?` one character, not `/`
|
|
12
|
+
* `[ab]` `[a-z]` `[!ab]` a character class (`!` negates), never `/`
|
|
13
|
+
* A pattern WITHOUT a `/` matches any single segment — a file name or a folder name at any depth, a
|
|
14
|
+
* folder matching taking everything beneath it — so `*.test.js` and `*e2e*` mean what they mean in a
|
|
15
|
+
* shell or `rg -g`. A pattern WITH a `/` is anchored at the repository root (`src/*.ts`,
|
|
16
|
+
* `**\/handlers/*.js`); a leading `/` or `./` is accepted and means the same.
|
|
17
|
+
*
|
|
18
|
+
* DUPLICATED in `workers/api/src/lib/path-glob.ts`: the cloud filters with it when the runner is too old
|
|
19
|
+
* to (an old runner ignores `glob`). `workers/api/src/lib/path-glob.parity.test.ts` holds the two equal.
|
|
20
|
+
*/
|
|
21
|
+
/** Does this pattern use glob syntax? A pattern without any is the plain substring search, unchanged. */
|
|
22
|
+
export function isGlob(pattern) {
|
|
23
|
+
return /[*?[]/.test(pattern);
|
|
24
|
+
}
|
|
25
|
+
function toRegExp(glob) {
|
|
26
|
+
let re = "";
|
|
27
|
+
for (let i = 0; i < glob.length; i++) {
|
|
28
|
+
const c = glob[i];
|
|
29
|
+
if (c === "*") {
|
|
30
|
+
if (glob[i + 1] === "*") {
|
|
31
|
+
i++;
|
|
32
|
+
if (glob[i + 1] === "/") {
|
|
33
|
+
i++;
|
|
34
|
+
re += "(?:.*/)?";
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
re += ".*";
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
re += "[^/]*";
|
|
41
|
+
}
|
|
42
|
+
else if (c === "?")
|
|
43
|
+
re += "[^/]";
|
|
44
|
+
else if (c === "[") {
|
|
45
|
+
const end = glob.indexOf("]", i + 2);
|
|
46
|
+
if (end === -1) {
|
|
47
|
+
re += "\\[";
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let body = glob.slice(i + 1, end).replace(/[\\^\]]/g, "\\$&");
|
|
51
|
+
const negate = body.startsWith("!");
|
|
52
|
+
if (negate)
|
|
53
|
+
body = body.slice(1);
|
|
54
|
+
re += `(?!/)[${negate ? "^" : ""}${body}]`;
|
|
55
|
+
i = end;
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
re += c.replace(/[.+^${}()|\\\]/]/g, "\\$&");
|
|
59
|
+
}
|
|
60
|
+
return new RegExp(`^${re}$`, "i");
|
|
61
|
+
}
|
|
62
|
+
/** Does `path` (repo-relative, `/`-separated) match `glob`? */
|
|
63
|
+
export function globMatches(glob, path) {
|
|
64
|
+
const g = glob.trim().replace(/^\.?\/+/, "").replace(/\/+$/, "");
|
|
65
|
+
if (!g)
|
|
66
|
+
return false;
|
|
67
|
+
const re = toRegExp(g);
|
|
68
|
+
if (g.includes("/"))
|
|
69
|
+
return re.test(path);
|
|
70
|
+
return path.split("/").some((segment) => re.test(segment));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The longest run of literal text a match MUST contain — what an old runner's substring search is given,
|
|
74
|
+
* so the glob filter then runs over a superset that cannot have dropped a match. `""` when there is none
|
|
75
|
+
* (`*`, `**`, `???`): nothing literal to search by.
|
|
76
|
+
*/
|
|
77
|
+
export function globLiteral(glob) {
|
|
78
|
+
// Normalised exactly as `globMatches` normalises, or `./src/*.ts` would ask for "./src", which no path contains.
|
|
79
|
+
const pieces = glob.trim().replace(/^\.?\/+/, "").split(/\[[^\]]*\]|[*?]+/);
|
|
80
|
+
const longest = pieces.reduce((a, b) => (b.length > a.length ? b : a), "");
|
|
81
|
+
return longest.replace(/^\/+|\/+$/g, "");
|
|
82
|
+
}
|
|
@@ -19,6 +19,29 @@ export const MASK_FORM_CONTROLS = `() => {
|
|
|
19
19
|
return document.getElementById(${JSON.stringify(MASK_ID)}) ? "masked" : "unmasked";
|
|
20
20
|
}`;
|
|
21
21
|
export const UNMASK_FORM_CONTROLS = `() => { const s = document.getElementById(${JSON.stringify(MASK_ID)}); if (s) s.remove(); return "unmasked"; }`;
|
|
22
|
+
/**
|
|
23
|
+
* The same form-control policy for a Playwright `Page` (#1067) — the legacy workflow browser tasks
|
|
24
|
+
* (`runtime_task`: JOB_APPLY / BROWSER_TASK), whose per-action frames were taken with nothing blurred,
|
|
25
|
+
* so a step that had just typed a phone number stored it in plain pixels. Blur, capture, unblur — and
|
|
26
|
+
* NO frame (undefined) when the blur cannot be confirmed, exactly as {@link ShotRecorder.capture}. The
|
|
27
|
+
* unblur runs even when the capture throws, so a failed frame never leaves the page blurred.
|
|
28
|
+
*/
|
|
29
|
+
export async function maskedPageScreenshot(page, opts) {
|
|
30
|
+
try {
|
|
31
|
+
if ((await page.evaluate(`(${MASK_FORM_CONTROLS})()`)) !== "masked")
|
|
32
|
+
return undefined;
|
|
33
|
+
try {
|
|
34
|
+
const buf = await page.screenshot({ type: "jpeg", quality: opts.quality, scale: "css", timeout: opts.timeout });
|
|
35
|
+
return buf.toString("base64");
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await page.evaluate(`(${UNMASK_FORM_CONTROLS})()`).catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
const textOf = (r) => (r.content ?? []).map((c) => c.text ?? "").join("\n");
|
|
23
46
|
export class ShotRecorder {
|
|
24
47
|
cap;
|
|
@@ -16,6 +16,7 @@ import { RunnerStore } from "./store.js";
|
|
|
16
16
|
import { CodingRuntime } from "./coding/runtime.js";
|
|
17
17
|
import { LocalBrowserRuntime } from "./local-browser/runtime.js";
|
|
18
18
|
import { LOCAL_BROWSER_TASK_TYPE } from "./local-browser/contract.js";
|
|
19
|
+
import { maskedPageScreenshot } from "./local-browser/replay.js";
|
|
19
20
|
import { LocalArtifactRuntime } from "./local-artifact/runtime.js";
|
|
20
21
|
import { LOCAL_ARTIFACT_TASK_TYPE } from "./local-artifact/contract.js";
|
|
21
22
|
import { LocalApplyRuntime } from "./local-apply/runtime.js";
|
|
@@ -908,17 +909,13 @@ export class LocalRunner {
|
|
|
908
909
|
return head.slice(0, 400);
|
|
909
910
|
}
|
|
910
911
|
/** A small JPEG screenshot (base64, no data: prefix) of the page — one per action,
|
|
911
|
-
* so the cloud can store it per step and the whole run can be replayed visually.
|
|
912
|
+
* so the cloud can store it per step and the whole run can be replayed visually.
|
|
913
|
+
* Form controls are BLURRED first, and no frame is kept when that cannot be confirmed (#1067) —
|
|
914
|
+
* the Scout/Runner replay policy (#1063), which this legacy path used to skip.
|
|
915
|
+
* scale:"css" avoids 2× retina bloat; a short timeout means a stuck page never delays the
|
|
916
|
+
* action (the screenshot is best-effort, not the point). */
|
|
912
917
|
async shot(page) {
|
|
913
|
-
|
|
914
|
-
// scale:"css" avoids 2× retina bloat; a short timeout means a stuck page
|
|
915
|
-
// never delays the action (the screenshot is best-effort, not the point).
|
|
916
|
-
const buf = await page.screenshot({ type: "jpeg", quality: 45, scale: "css", timeout: 8_000 });
|
|
917
|
-
return buf.toString("base64");
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
return undefined;
|
|
921
|
-
}
|
|
918
|
+
return maskedPageScreenshot(page, { quality: 45, timeout: 8_000 });
|
|
922
919
|
}
|
|
923
920
|
/**
|
|
924
921
|
* After a WRITE action, read the field's REAL value back via the standard
|